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

Compare commits

...

202 Commits

Author SHA1 Message Date
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
88 changed files with 4617 additions and 2714 deletions

7
.codecov.yml Normal file
View File

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

4
.gitignore vendored
View File

@@ -6,8 +6,12 @@ build/
dist/ dist/
*.egg/ *.egg/
.coverage .coverage
coverage.xml
htmlcov/
*.egg-info/ *.egg-info/
pytestdebug.log pytestdebug.log
pip-wheel-metadata/
.python-version
fixtures/ fixtures/
/docs/_build /docs/_build

View File

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

View File

@@ -1,10 +1,16 @@
|PyPI| |Python versions| |Build Status| |Waffle Ready| |Gitter|
VCR.py ###########
====== VCR.py 📼
###########
|PyPI| |Python versions| |Build Status| |CodeCov| |Gitter| |CodeStyleBlack|
----
.. image:: https://vcrpy.readthedocs.io/en/latest/_images/vcr.svg
:alt: vcr.py logo
.. image:: https://raw.github.com/kevin1024/vcrpy/master/vcr.png
:alt: vcr.py
This is a Python version of `Ruby's VCR This is a Python version of `Ruby's VCR
library <https://github.com/vcr/vcr>`__. library <https://github.com/vcr/vcr>`__.
@@ -41,33 +47,29 @@ VCR.py will detect the absence of a cassette file and once again record
all HTTP interactions, which will update them to correspond to the new all HTTP interactions, which will update them to correspond to the new
API. API.
Support Usage with Pytest
------- -----------------
VCR.py works great with the following HTTP clients:
- requests
- aiohttp
- urllib3
- tornado
- urllib2
- boto3
There is a library to provide some pytest fixtures called pytest-recording https://github.com/kiwicom/pytest-recording
License License
======= -------
This library uses the MIT license. See `LICENSE.txt <LICENSE.txt>`__ for This library uses the MIT license. See `LICENSE.txt <LICENSE.txt>`__ for
more details more details
.. |PyPI| image:: https://img.shields.io/pypi/v/vcrpy.svg .. |PyPI| image:: https://img.shields.io/pypi/v/vcrpy.svg
:target: https://pypi.python.org/pypi/vcrpy-unittest :target: https://pypi.python.org/pypi/vcrpy
.. |Python versions| image:: https://img.shields.io/pypi/pyversions/vcrpy-unittest.svg .. |Python versions| image:: https://img.shields.io/pypi/pyversions/vcrpy.svg
:target: https://pypi.python.org/pypi/vcrpy-unittest :target: https://pypi.python.org/pypi/vcrpy
.. |Build Status| image:: https://secure.travis-ci.org/kevin1024/vcrpy.png?branch=master .. |Build Status| image:: https://secure.travis-ci.org/kevin1024/vcrpy.svg?branch=master
:target: http://travis-ci.org/kevin1024/vcrpy :target: http://travis-ci.org/kevin1024/vcrpy
.. |Waffle Ready| image:: https://badge.waffle.io/kevin1024/vcrpy.png?label=ready&title=waffle
:target: https://waffle.io/kevin1024/vcrpy
.. |Gitter| image:: https://badges.gitter.im/Join%20Chat.svg .. |Gitter| image:: https://badges.gitter.im/Join%20Chat.svg
:alt: Join the chat at https://gitter.im/kevin1024/vcrpy :alt: Join the chat at https://gitter.im/kevin1024/vcrpy
:target: https://gitter.im/kevin1024/vcrpy?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge :target: https://gitter.im/kevin1024/vcrpy?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
.. |CodeCov| image:: https://codecov.io/gh/kevin1024/vcrpy/branch/master/graph/badge.svg
:target: https://codecov.io/gh/kevin1024/vcrpy
:alt: Code Coverage Status
.. |CodeStyleBlack| image:: https://img.shields.io/badge/code%20style-black-000000.svg
:target: https://github.com/psf/black
:alt: Code Style: black

BIN
docs/_static/vcr.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

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

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

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

@@ -33,6 +33,8 @@ consider part of the API. The fields are as follows:
been played back. been played back.
- ``responses_of(request)``: Access the responses that match a given - ``responses_of(request)``: Access the responses that match a given
request request
- ``allow_playback_repeats``: A boolean indicating whether responses
can be played back more than once.
The ``Request`` object has the following properties: The ``Request`` object has the following properties:
@@ -97,8 +99,12 @@ Create your own method with the following signature
def my_matcher(r1, r2): def my_matcher(r1, r2):
Your method receives the two requests and must return ``True`` if they Your method receives the two requests and can either:
match, ``False`` if they don't.
- 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. Finally, register your method with VCR to use your new request matcher.
@@ -107,7 +113,8 @@ Finally, register your method with VCR to use your new request matcher.
import vcr import vcr
def jurassic_matcher(r1, r2): def jurassic_matcher(r1, r2):
return r1.uri == r2.uri and 'JURASSIC PARK' in r1.body 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 = vcr.VCR()
my_vcr.register_matcher('jurassic', jurassic_matcher) my_vcr.register_matcher('jurassic', jurassic_matcher)
@@ -125,11 +132,11 @@ Finally, register your method with VCR to use your new request matcher.
Register your own cassette persister Register your own cassette persister
------------------------------------ ------------------------------------
Create your own persistence class, see the :ref:`persister_example`. Create your own persistence class, see the example below:
Your custom persister must implement both ``load_cassette`` and ``save_cassette`` Your custom persister must implement both ``load_cassette`` and ``save_cassette``
methods. The ``load_cassette`` method must return a deserialized cassette or raise methods. The ``load_cassette`` method must return a deserialized cassette or raise
``ValueError`` if no cassette is found. ``ValueError`` if no cassette is found.
Once the persister class is defined, register with VCR like so... Once the persister class is defined, register with VCR like so...
@@ -138,7 +145,7 @@ Once the persister class is defined, register with VCR like so...
import vcr import vcr
my_vcr = vcr.VCR() my_vcr = vcr.VCR()
class CustomerPersister(object): class CustomerPersister:
# implement Persister methods... # implement Persister methods...
my_vcr.register_persister(CustomPersister) my_vcr.register_persister(CustomPersister)
@@ -221,24 +228,25 @@ Custom Request filtering
~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~
If none of these covers your request filtering needs, you can register a If none of these covers your request filtering needs, you can register a
callback that will manipulate the HTTP request before adding it to the callback with the ``before_record_request`` configuration option to
cassette. Use the ``before_record_request`` configuration option to so this. manipulate the HTTP request before adding it to the cassette, or return
Here is an example that will never record requests to the /login ``None`` to ignore it entirely. Here is an example that will never record
endpoint. requests to the ``'/login'`` path:
.. code:: python .. code:: python
def before_record_cb(request): def before_record_cb(request):
if request.path != '/login': if request.path == '/login':
return request return None
return request
my_vcr = vcr.VCR( my_vcr = vcr.VCR(
before_record_request = before_record_cb, before_record_request=before_record_cb,
) )
with my_vcr.use_cassette('test.yml'): with my_vcr.use_cassette('test.yml'):
# your http code here # your http code here
You can also mutate the response using this callback. For example, you You can also mutate the request using this callback. For example, you
could remove all query parameters from any requests to the ``'/login'`` could remove all query parameters from any requests to the ``'/login'``
path. path.
@@ -246,7 +254,7 @@ path.
def scrub_login_request(request): def scrub_login_request(request):
if request.path == '/login': if request.path == '/login':
request.uri, _ = urllib.splitquery(response.uri) request.uri, _ = urllib.splitquery(request.uri)
return request return request
my_vcr = vcr.VCR( my_vcr = vcr.VCR(
@@ -258,9 +266,12 @@ path.
Custom Response Filtering Custom Response Filtering
~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~
VCR.py also suports response filtering with the You can also do response filtering with the
``before_record_response`` keyword argument. It's usage is similar to ``before_record_response`` configuration option. Its usage is
that of ``before_record``: similar to the above ``before_record_request`` - you can
mutate the response, or return ``None`` to avoid recording
the request and response altogether. For example to hide
sensitive data from the request body:
.. code:: python .. code:: python
@@ -302,8 +313,8 @@ in a few ways:
or 0.0.0.0. or 0.0.0.0.
- Set the ``ignore_hosts`` configuration option to a list of hosts to - Set the ``ignore_hosts`` configuration option to a list of hosts to
ignore ignore
- Add a ``before_record`` callback that returns None for requests you - Add a ``before_record_request`` or ``before_record_response`` callback
want to ignore 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 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 played back from a cassette. VCR will completely ignore those requests
@@ -364,3 +375,32 @@ cassette names, use ``VCR.ensure_suffix`` as follows:
@my_vcr.use_cassette @my_vcr.use_cassette
def my_test_function(): 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

View File

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

View File

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

View File

@@ -1,20 +1,106 @@
Contributing 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.
- ``python 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 Running VCR's test suite
------------------------ ------------------------
The tests are all run automatically on `Travis The tests are all run automatically on `Travis
CI <https://travis-ci.org/kevin1024/vcrpy>`__, but you can also run them CI <https://travis-ci.org/kevin1024/vcrpy>`__, but you can also run them
yourself using `py.test <http://pytest.org/>`__ and yourself using `pytest <http://pytest.org/>`__ and
`Tox <http://tox.testrun.org/>`__. Tox will automatically run them in `Tox <http://tox.testrun.org/>`__.
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'" Tox will automatically run them in all environments VCR.py supports if they are available on your `PATH`. Alternatively you can use `tox-pyenv <https://pypi.org/project/tox-pyenv/>`_ with
`pyenv <https://github.com/pyenv/pyenv>`_.
We recommend you read the documentation for each and see the section further below.
The test suite is pretty big and slow, but you can tell tox to only run specific tests like this::
tox -e {pyNN}-{HTTP_LIBRARY} -- <pytest flags passed through>
tox -e py36-requests -- -v -k "'test_status_code or test_gzip'"
tox -e py37-requests -- -v --last-failed
This will run only tests that look like ``test_status_code`` or This will run only tests that look like ``test_status_code`` or
``test_gzip`` in the test suite, and only in the python 2.7 environment ``test_gzip`` in the test suite, and only in the python 3.6 environment
that has ``requests`` installed. that has ``requests`` installed.
Also, in order for the boto tests to run, you will need an AWS key. Also, in order for the boto tests to run, you will need an AWS key.
@@ -23,3 +109,56 @@ documentation <https://boto.readthedocs.io/en/latest/getting_started.html>`__
for how to set this up. I have marked the boto tests as optional in 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 Travis so you don't have to worry about them failing if you submit a
pull request. 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>`_
but we are also going to use `tox-pyenv <https://pypi.org/project/tox-pyenv/>`_
in this example::
git clone https://github.com/pyenv/pyenv ~/.pyenv
# Add ~/.pyenv/bin to your PATH
export PATH="$PATH:~/.pyenv/bin"
# Setup shim paths
eval "$(pyenv init -)"
# Setup your local system tox tooling
pip install tox tox-pyenv
# Install supported versions (at time of writing), this does not activate them
pyenv install 3.5.9 3.6.9 3.7.5 3.8.0 pypy3.6-7.2.0
# This activates them
pyenv local 3.5.9 3.6.9 3.7.5 3.8.0 pypy3.6-7.2.0
# Run the whole test suite
tox
# Run the whole test suite or just part of it
tox -e lint
tox -e py37-requests
Troubleshooting on MacOSX
-------------------------
If you have this kind of error when running tox :
.. 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>`__

View File

@@ -29,3 +29,29 @@ The second time, you will see::
If you set the loglevel to DEBUG, you will also get information about 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 which matchers didn't match. This can help you with debugging custom
matchers. 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')]

View File

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

View File

@@ -9,18 +9,20 @@ with pip::
Compatibility Compatibility
------------- -------------
VCR.py supports Python 2.7 and 3.4+, and VCR.py supports Python 3.5+, and `pypy <http://pypy.org>`__.
`pypy <http://pypy.org>`__.
The following http libraries are supported: The following HTTP libraries are supported:
- urllib2 - ``aiohttp``
- urllib3 - ``boto``
- http.client (python3) - ``boto3``
- requests (both 1.x and 2.x versions) - ``http.client``
- httplib2 - ``httplib2``
- boto - ``requests`` (both 1.x and 2.x versions)
- Tornado's AsyncHTTPClient - ``tornado.httpclient``
- ``urllib2``
- ``urllib3``
- ``httpx``
Speed Speed
----- -----

View File

@@ -11,7 +11,7 @@ Usage
assert 'Example domains' in response assert 'Example domains' in response
Run this test once, and VCR.py will record the HTTP request to Run this test once, and VCR.py will record the HTTP request to
``fixtures/vcr_cassettes/synopsis.yml``. Run it again, and VCR.py will ``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 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 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 (the test will continue to pass, even if you are offline, or iana.org
@@ -95,3 +95,12 @@ Unittest Integration
While it's possible to use the context manager or decorator forms with unittest, While it's possible to use the context manager or decorator forms with unittest,
there's also a ``VCRTestCase`` provided separately by `vcrpy-unittest there's also a ``VCRTestCase`` provided separately by `vcrpy-unittest
<https://github.com/agriffis/vcrpy-unittest>`__. <https://github.com/agriffis/vcrpy-unittest>`__.
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>`__.

2
pyproject.toml Normal file
View File

@@ -0,0 +1,2 @@
[tool.black]
line-length=110

View File

@@ -1,3 +1,7 @@
#!/bin/bash #!/bin/bash
# https://blog.ionelmc.ro/2015/04/14/tox-tricks-and-patterns/#when-it-inevitably-leads-to-shell-scripts
# If you are getting an INVOCATION ERROR for this script then there is
# a good chance you are running on Windows.
# You can and should use WSL for running tox on Windows when it calls bash scripts.
REQUESTS_CA_BUNDLE=`python -m pytest_httpbin.certs` py.test $* REQUESTS_CA_BUNDLE=`python -m pytest_httpbin.certs` py.test $*

View File

@@ -1,15 +1,34 @@
#!/usr/bin/env python #!/usr/bin/env python
import codecs
import os
import re
import sys import sys
from setuptools import setup, find_packages from setuptools import setup, find_packages
from setuptools.command.test import test as TestCommand from setuptools.command.test import test as TestCommand
long_description = open('README.rst', 'r').read() long_description = open("README.rst", "r").read()
here = os.path.abspath(os.path.dirname(__file__))
def read(*parts):
# intentionally *not* adding an encoding option to open, See:
# https://github.com/pypa/virtualenv/issues/201#issuecomment-3145690
with codecs.open(os.path.join(here, *parts), "r") as fp:
return fp.read()
def find_version(*file_paths):
version_file = read(*file_paths)
version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", version_file, re.M)
if version_match:
return version_match.group(1)
raise RuntimeError("Unable to find version string.")
class PyTest(TestCommand): class PyTest(TestCommand):
def finalize_options(self): def finalize_options(self):
TestCommand.finalize_options(self) TestCommand.finalize_options(self)
self.test_args = [] self.test_args = []
@@ -18,54 +37,48 @@ class PyTest(TestCommand):
def run_tests(self): def run_tests(self):
# import here, cause outside the eggs aren't loaded # import here, cause outside the eggs aren't loaded
import pytest import pytest
errno = pytest.main(self.test_args) errno = pytest.main(self.test_args)
sys.exit(errno) sys.exit(errno)
install_requires = [ install_requires = [
'PyYAML', "PyYAML",
'wrapt', "wrapt",
'six>=1.5', "six>=1.5",
'contextlib2; python_version=="2.7"', 'yarl; python_version>="3.6"',
'mock; python_version=="2.7"', 'yarl<1.4; python_version=="3.5"',
'yarl; python_version>="3.4"',
] ]
excluded_packages = ["tests*"]
if sys.version_info[0] == 2:
excluded_packages.append("vcr.stubs.aiohttp_stubs")
setup( setup(
name='vcrpy', name="vcrpy",
version='1.12.0', version=find_version("vcr", "__init__.py"),
description=( description=("Automatically mock your HTTP interactions to simplify and speed up testing"),
"Automatically mock your HTTP interactions to simplify and "
"speed up testing"
),
long_description=long_description, long_description=long_description,
author='Kevin McCarthy', long_description_content_type="text/x-rst",
author_email='me@kevinmccarthy.org', author="Kevin McCarthy",
url='https://github.com/kevin1024/vcrpy', author_email="me@kevinmccarthy.org",
packages=find_packages(exclude=excluded_packages), url="https://github.com/kevin1024/vcrpy",
python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*', packages=find_packages(exclude=["tests*"]),
python_requires=">=3.5",
install_requires=install_requires, install_requires=install_requires,
license='MIT', license="MIT",
tests_require=['pytest', 'mock', 'pytest-httpbin'], tests_require=["pytest", "mock", "pytest-httpbin"],
classifiers=[ classifiers=[
'Development Status :: 4 - Beta', "Development Status :: 5 - Production/Stable",
'Environment :: Console', "Environment :: Console",
'Intended Audience :: Developers', "Intended Audience :: Developers",
'Programming Language :: Python', "Programming Language :: Python",
'Programming Language :: Python :: 2', "Programming Language :: Python :: 3",
'Programming Language :: Python :: 2.7', "Programming Language :: Python :: 3.5",
'Programming Language :: Python :: 3', "Programming Language :: Python :: 3.6",
'Programming Language :: Python :: 3.4', "Programming Language :: Python :: 3.7",
'Programming Language :: Python :: 3.5', "Programming Language :: Python :: 3.8",
'Programming Language :: Python :: 3.6', "Programming Language :: Python :: 3 :: Only",
'Programming Language :: Python :: Implementation :: CPython', "Programming Language :: Python :: Implementation :: CPython",
'Programming Language :: Python :: Implementation :: PyPy', "Programming Language :: Python :: Implementation :: PyPy",
'Topic :: Software Development :: Testing', "Topic :: Software Development :: Testing",
'Topic :: Internet :: WWW/HTTP', "Topic :: Internet :: WWW/HTTP",
'License :: OSI Approved :: MIT License', "License :: OSI Approved :: MIT License",
] ],
) )

View File

@@ -13,7 +13,7 @@ def assert_cassette_has_one_response(cass):
def assert_is_json(a_string): def assert_is_json(a_string):
try: try:
json.loads(a_string.decode('utf-8')) json.loads(a_string.decode("utf-8"))
except Exception: except Exception:
assert False assert False
assert True assert True

View File

@@ -9,7 +9,7 @@ interactions:
method: GET method: GET
uri: http://httpbin.org/ip uri: http://httpbin.org/ip
response: response:
body: {string: !!python/unicode "{\n \"origin\": \"217.122.164.194\"\n}"} body: {string: "{\n \"origin\": \"217.122.164.194\"\n}"}
headers: headers:
access-control-allow-origin: ['*'] access-control-allow-origin: ['*']
content-type: [application/json] content-type: [application/json]

View File

@@ -2,22 +2,42 @@
import asyncio import asyncio
import aiohttp import aiohttp
from aiohttp.test_utils import TestClient
@asyncio.coroutine async def aiohttp_request(loop, method, url, output="text", encoding="utf-8", content_type=None, **kwargs):
def aiohttp_request(loop, method, url, output='text', encoding='utf-8', **kwargs):
session = aiohttp.ClientSession(loop=loop) session = aiohttp.ClientSession(loop=loop)
response_ctx = session.request(method, url, **kwargs) response_ctx = session.request(method, url, **kwargs)
response = yield from response_ctx.__aenter__() response = await response_ctx.__aenter__()
if output == 'text': if output == "text":
content = yield from response.text() content = await response.text()
elif output == 'json': elif output == "json":
content = yield from response.json(encoding=encoding) content_type = content_type or "application/json"
elif output == 'raw': content = await response.json(encoding=encoding, content_type=content_type)
content = yield from response.read() elif output == "raw":
content = await response.read()
elif output == "stream":
content = await response.content.read()
response_ctx._resp.close() response_ctx._resp.close()
yield from session.close() await session.close()
return response, content 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.12.1
method: GET
uri: https://httpbin.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

@@ -1,11 +1,14 @@
import contextlib import contextlib
import logging
import urllib.parse
import pytest import pytest
asyncio = pytest.importorskip("asyncio") asyncio = pytest.importorskip("asyncio")
aiohttp = pytest.importorskip("aiohttp") aiohttp = pytest.importorskip("aiohttp")
import vcr # noqa: E402 import vcr # noqa: E402
from .aiohttp_utils import aiohttp_request # noqa: E402 from .aiohttp_utils import aiohttp_app, aiohttp_request # noqa: E402
def run_in_loop(fn): def run_in_loop(fn):
@@ -15,125 +18,384 @@ def run_in_loop(fn):
return loop.run_until_complete(task) return loop.run_until_complete(task)
def request(method, url, output='text', **kwargs): def request(method, url, output="text", **kwargs):
def run(loop): def run(loop):
return aiohttp_request(loop, method, url, output=output, **kwargs) return aiohttp_request(loop, method, url, output=output, **kwargs)
return run_in_loop(run) return run_in_loop(run)
def get(url, output='text', **kwargs): def get(url, output="text", **kwargs):
return request('GET', url, output=output, **kwargs) return request("GET", url, output=output, **kwargs)
def post(url, output='text', **kwargs): def post(url, output="text", **kwargs):
return request('POST', url, output='text', **kwargs) return request("POST", url, output="text", **kwargs)
@pytest.fixture(params=["https", "http"]) @pytest.fixture(params=["https", "http"])
def scheme(request): def scheme(request):
'''Fixture that returns both http and https.''' """Fixture that returns both http and https."""
return request.param return request.param
def test_status(tmpdir, scheme): def test_status(tmpdir, scheme):
url = scheme + '://httpbin.org' url = scheme + "://httpbin.org"
with vcr.use_cassette(str(tmpdir.join('status.yaml'))): with vcr.use_cassette(str(tmpdir.join("status.yaml"))):
response, _ = get(url) response, _ = get(url)
with vcr.use_cassette(str(tmpdir.join('status.yaml'))) as cassette: with vcr.use_cassette(str(tmpdir.join("status.yaml"))) as cassette:
cassette_response, _ = get(url) cassette_response, _ = get(url)
assert cassette_response.status == response.status assert cassette_response.status == response.status
assert cassette.play_count == 1 assert cassette.play_count == 1
def test_headers(tmpdir, scheme): @pytest.mark.parametrize("auth", [None, aiohttp.BasicAuth("vcrpy", "test")])
url = scheme + '://httpbin.org' def test_headers(tmpdir, scheme, auth):
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))): url = scheme + "://httpbin.org"
response, _ = get(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: 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 dict(cassette_response.headers) == dict(response.headers)
assert cassette.play_count == 1
assert "istr" not in cassette.data[0]
assert "yarl.URL" not in cassette.data[0]
def test_case_insensitive_headers(tmpdir, scheme):
url = scheme + "://httpbin.org"
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) cassette_response, _ = get(url)
assert cassette_response.headers == response.headers assert "Content-Type" in cassette_response.headers
assert "content-type" in cassette_response.headers
assert cassette.play_count == 1 assert cassette.play_count == 1
def test_text(tmpdir, scheme): def test_text(tmpdir, scheme):
url = scheme + '://httpbin.org' url = scheme + "://httpbin.org"
with vcr.use_cassette(str(tmpdir.join('text.yaml'))): with vcr.use_cassette(str(tmpdir.join("text.yaml"))):
_, response_text = get(url) _, response_text = get(url)
with vcr.use_cassette(str(tmpdir.join('text.yaml'))) as cassette: with vcr.use_cassette(str(tmpdir.join("text.yaml"))) as cassette:
_, cassette_response_text = get(url) _, cassette_response_text = get(url)
assert cassette_response_text == response_text assert cassette_response_text == response_text
assert cassette.play_count == 1 assert cassette.play_count == 1
def test_json(tmpdir, scheme): def test_json(tmpdir, scheme):
url = scheme + '://httpbin.org/get' url = scheme + "://httpbin.org/get"
headers = {'Content-Type': 'application/json'} headers = {"Content-Type": "application/json"}
with vcr.use_cassette(str(tmpdir.join('json.yaml'))): with vcr.use_cassette(str(tmpdir.join("json.yaml"))):
_, response_json = get(url, output='json', headers=headers) _, response_json = get(url, output="json", headers=headers)
with vcr.use_cassette(str(tmpdir.join('json.yaml'))) as cassette: with vcr.use_cassette(str(tmpdir.join("json.yaml"))) as cassette:
_, cassette_response_json = get(url, output='json', headers=headers) _, cassette_response_json = get(url, output="json", headers=headers)
assert cassette_response_json == response_json assert cassette_response_json == response_json
assert cassette.play_count == 1 assert cassette.play_count == 1
def test_binary(tmpdir, scheme): def test_binary(tmpdir, scheme):
url = scheme + '://httpbin.org/image/png' url = scheme + "://httpbin.org/image/png"
with vcr.use_cassette(str(tmpdir.join('binary.yaml'))): with vcr.use_cassette(str(tmpdir.join("binary.yaml"))):
_, response_binary = get(url, output='raw') _, response_binary = get(url, output="raw")
with vcr.use_cassette(str(tmpdir.join('binary.yaml'))) as cassette: with vcr.use_cassette(str(tmpdir.join("binary.yaml"))) as cassette:
_, cassette_response_binary = get(url, output='raw') _, cassette_response_binary = get(url, output="raw")
assert cassette_response_binary == response_binary assert cassette_response_binary == response_binary
assert cassette.play_count == 1 assert cassette.play_count == 1
def test_post(tmpdir, scheme): def test_stream(tmpdir, scheme):
data = {'key1': 'value1', 'key2': 'value2'} url = scheme + "://httpbin.org/get"
url = scheme + '://httpbin.org/post'
with vcr.use_cassette(str(tmpdir.join('post.yaml'))):
_, response_json = post(url, data=data)
with vcr.use_cassette(str(tmpdir.join('post.yaml'))) as cassette: with vcr.use_cassette(str(tmpdir.join("stream.yaml"))):
_, cassette_response_json = post(url, data=data) resp, body = get(url, output="raw") # Do not use stream here, as the stream is exhausted by vcr
assert cassette_response_json == response_json
with vcr.use_cassette(str(tmpdir.join("stream.yaml"))) as cassette:
cassette_resp, cassette_body = get(url, output="stream")
assert cassette_body == body
assert cassette.play_count == 1 assert cassette.play_count == 1
@pytest.mark.parametrize("body", ["data", "json"])
def test_post(tmpdir, scheme, body, caplog):
caplog.set_level(logging.INFO)
data = {"key1": "value1", "key2": "value2"}
url = scheme + "://httpbin.org/post"
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() == "<Request (POST) {}> not in cassette, sending to real server".format(url)
),
None,
), "Log message not found."
def test_params(tmpdir, scheme): def test_params(tmpdir, scheme):
url = scheme + '://httpbin.org/get' url = scheme + "://httpbin.org/get"
headers = {'Content-Type': 'application/json'} headers = {"Content-Type": "application/json"}
params = {'a': 1, 'b': False, 'c': 'c'} params = {"a": 1, "b": False, "c": "c"}
with vcr.use_cassette(str(tmpdir.join('get.yaml'))) as cassette: with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
_, response_json = get(url, output='json', params=params, headers=headers) _, response_json = get(url, output="json", params=params, headers=headers)
with vcr.use_cassette(str(tmpdir.join('get.yaml'))) as cassette: with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
_, cassette_response_json = get(url, output='json', params=params, headers=headers) _, cassette_response_json = get(url, output="json", params=params, headers=headers)
assert cassette_response_json == response_json assert cassette_response_json == response_json
assert cassette.play_count == 1 assert cassette.play_count == 1
def test_params_same_url_distinct_params(tmpdir, scheme): def test_params_same_url_distinct_params(tmpdir, scheme):
url = scheme + '://httpbin.org/get' url = scheme + "://httpbin.org/get"
headers = {'Content-Type': 'application/json'} headers = {"Content-Type": "application/json"}
params = {'a': 1, 'b': False, 'c': 'c'} params = {"a": 1, "b": False, "c": "c"}
with vcr.use_cassette(str(tmpdir.join('get.yaml'))) as cassette: with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
_, response_json = get(url, output='json', params=params, headers=headers) _, response_json = get(url, output="json", params=params, headers=headers)
with vcr.use_cassette(str(tmpdir.join('get.yaml'))) as cassette: with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
_, cassette_response_json = get(url, output='json', params=params, headers=headers) _, cassette_response_json = get(url, output="json", params=params, headers=headers)
assert cassette_response_json == response_json assert cassette_response_json == response_json
assert cassette.play_count == 1 assert cassette.play_count == 1
other_params = {'other': 'params'} other_params = {"other": "params"}
with vcr.use_cassette(str(tmpdir.join('get.yaml'))) as cassette: with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
response, cassette_response_text = get(url, output='text', params=other_params) with pytest.raises(vcr.errors.CannotOverwriteExistingCassetteException):
assert 'No match for the request' in cassette_response_text get(url, output="text", params=other_params)
assert response.status == 599
def test_params_on_url(tmpdir, scheme):
url = scheme + "://httpbin.org/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
def test_redirect(aiohttp_client, tmpdir):
url = "https://mockbin.org/redirect/302/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 {k: v for k, v in cassette_response.request_info.headers.items()} == {
k: v for k, v in response.request_info.headers.items()
}
assert cassette_response.request_info.real_url == response.request_info.real_url
def test_not_modified(aiohttp_client, tmpdir):
"""It doesn't try to redirect on 304"""
url = "https://httpbin.org/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
def test_double_requests(tmpdir):
"""We should capture, record, and replay all requests and response chains,
even if there are duplicate ones.
We should replay in the order we saw them.
"""
url = "https://httpbin.org/get"
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(scheme, tmpdir):
async def run(loop):
cookies_url = scheme + (
"://httpbin.org/response-headers?"
"set-cookie=" + urllib.parse.quote("cookie_1=val_1; Path=/") + "&"
"Set-Cookie=" + urllib.parse.quote("Cookie_2=Val_2; Path=/")
)
home_url = scheme + "://httpbin.org/"
tmp = str(tmpdir.join("cookies.yaml"))
req_cookies = {"Cookie_3": "Val_3"}
req_headers = {"Cookie": "Cookie_4=Val_4"}
# ------------------------- Record -------------------------- #
with vcr.use_cassette(tmp) as cassette:
async with aiohttp.ClientSession(loop=loop) as session:
cookies_resp = await session.get(cookies_url)
home_resp = await session.get(home_url, cookies=req_cookies, headers=req_headers)
assert cassette.play_count == 0
assert_responses(cookies_resp, home_resp)
# -------------------------- Play --------------------------- #
with vcr.use_cassette(tmp, record_mode=vcr.mode.NONE) as cassette:
async with aiohttp.ClientSession(loop=loop) as session:
cookies_resp = await session.get(cookies_url)
home_resp = await session.get(home_url, cookies=req_cookies, headers=req_headers)
assert cassette.play_count == 2
assert_responses(cookies_resp, home_resp)
def assert_responses(cookies_resp, home_resp):
assert cookies_resp.cookies.get("cookie_1").value == "val_1"
assert cookies_resp.cookies.get("Cookie_2").value == "Val_2"
request_cookies = home_resp.request_info.headers["cookie"]
assert "cookie_1=val_1" in request_cookies
assert "Cookie_2=Val_2" in request_cookies
assert "Cookie_3=Val_3" in request_cookies
assert "Cookie_4=Val_4" in request_cookies
run_in_loop(run)
def test_cookies_redirect(scheme, tmpdir):
async def run(loop):
# Sets cookie as provided by the query string and redirects
cookies_url = scheme + "://httpbin.org/cookies/set?Cookie_1=Val_1"
tmp = str(tmpdir.join("cookies.yaml"))
# ------------------------- Record -------------------------- #
with vcr.use_cassette(tmp) as cassette:
async with aiohttp.ClientSession(loop=loop) as session:
cookies_resp = await session.get(cookies_url)
assert not cookies_resp.cookies
cookies = session.cookie_jar.filter_cookies(cookies_url)
assert cookies["Cookie_1"].value == "Val_1"
assert cassette.play_count == 0
cassette.requests[1].headers["Cookie"] == "Cookie_1=Val_1"
# -------------------------- Play --------------------------- #
with vcr.use_cassette(tmp, record_mode=vcr.mode.NONE) as cassette:
async with aiohttp.ClientSession(loop=loop) as session:
cookies_resp = await session.get(cookies_url)
assert not cookies_resp.cookies
cookies = session.cookie_jar.filter_cookies(cookies_url)
assert cookies["Cookie_1"].value == "Val_1"
assert cassette.play_count == 2
cassette.requests[1].headers["Cookie"] == "Cookie_1=Val_1"
# Assert that it's ignoring expiration date
with vcr.use_cassette(tmp, record_mode=vcr.mode.NONE) as cassette:
cassette.responses[0]["headers"]["set-cookie"] = [
"Cookie_1=Val_1; Expires=Wed, 21 Oct 2015 07:28:00 GMT"
]
async with aiohttp.ClientSession(loop=loop) as session:
cookies_resp = await session.get(cookies_url)
assert not cookies_resp.cookies
cookies = session.cookie_jar.filter_cookies(cookies_url)
assert cookies["Cookie_1"].value == "Val_1"
run_in_loop(run)

View File

@@ -1,30 +1,30 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
'''Basic tests for cassettes''' """Basic tests for cassettes"""
# External imports # External imports
import os import os
from six.moves.urllib.request import urlopen from urllib.request import urlopen
# Internal imports # Internal imports
import vcr import vcr
def test_nonexistent_directory(tmpdir, httpbin): def test_nonexistent_directory(tmpdir, httpbin):
'''If we load a cassette in a nonexistent directory, it can save ok''' """If we load a cassette in a nonexistent directory, it can save ok"""
# Check to make sure directory doesnt exist # Check to make sure directory doesnt exist
assert not os.path.exists(str(tmpdir.join('nonexistent'))) assert not os.path.exists(str(tmpdir.join("nonexistent")))
# Run VCR to create dir and cassette file # Run VCR to create dir and cassette file
with vcr.use_cassette(str(tmpdir.join('nonexistent', 'cassette.yml'))): with vcr.use_cassette(str(tmpdir.join("nonexistent", "cassette.yml"))):
urlopen(httpbin.url).read() urlopen(httpbin.url).read()
# This should have made the file and the directory # 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, httpbin): def test_unpatch(tmpdir, httpbin):
'''Ensure that our cassette gets unpatched when we're done''' """Ensure that our cassette gets unpatched when we're done"""
with vcr.use_cassette(str(tmpdir.join('unpatch.yaml'))) as cass: with vcr.use_cassette(str(tmpdir.join("unpatch.yaml"))) as cass:
urlopen(httpbin.url).read() urlopen(httpbin.url).read()
# Make the same request, and assert that we haven't served any more # Make the same request, and assert that we haven't served any more
@@ -34,30 +34,30 @@ def test_unpatch(tmpdir, httpbin):
def test_basic_json_use(tmpdir, httpbin): def test_basic_json_use(tmpdir, httpbin):
''' """
Ensure you can load a json serialized cassette Ensure you can load a json serialized cassette
''' """
test_fixture = str(tmpdir.join('synopsis.json')) test_fixture = str(tmpdir.join("synopsis.json"))
with vcr.use_cassette(test_fixture, serializer='json'): with vcr.use_cassette(test_fixture, serializer="json"):
response = urlopen(httpbin.url).read() response = urlopen(httpbin.url).read()
assert b'difficult sometimes' in response assert b"difficult sometimes" in response
def test_patched_content(tmpdir, httpbin): def test_patched_content(tmpdir, httpbin):
''' """
Ensure that what you pull from a cassette is what came from the Ensure that what you pull from a cassette is what came from the
request request
''' """
with vcr.use_cassette(str(tmpdir.join('synopsis.yaml'))) as cass: with vcr.use_cassette(str(tmpdir.join("synopsis.yaml"))) as cass:
response = urlopen(httpbin.url).read() response = urlopen(httpbin.url).read()
assert cass.play_count == 0 assert cass.play_count == 0
with vcr.use_cassette(str(tmpdir.join('synopsis.yaml'))) as cass: with vcr.use_cassette(str(tmpdir.join("synopsis.yaml"))) as cass:
response2 = urlopen(httpbin.url).read() response2 = urlopen(httpbin.url).read()
assert cass.play_count == 1 assert cass.play_count == 1
cass._save(force=True) cass._save(force=True)
with vcr.use_cassette(str(tmpdir.join('synopsis.yaml'))) as cass: with vcr.use_cassette(str(tmpdir.join("synopsis.yaml"))) as cass:
response3 = urlopen(httpbin.url).read() response3 = urlopen(httpbin.url).read()
assert cass.play_count == 1 assert cass.play_count == 1
@@ -66,12 +66,12 @@ def test_patched_content(tmpdir, httpbin):
def test_patched_content_json(tmpdir, httpbin): def test_patched_content_json(tmpdir, httpbin):
''' """
Ensure that what you pull from a json cassette is what came from the Ensure that what you pull from a json cassette is what came from the
request request
''' """
testfile = str(tmpdir.join('synopsis.json')) testfile = str(tmpdir.join("synopsis.json"))
with vcr.use_cassette(testfile) as cass: with vcr.use_cassette(testfile) as cass:
response = urlopen(httpbin.url).read() response = urlopen(httpbin.url).read()

View File

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

View File

@@ -1,67 +1,122 @@
import pytest import pytest
import os
boto3 = pytest.importorskip("boto3") boto3 = pytest.importorskip("boto3")
import boto3 # NOQA import boto3 # NOQA
import botocore # NOQA
import vcr # NOQA import vcr # NOQA
bucket = 'boto3-demo-1337' # a bucket you can access try:
key = 'test/my_test.txt' # key with r+w access from botocore import awsrequest # NOQA
content = 'hello world i am a string' # content to put in the test file
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="botocore version {ver} does not use vendored requests anymore.".format(ver=botocore.__version__),
)
boto3_skip_awsrequest = pytest.mark.skipif(
not botocore_awsrequest,
reason="botocore version {ver} still uses vendored requests.".format(ver=botocore.__version__),
)
IAM_USER_NAME = "vcrpy"
def test_boto_stubs(tmpdir): @pytest.fixture
with vcr.use_cassette(str(tmpdir.join('boto3-stubs.yml'))): 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
@boto3_skip_vendored_requests
def test_boto_vendored_stubs(tmpdir):
with vcr.use_cassette(str(tmpdir.join("boto3-stubs.yml"))):
# Perform the imports within the patched context so that # Perform the imports within the patched context so that
# HTTPConnection, VerifiedHTTPSConnection refers to the patched version. # HTTPConnection, VerifiedHTTPSConnection refers to the patched version.
from botocore.vendored.requests.packages.urllib3.connectionpool import \ from botocore.vendored.requests.packages.urllib3.connectionpool import (
HTTPConnection, VerifiedHTTPSConnection HTTPConnection,
VerifiedHTTPSConnection,
)
from vcr.stubs.boto3_stubs import VCRRequestsHTTPConnection, VCRRequestsHTTPSConnection from vcr.stubs.boto3_stubs import VCRRequestsHTTPConnection, VCRRequestsHTTPSConnection
# Prove that the class was patched by the stub and that we can instantiate it. # Prove that the class was patched by the stub and that we can instantiate it.
assert issubclass(HTTPConnection, VCRRequestsHTTPConnection) assert issubclass(HTTPConnection, VCRRequestsHTTPConnection)
assert issubclass(VerifiedHTTPSConnection, VCRRequestsHTTPSConnection) assert issubclass(VerifiedHTTPSConnection, VCRRequestsHTTPSConnection)
HTTPConnection('hostname.does.not.matter') HTTPConnection("hostname.does.not.matter")
VerifiedHTTPSConnection('hostname.does.not.matter') VerifiedHTTPSConnection("hostname.does.not.matter")
def test_boto3_without_vcr(): @pytest.mark.skipif(
s3_resource = boto3.resource('s3') os.environ.get("TRAVIS_PULL_REQUEST") != "false",
b = s3_resource.Bucket(bucket) reason="Encrypted Environment Variables from Travis Repository Settings"
b.put_object(Key=key, Body=content) " 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):
# retrieve content to check it with vcr.use_cassette(str(tmpdir.join("boto3-medium.yml"))):
o = s3_resource.Object(bucket, key).get() response = get_user()
assert response["User"]["UserName"] == IAM_USER_NAME
# decode for python3 with vcr.use_cassette(str(tmpdir.join("boto3-medium.yml"))) as cass:
assert content == o['Body'].read().decode('utf-8') response = get_user()
assert response["User"]["UserName"] == IAM_USER_NAME
def test_boto_medium_difficulty(tmpdir):
s3_resource = boto3.resource('s3')
b = s3_resource.Bucket(bucket)
with vcr.use_cassette(str(tmpdir.join('boto3-medium.yml'))):
b.put_object(Key=key, Body=content)
o = s3_resource.Object(bucket, key).get()
assert content == o['Body'].read().decode('utf-8')
with vcr.use_cassette(str(tmpdir.join('boto3-medium.yml'))) as cass:
b.put_object(Key=key, Body=content)
o = s3_resource.Object(bucket, key).get()
assert content == o['Body'].read().decode('utf-8')
assert cass.all_played assert cass.all_played
def test_boto_hardcore_mode(tmpdir): @pytest.mark.skipif(
with vcr.use_cassette(str(tmpdir.join('boto3-hardcore.yml'))): os.environ.get("TRAVIS_PULL_REQUEST") != "false",
s3_resource = boto3.resource('s3') reason="Encrypted Environment Variables from Travis Repository Settings"
b = s3_resource.Bucket(bucket) " are disabled on PRs from forks. "
b.put_object(Key=key, Body=content) "https://docs.travis-ci.com/user/pull-requests/#pull-requests-and-security-restrictions",
o = s3_resource.Object(bucket, key).get() )
assert content == o['Body'].read().decode('utf-8') 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: with vcr.use_cassette(str(tmpdir.join("boto3-hardcore.yml"))) as cass:
s3_resource = boto3.resource('s3') ses = boto3.Session(
b = s3_resource.Bucket(bucket) aws_access_key_id=os.environ.get("AWS_ACCESS_KEY_ID"),
b.put_object(Key=key, Body=content) aws_secret_access_key=os.environ.get("AWS_SECRET_ACCESS_KEY"),
o = s3_resource.Object(bucket, key).get() aws_session_token=None,
assert content == o['Body'].read().decode('utf-8') 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 assert cass.all_played

View File

@@ -2,49 +2,49 @@ import os
import json import json
import pytest import pytest
import vcr import vcr
from six.moves.urllib.request import urlopen from urllib.request import urlopen
def test_set_serializer_default_config(tmpdir, httpbin): def test_set_serializer_default_config(tmpdir, httpbin):
my_vcr = vcr.VCR(serializer='json') my_vcr = vcr.VCR(serializer="json")
with my_vcr.use_cassette(str(tmpdir.join('test.json'))): with my_vcr.use_cassette(str(tmpdir.join("test.json"))):
assert my_vcr.serializer == 'json' assert my_vcr.serializer == "json"
urlopen(httpbin.url + '/get') urlopen(httpbin.url + "/get")
with open(str(tmpdir.join('test.json'))) as f: with open(str(tmpdir.join("test.json"))) as f:
assert json.loads(f.read()) assert json.loads(f.read())
def test_default_set_cassette_library_dir(tmpdir, httpbin): def test_default_set_cassette_library_dir(tmpdir, httpbin):
my_vcr = vcr.VCR(cassette_library_dir=str(tmpdir.join('subdir'))) my_vcr = vcr.VCR(cassette_library_dir=str(tmpdir.join("subdir")))
with my_vcr.use_cassette('test.json'): with my_vcr.use_cassette("test.json"):
urlopen(httpbin.url + '/get') urlopen(httpbin.url + "/get")
assert os.path.exists(str(tmpdir.join('subdir').join('test.json'))) assert os.path.exists(str(tmpdir.join("subdir").join("test.json")))
def test_override_set_cassette_library_dir(tmpdir, httpbin): def test_override_set_cassette_library_dir(tmpdir, httpbin):
my_vcr = vcr.VCR(cassette_library_dir=str(tmpdir.join('subdir'))) my_vcr = vcr.VCR(cassette_library_dir=str(tmpdir.join("subdir")))
cld = str(tmpdir.join('subdir2')) cld = str(tmpdir.join("subdir2"))
with my_vcr.use_cassette('test.json', cassette_library_dir=cld): with my_vcr.use_cassette("test.json", cassette_library_dir=cld):
urlopen(httpbin.url + '/get') urlopen(httpbin.url + "/get")
assert os.path.exists(str(tmpdir.join('subdir2').join('test.json'))) assert os.path.exists(str(tmpdir.join("subdir2").join("test.json")))
assert not os.path.exists(str(tmpdir.join('subdir').join('test.json'))) assert not os.path.exists(str(tmpdir.join("subdir").join("test.json")))
def test_override_match_on(tmpdir, httpbin): def test_override_match_on(tmpdir, httpbin):
my_vcr = vcr.VCR(match_on=['method']) my_vcr = vcr.VCR(match_on=["method"])
with my_vcr.use_cassette(str(tmpdir.join('test.json'))): with my_vcr.use_cassette(str(tmpdir.join("test.json"))):
urlopen(httpbin.url) urlopen(httpbin.url)
with my_vcr.use_cassette(str(tmpdir.join('test.json'))) as cass: with my_vcr.use_cassette(str(tmpdir.join("test.json"))) as cass:
urlopen(httpbin.url + '/get') urlopen(httpbin.url + "/get")
assert len(cass) == 1 assert len(cass) == 1
assert cass.play_count == 1 assert cass.play_count == 1
@@ -54,5 +54,5 @@ def test_missing_matcher():
my_vcr = vcr.VCR() my_vcr = vcr.VCR()
my_vcr.register_matcher("awesome", object) my_vcr.register_matcher("awesome", object)
with pytest.raises(KeyError): with pytest.raises(KeyError):
with my_vcr.use_cassette("test.yaml", match_on=['notawesome']): with my_vcr.use_cassette("test.yaml", match_on=["notawesome"]):
pass pass

View File

@@ -1,21 +1,21 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
'''Basic tests about save behavior''' """Basic tests about save behavior"""
# External imports # External imports
import os import os
import time import time
from six.moves.urllib.request import urlopen from urllib.request import urlopen
# Internal imports # Internal imports
import vcr import vcr
def test_disk_saver_nowrite(tmpdir, httpbin): def test_disk_saver_nowrite(tmpdir, httpbin):
''' """
Ensure that when you close a cassette without changing it it doesn't Ensure that when you close a cassette without changing it it doesn't
rewrite the file rewrite the file
''' """
fname = str(tmpdir.join('synopsis.yaml')) fname = str(tmpdir.join("synopsis.yaml"))
with vcr.use_cassette(fname) as cass: with vcr.use_cassette(fname) as cass:
urlopen(httpbin.url).read() urlopen(httpbin.url).read()
assert cass.play_count == 0 assert cass.play_count == 0
@@ -31,11 +31,11 @@ def test_disk_saver_nowrite(tmpdir, httpbin):
def test_disk_saver_write(tmpdir, httpbin): def test_disk_saver_write(tmpdir, httpbin):
''' """
Ensure that when you close a cassette after changing it it does Ensure that when you close a cassette after changing it it does
rewrite the file rewrite the file
''' """
fname = str(tmpdir.join('synopsis.yaml')) fname = str(tmpdir.join("synopsis.yaml"))
with vcr.use_cassette(fname) as cass: with vcr.use_cassette(fname) as cass:
urlopen(httpbin.url).read() urlopen(httpbin.url).read()
assert cass.play_count == 0 assert cass.play_count == 0
@@ -45,9 +45,9 @@ def test_disk_saver_write(tmpdir, httpbin):
# the mtime doesn't change # the mtime doesn't change
time.sleep(1) time.sleep(1)
with vcr.use_cassette(fname, record_mode='any') as cass: with vcr.use_cassette(fname, record_mode=vcr.mode.ANY) as cass:
urlopen(httpbin.url).read() urlopen(httpbin.url).read()
urlopen(httpbin.url + '/get').read() urlopen(httpbin.url + "/get").read()
assert cass.play_count == 1 assert cass.play_count == 1
assert cass.dirty assert cass.dirty
last_mod2 = os.path.getmtime(fname) last_mod2 = os.path.getmtime(fname)

View File

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

View File

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

View File

@@ -0,0 +1,255 @@
import pytest
import contextlib
import os
asyncio = pytest.importorskip("asyncio")
httpx = pytest.importorskip("httpx")
import vcr # noqa: E402
class BaseDoRequest:
_client_class = None
def __init__(self, *args, **kwargs):
self._client = self._client_class(*args, **kwargs)
class DoSyncRequest(BaseDoRequest):
_client_class = httpx.Client
def __call__(self, *args, **kwargs):
return self._client.request(*args, timeout=60, **kwargs)
class DoAsyncRequest(BaseDoRequest):
_client_class = httpx.AsyncClient
@staticmethod
def run_in_loop(coroutine):
with contextlib.closing(asyncio.new_event_loop()) as loop:
asyncio.set_event_loop(loop)
task = loop.create_task(coroutine)
return loop.run_until_complete(task)
def __call__(self, *args, **kwargs):
async def _request():
async with self._client as c:
return await c.request(*args, **kwargs)
return DoAsyncRequest.run_in_loop(_request())
def pytest_generate_tests(metafunc):
if "do_request" in metafunc.fixturenames:
metafunc.parametrize("do_request", [DoAsyncRequest, DoSyncRequest])
if "scheme" in metafunc.fixturenames:
metafunc.parametrize("scheme", ["http", "https"])
@pytest.fixture
def yml(tmpdir, request):
return str(tmpdir.join(request.function.__name__ + ".yaml"))
def test_status(tmpdir, scheme, do_request):
url = scheme + "://mockbin.org/request"
with vcr.use_cassette(str(tmpdir.join("status.yaml"))):
response = do_request()("GET", url)
with vcr.use_cassette(str(tmpdir.join("status.yaml"))) as cassette:
cassette_response = do_request()("GET", url)
assert cassette_response.status_code == response.status_code
assert cassette.play_count == 1
def test_case_insensitive_headers(tmpdir, scheme, do_request):
url = scheme + "://mockbin.org/request"
with vcr.use_cassette(str(tmpdir.join("whatever.yaml"))):
do_request()("GET", url)
with vcr.use_cassette(str(tmpdir.join("whatever.yaml"))) as cassette:
cassette_response = do_request()("GET", url)
assert "Content-Type" in cassette_response.headers
assert "content-type" in cassette_response.headers
assert cassette.play_count == 1
def test_content(tmpdir, scheme, do_request):
url = scheme + "://httpbin.org"
with vcr.use_cassette(str(tmpdir.join("cointent.yaml"))):
response = do_request()("GET", url)
with vcr.use_cassette(str(tmpdir.join("cointent.yaml"))) as cassette:
cassette_response = do_request()("GET", url)
assert cassette_response.content == response.content
assert cassette.play_count == 1
def test_json(tmpdir, scheme, do_request):
url = scheme + "://httpbin.org/get"
headers = {"Content-Type": "application/json"}
with vcr.use_cassette(str(tmpdir.join("json.yaml"))):
response = do_request(headers=headers)("GET", url)
with vcr.use_cassette(str(tmpdir.join("json.yaml"))) as cassette:
cassette_response = do_request(headers=headers)("GET", url)
assert cassette_response.json() == response.json()
assert cassette.play_count == 1
def test_params_same_url_distinct_params(tmpdir, scheme, do_request):
url = scheme + "://httpbin.org/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:
with pytest.raises(vcr.errors.CannotOverwriteExistingCassetteException):
do_request()("GET", url, params=params, headers=headers)
def test_redirect(tmpdir, do_request, yml):
url = "https://mockbin.org/redirect/303/2"
response = do_request()("GET", url)
with vcr.use_cassette(yml):
response = do_request()("GET", url)
with vcr.use_cassette(yml) as cassette:
cassette_response = do_request()("GET", url)
assert cassette_response.status_code == response.status_code
assert len(cassette_response.history) == len(response.history)
assert len(cassette) == 3
assert cassette.play_count == 3
# Assert that the real response and the cassette response have a similar
# looking request_info.
assert cassette_response.request.url == response.request.url
assert cassette_response.request.method == response.request.method
assert {k: v for k, v in cassette_response.request.headers.items()} == {
k: v for k, v in response.request.headers.items()
}
def test_work_with_gzipped_data(tmpdir, do_request, yml):
with vcr.use_cassette(yml):
do_request()("GET", "https://httpbin.org/gzip")
with vcr.use_cassette(yml) as cassette:
cassette_response = do_request()("GET", "https://httpbin.org/gzip")
assert "gzip" in cassette_response.json()["headers"]["Accept-Encoding"]
assert cassette_response.read()
assert cassette.play_count == 1
@pytest.mark.parametrize("url", ["https://github.com/kevin1024/vcrpy/issues/" + str(i) for i in range(3, 6)])
def test_simple_fetching(tmpdir, do_request, yml, url):
with vcr.use_cassette(yml):
do_request()("GET", url)
with vcr.use_cassette(yml) as cassette:
cassette_response = do_request()("GET", url)
assert str(cassette_response.request.url) == url
assert cassette.play_count == 1
def test_behind_proxy(do_request):
# This is recorded because otherwise we should have a live proxy somewhere.
yml = (
os.path.dirname(os.path.realpath(__file__)) + "/cassettes/" + "test_httpx_test_test_behind_proxy.yml"
)
url = "https://httpbin.org/headers"
proxy = "http://localhost:8080"
proxies = {"http": proxy, "https": proxy}
with vcr.use_cassette(yml):
response = do_request(proxies=proxies, verify=False)("GET", url)
with vcr.use_cassette(yml) as cassette:
cassette_response = do_request(proxies=proxies, verify=False)("GET", url)
assert str(cassette_response.request.url) == url
assert cassette.play_count == 1
assert cassette_response.headers["Via"] == "my_own_proxy", str(cassette_response.headers)
assert cassette_response.request.url == response.request.url
def test_cookies(tmpdir, scheme, do_request):
def client_cookies(client):
return [c for c in client._client.cookies]
def response_cookies(response):
return [c for c in response.cookies]
client = do_request()
assert client_cookies(client) == []
url = scheme + "://httpbin.org"
testfile = str(tmpdir.join("cookies.yml"))
with vcr.use_cassette(testfile):
r1 = client("GET", url + "/cookies/set?k1=v1&k2=v2")
assert response_cookies(r1.history[0]) == ["k1", "k2"]
assert response_cookies(r1) == []
r2 = client("GET", url + "/cookies")
assert len(r2.json()["cookies"]) == 2
assert client_cookies(client) == ["k1", "k2"]
new_client = do_request()
assert client_cookies(new_client) == []
with vcr.use_cassette(testfile) as cassette:
cassette_response = new_client("GET", url + "/cookies/set?k1=v1&k2=v2")
assert response_cookies(cassette_response.history[0]) == ["k1", "k2"]
assert response_cookies(cassette_response) == []
assert cassette.play_count == 2
assert client_cookies(new_client) == ["k1", "k2"]
def test_relative_redirects(tmpdir, scheme, do_request):
url = scheme + "://mockbin.com/redirect/301?to=/redirect/301?to=/request"
testfile = str(tmpdir.join("relative_redirects.yml"))
with vcr.use_cassette(testfile):
response = do_request()("GET", url)
assert len(response.history) == 2, response
assert response.json()["url"].endswith("request")
with vcr.use_cassette(testfile) as cassette:
response = do_request()("GET", url)
assert len(response.history) == 2
assert response.json()["url"].endswith("request")
assert cassette.play_count == 3
def test_redirect_wo_allow_redirects(do_request, yml):
url = "https://mockbin.org/redirect/308/5"
with vcr.use_cassette(yml):
response = do_request()("GET", url, allow_redirects=False)
assert str(response.url).endswith("308/5")
assert response.status_code == 308
with vcr.use_cassette(yml) as cassette:
response = do_request()("GET", url, allow_redirects=False)
assert str(response.url).endswith("308/5")
assert response.status_code == 308
assert cassette.play_count == 1

View File

@@ -1,4 +1,4 @@
from six.moves.urllib.request import urlopen from urllib.request import urlopen
import socket import socket
from contextlib import contextmanager from contextlib import contextmanager
import vcr import vcr
@@ -15,59 +15,53 @@ def overridden_dns(overrides):
def fake_getaddrinfo(*args, **kwargs): def fake_getaddrinfo(*args, **kwargs):
if args[0] in overrides: if args[0] in overrides:
address = overrides[args[0]] address = overrides[args[0]]
return [(2, 1, 6, '', (address, args[1]))] return [(2, 1, 6, "", (address, args[1]))]
return real_getaddrinfo(*args, **kwargs) return real_getaddrinfo(*args, **kwargs)
socket.getaddrinfo = fake_getaddrinfo socket.getaddrinfo = fake_getaddrinfo
yield yield
socket.getaddrinfo = real_getaddrinfo socket.getaddrinfo = real_getaddrinfo
def test_ignore_localhost(tmpdir, httpbin): def test_ignore_localhost(tmpdir, httpbin):
with overridden_dns({'httpbin.org': '127.0.0.1'}): with overridden_dns({"httpbin.org": "127.0.0.1"}):
cass_file = str(tmpdir.join('filter_qs.yaml')) cass_file = str(tmpdir.join("filter_qs.yaml"))
with vcr.use_cassette(cass_file, ignore_localhost=True) as cass: with vcr.use_cassette(cass_file, ignore_localhost=True) as cass:
urlopen('http://localhost:{}/'.format(httpbin.port)) urlopen("http://localhost:{}/".format(httpbin.port))
assert len(cass) == 0 assert len(cass) == 0
urlopen('http://httpbin.org:{}/'.format(httpbin.port)) urlopen("http://httpbin.org:{}/".format(httpbin.port))
assert len(cass) == 1 assert len(cass) == 1
def test_ignore_httpbin(tmpdir, httpbin): def test_ignore_httpbin(tmpdir, httpbin):
with overridden_dns({'httpbin.org': '127.0.0.1'}): with overridden_dns({"httpbin.org": "127.0.0.1"}):
cass_file = str(tmpdir.join('filter_qs.yaml')) cass_file = str(tmpdir.join("filter_qs.yaml"))
with vcr.use_cassette( with vcr.use_cassette(cass_file, ignore_hosts=["httpbin.org"]) as cass:
cass_file, urlopen("http://httpbin.org:{}/".format(httpbin.port))
ignore_hosts=['httpbin.org']
) as cass:
urlopen('http://httpbin.org:{}/'.format(httpbin.port))
assert len(cass) == 0 assert len(cass) == 0
urlopen('http://localhost:{}/'.format(httpbin.port)) urlopen("http://localhost:{}/".format(httpbin.port))
assert len(cass) == 1 assert len(cass) == 1
def test_ignore_localhost_and_httpbin(tmpdir, httpbin): def test_ignore_localhost_and_httpbin(tmpdir, httpbin):
with overridden_dns({'httpbin.org': '127.0.0.1'}): with overridden_dns({"httpbin.org": "127.0.0.1"}):
cass_file = str(tmpdir.join('filter_qs.yaml')) cass_file = str(tmpdir.join("filter_qs.yaml"))
with vcr.use_cassette( with vcr.use_cassette(cass_file, ignore_hosts=["httpbin.org"], ignore_localhost=True) as cass:
cass_file, urlopen("http://httpbin.org:{}".format(httpbin.port))
ignore_hosts=['httpbin.org'], urlopen("http://localhost:{}".format(httpbin.port))
ignore_localhost=True
) as cass:
urlopen('http://httpbin.org:{}'.format(httpbin.port))
urlopen('http://localhost:{}'.format(httpbin.port))
assert len(cass) == 0 assert len(cass) == 0
def test_ignore_localhost_twice(tmpdir, httpbin): def test_ignore_localhost_twice(tmpdir, httpbin):
with overridden_dns({'httpbin.org': '127.0.0.1'}): with overridden_dns({"httpbin.org": "127.0.0.1"}):
cass_file = str(tmpdir.join('filter_qs.yaml')) cass_file = str(tmpdir.join("filter_qs.yaml"))
with vcr.use_cassette(cass_file, ignore_localhost=True) as cass: with vcr.use_cassette(cass_file, ignore_localhost=True) as cass:
urlopen('http://localhost:{}'.format(httpbin.port)) urlopen("http://localhost:{}".format(httpbin.port))
assert len(cass) == 0 assert len(cass) == 0
urlopen('http://httpbin.org:{}'.format(httpbin.port)) urlopen("http://httpbin.org:{}".format(httpbin.port))
assert len(cass) == 1 assert len(cass) == 1
with vcr.use_cassette(cass_file, ignore_localhost=True) as cass: with vcr.use_cassette(cass_file, ignore_localhost=True) as cass:
assert len(cass) == 1 assert len(cass) == 1
urlopen('http://localhost:{}'.format(httpbin.port)) urlopen("http://localhost:{}".format(httpbin.port))
urlopen('http://httpbin.org:{}'.format(httpbin.port)) urlopen("http://httpbin.org:{}".format(httpbin.port))
assert len(cass) == 1 assert len(cass) == 1

View File

@@ -1,46 +1,39 @@
import vcr import vcr
import pytest import pytest
from six.moves.urllib.request import urlopen from urllib.request import urlopen
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): def _replace_httpbin(uri, httpbin, httpbin_secure):
return uri.replace('http://httpbin.org', httpbin.url).replace('https://httpbin.org', httpbin_secure.url) return uri.replace("http://httpbin.org", httpbin.url).replace("https://httpbin.org", httpbin_secure.url)
@pytest.fixture @pytest.fixture
def cassette(tmpdir, httpbin, httpbin_secure): def cassette(tmpdir, httpbin, httpbin_secure):
""" """
Helper fixture used to prepare the cassete Helper fixture used to prepare the cassette
returns path to the recorded cassette returns path to the recorded cassette
""" """
default_uri = _replace_httpbin(DEFAULT_URI, httpbin, httpbin_secure) default_uri = _replace_httpbin(DEFAULT_URI, httpbin, httpbin_secure)
cassette_path = str(tmpdir.join('test.yml')) cassette_path = str(tmpdir.join("test.yml"))
with vcr.use_cassette(cassette_path, record_mode='all'): with vcr.use_cassette(cassette_path, record_mode=vcr.mode.ALL):
urlopen(default_uri) urlopen(default_uri)
return cassette_path return cassette_path
@pytest.mark.parametrize("matcher, matching_uri, not_matching_uri", [ @pytest.mark.parametrize(
('uri', "matcher, matching_uri, not_matching_uri",
'http://httpbin.org/get?p1=q1&p2=q2', [
'http://httpbin.org/get?p2=q2&p1=q1'), ("uri", "http://httpbin.org/get?p1=q1&p2=q2", "http://httpbin.org/get?p2=q2&p1=q1"),
('scheme', ("scheme", "http://google.com/post?a=b", "https://httpbin.org/get?p1=q1&p2=q2"),
'http://google.com/post?a=b', ("host", "https://httpbin.org/post?a=b", "http://google.com/get?p1=q1&p2=q2"),
'https://httpbin.org/get?p1=q1&p2=q2'), ("path", "https://google.com/get?a=b", "http://httpbin.org/post?p1=q1&p2=q2"),
('host', ("query", "https://google.com/get?p2=q2&p1=q1", "http://httpbin.org/get?p1=q1&a=b"),
'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): def test_matchers(httpbin, httpbin_secure, cassette, matcher, matching_uri, not_matching_uri):
matching_uri = _replace_httpbin(matching_uri, httpbin, httpbin_secure) matching_uri = _replace_httpbin(matching_uri, httpbin, httpbin_secure)
@@ -67,22 +60,20 @@ def test_method_matcher(cassette, httpbin, httpbin_secure):
default_uri = _replace_httpbin(DEFAULT_URI, httpbin, httpbin_secure) default_uri = _replace_httpbin(DEFAULT_URI, httpbin, httpbin_secure)
# play cassette with matching on method # play cassette with matching on method
with vcr.use_cassette(cassette, match_on=['method']) as cass: with vcr.use_cassette(cassette, match_on=["method"]) as cass:
urlopen('https://google.com/get?a=b') urlopen("https://google.com/get?a=b")
assert cass.play_count == 1 assert cass.play_count == 1
# should fail if method does not match # should fail if method does not match
with pytest.raises(vcr.errors.CannotOverwriteExistingCassetteException): with pytest.raises(vcr.errors.CannotOverwriteExistingCassetteException):
with vcr.use_cassette(cassette, match_on=['method']) as cass: with vcr.use_cassette(cassette, match_on=["method"]) as cass:
# is a POST request # is a POST request
urlopen(default_uri, data=b'') urlopen(default_uri, data=b"")
@pytest.mark.parametrize("uri", [ @pytest.mark.parametrize(
DEFAULT_URI, "uri", [DEFAULT_URI, "http://httpbin.org/get?p2=q2&p1=q1", "http://httpbin.org/get?p2=q2&p1=q1"]
'http://httpbin.org/get?p2=q2&p1=q1', )
'http://httpbin.org/get?p2=q2&p1=q1',
])
def test_default_matcher_matches(cassette, uri, httpbin, httpbin_secure): def test_default_matcher_matches(cassette, uri, httpbin, httpbin_secure):
uri = _replace_httpbin(uri, httpbin, httpbin_secure) uri = _replace_httpbin(uri, httpbin, httpbin_secure)
@@ -92,12 +83,15 @@ def test_default_matcher_matches(cassette, uri, httpbin, httpbin_secure):
assert cass.play_count == 1 assert cass.play_count == 1
@pytest.mark.parametrize("uri", [ @pytest.mark.parametrize(
'https://httpbin.org/get?p1=q1&p2=q2', "uri",
'http://google.com/get?p1=q1&p2=q2', [
'http://httpbin.org/post?p1=q1&p2=q2', "https://httpbin.org/get?p1=q1&p2=q2",
'http://httpbin.org/get?p1=q1&a=b' "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): def test_default_matcher_does_not_match(cassette, uri, httpbin, httpbin_secure):
uri = _replace_httpbin(uri, httpbin, httpbin_secure) uri = _replace_httpbin(uri, httpbin, httpbin_secure)
with pytest.raises(vcr.errors.CannotOverwriteExistingCassetteException): with pytest.raises(vcr.errors.CannotOverwriteExistingCassetteException):
@@ -110,4 +104,4 @@ def test_default_matcher_does_not_match_on_method(cassette, httpbin, httpbin_sec
with pytest.raises(vcr.errors.CannotOverwriteExistingCassetteException): with pytest.raises(vcr.errors.CannotOverwriteExistingCassetteException):
with vcr.use_cassette(cassette): with vcr.use_cassette(cassette):
# is a POST request # is a POST request
urlopen(default_uri, data=b'') urlopen(default_uri, data=b"")

View File

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

View File

@@ -0,0 +1,60 @@
# -*- coding: utf-8 -*-
"""Test using a proxy."""
# External imports
import multiprocessing
import pytest
import http.server
import socketserver
from urllib.request import urlopen
# Internal imports
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.send_response(status, upstream_response.msg)
for header in headers:
self.send_header(*header)
self.end_headers()
self.copyfile(upstream_response, self.wfile)
@pytest.yield_fixture(scope="session")
def proxy_server():
httpd = socketserver.ThreadingTCPServer(("", 0), Proxy)
proxy_process = multiprocessing.Process(target=httpd.serve_forever)
proxy_process.start()
yield "http://{}:{}".format(*httpd.server_address)
proxy_process.terminate()
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"))) as cassette:
cassette_response = requests.get(httpbin.url, proxies={"http": proxy_server})
assert cassette_response.headers == response.headers
assert cassette.play_count == 1

View File

@@ -1,15 +1,15 @@
import pytest import pytest
import vcr import vcr
from six.moves.urllib.request import urlopen from urllib.request import urlopen
def test_once_record_mode(tmpdir, httpbin): def test_once_record_mode(tmpdir, httpbin):
testfile = str(tmpdir.join('recordmode.yml')) testfile = str(tmpdir.join("recordmode.yml"))
with vcr.use_cassette(testfile, record_mode="once"): with vcr.use_cassette(testfile, record_mode=vcr.mode.ONCE):
# cassette file doesn't exist, so create. # cassette file doesn't exist, so create.
urlopen(httpbin.url).read() urlopen(httpbin.url).read()
with vcr.use_cassette(testfile, record_mode="once"): with vcr.use_cassette(testfile, record_mode=vcr.mode.ONCE):
# make the same request again # make the same request again
urlopen(httpbin.url).read() urlopen(httpbin.url).read()
@@ -17,25 +17,25 @@ def test_once_record_mode(tmpdir, httpbin):
# but, try to access something else from the same cassette, and an # but, try to access something else from the same cassette, and an
# exception is raised. # exception is raised.
with pytest.raises(Exception): with pytest.raises(Exception):
urlopen(httpbin.url + '/get').read() urlopen(httpbin.url + "/get").read()
def test_once_record_mode_two_times(tmpdir, httpbin): def test_once_record_mode_two_times(tmpdir, httpbin):
testfile = str(tmpdir.join('recordmode.yml')) testfile = str(tmpdir.join("recordmode.yml"))
with vcr.use_cassette(testfile, record_mode="once"): with vcr.use_cassette(testfile, record_mode=vcr.mode.ONCE):
# get two of the same file # get two of the same file
urlopen(httpbin.url).read() urlopen(httpbin.url).read()
urlopen(httpbin.url).read() urlopen(httpbin.url).read()
with vcr.use_cassette(testfile, record_mode="once"): with vcr.use_cassette(testfile, record_mode=vcr.mode.ONCE):
# do it again # do it again
urlopen(httpbin.url).read() urlopen(httpbin.url).read()
urlopen(httpbin.url).read() urlopen(httpbin.url).read()
def test_once_mode_three_times(tmpdir, httpbin): def test_once_mode_three_times(tmpdir, httpbin):
testfile = str(tmpdir.join('recordmode.yml')) testfile = str(tmpdir.join("recordmode.yml"))
with vcr.use_cassette(testfile, record_mode="once"): with vcr.use_cassette(testfile, record_mode=vcr.mode.ONCE):
# get three of the same file # get three of the same file
urlopen(httpbin.url).read() urlopen(httpbin.url).read()
urlopen(httpbin.url).read() urlopen(httpbin.url).read()
@@ -43,13 +43,13 @@ def test_once_mode_three_times(tmpdir, httpbin):
def test_new_episodes_record_mode(tmpdir, httpbin): def test_new_episodes_record_mode(tmpdir, httpbin):
testfile = str(tmpdir.join('recordmode.yml')) testfile = str(tmpdir.join("recordmode.yml"))
with vcr.use_cassette(testfile, record_mode="new_episodes"): with vcr.use_cassette(testfile, record_mode=vcr.mode.NEW_EPISODES):
# cassette file doesn't exist, so create. # cassette file doesn't exist, so create.
urlopen(httpbin.url).read() urlopen(httpbin.url).read()
with vcr.use_cassette(testfile, record_mode="new_episodes") as cass: with vcr.use_cassette(testfile, record_mode=vcr.mode.NEW_EPISODES) as cass:
# make the same request again # make the same request again
urlopen(httpbin.url).read() urlopen(httpbin.url).read()
@@ -58,7 +58,7 @@ def test_new_episodes_record_mode(tmpdir, httpbin):
# in the "new_episodes" record mode, we can add more requests to # in the "new_episodes" record mode, we can add more requests to
# a cassette without repurcussions. # a cassette without repurcussions.
urlopen(httpbin.url + '/get').read() urlopen(httpbin.url + "/get").read()
# one of the responses has been played # one of the responses has been played
assert cass.play_count == 1 assert cass.play_count == 1
@@ -66,19 +66,19 @@ def test_new_episodes_record_mode(tmpdir, httpbin):
# not all responses have been played # not all responses have been played
assert not cass.all_played assert not cass.all_played
with vcr.use_cassette(testfile, record_mode="new_episodes") as cass: with vcr.use_cassette(testfile, record_mode=vcr.mode.NEW_EPISODES) as cass:
# the cassette should now have 2 responses # the cassette should now have 2 responses
assert len(cass.responses) == 2 assert len(cass.responses) == 2
def test_new_episodes_record_mode_two_times(tmpdir, httpbin): def test_new_episodes_record_mode_two_times(tmpdir, httpbin):
testfile = str(tmpdir.join('recordmode.yml')) testfile = str(tmpdir.join("recordmode.yml"))
url = httpbin.url + '/bytes/1024' url = httpbin.url + "/bytes/1024"
with vcr.use_cassette(testfile, record_mode="new_episodes"): with vcr.use_cassette(testfile, record_mode=vcr.mode.NEW_EPISODES):
# cassette file doesn't exist, so create. # cassette file doesn't exist, so create.
original_first_response = urlopen(url).read() original_first_response = urlopen(url).read()
with vcr.use_cassette(testfile, record_mode="new_episodes"): with vcr.use_cassette(testfile, record_mode=vcr.mode.NEW_EPISODES):
# make the same request again # make the same request again
assert urlopen(url).read() == original_first_response assert urlopen(url).read() == original_first_response
@@ -86,7 +86,7 @@ def test_new_episodes_record_mode_two_times(tmpdir, httpbin):
# to the cassette without repercussions # to the cassette without repercussions
original_second_response = urlopen(url).read() original_second_response = urlopen(url).read()
with vcr.use_cassette(testfile, record_mode="once"): with vcr.use_cassette(testfile, record_mode=vcr.mode.ONCE):
# make the same request again # make the same request again
assert urlopen(url).read() == original_first_response assert urlopen(url).read() == original_first_response
assert urlopen(url).read() == original_second_response assert urlopen(url).read() == original_second_response
@@ -97,19 +97,19 @@ def test_new_episodes_record_mode_two_times(tmpdir, httpbin):
def test_all_record_mode(tmpdir, httpbin): def test_all_record_mode(tmpdir, httpbin):
testfile = str(tmpdir.join('recordmode.yml')) testfile = str(tmpdir.join("recordmode.yml"))
with vcr.use_cassette(testfile, record_mode="all"): with vcr.use_cassette(testfile, record_mode=vcr.mode.ALL):
# cassette file doesn't exist, so create. # cassette file doesn't exist, so create.
urlopen(httpbin.url).read() urlopen(httpbin.url).read()
with vcr.use_cassette(testfile, record_mode="all") as cass: with vcr.use_cassette(testfile, record_mode=vcr.mode.ALL) as cass:
# make the same request again # make the same request again
urlopen(httpbin.url).read() urlopen(httpbin.url).read()
# in the "all" record mode, we can add more requests to # in the "all" record mode, we can add more requests to
# a cassette without repurcussions. # a cassette without repurcussions.
urlopen(httpbin.url + '/get').read() urlopen(httpbin.url + "/get").read()
# The cassette was never actually played, even though it existed. # The cassette was never actually played, even though it existed.
# that's because, in "all" mode, the requests all go directly to # that's because, in "all" mode, the requests all go directly to
@@ -120,23 +120,23 @@ def test_all_record_mode(tmpdir, httpbin):
def test_none_record_mode(tmpdir, httpbin): def test_none_record_mode(tmpdir, httpbin):
# Cassette file doesn't exist, yet we are trying to make a request. # Cassette file doesn't exist, yet we are trying to make a request.
# raise hell. # raise hell.
testfile = str(tmpdir.join('recordmode.yml')) testfile = str(tmpdir.join("recordmode.yml"))
with vcr.use_cassette(testfile, record_mode="none"): with vcr.use_cassette(testfile, record_mode=vcr.mode.NONE):
with pytest.raises(Exception): with pytest.raises(Exception):
urlopen(httpbin.url).read() urlopen(httpbin.url).read()
def test_none_record_mode_with_existing_cassette(tmpdir, httpbin): def test_none_record_mode_with_existing_cassette(tmpdir, httpbin):
# create a cassette file # create a cassette file
testfile = str(tmpdir.join('recordmode.yml')) testfile = str(tmpdir.join("recordmode.yml"))
with vcr.use_cassette(testfile, record_mode="all"): with vcr.use_cassette(testfile, record_mode=vcr.mode.ALL):
urlopen(httpbin.url).read() urlopen(httpbin.url).read()
# play from cassette file # play from cassette file
with vcr.use_cassette(testfile, record_mode="none") as cass: with vcr.use_cassette(testfile, record_mode=vcr.mode.NONE) as cass:
urlopen(httpbin.url).read() urlopen(httpbin.url).read()
assert cass.play_count == 1 assert cass.play_count == 1
# but if I try to hit the net, raise an exception. # but if I try to hit the net, raise an exception.
with pytest.raises(Exception): with pytest.raises(Exception):
urlopen(httpbin.url + '/get').read() urlopen(httpbin.url + "/get").read()

View File

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

View File

@@ -1,9 +1,9 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
'''Tests for cassettes with custom persistence''' """Tests for cassettes with custom persistence"""
# External imports # External imports
import os import os
from six.moves.urllib.request import urlopen from urllib.request import urlopen
# Internal imports # Internal imports
import vcr import vcr
@@ -11,45 +11,45 @@ from vcr.persisters.filesystem import FilesystemPersister
class CustomFilesystemPersister(object): class CustomFilesystemPersister(object):
'''Behaves just like default FilesystemPersister but adds .test extension """Behaves just like default FilesystemPersister but adds .test extension
to the cassette file''' to the cassette file"""
@staticmethod @staticmethod
def load_cassette(cassette_path, serializer): def load_cassette(cassette_path, serializer):
cassette_path += '.test' cassette_path += ".test"
return FilesystemPersister.load_cassette(cassette_path, serializer) return FilesystemPersister.load_cassette(cassette_path, serializer)
@staticmethod @staticmethod
def save_cassette(cassette_path, cassette_dict, serializer): def save_cassette(cassette_path, cassette_dict, serializer):
cassette_path += '.test' cassette_path += ".test"
FilesystemPersister.save_cassette(cassette_path, cassette_dict, FilesystemPersister.save_cassette(cassette_path, cassette_dict, serializer)
serializer)
def test_save_cassette_with_custom_persister(tmpdir, httpbin): def test_save_cassette_with_custom_persister(tmpdir, httpbin):
'''Ensure you can save a cassette using custom persister''' """Ensure you can save a cassette using custom persister"""
my_vcr = vcr.VCR() my_vcr = vcr.VCR()
my_vcr.register_persister(CustomFilesystemPersister) my_vcr.register_persister(CustomFilesystemPersister)
# Check to make sure directory doesnt exist # Check to make sure directory doesnt exist
assert not os.path.exists(str(tmpdir.join('nonexistent'))) assert not os.path.exists(str(tmpdir.join("nonexistent")))
# Run VCR to create dir and cassette file using new save_cassette callback # Run VCR to create dir and cassette file using new save_cassette callback
with my_vcr.use_cassette(str(tmpdir.join('nonexistent', 'cassette.yml'))): with my_vcr.use_cassette(str(tmpdir.join("nonexistent", "cassette.yml"))):
urlopen(httpbin.url).read() urlopen(httpbin.url).read()
# Callback should have made the file and the directory # Callback should have made the file and the directory
assert os.path.exists(str(tmpdir.join('nonexistent', 'cassette.yml.test'))) assert os.path.exists(str(tmpdir.join("nonexistent", "cassette.yml.test")))
def test_load_cassette_with_custom_persister(tmpdir, httpbin): def test_load_cassette_with_custom_persister(tmpdir, httpbin):
''' """
Ensure you can load a cassette using custom persister Ensure you can load a cassette using custom persister
''' """
my_vcr = vcr.VCR() my_vcr = vcr.VCR()
my_vcr.register_persister(CustomFilesystemPersister) my_vcr.register_persister(CustomFilesystemPersister)
test_fixture = str(tmpdir.join('synopsis.json.test')) test_fixture = str(tmpdir.join("synopsis.json.test"))
with my_vcr.use_cassette(test_fixture, serializer='json'): with my_vcr.use_cassette(test_fixture, serializer="json"):
response = urlopen(httpbin.url).read() response = urlopen(httpbin.url).read()
assert b'difficult sometimes' in response assert b"difficult sometimes" in response

View File

@@ -1,7 +1,7 @@
import vcr import vcr
class MockSerializer(object): class MockSerializer:
def __init__(self): def __init__(self):
self.serialize_count = 0 self.serialize_count = 0
self.deserialize_count = 0 self.deserialize_count = 0
@@ -10,7 +10,7 @@ class MockSerializer(object):
def deserialize(self, cassette_string): def deserialize(self, cassette_string):
self.serialize_count += 1 self.serialize_count += 1
self.cassette_string = cassette_string self.cassette_string = cassette_string
return {'interactions': []} return {"interactions": []}
def serialize(self, cassette_dict): def serialize(self, cassette_dict):
self.deserialize_count += 1 self.deserialize_count += 1
@@ -20,13 +20,13 @@ class MockSerializer(object):
def test_registered_serializer(tmpdir): def test_registered_serializer(tmpdir):
ms = MockSerializer() ms = MockSerializer()
my_vcr = vcr.VCR() my_vcr = vcr.VCR()
my_vcr.register_serializer('mock', ms) my_vcr.register_serializer("mock", ms)
tmpdir.join('test.mock').write('test_data') tmpdir.join("test.mock").write("test_data")
with my_vcr.use_cassette(str(tmpdir.join('test.mock')), serializer='mock'): with my_vcr.use_cassette(str(tmpdir.join("test.mock")), serializer="mock"):
# Serializer deserialized once # Serializer deserialized once
assert ms.serialize_count == 1 assert ms.serialize_count == 1
# and serialized the test data string # and serialized the test data string
assert ms.cassette_string == 'test_data' assert ms.cassette_string == "test_data"
# and hasn't serialized yet # and hasn't serialized yet
assert ms.deserialize_count == 0 assert ms.deserialize_count == 0

View File

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

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
'''Test requests' interaction with vcr''' """Test requests' interaction with vcr"""
import platform import platform
import pytest import pytest
import sys import sys
@@ -11,76 +11,76 @@ from requests.exceptions import ConnectionError # noqa E402
def test_status_code(httpbin_both, tmpdir): def test_status_code(httpbin_both, tmpdir):
'''Ensure that we can read the status code''' """Ensure that we can read the status code"""
url = httpbin_both.url + '/' url = httpbin_both.url + "/"
with vcr.use_cassette(str(tmpdir.join('atts.yaml'))): with vcr.use_cassette(str(tmpdir.join("atts.yaml"))):
status_code = requests.get(url).status_code status_code = requests.get(url).status_code
with vcr.use_cassette(str(tmpdir.join('atts.yaml'))): with vcr.use_cassette(str(tmpdir.join("atts.yaml"))):
assert status_code == requests.get(url).status_code assert status_code == requests.get(url).status_code
def test_headers(httpbin_both, tmpdir): def test_headers(httpbin_both, tmpdir):
'''Ensure that we can read the headers back''' """Ensure that we can read the headers back"""
url = httpbin_both + '/' url = httpbin_both + "/"
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))): with vcr.use_cassette(str(tmpdir.join("headers.yaml"))):
headers = requests.get(url).headers headers = requests.get(url).headers
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))): with vcr.use_cassette(str(tmpdir.join("headers.yaml"))):
assert headers == requests.get(url).headers assert headers == requests.get(url).headers
def test_body(tmpdir, httpbin_both): def test_body(tmpdir, httpbin_both):
'''Ensure the responses are all identical enough''' """Ensure the responses are all identical enough"""
url = httpbin_both + '/bytes/1024' url = httpbin_both + "/bytes/1024"
with vcr.use_cassette(str(tmpdir.join('body.yaml'))): with vcr.use_cassette(str(tmpdir.join("body.yaml"))):
content = requests.get(url).content content = requests.get(url).content
with vcr.use_cassette(str(tmpdir.join('body.yaml'))): with vcr.use_cassette(str(tmpdir.join("body.yaml"))):
assert content == requests.get(url).content assert content == requests.get(url).content
def test_get_empty_content_type_json(tmpdir, httpbin_both): def test_get_empty_content_type_json(tmpdir, httpbin_both):
'''Ensure GET with application/json content-type and empty request body doesn't crash''' """Ensure GET with application/json content-type and empty request body doesn't crash"""
url = httpbin_both + '/status/200' url = httpbin_both + "/status/200"
headers = {'Content-Type': 'application/json'} headers = {"Content-Type": "application/json"}
with vcr.use_cassette(str(tmpdir.join('get_empty_json.yaml')), match_on=('body',)): with vcr.use_cassette(str(tmpdir.join("get_empty_json.yaml")), match_on=("body",)):
status = requests.get(url, headers=headers).status_code status = requests.get(url, headers=headers).status_code
with vcr.use_cassette(str(tmpdir.join('get_empty_json.yaml')), match_on=('body',)): with vcr.use_cassette(str(tmpdir.join("get_empty_json.yaml")), match_on=("body",)):
assert status == requests.get(url, headers=headers).status_code assert status == requests.get(url, headers=headers).status_code
def test_effective_url(tmpdir, httpbin_both): def test_effective_url(tmpdir, httpbin_both):
'''Ensure that the effective_url is captured''' """Ensure that the effective_url is captured"""
url = httpbin_both.url + '/redirect-to?url=/html' url = httpbin_both.url + "/redirect-to?url=/html"
with vcr.use_cassette(str(tmpdir.join('url.yaml'))): with vcr.use_cassette(str(tmpdir.join("url.yaml"))):
effective_url = requests.get(url).url effective_url = requests.get(url).url
assert effective_url == httpbin_both.url + '/html' assert effective_url == httpbin_both.url + "/html"
with vcr.use_cassette(str(tmpdir.join('url.yaml'))): with vcr.use_cassette(str(tmpdir.join("url.yaml"))):
assert effective_url == requests.get(url).url assert effective_url == requests.get(url).url
def test_auth(tmpdir, httpbin_both): def test_auth(tmpdir, httpbin_both):
'''Ensure that we can handle basic auth''' """Ensure that we can handle basic auth"""
auth = ('user', 'passwd') auth = ("user", "passwd")
url = httpbin_both + '/basic-auth/user/passwd' url = httpbin_both + "/basic-auth/user/passwd"
with vcr.use_cassette(str(tmpdir.join('auth.yaml'))): with vcr.use_cassette(str(tmpdir.join("auth.yaml"))):
one = requests.get(url, auth=auth) one = requests.get(url, auth=auth)
with vcr.use_cassette(str(tmpdir.join('auth.yaml'))): with vcr.use_cassette(str(tmpdir.join("auth.yaml"))):
two = requests.get(url, auth=auth) two = requests.get(url, auth=auth)
assert one.content == two.content assert one.content == two.content
assert one.status_code == two.status_code assert one.status_code == two.status_code
def test_auth_failed(tmpdir, httpbin_both): def test_auth_failed(tmpdir, httpbin_both):
'''Ensure that we can save failed auth statuses''' """Ensure that we can save failed auth statuses"""
auth = ('user', 'wrongwrongwrong') auth = ("user", "wrongwrongwrong")
url = httpbin_both + '/basic-auth/user/passwd' url = httpbin_both + "/basic-auth/user/passwd"
with vcr.use_cassette(str(tmpdir.join('auth-failed.yaml'))) as cass: with vcr.use_cassette(str(tmpdir.join("auth-failed.yaml"))) as cass:
# Ensure that this is empty to begin with # Ensure that this is empty to begin with
assert_cassette_empty(cass) assert_cassette_empty(cass)
one = requests.get(url, auth=auth) one = requests.get(url, auth=auth)
@@ -90,58 +90,59 @@ def test_auth_failed(tmpdir, httpbin_both):
def test_post(tmpdir, httpbin_both): def test_post(tmpdir, httpbin_both):
'''Ensure that we can post and cache the results''' """Ensure that we can post and cache the results"""
data = {'key1': 'value1', 'key2': 'value2'} data = {"key1": "value1", "key2": "value2"}
url = httpbin_both + '/post' url = httpbin_both + "/post"
with vcr.use_cassette(str(tmpdir.join('requests.yaml'))): with vcr.use_cassette(str(tmpdir.join("requests.yaml"))):
req1 = requests.post(url, data).content req1 = requests.post(url, data).content
with vcr.use_cassette(str(tmpdir.join('requests.yaml'))): with vcr.use_cassette(str(tmpdir.join("requests.yaml"))):
req2 = requests.post(url, data).content req2 = requests.post(url, data).content
assert req1 == req2 assert req1 == req2
def test_post_chunked_binary(tmpdir, httpbin): def test_post_chunked_binary(tmpdir, httpbin):
'''Ensure that we can send chunked binary without breaking while trying to concatenate bytes with str.''' """Ensure that we can send chunked binary without breaking while trying to concatenate bytes with str."""
data1 = iter([b'data', b'to', b'send']) data1 = iter([b"data", b"to", b"send"])
data2 = iter([b'data', b'to', b'send']) data2 = iter([b"data", b"to", b"send"])
url = httpbin.url + '/post' url = httpbin.url + "/post"
with vcr.use_cassette(str(tmpdir.join('requests.yaml'))): with vcr.use_cassette(str(tmpdir.join("requests.yaml"))):
req1 = requests.post(url, data1).content req1 = requests.post(url, data1).content
with vcr.use_cassette(str(tmpdir.join('requests.yaml'))): with vcr.use_cassette(str(tmpdir.join("requests.yaml"))):
req2 = requests.post(url, data2).content req2 = requests.post(url, data2).content
assert req1 == req2 assert req1 == req2
@pytest.mark.xskip('sys.version_info >= (3, 6)', strict=True, raises=ConnectionError) @pytest.mark.skipif("sys.version_info >= (3, 6)", strict=True, raises=ConnectionError)
@pytest.mark.xskip((3, 5) < sys.version_info < (3, 6) and @pytest.mark.skipif(
platform.python_implementation() == 'CPython', (3, 5) < sys.version_info < (3, 6) and platform.python_implementation() == "CPython",
reason='Fails on CPython 3.5') reason="Fails on CPython 3.5",
)
def test_post_chunked_binary_secure(tmpdir, httpbin_secure): def test_post_chunked_binary_secure(tmpdir, httpbin_secure):
'''Ensure that we can send chunked binary without breaking while trying to concatenate bytes with str.''' """Ensure that we can send chunked binary without breaking while trying to concatenate bytes with str."""
data1 = iter([b'data', b'to', b'send']) data1 = iter([b"data", b"to", b"send"])
data2 = iter([b'data', b'to', b'send']) data2 = iter([b"data", b"to", b"send"])
url = httpbin_secure.url + '/post' url = httpbin_secure.url + "/post"
with vcr.use_cassette(str(tmpdir.join('requests.yaml'))): with vcr.use_cassette(str(tmpdir.join("requests.yaml"))):
req1 = requests.post(url, data1).content req1 = requests.post(url, data1).content
print(req1) print(req1)
with vcr.use_cassette(str(tmpdir.join('requests.yaml'))): with vcr.use_cassette(str(tmpdir.join("requests.yaml"))):
req2 = requests.post(url, data2).content req2 = requests.post(url, data2).content
assert req1 == req2 assert req1 == req2
def test_redirects(tmpdir, httpbin_both): def test_redirects(tmpdir, httpbin_both):
'''Ensure that we can handle redirects''' """Ensure that we can handle redirects"""
url = httpbin_both + '/redirect-to?url=bytes/1024' url = httpbin_both + "/redirect-to?url=bytes/1024"
with vcr.use_cassette(str(tmpdir.join('requests.yaml'))): with vcr.use_cassette(str(tmpdir.join("requests.yaml"))):
content = requests.get(url).content 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 assert content == requests.get(url).content
# Ensure that we've now cached *two* responses. One for the redirect # Ensure that we've now cached *two* responses. One for the redirect
# and one for the final fetch # and one for the final fetch
@@ -150,135 +151,151 @@ def test_redirects(tmpdir, httpbin_both):
def test_cross_scheme(tmpdir, httpbin_secure, httpbin): def test_cross_scheme(tmpdir, httpbin_secure, httpbin):
'''Ensure that requests between schemes are treated separately''' """Ensure that requests between schemes are treated separately"""
# First fetch a url under http, and then again under https and then # 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 # ensure that we haven't served anything out of cache, and we have two
# requests / response pairs in the cassette # requests / response pairs in the cassette
with vcr.use_cassette(str(tmpdir.join('cross_scheme.yaml'))) as cass: with vcr.use_cassette(str(tmpdir.join("cross_scheme.yaml"))) as cass:
requests.get(httpbin_secure + '/') requests.get(httpbin_secure + "/")
requests.get(httpbin + '/') requests.get(httpbin + "/")
assert cass.play_count == 0 assert cass.play_count == 0
assert len(cass) == 2 assert len(cass) == 2
def test_gzip(tmpdir, httpbin_both): def test_gzip(tmpdir, httpbin_both):
''' """
Ensure that requests (actually urllib3) is able to automatically decompress Ensure that requests (actually urllib3) is able to automatically decompress
the response body the response body
''' """
url = httpbin_both + '/gzip' url = httpbin_both + "/gzip"
response = requests.get(url) response = requests.get(url)
with vcr.use_cassette(str(tmpdir.join('gzip.yaml'))): with vcr.use_cassette(str(tmpdir.join("gzip.yaml"))):
response = requests.get(url) response = requests.get(url)
assert_is_json(response.content) assert_is_json(response.content)
with vcr.use_cassette(str(tmpdir.join('gzip.yaml'))): with vcr.use_cassette(str(tmpdir.join("gzip.yaml"))):
assert_is_json(response.content) assert_is_json(response.content)
def test_session_and_connection_close(tmpdir, httpbin): def test_session_and_connection_close(tmpdir, httpbin):
''' """
This tests the issue in https://github.com/kevin1024/vcrpy/issues/48 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 If you use a requests.session and the connection is closed, then an
exception is raised in the urllib3 module vendored into requests: exception is raised in the urllib3 module vendored into requests:
`AttributeError: 'NoneType' object has no attribute 'settimeout'` `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() session = requests.session()
session.get(httpbin + '/get', headers={'Connection': 'close'}) session.get(httpbin + "/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): def test_https_with_cert_validation_disabled(tmpdir, httpbin_secure):
with vcr.use_cassette(str(tmpdir.join('cert_validation_disabled.yaml'))): with vcr.use_cassette(str(tmpdir.join("cert_validation_disabled.yaml"))):
requests.get(httpbin_secure.url, verify=False) requests.get(httpbin_secure.url, verify=False)
def test_session_can_make_requests_after_requests_unpatched(tmpdir, httpbin): def test_session_can_make_requests_after_requests_unpatched(tmpdir, httpbin):
with vcr.use_cassette(str(tmpdir.join('test_session_after_unpatched.yaml'))): with vcr.use_cassette(str(tmpdir.join("test_session_after_unpatched.yaml"))):
session = requests.session() session = requests.session()
session.get(httpbin + '/get') session.get(httpbin + "/get")
with vcr.use_cassette(str(tmpdir.join('test_session_after_unpatched.yaml'))): with vcr.use_cassette(str(tmpdir.join("test_session_after_unpatched.yaml"))):
session = requests.session() session = requests.session()
session.get(httpbin + '/get') session.get(httpbin + "/get")
session.get(httpbin + '/status/200') session.get(httpbin + "/status/200")
def test_session_created_before_use_cassette_is_patched(tmpdir, httpbin_both): def test_session_created_before_use_cassette_is_patched(tmpdir, httpbin_both):
url = httpbin_both + '/bytes/1024' url = httpbin_both + "/bytes/1024"
# Record arbitrary, random data to the cassette # Record arbitrary, random data to the cassette
with vcr.use_cassette(str(tmpdir.join('session_created_outside.yaml'))): with vcr.use_cassette(str(tmpdir.join("session_created_outside.yaml"))):
session = requests.session() session = requests.session()
body = session.get(url).content body = session.get(url).content
# Create a session outside of any cassette context manager # Create a session outside of any cassette context manager
session = requests.session() session = requests.session()
# Make a request to make sure that a connectionpool is instantiated # Make a request to make sure that a connectionpool is instantiated
session.get(httpbin_both + '/get') session.get(httpbin_both + "/get")
with vcr.use_cassette(str(tmpdir.join('session_created_outside.yaml'))): with vcr.use_cassette(str(tmpdir.join("session_created_outside.yaml"))):
# These should only be the same if the patching succeeded. # These should only be the same if the patching succeeded.
assert session.get(url).content == body assert session.get(url).content == body
def test_nested_cassettes_with_session_created_before_nesting(httpbin_both, tmpdir): 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 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 active is patched to the use the responses of a second cassette when it
is enabled. is enabled.
''' """
url = httpbin_both + '/bytes/1024' url = httpbin_both + "/bytes/1024"
with vcr.use_cassette(str(tmpdir.join('first_nested.yaml'))): with vcr.use_cassette(str(tmpdir.join("first_nested.yaml"))):
session = requests.session() session = requests.session()
first_body = session.get(url).content first_body = session.get(url).content
with vcr.use_cassette(str(tmpdir.join('second_nested.yaml'))): with vcr.use_cassette(str(tmpdir.join("second_nested.yaml"))):
second_body = session.get(url).content second_body = session.get(url).content
third_body = requests.get(url).content third_body = requests.get(url).content
with vcr.use_cassette(str(tmpdir.join('second_nested.yaml'))): with vcr.use_cassette(str(tmpdir.join("second_nested.yaml"))):
session = requests.session() session = requests.session()
assert session.get(url).content == second_body assert session.get(url).content == second_body
with vcr.use_cassette(str(tmpdir.join('first_nested.yaml'))): with vcr.use_cassette(str(tmpdir.join("first_nested.yaml"))):
assert session.get(url).content == first_body assert session.get(url).content == first_body
assert session.get(url).content == third_body assert session.get(url).content == third_body
# Make sure that the session can now get content normally. # Make sure that the session can now get content normally.
assert 'User-agent' in session.get(httpbin_both.url + '/robots.txt').text assert "User-agent" in session.get(httpbin_both.url + "/robots.txt").text
def test_post_file(tmpdir, httpbin_both): def test_post_file(tmpdir, httpbin_both):
'''Ensure that we handle posting a file.''' """Ensure that we handle posting a file."""
url = httpbin_both + '/post' url = httpbin_both + "/post"
with vcr.use_cassette(str(tmpdir.join('post_file.yaml'))) as cass, open('tox.ini') as f: with vcr.use_cassette(str(tmpdir.join("post_file.yaml"))) as cass, open("tox.ini", "rb") as f:
original_response = requests.post(url, f).content original_response = requests.post(url, f).content
# This also tests that we do the right thing with matching the body when they are files. # 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')), with vcr.use_cassette(
match_on=('method', 'scheme', 'host', 'port', 'path', 'query', 'body')) as cass: str(tmpdir.join("post_file.yaml")),
with open('tox.ini', 'rb') as f: match_on=("method", "scheme", "host", "port", "path", "query", "body"),
) as cass:
with open("tox.ini", "rb") as f:
tox_content = f.read() tox_content = f.read()
assert cass.requests[0].body.read() == tox_content assert cass.requests[0].body.read() == tox_content
with open('tox.ini', 'rb') as f: with open("tox.ini", "rb") as f:
new_response = requests.post(url, f).content new_response = requests.post(url, f).content
assert original_response == new_response assert original_response == new_response
def test_filter_post_params(tmpdir, httpbin_both): def test_filter_post_params(tmpdir, httpbin_both):
''' """
This tests the issue in https://github.com/kevin1024/vcrpy/issues/158 This tests the issue in https://github.com/kevin1024/vcrpy/issues/158
Ensure that a post request made through requests can still be filtered. 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: with vcr.use_cassette(cass_file, filter_post_data_parameters=['id']) as cass:
assert b'id=secret' not in cass.requests[0].body assert b'id=secret' not in cass.requests[0].body
''' """
url = httpbin_both.url + '/post' url = httpbin_both.url + "/post"
cass_loc = str(tmpdir.join('filter_post_params.yaml')) cass_loc = str(tmpdir.join("filter_post_params.yaml"))
with vcr.use_cassette(cass_loc, filter_post_data_parameters=['key']) as cass: with vcr.use_cassette(cass_loc, filter_post_data_parameters=["key"]) as cass:
requests.post(url, data={'key': 'value'}) requests.post(url, data={"key": "value"})
with vcr.use_cassette(cass_loc, filter_post_data_parameters=['key']) as cass: with vcr.use_cassette(cass_loc, filter_post_data_parameters=["key"]) as cass:
assert b'key=value' not in cass.requests[0].body 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

@@ -1,24 +1,25 @@
import vcr import vcr
import zlib import zlib
import six.moves.http_client as httplib import json
import http.client as httplib
from assertions import assert_is_json from assertions import assert_is_json
def _headers_are_case_insensitive(host, port): def _headers_are_case_insensitive(host, port):
conn = httplib.HTTPConnection(host, port) conn = httplib.HTTPConnection(host, port)
conn.request('GET', "/cookies/set?k1=v1") conn.request("GET", "/cookies/set?k1=v1")
r1 = conn.getresponse() r1 = conn.getresponse()
cookie_data1 = r1.getheader('set-cookie') cookie_data1 = r1.getheader("set-cookie")
conn = httplib.HTTPConnection(host, port) conn = httplib.HTTPConnection(host, port)
conn.request('GET', "/cookies/set?k1=v1") conn.request("GET", "/cookies/set?k1=v1")
r2 = conn.getresponse() r2 = conn.getresponse()
cookie_data2 = r2.getheader('Set-Cookie') cookie_data2 = r2.getheader("Set-Cookie")
return cookie_data1 == cookie_data2 return cookie_data1 == cookie_data2
def test_case_insensitivity(tmpdir, httpbin): def test_case_insensitivity(tmpdir, httpbin):
testfile = str(tmpdir.join('case_insensitivity.yml')) testfile = str(tmpdir.join("case_insensitivity.yml"))
# check if headers are case insensitive outside of vcrpy # check if headers are case insensitive outside of vcrpy
host, port = httpbin.host, httpbin.port host, port = httpbin.host, httpbin.port
outside = _headers_are_case_insensitive(host, port) outside = _headers_are_case_insensitive(host, port)
@@ -34,13 +35,13 @@ def test_case_insensitivity(tmpdir, httpbin):
def _multiple_header_value(httpbin): def _multiple_header_value(httpbin):
conn = httplib.HTTPConnection(httpbin.host, httpbin.port) conn = httplib.HTTPConnection(httpbin.host, httpbin.port)
conn.request('GET', "/response-headers?foo=bar&foo=baz") conn.request("GET", "/response-headers?foo=bar&foo=baz")
r = conn.getresponse() r = conn.getresponse()
return r.getheader('foo') return r.getheader("foo")
def test_multiple_headers(tmpdir, httpbin): def test_multiple_headers(tmpdir, httpbin):
testfile = str(tmpdir.join('multiple_headers.yaml')) testfile = str(tmpdir.join("multiple_headers.yaml"))
outside = _multiple_header_value(httpbin) outside = _multiple_header_value(httpbin)
with vcr.use_cassette(testfile): with vcr.use_cassette(testfile):
@@ -50,36 +51,84 @@ def test_multiple_headers(tmpdir, httpbin):
def test_original_decoded_response_is_not_modified(tmpdir, httpbin): def test_original_decoded_response_is_not_modified(tmpdir, httpbin):
testfile = str(tmpdir.join('decoded_response.yml')) testfile = str(tmpdir.join("decoded_response.yml"))
host, port = httpbin.host, httpbin.port host, port = httpbin.host, httpbin.port
conn = httplib.HTTPConnection(host, port) conn = httplib.HTTPConnection(host, port)
conn.request('GET', '/gzip') conn.request("GET", "/gzip")
outside = conn.getresponse() outside = conn.getresponse()
with vcr.use_cassette(testfile, decode_compressed_response=True): with vcr.use_cassette(testfile, decode_compressed_response=True):
conn = httplib.HTTPConnection(host, port) conn = httplib.HTTPConnection(host, port)
conn.request('GET', '/gzip') conn.request("GET", "/gzip")
inside = conn.getresponse() inside = conn.getresponse()
# Assert that we do not modify the original response while appending # Assert that we do not modify the original response while appending
# to the casssette. # to the casssette.
assert 'gzip' == inside.headers['content-encoding'] assert "gzip" == inside.headers["content-encoding"]
# They should effectively be the same response. # They should effectively be the same response.
inside_headers = (h for h in inside.headers.items() if h[0].lower() != 'date') 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') outside_headers = (h for h in outside.getheaders() if h[0].lower() != "date")
assert set(inside_headers) == set(outside_headers) assert set(inside_headers) == set(outside_headers)
inside = zlib.decompress(inside.read(), 16+zlib.MAX_WBITS) inside = zlib.decompress(inside.read(), 16 + zlib.MAX_WBITS)
outside = zlib.decompress(outside.read(), 16+zlib.MAX_WBITS) outside = zlib.decompress(outside.read(), 16 + zlib.MAX_WBITS)
assert inside == outside assert inside == outside
# Even though the above are raw bytes, the JSON data should have been # Even though the above are raw bytes, the JSON data should have been
# decoded and saved to the cassette. # decoded and saved to the cassette.
with vcr.use_cassette(testfile): with vcr.use_cassette(testfile):
conn = httplib.HTTPConnection(host, port) conn = httplib.HTTPConnection(host, port)
conn.request('GET', '/gzip') conn.request("GET", "/gzip")
inside = conn.getresponse() inside = conn.getresponse()
assert 'content-encoding' not in inside.headers assert "content-encoding" not in inside.headers
assert_is_json(inside.read()) assert_is_json(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().decode("utf-8"))
outside_body = json.loads(outside.read().decode("utf-8"))
assert not 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().decode("utf-8"))
assert inside_body[field_to_scrub] == replacement

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
'''Test requests' interaction with vcr''' """Test requests' interaction with vcr"""
import json import json
@@ -17,105 +17,99 @@ http = pytest.importorskip("tornado.httpclient")
supports_raise_error = tornado.version_info >= (4,) supports_raise_error = tornado.version_info >= (4,)
@pytest.fixture(params=['simple', 'curl', 'default']) @pytest.fixture(params=["simple", "curl", "default"])
def get_client(request): def get_client(request):
if request.param == 'simple': if request.param == "simple":
from tornado import simple_httpclient as simple from tornado import simple_httpclient as simple
return (lambda: simple.SimpleAsyncHTTPClient())
elif request.param == 'curl': return lambda: simple.SimpleAsyncHTTPClient()
elif request.param == "curl":
curl = pytest.importorskip("tornado.curl_httpclient") curl = pytest.importorskip("tornado.curl_httpclient")
return (lambda: curl.CurlAsyncHTTPClient()) return lambda: curl.CurlAsyncHTTPClient()
else: else:
return (lambda: http.AsyncHTTPClient()) return lambda: http.AsyncHTTPClient()
def get(client, url, **kwargs): def get(client, url, **kwargs):
fetch_kwargs = {} fetch_kwargs = {}
if supports_raise_error: if supports_raise_error:
fetch_kwargs['raise_error'] = kwargs.pop('raise_error', True) fetch_kwargs["raise_error"] = kwargs.pop("raise_error", True)
return client.fetch( return client.fetch(http.HTTPRequest(url, method="GET", **kwargs), **fetch_kwargs)
http.HTTPRequest(url, method='GET', **kwargs),
**fetch_kwargs
)
def post(client, url, data=None, **kwargs): def post(client, url, data=None, **kwargs):
if data: if data:
kwargs['body'] = json.dumps(data) kwargs["body"] = json.dumps(data)
return client.fetch(http.HTTPRequest(url, method='POST', **kwargs)) return client.fetch(http.HTTPRequest(url, method="POST", **kwargs))
@pytest.fixture(params=["https", "http"]) @pytest.fixture(params=["https", "http"])
def scheme(request): def scheme(request):
'''Fixture that returns both http and https.''' """Fixture that returns both http and https."""
return request.param return request.param
@pytest.mark.gen_test @pytest.mark.gen_test
def test_status_code(get_client, scheme, tmpdir): def test_status_code(get_client, scheme, tmpdir):
'''Ensure that we can read the status code''' """Ensure that we can read the status code"""
url = scheme + '://httpbin.org/' url = scheme + "://httpbin.org/"
with vcr.use_cassette(str(tmpdir.join('atts.yaml'))): with vcr.use_cassette(str(tmpdir.join("atts.yaml"))):
status_code = (yield get(get_client(), url)).code status_code = (yield get(get_client(), url)).code
with vcr.use_cassette(str(tmpdir.join('atts.yaml'))) as cass: with vcr.use_cassette(str(tmpdir.join("atts.yaml"))) as cass:
assert status_code == (yield get(get_client(), url)).code assert status_code == (yield get(get_client(), url)).code
assert 1 == cass.play_count assert 1 == cass.play_count
@pytest.mark.gen_test @pytest.mark.gen_test
def test_headers(get_client, scheme, tmpdir): def test_headers(get_client, scheme, tmpdir):
'''Ensure that we can read the headers back''' """Ensure that we can read the headers back"""
url = scheme + '://httpbin.org/' url = scheme + "://httpbin.org/"
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))): with vcr.use_cassette(str(tmpdir.join("headers.yaml"))):
headers = (yield get(get_client(), url)).headers headers = (yield get(get_client(), url)).headers
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))) as cass: with vcr.use_cassette(str(tmpdir.join("headers.yaml"))) as cass:
assert headers == (yield get(get_client(), url)).headers assert headers == (yield get(get_client(), url)).headers
assert 1 == cass.play_count assert 1 == cass.play_count
@pytest.mark.gen_test @pytest.mark.gen_test
def test_body(get_client, tmpdir, scheme): def test_body(get_client, tmpdir, scheme):
'''Ensure the responses are all identical enough''' """Ensure the responses are all identical enough"""
url = scheme + '://httpbin.org/bytes/1024' url = scheme + "://httpbin.org/bytes/1024"
with vcr.use_cassette(str(tmpdir.join('body.yaml'))): with vcr.use_cassette(str(tmpdir.join("body.yaml"))):
content = (yield get(get_client(), url)).body content = (yield get(get_client(), url)).body
with vcr.use_cassette(str(tmpdir.join('body.yaml'))) as cass: with vcr.use_cassette(str(tmpdir.join("body.yaml"))) as cass:
assert content == (yield get(get_client(), url)).body assert content == (yield get(get_client(), url)).body
assert 1 == cass.play_count assert 1 == cass.play_count
@pytest.mark.gen_test @pytest.mark.gen_test
def test_effective_url(get_client, scheme, tmpdir): def test_effective_url(get_client, scheme, tmpdir):
'''Ensure that the effective_url is captured''' """Ensure that the effective_url is captured"""
url = scheme + '://httpbin.org/redirect-to?url=/html' url = scheme + "://httpbin.org/redirect-to?url=/html"
with vcr.use_cassette(str(tmpdir.join('url.yaml'))): with vcr.use_cassette(str(tmpdir.join("url.yaml"))):
effective_url = (yield get(get_client(), url)).effective_url effective_url = (yield get(get_client(), url)).effective_url
assert effective_url == scheme + '://httpbin.org/html' assert effective_url == scheme + "://httpbin.org/html"
with vcr.use_cassette(str(tmpdir.join('url.yaml'))) as cass: with vcr.use_cassette(str(tmpdir.join("url.yaml"))) as cass:
assert effective_url == (yield get(get_client(), url)).effective_url assert effective_url == (yield get(get_client(), url)).effective_url
assert 1 == cass.play_count assert 1 == cass.play_count
@pytest.mark.gen_test @pytest.mark.gen_test
def test_auth(get_client, tmpdir, scheme): def test_auth(get_client, tmpdir, scheme):
'''Ensure that we can handle basic auth''' """Ensure that we can handle basic auth"""
auth = ('user', 'passwd') auth = ("user", "passwd")
url = scheme + '://httpbin.org/basic-auth/user/passwd' url = scheme + "://httpbin.org/basic-auth/user/passwd"
with vcr.use_cassette(str(tmpdir.join('auth.yaml'))): with vcr.use_cassette(str(tmpdir.join("auth.yaml"))):
one = yield get( one = yield get(get_client(), url, auth_username=auth[0], auth_password=auth[1])
get_client(), url, auth_username=auth[0], auth_password=auth[1]
)
with vcr.use_cassette(str(tmpdir.join('auth.yaml'))) as cass: with vcr.use_cassette(str(tmpdir.join("auth.yaml"))) as cass:
two = yield get( two = yield get(get_client(), url, auth_username=auth[0], auth_password=auth[1])
get_client(), url, auth_username=auth[0], auth_password=auth[1]
)
assert one.body == two.body assert one.body == two.body
assert one.code == two.code assert one.code == two.code
assert 1 == cass.play_count assert 1 == cass.play_count
@@ -123,30 +117,20 @@ def test_auth(get_client, tmpdir, scheme):
@pytest.mark.gen_test @pytest.mark.gen_test
def test_auth_failed(get_client, tmpdir, scheme): def test_auth_failed(get_client, tmpdir, scheme):
'''Ensure that we can save failed auth statuses''' """Ensure that we can save failed auth statuses"""
auth = ('user', 'wrongwrongwrong') auth = ("user", "wrongwrongwrong")
url = scheme + '://httpbin.org/basic-auth/user/passwd' url = scheme + "://httpbin.org/basic-auth/user/passwd"
with vcr.use_cassette(str(tmpdir.join('auth-failed.yaml'))) as cass: with vcr.use_cassette(str(tmpdir.join("auth-failed.yaml"))) as cass:
# Ensure that this is empty to begin with # Ensure that this is empty to begin with
assert_cassette_empty(cass) assert_cassette_empty(cass)
with pytest.raises(http.HTTPError) as exc_info: with pytest.raises(http.HTTPError) as exc_info:
yield get( yield get(get_client(), url, auth_username=auth[0], auth_password=auth[1])
get_client(),
url,
auth_username=auth[0],
auth_password=auth[1],
)
one = exc_info.value.response one = exc_info.value.response
assert exc_info.value.code == 401 assert exc_info.value.code == 401
with vcr.use_cassette(str(tmpdir.join('auth-failed.yaml'))) as cass: with vcr.use_cassette(str(tmpdir.join("auth-failed.yaml"))) as cass:
with pytest.raises(http.HTTPError) as exc_info: with pytest.raises(http.HTTPError) as exc_info:
two = yield get( two = yield get(get_client(), url, auth_username=auth[0], auth_password=auth[1])
get_client(),
url,
auth_username=auth[0],
auth_password=auth[1],
)
two = exc_info.value.response two = exc_info.value.response
assert exc_info.value.code == 401 assert exc_info.value.code == 401
assert one.body == two.body assert one.body == two.body
@@ -156,13 +140,13 @@ def test_auth_failed(get_client, tmpdir, scheme):
@pytest.mark.gen_test @pytest.mark.gen_test
def test_post(get_client, tmpdir, scheme): def test_post(get_client, tmpdir, scheme):
'''Ensure that we can post and cache the results''' """Ensure that we can post and cache the results"""
data = {'key1': 'value1', 'key2': 'value2'} data = {"key1": "value1", "key2": "value2"}
url = scheme + '://httpbin.org/post' url = scheme + "://httpbin.org/post"
with vcr.use_cassette(str(tmpdir.join('requests.yaml'))): with vcr.use_cassette(str(tmpdir.join("requests.yaml"))):
req1 = (yield post(get_client(), url, data)).body req1 = (yield post(get_client(), url, data)).body
with vcr.use_cassette(str(tmpdir.join('requests.yaml'))) as cass: with vcr.use_cassette(str(tmpdir.join("requests.yaml"))) as cass:
req2 = (yield post(get_client(), url, data)).body req2 = (yield post(get_client(), url, data)).body
assert req1 == req2 assert req1 == req2
@@ -171,55 +155,55 @@ def test_post(get_client, tmpdir, scheme):
@pytest.mark.gen_test @pytest.mark.gen_test
def test_redirects(get_client, tmpdir, scheme): def test_redirects(get_client, tmpdir, scheme):
'''Ensure that we can handle redirects''' """Ensure that we can handle redirects"""
url = scheme + '://httpbin.org/redirect-to?url=bytes/1024' url = scheme + "://httpbin.org/redirect-to?url=bytes/1024"
with vcr.use_cassette(str(tmpdir.join('requests.yaml'))): with vcr.use_cassette(str(tmpdir.join("requests.yaml"))):
content = (yield get(get_client(), url)).body content = (yield get(get_client(), url)).body
with vcr.use_cassette(str(tmpdir.join('requests.yaml'))) as cass: with vcr.use_cassette(str(tmpdir.join("requests.yaml"))) as cass:
assert content == (yield get(get_client(), url)).body assert content == (yield get(get_client(), url)).body
assert cass.play_count == 1 assert cass.play_count == 1
@pytest.mark.gen_test @pytest.mark.gen_test
def test_cross_scheme(get_client, tmpdir, scheme): def test_cross_scheme(get_client, tmpdir, scheme):
'''Ensure that requests between schemes are treated separately''' """Ensure that requests between schemes are treated separately"""
# First fetch a url under http, and then again under https and then # 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 # ensure that we haven't served anything out of cache, and we have two
# requests / response pairs in the cassette # requests / response pairs in the cassette
with vcr.use_cassette(str(tmpdir.join('cross_scheme.yaml'))) as cass: with vcr.use_cassette(str(tmpdir.join("cross_scheme.yaml"))) as cass:
yield get(get_client(), 'https://httpbin.org/') yield get(get_client(), "https://httpbin.org/")
yield get(get_client(), 'http://httpbin.org/') yield get(get_client(), "http://httpbin.org/")
assert cass.play_count == 0 assert cass.play_count == 0
assert len(cass) == 2 assert len(cass) == 2
# Then repeat the same requests and ensure both were replayed. # Then repeat the same requests and ensure both were replayed.
with vcr.use_cassette(str(tmpdir.join('cross_scheme.yaml'))) as cass: with vcr.use_cassette(str(tmpdir.join("cross_scheme.yaml"))) as cass:
yield get(get_client(), 'https://httpbin.org/') yield get(get_client(), "https://httpbin.org/")
yield get(get_client(), 'http://httpbin.org/') yield get(get_client(), "http://httpbin.org/")
assert cass.play_count == 2 assert cass.play_count == 2
@pytest.mark.gen_test @pytest.mark.gen_test
def test_gzip(get_client, tmpdir, scheme): def test_gzip(get_client, tmpdir, scheme):
''' """
Ensure that httpclient is able to automatically decompress the response Ensure that httpclient is able to automatically decompress the response
body body
''' """
url = scheme + '://httpbin.org/gzip' url = scheme + "://httpbin.org/gzip"
# use_gzip was renamed to decompress_response in 4.0 # use_gzip was renamed to decompress_response in 4.0
kwargs = {} kwargs = {}
if tornado.version_info < (4,): if tornado.version_info < (4,):
kwargs['use_gzip'] = True kwargs["use_gzip"] = True
else: else:
kwargs['decompress_response'] = True kwargs["decompress_response"] = True
with vcr.use_cassette(str(tmpdir.join('gzip.yaml'))): with vcr.use_cassette(str(tmpdir.join("gzip.yaml"))):
response = yield get(get_client(), url, **kwargs) response = yield get(get_client(), url, **kwargs)
assert_is_json(response.body) assert_is_json(response.body)
with vcr.use_cassette(str(tmpdir.join('gzip.yaml'))) as cass: with vcr.use_cassette(str(tmpdir.join("gzip.yaml"))) as cass:
response = yield get(get_client(), url, **kwargs) response = yield get(get_client(), url, **kwargs)
assert_is_json(response.body) assert_is_json(response.body)
assert 1 == cass.play_count assert 1 == cass.play_count
@@ -227,28 +211,26 @@ def test_gzip(get_client, tmpdir, scheme):
@pytest.mark.gen_test @pytest.mark.gen_test
def test_https_with_cert_validation_disabled(get_client, tmpdir): def test_https_with_cert_validation_disabled(get_client, tmpdir):
cass_path = str(tmpdir.join('cert_validation_disabled.yaml')) cass_path = str(tmpdir.join("cert_validation_disabled.yaml"))
with vcr.use_cassette(cass_path): with vcr.use_cassette(cass_path):
yield get(get_client(), 'https://httpbin.org', validate_cert=False) yield get(get_client(), "https://httpbin.org", validate_cert=False)
with vcr.use_cassette(cass_path) as cass: with vcr.use_cassette(cass_path) as cass:
yield get(get_client(), 'https://httpbin.org', validate_cert=False) yield get(get_client(), "https://httpbin.org", validate_cert=False)
assert 1 == cass.play_count assert 1 == cass.play_count
@pytest.mark.gen_test @pytest.mark.gen_test
def test_unsupported_features_raises_in_future(get_client, tmpdir): def test_unsupported_features_raises_in_future(get_client, tmpdir):
'''Ensure that the exception for an AsyncHTTPClient feature not being """Ensure that the exception for an AsyncHTTPClient feature not being
supported is raised inside the future.''' supported is raised inside the future."""
def callback(chunk): def callback(chunk):
assert False, "Did not expect to be called." assert False, "Did not expect to be called."
with vcr.use_cassette(str(tmpdir.join('invalid.yaml'))): with vcr.use_cassette(str(tmpdir.join("invalid.yaml"))):
future = get( future = get(get_client(), "http://httpbin.org", streaming_callback=callback)
get_client(), 'http://httpbin.org', streaming_callback=callback
)
with pytest.raises(Exception) as excinfo: with pytest.raises(Exception) as excinfo:
yield future yield future
@@ -256,24 +238,18 @@ def test_unsupported_features_raises_in_future(get_client, tmpdir):
assert "not yet supported by VCR" in str(excinfo) assert "not yet supported by VCR" in str(excinfo)
@pytest.mark.skipif( @pytest.mark.skipif(not supports_raise_error, reason="raise_error unavailable in tornado <= 3")
not supports_raise_error,
reason='raise_error unavailable in tornado <= 3',
)
@pytest.mark.gen_test @pytest.mark.gen_test
def test_unsupported_features_raise_error_disabled(get_client, tmpdir): def test_unsupported_features_raise_error_disabled(get_client, tmpdir):
'''Ensure that the exception for an AsyncHTTPClient feature not being """Ensure that the exception for an AsyncHTTPClient feature not being
supported is not raised if raise_error=False.''' supported is not raised if raise_error=False."""
def callback(chunk): def callback(chunk):
assert False, "Did not expect to be called." assert False, "Did not expect to be called."
with vcr.use_cassette(str(tmpdir.join('invalid.yaml'))): with vcr.use_cassette(str(tmpdir.join("invalid.yaml"))):
response = yield get( response = yield get(
get_client(), get_client(), "http://httpbin.org", streaming_callback=callback, raise_error=False
'http://httpbin.org',
streaming_callback=callback,
raise_error=False,
) )
assert "not yet supported by VCR" in str(response.error) assert "not yet supported by VCR" in str(response.error)
@@ -281,60 +257,51 @@ def test_unsupported_features_raise_error_disabled(get_client, tmpdir):
@pytest.mark.gen_test @pytest.mark.gen_test
def test_cannot_overwrite_cassette_raises_in_future(get_client, tmpdir): def test_cannot_overwrite_cassette_raises_in_future(get_client, tmpdir):
'''Ensure that CannotOverwriteExistingCassetteException is raised inside """Ensure that CannotOverwriteExistingCassetteException is raised inside
the future.''' the future."""
with vcr.use_cassette(str(tmpdir.join('overwrite.yaml'))): with vcr.use_cassette(str(tmpdir.join("overwrite.yaml"))):
yield get(get_client(), 'http://httpbin.org/get') yield get(get_client(), "http://httpbin.org/get")
with vcr.use_cassette(str(tmpdir.join('overwrite.yaml'))): with vcr.use_cassette(str(tmpdir.join("overwrite.yaml"))):
future = get(get_client(), 'http://httpbin.org/headers') future = get(get_client(), "http://httpbin.org/headers")
with pytest.raises(CannotOverwriteExistingCassetteException): with pytest.raises(CannotOverwriteExistingCassetteException):
yield future yield future
@pytest.mark.skipif( @pytest.mark.skipif(not supports_raise_error, reason="raise_error unavailable in tornado <= 3")
not supports_raise_error,
reason='raise_error unavailable in tornado <= 3',
)
@pytest.mark.gen_test @pytest.mark.gen_test
def test_cannot_overwrite_cassette_raise_error_disabled(get_client, tmpdir): def test_cannot_overwrite_cassette_raise_error_disabled(get_client, tmpdir):
'''Ensure that CannotOverwriteExistingCassetteException is not raised if """Ensure that CannotOverwriteExistingCassetteException is not raised if
raise_error=False in the fetch() call.''' raise_error=False in the fetch() call."""
with vcr.use_cassette(str(tmpdir.join('overwrite.yaml'))): with vcr.use_cassette(str(tmpdir.join("overwrite.yaml"))):
yield get( yield get(get_client(), "http://httpbin.org/get", raise_error=False)
get_client(), 'http://httpbin.org/get', raise_error=False
)
with vcr.use_cassette(str(tmpdir.join('overwrite.yaml'))): with vcr.use_cassette(str(tmpdir.join("overwrite.yaml"))):
response = yield get( response = yield get(get_client(), "http://httpbin.org/headers", raise_error=False)
get_client(), 'http://httpbin.org/headers', raise_error=False
)
assert isinstance(response.error, CannotOverwriteExistingCassetteException) assert isinstance(response.error, CannotOverwriteExistingCassetteException)
@pytest.mark.gen_test @pytest.mark.gen_test
@vcr.use_cassette(path_transformer=vcr.default_vcr.ensure_suffix('.yaml')) @vcr.use_cassette(path_transformer=vcr.default_vcr.ensure_suffix(".yaml"))
def test_tornado_with_decorator_use_cassette(get_client): def test_tornado_with_decorator_use_cassette(get_client):
response = yield get_client().fetch( response = yield get_client().fetch(http.HTTPRequest("http://www.google.com/", method="GET"))
http.HTTPRequest('http://www.google.com/', method='GET') assert response.body.decode("utf-8") == "not actually google"
)
assert response.body.decode('utf-8') == "not actually google"
@pytest.mark.gen_test @pytest.mark.gen_test
@vcr.use_cassette(path_transformer=vcr.default_vcr.ensure_suffix('.yaml')) @vcr.use_cassette(path_transformer=vcr.default_vcr.ensure_suffix(".yaml"))
def test_tornado_exception_can_be_caught(get_client): def test_tornado_exception_can_be_caught(get_client):
try: try:
yield get(get_client(), 'http://httpbin.org/status/500') yield get(get_client(), "http://httpbin.org/status/500")
except http.HTTPError as e: except http.HTTPError as e:
assert e.code == 500 assert e.code == 500
try: try:
yield get(get_client(), 'http://httpbin.org/status/404') yield get(get_client(), "http://httpbin.org/status/404")
except http.HTTPError as e: except http.HTTPError as e:
assert e.code == 404 assert e.code == 404
@@ -343,41 +310,41 @@ def test_tornado_exception_can_be_caught(get_client):
def test_existing_references_get_patched(tmpdir): def test_existing_references_get_patched(tmpdir):
from tornado.httpclient import AsyncHTTPClient from tornado.httpclient import AsyncHTTPClient
with vcr.use_cassette(str(tmpdir.join('data.yaml'))): with vcr.use_cassette(str(tmpdir.join("data.yaml"))):
client = AsyncHTTPClient() client = AsyncHTTPClient()
yield get(client, 'http://httpbin.org/get') yield get(client, "http://httpbin.org/get")
with vcr.use_cassette(str(tmpdir.join('data.yaml'))) as cass: with vcr.use_cassette(str(tmpdir.join("data.yaml"))) as cass:
yield get(client, 'http://httpbin.org/get') yield get(client, "http://httpbin.org/get")
assert cass.play_count == 1 assert cass.play_count == 1
@pytest.mark.gen_test @pytest.mark.gen_test
def test_existing_instances_get_patched(get_client, tmpdir): def test_existing_instances_get_patched(get_client, tmpdir):
'''Ensure that existing instances of AsyncHTTPClient get patched upon """Ensure that existing instances of AsyncHTTPClient get patched upon
entering VCR context.''' entering VCR context."""
client = get_client() client = get_client()
with vcr.use_cassette(str(tmpdir.join('data.yaml'))): with vcr.use_cassette(str(tmpdir.join("data.yaml"))):
yield get(client, 'http://httpbin.org/get') yield get(client, "http://httpbin.org/get")
with vcr.use_cassette(str(tmpdir.join('data.yaml'))) as cass: with vcr.use_cassette(str(tmpdir.join("data.yaml"))) as cass:
yield get(client, 'http://httpbin.org/get') yield get(client, "http://httpbin.org/get")
assert cass.play_count == 1 assert cass.play_count == 1
@pytest.mark.gen_test @pytest.mark.gen_test
def test_request_time_is_set(get_client, tmpdir): def test_request_time_is_set(get_client, tmpdir):
'''Ensures that the request_time on HTTPResponses is set.''' """Ensures that the request_time on HTTPResponses is set."""
with vcr.use_cassette(str(tmpdir.join('data.yaml'))): with vcr.use_cassette(str(tmpdir.join("data.yaml"))):
client = get_client() client = get_client()
response = yield get(client, 'http://httpbin.org/get') response = yield get(client, "http://httpbin.org/get")
assert response.request_time is not None assert response.request_time is not None
with vcr.use_cassette(str(tmpdir.join('data.yaml'))) as cass: with vcr.use_cassette(str(tmpdir.join("data.yaml"))) as cass:
client = get_client() client = get_client()
response = yield get(client, 'http://httpbin.org/get') response = yield get(client, "http://httpbin.org/get")
assert response.request_time is not None assert response.request_time is not None
assert cass.play_count == 1 assert cass.play_count == 1

View File

@@ -1,8 +1,9 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
'''Integration tests with urllib2''' """Integration tests with urllib2"""
from six.moves.urllib.request import urlopen import ssl
from six.moves.urllib_parse import urlencode from urllib.request import urlopen
from urllib.parse import urlencode
import pytest_httpbin.certs import pytest_httpbin.certs
# Internal imports # Internal imports
@@ -12,91 +13,88 @@ from assertions import assert_cassette_has_one_response
def urlopen_with_cafile(*args, **kwargs): def urlopen_with_cafile(*args, **kwargs):
kwargs['cafile'] = pytest_httpbin.certs.where() context = ssl.create_default_context(cafile=pytest_httpbin.certs.where())
context.check_hostname = False
kwargs["context"] = context
try: try:
return urlopen(*args, **kwargs) return urlopen(*args, **kwargs)
except TypeError: except TypeError:
# python2/pypi don't let us override this # python2/pypi don't let us override this
del kwargs['cafile'] del kwargs["cafile"]
return urlopen(*args, **kwargs) return urlopen(*args, **kwargs)
def test_response_code(httpbin_both, tmpdir): def test_response_code(httpbin_both, tmpdir):
'''Ensure we can read a response code from a fetch''' """Ensure we can read a response code from a fetch"""
url = httpbin_both.url url = httpbin_both.url
with vcr.use_cassette(str(tmpdir.join('atts.yaml'))): with vcr.use_cassette(str(tmpdir.join("atts.yaml"))):
code = urlopen_with_cafile(url).getcode() code = urlopen_with_cafile(url).getcode()
with vcr.use_cassette(str(tmpdir.join('atts.yaml'))): with vcr.use_cassette(str(tmpdir.join("atts.yaml"))):
assert code == urlopen_with_cafile(url).getcode() assert code == urlopen_with_cafile(url).getcode()
def test_random_body(httpbin_both, tmpdir): def test_random_body(httpbin_both, tmpdir):
'''Ensure we can read the content, and that it's served from cache''' """Ensure we can read the content, and that it's served from cache"""
url = httpbin_both.url + '/bytes/1024' url = httpbin_both.url + "/bytes/1024"
with vcr.use_cassette(str(tmpdir.join('body.yaml'))): with vcr.use_cassette(str(tmpdir.join("body.yaml"))):
body = urlopen_with_cafile(url).read() body = urlopen_with_cafile(url).read()
with vcr.use_cassette(str(tmpdir.join('body.yaml'))): with vcr.use_cassette(str(tmpdir.join("body.yaml"))):
assert body == urlopen_with_cafile(url).read() assert body == urlopen_with_cafile(url).read()
def test_response_headers(httpbin_both, tmpdir): def test_response_headers(httpbin_both, tmpdir):
'''Ensure we can get information from the response''' """Ensure we can get information from the response"""
url = httpbin_both.url url = httpbin_both.url
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))): with vcr.use_cassette(str(tmpdir.join("headers.yaml"))):
open1 = urlopen_with_cafile(url).info().items() open1 = urlopen_with_cafile(url).info().items()
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))): with vcr.use_cassette(str(tmpdir.join("headers.yaml"))):
open2 = urlopen_with_cafile(url).info().items() open2 = urlopen_with_cafile(url).info().items()
assert sorted(open1) == sorted(open2) assert sorted(open1) == sorted(open2)
def test_effective_url(httpbin_both, tmpdir): def test_effective_url(httpbin_both, tmpdir):
'''Ensure that the effective_url is captured''' """Ensure that the effective_url is captured"""
url = httpbin_both.url + '/redirect-to?url=/html' url = httpbin_both.url + "/redirect-to?url=/html"
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))): with vcr.use_cassette(str(tmpdir.join("headers.yaml"))):
effective_url = urlopen_with_cafile(url).geturl() effective_url = urlopen_with_cafile(url).geturl()
assert effective_url == httpbin_both.url + '/html' assert effective_url == httpbin_both.url + "/html"
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))): with vcr.use_cassette(str(tmpdir.join("headers.yaml"))):
assert effective_url == urlopen_with_cafile(url).geturl() assert effective_url == urlopen_with_cafile(url).geturl()
def test_multiple_requests(httpbin_both, tmpdir): def test_multiple_requests(httpbin_both, tmpdir):
'''Ensure that we can cache multiple requests''' """Ensure that we can cache multiple requests"""
urls = [ urls = [httpbin_both.url, httpbin_both.url, httpbin_both.url + "/get", httpbin_both.url + "/bytes/1024"]
httpbin_both.url, with vcr.use_cassette(str(tmpdir.join("multiple.yaml"))) as cass:
httpbin_both.url,
httpbin_both.url + '/get',
httpbin_both.url + '/bytes/1024',
]
with vcr.use_cassette(str(tmpdir.join('multiple.yaml'))) as cass:
[urlopen_with_cafile(url) for url in urls] [urlopen_with_cafile(url) for url in urls]
assert len(cass) == len(urls) assert len(cass) == len(urls)
def test_get_data(httpbin_both, tmpdir): def test_get_data(httpbin_both, tmpdir):
'''Ensure that it works with query data''' """Ensure that it works with query data"""
data = urlencode({'some': 1, 'data': 'here'}) data = urlencode({"some": 1, "data": "here"})
url = httpbin_both.url + '/get?' + data url = httpbin_both.url + "/get?" + data
with vcr.use_cassette(str(tmpdir.join('get_data.yaml'))): with vcr.use_cassette(str(tmpdir.join("get_data.yaml"))):
res1 = urlopen_with_cafile(url).read() res1 = urlopen_with_cafile(url).read()
with vcr.use_cassette(str(tmpdir.join('get_data.yaml'))): with vcr.use_cassette(str(tmpdir.join("get_data.yaml"))):
res2 = urlopen_with_cafile(url).read() res2 = urlopen_with_cafile(url).read()
assert res1 == res2 assert res1 == res2
def test_post_data(httpbin_both, tmpdir): def test_post_data(httpbin_both, tmpdir):
'''Ensure that it works when posting data''' """Ensure that it works when posting data"""
data = urlencode({'some': 1, 'data': 'here'}).encode('utf-8') data = urlencode({"some": 1, "data": "here"}).encode("utf-8")
url = httpbin_both.url + '/post' url = httpbin_both.url + "/post"
with vcr.use_cassette(str(tmpdir.join('post_data.yaml'))): with vcr.use_cassette(str(tmpdir.join("post_data.yaml"))):
res1 = urlopen_with_cafile(url, data).read() res1 = urlopen_with_cafile(url, data).read()
with vcr.use_cassette(str(tmpdir.join('post_data.yaml'))) as cass: with vcr.use_cassette(str(tmpdir.join("post_data.yaml"))) as cass:
res2 = urlopen_with_cafile(url, data).read() res2 = urlopen_with_cafile(url, data).read()
assert len(cass) == 1 assert len(cass) == 1
@@ -105,13 +103,13 @@ def test_post_data(httpbin_both, tmpdir):
def test_post_unicode_data(httpbin_both, tmpdir): def test_post_unicode_data(httpbin_both, tmpdir):
'''Ensure that it works when posting unicode data''' """Ensure that it works when posting unicode data"""
data = urlencode({'snowman': u''.encode('utf-8')}).encode('utf-8') data = urlencode({"snowman": "".encode()}).encode("utf-8")
url = httpbin_both.url + '/post' url = httpbin_both.url + "/post"
with vcr.use_cassette(str(tmpdir.join('post_data.yaml'))): with vcr.use_cassette(str(tmpdir.join("post_data.yaml"))):
res1 = urlopen_with_cafile(url, data).read() res1 = urlopen_with_cafile(url, data).read()
with vcr.use_cassette(str(tmpdir.join('post_data.yaml'))) as cass: with vcr.use_cassette(str(tmpdir.join("post_data.yaml"))) as cass:
res2 = urlopen_with_cafile(url, data).read() res2 = urlopen_with_cafile(url, data).read()
assert len(cass) == 1 assert len(cass) == 1
@@ -120,11 +118,11 @@ def test_post_unicode_data(httpbin_both, tmpdir):
def test_cross_scheme(tmpdir, httpbin_secure, httpbin): def test_cross_scheme(tmpdir, httpbin_secure, httpbin):
'''Ensure that requests between schemes are treated separately''' """Ensure that requests between schemes are treated separately"""
# First fetch a url under https, and then again under https and then # 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 # ensure that we haven't served anything out of cache, and we have two
# requests / response pairs in the cassette # requests / response pairs in the cassette
with vcr.use_cassette(str(tmpdir.join('cross_scheme.yaml'))) as cass: with vcr.use_cassette(str(tmpdir.join("cross_scheme.yaml"))) as cass:
urlopen_with_cafile(httpbin_secure.url) urlopen_with_cafile(httpbin_secure.url)
urlopen_with_cafile(httpbin.url) urlopen_with_cafile(httpbin.url)
assert len(cass) == 2 assert len(cass) == 2
@@ -132,14 +130,14 @@ def test_cross_scheme(tmpdir, httpbin_secure, httpbin):
def test_decorator(httpbin_both, tmpdir): def test_decorator(httpbin_both, tmpdir):
'''Test the decorator version of VCR.py''' """Test the decorator version of VCR.py"""
url = httpbin_both.url url = httpbin_both.url
@vcr.use_cassette(str(tmpdir.join('atts.yaml'))) @vcr.use_cassette(str(tmpdir.join("atts.yaml")))
def inner1(): def inner1():
return urlopen_with_cafile(url).getcode() return urlopen_with_cafile(url).getcode()
@vcr.use_cassette(str(tmpdir.join('atts.yaml'))) @vcr.use_cassette(str(tmpdir.join("atts.yaml")))
def inner2(): def inner2():
return urlopen_with_cafile(url).getcode() return urlopen_with_cafile(url).getcode()

View File

@@ -1,4 +1,4 @@
'''Integration tests with urllib3''' """Integration tests with urllib3"""
# coding=utf-8 # coding=utf-8
@@ -7,101 +7,101 @@ import pytest_httpbin
import vcr import vcr
from vcr.patch import force_reset from vcr.patch import force_reset
from assertions import assert_cassette_empty, assert_is_json from assertions import assert_cassette_empty, assert_is_json
urllib3 = pytest.importorskip("urllib3") urllib3 = pytest.importorskip("urllib3")
@pytest.fixture(scope='module') @pytest.fixture(scope="module")
def verify_pool_mgr(): def verify_pool_mgr():
return urllib3.PoolManager( return urllib3.PoolManager(
cert_reqs='CERT_REQUIRED', # Force certificate check. cert_reqs="CERT_REQUIRED", ca_certs=pytest_httpbin.certs.where() # Force certificate check.
ca_certs=pytest_httpbin.certs.where()
) )
@pytest.fixture(scope='module') @pytest.fixture(scope="module")
def pool_mgr(): def pool_mgr():
return urllib3.PoolManager() return urllib3.PoolManager(cert_reqs="CERT_NONE")
def test_status_code(httpbin_both, tmpdir, verify_pool_mgr): def test_status_code(httpbin_both, tmpdir, verify_pool_mgr):
'''Ensure that we can read the status code''' """Ensure that we can read the status code"""
url = httpbin_both.url url = httpbin_both.url
with vcr.use_cassette(str(tmpdir.join('atts.yaml'))): with vcr.use_cassette(str(tmpdir.join("atts.yaml"))):
status_code = verify_pool_mgr.request('GET', url).status status_code = verify_pool_mgr.request("GET", url).status
with vcr.use_cassette(str(tmpdir.join('atts.yaml'))): with vcr.use_cassette(str(tmpdir.join("atts.yaml"))):
assert status_code == verify_pool_mgr.request('GET', url).status assert status_code == verify_pool_mgr.request("GET", url).status
def test_headers(tmpdir, httpbin_both, verify_pool_mgr): def test_headers(tmpdir, httpbin_both, verify_pool_mgr):
'''Ensure that we can read the headers back''' """Ensure that we can read the headers back"""
url = httpbin_both.url url = httpbin_both.url
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))): with vcr.use_cassette(str(tmpdir.join("headers.yaml"))):
headers = verify_pool_mgr.request('GET', url).headers headers = verify_pool_mgr.request("GET", url).headers
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))): with vcr.use_cassette(str(tmpdir.join("headers.yaml"))):
assert headers == verify_pool_mgr.request('GET', url).headers assert headers == verify_pool_mgr.request("GET", url).headers
def test_body(tmpdir, httpbin_both, verify_pool_mgr): def test_body(tmpdir, httpbin_both, verify_pool_mgr):
'''Ensure the responses are all identical enough''' """Ensure the responses are all identical enough"""
url = httpbin_both.url + '/bytes/1024' url = httpbin_both.url + "/bytes/1024"
with vcr.use_cassette(str(tmpdir.join('body.yaml'))): with vcr.use_cassette(str(tmpdir.join("body.yaml"))):
content = verify_pool_mgr.request('GET', url).data content = verify_pool_mgr.request("GET", url).data
with vcr.use_cassette(str(tmpdir.join('body.yaml'))): with vcr.use_cassette(str(tmpdir.join("body.yaml"))):
assert content == verify_pool_mgr.request('GET', url).data assert content == verify_pool_mgr.request("GET", url).data
def test_auth(tmpdir, httpbin_both, verify_pool_mgr): def test_auth(tmpdir, httpbin_both, verify_pool_mgr):
'''Ensure that we can handle basic auth''' """Ensure that we can handle basic auth"""
auth = ('user', 'passwd') auth = ("user", "passwd")
headers = urllib3.util.make_headers(basic_auth='{}:{}'.format(*auth)) headers = urllib3.util.make_headers(basic_auth="{}:{}".format(*auth))
url = httpbin_both.url + '/basic-auth/user/passwd' url = httpbin_both.url + "/basic-auth/user/passwd"
with vcr.use_cassette(str(tmpdir.join('auth.yaml'))): with vcr.use_cassette(str(tmpdir.join("auth.yaml"))):
one = verify_pool_mgr.request('GET', url, headers=headers) one = verify_pool_mgr.request("GET", url, headers=headers)
with vcr.use_cassette(str(tmpdir.join('auth.yaml'))): with vcr.use_cassette(str(tmpdir.join("auth.yaml"))):
two = verify_pool_mgr.request('GET', url, headers=headers) two = verify_pool_mgr.request("GET", url, headers=headers)
assert one.data == two.data assert one.data == two.data
assert one.status == two.status assert one.status == two.status
def test_auth_failed(tmpdir, httpbin_both, verify_pool_mgr): def test_auth_failed(tmpdir, httpbin_both, verify_pool_mgr):
'''Ensure that we can save failed auth statuses''' """Ensure that we can save failed auth statuses"""
auth = ('user', 'wrongwrongwrong') auth = ("user", "wrongwrongwrong")
headers = urllib3.util.make_headers(basic_auth='{}:{}'.format(*auth)) headers = urllib3.util.make_headers(basic_auth="{}:{}".format(*auth))
url = httpbin_both.url + '/basic-auth/user/passwd' url = httpbin_both.url + "/basic-auth/user/passwd"
with vcr.use_cassette(str(tmpdir.join('auth-failed.yaml'))) as cass: with vcr.use_cassette(str(tmpdir.join("auth-failed.yaml"))) as cass:
# Ensure that this is empty to begin with # Ensure that this is empty to begin with
assert_cassette_empty(cass) assert_cassette_empty(cass)
one = verify_pool_mgr.request('GET', url, headers=headers) one = verify_pool_mgr.request("GET", url, headers=headers)
two = verify_pool_mgr.request('GET', url, headers=headers) two = verify_pool_mgr.request("GET", url, headers=headers)
assert one.data == two.data assert one.data == two.data
assert one.status == two.status == 401 assert one.status == two.status == 401
def test_post(tmpdir, httpbin_both, verify_pool_mgr): def test_post(tmpdir, httpbin_both, verify_pool_mgr):
'''Ensure that we can post and cache the results''' """Ensure that we can post and cache the results"""
data = {'key1': 'value1', 'key2': 'value2'} data = {"key1": "value1", "key2": "value2"}
url = httpbin_both.url + '/post' url = httpbin_both.url + "/post"
with vcr.use_cassette(str(tmpdir.join('verify_pool_mgr.yaml'))): with vcr.use_cassette(str(tmpdir.join("verify_pool_mgr.yaml"))):
req1 = verify_pool_mgr.request('POST', url, data).data req1 = verify_pool_mgr.request("POST", url, data).data
with vcr.use_cassette(str(tmpdir.join('verify_pool_mgr.yaml'))): with vcr.use_cassette(str(tmpdir.join("verify_pool_mgr.yaml"))):
req2 = verify_pool_mgr.request('POST', url, data).data req2 = verify_pool_mgr.request("POST", url, data).data
assert req1 == req2 assert req1 == req2
def test_redirects(tmpdir, httpbin_both, verify_pool_mgr): def test_redirects(tmpdir, httpbin_both, verify_pool_mgr):
'''Ensure that we can handle redirects''' """Ensure that we can handle redirects"""
url = httpbin_both.url + '/redirect-to?url=bytes/1024' url = httpbin_both.url + "/redirect-to?url=bytes/1024"
with vcr.use_cassette(str(tmpdir.join('verify_pool_mgr.yaml'))): with vcr.use_cassette(str(tmpdir.join("verify_pool_mgr.yaml"))):
content = verify_pool_mgr.request('GET', url).data content = verify_pool_mgr.request("GET", url).data
with vcr.use_cassette(str(tmpdir.join('verify_pool_mgr.yaml'))) as cass: with vcr.use_cassette(str(tmpdir.join("verify_pool_mgr.yaml"))) as cass:
assert content == verify_pool_mgr.request('GET', url).data assert content == verify_pool_mgr.request("GET", url).data
# Ensure that we've now cached *two* responses. One for the redirect # Ensure that we've now cached *two* responses. One for the redirect
# and one for the final fetch # and one for the final fetch
assert len(cass) == 2 assert len(cass) == 2
@@ -109,36 +109,36 @@ def test_redirects(tmpdir, httpbin_both, verify_pool_mgr):
def test_cross_scheme(tmpdir, httpbin, httpbin_secure, verify_pool_mgr): def test_cross_scheme(tmpdir, httpbin, httpbin_secure, verify_pool_mgr):
'''Ensure that requests between schemes are treated separately''' """Ensure that requests between schemes are treated separately"""
# First fetch a url under http, and then again under https and then # 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 # ensure that we haven't served anything out of cache, and we have two
# requests / response pairs in the cassette # requests / response pairs in the cassette
with vcr.use_cassette(str(tmpdir.join('cross_scheme.yaml'))) as cass: 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_secure.url)
verify_pool_mgr.request('GET', httpbin.url) verify_pool_mgr.request("GET", httpbin.url)
assert cass.play_count == 0 assert cass.play_count == 0
assert len(cass) == 2 assert len(cass) == 2
def test_gzip(tmpdir, httpbin_both, verify_pool_mgr): def test_gzip(tmpdir, httpbin_both, verify_pool_mgr):
''' """
Ensure that requests (actually urllib3) is able to automatically decompress Ensure that requests (actually urllib3) is able to automatically decompress
the response body the response body
''' """
url = httpbin_both.url + '/gzip' url = httpbin_both.url + "/gzip"
response = verify_pool_mgr.request('GET', url) response = verify_pool_mgr.request("GET", url)
with vcr.use_cassette(str(tmpdir.join('gzip.yaml'))): with vcr.use_cassette(str(tmpdir.join("gzip.yaml"))):
response = verify_pool_mgr.request('GET', url) response = verify_pool_mgr.request("GET", url)
assert_is_json(response.data) assert_is_json(response.data)
with vcr.use_cassette(str(tmpdir.join('gzip.yaml'))): with vcr.use_cassette(str(tmpdir.join("gzip.yaml"))):
assert_is_json(response.data) assert_is_json(response.data)
def test_https_with_cert_validation_disabled(tmpdir, httpbin_secure, pool_mgr): def test_https_with_cert_validation_disabled(tmpdir, httpbin_secure, pool_mgr):
with vcr.use_cassette(str(tmpdir.join('cert_validation_disabled.yaml'))): with vcr.use_cassette(str(tmpdir.join("cert_validation_disabled.yaml"))):
pool_mgr.request('GET', httpbin_secure.url) pool_mgr.request("GET", httpbin_secure.url)
def test_urllib3_force_reset(): def test_urllib3_force_reset():
@@ -146,7 +146,7 @@ def test_urllib3_force_reset():
http_original = cpool.HTTPConnection http_original = cpool.HTTPConnection
https_original = cpool.HTTPSConnection https_original = cpool.HTTPSConnection
verified_https_original = cpool.VerifiedHTTPSConnection verified_https_original = cpool.VerifiedHTTPSConnection
with vcr.use_cassette(path='test'): with vcr.use_cassette(path="test"):
first_cassette_HTTPConnection = cpool.HTTPConnection first_cassette_HTTPConnection = cpool.HTTPConnection
first_cassette_HTTPSConnection = cpool.HTTPSConnection first_cassette_HTTPSConnection = cpool.HTTPSConnection
first_cassette_VerifiedHTTPSConnection = cpool.VerifiedHTTPSConnection first_cassette_VerifiedHTTPSConnection = cpool.VerifiedHTTPSConnection

View File

@@ -1,25 +1,22 @@
import http.client as httplib
import multiprocessing import multiprocessing
import pytest import pytest
from six.moves import xmlrpc_client, xmlrpc_server from xmlrpc.client import ServerProxy
from xmlrpc.server import SimpleXMLRPCServer
requests = pytest.importorskip("requests") requests = pytest.importorskip("requests")
import vcr # NOQA import vcr # NOQA
try:
import httplib
except ImportError:
import http.client as httplib
def test_domain_redirect(): 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 # In this example, seomoz.org redirects to moz.com, and if those
# requests are considered identical, then we'll be stuck in a redirect # requests are considered identical, then we'll be stuck in a redirect
# loop. # loop.
url = 'http://seomoz.org/' url = "http://seomoz.org/"
with vcr.use_cassette('tests/fixtures/wild/domain_redirect.yaml') as cass: with vcr.use_cassette("tests/fixtures/wild/domain_redirect.yaml") as cass:
requests.get(url, headers={'User-Agent': 'vcrpy-test'}) requests.get(url, headers={"User-Agent": "vcrpy-test"})
# Ensure that we've now served two responses. One for the original # Ensure that we've now served two responses. One for the original
# redirect, and a second for the actual fetch # redirect, and a second for the actual fetch
assert len(cass) == 2 assert len(cass) == 2
@@ -30,13 +27,11 @@ def test_flickr_multipart_upload(httpbin, tmpdir):
The python-flickr-api project does a multipart The python-flickr-api project does a multipart
upload that confuses vcrpy upload that confuses vcrpy
""" """
def _pretend_to_be_flickr_library(): def _pretend_to_be_flickr_library():
content_type, body = "text/plain", "HELLO WORLD" content_type, body = "text/plain", "HELLO WORLD"
h = httplib.HTTPConnection(httpbin.host, httpbin.port) h = httplib.HTTPConnection(httpbin.host, httpbin.port)
headers = { headers = {"Content-Type": content_type, "content-length": str(len(body))}
"Content-Type": content_type,
"content-length": str(len(body))
}
h.request("POST", "/post/", headers=headers) h.request("POST", "/post/", headers=headers)
h.send(body) h.send(body)
r = h.getresponse() r = h.getresponse()
@@ -45,7 +40,7 @@ def test_flickr_multipart_upload(httpbin, tmpdir):
return data return data
testfile = str(tmpdir.join('flickr.yml')) testfile = str(tmpdir.join("flickr.yml"))
with vcr.use_cassette(testfile) as cass: with vcr.use_cassette(testfile) as cass:
_pretend_to_be_flickr_library() _pretend_to_be_flickr_library()
assert len(cass) == 1 assert len(cass) == 1
@@ -57,51 +52,55 @@ def test_flickr_multipart_upload(httpbin, tmpdir):
def test_flickr_should_respond_with_200(tmpdir): 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): 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 assert r.status_code == 200
def test_cookies(tmpdir, httpbin): def test_cookies(tmpdir, httpbin):
testfile = str(tmpdir.join('cookies.yml')) testfile = str(tmpdir.join("cookies.yml"))
with vcr.use_cassette(testfile): with vcr.use_cassette(testfile):
s = requests.Session() s = requests.Session()
s.get(httpbin.url + "/cookies/set?k1=v1&k2=v2") s.get(httpbin.url + "/cookies/set?k1=v1&k2=v2")
r2 = s.get(httpbin.url + "/cookies") r2 = s.get(httpbin.url + "/cookies")
assert len(r2.json()['cookies']) == 2 assert len(r2.json()["cookies"]) == 2
def test_amazon_doctype(tmpdir): def test_amazon_doctype(tmpdir):
# amazon gzips its homepage. For some reason, in requests 2.7, it's not # amazon gzips its homepage. For some reason, in requests 2.7, it's not
# getting gunzipped. # getting gunzipped.
with vcr.use_cassette(str(tmpdir.join('amz.yml'))): with vcr.use_cassette(str(tmpdir.join("amz.yml"))):
r = requests.get('http://www.amazon.com', verify=False) r = requests.get("http://www.amazon.com", verify=False)
assert 'html' in r.text assert "html" in r.text
@pytest.yield_fixture(scope='session') def start_rpc_server(q):
def rpc_server(): httpd = SimpleXMLRPCServer(("127.0.0.1", 0))
httpd = xmlrpc_server.SimpleXMLRPCServer(('', 0))
httpd.register_function(pow) httpd.register_function(pow)
proxy_process = multiprocessing.Process( q.put("http://{}:{}".format(*httpd.server_address))
target=httpd.serve_forever, httpd.serve_forever()
)
@pytest.yield_fixture(scope="session")
def rpc_server():
q = multiprocessing.Queue()
proxy_process = multiprocessing.Process(target=start_rpc_server, args=(q,))
try: try:
proxy_process.start() proxy_process.start()
yield 'http://{}:{}'.format(*httpd.server_address) yield q.get()
finally: finally:
proxy_process.terminate() proxy_process.terminate()
def test_xmlrpclib(tmpdir, rpc_server): def test_xmlrpclib(tmpdir, rpc_server):
with vcr.use_cassette(str(tmpdir.join('xmlrpcvideo.yaml'))): with vcr.use_cassette(str(tmpdir.join("xmlrpcvideo.yaml"))):
roundup_server = xmlrpc_client.ServerProxy(rpc_server, allow_none=True) roundup_server = ServerProxy(rpc_server, allow_none=True)
original_schema = roundup_server.pow(2, 4) original_schema = roundup_server.pow(2, 4)
with vcr.use_cassette(str(tmpdir.join('xmlrpcvideo.yaml'))): with vcr.use_cassette(str(tmpdir.join("xmlrpcvideo.yaml"))):
roundup_server = xmlrpc_client.ServerProxy(rpc_server, allow_none=True) roundup_server = ServerProxy(rpc_server, allow_none=True)
second_schema = roundup_server.pow(2, 4) second_schema = roundup_server.pow(2, 4)
assert original_schema == second_schema assert original_schema == second_schema

View File

@@ -1,12 +1,12 @@
import contextlib
import copy import copy
import http.client as httplib
import inspect import inspect
import os import os
from unittest import mock
from six.moves import http_client as httplib
import pytest import pytest
import yaml import yaml
from vcr.compat import mock, contextlib
from vcr.cassette import Cassette from vcr.cassette import Cassette
from vcr.errors import UnhandledHTTPRequestError from vcr.errors import UnhandledHTTPRequestError
from vcr.patch import force_reset from vcr.patch import force_reset
@@ -14,31 +14,36 @@ from vcr.stubs import VCRHTTPSConnection
def test_cassette_load(tmpdir): def test_cassette_load(tmpdir):
a_file = tmpdir.join('test_cassette.yml') a_file = tmpdir.join("test_cassette.yml")
a_file.write(yaml.dump({'interactions': [ a_file.write(
{'request': {'body': '', 'uri': 'foo', 'method': 'GET', 'headers': {}}, yaml.dump(
'response': 'bar'} {
]})) "interactions": [
{"request": {"body": "", "uri": "foo", "method": "GET", "headers": {}}, "response": "bar"}
]
}
)
)
a_cassette = Cassette.load(path=str(a_file)) a_cassette = Cassette.load(path=str(a_file))
assert len(a_cassette) == 1 assert len(a_cassette) == 1
def test_cassette_not_played(): def test_cassette_not_played():
a = Cassette('test') a = Cassette("test")
assert not a.play_count assert not a.play_count
def test_cassette_append(): def test_cassette_append():
a = Cassette('test') a = Cassette("test")
a.append('foo', 'bar') a.append("foo", "bar")
assert a.requests == ['foo'] assert a.requests == ["foo"]
assert a.responses == ['bar'] assert a.responses == ["bar"]
def test_cassette_len(): def test_cassette_len():
a = Cassette('test') a = Cassette("test")
a.append('foo', 'bar') a.append("foo", "bar")
a.append('foo2', 'bar2') a.append("foo2", "bar2")
assert len(a) == 2 assert len(a) == 2
@@ -46,34 +51,34 @@ def _mock_requests_match(request1, request2, matchers):
return request1 == request2 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(): def test_cassette_contains():
a = Cassette('test') a = Cassette("test")
a.append('foo', 'bar') a.append("foo", "bar")
assert 'foo' in a 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(): def test_cassette_responses_of():
a = Cassette('test') a = Cassette("test")
a.append('foo', 'bar') a.append("foo", "bar")
assert a.responses_of('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(): def test_cassette_get_missing_response():
a = Cassette('test') a = Cassette("test")
with pytest.raises(UnhandledHTTPRequestError): 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(): def test_cassette_cant_read_same_request_twice():
a = Cassette('test') a = Cassette("test")
a.append('foo', 'bar') a.append("foo", "bar")
a.play_response('foo') a.play_response("foo")
with pytest.raises(UnhandledHTTPRequestError): with pytest.raises(UnhandledHTTPRequestError):
a.play_response('foo') a.play_response("foo")
def make_get_request(): def make_get_request():
@@ -82,105 +87,144 @@ def make_get_request():
return conn.getresponse() return conn.getresponse()
@mock.patch('vcr.cassette.requests_match', return_value=True) @mock.patch("vcr.cassette.requests_match", return_value=True)
@mock.patch('vcr.cassette.FilesystemPersister.load_cassette', @mock.patch(
classmethod(lambda *args, **kwargs: (('foo',), (mock.MagicMock(),)))) "vcr.cassette.FilesystemPersister.load_cassette",
@mock.patch('vcr.cassette.Cassette.can_play_response_for', return_value=True) classmethod(lambda *args, **kwargs: (("foo",), (mock.MagicMock(),))),
@mock.patch('vcr.stubs.VCRHTTPResponse') )
@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): def test_function_decorated_with_use_cassette_can_be_invoked_multiple_times(*args):
decorated_function = Cassette.use(path='test')(make_get_request) decorated_function = Cassette.use(path="test")(make_get_request)
for i in range(4): for i in range(4):
decorated_function() decorated_function()
def test_arg_getter_functionality(): def test_arg_getter_functionality():
arg_getter = mock.Mock(return_value={'path': 'test'}) arg_getter = mock.Mock(return_value={"path": "test"})
context_decorator = Cassette.use_arg_getter(arg_getter) context_decorator = Cassette.use_arg_getter(arg_getter)
with context_decorator as cassette: with context_decorator as cassette:
assert cassette._path == 'test' assert cassette._path == "test"
arg_getter.return_value = {'path': 'other'} arg_getter.return_value = {"path": "other"}
with context_decorator as cassette: with context_decorator as cassette:
assert cassette._path == 'other' assert cassette._path == "other"
arg_getter.return_value = {'path': 'other', 'filter_headers': ('header_name',)} arg_getter.return_value = {"path": "other", "filter_headers": ("header_name",)}
@context_decorator @context_decorator
def function(): def function():
pass pass
with mock.patch.object( with mock.patch.object(Cassette, "load", return_value=mock.MagicMock(inject=False)) as cassette_load:
Cassette, 'load',
return_value=mock.MagicMock(inject=False)
) as cassette_load:
function() function()
cassette_load.assert_called_once_with(**arg_getter.return_value) cassette_load.assert_called_once_with(**arg_getter.return_value)
def test_cassette_not_all_played(): def test_cassette_not_all_played():
a = Cassette('test') a = Cassette("test")
a.append('foo', 'bar') a.append("foo", "bar")
assert not a.all_played 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(): def test_cassette_all_played():
a = Cassette('test') a = Cassette("test")
a.append('foo', 'bar') a.append("foo", "bar")
a.play_response('foo') a.play_response("foo")
assert a.all_played assert a.all_played
def test_before_record_response(): @mock.patch("vcr.cassette.requests_match", _mock_requests_match)
before_record_response = mock.Mock(return_value='mutated') def test_cassette_allow_playback_repeats():
cassette = Cassette('test', before_record_response=before_record_response) a = Cassette("test", allow_playback_repeats=True)
cassette.append('req', 'res') a.append("foo", "bar")
a.append("other", "resp")
for x in range(10):
assert a.play_response("foo") == "bar"
assert a.play_count == 10
assert a.all_played is False
assert a.play_response("other") == "resp"
assert a.play_count == 11
assert a.all_played
before_record_response.assert_called_once_with('res') a.allow_playback_repeats = False
assert cassette.responses[0] == 'mutated' 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): def assert_get_response_body_is(value):
conn = httplib.HTTPConnection("www.python.org") conn = httplib.HTTPConnection("www.python.org")
conn.request("GET", "/index.html") conn.request("GET", "/index.html")
assert conn.getresponse().read().decode('utf8') == value assert conn.getresponse().read().decode("utf8") == value
@mock.patch('vcr.cassette.requests_match', _mock_requests_match) @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.can_play_response_for", return_value=True)
@mock.patch('vcr.cassette.Cassette._save', return_value=True) @mock.patch("vcr.cassette.Cassette._save", return_value=True)
def test_nesting_cassette_context_managers(*args): def test_nesting_cassette_context_managers(*args):
first_response = {'body': {'string': b'first_response'}, 'headers': {}, first_response = {
'status': {'message': 'm', 'code': 200}} "body": {"string": b"first_response"},
"headers": {},
"status": {"message": "m", "code": 200},
}
second_response = copy.deepcopy(first_response) second_response = copy.deepcopy(first_response)
second_response['body']['string'] = b'second_response' second_response["body"]["string"] = b"second_response"
with contextlib.ExitStack() as exit_stack: with contextlib.ExitStack() as exit_stack:
first_cassette = exit_stack.enter_context(Cassette.use(path='test')) first_cassette = exit_stack.enter_context(Cassette.use(path="test"))
exit_stack.enter_context(mock.patch.object(first_cassette, 'play_response', exit_stack.enter_context(
return_value=first_response)) mock.patch.object(first_cassette, "play_response", return_value=first_response)
assert_get_response_body_is('first_response') )
assert_get_response_body_is("first_response")
# Make sure a second cassette can supercede the first # Make sure a second cassette can supercede the first
with Cassette.use(path='test') as second_cassette: with Cassette.use(path="test") as second_cassette:
with mock.patch.object(second_cassette, 'play_response', return_value=second_response): with mock.patch.object(second_cassette, "play_response", return_value=second_response):
assert_get_response_body_is('second_response') assert_get_response_body_is("second_response")
# Now the first cassette should be back in effect # Now the first cassette should be back in effect
assert_get_response_body_is('first_response') assert_get_response_body_is("first_response")
def test_nesting_context_managers_by_checking_references_of_http_connection(): def test_nesting_context_managers_by_checking_references_of_http_connection():
original = httplib.HTTPConnection original = httplib.HTTPConnection
with Cassette.use(path='test'): with Cassette.use(path="test"):
first_cassette_HTTPConnection = httplib.HTTPConnection first_cassette_HTTPConnection = httplib.HTTPConnection
with Cassette.use(path='test'): with Cassette.use(path="test"):
second_cassette_HTTPConnection = httplib.HTTPConnection second_cassette_HTTPConnection = httplib.HTTPConnection
assert second_cassette_HTTPConnection is not first_cassette_HTTPConnection assert second_cassette_HTTPConnection is not first_cassette_HTTPConnection
with Cassette.use(path='test'): with Cassette.use(path="test"):
assert httplib.HTTPConnection is not second_cassette_HTTPConnection assert httplib.HTTPConnection is not second_cassette_HTTPConnection
with force_reset(): with force_reset():
assert httplib.HTTPConnection is original assert httplib.HTTPConnection is original
@@ -189,16 +233,15 @@ def test_nesting_context_managers_by_checking_references_of_http_connection():
def test_custom_patchers(): def test_custom_patchers():
class Test(object): class Test:
attribute = None attribute = None
with Cassette.use(path='custom_patches',
custom_patches=((Test, 'attribute', VCRHTTPSConnection),)): with Cassette.use(path="custom_patches", custom_patches=((Test, "attribute", VCRHTTPSConnection),)):
assert issubclass(Test.attribute, VCRHTTPSConnection) assert issubclass(Test.attribute, VCRHTTPSConnection)
assert VCRHTTPSConnection is not Test.attribute assert VCRHTTPSConnection is not Test.attribute
old_attribute = Test.attribute old_attribute = Test.attribute
with Cassette.use(path='custom_patches', with Cassette.use(path="custom_patches", custom_patches=((Test, "attribute", VCRHTTPSConnection),)):
custom_patches=((Test, 'attribute', VCRHTTPSConnection),)):
assert issubclass(Test.attribute, VCRHTTPSConnection) assert issubclass(Test.attribute, VCRHTTPSConnection)
assert VCRHTTPSConnection is not Test.attribute assert VCRHTTPSConnection is not Test.attribute
assert Test.attribute is not old_attribute assert Test.attribute is not old_attribute
@@ -212,15 +255,15 @@ def test_decorated_functions_are_reentrant():
info = {"second": False} info = {"second": False}
original_conn = httplib.HTTPConnection original_conn = httplib.HTTPConnection
@Cassette.use(path='whatever', inject=True) @Cassette.use(path="whatever", inject=True)
def test_function(cassette): def test_function(cassette):
if info['second']: if info["second"]:
assert httplib.HTTPConnection is not info['first_conn'] assert httplib.HTTPConnection is not info["first_conn"]
else: else:
info['first_conn'] = httplib.HTTPConnection info["first_conn"] = httplib.HTTPConnection
info['second'] = True info["second"] = True
test_function() test_function()
assert httplib.HTTPConnection is info['first_conn'] assert httplib.HTTPConnection is info["first_conn"]
test_function() test_function()
assert httplib.HTTPConnection is original_conn assert httplib.HTTPConnection is original_conn
@@ -229,43 +272,39 @@ def test_decorated_functions_are_reentrant():
def test_cassette_use_called_without_path_uses_function_to_generate_path(): def test_cassette_use_called_without_path_uses_function_to_generate_path():
@Cassette.use(inject=True) @Cassette.use(inject=True)
def function_name(cassette): def function_name(cassette):
assert cassette._path == 'function_name' assert cassette._path == "function_name"
function_name() function_name()
def test_path_transformer_with_function_path(): def test_path_transformer_with_function_path():
def path_transformer(path): def path_transformer(path):
return os.path.join('a', path) return os.path.join("a", path)
@Cassette.use(inject=True, path_transformer=path_transformer) @Cassette.use(inject=True, path_transformer=path_transformer)
def function_name(cassette): def function_name(cassette):
assert cassette._path == os.path.join('a', 'function_name') assert cassette._path == os.path.join("a", "function_name")
function_name() function_name()
def test_path_transformer_with_context_manager(): def test_path_transformer_with_context_manager():
with Cassette.use( with Cassette.use(path="b", path_transformer=lambda *args: "a") as cassette:
path='b', path_transformer=lambda *args: 'a' assert cassette._path == "a"
) as cassette:
assert cassette._path == 'a'
def test_path_transformer_None(): def test_path_transformer_None():
with Cassette.use( with Cassette.use(path="a", path_transformer=None) as cassette:
path='a', path_transformer=None, assert cassette._path == "a"
) as cassette:
assert cassette._path == 'a'
def test_func_path_generator(): def test_func_path_generator():
def generator(function): def generator(function):
return os.path.join(os.path.dirname(inspect.getfile(function)), return os.path.join(os.path.dirname(inspect.getfile(function)), function.__name__)
function.__name__)
@Cassette.use(inject=True, func_path_generator=generator) @Cassette.use(inject=True, func_path_generator=generator)
def function_name(cassette): def function_name(cassette):
assert cassette._path == os.path.join(os.path.dirname(__file__), 'function_name') assert cassette._path == os.path.join(os.path.dirname(__file__), "function_name")
function_name() function_name()
@@ -306,3 +345,51 @@ def test_use_as_decorator_on_generator():
yield 2 yield 2
assert list(test_function()) == [1, 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")]),
]

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

@@ -0,0 +1,69 @@
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 = (
"Can't overwrite existing cassette (%r) in your current record mode (%r).\n"
"No match for the request (%r) was found.\n"
"%s" % (cassette._path, cassette.record_mode, failed_request, expected_message)
)
assert exception_message == expected

View File

@@ -1,14 +1,17 @@
from six import BytesIO from io import BytesIO
from vcr.filters import ( from vcr.filters import (
remove_headers, replace_headers, remove_headers,
remove_query_parameters, replace_query_parameters, replace_headers,
remove_post_data_parameters, replace_post_data_parameters, remove_query_parameters,
decode_response replace_query_parameters,
remove_post_data_parameters,
replace_post_data_parameters,
decode_response,
) )
from vcr.compat import mock
from vcr.request import Request from vcr.request import Request
import gzip import gzip
import json import json
from unittest import mock
import zlib import zlib
@@ -20,31 +23,24 @@ def test_replace_headers():
# 4. replacing a header using a callable # 4. replacing a header using a callable
# 5. removing a header using a callable # 5. removing a header using a callable
# 6. replacing a header that doesn't exist # 6. replacing a header that doesn't exist
headers = { headers = {"one": ["keep"], "two": ["lose"], "three": ["change"], "four": ["shout"], "five": ["whisper"]}
'one': ['keep'], request = Request("GET", "http://google.com", "", headers)
'two': ['lose'], replace_headers(
'three': ['change'], request,
'four': ['shout'], [
'five': ['whisper'], ("two", None),
} ("three", "tada"),
request = Request('GET', 'http://google.com', '', headers) ("four", lambda key, value, request: value.upper()),
replace_headers(request, [ ("five", lambda key, value, request: None),
('two', None), ("six", "doesntexist"),
('three', 'tada'), ],
('four', lambda key, value, request: value.upper()), )
('five', lambda key, value, request: None), assert request.headers == {"one": "keep", "three": "tada", "four": "SHOUT"}
('six', 'doesntexist'),
])
assert request.headers == {
'one': 'keep',
'three': 'tada',
'four': 'SHOUT',
}
def test_replace_headers_empty(): def test_replace_headers_empty():
headers = {'hello': 'goodbye', 'secret': 'header'} headers = {"hello": "goodbye", "secret": "header"}
request = Request('GET', 'http://google.com', '', headers) request = Request("GET", "http://google.com", "", headers)
replace_headers(request, []) replace_headers(request, [])
assert request.headers == headers assert request.headers == headers
@@ -52,22 +48,20 @@ def test_replace_headers_empty():
def test_replace_headers_callable(): def test_replace_headers_callable():
# This goes beyond test_replace_headers() to ensure that the callable # This goes beyond test_replace_headers() to ensure that the callable
# receives the expected arguments. # receives the expected arguments.
headers = {'hey': 'there'} headers = {"hey": "there"}
request = Request('GET', 'http://google.com', '', headers) request = Request("GET", "http://google.com", "", headers)
callme = mock.Mock(return_value='ho') callme = mock.Mock(return_value="ho")
replace_headers(request, [('hey', callme)]) replace_headers(request, [("hey", callme)])
assert request.headers == {'hey': 'ho'} assert request.headers == {"hey": "ho"}
assert callme.call_args == ((), {'request': request, assert callme.call_args == ((), {"request": request, "key": "hey", "value": "there"})
'key': 'hey',
'value': 'there'})
def test_remove_headers(): def test_remove_headers():
# Test the backward-compatible API wrapper. # Test the backward-compatible API wrapper.
headers = {'hello': ['goodbye'], 'secret': ['header']} headers = {"hello": ["goodbye"], "secret": ["header"]}
request = Request('GET', 'http://google.com', '', headers) request = Request("GET", "http://google.com", "", headers)
remove_headers(request, ['secret']) remove_headers(request, ["secret"])
assert request.headers == {'hello': 'goodbye'} assert request.headers == {"hello": "goodbye"}
def test_replace_query_parameters(): def test_replace_query_parameters():
@@ -78,48 +72,45 @@ def test_replace_query_parameters():
# 4. replacing a parameter using a callable # 4. replacing a parameter using a callable
# 5. removing a parameter using a callable # 5. removing a parameter using a callable
# 6. replacing a parameter that doesn't exist # 6. replacing a parameter that doesn't exist
uri = 'http://g.com/?one=keep&two=lose&three=change&four=shout&five=whisper' uri = "http://g.com/?one=keep&two=lose&three=change&four=shout&five=whisper"
request = Request('GET', uri, '', {}) request = Request("GET", uri, "", {})
replace_query_parameters(request, [ replace_query_parameters(
('two', None), request,
('three', 'tada'), [
('four', lambda key, value, request: value.upper()), ("two", None),
('five', lambda key, value, request: None), ("three", "tada"),
('six', 'doesntexist'), ("four", lambda key, value, request: value.upper()),
]) ("five", lambda key, value, request: None),
assert request.query == [ ("six", "doesntexist"),
('four', 'SHOUT'), ],
('one', 'keep'), )
('three', 'tada'), assert request.query == [("four", "SHOUT"), ("one", "keep"), ("three", "tada")]
]
def test_remove_all_query_parameters(): def test_remove_all_query_parameters():
uri = 'http://g.com/?q=cowboys&w=1' uri = "http://g.com/?q=cowboys&w=1"
request = Request('GET', uri, '', {}) request = Request("GET", uri, "", {})
replace_query_parameters(request, [('w', None), ('q', None)]) replace_query_parameters(request, [("w", None), ("q", None)])
assert request.uri == 'http://g.com/' assert request.uri == "http://g.com/"
def test_replace_query_parameters_callable(): def test_replace_query_parameters_callable():
# This goes beyond test_replace_query_parameters() to ensure that the # This goes beyond test_replace_query_parameters() to ensure that the
# callable receives the expected arguments. # callable receives the expected arguments.
uri = 'http://g.com/?hey=there' uri = "http://g.com/?hey=there"
request = Request('GET', uri, '', {}) request = Request("GET", uri, "", {})
callme = mock.Mock(return_value='ho') callme = mock.Mock(return_value="ho")
replace_query_parameters(request, [('hey', callme)]) replace_query_parameters(request, [("hey", callme)])
assert request.uri == 'http://g.com/?hey=ho' assert request.uri == "http://g.com/?hey=ho"
assert callme.call_args == ((), {'request': request, assert callme.call_args == ((), {"request": request, "key": "hey", "value": "there"})
'key': 'hey',
'value': 'there'})
def test_remove_query_parameters(): def test_remove_query_parameters():
# Test the backward-compatible API wrapper. # Test the backward-compatible API wrapper.
uri = 'http://g.com/?q=cowboys&w=1' uri = "http://g.com/?q=cowboys&w=1"
request = Request('GET', uri, '', {}) request = Request("GET", uri, "", {})
remove_query_parameters(request, ['w']) remove_query_parameters(request, ["w"])
assert request.uri == 'http://g.com/?q=cowboys' assert request.uri == "http://g.com/?q=cowboys"
def test_replace_post_data_parameters(): def test_replace_post_data_parameters():
@@ -130,38 +121,58 @@ def test_replace_post_data_parameters():
# 4. replacing a parameter using a callable # 4. replacing a parameter using a callable
# 5. removing a parameter using a callable # 5. removing a parameter using a callable
# 6. replacing a parameter that doesn't exist # 6. replacing a parameter that doesn't exist
body = b'one=keep&two=lose&three=change&four=shout&five=whisper' body = b"one=keep&two=lose&three=change&four=shout&five=whisper"
request = Request('POST', 'http://google.com', body, {}) request = Request("POST", "http://google.com", body, {})
replace_post_data_parameters(request, [ replace_post_data_parameters(
('two', None), request,
('three', 'tada'), [
('four', lambda key, value, request: value.upper()), ("two", None),
('five', lambda key, value, request: None), ("three", "tada"),
('six', 'doesntexist'), ("four", lambda key, value, request: value.upper()),
]) ("five", lambda key, value, request: None),
assert request.body == b'one=keep&three=tada&four=SHOUT' ("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(): def test_remove_post_data_parameters():
# Test the backward-compatible API wrapper. # Test the backward-compatible API wrapper.
body = b'id=secret&foo=bar' body = b"id=secret&foo=bar"
request = Request('POST', 'http://google.com', body, {}) request = Request("POST", "http://google.com", body, {})
remove_post_data_parameters(request, ['id']) remove_post_data_parameters(request, ["id"])
assert request.body == b'foo=bar' assert request.body == b"foo=bar"
def test_preserve_multiple_post_data_parameters(): def test_preserve_multiple_post_data_parameters():
body = b'id=secret&foo=bar&foo=baz' body = b"id=secret&foo=bar&foo=baz"
request = Request('POST', 'http://google.com', body, {}) request = Request("POST", "http://google.com", body, {})
replace_post_data_parameters(request, [('id', None)]) replace_post_data_parameters(request, [("id", None)])
assert request.body == b'foo=bar&foo=baz' assert request.body == b"foo=bar&foo=baz"
def test_remove_all_post_data_parameters(): def test_remove_all_post_data_parameters():
body = b'id=secret&foo=bar' body = b"id=secret&foo=bar"
request = Request('POST', 'http://google.com', body, {}) request = Request("POST", "http://google.com", body, {})
replace_post_data_parameters(request, [('id', None), ('foo', None)]) replace_post_data_parameters(request, [("id", None), ("foo", None)])
assert request.body == b'' assert request.body == b""
def test_replace_json_post_data_parameters(): def test_replace_json_post_data_parameters():
@@ -173,16 +184,19 @@ def test_replace_json_post_data_parameters():
# 5. removing a parameter using a callable # 5. removing a parameter using a callable
# 6. replacing a parameter that doesn't exist # 6. replacing a parameter that doesn't exist
body = b'{"one": "keep", "two": "lose", "three": "change", "four": "shout", "five": "whisper"}' body = b'{"one": "keep", "two": "lose", "three": "change", "four": "shout", "five": "whisper"}'
request = Request('POST', 'http://google.com', body, {}) request = Request("POST", "http://google.com", body, {})
request.headers['Content-Type'] = 'application/json' request.headers["Content-Type"] = "application/json"
replace_post_data_parameters(request, [ replace_post_data_parameters(
('two', None), request,
('three', 'tada'), [
('four', lambda key, value, request: value.upper()), ("two", None),
('five', lambda key, value, request: None), ("three", "tada"),
('six', 'doesntexist'), ("four", lambda key, value, request: value.upper()),
]) ("five", lambda key, value, request: None),
request_data = json.loads(request.body.decode('utf-8')) ("six", "doesntexist"),
],
)
request_data = json.loads(request.body.decode("utf-8"))
expected_data = json.loads('{"one": "keep", "three": "tada", "four": "SHOUT"}') expected_data = json.loads('{"one": "keep", "three": "tada", "four": "SHOUT"}')
assert request_data == expected_data assert request_data == expected_data
@@ -190,85 +204,80 @@ def test_replace_json_post_data_parameters():
def test_remove_json_post_data_parameters(): def test_remove_json_post_data_parameters():
# Test the backward-compatible API wrapper. # Test the backward-compatible API wrapper.
body = b'{"id": "secret", "foo": "bar", "baz": "qux"}' body = b'{"id": "secret", "foo": "bar", "baz": "qux"}'
request = Request('POST', 'http://google.com', body, {}) request = Request("POST", "http://google.com", body, {})
request.headers['Content-Type'] = 'application/json' request.headers["Content-Type"] = "application/json"
remove_post_data_parameters(request, ['id']) remove_post_data_parameters(request, ["id"])
request_body_json = json.loads(request.body.decode('utf-8')) request_body_json = json.loads(request.body.decode("utf-8"))
expected_json = json.loads(b'{"foo": "bar", "baz": "qux"}'.decode('utf-8')) expected_json = json.loads(b'{"foo": "bar", "baz": "qux"}'.decode("utf-8"))
assert request_body_json == expected_json assert request_body_json == expected_json
def test_remove_all_json_post_data_parameters(): def test_remove_all_json_post_data_parameters():
body = b'{"id": "secret", "foo": "bar"}' body = b'{"id": "secret", "foo": "bar"}'
request = Request('POST', 'http://google.com', body, {}) request = Request("POST", "http://google.com", body, {})
request.headers['Content-Type'] = 'application/json' request.headers["Content-Type"] = "application/json"
replace_post_data_parameters(request, [('id', None), ('foo', None)]) replace_post_data_parameters(request, [("id", None), ("foo", None)])
assert request.body == b'{}' assert request.body == b"{}"
def test_decode_response_uncompressed(): def test_decode_response_uncompressed():
recorded_response = { recorded_response = {
"status": { "status": {"message": "OK", "code": 200},
"message": "OK",
"code": 200
},
"headers": { "headers": {
"content-length": ["10806"], "content-length": ["10806"],
"date": ["Fri, 24 Oct 2014 18:35:37 GMT"], "date": ["Fri, 24 Oct 2014 18:35:37 GMT"],
"content-type": ["text/html; charset=utf-8"], "content-type": ["text/html; charset=utf-8"],
}, },
"body": { "body": {"string": b""},
"string": b""
}
} }
assert decode_response(recorded_response) == recorded_response assert decode_response(recorded_response) == recorded_response
def test_decode_response_deflate(): def test_decode_response_deflate():
body = b'deflate message' body = b"deflate message"
deflate_response = { deflate_response = {
'body': {'string': zlib.compress(body)}, "body": {"string": zlib.compress(body)},
'headers': { "headers": {
'access-control-allow-credentials': ['true'], "access-control-allow-credentials": ["true"],
'access-control-allow-origin': ['*'], "access-control-allow-origin": ["*"],
'connection': ['keep-alive'], "connection": ["keep-alive"],
'content-encoding': ['deflate'], "content-encoding": ["deflate"],
'content-length': ['177'], "content-length": ["177"],
'content-type': ['application/json'], "content-type": ["application/json"],
'date': ['Wed, 02 Dec 2015 19:44:32 GMT'], "date": ["Wed, 02 Dec 2015 19:44:32 GMT"],
'server': ['nginx'] "server": ["nginx"],
}, },
'status': {'code': 200, 'message': 'OK'} "status": {"code": 200, "message": "OK"},
} }
decoded_response = decode_response(deflate_response) decoded_response = decode_response(deflate_response)
assert decoded_response['body']['string'] == body assert decoded_response["body"]["string"] == body
assert decoded_response['headers']['content-length'] == [str(len(body))] assert decoded_response["headers"]["content-length"] == [str(len(body))]
def test_decode_response_gzip(): def test_decode_response_gzip():
body = b'gzip message' body = b"gzip message"
buf = BytesIO() buf = BytesIO()
f = gzip.GzipFile('a', fileobj=buf, mode='wb') f = gzip.GzipFile("a", fileobj=buf, mode="wb")
f.write(body) f.write(body)
f.close() f.close()
compressed_body = buf.getvalue() compressed_body = buf.getvalue()
buf.close() buf.close()
gzip_response = { gzip_response = {
'body': {'string': compressed_body}, "body": {"string": compressed_body},
'headers': { "headers": {
'access-control-allow-credentials': ['true'], "access-control-allow-credentials": ["true"],
'access-control-allow-origin': ['*'], "access-control-allow-origin": ["*"],
'connection': ['keep-alive'], "connection": ["keep-alive"],
'content-encoding': ['gzip'], "content-encoding": ["gzip"],
'content-length': ['177'], "content-length": ["177"],
'content-type': ['application/json'], "content-type": ["application/json"],
'date': ['Wed, 02 Dec 2015 19:44:32 GMT'], "date": ["Wed, 02 Dec 2015 19:44:32 GMT"],
'server': ['nginx'] "server": ["nginx"],
}, },
'status': {'code': 200, 'message': 'OK'} "status": {"code": 200, "message": "OK"},
} }
decoded_response = decode_response(gzip_response) decoded_response = decode_response(gzip_response)
assert decoded_response['body']['string'] == body assert decoded_response["body"]["string"] == body
assert decoded_response['headers']['content-length'] == [str(len(body))] assert decoded_response["headers"]["content-length"] == [str(len(body))]

View File

@@ -4,16 +4,14 @@ from vcr.request import Request
def test_serialize_binary(): def test_serialize_binary():
request = Request( request = Request(method="GET", uri="http://localhost/", body="", headers={})
method='GET', cassette = {"requests": [request], "responses": [{"body": b"\x8c"}]}
uri='http://localhost/',
body='',
headers={},
)
cassette = {'requests': [request], 'responses': [{'body': b'\x8c'}]}
with pytest.raises(Exception) as e: with pytest.raises(Exception) as e:
serialize(cassette) 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 \ HTTP interaction contain binary data? If so, use a different \
serializer (like the yaml serializer) for this request" serializer (like the yaml serializer) for this request"
)

View File

@@ -1,4 +1,5 @@
import itertools import itertools
from unittest import mock
import pytest import pytest
@@ -8,152 +9,266 @@ from vcr import request
# the dict contains requests with corresponding to its key difference # the dict contains requests with corresponding to its key difference
# with 'base' request. # with 'base' request.
REQUESTS = { REQUESTS = {
'base': request.Request('GET', 'http://host.com/p?a=b', '', {}), "base": request.Request("GET", "http://host.com/p?a=b", "", {}),
'method': request.Request('POST', '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', '', {}), "scheme": request.Request("GET", "https://host.com:80/p?a=b", "", {}),
'host': request.Request('GET', 'http://another-host.com/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', '', {}), "port": request.Request("GET", "http://host.com:90/p?a=b", "", {}),
'path': request.Request('GET', 'http://host.com/x?a=b', '', {}), "path": request.Request("GET", "http://host.com/x?a=b", "", {}),
'query': request.Request('GET', 'http://host.com/p?c=d', '', {}), "query": request.Request("GET", "http://host.com/p?c=d", "", {}),
} }
def assert_matcher(matcher_name): def assert_matcher(matcher_name):
matcher = getattr(matchers, matcher_name) matcher = getattr(matchers, matcher_name)
for k1, k2 in itertools.permutations(REQUESTS, 2): for k1, k2 in itertools.permutations(REQUESTS, 2):
matched = matcher(REQUESTS[k1], REQUESTS[k2]) expecting_assertion_error = matcher_name in {k1, k2}
if matcher_name in {k1, k2}: if expecting_assertion_error:
assert not matched with pytest.raises(AssertionError):
matcher(REQUESTS[k1], REQUESTS[k2])
else: else:
assert matched assert matcher(REQUESTS[k1], REQUESTS[k2]) is None
def test_uri_matcher(): def test_uri_matcher():
for k1, k2 in itertools.permutations(REQUESTS, 2): for k1, k2 in itertools.permutations(REQUESTS, 2):
matched = matchers.uri(REQUESTS[k1], REQUESTS[k2]) expecting_assertion_error = {k1, k2} != {"base", "method"}
if {k1, k2} != {'base', 'method'}: if expecting_assertion_error:
assert not matched with pytest.raises(AssertionError):
matchers.uri(REQUESTS[k1], REQUESTS[k2])
else: else:
assert matched assert matchers.uri(REQUESTS[k1], REQUESTS[k2]) is None
req1_body = (b"<?xml version='1.0'?><methodCall><methodName>test</methodName>" req1_body = (
b"<params><param><value><array><data><value><struct>" b"<?xml version='1.0'?><methodCall><methodName>test</methodName>"
b"<member><name>a</name><value><string>1</string></value></member>" 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>") b"<member><name>b</name><value><string>2</string></value></member>"
req2_body = (b"<?xml version='1.0'?><methodCall><methodName>test</methodName>" b"</struct></value></data></array></value></param></params></methodCall>"
b"<params><param><value><array><data><value><struct>" )
b"<member><name>b</name><value><string>2</string></value></member>" req2_body = (
b"<member><name>a</name><value><string>1</string></value></member>" b"<?xml version='1.0'?><methodCall><methodName>test</methodName>"
b"</struct></value></data></array></value></param></params></methodCall>") 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",
}
@pytest.mark.parametrize("r1, r2", [ @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/", "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', request.Request(
{'Content-Type': 'application/x-www-form-urlencoded'} "POST", "http://host.com/", "a=1&b=2", {"Content-Type": "application/x-www-form-urlencoded"}
) ),
), request.Request(
( "POST", "http://host.com/", "b=2&a=1", {"Content-Type": "application/x-www-form-urlencoded"}
request.Request( ),
'POST', 'http://host.com/', '{"a": 1, "b": 2}',
{'Content-Type': 'application/json'}
), ),
request.Request( (
'POST', 'http://host.com/', '{"b": 2, "a": 1}', request.Request("POST", "http://host.com/", "123", {}),
{'content-type': 'application/json'} request.Request("POST", "http://another-host.com/", "123", {"Some-Header": "value"}),
)
),
(
request.Request(
'POST', 'http://host.com/', req1_body,
{'User-Agent': 'xmlrpclib', 'Content-Type': 'text/xml'}
), ),
request.Request( (
'POST', 'http://host.com/', req2_body, request.Request(
{'user-agent': 'somexmlrpc', 'content-type': 'text/xml'} "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/', request.Request(
'{"b": 2, "a": 1}', {'content-type': 'application/json'} "POST", "http://host.com/", '{"a": 1, "b": 2}', {"Content-Type": "application/json"}
) ),
) request.Request(
]) "POST", "http://host.com/", '{"b": 2, "a": 1}', {"content-type": "application/json"}
),
),
(
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),
),
],
)
def test_body_matcher_does_match(r1, r2): def test_body_matcher_does_match(r1, r2):
assert matchers.body(r1, r2) assert matchers.body(r1, r2) is None
@pytest.mark.parametrize("r1, r2", [ @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": 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/', request.Request(
'{"b": 2, "a": 1}', {'content-type': 'application/json'} "POST", "http://host.com/", '{"a": 1, "b": 3}', {"Content-Type": "application/json"}
) ),
), request.Request(
( "POST", "http://host.com/", '{"b": 2, "a": 1}', {"content-type": "application/json"}
request.Request( ),
'POST', 'http://host.com/', req1_body, {'Content-Type': 'text/xml'}
), ),
request.Request( (
'POST', 'http://host.com/', req2_body, {'content-type': 'text/xml'} 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): def test_body_match_does_not_match(r1, r2):
assert not matchers.body(r1, r2) with pytest.raises(AssertionError):
matchers.body(r1, r2)
def test_query_matcher(): def test_query_matcher():
req1 = request.Request('GET', 'http://host.com/?a=b&c=d', '', {}) req1 = request.Request("GET", "http://host.com/?a=b&c=d", "", {})
req2 = request.Request('GET', 'http://host.com/?c=d&a=b', '', {}) req2 = request.Request("GET", "http://host.com/?c=d&a=b", "", {})
assert matchers.query(req1, req2) assert matchers.query(req1, req2) is None
req1 = request.Request('GET', 'http://host.com/?a=b&a=b&c=d', '', {}) 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', '', {}) 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', '', {}) req3 = request.Request("GET", "http://host.com/?c=d&a=b&a=b", "", {})
assert matchers.query(req1, req2) assert matchers.query(req1, req2) is None
assert matchers.query(req1, req3) assert matchers.query(req1, req3) is None
def test_metchers(): def test_matchers():
assert_matcher('method') assert_matcher("method")
assert_matcher('scheme') assert_matcher("scheme")
assert_matcher('host') assert_matcher("host")
assert_matcher('port') assert_matcher("port")
assert_matcher('path') assert_matcher("path")
assert_matcher('query') 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

@@ -5,35 +5,41 @@ import yaml
import vcr.migration 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): def test_try_migrate_with_json(tmpdir):
cassette = tmpdir.join('cassette.json').strpath cassette = tmpdir.join("cassette.json").strpath
shutil.copy('tests/fixtures/migration/old_cassette.json', cassette) shutil.copy("tests/fixtures/migration/old_cassette.json", cassette)
assert vcr.migration.try_migrate(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", "r") as f:
expected_json = json.load(f) expected_json = json.load(f)
with open(cassette, 'r') as f: with open(cassette, "r") as f:
actual_json = json.load(f) actual_json = json.load(f)
assert actual_json == expected_json assert actual_json == expected_json
def test_try_migrate_with_yaml(tmpdir): def test_try_migrate_with_yaml(tmpdir):
cassette = tmpdir.join('cassette.yaml').strpath cassette = tmpdir.join("cassette.yaml").strpath
shutil.copy('tests/fixtures/migration/old_cassette.yaml', cassette) shutil.copy("tests/fixtures/migration/old_cassette.yaml", cassette)
assert vcr.migration.try_migrate(cassette) assert vcr.migration.try_migrate(cassette)
with open('tests/fixtures/migration/new_cassette.yaml', 'r') as f: with open("tests/fixtures/migration/new_cassette.yaml", "r") as f:
expected_yaml = yaml.load(f) expected_yaml = yaml.load(f, Loader=Loader)
with open(cassette, 'r') as f: with open(cassette, "r") as f:
actual_yaml = yaml.load(f) actual_yaml = yaml.load(f, Loader=Loader)
assert actual_yaml == expected_yaml assert actual_yaml == expected_yaml
def test_try_migrate_with_invalid_or_new_cassettes(tmpdir): def test_try_migrate_with_invalid_or_new_cassettes(tmpdir):
cassette = tmpdir.join('cassette').strpath cassette = tmpdir.join("cassette").strpath
files = [ files = [
'tests/fixtures/migration/not_cassette.txt', "tests/fixtures/migration/not_cassette.txt",
'tests/fixtures/migration/new_cassette.yaml', "tests/fixtures/migration/new_cassette.yaml",
'tests/fixtures/migration/new_cassette.json', "tests/fixtures/migration/new_cassette.json",
] ]
for file_path in files: for file_path in files:
shutil.copy(file_path, cassette) shutil.copy(file_path, cassette)

View File

@@ -4,20 +4,26 @@ from vcr.persisters.filesystem import FilesystemPersister
from vcr.serializers import jsonserializer, yamlserializer from vcr.serializers import jsonserializer, yamlserializer
@pytest.mark.parametrize("cassette_path, serializer", [ @pytest.mark.parametrize(
('tests/fixtures/migration/old_cassette.json', jsonserializer), "cassette_path, serializer",
('tests/fixtures/migration/old_cassette.yaml', yamlserializer), [
]) ("tests/fixtures/migration/old_cassette.json", jsonserializer),
("tests/fixtures/migration/old_cassette.yaml", yamlserializer),
],
)
def test_load_cassette_with_old_cassettes(cassette_path, serializer): def test_load_cassette_with_old_cassettes(cassette_path, serializer):
with pytest.raises(ValueError) as excinfo: with pytest.raises(ValueError) as excinfo:
FilesystemPersister.load_cassette(cassette_path, serializer) FilesystemPersister.load_cassette(cassette_path, serializer)
assert "run the migration script" in excinfo.exconly() assert "run the migration script" in excinfo.exconly()
@pytest.mark.parametrize("cassette_path, serializer", [ @pytest.mark.parametrize(
('tests/fixtures/migration/not_cassette.txt', jsonserializer), "cassette_path, serializer",
('tests/fixtures/migration/not_cassette.txt', yamlserializer), [
]) ("tests/fixtures/migration/not_cassette.txt", jsonserializer),
("tests/fixtures/migration/not_cassette.txt", yamlserializer),
],
)
def test_load_cassette_with_invalid_cassettes(cassette_path, serializer): def test_load_cassette_with_invalid_cassettes(cassette_path, serializer):
with pytest.raises(Exception) as excinfo: with pytest.raises(Exception) as excinfo:
FilesystemPersister.load_cassette(cassette_path, serializer) FilesystemPersister.load_cassette(cassette_path, serializer)

View File

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

View File

@@ -1,13 +1,67 @@
# coding: UTF-8 # coding: UTF-8
import io
from vcr.stubs import VCRHTTPResponse from vcr.stubs import VCRHTTPResponse
def test_response_should_have_headers_field(): def test_response_should_have_headers_field():
recorded_response = { recorded_response = {
"status": { "status": {"message": "OK", "code": 200},
"message": "OK", "headers": {
"code": 200 "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": { "headers": {
"content-length": ["0"], "content-length": ["0"],
"server": ["gunicorn/18.0"], "server": ["gunicorn/18.0"],
@@ -18,51 +72,28 @@ def test_response_should_have_headers_field():
"content-type": ["text/html; charset=utf-8"], "content-type": ["text/html; charset=utf-8"],
}, },
"body": { "body": {
"string": b"" "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 "
},
} }
response = VCRHTTPResponse(recorded_response) vcr_response = VCRHTTPResponse(recorded_response)
handle = io.TextIOWrapper(io.BufferedReader(vcr_response), encoding="utf-8")
assert response.headers is not None handle = iter(handle)
articles = [line for line in handle]
assert len(articles) > 1
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"

View File

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

View File

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

View File

@@ -1,10 +1,10 @@
from unittest import mock
import os import os
import pytest import pytest
from six.moves import http_client as httplib import http.client as httplib
from vcr import VCR, use_cassette from vcr import VCR, mode, use_cassette
from vcr.compat import mock
from vcr.request import Request from vcr.request import Request
from vcr.stubs import VCRHTTPSConnection from vcr.stubs import VCRHTTPSConnection
from vcr.patch import _HTTPConnection, force_reset from vcr.patch import _HTTPConnection, force_reset
@@ -14,75 +14,75 @@ def test_vcr_use_cassette():
record_mode = mock.Mock() record_mode = mock.Mock()
test_vcr = VCR(record_mode=record_mode) test_vcr = VCR(record_mode=record_mode)
with mock.patch( with mock.patch(
'vcr.cassette.Cassette.load', "vcr.cassette.Cassette.load", return_value=mock.MagicMock(inject=False)
return_value=mock.MagicMock(inject=False)
) as mock_cassette_load: ) as mock_cassette_load:
@test_vcr.use_cassette('test') @test_vcr.use_cassette("test")
def function(): def function():
pass pass
assert mock_cassette_load.call_count == 0 assert mock_cassette_load.call_count == 0
function() function()
assert mock_cassette_load.call_args[1]['record_mode'] is record_mode assert mock_cassette_load.call_args[1]["record_mode"] is record_mode
# Make sure that calls to function now use cassettes with the # Make sure that calls to function now use cassettes with the
# new filter_header_settings # new filter_header_settings
test_vcr.record_mode = mock.Mock() test_vcr.record_mode = mock.Mock()
function() function()
assert mock_cassette_load.call_args[1]['record_mode'] == test_vcr.record_mode assert mock_cassette_load.call_args[1]["record_mode"] == test_vcr.record_mode
# Ensure that explicitly provided arguments still supercede # Ensure that explicitly provided arguments still supercede
# those on the vcr. # those on the vcr.
new_record_mode = mock.Mock() new_record_mode = mock.Mock()
with test_vcr.use_cassette('test', record_mode=new_record_mode) as cassette: with test_vcr.use_cassette("test", record_mode=new_record_mode) as cassette:
assert cassette.record_mode == new_record_mode assert cassette.record_mode == new_record_mode
def test_vcr_before_record_request_params(): def test_vcr_before_record_request_params():
base_path = 'http://httpbin.org/' base_path = "http://httpbin.org/"
def before_record_cb(request): def before_record_cb(request):
if request.path != '/get': if request.path != "/get":
return request return request
test_vcr = VCR(filter_headers=('cookie', ('bert', 'ernie')), test_vcr = VCR(
before_record_request=before_record_cb, filter_headers=("cookie", ("bert", "ernie")),
ignore_hosts=('www.test.com',), ignore_localhost=True, before_record_request=before_record_cb,
filter_query_parameters=('foo', ('tom', 'jerry')), ignore_hosts=("www.test.com",),
filter_post_data_parameters=('posted', ('no', 'trespassing'))) ignore_localhost=True,
filter_query_parameters=("foo", ("tom", "jerry")),
filter_post_data_parameters=("posted", ("no", "trespassing")),
)
with test_vcr.use_cassette('test') as cassette: with test_vcr.use_cassette("test") as cassette:
# Test explicit before_record_cb # Test explicit before_record_cb
request_get = Request('GET', base_path + 'get', '', {}) request_get = Request("GET", base_path + "get", "", {})
assert cassette.filter_request(request_get) is None assert cassette.filter_request(request_get) is None
request = Request('GET', base_path + 'get2', '', {}) request = Request("GET", base_path + "get2", "", {})
assert cassette.filter_request(request) is not None assert cassette.filter_request(request) is not None
# Test filter_query_parameters # Test filter_query_parameters
request = Request('GET', base_path + '?foo=bar', '', {}) request = Request("GET", base_path + "?foo=bar", "", {})
assert cassette.filter_request(request).query == [] assert cassette.filter_request(request).query == []
request = Request('GET', base_path + '?tom=nobody', '', {}) request = Request("GET", base_path + "?tom=nobody", "", {})
assert cassette.filter_request(request).query == [('tom', 'jerry')] assert cassette.filter_request(request).query == [("tom", "jerry")]
# Test filter_headers # Test filter_headers
request = Request('GET', base_path + '?foo=bar', '', request = Request(
{'cookie': 'test', 'other': 'fun', 'bert': 'nobody'}) "GET", base_path + "?foo=bar", "", {"cookie": "test", "other": "fun", "bert": "nobody"}
assert (cassette.filter_request(request).headers == )
{'other': 'fun', 'bert': 'ernie'}) assert cassette.filter_request(request).headers == {"other": "fun", "bert": "ernie"}
# Test ignore_hosts # Test ignore_hosts
request = Request('GET', 'http://www.test.com' + '?foo=bar', '', request = Request("GET", "http://www.test.com" + "?foo=bar", "", {"cookie": "test", "other": "fun"})
{'cookie': 'test', 'other': 'fun'})
assert cassette.filter_request(request) is None assert cassette.filter_request(request) is None
# Test ignore_localhost # Test ignore_localhost
request = Request('GET', 'http://localhost:8000' + '?foo=bar', '', request = Request("GET", "http://localhost:8000" + "?foo=bar", "", {"cookie": "test", "other": "fun"})
{'cookie': 'test', 'other': 'fun'})
assert cassette.filter_request(request) is None assert cassette.filter_request(request) is None
with test_vcr.use_cassette('test', before_record_request=None) as cassette: with test_vcr.use_cassette("test", before_record_request=None) as cassette:
# Test that before_record can be overwritten in context manager. # Test that before_record can be overwritten in context manager.
assert cassette.filter_request(request_get) is not None assert cassette.filter_request(request_get) is not None
@@ -90,16 +90,16 @@ def test_vcr_before_record_request_params():
def test_vcr_before_record_response_iterable(): def test_vcr_before_record_response_iterable():
# Regression test for #191 # Regression test for #191
request = Request('GET', '/', '', {}) request = Request("GET", "/", "", {})
response = object() # just can't be None response = object() # just can't be None
# Prevent actually saving the cassette # Prevent actually saving the cassette
with mock.patch('vcr.cassette.FilesystemPersister.save_cassette'): with mock.patch("vcr.cassette.FilesystemPersister.save_cassette"):
# Baseline: non-iterable before_record_response should work # Baseline: non-iterable before_record_response should work
mock_filter = mock.Mock() mock_filter = mock.Mock()
vcr = VCR(before_record_response=mock_filter) vcr = VCR(before_record_response=mock_filter)
with vcr.use_cassette('test') as cassette: with vcr.use_cassette("test") as cassette:
assert mock_filter.call_count == 0 assert mock_filter.call_count == 0
cassette.append(request, response) cassette.append(request, response)
assert mock_filter.call_count == 1 assert mock_filter.call_count == 1
@@ -107,22 +107,22 @@ def test_vcr_before_record_response_iterable():
# Regression test: iterable before_record_response should work too # Regression test: iterable before_record_response should work too
mock_filter = mock.Mock() mock_filter = mock.Mock()
vcr = VCR(before_record_response=(mock_filter,)) vcr = VCR(before_record_response=(mock_filter,))
with vcr.use_cassette('test') as cassette: with vcr.use_cassette("test") as cassette:
assert mock_filter.call_count == 0 assert mock_filter.call_count == 0
cassette.append(request, response) cassette.append(request, response)
assert mock_filter.call_count == 1 assert mock_filter.call_count == 1
def test_before_record_response_as_filter(): def test_before_record_response_as_filter():
request = Request('GET', '/', '', {}) request = Request("GET", "/", "", {})
response = object() # just can't be None response = object() # just can't be None
# Prevent actually saving the cassette # Prevent actually saving the cassette
with mock.patch('vcr.cassette.FilesystemPersister.save_cassette'): with mock.patch("vcr.cassette.FilesystemPersister.save_cassette"):
filter_all = mock.Mock(return_value=None) filter_all = mock.Mock(return_value=None)
vcr = VCR(before_record_response=filter_all) vcr = VCR(before_record_response=filter_all)
with vcr.use_cassette('test') as cassette: with vcr.use_cassette("test") as cassette:
cassette.append(request, response) cassette.append(request, response)
assert cassette.data == [] assert cassette.data == []
assert not cassette.dirty assert not cassette.dirty
@@ -132,22 +132,22 @@ def test_vcr_path_transformer():
# Regression test for #199 # Regression test for #199
# Prevent actually saving the cassette # Prevent actually saving the cassette
with mock.patch('vcr.cassette.FilesystemPersister.save_cassette'): with mock.patch("vcr.cassette.FilesystemPersister.save_cassette"):
# Baseline: path should be unchanged # Baseline: path should be unchanged
vcr = VCR() vcr = VCR()
with vcr.use_cassette('test') as cassette: with vcr.use_cassette("test") as cassette:
assert cassette._path == 'test' assert cassette._path == "test"
# Regression test: path_transformer=None should do the same. # Regression test: path_transformer=None should do the same.
vcr = VCR(path_transformer=None) vcr = VCR(path_transformer=None)
with vcr.use_cassette('test') as cassette: with vcr.use_cassette("test") as cassette:
assert cassette._path == 'test' assert cassette._path == "test"
# and it should still work with cassette_library_dir # and it should still work with cassette_library_dir
vcr = VCR(cassette_library_dir='/foo') vcr = VCR(cassette_library_dir="/foo")
with vcr.use_cassette('test') as cassette: with vcr.use_cassette("test") as cassette:
assert cassette._path == '/foo/test' assert os.path.abspath(cassette._path) == os.path.abspath("/foo/test")
@pytest.fixture @pytest.fixture
@@ -155,7 +155,7 @@ def random_fixture():
return 1 return 1
@use_cassette('test') @use_cassette("test")
def test_fixtures_with_use_cassette(random_fixture): def test_fixtures_with_use_cassette(random_fixture):
# Applying a decorator to a test function that requests features can cause # Applying a decorator to a test function that requests features can cause
# problems if the decorator does not preserve the signature of the original # problems if the decorator does not preserve the signature of the original
@@ -170,18 +170,16 @@ def test_fixtures_with_use_cassette(random_fixture):
def test_custom_patchers(): def test_custom_patchers():
class Test(object): class Test:
attribute = None attribute = None
attribute2 = None attribute2 = None
test_vcr = VCR(custom_patches=((Test, 'attribute', VCRHTTPSConnection),))
with test_vcr.use_cassette('custom_patches'): test_vcr = VCR(custom_patches=((Test, "attribute", VCRHTTPSConnection),))
with test_vcr.use_cassette("custom_patches"):
assert issubclass(Test.attribute, VCRHTTPSConnection) assert issubclass(Test.attribute, VCRHTTPSConnection)
assert VCRHTTPSConnection is not Test.attribute assert VCRHTTPSConnection is not Test.attribute
with test_vcr.use_cassette( with test_vcr.use_cassette("custom_patches", custom_patches=((Test, "attribute2", VCRHTTPSConnection),)):
'custom_patches',
custom_patches=((Test, 'attribute2', VCRHTTPSConnection),)
):
assert issubclass(Test.attribute, VCRHTTPSConnection) assert issubclass(Test.attribute, VCRHTTPSConnection)
assert VCRHTTPSConnection is not Test.attribute assert VCRHTTPSConnection is not Test.attribute
assert Test.attribute is Test.attribute2 assert Test.attribute is Test.attribute2
@@ -190,11 +188,11 @@ def test_custom_patchers():
def test_inject_cassette(): def test_inject_cassette():
vcr = VCR(inject_cassette=True) vcr = VCR(inject_cassette=True)
@vcr.use_cassette('test', record_mode='once') @vcr.use_cassette("test", record_mode=mode.ONCE)
def with_cassette_injected(cassette): def with_cassette_injected(cassette):
assert cassette.record_mode == 'once' assert cassette.record_mode == mode.ONCE
@vcr.use_cassette('test', record_mode='once', inject_cassette=False) @vcr.use_cassette("test", record_mode=mode.ONCE, inject_cassette=False)
def without_cassette_injected(): def without_cassette_injected():
pass pass
@@ -203,92 +201,92 @@ def test_inject_cassette():
def test_with_current_defaults(): def test_with_current_defaults():
vcr = VCR(inject_cassette=True, record_mode='once') vcr = VCR(inject_cassette=True, record_mode=mode.ONCE)
@vcr.use_cassette('test', with_current_defaults=False) @vcr.use_cassette("test", with_current_defaults=False)
def changing_defaults(cassette, checks): def changing_defaults(cassette, checks):
checks(cassette) checks(cassette)
@vcr.use_cassette('test', with_current_defaults=True) @vcr.use_cassette("test", with_current_defaults=True)
def current_defaults(cassette, checks): def current_defaults(cassette, checks):
checks(cassette) checks(cassette)
def assert_record_mode_once(cassette): def assert_record_mode_once(cassette):
assert cassette.record_mode == 'once' assert cassette.record_mode == mode.ONCE
def assert_record_mode_all(cassette): def assert_record_mode_all(cassette):
assert cassette.record_mode == 'all' assert cassette.record_mode == mode.ALL
changing_defaults(assert_record_mode_once) changing_defaults(assert_record_mode_once)
current_defaults(assert_record_mode_once) current_defaults(assert_record_mode_once)
vcr.record_mode = 'all' vcr.record_mode = "all"
changing_defaults(assert_record_mode_all) changing_defaults(assert_record_mode_all)
current_defaults(assert_record_mode_once) current_defaults(assert_record_mode_once)
def test_cassette_library_dir_with_decoration_and_no_explicit_path(): def test_cassette_library_dir_with_decoration_and_no_explicit_path():
library_dir = '/libary_dir' library_dir = "/libary_dir"
vcr = VCR(inject_cassette=True, cassette_library_dir=library_dir) vcr = VCR(inject_cassette=True, cassette_library_dir=library_dir)
@vcr.use_cassette() @vcr.use_cassette()
def function_name(cassette): def function_name(cassette):
assert cassette._path == os.path.join(library_dir, 'function_name') assert cassette._path == os.path.join(library_dir, "function_name")
function_name() function_name()
def test_cassette_library_dir_with_decoration_and_explicit_path(): def test_cassette_library_dir_with_decoration_and_explicit_path():
library_dir = '/libary_dir' library_dir = "/libary_dir"
vcr = VCR(inject_cassette=True, cassette_library_dir=library_dir) vcr = VCR(inject_cassette=True, cassette_library_dir=library_dir)
@vcr.use_cassette(path='custom_name') @vcr.use_cassette(path="custom_name")
def function_name(cassette): def function_name(cassette):
assert cassette._path == os.path.join(library_dir, 'custom_name') assert cassette._path == os.path.join(library_dir, "custom_name")
function_name() function_name()
def test_cassette_library_dir_with_decoration_and_super_explicit_path(): def test_cassette_library_dir_with_decoration_and_super_explicit_path():
library_dir = '/libary_dir' library_dir = "/libary_dir"
vcr = VCR(inject_cassette=True, cassette_library_dir=library_dir) vcr = VCR(inject_cassette=True, cassette_library_dir=library_dir)
@vcr.use_cassette(path=os.path.join(library_dir, 'custom_name')) @vcr.use_cassette(path=os.path.join(library_dir, "custom_name"))
def function_name(cassette): def function_name(cassette):
assert cassette._path == os.path.join(library_dir, 'custom_name') assert cassette._path == os.path.join(library_dir, "custom_name")
function_name() function_name()
def test_cassette_library_dir_with_path_transformer(): def test_cassette_library_dir_with_path_transformer():
library_dir = '/libary_dir' library_dir = "/libary_dir"
vcr = VCR(inject_cassette=True, cassette_library_dir=library_dir, vcr = VCR(
path_transformer=lambda path: path + '.json') inject_cassette=True, cassette_library_dir=library_dir, path_transformer=lambda path: path + ".json"
)
@vcr.use_cassette() @vcr.use_cassette()
def function_name(cassette): def function_name(cassette):
assert cassette._path == os.path.join(library_dir, 'function_name.json') assert cassette._path == os.path.join(library_dir, "function_name.json")
function_name() function_name()
def test_use_cassette_with_no_extra_invocation(): def test_use_cassette_with_no_extra_invocation():
vcr = VCR(inject_cassette=True, cassette_library_dir='/') vcr = VCR(inject_cassette=True, cassette_library_dir="/")
@vcr.use_cassette @vcr.use_cassette
def function_name(cassette): def function_name(cassette):
assert cassette._path == os.path.join('/', 'function_name') assert cassette._path == os.path.join("/", "function_name")
function_name() function_name()
def test_path_transformer(): def test_path_transformer():
vcr = VCR(inject_cassette=True, cassette_library_dir='/', vcr = VCR(inject_cassette=True, cassette_library_dir="/", path_transformer=lambda x: x + "_test")
path_transformer=lambda x: x + '_test')
@vcr.use_cassette @vcr.use_cassette
def function_name(cassette): def function_name(cassette):
assert cassette._path == os.path.join('/', 'function_name_test') assert cassette._path == os.path.join("/", "function_name_test")
function_name() function_name()
@@ -298,32 +296,31 @@ def test_cassette_name_generator_defaults_to_using_module_function_defined_in():
@vcr.use_cassette @vcr.use_cassette
def function_name(cassette): def function_name(cassette):
assert cassette._path == os.path.join(os.path.dirname(__file__), assert cassette._path == os.path.join(os.path.dirname(__file__), "function_name")
'function_name')
function_name() function_name()
def test_ensure_suffix(): def test_ensure_suffix():
vcr = VCR(inject_cassette=True, path_transformer=VCR.ensure_suffix('.yaml')) vcr = VCR(inject_cassette=True, path_transformer=VCR.ensure_suffix(".yaml"))
@vcr.use_cassette @vcr.use_cassette
def function_name(cassette): def function_name(cassette):
assert cassette._path == os.path.join(os.path.dirname(__file__), assert cassette._path == os.path.join(os.path.dirname(__file__), "function_name.yaml")
'function_name.yaml')
function_name() function_name()
def test_additional_matchers(): def test_additional_matchers():
vcr = VCR(match_on=('uri',), inject_cassette=True) vcr = VCR(match_on=("uri",), inject_cassette=True)
@vcr.use_cassette @vcr.use_cassette
def function_defaults(cassette): def function_defaults(cassette):
assert set(cassette._match_on) == {vcr.matchers['uri']} assert set(cassette._match_on) == {vcr.matchers["uri"]}
@vcr.use_cassette(additional_matchers=('body',)) @vcr.use_cassette(additional_matchers=("body",))
def function_additional(cassette): def function_additional(cassette):
assert set(cassette._match_on) == {vcr.matchers['uri'], vcr.matchers['body']} assert set(cassette._match_on) == {vcr.matchers["uri"], vcr.matchers["body"]}
function_defaults() function_defaults()
function_additional() function_additional()
@@ -331,7 +328,7 @@ def test_additional_matchers():
def test_decoration_should_respect_function_return_value(): def test_decoration_should_respect_function_return_value():
vcr = VCR() vcr = VCR()
ret = 'a-return-value' ret = "a-return-value"
@vcr.use_cassette @vcr.use_cassette
def function_with_return(): def function_with_return():
@@ -341,7 +338,6 @@ def test_decoration_should_respect_function_return_value():
class TestVCRClass(VCR().test_case()): class TestVCRClass(VCR().test_case()):
def no_decoration(self): def no_decoration(self):
assert httplib.HTTPConnection == _HTTPConnection assert httplib.HTTPConnection == _HTTPConnection
self.test_dynamically_added() self.test_dynamically_added()

View File

@@ -0,0 +1,16 @@
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
if sys.version_info[0] == 2:
assert len(recwarn) == 1
assert issubclass(recwarn[0].category, DeprecationWarning)
else:
assert len(recwarn) == 0

84
tox.ini
View File

@@ -1,32 +1,94 @@
[tox] [tox]
envlist = {py27,py35,py36,pypy}-{flakes,requests27,httplib2,urllib3121,tornado4,boto3,aiohttp} skip_missing_interpreters=true
envlist =
cov-clean,
lint,
{py35,py36,py37,py38}-{requests,httplib2,urllib3,tornado4,boto3,aiohttp},
{py36,py37,py38}-{httpx}
{pypy3}-{requests,httplib2,urllib3,tornado4,boto3},
cov-report
[testenv:flakes]
# Coverage environment tasks: cov-clean and cov-report
# https://pytest-cov.readthedocs.io/en/latest/tox.html
[testenv:cov-clean]
deps = coverage
skip_install=true
commands = coverage erase
[testenv:cov-report]
deps = coverage
skip_install=true
commands =
coverage html
coverage report --fail-under=90
[testenv:lint]
skipsdist = True skipsdist = True
commands = commands =
black --version
black --check --diff .
flake8 --version flake8 --version
flake8 --exclude=./docs/conf.py,./.tox/ flake8 --exclude=./docs/conf.py,./.tox/
pyflakes ./docs/conf.py pyflakes ./docs/conf.py
deps = flake8 deps =
flake8
black
[testenv:docs]
# Running sphinx from inside the "docs" directory
# ensures it will not pick up any stray files that might
# get into a virtual environment under the top-level directory
# or other artifacts under build/
changedir = docs
# The only dependency is sphinx
# If we were using extensions packaged separately,
# we would specify them here.
# A better practice is to specify a specific version of sphinx.
deps =
sphinx
sphinx_rtd_theme
# This is the sphinx command to generate HTML.
# In other circumstances, we might want to generate a PDF or an ebook
commands =
sphinx-build -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html
# We use Python 3.7. Tox sometimes tries to autodetect it based on the name of
# the testenv, but "docs" does not give useful clues so we have to be explicit.
basepython = python3.7
[testenv] [testenv]
# Need to use develop install so that paths
# for aggregate code coverage combine
usedevelop=true
commands = commands =
./runtests.sh {posargs} ./runtests.sh --cov=./vcr --cov-branch --cov-report=xml --cov-append {posargs}
deps = deps =
Flask<1 Flask
mock
pytest pytest
pytest-httpbin pytest-httpbin
pytest-cov
PyYAML PyYAML
requests27: requests==2.7.0 ipaddress
requests: requests>=2.22.0
httplib2: httplib2 httplib2: httplib2
urllib3121: urllib3==1.21.1 urllib3: urllib3
{py27,py35,py36,pypy}-tornado4: tornado>=4,<5 {py35,py36}-tornado4: tornado>=4,<5
{py27,py35,py36,pypy}-tornado4: pytest-tornado {py35,py36}-tornado4: pytest-tornado
{py27,py35,py36}-tornado4: pycurl {py35,py36}-tornado4: pycurl
boto3: boto3 boto3: boto3
boto3: urllib3
aiohttp: aiohttp aiohttp: aiohttp
aiohttp: pytest-asyncio aiohttp: pytest-asyncio
aiohttp: pytest-aiohttp
{py36,py37,py38}-{httpx}: httpx
{py36,py37,py38}-{httpx}: pytest-asyncio
depends =
lint,{py35,py36,py37,py38,pypy3}-{requests,httplib2,urllib3,tornado4,boto3},{py35,py36,py37,py38}-{aiohttp},{py36,py37,py38}-{httpx}: cov-clean
cov-report: lint,{py35,py36,py37,py38,pypy3}-{requests,httplib2,urllib3,tornado4,boto3},{py35,py36,py37,py38}-{aiohttp}
passenv =
AWS_ACCESS_KEY_ID
AWS_DEFAULT_REGION
AWS_SECRET_ACCESS_KEY
[flake8] [flake8]
max_line_length = 110 max_line_length = 110

BIN
vcr.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 240 KiB

After

Width:  |  Height:  |  Size: 114 KiB

View File

@@ -1,14 +1,9 @@
import logging import logging
from .config import VCR from .config import VCR
from logging import NullHandler
from .record_mode import RecordMode as mode # noqa import is not used in this file
# Set default logging handler to avoid "No handler found" warnings. __version__ = "4.1.0"
try: # Python 2.7+
from logging import NullHandler
except ImportError:
class NullHandler(logging.Handler):
def emit(self, record):
pass
logging.getLogger(__name__).addHandler(NullHandler()) logging.getLogger(__name__).addHandler(NullHandler())

View File

@@ -1,7 +1,3 @@
import asyncio async def handle_coroutine(vcr, fn): # noqa: E999
@asyncio.coroutine
def handle_coroutine(vcr, fn):
with vcr as cassette: with vcr as cassette:
return (yield from fn(cassette)) # noqa: E999 return await fn(cassette) # noqa: E999

View File

@@ -1,33 +1,33 @@
import collections import collections
import contextlib
import copy
import sys import sys
import inspect import inspect
import logging import logging
import wrapt import wrapt
from .compat import contextlib
from .errors import UnhandledHTTPRequestError from .errors import UnhandledHTTPRequestError
from .matchers import requests_match, uri, method from .matchers import requests_match, uri, method, get_matchers_results
from .patch import CassettePatcherBuilder from .patch import CassettePatcherBuilder
from .serializers import yamlserializer from .serializers import yamlserializer
from .persisters.filesystem import FilesystemPersister from .persisters.filesystem import FilesystemPersister
from .util import partition_dict from .util import partition_dict
from ._handle_coroutine import handle_coroutine
from .record_mode import RecordMode
try: try:
from asyncio import iscoroutinefunction from asyncio import iscoroutinefunction
from ._handle_coroutine import handle_coroutine
except ImportError: except ImportError:
def iscoroutinefunction(*args, **kwargs): def iscoroutinefunction(*args, **kwargs):
return False return False
def handle_coroutine(*args, **kwags):
raise NotImplementedError('Not implemented on Python 2')
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class CassetteContextDecorator(object): class CassetteContextDecorator:
"""Context manager/decorator that handles installing the cassette and """Context manager/decorator that handles installing the cassette and
removing cassettes. removing cassettes.
@@ -45,7 +45,7 @@ class CassetteContextDecorator(object):
this class as a context manager in ``__exit__``. this class as a context manager in ``__exit__``.
""" """
_non_cassette_arguments = ('path_transformer', 'func_path_generator') _non_cassette_arguments = ("path_transformer", "func_path_generator")
@classmethod @classmethod
def from_args(cls, cassette_class, **kwargs): def from_args(cls, cassette_class, **kwargs):
@@ -60,14 +60,10 @@ class CassetteContextDecorator(object):
with contextlib.ExitStack() as exit_stack: with contextlib.ExitStack() as exit_stack:
for patcher in CassettePatcherBuilder(cassette).build(): for patcher in CassettePatcherBuilder(cassette).build():
exit_stack.enter_context(patcher) exit_stack.enter_context(patcher)
log_format = '{action} context for cassette at {path}.' log_format = "{action} context for cassette at {path}."
log.debug(log_format.format( log.debug(log_format.format(action="Entering", path=cassette._path))
action="Entering", path=cassette._path
))
yield cassette yield cassette
log.debug(log_format.format( log.debug(log_format.format(action="Exiting", path=cassette._path))
action="Exiting", path=cassette._path
))
# TODO(@IvanMalison): Hmmm. it kind of feels like this should be # TODO(@IvanMalison): Hmmm. it kind of feels like this should be
# somewhere else. # somewhere else.
cassette._save() cassette._save()
@@ -83,12 +79,11 @@ class CassetteContextDecorator(object):
# pass # pass
assert self.__finish is None, "Cassette already open." assert self.__finish is None, "Cassette already open."
other_kwargs, cassette_kwargs = partition_dict( other_kwargs, cassette_kwargs = partition_dict(
lambda key, _: key in self._non_cassette_arguments, lambda key, _: key in self._non_cassette_arguments, self._args_getter()
self._args_getter()
) )
if other_kwargs.get('path_transformer'): if other_kwargs.get("path_transformer"):
transformer = other_kwargs['path_transformer'] transformer = other_kwargs["path_transformer"]
cassette_kwargs['path'] = transformer(cassette_kwargs['path']) cassette_kwargs["path"] = transformer(cassette_kwargs["path"])
self.__finish = self._patch_generator(self.cls.load(**cassette_kwargs)) self.__finish = self._patch_generator(self.cls.load(**cassette_kwargs))
return next(self.__finish) return next(self.__finish)
@@ -102,9 +97,7 @@ class CassetteContextDecorator(object):
# functions are reentrant. This is required for thread # functions are reentrant. This is required for thread
# safety and the correct operation of recursive functions. # safety and the correct operation of recursive functions.
args_getter = self._build_args_getter_for_decorator(function) args_getter = self._build_args_getter_for_decorator(function)
return type(self)(self.cls, args_getter)._execute_function( return type(self)(self.cls, args_getter)._execute_function(function, args, kwargs)
function, args, kwargs
)
def _execute_function(self, function, args, kwargs): def _execute_function(self, function, args, kwargs):
def handle_function(cassette): def handle_function(cassette):
@@ -135,7 +128,10 @@ class CassetteContextDecorator(object):
except Exception: except Exception:
to_yield = coroutine.throw(*sys.exc_info()) to_yield = coroutine.throw(*sys.exc_info())
else: else:
to_yield = coroutine.send(to_send) try:
to_yield = coroutine.send(to_send)
except StopIteration:
break
def _handle_function(self, fn): def _handle_function(self, fn):
with self as cassette: with self as cassette:
@@ -148,16 +144,16 @@ class CassetteContextDecorator(object):
def _build_args_getter_for_decorator(self, function): def _build_args_getter_for_decorator(self, function):
def new_args_getter(): def new_args_getter():
kwargs = self._args_getter() kwargs = self._args_getter()
if 'path' not in kwargs: if "path" not in kwargs:
name_generator = (kwargs.get('func_path_generator') or name_generator = kwargs.get("func_path_generator") or self.get_function_name
self.get_function_name)
path = name_generator(function) path = name_generator(function)
kwargs['path'] = path kwargs["path"] = path
return kwargs return kwargs
return new_args_getter return new_args_getter
class Cassette(object): class Cassette:
"""A container for recorded requests and responses""" """A container for recorded requests and responses"""
@classmethod @classmethod
@@ -175,19 +171,30 @@ class Cassette(object):
def use(cls, **kwargs): def use(cls, **kwargs):
return CassetteContextDecorator.from_args(cls, **kwargs) return CassetteContextDecorator.from_args(cls, **kwargs)
def __init__(self, path, serializer=None, persister=None, record_mode='once', def __init__(
match_on=(uri, method), before_record_request=None, self,
before_record_response=None, custom_patches=(), path,
inject=False): 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,
):
self._persister = persister or FilesystemPersister self._persister = persister or FilesystemPersister
self._path = path self._path = path
self._serializer = serializer or yamlserializer self._serializer = serializer or yamlserializer
self._match_on = match_on self._match_on = match_on
self._before_record_request = before_record_request or (lambda x: x) 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._before_record_response = before_record_response or (lambda x: x)
self.inject = inject self.inject = inject
self.record_mode = record_mode self.record_mode = record_mode
self.custom_patches = custom_patches self.custom_patches = custom_patches
self.allow_playback_repeats = allow_playback_repeats
# self.data is the list of (req, resp) tuples # self.data is the list of (req, resp) tuples
self.data = [] self.data = []
@@ -202,7 +209,7 @@ class Cassette(object):
@property @property
def all_played(self): def all_played(self):
"""Returns True if all responses have been played, False otherwise.""" """Returns True if all responses have been played, False otherwise."""
return self.play_count == len(self) return len(self.play_counts.values()) == len(self)
@property @property
def requests(self): def requests(self):
@@ -214,14 +221,17 @@ class Cassette(object):
@property @property
def write_protected(self): def write_protected(self):
return self.rewound and self.record_mode == 'once' or \ return self.rewound and self.record_mode == RecordMode.ONCE or self.record_mode == RecordMode.NONE
self.record_mode == 'none'
def append(self, request, response): def append(self, request, response):
"""Add a request, response pair to this cassette""" """Add a request, response pair to this cassette"""
log.info("Appending request %s and response %s", request, response)
request = self._before_record_request(request) request = self._before_record_request(request)
if not request: if not request:
return return
# Deepcopy is here because mutation of `response` will corrupt the
# real response.
response = copy.deepcopy(response)
response = self._before_record_response(response) response = self._before_record_response(response)
if response is None: if response is None:
return return
@@ -243,9 +253,7 @@ class Cassette(object):
def can_play_response_for(self, request): def can_play_response_for(self, request):
request = self._before_record_request(request) request = self._before_record_request(request)
return request and request in self and \ return request and request in self and self.record_mode != RecordMode.ALL and self.rewound
self.record_mode != 'all' and \
self.rewound
def play_response(self, request): def play_response(self, request):
""" """
@@ -253,13 +261,12 @@ class Cassette(object):
hasn't been played back before, and mark it as played hasn't been played back before, and mark it as played
""" """
for index, response in self._responses(request): for index, response in self._responses(request):
if self.play_counts[index] == 0: if self.play_counts[index] == 0 or self.allow_playback_repeats:
self.play_counts[index] += 1 self.play_counts[index] += 1
return response return response
# The cassette doesn't contain the request asked for. # The cassette doesn't contain the request asked for.
raise UnhandledHTTPRequestError( raise UnhandledHTTPRequestError(
"The cassette (%r) doesn't contain the request (%r) asked for" "The cassette (%r) doesn't contain the request (%r) asked for" % (self._path, request)
% (self._path, request)
) )
def responses_of(self, request): def responses_of(self, request):
@@ -274,28 +281,59 @@ class Cassette(object):
return responses return responses
# The cassette doesn't contain the request asked for. # The cassette doesn't contain the request asked for.
raise UnhandledHTTPRequestError( raise UnhandledHTTPRequestError(
"The cassette (%r) doesn't contain the request (%r) asked for" "The cassette (%r) doesn't contain the request (%r) asked for" % (self._path, request)
% (self._path, request)
) )
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 index, (stored_request, response) 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 _as_dict(self): def _as_dict(self):
return {"requests": self.requests, "responses": self.responses} return {"requests": self.requests, "responses": self.responses}
def _save(self, force=False): def _save(self, force=False):
if force or self.dirty: if force or self.dirty:
self._persister.save_cassette( self._persister.save_cassette(self._path, self._as_dict(), serializer=self._serializer)
self._path,
self._as_dict(),
serializer=self._serializer,
)
self.dirty = False self.dirty = False
def _load(self): def _load(self):
try: try:
requests, responses = self._persister.load_cassette( requests, responses = self._persister.load_cassette(self._path, serializer=self._serializer)
self._path,
serializer=self._serializer,
)
for request, response in zip(requests, responses): for request, response in zip(requests, responses):
self.append(request, response) self.append(request, response)
self.dirty = False self.dirty = False
@@ -304,9 +342,7 @@ class Cassette(object):
pass pass
def __str__(self): def __str__(self):
return "<Cassette containing {} recorded response(s)>".format( return "<Cassette containing {} recorded response(s)>".format(len(self))
len(self)
)
def __len__(self): def __len__(self):
"""Return the number of request,response pairs stored in here""" """Return the number of request,response pairs stored in here"""
@@ -315,6 +351,6 @@ class Cassette(object):
def __contains__(self, request): def __contains__(self, request):
"""Return whether or not a request has been stored""" """Return whether or not a request has been stored"""
for index, response in self._responses(request): for index, response in self._responses(request):
if self.play_counts[index] == 0: if self.play_counts[index] == 0 or self.allow_playback_repeats:
return True return True
return False return False

View File

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

View File

@@ -1,5 +1,6 @@
import copy import copy
import collections
from collections import abc as collections_abc
import functools import functools
import inspect import inspect
import os import os
@@ -11,16 +12,15 @@ from .cassette import Cassette
from .serializers import yamlserializer, jsonserializer from .serializers import yamlserializer, jsonserializer
from .persisters.filesystem import FilesystemPersister from .persisters.filesystem import FilesystemPersister
from .util import compose, auto_decorate from .util import compose, auto_decorate
from .record_mode import RecordMode
from . import matchers from . import matchers
from . import filters from . import filters
class VCR(object): class VCR:
@staticmethod @staticmethod
def is_test_method(method_name, function): def is_test_method(method_name, function):
return method_name.startswith('test') and \ return method_name.startswith("test") and isinstance(function, types.FunctionType)
isinstance(function, types.FunctionType)
@staticmethod @staticmethod
def ensure_suffix(suffix): def ensure_suffix(suffix):
@@ -28,35 +28,45 @@ class VCR(object):
if not path.endswith(suffix): if not path.endswith(suffix):
return path + suffix return path + suffix
return path return path
return ensure return ensure
def __init__(self, path_transformer=None, before_record_request=None, def __init__(
custom_patches=(), filter_query_parameters=(), ignore_hosts=(), self,
record_mode="once", ignore_localhost=False, filter_headers=(), path_transformer=None,
before_record_response=None, filter_post_data_parameters=(), before_record_request=None,
match_on=('method', 'scheme', 'host', 'port', 'path', 'query'), custom_patches=(),
before_record=None, inject_cassette=False, serializer='yaml', filter_query_parameters=(),
cassette_library_dir=None, func_path_generator=None, ignore_hosts=(),
decode_compressed_response=False): 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,
):
self.serializer = serializer self.serializer = serializer
self.match_on = match_on self.match_on = match_on
self.cassette_library_dir = cassette_library_dir self.cassette_library_dir = cassette_library_dir
self.serializers = { self.serializers = {"yaml": yamlserializer, "json": jsonserializer}
'yaml': yamlserializer,
'json': jsonserializer,
}
self.matchers = { self.matchers = {
'method': matchers.method, "method": matchers.method,
'uri': matchers.uri, "uri": matchers.uri,
'url': matchers.uri, # matcher for backwards compatibility "url": matchers.uri, # matcher for backwards compatibility
'scheme': matchers.scheme, "scheme": matchers.scheme,
'host': matchers.host, "host": matchers.host,
'port': matchers.port, "port": matchers.port,
'path': matchers.path, "path": matchers.path,
'query': matchers.query, "query": matchers.query,
'headers': matchers.headers, "headers": matchers.headers,
'raw_body': matchers.raw_body, "raw_body": matchers.raw_body,
'body': matchers.body, "body": matchers.body,
} }
self.persister = FilesystemPersister self.persister = FilesystemPersister
self.record_mode = record_mode self.record_mode = record_mode
@@ -77,11 +87,7 @@ class VCR(object):
try: try:
serializer = self.serializers[serializer_name] serializer = self.serializers[serializer_name]
except KeyError: except KeyError:
raise KeyError( raise KeyError("Serializer {} doesn't exist or isn't registered".format(serializer_name))
"Serializer {} doesn't exist or isn't registered".format(
serializer_name
)
)
return serializer return serializer
def _get_matchers(self, matcher_names): def _get_matchers(self, matcher_names):
@@ -90,13 +96,11 @@ class VCR(object):
for m in matcher_names: for m in matcher_names:
matchers.append(self.matchers[m]) matchers.append(self.matchers[m])
except KeyError: except KeyError:
raise KeyError( raise KeyError("Matcher {} doesn't exist or isn't registered".format(m))
"Matcher {} doesn't exist or isn't registered".format(m)
)
return matchers return matchers
def use_cassette(self, path=None, **kwargs): def use_cassette(self, path=None, **kwargs):
if path is not None and not isinstance(path, six.string_types): if path is not None and not isinstance(path, str):
function = path function = path
# Assume this is an attempt to decorate a function # Assume this is an attempt to decorate a function
return self._use_cassette(**kwargs)(function) return self._use_cassette(**kwargs)(function)
@@ -114,68 +118,54 @@ class VCR(object):
return Cassette.use_arg_getter(args_getter) return Cassette.use_arg_getter(args_getter)
def get_merged_config(self, **kwargs): def get_merged_config(self, **kwargs):
serializer_name = kwargs.get('serializer', self.serializer) serializer_name = kwargs.get("serializer", self.serializer)
matcher_names = kwargs.get('match_on', self.match_on) matcher_names = kwargs.get("match_on", self.match_on)
path_transformer = kwargs.get( path_transformer = kwargs.get("path_transformer", self.path_transformer)
'path_transformer', func_path_generator = kwargs.get("func_path_generator", self.func_path_generator)
self.path_transformer cassette_library_dir = kwargs.get("cassette_library_dir", self.cassette_library_dir)
) additional_matchers = kwargs.get("additional_matchers", ())
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', ())
if cassette_library_dir: if cassette_library_dir:
def add_cassette_library_dir(path): def add_cassette_library_dir(path):
if not path.startswith(cassette_library_dir): if not path.startswith(cassette_library_dir):
return os.path.join(cassette_library_dir, path) return os.path.join(cassette_library_dir, path)
return path return path
path_transformer = compose(
add_cassette_library_dir, path_transformer path_transformer = compose(add_cassette_library_dir, path_transformer)
)
elif not func_path_generator: elif not func_path_generator:
# If we don't have a library dir, use the functions # If we don't have a library dir, use the functions
# location to build a full path for cassettes. # location to build a full path for cassettes.
func_path_generator = self._build_path_from_func_using_module func_path_generator = self._build_path_from_func_using_module
merged_config = { merged_config = {
'serializer': self._get_serializer(serializer_name), "serializer": self._get_serializer(serializer_name),
'persister': self.persister, "persister": self.persister,
'match_on': self._get_matchers( "match_on": self._get_matchers(tuple(matcher_names) + tuple(additional_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),
'record_mode': kwargs.get('record_mode', self.record_mode), "before_record_response": self._build_before_record_response(kwargs),
'before_record_request': self._build_before_record_request(kwargs), "custom_patches": self._custom_patches + kwargs.get("custom_patches", ()),
'before_record_response': self._build_before_record_response(kwargs), "inject": kwargs.get("inject_cassette", self.inject_cassette),
'custom_patches': self._custom_patches + kwargs.get( "path_transformer": path_transformer,
'custom_patches', () "func_path_generator": func_path_generator,
), "allow_playback_repeats": kwargs.get("allow_playback_repeats", False),
'inject': kwargs.get('inject_cassette', self.inject_cassette),
'path_transformer': path_transformer,
'func_path_generator': func_path_generator
} }
path = kwargs.get('path') path = kwargs.get("path")
if path: if path:
merged_config['path'] = path merged_config["path"] = path
return merged_config return merged_config
def _build_before_record_response(self, options): def _build_before_record_response(self, options):
before_record_response = options.get( before_record_response = options.get("before_record_response", self.before_record_response)
'before_record_response', self.before_record_response
)
decode_compressed_response = options.get( decode_compressed_response = options.get(
'decode_compressed_response', self.decode_compressed_response "decode_compressed_response", self.decode_compressed_response
) )
filter_functions = [] filter_functions = []
if decode_compressed_response: if decode_compressed_response:
filter_functions.append(filters.decode_response) filter_functions.append(filters.decode_response)
if before_record_response: if before_record_response:
if not isinstance(before_record_response, collections.Iterable): if not isinstance(before_record_response, collections_abc.Iterable):
before_record_response = (before_record_response,) before_record_response = (before_record_response,)
filter_functions.extend(before_record_response) filter_functions.extend(before_record_response)
@@ -185,63 +175,43 @@ class VCR(object):
break break
response = function(response) response = function(response)
return response return response
return before_record_response return before_record_response
def _build_before_record_request(self, options): def _build_before_record_request(self, options):
filter_functions = [] filter_functions = []
filter_headers = options.get( filter_headers = options.get("filter_headers", self.filter_headers)
'filter_headers', self.filter_headers filter_query_parameters = options.get("filter_query_parameters", self.filter_query_parameters)
)
filter_query_parameters = options.get(
'filter_query_parameters', self.filter_query_parameters
)
filter_post_data_parameters = options.get( filter_post_data_parameters = options.get(
'filter_post_data_parameters', self.filter_post_data_parameters "filter_post_data_parameters", self.filter_post_data_parameters
) )
before_record_request = options.get( before_record_request = options.get(
"before_record_request", "before_record_request", options.get("before_record", self.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
) )
ignore_hosts = options.get("ignore_hosts", self.ignore_hosts)
ignore_localhost = options.get("ignore_localhost", self.ignore_localhost)
if filter_headers: if filter_headers:
replacements = [h if isinstance(h, tuple) else (h, None) replacements = [h if isinstance(h, tuple) else (h, None) for h in filter_headers]
for h in filter_headers] filter_functions.append(functools.partial(filters.replace_headers, replacements=replacements))
filter_functions.append(
functools.partial(
filters.replace_headers,
replacements=replacements,
)
)
if filter_query_parameters: if filter_query_parameters:
replacements = [p if isinstance(p, tuple) else (p, None) replacements = [p if isinstance(p, tuple) else (p, None) for p in filter_query_parameters]
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( filter_functions.append(
functools.partial( functools.partial(filters.replace_query_parameters, replacements=replacements)
filters.replace_post_data_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) hosts_to_ignore = set(ignore_hosts)
if ignore_localhost: if ignore_localhost:
hosts_to_ignore.update(('localhost', '0.0.0.0', '127.0.0.1')) hosts_to_ignore.update(("localhost", "0.0.0.0", "127.0.0.1"))
if hosts_to_ignore: if hosts_to_ignore:
filter_functions.append(self._build_ignore_hosts(hosts_to_ignore)) filter_functions.append(self._build_ignore_hosts(hosts_to_ignore))
if before_record_request: if before_record_request:
if not isinstance(before_record_request, collections.Iterable): if not isinstance(before_record_request, collections_abc.Iterable):
before_record_request = (before_record_request,) before_record_request = (before_record_request,)
filter_functions.extend(before_record_request) filter_functions.extend(before_record_request)
@@ -252,20 +222,21 @@ class VCR(object):
break break
request = function(request) request = function(request)
return request return request
return before_record_request return before_record_request
@staticmethod @staticmethod
def _build_ignore_hosts(hosts_to_ignore): def _build_ignore_hosts(hosts_to_ignore):
def filter_ignored_hosts(request): def filter_ignored_hosts(request):
if hasattr(request, 'host') and request.host in hosts_to_ignore: if hasattr(request, "host") and request.host in hosts_to_ignore:
return return
return request return request
return filter_ignored_hosts return filter_ignored_hosts
@staticmethod @staticmethod
def _build_path_from_func_using_module(function): def _build_path_from_func_using_module(function):
return os.path.join(os.path.dirname(inspect.getfile(function)), return os.path.join(os.path.dirname(inspect.getfile(function)), function.__name__)
function.__name__)
def register_serializer(self, name, serializer): def register_serializer(self, name, serializer):
self.serializers[name] = serializer self.serializers[name] = serializer
@@ -279,4 +250,5 @@ class VCR(object):
def test_case(self, predicate=None): def test_case(self, predicate=None):
predicate = predicate or self.is_test_method predicate = predicate or self.is_test_method
# TODO: Remove this reference to `six` in favor of the Python3 equivalent
return six.with_metaclass(auto_decorate(self.use_cassette, predicate)) return six.with_metaclass(auto_decorate(self.use_cassette, predicate))

View File

@@ -1,7 +1,42 @@
class CannotOverwriteExistingCassetteException(Exception): 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 = "Found {} similar requests with {} different matcher(s) :\n".format(
len(best_matches), len(best_matches[0][2])
)
for idx, best_match in enumerate(best_matches, start=1):
request, succeeded_matchers, failed_matchers_assertion_msgs = best_match
best_matches_msg += (
"\n%s - (%r).\n"
"Matchers succeeded : %s\n"
"Matchers failed :\n" % (idx, request, succeeded_matchers)
)
for failed_matcher, assertion_msg in failed_matchers_assertion_msgs:
best_matches_msg += "%s - assertion failure :\n" "%s\n" % (failed_matcher, assertion_msg)
else:
best_matches_msg = "No similar requests, that have not been played, found."
return (
"Can't overwrite existing cassette (%r) in "
"your current record mode (%r).\n"
"No match for the request (%r) was found.\n"
"%s" % (cassette._path, cassette.record_mode, failed_request, best_matches_msg)
)
class UnhandledHTTPRequestError(KeyError): class UnhandledHTTPRequestError(KeyError):
"""Raised when a cassette does not contain the request we want.""" """Raised when a cassette does not contain the request we want."""
pass pass

View File

@@ -1,5 +1,5 @@
from six import BytesIO, text_type from io import BytesIO
from six.moves.urllib.parse import urlparse, urlencode, urlunparse from urllib.parse import urlparse, urlencode, urlunparse
import copy import copy
import json import json
import zlib import zlib
@@ -8,13 +8,11 @@ from .util import CaseInsensitiveDict
def replace_headers(request, replacements): def replace_headers(request, replacements):
""" """Replace headers in request according to replacements.
Replace headers in request according to replacements. The replacements The replacements should be a list of (key, value) pairs where the value can be any of:
should be a list of (key, value) pairs where the value can be any of: 1. A simple replacement string value.
1. A simple replacement string value. 2. None to remove the given header.
2. None to remove the given header. 3. A callable which accepts (key, value, request) and returns a string value or None.
3. A callable which accepts (key, value, request) and returns a string
value or None.
""" """
new_headers = request.headers.copy() new_headers = request.headers.copy()
for k, rv in replacements: for k, rv in replacements:
@@ -37,10 +35,9 @@ def remove_headers(request, headers_to_remove):
def replace_query_parameters(request, replacements): def replace_query_parameters(request, replacements):
""" """Replace query parameters in request according to replacements.
Replace query parameters in request according to replacements. The
replacements should be a list of (key, value) pairs where the value can be The replacements should be a list of (key, value) pairs where the value can be any of:
any of:
1. A simple replacement string value. 1. A simple replacement string value.
2. None to remove the given header. 2. None to remove the given header.
3. A callable which accepts (key, value, request) and returns a string 3. A callable which accepts (key, value, request) and returns a string
@@ -73,19 +70,22 @@ def remove_query_parameters(request, query_parameters_to_remove):
def replace_post_data_parameters(request, replacements): def replace_post_data_parameters(request, replacements):
""" """Replace post data in request--either form data or json--according to 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 replacements should be a list of (key, value) pairs where the value can be any of:
the value can be any of:
1. A simple replacement string value. 1. A simple replacement string value.
2. None to remove the given header. 2. None to remove the given header.
3. A callable which accepts (key, value, request) and returns a string 3. A callable which accepts (key, value, request) and returns a string
value or None. value or None.
""" """
if not request.body:
# Nothing to replace
return request
replacements = dict(replacements) replacements = dict(replacements)
if request.method == 'POST' and not isinstance(request.body, BytesIO): if request.method == "POST" and not isinstance(request.body, BytesIO):
if request.headers.get('Content-Type') == 'application/json': if request.headers.get("Content-Type") == "application/json":
json_data = json.loads(request.body.decode('utf-8')) json_data = json.loads(request.body.decode("utf-8"))
for k, rv in replacements.items(): for k, rv in replacements.items():
if k in json_data: if k in json_data:
ov = json_data.pop(k) ov = json_data.pop(k)
@@ -93,28 +93,26 @@ def replace_post_data_parameters(request, replacements):
rv = rv(key=k, value=ov, request=request) rv = rv(key=k, value=ov, request=request)
if rv is not None: if rv is not None:
json_data[k] = rv json_data[k] = rv
request.body = json.dumps(json_data).encode('utf-8') request.body = json.dumps(json_data).encode("utf-8")
else: else:
if isinstance(request.body, text_type): if isinstance(request.body, str):
request.body = request.body.encode('utf-8') request.body = request.body.encode("utf-8")
splits = [p.partition(b'=') for p in request.body.split(b'&')] splits = [p.partition(b"=") for p in request.body.split(b"&")]
new_splits = [] new_splits = []
for k, sep, ov in splits: for k, sep, ov in splits:
if sep is None: if sep is None:
new_splits.append((k, sep, ov)) new_splits.append((k, sep, ov))
else: else:
rk = k.decode('utf-8') rk = k.decode("utf-8")
if rk not in replacements: if rk not in replacements:
new_splits.append((k, sep, ov)) new_splits.append((k, sep, ov))
else: else:
rv = replacements[rk] rv = replacements[rk]
if callable(rv): if callable(rv):
rv = rv(key=rk, value=ov.decode('utf-8'), rv = rv(key=rk, value=ov.decode("utf-8"), request=request)
request=request)
if rv is not None: if rv is not None:
new_splits.append((k, sep, rv.encode('utf-8'))) new_splits.append((k, sep, rv.encode("utf-8")))
request.body = b'&'.join(k if sep is None else b''.join([k, sep, v]) request.body = b"&".join(k if sep is None else b"".join([k, sep, v]) for k, sep, v in new_splits)
for k, sep, v in new_splits)
return request return request
@@ -133,15 +131,16 @@ def decode_response(response):
2. delete the content-encoding header 2. delete the content-encoding header
3. update content-length header to decompressed length 3. update content-length header to decompressed length
""" """
def is_compressed(headers): def is_compressed(headers):
encoding = headers.get('content-encoding', []) encoding = headers.get("content-encoding", [])
return encoding and encoding[0] in ('gzip', 'deflate') return encoding and encoding[0] in ("gzip", "deflate")
def decompress_body(body, encoding): def decompress_body(body, encoding):
"""Returns decompressed body according to encoding using zlib. """Returns decompressed body according to encoding using zlib.
to (de-)compress gzip format, use wbits = zlib.MAX_WBITS | 16 to (de-)compress gzip format, use wbits = zlib.MAX_WBITS | 16
""" """
if encoding == 'gzip': if encoding == "gzip":
return zlib.decompress(body, zlib.MAX_WBITS | 16) return zlib.decompress(body, zlib.MAX_WBITS | 16)
else: # encoding == 'deflate' else: # encoding == 'deflate'
return zlib.decompress(body) return zlib.decompress(body)
@@ -149,15 +148,15 @@ def decode_response(response):
# Deepcopy here in case `headers` contain objects that could # Deepcopy here in case `headers` contain objects that could
# be mutated by a shallow copy and corrupt the real response. # be mutated by a shallow copy and corrupt the real response.
response = copy.deepcopy(response) response = copy.deepcopy(response)
headers = CaseInsensitiveDict(response['headers']) headers = CaseInsensitiveDict(response["headers"])
if is_compressed(headers): if is_compressed(headers):
encoding = headers['content-encoding'][0] encoding = headers["content-encoding"][0]
headers['content-encoding'].remove(encoding) headers["content-encoding"].remove(encoding)
if not headers['content-encoding']: if not headers["content-encoding"]:
del headers['content-encoding'] del headers["content-encoding"]
new_body = decompress_body(response['body']['string'], encoding) new_body = decompress_body(response["body"]["string"], encoding)
response['body']['string'] = new_body response["body"]["string"] = new_body
headers['content-length'] = [str(len(new_body))] headers["content-length"] = [str(len(new_body))]
response['headers'] = dict(headers) response["headers"] = dict(headers)
return response return response

View File

@@ -1,5 +1,6 @@
import json import json
from six.moves import urllib, xmlrpc_client import urllib
import xmlrpc.client
from .util import read_body from .util import read_body
import logging import logging
@@ -8,40 +9,56 @@ log = logging.getLogger(__name__)
def method(r1, r2): def method(r1, r2):
return r1.method == r2.method assert r1.method == r2.method, "{} != {}".format(r1.method, r2.method)
def uri(r1, r2): def uri(r1, r2):
return r1.uri == r2.uri assert r1.uri == r2.uri, "{} != {}".format(r1.uri, r2.uri)
def host(r1, r2): def host(r1, r2):
return r1.host == r2.host assert r1.host == r2.host, "{} != {}".format(r1.host, r2.host)
def scheme(r1, r2): def scheme(r1, r2):
return r1.scheme == r2.scheme assert r1.scheme == r2.scheme, "{} != {}".format(r1.scheme, r2.scheme)
def port(r1, r2): def port(r1, r2):
return r1.port == r2.port assert r1.port == r2.port, "{} != {}".format(r1.port, r2.port)
def path(r1, r2): def path(r1, r2):
return r1.path == r2.path assert r1.path == r2.path, "{} != {}".format(r1.path, r2.path)
def query(r1, r2): def query(r1, r2):
return r1.query == r2.query assert r1.query == r2.query, "{} != {}".format(r1.query, r2.query)
def raw_body(r1, r2): def raw_body(r1, r2):
return read_body(r1) == read_body(r2) assert read_body(r1) == read_body(r2)
def _header_checker(value, header='Content-Type'): def body(r1, r2):
transformer = _get_transformer(r1)
r2_transformer = _get_transformer(r2)
if transformer != r2_transformer:
transformer = _identity
assert transformer(read_body(r1)) == transformer(read_body(r2))
def headers(r1, r2):
assert r1.headers == r2.headers, "{} != {}".format(r1.headers, r2.headers)
def _header_checker(value, header="Content-Type"):
def checker(headers): def checker(headers):
return value in headers.get(header, '').lower() _header = headers.get(header, "")
if isinstance(_header, bytes):
_header = _header.decode("utf-8")
return value in _header.lower()
return checker return checker
@@ -50,15 +67,18 @@ def _transform_json(body):
# string. RFC 7159 says the default encoding is UTF-8 (although UTF-16 # string. RFC 7159 says the default encoding is UTF-8 (although UTF-16
# and UTF-32 are also allowed: hmmmmm). # and UTF-32 are also allowed: hmmmmm).
if body: if body:
return json.loads(body.decode('utf-8')) return json.loads(body.decode("utf-8"))
_xml_header_checker = _header_checker('text/xml') _xml_header_checker = _header_checker("text/xml")
_xmlrpc_header_checker = _header_checker('xmlrpc', header='User-Agent') _xmlrpc_header_checker = _header_checker("xmlrpc", header="User-Agent")
_checker_transformer_pairs = ( _checker_transformer_pairs = (
(_header_checker('application/x-www-form-urlencoded'), urllib.parse.parse_qs), (
(_header_checker('application/json'), _transform_json), _header_checker("application/x-www-form-urlencoded"),
(lambda request: _xml_header_checker(request) and _xmlrpc_header_checker(request), xmlrpc_client.loads), 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),
) )
@@ -74,28 +94,50 @@ def _get_transformer(request):
return _identity return _identity
def body(r1, r2):
transformer = _get_transformer(r1)
r2_transformer = _get_transformer(r2)
if transformer != r2_transformer:
transformer = _identity
return transformer(read_body(r1)) == transformer(read_body(r2))
def headers(r1, r2):
return r1.headers == r2.headers
def _log_matches(r1, r2, matches):
differences = [m for m in matches if not m[0]]
if differences:
log.debug(
"Requests {} and {} differ according to "
"the following matchers: {}".format(r1, r2, differences)
)
def requests_match(r1, r2, matchers): def requests_match(r1, r2, matchers):
matches = [(m(r1, r2), m) for m in matchers] successes, failures = get_matchers_results(r1, r2, matchers)
_log_matches(r1, r2, matches) if failures:
return all(m[0] for m in matches) log.debug("Requests {} and {} differ.\n" "Failure details:\n" "{}".format(r1, r2, 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

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

View File

@@ -1,26 +1,35 @@
'''Utilities for patching in cassettes''' """Utilities for patching in cassettes"""
import contextlib
import functools import functools
import itertools import itertools
from unittest import mock
from .compat import contextlib, mock
from .stubs import VCRHTTPConnection, VCRHTTPSConnection from .stubs import VCRHTTPConnection, VCRHTTPSConnection
from six.moves import http_client as httplib import http.client as httplib
import logging
log = logging.getLogger(__name__)
# Save some of the original types for the purposes of unpatching # Save some of the original types for the purposes of unpatching
_HTTPConnection = httplib.HTTPConnection _HTTPConnection = httplib.HTTPConnection
_HTTPSConnection = httplib.HTTPSConnection _HTTPSConnection = httplib.HTTPSConnection
# Try to save the original types for boto3 # Try to save the original types for boto3
try: try:
import botocore.vendored.requests.packages.urllib3.connectionpool as cpool from botocore.awsrequest import AWSHTTPSConnection, AWSHTTPConnection
except ImportError: # pragma: no cover except ImportError:
pass try:
import botocore.vendored.requests.packages.urllib3.connectionpool as cpool
except ImportError: # pragma: no cover
pass
else:
_Boto3VerifiedHTTPSConnection = cpool.VerifiedHTTPSConnection
_cpoolBoto3HTTPConnection = cpool.HTTPConnection
_cpoolBoto3HTTPSConnection = cpool.HTTPSConnection
else: else:
_Boto3VerifiedHTTPSConnection = cpool.VerifiedHTTPSConnection _Boto3VerifiedHTTPSConnection = AWSHTTPSConnection
_cpoolBoto3HTTPConnection = cpool.HTTPConnection _cpoolBoto3HTTPConnection = AWSHTTPConnection
_cpoolBoto3HTTPSConnection = cpool.HTTPSConnection _cpoolBoto3HTTPSConnection = AWSHTTPSConnection
cpool = None cpool = None
# Try to save the original types for urllib3 # Try to save the original types for urllib3
@@ -44,7 +53,6 @@ else:
_cpoolHTTPConnection = cpool.HTTPConnection _cpoolHTTPConnection = cpool.HTTPConnection
_cpoolHTTPSConnection = cpool.HTTPSConnection _cpoolHTTPSConnection = cpool.HTTPSConnection
# Try to save the original types for httplib2 # Try to save the original types for httplib2
try: try:
import httplib2 import httplib2
@@ -55,7 +63,6 @@ else:
_HTTPSConnectionWithTimeout = httplib2.HTTPSConnectionWithTimeout _HTTPSConnectionWithTimeout = httplib2.HTTPSConnectionWithTimeout
_SCHEME_TO_CONNECTION = httplib2.SCHEME_TO_CONNECTION _SCHEME_TO_CONNECTION = httplib2.SCHEME_TO_CONNECTION
# Try to save the original types for boto # Try to save the original types for boto
try: try:
import boto.https_connection import boto.https_connection
@@ -64,24 +71,20 @@ except ImportError: # pragma: no cover
else: else:
_CertValidatingHTTPSConnection = boto.https_connection.CertValidatingHTTPSConnection _CertValidatingHTTPSConnection = boto.https_connection.CertValidatingHTTPSConnection
# Try to save the original types for Tornado # Try to save the original types for Tornado
try: try:
import tornado.simple_httpclient import tornado.simple_httpclient
except ImportError: # pragma: no cover except ImportError: # pragma: no cover
pass pass
else: else:
_SimpleAsyncHTTPClient_fetch_impl = \ _SimpleAsyncHTTPClient_fetch_impl = tornado.simple_httpclient.SimpleAsyncHTTPClient.fetch_impl
tornado.simple_httpclient.SimpleAsyncHTTPClient.fetch_impl
try: try:
import tornado.curl_httpclient import tornado.curl_httpclient
except ImportError: # pragma: no cover except ImportError: # pragma: no cover
pass pass
else: else:
_CurlAsyncHTTPClient_fetch_impl = \ _CurlAsyncHTTPClient_fetch_impl = tornado.curl_httpclient.CurlAsyncHTTPClient.fetch_impl
tornado.curl_httpclient.CurlAsyncHTTPClient.fetch_impl
try: try:
import aiohttp.client import aiohttp.client
@@ -91,14 +94,21 @@ else:
_AiohttpClientSessionRequest = aiohttp.client.ClientSession._request _AiohttpClientSessionRequest = aiohttp.client.ClientSession._request
class CassettePatcherBuilder(object): try:
import httpx
except ImportError: # pragma: no cover
pass
else:
_HttpxSyncClient_send = httpx.Client.send
_HttpxAsyncClient_send = httpx.AsyncClient.send
class CassettePatcherBuilder:
def _build_patchers_from_mock_triples_decorator(function): def _build_patchers_from_mock_triples_decorator(function):
@functools.wraps(function) @functools.wraps(function)
def wrapped(self, *args, **kwargs): def wrapped(self, *args, **kwargs):
return self._build_patchers_from_mock_triples( return self._build_patchers_from_mock_triples(function(self, *args, **kwargs))
function(self, *args, **kwargs)
)
return wrapped return wrapped
def __init__(self, cassette): def __init__(self, cassette):
@@ -107,11 +117,16 @@ class CassettePatcherBuilder(object):
def build(self): def build(self):
return itertools.chain( return itertools.chain(
self._httplib(), self._requests(), self._boto3(), self._urllib3(), self._httplib(),
self._httplib2(), self._boto(), self._tornado(), self._aiohttp(), self._requests(),
self._build_patchers_from_mock_triples( self._boto3(),
self._cassette.custom_patches self._urllib3(),
), self._httplib2(),
self._boto(),
self._tornado(),
self._aiohttp(),
self._httpx(),
self._build_patchers_from_mock_triples(self._cassette.custom_patches),
) )
def _build_patchers_from_mock_triples(self, mock_triples): def _build_patchers_from_mock_triples(self, mock_triples):
@@ -124,9 +139,9 @@ class CassettePatcherBuilder(object):
if not hasattr(obj, patched_attribute): if not hasattr(obj, patched_attribute):
return return
return mock.patch.object(obj, patched_attribute, return mock.patch.object(
self._recursively_apply_get_cassette_subclass( obj, patched_attribute, self._recursively_apply_get_cassette_subclass(replacement_class)
replacement_class)) )
def _recursively_apply_get_cassette_subclass(self, replacement_dict_or_obj): def _recursively_apply_get_cassette_subclass(self, replacement_dict_or_obj):
"""One of the subtleties of this class is that it does not directly """One of the subtleties of this class is that it does not directly
@@ -148,13 +163,11 @@ class CassettePatcherBuilder(object):
""" """
if isinstance(replacement_dict_or_obj, dict): if isinstance(replacement_dict_or_obj, dict):
for key, replacement_obj in replacement_dict_or_obj.items(): for key, replacement_obj in replacement_dict_or_obj.items():
replacement_obj = self._recursively_apply_get_cassette_subclass( replacement_obj = self._recursively_apply_get_cassette_subclass(replacement_obj)
replacement_obj)
replacement_dict_or_obj[key] = replacement_obj replacement_dict_or_obj[key] = replacement_obj
return replacement_dict_or_obj return replacement_dict_or_obj
if hasattr(replacement_dict_or_obj, 'cassette'): if hasattr(replacement_dict_or_obj, "cassette"):
replacement_dict_or_obj = self._get_cassette_subclass( replacement_dict_or_obj = self._get_cassette_subclass(replacement_dict_or_obj)
replacement_dict_or_obj)
return replacement_dict_or_obj return replacement_dict_or_obj
def _get_cassette_subclass(self, klass): def _get_cassette_subclass(self, klass):
@@ -169,13 +182,14 @@ class CassettePatcherBuilder(object):
bases = (base_class,) bases = (base_class,)
if not issubclass(base_class, object): # Check for old style class if not issubclass(base_class, object): # Check for old style class
bases += (object,) bases += (object,)
return type('{}{}'.format(base_class.__name__, self._cassette._path), return type(
bases, dict(cassette=self._cassette)) "{}{}".format(base_class.__name__, self._cassette._path), bases, dict(cassette=self._cassette)
)
@_build_patchers_from_mock_triples_decorator @_build_patchers_from_mock_triples_decorator
def _httplib(self): def _httplib(self):
yield httplib, 'HTTPConnection', VCRHTTPConnection yield httplib, "HTTPConnection", VCRHTTPConnection
yield httplib, 'HTTPSConnection', VCRHTTPSConnection yield httplib, "HTTPSConnection", VCRHTTPSConnection
def _requests(self): def _requests(self):
try: try:
@@ -184,13 +198,28 @@ class CassettePatcherBuilder(object):
return () return ()
return self._urllib3_patchers(cpool, requests_stubs) return self._urllib3_patchers(cpool, requests_stubs)
@_build_patchers_from_mock_triples_decorator
def _boto3(self): def _boto3(self):
try: try:
import botocore.vendored.requests.packages.urllib3.connectionpool as cpool # botocore using awsrequest
import botocore.awsrequest as cpool
except ImportError: # pragma: no cover except ImportError: # pragma: no cover
return () try:
from .stubs import boto3_stubs # botocore using vendored requests
return self._urllib3_patchers(cpool, boto3_stubs) import botocore.vendored.requests.packages.urllib3.connectionpool as cpool
except ImportError: # pragma: no cover
pass
else:
from .stubs import boto3_stubs
yield self._urllib3_patchers(cpool, boto3_stubs)
else:
from .stubs import boto3_stubs
log.debug("Patching boto3 cpool with %s", cpool)
yield cpool.AWSHTTPConnectionPool, "ConnectionCls", boto3_stubs.VCRRequestsHTTPConnection
yield cpool.AWSHTTPSConnectionPool, "ConnectionCls", boto3_stubs.VCRRequestsHTTPSConnection
def _patched_get_conn(self, connection_pool_class, connection_class_getter): def _patched_get_conn(self, connection_pool_class, connection_class_getter):
get_conn = connection_pool_class._get_conn get_conn = connection_pool_class._get_conn
@@ -199,8 +228,8 @@ class CassettePatcherBuilder(object):
def patched_get_conn(pool, timeout=None): def patched_get_conn(pool, timeout=None):
connection = get_conn(pool, timeout) connection = get_conn(pool, timeout)
connection_class = ( connection_class = (
pool.ConnectionCls if hasattr(pool, 'ConnectionCls') pool.ConnectionCls if hasattr(pool, "ConnectionCls") else connection_class_getter()
else connection_class_getter()) )
# We need to make sure that we are actually providing a # We need to make sure that we are actually providing a
# patched version of the connection class. This might not # patched version of the connection class. This might not
# always be the case because the pool keeps previously # always be the case because the pool keeps previously
@@ -230,6 +259,7 @@ class CassettePatcherBuilder(object):
except ImportError: # pragma: no cover except ImportError: # pragma: no cover
return () return ()
from .stubs import urllib3_stubs from .stubs import urllib3_stubs
return self._urllib3_patchers(cpool, urllib3_stubs) return self._urllib3_patchers(cpool, urllib3_stubs)
@_build_patchers_from_mock_triples_decorator @_build_patchers_from_mock_triples_decorator
@@ -242,10 +272,12 @@ class CassettePatcherBuilder(object):
from .stubs.httplib2_stubs import VCRHTTPConnectionWithTimeout from .stubs.httplib2_stubs import VCRHTTPConnectionWithTimeout
from .stubs.httplib2_stubs import VCRHTTPSConnectionWithTimeout from .stubs.httplib2_stubs import VCRHTTPSConnectionWithTimeout
yield cpool, 'HTTPConnectionWithTimeout', VCRHTTPConnectionWithTimeout yield cpool, "HTTPConnectionWithTimeout", VCRHTTPConnectionWithTimeout
yield cpool, 'HTTPSConnectionWithTimeout', VCRHTTPSConnectionWithTimeout yield cpool, "HTTPSConnectionWithTimeout", VCRHTTPSConnectionWithTimeout
yield cpool, 'SCHEME_TO_CONNECTION', {'http': VCRHTTPConnectionWithTimeout, yield cpool, "SCHEME_TO_CONNECTION", {
'https': VCRHTTPSConnectionWithTimeout} "http": VCRHTTPConnectionWithTimeout,
"https": VCRHTTPSConnectionWithTimeout,
}
@_build_patchers_from_mock_triples_decorator @_build_patchers_from_mock_triples_decorator
def _boto(self): def _boto(self):
@@ -255,7 +287,8 @@ class CassettePatcherBuilder(object):
pass pass
else: else:
from .stubs.boto_stubs import VCRCertValidatingHTTPSConnection from .stubs.boto_stubs import VCRCertValidatingHTTPSConnection
yield cpool, 'CertValidatingHTTPSConnection', VCRCertValidatingHTTPSConnection
yield cpool, "CertValidatingHTTPSConnection", VCRCertValidatingHTTPSConnection
@_build_patchers_from_mock_triples_decorator @_build_patchers_from_mock_triples_decorator
def _tornado(self): def _tornado(self):
@@ -266,10 +299,8 @@ class CassettePatcherBuilder(object):
else: else:
from .stubs.tornado_stubs import vcr_fetch_impl from .stubs.tornado_stubs import vcr_fetch_impl
new_fetch_impl = vcr_fetch_impl( new_fetch_impl = vcr_fetch_impl(self._cassette, _SimpleAsyncHTTPClient_fetch_impl)
self._cassette, _SimpleAsyncHTTPClient_fetch_impl yield simple.SimpleAsyncHTTPClient, "fetch_impl", new_fetch_impl
)
yield simple.SimpleAsyncHTTPClient, 'fetch_impl', new_fetch_impl
try: try:
import tornado.curl_httpclient as curl import tornado.curl_httpclient as curl
except ImportError: # pragma: no cover except ImportError: # pragma: no cover
@@ -277,10 +308,8 @@ class CassettePatcherBuilder(object):
else: else:
from .stubs.tornado_stubs import vcr_fetch_impl from .stubs.tornado_stubs import vcr_fetch_impl
new_fetch_impl = vcr_fetch_impl( new_fetch_impl = vcr_fetch_impl(self._cassette, _CurlAsyncHTTPClient_fetch_impl)
self._cassette, _CurlAsyncHTTPClient_fetch_impl yield curl.CurlAsyncHTTPClient, "fetch_impl", new_fetch_impl
)
yield curl.CurlAsyncHTTPClient, 'fetch_impl', new_fetch_impl
@_build_patchers_from_mock_triples_decorator @_build_patchers_from_mock_triples_decorator
def _aiohttp(self): def _aiohttp(self):
@@ -290,10 +319,24 @@ class CassettePatcherBuilder(object):
pass pass
else: else:
from .stubs.aiohttp_stubs import vcr_request from .stubs.aiohttp_stubs import vcr_request
new_request = vcr_request(
self._cassette, _AiohttpClientSessionRequest new_request = vcr_request(self._cassette, _AiohttpClientSessionRequest)
) yield client.ClientSession, "_request", new_request
yield client.ClientSession, '_request', new_request
@_build_patchers_from_mock_triples_decorator
def _httpx(self):
try:
import httpx
except ImportError: # pragma: no cover
return
else:
from .stubs.httpx_stubs import async_vcr_send, sync_vcr_send
new_async_client_send = async_vcr_send(self._cassette, _HttpxAsyncClient_send)
yield httpx.AsyncClient, "send", new_async_client_send
new_sync_client_send = sync_vcr_send(self._cassette, _HttpxSyncClient_send)
yield httpx.Client, "send", new_sync_client_send
def _urllib3_patchers(self, cpool, stubs): def _urllib3_patchers(self, cpool, stubs):
http_connection_remover = ConnectionRemover( http_connection_remover = ConnectionRemover(
@@ -303,34 +346,45 @@ class CassettePatcherBuilder(object):
self._get_cassette_subclass(stubs.VCRRequestsHTTPSConnection) self._get_cassette_subclass(stubs.VCRRequestsHTTPSConnection)
) )
mock_triples = ( mock_triples = (
(cpool, 'VerifiedHTTPSConnection', stubs.VCRRequestsHTTPSConnection), (cpool, "VerifiedHTTPSConnection", stubs.VCRRequestsHTTPSConnection),
(cpool, 'HTTPConnection', stubs.VCRRequestsHTTPConnection), (cpool, "HTTPConnection", stubs.VCRRequestsHTTPConnection),
(cpool, 'HTTPSConnection', stubs.VCRRequestsHTTPSConnection), (cpool, "HTTPSConnection", stubs.VCRRequestsHTTPSConnection),
(cpool, 'is_connection_dropped', mock.Mock(return_value=False)), # Needed on Windows only (cpool, "is_connection_dropped", mock.Mock(return_value=False)), # Needed on Windows only
(cpool.HTTPConnectionPool, 'ConnectionCls', stubs.VCRRequestsHTTPConnection), (cpool.HTTPConnectionPool, "ConnectionCls", stubs.VCRRequestsHTTPConnection),
(cpool.HTTPSConnectionPool, 'ConnectionCls', stubs.VCRRequestsHTTPSConnection), (cpool.HTTPSConnectionPool, "ConnectionCls", stubs.VCRRequestsHTTPSConnection),
) )
# These handle making sure that sessions only use the # These handle making sure that sessions only use the
# connections of the appropriate type. # connections of the appropriate type.
mock_triples += ((cpool.HTTPConnectionPool, '_get_conn', mock_triples += (
self._patched_get_conn(cpool.HTTPConnectionPool, (
lambda: cpool.HTTPConnection)), cpool.HTTPConnectionPool,
(cpool.HTTPSConnectionPool, '_get_conn', "_get_conn",
self._patched_get_conn(cpool.HTTPSConnectionPool, self._patched_get_conn(cpool.HTTPConnectionPool, lambda: cpool.HTTPConnection),
lambda: cpool.HTTPSConnection)), ),
(cpool.HTTPConnectionPool, '_new_conn', (
self._patched_new_conn(cpool.HTTPConnectionPool, cpool.HTTPSConnectionPool,
http_connection_remover)), "_get_conn",
(cpool.HTTPSConnectionPool, '_new_conn', self._patched_get_conn(cpool.HTTPSConnectionPool, lambda: cpool.HTTPSConnection),
self._patched_new_conn(cpool.HTTPSConnectionPool, ),
https_connection_remover))) (
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), return itertools.chain(
(http_connection_remover, https_connection_remover)) self._build_patchers_from_mock_triples(mock_triples),
(http_connection_remover, https_connection_remover),
)
class ConnectionRemover(object): class ConnectionRemover:
def __init__(self, connection_class): def __init__(self, connection_class):
self._connection_class = connection_class self._connection_class = connection_class
self._connection_pool_to_connections = {} self._connection_pool_to_connections = {}
@@ -360,11 +414,12 @@ class ConnectionRemover(object):
def reset_patchers(): def reset_patchers():
yield mock.patch.object(httplib, 'HTTPConnection', _HTTPConnection) yield mock.patch.object(httplib, "HTTPConnection", _HTTPConnection)
yield mock.patch.object(httplib, 'HTTPSConnection', _HTTPSConnection) yield mock.patch.object(httplib, "HTTPSConnection", _HTTPSConnection)
try: try:
import requests import requests
if requests.__build__ < 0x021603: if requests.__build__ < 0x021603:
# Avoid double unmock if requests 2.16.3 # Avoid double unmock if requests 2.16.3
# First, this is pointless, requests.packages.urllib3 *IS* urllib3 (see packages.py) # First, this is pointless, requests.packages.urllib3 *IS* urllib3 (see packages.py)
@@ -382,85 +437,86 @@ def reset_patchers():
pass pass
else: else:
# unpatch requests v1.x # unpatch requests v1.x
yield mock.patch.object(cpool, 'VerifiedHTTPSConnection', _VerifiedHTTPSConnection) yield mock.patch.object(cpool, "VerifiedHTTPSConnection", _VerifiedHTTPSConnection)
yield mock.patch.object(cpool, 'HTTPConnection', _cpoolHTTPConnection) yield mock.patch.object(cpool, "HTTPConnection", _cpoolHTTPConnection)
# unpatch requests v2.x # unpatch requests v2.x
if hasattr(cpool.HTTPConnectionPool, 'ConnectionCls'): if hasattr(cpool.HTTPConnectionPool, "ConnectionCls"):
yield mock.patch.object(cpool.HTTPConnectionPool, 'ConnectionCls', yield mock.patch.object(cpool.HTTPConnectionPool, "ConnectionCls", _cpoolHTTPConnection)
_cpoolHTTPConnection) yield mock.patch.object(cpool.HTTPSConnectionPool, "ConnectionCls", _cpoolHTTPSConnection)
yield mock.patch.object(cpool.HTTPSConnectionPool, 'ConnectionCls',
_cpoolHTTPSConnection)
if hasattr(cpool, 'HTTPSConnection'): if hasattr(cpool, "HTTPSConnection"):
yield mock.patch.object(cpool, 'HTTPSConnection', _cpoolHTTPSConnection) yield mock.patch.object(cpool, "HTTPSConnection", _cpoolHTTPSConnection)
try: try:
import urllib3.connectionpool as cpool import urllib3.connectionpool as cpool
except ImportError: # pragma: no cover except ImportError: # pragma: no cover
pass pass
else: else:
yield mock.patch.object(cpool, 'VerifiedHTTPSConnection', _VerifiedHTTPSConnection) yield mock.patch.object(cpool, "VerifiedHTTPSConnection", _VerifiedHTTPSConnection)
yield mock.patch.object(cpool, 'HTTPConnection', _cpoolHTTPConnection) yield mock.patch.object(cpool, "HTTPConnection", _cpoolHTTPConnection)
yield mock.patch.object(cpool, 'HTTPSConnection', _cpoolHTTPSConnection) yield mock.patch.object(cpool, "HTTPSConnection", _cpoolHTTPSConnection)
if hasattr(cpool.HTTPConnectionPool, 'ConnectionCls'): if hasattr(cpool.HTTPConnectionPool, "ConnectionCls"):
yield mock.patch.object(cpool.HTTPConnectionPool, 'ConnectionCls', _cpoolHTTPConnection) yield mock.patch.object(cpool.HTTPConnectionPool, "ConnectionCls", _cpoolHTTPConnection)
yield mock.patch.object(cpool.HTTPSConnectionPool, 'ConnectionCls', _cpoolHTTPSConnection) yield mock.patch.object(cpool.HTTPSConnectionPool, "ConnectionCls", _cpoolHTTPSConnection)
try: try:
import botocore.vendored.requests.packages.urllib3.connectionpool as cpool # unpatch botocore with awsrequest
import botocore.awsrequest as cpool
except ImportError: # pragma: no cover except ImportError: # pragma: no cover
pass try:
else: # unpatch botocore with vendored requests
# unpatch requests v1.x import botocore.vendored.requests.packages.urllib3.connectionpool as cpool
yield mock.patch.object(cpool, 'VerifiedHTTPSConnection', _Boto3VerifiedHTTPSConnection) except ImportError: # pragma: no cover
yield mock.patch.object(cpool, 'HTTPConnection', _cpoolBoto3HTTPConnection) pass
# unpatch requests v2.x else:
if hasattr(cpool.HTTPConnectionPool, 'ConnectionCls'): # unpatch requests v1.x
yield mock.patch.object(cpool.HTTPConnectionPool, 'ConnectionCls', yield mock.patch.object(cpool, "VerifiedHTTPSConnection", _Boto3VerifiedHTTPSConnection)
_cpoolBoto3HTTPConnection) yield mock.patch.object(cpool, "HTTPConnection", _cpoolBoto3HTTPConnection)
yield mock.patch.object(cpool.HTTPSConnectionPool, 'ConnectionCls', # unpatch requests v2.x
_cpoolBoto3HTTPSConnection) if hasattr(cpool.HTTPConnectionPool, "ConnectionCls"):
yield mock.patch.object(cpool.HTTPConnectionPool, "ConnectionCls", _cpoolBoto3HTTPConnection)
yield mock.patch.object(
cpool.HTTPSConnectionPool, "ConnectionCls", _cpoolBoto3HTTPSConnection
)
if hasattr(cpool, 'HTTPSConnection'): if hasattr(cpool, "HTTPSConnection"):
yield mock.patch.object(cpool, 'HTTPSConnection', _cpoolBoto3HTTPSConnection) yield mock.patch.object(cpool, "HTTPSConnection", _cpoolBoto3HTTPSConnection)
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: try:
import httplib2 as cpool import httplib2 as cpool
except ImportError: # pragma: no cover except ImportError: # pragma: no cover
pass pass
else: else:
yield mock.patch.object(cpool, 'HTTPConnectionWithTimeout', _HTTPConnectionWithTimeout) yield mock.patch.object(cpool, "HTTPConnectionWithTimeout", _HTTPConnectionWithTimeout)
yield mock.patch.object(cpool, 'HTTPSConnectionWithTimeout', _HTTPSConnectionWithTimeout) yield mock.patch.object(cpool, "HTTPSConnectionWithTimeout", _HTTPSConnectionWithTimeout)
yield mock.patch.object(cpool, 'SCHEME_TO_CONNECTION', _SCHEME_TO_CONNECTION) yield mock.patch.object(cpool, "SCHEME_TO_CONNECTION", _SCHEME_TO_CONNECTION)
try: try:
import boto.https_connection as cpool import boto.https_connection as cpool
except ImportError: # pragma: no cover except ImportError: # pragma: no cover
pass pass
else: else:
yield mock.patch.object(cpool, 'CertValidatingHTTPSConnection', yield mock.patch.object(cpool, "CertValidatingHTTPSConnection", _CertValidatingHTTPSConnection)
_CertValidatingHTTPSConnection)
try: try:
import tornado.simple_httpclient as simple import tornado.simple_httpclient as simple
except ImportError: # pragma: no cover except ImportError: # pragma: no cover
pass pass
else: else:
yield mock.patch.object( yield mock.patch.object(simple.SimpleAsyncHTTPClient, "fetch_impl", _SimpleAsyncHTTPClient_fetch_impl)
simple.SimpleAsyncHTTPClient,
'fetch_impl',
_SimpleAsyncHTTPClient_fetch_impl,
)
try: try:
import tornado.curl_httpclient as curl import tornado.curl_httpclient as curl
except ImportError: # pragma: no cover except ImportError: # pragma: no cover
pass pass
else: else:
yield mock.patch.object( yield mock.patch.object(curl.CurlAsyncHTTPClient, "fetch_impl", _CurlAsyncHTTPClient_fetch_impl)
curl.CurlAsyncHTTPClient,
'fetch_impl',
_CurlAsyncHTTPClient_fetch_impl,
)
@contextlib.contextmanager @contextlib.contextmanager

View File

@@ -4,15 +4,14 @@ import os
from ..serialize import serialize, deserialize from ..serialize import serialize, deserialize
class FilesystemPersister(object): class FilesystemPersister:
@classmethod @classmethod
def load_cassette(cls, cassette_path, serializer): def load_cassette(cls, cassette_path, serializer):
try: try:
with open(cassette_path) as f: with open(cassette_path) as f:
cassette_content = f.read() cassette_content = f.read()
except IOError: except OSError:
raise ValueError('Cassette not found.') raise ValueError("Cassette not found.")
cassette = deserialize(cassette_content, serializer) cassette = deserialize(cassette_content, serializer)
return cassette return cassette
@@ -22,5 +21,5 @@ class FilesystemPersister(object):
dirname, filename = os.path.split(cassette_path) dirname, filename = os.path.split(cassette_path)
if dirname and not os.path.exists(dirname): if dirname and not os.path.exists(dirname):
os.makedirs(dirname) os.makedirs(dirname)
with open(cassette_path, 'w') as f: with open(cassette_path, "w") as f:
f.write(data) f.write(data)

23
vcr/record_mode.py Normal file
View File

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

View File

@@ -1,10 +1,13 @@
import warnings import warnings
from six import BytesIO, text_type from io import BytesIO
from six.moves.urllib.parse import urlparse, parse_qsl from urllib.parse import urlparse, parse_qsl
from .util import CaseInsensitiveDict from .util import CaseInsensitiveDict
import logging
log = logging.getLogger(__name__)
class Request(object): class Request:
""" """
VCR's representation of a request. VCR's representation of a request.
""" """
@@ -12,12 +15,13 @@ class Request(object):
def __init__(self, method, uri, body, headers): def __init__(self, method, uri, body, headers):
self.method = method self.method = method
self.uri = uri self.uri = uri
self._was_file = hasattr(body, 'read') self._was_file = hasattr(body, "read")
if self._was_file: if self._was_file:
self.body = body.read() self.body = body.read()
else: else:
self.body = body self.body = body
self.headers = headers self.headers = headers
log.debug("Invoking Request %s", self.uri)
@property @property
def headers(self): def headers(self):
@@ -35,14 +39,15 @@ class Request(object):
@body.setter @body.setter
def body(self, value): def body(self, value):
if isinstance(value, text_type): if isinstance(value, str):
value = value.encode('utf-8') value = value.encode("utf-8")
self._body = value self._body = value
def add_header(self, key, value): def add_header(self, key, value):
warnings.warn("Request.add_header is deprecated. " warnings.warn(
"Please assign to request.headers instead.", "Request.add_header is deprecated. " "Please assign to request.headers instead.",
DeprecationWarning) DeprecationWarning,
)
self.headers[key] = value self.headers[key] = value
@property @property
@@ -58,7 +63,10 @@ class Request(object):
parse_uri = urlparse(self.uri) parse_uri = urlparse(self.uri)
port = parse_uri.port port = parse_uri.port
if port is None: if port is None:
port = {'https': 443, 'http': 80}[parse_uri.scheme] try:
port = {"https": 443, "http": 80}[parse_uri.scheme]
except KeyError:
pass
return port return port
@property @property
@@ -88,10 +96,10 @@ class Request(object):
def _to_dict(self): def _to_dict(self):
return { return {
'method': self.method, "method": self.method,
'uri': self.uri, "uri": self.uri,
'body': self.body, "body": self.body,
'headers': dict(((k, [v]) for k, v in self.headers.items())), "headers": {k: [v] for k, v in self.headers.items()},
} }
@classmethod @classmethod
@@ -112,7 +120,7 @@ class HeadersDict(CaseInsensitiveDict):
In addition, some servers sometimes send the same header more than once, In addition, some servers sometimes send the same header more than once,
and httplib *can* deal with this situation. and httplib *can* deal with this situation.
Futhermore, I wanted to keep the request and response cassette format as Furthermore, I wanted to keep the request and response cassette format as
similar as possible. similar as possible.
For this reason, in cassettes I keep a dict with lists as keys, but once For this reason, in cassettes I keep a dict with lists as keys, but once
@@ -128,4 +136,4 @@ class HeadersDict(CaseInsensitiveDict):
if old: if old:
key = old[0] key = old[0]
super(HeadersDict, self).__setitem__(key, value) super().__setitem__(key, value)

View File

@@ -20,7 +20,7 @@ Deserializing: string (yaml converts from utf-8) -> bytestring
def _looks_like_an_old_cassette(data): def _looks_like_an_old_cassette(data):
return isinstance(data, list) and len(data) and 'request' in data[0] return isinstance(data, list) and len(data) and "request" in data[0]
def _warn_about_old_cassette_format(): def _warn_about_old_cassette_format():
@@ -41,23 +41,18 @@ def deserialize(cassette_string, serializer):
if _looks_like_an_old_cassette(data): if _looks_like_an_old_cassette(data):
_warn_about_old_cassette_format() _warn_about_old_cassette_format()
requests = [Request._from_dict(r['request']) for r in data['interactions']] requests = [Request._from_dict(r["request"]) for r in data["interactions"]]
responses = [ responses = [compat.convert_to_bytes(r["response"]) for r in data["interactions"]]
compat.convert_to_bytes(r['response']) for r in data['interactions']
]
return requests, responses return requests, responses
def serialize(cassette_dict, serializer): def serialize(cassette_dict, serializer):
interactions = ([{ interactions = [
'request': compat.convert_to_unicode(request._to_dict()), {
'response': compat.convert_to_unicode(response), "request": compat.convert_to_unicode(request._to_dict()),
} for request, response in zip( "response": compat.convert_to_unicode(response),
cassette_dict['requests'], }
cassette_dict['responses'], for request, response in zip(cassette_dict["requests"], cassette_dict["responses"])
)]) ]
data = { data = {"version": CASSETTE_FORMAT_VERSION, "interactions": interactions}
'version': CASSETTE_FORMAT_VERSION,
'interactions': interactions,
}
return serializer.serialize(data) return serializer.serialize(data)

View File

@@ -1,6 +1,3 @@
import six
def convert_to_bytes(resp): def convert_to_bytes(resp):
resp = convert_body_to_bytes(resp) resp = convert_body_to_bytes(resp)
return resp return resp
@@ -24,8 +21,8 @@ def convert_body_to_bytes(resp):
http://pyyaml.org/wiki/PyYAMLDocumentation#Python3support http://pyyaml.org/wiki/PyYAMLDocumentation#Python3support
""" """
try: try:
if resp['body']['string'] is not None and not isinstance(resp['body']['string'], six.binary_type): if resp["body"]["string"] is not None and not isinstance(resp["body"]["string"], bytes):
resp['body']['string'] = resp['body']['string'].encode('utf-8') resp["body"]["string"] = resp["body"]["string"].encode("utf-8")
except (KeyError, TypeError, UnicodeEncodeError): except (KeyError, TypeError, UnicodeEncodeError):
# The thing we were converting either wasn't a dictionary or didn't # The thing we were converting either wasn't a dictionary or didn't
# have the keys we were expecting. Some of the tests just serialize # have the keys we were expecting. Some of the tests just serialize
@@ -44,8 +41,8 @@ def _convert_string_to_unicode(string):
result = string result = string
try: try:
if string is not None and not isinstance(string, six.text_type): if string is not None and not isinstance(string, str):
result = string.decode('utf-8') result = string.decode("utf-8")
except (TypeError, UnicodeDecodeError, AttributeError): except (TypeError, UnicodeDecodeError, AttributeError):
# Sometimes the string actually is binary or StringIO object, # Sometimes the string actually is binary or StringIO object,
# so if you can't decode it, just give up. # so if you can't decode it, just give up.
@@ -63,17 +60,15 @@ def convert_body_to_unicode(resp):
# Some of the tests just serialize and deserialize a string. # Some of the tests just serialize and deserialize a string.
return _convert_string_to_unicode(resp) return _convert_string_to_unicode(resp)
else: else:
body = resp.get('body') body = resp.get("body")
if body is not None: if body is not None:
try: try:
body['string'] = _convert_string_to_unicode( body["string"] = _convert_string_to_unicode(body["string"])
body['string']
)
except (KeyError, TypeError, AttributeError): except (KeyError, TypeError, AttributeError):
# The thing we were converting either wasn't a dictionary or # The thing we were converting either wasn't a dictionary or
# didn't have the keys we were expecting. # didn't have the keys we were expecting.
# For example request object has no 'string' key. # For example request object has no 'string' key.
resp['body'] = _convert_string_to_unicode(body) resp["body"] = _convert_string_to_unicode(body)
return resp return resp

View File

@@ -23,7 +23,7 @@ def serialize(cassette_dict):
b"Error serializing cassette to JSON", b"Error serializing cassette to JSON",
original.start, original.start,
original.end, original.end,
original.args[-1] + error_message original.args[-1] + error_message,
) )
except TypeError as original: # py3 except TypeError: # py3
raise TypeError(error_message) raise TypeError(error_message)

View File

@@ -1,13 +1,12 @@
'''Stubs for patching HTTP and HTTPS requests''' """Stubs for patching HTTP and HTTPS requests"""
import logging import logging
import six from http.client import (
from six.moves.http_client import (
HTTPConnection, HTTPConnection,
HTTPSConnection, HTTPSConnection,
HTTPResponse, HTTPResponse,
) )
from six import BytesIO from io import BytesIO
from vcr.request import Request from vcr.request import Request
from vcr.errors import CannotOverwriteExistingCassetteException from vcr.errors import CannotOverwriteExistingCassetteException
from . import compat from . import compat
@@ -15,10 +14,10 @@ from . import compat
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class VCRFakeSocket(object): class VCRFakeSocket:
""" """
A socket that doesn't do anything! A socket that doesn't do anything!
Used when playing back casssettes, when there Used when playing back cassettes, when there
is no actual open socket. is no actual open socket.
""" """
@@ -45,8 +44,7 @@ def parse_headers(header_list):
header_string = b"" header_string = b""
for key, values in header_list.items(): for key, values in header_list.items():
for v in values: for v in values:
header_string += \ header_string += key.encode("utf-8") + b":" + v.encode("utf-8") + b"\r\n"
key.encode('utf-8') + b":" + v.encode('utf-8') + b"\r\n"
return compat.get_httpmessage(header_string) return compat.get_httpmessage(header_string)
@@ -60,28 +58,30 @@ def serialize_headers(response):
class VCRHTTPResponse(HTTPResponse): class VCRHTTPResponse(HTTPResponse):
""" """
Stub reponse class that gets returned instead of a HTTPResponse Stub response class that gets returned instead of a HTTPResponse
""" """
def __init__(self, recorded_response): def __init__(self, recorded_response):
self.fp = None
self.recorded_response = recorded_response self.recorded_response = recorded_response
self.reason = recorded_response['status']['message'] self.reason = recorded_response["status"]["message"]
self.status = self.code = recorded_response['status']['code'] self.status = self.code = recorded_response["status"]["code"]
self.version = None self.version = None
self._content = BytesIO(self.recorded_response['body']['string']) self._content = BytesIO(self.recorded_response["body"]["string"])
self._closed = False self._closed = False
headers = self.recorded_response['headers'] headers = self.recorded_response["headers"]
# Since we are loading a response that has already been serialized, our # Since we are loading a response that has already been serialized, our
# response is no longer chunked. That means we don't want any # response is no longer chunked. That means we don't want any
# libraries trying to process a chunked response. By removing the # libraries trying to process a chunked response. By removing the
# transfer-encoding: chunked header, this should cause the downstream # transfer-encoding: chunked header, this should cause the downstream
# libraries to process this as a non-chunked response. # libraries to process this as a non-chunked response.
te_key = [h for h in headers.keys() if h.upper() == 'TRANSFER-ENCODING'] te_key = [h for h in headers.keys() if h.upper() == "TRANSFER-ENCODING"]
if te_key: if te_key:
del headers[te_key[0]] del headers[te_key[0]]
self.headers = self.msg = parse_headers(headers) self.headers = self.msg = parse_headers(headers)
self.length = compat.get_header(self.msg, 'content-length') or None self.length = compat.get_header(self.msg, "content-length") or None
@property @property
def closed(self): def closed(self):
@@ -93,9 +93,30 @@ class VCRHTTPResponse(HTTPResponse):
def read(self, *args, **kwargs): def read(self, *args, **kwargs):
return self._content.read(*args, **kwargs) return self._content.read(*args, **kwargs)
def readall(self):
return self._content.readall()
def readinto(self, *args, **kwargs):
return self._content.readinto(*args, **kwargs)
def readline(self, *args, **kwargs): def readline(self, *args, **kwargs):
return self._content.readline(*args, **kwargs) return self._content.readline(*args, **kwargs)
def readlines(self, *args, **kwargs):
return self._content.readlines(*args, **kwargs)
def seekable(self):
return self._content.seekable()
def tell(self):
return self._content.tell()
def isatty(self):
return self._content.isatty()
def seek(self, *args, **kwargs):
return self._content.seek(*args, **kwargs)
def close(self): def close(self):
self._closed = True self._closed = True
return True return True
@@ -107,22 +128,25 @@ class VCRHTTPResponse(HTTPResponse):
return self.closed return self.closed
def info(self): def info(self):
return parse_headers(self.recorded_response['headers']) return parse_headers(self.recorded_response["headers"])
def getheaders(self): def getheaders(self):
message = parse_headers(self.recorded_response['headers']) message = parse_headers(self.recorded_response["headers"])
return list(compat.get_header_items(message)) return list(compat.get_header_items(message))
def getheader(self, header, default=None): def getheader(self, header, default=None):
values = [v for (k, v) in self.getheaders() if k.lower() == header.lower()] values = [v for (k, v) in self.getheaders() if k.lower() == header.lower()]
if values: if values:
return ', '.join(values) return ", ".join(values)
else: else:
return default return default
def readable(self):
return self._content.readable()
class VCRConnection(object):
class VCRConnection:
# A reference to the cassette that's currently being patched in # A reference to the cassette that's currently being patched in
cassette = None cassette = None
@@ -131,68 +155,54 @@ class VCRConnection(object):
Returns empty string for the default port and ':port' otherwise Returns empty string for the default port and ':port' otherwise
""" """
port = self.real_connection.port port = self.real_connection.port
default_port = {'https': 443, 'http': 80}[self._protocol] default_port = {"https": 443, "http": 80}[self._protocol]
return ':{}'.format(port) if port != default_port else '' return ":{}".format(port) if port != default_port else ""
def _uri(self, url): def _uri(self, url):
"""Returns request absolute URI""" """Returns request absolute URI"""
uri = "{}://{}{}{}".format( if url and not url.startswith("/"):
self._protocol, # Then this must be a proxy request.
self.real_connection.host, return url
self._port_postfix(), uri = "{}://{}{}{}".format(self._protocol, self.real_connection.host, self._port_postfix(), url)
url, log.debug("Absolute URI: %s", uri)
)
return uri return uri
def _url(self, uri): def _url(self, uri):
"""Returns request selector url from absolute URI""" """Returns request selector url from absolute URI"""
prefix = "{}://{}{}".format( prefix = "{}://{}{}".format(self._protocol, self.real_connection.host, self._port_postfix())
self._protocol, return uri.replace(prefix, "", 1)
self.real_connection.host,
self._port_postfix(),
)
return uri.replace(prefix, '', 1)
def request(self, method, url, body=None, headers=None, *args, **kwargs): def request(self, method, url, body=None, headers=None, *args, **kwargs):
'''Persist the request metadata in self._vcr_request''' """Persist the request metadata in self._vcr_request"""
self._vcr_request = Request( self._vcr_request = Request(method=method, uri=self._uri(url), body=body, headers=headers or {})
method=method, log.debug("Got {}".format(self._vcr_request))
uri=self._uri(url),
body=body,
headers=headers or {}
)
log.debug('Got {}'.format(self._vcr_request))
# Note: The request may not actually be finished at this point, so # Note: The request may not actually be finished at this point, so
# I'm not sending the actual request until getresponse(). This # I'm not sending the actual request until getresponse(). This
# allows me to compare the entire length of the response to see if it # allows me to compare the entire length of the response to see if it
# exists in the cassette. # exists in the cassette.
self._sock = VCRFakeSocket()
def putrequest(self, method, url, *args, **kwargs): def putrequest(self, method, url, *args, **kwargs):
""" """
httplib gives you more than one way to do it. This is a way httplib gives you more than one way to do it. This is a way
to start building up a request. Usually followed by a bunch to start building up a request. Usually followed by a bunch
of putheader() calls. of putheader() calls.
""" """
self._vcr_request = Request( self._vcr_request = Request(method=method, uri=self._uri(url), body="", headers={})
method=method, log.debug("Got {}".format(self._vcr_request))
uri=self._uri(url),
body="",
headers={}
)
log.debug('Got {}'.format(self._vcr_request))
def putheader(self, header, *values): def putheader(self, header, *values):
self._vcr_request.headers[header] = values self._vcr_request.headers[header] = values
def send(self, data): def send(self, data):
''' """
This method is called after request(), to add additional data to the This method is called after request(), to add additional data to the
body of the request. So if that happens, let's just append the data body of the request. So if that happens, let's just append the data
onto the most recent request in the cassette. onto the most recent request in the cassette.
''' """
self._vcr_request.body = self._vcr_request.body + data \ self._vcr_request.body = self._vcr_request.body + data if self._vcr_request.body else data
if self._vcr_request.body else data
def close(self): def close(self):
# Note: the real connection will only close if it's open, so # Note: the real connection will only close if it's open, so
@@ -209,40 +219,27 @@ class VCRConnection(object):
self._vcr_request.body = message_body self._vcr_request.body = message_body
def getresponse(self, _=False, **kwargs): def getresponse(self, _=False, **kwargs):
'''Retrieve the response''' """Retrieve the response"""
# Check to see if the cassette has a response for this request. If so, # Check to see if the cassette has a response for this request. If so,
# then return it # then return it
if self.cassette.can_play_response_for(self._vcr_request): if self.cassette.can_play_response_for(self._vcr_request):
log.info( log.info("Playing response for {} from cassette".format(self._vcr_request))
"Playing response for {} from cassette".format(
self._vcr_request
)
)
response = self.cassette.play_response(self._vcr_request) response = self.cassette.play_response(self._vcr_request)
return VCRHTTPResponse(response) return VCRHTTPResponse(response)
else: else:
if self.cassette.write_protected and self.cassette.filter_request( if self.cassette.write_protected and self.cassette.filter_request(self._vcr_request):
self._vcr_request
):
raise CannotOverwriteExistingCassetteException( raise CannotOverwriteExistingCassetteException(
"No match for the request (%r) was found. " cassette=self.cassette, failed_request=self._vcr_request
"Can't overwrite existing cassette (%r) in "
"your current record mode (%r)."
% (self._vcr_request, self.cassette._path,
self.cassette.record_mode)
) )
# Otherwise, we should send the request, then get the response # Otherwise, we should send the request, then get the response
# and return it. # and return it.
log.info( log.info("{} not in cassette, sending to real server".format(self._vcr_request))
"{} not in cassette, sending to real server".format(
self._vcr_request
)
)
# This is imported here to avoid circular import. # This is imported here to avoid circular import.
# TODO(@IvanMalison): Refactor to allow normal import. # TODO(@IvanMalison): Refactor to allow normal import.
from vcr.patch import force_reset from vcr.patch import force_reset
with force_reset(): with force_reset():
self.real_connection.request( self.real_connection.request(
method=self._vcr_request.method, method=self._vcr_request.method,
@@ -256,12 +253,9 @@ class VCRConnection(object):
# put the response into the cassette # put the response into the cassette
response = { response = {
'status': { "status": {"code": response.status, "message": response.reason},
'code': response.status, "headers": serialize_headers(response),
'message': response.reason "body": {"string": response.read()},
},
'headers': serialize_headers(response),
'body': {'string': response.read()},
} }
self.cassette.append(self._vcr_request, response) self.cassette.append(self._vcr_request, response)
return VCRHTTPResponse(response) return VCRHTTPResponse(response)
@@ -277,8 +271,7 @@ class VCRConnection(object):
and are not write-protected. and are not write-protected.
""" """
if hasattr(self, '_vcr_request') and \ if hasattr(self, "_vcr_request") and self.cassette.can_play_response_for(self._vcr_request):
self.cassette.can_play_response_for(self._vcr_request):
# We already have a response we are going to play, don't # We already have a response we are going to play, don't
# actually connect # actually connect
return return
@@ -288,14 +281,17 @@ class VCRConnection(object):
return return
from vcr.patch import force_reset from vcr.patch import force_reset
with force_reset(): with force_reset():
return self.real_connection.connect(*args, **kwargs) return self.real_connection.connect(*args, **kwargs)
self._sock = VCRFakeSocket()
@property @property
def sock(self): def sock(self):
if self.real_connection.sock: if self.real_connection.sock:
return self.real_connection.sock return self.real_connection.sock
return VCRFakeSocket() return self._sock
@sock.setter @sock.setter
def sock(self, value): def sock(self, value):
@@ -303,16 +299,18 @@ class VCRConnection(object):
self.real_connection.sock = value self.real_connection.sock = value
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
if six.PY3: kwargs.pop("strict", None) # apparently this is gone in py3
kwargs.pop('strict', None) # apparently this is gone in py3
# need to temporarily reset here because the real connection # need to temporarily reset here because the real connection
# inherits from the thing that we are mocking out. Take out # inherits from the thing that we are mocking out. Take out
# the reset if you want to see what I mean :) # the reset if you want to see what I mean :)
from vcr.patch import force_reset from vcr.patch import force_reset
with force_reset(): with force_reset():
self.real_connection = self._baseclass(*args, **kwargs) self.real_connection = self._baseclass(*args, **kwargs)
self._sock = None
def __setattr__(self, name, value): def __setattr__(self, name, value):
""" """
We need to define this because any attributes that are set on the We need to define this because any attributes that are set on the
@@ -332,19 +330,19 @@ class VCRConnection(object):
# we're setting the real_connection itself for the first time # we're setting the real_connection itself for the first time
pass pass
super(VCRConnection, self).__setattr__(name, value) super().__setattr__(name, value)
def __getattr__(self, name): def __getattr__(self, name):
""" """
Send requests for weird attributes up to the real connection Send requests for weird attributes up to the real connection
(counterpart to __setattr above) (counterpart to __setattr above)
""" """
if self.__dict__.get('real_connection'): if self.__dict__.get("real_connection"):
# check in case real_connection has not been set yet, such as when # check in case real_connection has not been set yet, such as when
# we're setting the real_connection itself for the first time # we're setting the real_connection itself for the first time
return getattr(self.real_connection, name) return getattr(self.real_connection, name)
return super(VCRConnection, self).__getattr__(name) return super().__getattr__(name)
for k, v in HTTPConnection.__dict__.items(): for k, v in HTTPConnection.__dict__.items():
@@ -353,13 +351,15 @@ for k, v in HTTPConnection.__dict__.items():
class VCRHTTPConnection(VCRConnection): class VCRHTTPConnection(VCRConnection):
'''A Mocked class for HTTP requests''' """A Mocked class for HTTP requests"""
_baseclass = HTTPConnection _baseclass = HTTPConnection
_protocol = 'http' _protocol = "http"
class VCRHTTPSConnection(VCRConnection): class VCRHTTPSConnection(VCRConnection):
'''A Mocked class for HTTPS requests''' """A Mocked class for HTTPS requests"""
_baseclass = HTTPSConnection _baseclass = HTTPSConnection
_protocol = 'https' _protocol = "https"
is_verified = True is_verified = True

274
vcr/stubs/aiohttp_stubs.py Normal file
View File

@@ -0,0 +1,274 @@
"""Stubs for aiohttp HTTP clients"""
import asyncio
import functools
import logging
import json
from aiohttp import ClientConnectionError, ClientResponse, RequestInfo, streams
from aiohttp import hdrs, CookieJar
from http.cookies import CookieError, Morsel, SimpleCookie
from aiohttp.helpers import strip_auth_from_url
from multidict import CIMultiDict, CIMultiDictProxy
from yarl import URL
from vcr.errors import CannotOverwriteExistingCassetteException
from vcr.request import Request
log = logging.getLogger(__name__)
class MockStream(asyncio.StreamReader, streams.AsyncStreamReaderMixin):
pass
class MockClientResponse(ClientResponse):
def __init__(self, method, url, request_info=None):
super().__init__(
method=method,
url=url,
writer=None,
continue100=None,
timer=None,
request_info=request_info,
traces=None,
loop=asyncio.get_event_loop(),
session=None,
)
async def json(self, *, encoding="utf-8", loads=json.loads, **kwargs): # NOQA: E999
stripped = self._body.strip()
if not stripped:
return None
return loads(stripped.decode(encoding))
async def text(self, encoding="utf-8", errors="strict"):
return self._body.decode(encoding, errors=errors)
async def read(self):
return self._body
def release(self):
pass
@property
def content(self):
s = MockStream()
s.feed_data(self._body)
s.feed_eof()
return s
def build_response(vcr_request, vcr_response, history):
request_info = RequestInfo(
url=URL(vcr_request.url),
method=vcr_request.method,
headers=_deserialize_headers(vcr_request.headers),
real_url=URL(vcr_request.url),
)
response = MockClientResponse(vcr_request.method, URL(vcr_response.get("url")), request_info=request_info)
response.status = vcr_response["status"]["code"]
response._body = vcr_response["body"].get("string", b"")
response.reason = vcr_response["status"]["message"]
response._headers = _deserialize_headers(vcr_response["headers"])
response._history = tuple(history)
# cookies
for hdr in response.headers.getall(hdrs.SET_COOKIE, ()):
try:
cookies = SimpleCookie(hdr)
for cookie_name, cookie in cookies.items():
expires = cookie.get("expires", "").strip()
if expires:
log.debug('Ignoring expiration date: %s="%s"', cookie_name, expires)
cookie["expires"] = ""
response.cookies.load(cookie.output(header="").strip())
except CookieError as exc:
log.warning("Can not load response cookies: %s", exc)
response.close()
return response
def _serialize_headers(headers):
"""Serialize CIMultiDictProxy to a pickle-able dict because proxy
objects forbid pickling:
https://github.com/aio-libs/multidict/issues/340
"""
# Mark strings as keys so 'istr' types don't show up in
# the cassettes as comments.
serialized_headers = {}
for k, v in headers.items():
serialized_headers.setdefault(str(k), []).append(v)
return serialized_headers
def _deserialize_headers(headers):
deserialized_headers = CIMultiDict()
for k, vs in headers.items():
if isinstance(vs, list):
for v in vs:
deserialized_headers.add(k, v)
else:
deserialized_headers.add(k, vs)
return CIMultiDictProxy(deserialized_headers)
def play_responses(cassette, vcr_request):
history = []
vcr_response = cassette.play_response(vcr_request)
response = build_response(vcr_request, vcr_response, history)
# If we're following redirects, continue playing until we reach
# our final destination.
while 300 <= response.status <= 399:
if "location" not in response.headers:
break
next_url = URL(response.url).join(URL(response.headers["location"]))
# Make a stub VCR request that we can then use to look up the recorded
# VCR request saved to the cassette. This feels a little hacky and
# may have edge cases based on the headers we're providing (e.g. if
# there's a matcher that is used to filter by headers).
vcr_request = Request("GET", str(next_url), None, _serialize_headers(response.request_info.headers))
vcr_requests = cassette.find_requests_with_most_matches(vcr_request)
for vcr_request, *_ in vcr_requests:
if cassette.can_play_response_for(vcr_request):
break
# Tack on the response we saw from the redirect into the history
# list that is added on to the final response.
history.append(response)
vcr_response = cassette.play_response(vcr_request)
response = build_response(vcr_request, vcr_response, history)
return response
async def record_response(cassette, vcr_request, response):
"""Record a VCR request-response chain to the cassette."""
try:
body = {"string": (await response.read())}
# aiohttp raises a ClientConnectionError on reads when
# there is no body. We can use this to know to not write one.
except ClientConnectionError:
body = {}
vcr_response = {
"status": {"code": response.status, "message": response.reason},
"headers": _serialize_headers(response.headers),
"body": body, # NOQA: E999
"url": str(response.url),
}
cassette.append(vcr_request, vcr_response)
async def record_responses(cassette, vcr_request, response):
"""Because aiohttp follows redirects by default, we must support
them by default. This method is used to write individual
request-response chains that were implicitly followed to get
to the final destination.
"""
for past_response in response.history:
aiohttp_request = past_response.request_info
# No data because it's following a redirect.
past_request = Request(
aiohttp_request.method,
str(aiohttp_request.url),
None,
_serialize_headers(aiohttp_request.headers),
)
await record_response(cassette, past_request, past_response)
# If we're following redirects, then the last request-response
# we record is the one attached to the `response`.
if response.history:
aiohttp_request = response.request_info
vcr_request = Request(
aiohttp_request.method,
str(aiohttp_request.url),
None,
_serialize_headers(aiohttp_request.headers),
)
await record_response(cassette, vcr_request, response)
def _build_cookie_header(session, cookies, cookie_header, url):
url, _ = strip_auth_from_url(url)
all_cookies = session._cookie_jar.filter_cookies(url)
if cookies is not None:
tmp_cookie_jar = CookieJar()
tmp_cookie_jar.update_cookies(cookies)
req_cookies = tmp_cookie_jar.filter_cookies(url)
if req_cookies:
all_cookies.load(req_cookies)
if not all_cookies and not cookie_header:
return None
c = SimpleCookie()
if cookie_header:
c.load(cookie_header)
for name, value in all_cookies.items():
if isinstance(value, Morsel):
mrsl_val = value.get(value.key, Morsel())
mrsl_val.set(value.key, value.value, value.coded_value)
c[name] = mrsl_val
else:
c[name] = value
return c.output(header="", sep=";").strip()
def vcr_request(cassette, real_request):
@functools.wraps(real_request)
async def new_request(self, method, url, **kwargs):
headers = kwargs.get("headers")
auth = kwargs.get("auth")
headers = self._prepare_headers(headers)
data = kwargs.get("data", kwargs.get("json"))
params = kwargs.get("params")
cookies = kwargs.get("cookies")
if auth is not None:
headers["AUTHORIZATION"] = auth.encode()
request_url = URL(url)
if params:
for k, v in params.items():
params[k] = str(v)
request_url = URL(url).with_query(params)
c_header = headers.pop(hdrs.COOKIE, None)
cookie_header = _build_cookie_header(self, cookies, c_header, request_url)
if cookie_header:
headers[hdrs.COOKIE] = cookie_header
vcr_request = Request(method, str(request_url), data, _serialize_headers(headers))
if cassette.can_play_response_for(vcr_request):
log.info("Playing response for {} from cassette".format(vcr_request))
response = play_responses(cassette, vcr_request)
for redirect in response.history:
self._cookie_jar.update_cookies(redirect.cookies, redirect.url)
self._cookie_jar.update_cookies(response.cookies, response.url)
return response
if cassette.write_protected and cassette.filter_request(vcr_request):
raise CannotOverwriteExistingCassetteException(cassette=cassette, failed_request=vcr_request)
log.info("%s not in cassette, sending to real server", vcr_request)
response = await real_request(self, method, url, **kwargs) # NOQA: E999
await record_responses(cassette, vcr_request, response)
return response
return new_request

View File

@@ -1,99 +0,0 @@
'''Stubs for aiohttp HTTP clients'''
from __future__ import absolute_import
import asyncio
import functools
import json
from aiohttp import ClientResponse
from yarl import URL
from vcr.request import Request
class MockClientResponse(ClientResponse):
def __init__(self, method, url):
super().__init__(
method=method,
url=url,
writer=None,
continue100=None,
timer=None,
request_info=None,
auto_decompress=None,
traces=None,
loop=asyncio.get_event_loop(),
session=None,
)
# TODO: get encoding from header
@asyncio.coroutine
def json(self, *, encoding='utf-8', loads=json.loads): # NOQA: E999
return loads(self.content.decode(encoding))
@asyncio.coroutine
def text(self, encoding='utf-8'):
return self.content.decode(encoding)
@asyncio.coroutine
def read(self):
return self.content
@asyncio.coroutine
def release(self):
pass
def vcr_request(cassette, real_request):
@functools.wraps(real_request)
@asyncio.coroutine
def new_request(self, method, url, **kwargs):
headers = kwargs.get('headers')
headers = self._prepare_headers(headers)
data = kwargs.get('data')
params = kwargs.get('params')
if params:
for k, v in params.items():
params[k] = str(v)
request_url = URL(url).with_query(params)
vcr_request = Request(method, str(request_url), data, headers)
if cassette.can_play_response_for(vcr_request):
vcr_response = cassette.play_response(vcr_request)
response = MockClientResponse(method, URL(vcr_response.get('url')))
response.status = vcr_response['status']['code']
response.content = vcr_response['body']['string']
response.reason = vcr_response['status']['message']
response.headers = vcr_response['headers']
response.close()
return response
if cassette.write_protected and cassette.filter_request(vcr_request):
response = MockClientResponse(method, URL(url))
response.status = 599
msg = ("No match for the request {!r} was found. Can't overwrite "
"existing cassette {!r} in your current record mode {!r}.")
msg = msg.format(vcr_request, cassette._path, cassette.record_mode)
response.content = msg.encode()
response.close()
return response
response = yield from real_request(self, method, url, **kwargs) # NOQA: E999
vcr_response = {
'status': {
'code': response.status,
'message': response.reason,
},
'headers': dict(response.headers),
'body': {'string': (yield from response.read())}, # NOQA: E999
'url': response.url,
}
cassette.append(vcr_request, vcr_response)
return response
return new_request

View File

@@ -1,11 +1,20 @@
'''Stubs for boto3''' """Stubs for boto3"""
try:
# boto using awsrequest
from botocore.awsrequest import AWSHTTPConnection as HTTPConnection
from botocore.awsrequest import AWSHTTPSConnection as VerifiedHTTPSConnection
except ImportError: # pragma: nocover
# boto using vendored requests
# urllib3 defines its own HTTPConnection classes, which boto3 goes ahead and assumes
# you're using. It includes some polyfills for newer features missing in older pythons.
try:
from urllib3.connectionpool import HTTPConnection, VerifiedHTTPSConnection
except ImportError: # pragma: nocover
from requests.packages.urllib3.connectionpool import HTTPConnection, VerifiedHTTPSConnection
from botocore.vendored.requests.packages.urllib3.connectionpool import HTTPConnection, VerifiedHTTPSConnection
from ..stubs import VCRHTTPConnection, VCRHTTPSConnection from ..stubs import VCRHTTPConnection, VCRHTTPSConnection
# urllib3 defines its own HTTPConnection classes, which boto3 goes ahead and assumes
# you're using. It includes some polyfills for newer features missing in older pythons.
class VCRRequestsHTTPConnection(VCRHTTPConnection, HTTPConnection): class VCRRequestsHTTPConnection(VCRHTTPConnection, HTTPConnection):
_baseclass = HTTPConnection _baseclass = HTTPConnection
@@ -13,3 +22,20 @@ class VCRRequestsHTTPConnection(VCRHTTPConnection, HTTPConnection):
class VCRRequestsHTTPSConnection(VCRHTTPSConnection, VerifiedHTTPSConnection): class VCRRequestsHTTPSConnection(VCRHTTPSConnection, VerifiedHTTPSConnection):
_baseclass = VerifiedHTTPSConnection _baseclass = VerifiedHTTPSConnection
def __init__(self, *args, **kwargs):
kwargs.pop("strict", None)
# need to temporarily reset here because the real connection
# inherits from the thing that we are mocking out. Take out
# the reset if you want to see what I mean :)
from vcr.patch import force_reset
with force_reset():
self.real_connection = self._baseclass(*args, **kwargs)
# Make sure to set those attributes as it seems `AWSHTTPConnection` does not
# set them, making the connection to fail !
self.real_connection.assert_hostname = kwargs.get("assert_hostname", False)
self.real_connection.cert_reqs = kwargs.get("cert_reqs", "CERT_NONE")
self._sock = None

View File

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

View File

@@ -1,10 +1,5 @@
import six from io import BytesIO
from six import BytesIO import http.client
from six.moves.http_client import HTTPMessage
try:
import http.client
except ImportError:
pass
""" """
@@ -14,10 +9,7 @@ layer that tries to cope with this move.
def get_header(message, name): def get_header(message, name):
if six.PY3: return message.getallmatchingheaders(name)
return message.getallmatchingheaders(name)
else:
return message.getheader(name)
def get_header_items(message): def get_header_items(message):
@@ -28,16 +20,8 @@ def get_header_items(message):
def get_headers(message): def get_headers(message):
for key in set(message.keys()): for key in set(message.keys()):
if six.PY3: yield key, message.get_all(key)
yield key, message.get_all(key)
else:
yield key, message.getheaders(key)
def get_httpmessage(headers): def get_httpmessage(headers):
if six.PY3: return http.client.parse_headers(BytesIO(headers))
return http.client.parse_headers(BytesIO(headers))
msg = HTTPMessage(BytesIO(headers))
msg.fp.seek(0)
msg.readheaders()
return msg

View File

@@ -1,61 +1,60 @@
'''Stubs for httplib2''' """Stubs for httplib2"""
from httplib2 import HTTPConnectionWithTimeout, HTTPSConnectionWithTimeout from httplib2 import HTTPConnectionWithTimeout, HTTPSConnectionWithTimeout
from ..stubs import VCRHTTPConnection, VCRHTTPSConnection from ..stubs import VCRHTTPConnection, VCRHTTPSConnection
class VCRHTTPConnectionWithTimeout(VCRHTTPConnection, class VCRHTTPConnectionWithTimeout(VCRHTTPConnection, HTTPConnectionWithTimeout):
HTTPConnectionWithTimeout):
_baseclass = HTTPConnectionWithTimeout _baseclass = HTTPConnectionWithTimeout
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
'''I overrode the init because I need to clean kwargs before calling """I overrode the init because I need to clean kwargs before calling
HTTPConnection.__init__.''' HTTPConnection.__init__."""
# Delete the keyword arguments that HTTPConnection would not recognize # Delete the keyword arguments that HTTPConnection would not recognize
safe_keys = {'host', 'port', 'strict', 'timeout', 'source_address'} safe_keys = {"host", "port", "strict", "timeout", "source_address"}
unknown_keys = set(kwargs.keys()) - safe_keys unknown_keys = set(kwargs.keys()) - safe_keys
safe_kwargs = kwargs.copy() safe_kwargs = kwargs.copy()
for kw in unknown_keys: for kw in unknown_keys:
del safe_kwargs[kw] del safe_kwargs[kw]
self.proxy_info = kwargs.pop('proxy_info', None) self.proxy_info = kwargs.pop("proxy_info", None)
VCRHTTPConnection.__init__(self, *args, **safe_kwargs) VCRHTTPConnection.__init__(self, *args, **safe_kwargs)
self.sock = self.real_connection.sock self.sock = self.real_connection.sock
class VCRHTTPSConnectionWithTimeout(VCRHTTPSConnection, class VCRHTTPSConnectionWithTimeout(VCRHTTPSConnection, HTTPSConnectionWithTimeout):
HTTPSConnectionWithTimeout):
_baseclass = HTTPSConnectionWithTimeout _baseclass = HTTPSConnectionWithTimeout
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
# Delete the keyword arguments that HTTPSConnection would not recognize # Delete the keyword arguments that HTTPSConnection would not recognize
safe_keys = { safe_keys = {
'host', "host",
'port', "port",
'key_file', "key_file",
'cert_file', "cert_file",
'strict', "strict",
'timeout', "timeout",
'source_address', "source_address",
'ca_certs', "ca_certs",
"disable_ssl_certificate_validation",
} }
unknown_keys = set(kwargs.keys()) - safe_keys unknown_keys = set(kwargs.keys()) - safe_keys
safe_kwargs = kwargs.copy() safe_kwargs = kwargs.copy()
for kw in unknown_keys: for kw in unknown_keys:
del safe_kwargs[kw] del safe_kwargs[kw]
self.proxy_info = kwargs.pop('proxy_info', None) self.proxy_info = kwargs.pop("proxy_info", None)
if 'ca_certs' not in kwargs or kwargs['ca_certs'] is None: if "ca_certs" not in kwargs or kwargs["ca_certs"] is None:
try: try:
import httplib2 import httplib2
self.ca_certs = httplib2.CA_CERTS self.ca_certs = httplib2.CA_CERTS
except ImportError: except ImportError:
self.ca_certs = None self.ca_certs = None
else: else:
self.ca_certs = kwargs['ca_certs'] self.ca_certs = kwargs["ca_certs"]
self.disable_ssl_certificate_validation = kwargs.pop( self.disable_ssl_certificate_validation = kwargs.pop("disable_ssl_certificate_validation", None)
'disable_ssl_certificate_validation', None)
VCRHTTPSConnection.__init__(self, *args, **safe_kwargs) VCRHTTPSConnection.__init__(self, *args, **safe_kwargs)
self.sock = self.real_connection.sock self.sock = self.real_connection.sock

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

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

View File

@@ -1,4 +1,4 @@
'''Stubs for requests''' """Stubs for requests"""
try: try:
from urllib3.connectionpool import HTTPConnection, VerifiedHTTPSConnection from urllib3.connectionpool import HTTPConnection, VerifiedHTTPSConnection

View File

@@ -1,8 +1,6 @@
'''Stubs for tornado HTTP clients''' """Stubs for tornado HTTP clients"""
from __future__ import absolute_import
import functools import functools
from six import BytesIO from io import BytesIO
from tornado import httputil from tornado import httputil
from tornado.httpclient import HTTPResponse from tornado.httpclient import HTTPResponse
@@ -12,20 +10,19 @@ from vcr.request import Request
def vcr_fetch_impl(cassette, real_fetch_impl): def vcr_fetch_impl(cassette, real_fetch_impl):
@functools.wraps(real_fetch_impl) @functools.wraps(real_fetch_impl)
def new_fetch_impl(self, request, callback): def new_fetch_impl(self, request, callback):
headers = request.headers.copy() headers = request.headers.copy()
if request.user_agent: if request.user_agent:
headers.setdefault('User-Agent', request.user_agent) headers.setdefault("User-Agent", request.user_agent)
# TODO body_producer, header_callback, and streaming_callback are not # TODO body_producer, header_callback, and streaming_callback are not
# yet supported. # yet supported.
unsupported_call = ( unsupported_call = (
getattr(request, 'body_producer', None) is not None or getattr(request, "body_producer", None) is not None
request.header_callback is not None or or request.header_callback is not None
request.streaming_callback is not None or request.streaming_callback is not None
) )
if unsupported_call: if unsupported_call:
response = HTTPResponse( response = HTTPResponse(
@@ -40,18 +37,13 @@ def vcr_fetch_impl(cassette, real_fetch_impl):
) )
return callback(response) return callback(response)
vcr_request = Request( vcr_request = Request(request.method, request.url, request.body, headers)
request.method,
request.url,
request.body,
headers,
)
if cassette.can_play_response_for(vcr_request): if cassette.can_play_response_for(vcr_request):
vcr_response = cassette.play_response(vcr_request) vcr_response = cassette.play_response(vcr_request)
headers = httputil.HTTPHeaders() headers = httputil.HTTPHeaders()
recorded_headers = vcr_response['headers'] recorded_headers = vcr_response["headers"]
if isinstance(recorded_headers, dict): if isinstance(recorded_headers, dict):
recorded_headers = recorded_headers.items() recorded_headers = recorded_headers.items()
for k, vs in recorded_headers: for k, vs in recorded_headers:
@@ -59,45 +51,34 @@ def vcr_fetch_impl(cassette, real_fetch_impl):
headers.add(k, v) headers.add(k, v)
response = HTTPResponse( response = HTTPResponse(
request, request,
code=vcr_response['status']['code'], code=vcr_response["status"]["code"],
reason=vcr_response['status']['message'], reason=vcr_response["status"]["message"],
headers=headers, headers=headers,
buffer=BytesIO(vcr_response['body']['string']), buffer=BytesIO(vcr_response["body"]["string"]),
effective_url=vcr_response.get('url'), effective_url=vcr_response.get("url"),
request_time=self.io_loop.time() - request.start_time, request_time=self.io_loop.time() - request.start_time,
) )
return callback(response) return callback(response)
else: else:
if cassette.write_protected and cassette.filter_request( if cassette.write_protected and cassette.filter_request(vcr_request):
vcr_request
):
response = HTTPResponse( response = HTTPResponse(
request, request,
599, 599,
error=CannotOverwriteExistingCassetteException( error=CannotOverwriteExistingCassetteException(
"No match for the request (%r) was found. " cassette=cassette, failed_request=vcr_request
"Can't overwrite existing cassette (%r) in "
"your current record mode (%r)."
% (vcr_request, cassette._path, cassette.record_mode)
), ),
request_time=self.io_loop.time() - request.start_time, request_time=self.io_loop.time() - request.start_time,
) )
return callback(response) return callback(response)
def new_callback(response): def new_callback(response):
headers = [ headers = [(k, response.headers.get_list(k)) for k in response.headers.keys()]
(k, response.headers.get_list(k))
for k in response.headers.keys()
]
vcr_response = { vcr_response = {
'status': { "status": {"code": response.code, "message": response.reason},
'code': response.code, "headers": headers,
'message': response.reason, "body": {"string": response.body},
}, "url": response.effective_url,
'headers': headers,
'body': {'string': response.body},
'url': response.effective_url,
} }
cassette.append(vcr_request, vcr_response) cassette.append(vcr_request, vcr_response)
return callback(response) return callback(response)

View File

@@ -1,4 +1,4 @@
'''Stubs for urllib3''' """Stubs for urllib3"""
from urllib3.connectionpool import HTTPConnection, VerifiedHTTPSConnection from urllib3.connectionpool import HTTPConnection, VerifiedHTTPSConnection
from ..stubs import VCRHTTPConnection, VCRHTTPSConnection from ..stubs import VCRHTTPConnection, VCRHTTPSConnection

View File

@@ -1,13 +1,17 @@
import collections
import types import types
try:
from collections.abc import Mapping, MutableMapping
except ImportError:
from collections import Mapping, MutableMapping
# Shamelessly stolen from https://github.com/kennethreitz/requests/blob/master/requests/structures.py # Shamelessly stolen from https://github.com/kennethreitz/requests/blob/master/requests/structures.py
class CaseInsensitiveDict(collections.MutableMapping): class CaseInsensitiveDict(MutableMapping):
""" """
A case-insensitive ``dict``-like object. A case-insensitive ``dict``-like object.
Implements all methods and operations of Implements all methods and operations of
``collections.MutableMapping`` as well as dict's ``copy``. Also ``collections.abc.MutableMapping`` as well as dict's ``copy``. Also
provides ``lower_items``. provides ``lower_items``.
All keys are expected to be strings. The structure remembers the All keys are expected to be strings. The structure remembers the
case of the last key to be set, and ``iter(instance)``, case of the last key to be set, and ``iter(instance)``,
@@ -25,6 +29,7 @@ class CaseInsensitiveDict(collections.MutableMapping):
operations are given keys that have equal ``.lower()``s, the operations are given keys that have equal ``.lower()``s, the
behavior is undefined. behavior is undefined.
""" """
def __init__(self, data=None, **kwargs): def __init__(self, data=None, **kwargs):
self._store = dict() self._store = dict()
if data is None: if data is None:
@@ -50,14 +55,10 @@ class CaseInsensitiveDict(collections.MutableMapping):
def lower_items(self): def lower_items(self):
"""Like iteritems(), but with all lowercase keys.""" """Like iteritems(), but with all lowercase keys."""
return ( return ((lowerkey, keyval[1]) for (lowerkey, keyval) in self._store.items())
(lowerkey, keyval[1])
for (lowerkey, keyval)
in self._store.items()
)
def __eq__(self, other): def __eq__(self, other):
if isinstance(other, collections.Mapping): if isinstance(other, Mapping):
other = CaseInsensitiveDict(other) other = CaseInsensitiveDict(other)
else: else:
return NotImplemented return NotImplemented
@@ -88,37 +89,30 @@ def compose(*functions):
if function: if function:
res = function(res) res = function(res)
return res return res
return composed return composed
def read_body(request): def read_body(request):
if hasattr(request.body, 'read'): if hasattr(request.body, "read"):
return request.body.read() return request.body.read()
return request.body return request.body
def auto_decorate( def auto_decorate(decorator, predicate=lambda name, value: isinstance(value, types.FunctionType)):
decorator,
predicate=lambda name, value: isinstance(value, types.FunctionType)
):
def maybe_decorate(attribute, value): def maybe_decorate(attribute, value):
if predicate(attribute, value): if predicate(attribute, value):
value = decorator(value) value = decorator(value)
return value return value
class DecorateAll(type): class DecorateAll(type):
def __setattr__(cls, attribute, value): def __setattr__(cls, attribute, value):
return super(DecorateAll, cls).__setattr__( return super().__setattr__(attribute, maybe_decorate(attribute, value))
attribute, maybe_decorate(attribute, value)
)
def __new__(cls, name, bases, attributes_dict): def __new__(cls, name, bases, attributes_dict):
new_attributes_dict = dict( new_attributes_dict = {
(attribute, maybe_decorate(attribute, value)) attribute: maybe_decorate(attribute, value) for attribute, value in attributes_dict.items()
for attribute, value in attributes_dict.items() }
) return super().__new__(cls, name, bases, new_attributes_dict)
return super(DecorateAll, cls).__new__(
cls, name, bases, new_attributes_dict
)
return DecorateAll return DecorateAll