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

Compare commits

...

84 Commits

Author SHA1 Message Date
Ivan Malison
f821fed418 random whitespace 2016-01-11 15:54:13 -08:00
Ivan Malison
50246791e3 allow filtering by response 2016-01-11 15:53:04 -08:00
Ivan Malison
8a5bf23d34 better logging on cassettenotfound 2015-10-26 00:36:44 -07:00
Ivan Malison
f6b8e4f8e7 Lint cleanup 2015-10-25 20:30:33 -07:00
Ivan Malison
2ac3fa9abe v1.7.4 2015-10-18 16:48:27 -07:00
Ivan 'Goat' Malison
1324feae99 Merge pull request #217 from bcen/fix-decoration-no-return
Fix use_cassette when used as decorator does not return the wrapped function's return value
2015-10-02 00:11:20 -07:00
Bocai Cen
7990c549d1 fix decoration when in _handle_function does not return the wrapped function return value 2015-10-01 23:17:15 -04:00
Ivan 'Goat' Malison
327797c4ff Merge pull request #206 from tyewang/propogate_attribute_changes_to_real_connection_on_stubs
Attributes set on VCRConnection now also get set on the real_connection
2015-09-20 00:56:54 -07:00
Tye Wang
ac510097e0 Add TODO and elaborate on comment 2015-09-18 12:19:17 -04:00
Ivan Malison
00d973a0f5 sphinx skeleton 2015-09-13 11:27:08 -07:00
Tye Wang
79ff59feae Attributes set on VCRConnection now also get set on the real_connection 2015-09-03 14:35:28 -04:00
Ivan 'Goat' Malison
34252bc234 Merge pull request #192 from agriffis/insensitive-headers
Make request.headers always a CaseInsensitiveDict.
2015-08-28 14:47:45 -07:00
Ivan Malison
5f78657c52 test_boto cleanup 2015-08-28 13:03:09 -07:00
Aron Griffis
00b4e451fe Full image URL in README.rst
...so it shows properly on PyPI
2015-08-28 14:46:08 -04:00
Aron Griffis
44564ba39f Merge pull request #198 from gwillem/patch-1
Add instructions on using libyaml
2015-08-28 13:01:14 -04:00
Aron Griffis
7f02a7e999 Fix a bad merge in README.rst
This change seems to have gotten lost in #200
2015-08-28 12:57:05 -04:00
Ivan Malison
c28adea66d Fix typo in release notes 2015-08-28 09:20:54 -07:00
Aron Griffis
3f006cc261 Merge pull request #201 from agriffis/path-transformer-default
Default path_transformer=None. Fixes #199
2015-08-28 07:55:13 -04:00
Aron Griffis
0eda8ba482 Default path_transformer=None. Fixes #199 2015-08-28 07:25:03 -04:00
Ivan 'Goat' Malison
d620095c36 Merge pull request #200 from kevin1024/test_class_meta
Test class meta
2015-08-28 03:19:54 -07:00
Ivan Malison
c8180326ad Automatically decorate dynamically added methods with auto_decorate 2015-08-28 02:17:01 -07:00
Willem de Groot
d55d593d1c Changed bash to sh and fix syntax
Online Github editting is not very convenient after all :)
2015-08-28 11:14:01 +02:00
Willem de Groot
04f4a7fd2f Add bash code 2015-08-28 11:11:03 +02:00
Ivan Malison
6fd04f3675 Add test_case method to VCR
this method provides a class that can be inherited from to decorate all
test methods on the desired class with use_cassette using the relevant vcr
2015-08-28 01:44:39 -07:00
Ivan Malison
420f83b6b1 Move badges to top of README. 2015-08-28 01:44:39 -07:00
Aron Griffis
c6adcc83b3 Spello on use_cassette, thanks @Diaoul 2015-08-27 13:39:01 -04:00
Willem de Groot
dc61f5f520 Typoe 2015-08-27 11:20:11 +02:00
Willem de Groot
4450cb992f Add instructions on using libyaml
For a 10x speed increase!
2015-08-27 11:19:31 +02:00
Aron Griffis
083b1ec686 Move gitter chat with other badges 2015-08-26 17:34:45 -04:00
Ivan 'Goat' Malison
97c924d8dd Merge pull request #197 from gitter-badger/gitter-badge
Add a Gitter chat badge to README.rst
2015-08-26 11:49:26 -07:00
The Gitter Badger
65398131a4 Added Gitter badge 2015-08-26 17:29:20 +00:00
Aron Griffis
7312229aef Add HeadersDict, and mark add_header deprecated.
HeadersDict is a subclass of CaseInsensitiveDict with two new features:

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

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

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

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

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

Fixes #183.
2015-08-12 10:51:08 -07:00
Ivan Malison
8b59d73f25 v1.7.0 2015-08-02 13:36:57 -07:00
Ivan 'Goat' Malison
eb394b90d9 Merge pull request #181 from coagulant/fix-readme-custom-response
Fix example for custom response filtering in docs
2015-08-01 06:06:34 -07:00
Ilya Baryshev
14931dd47a Fix example for custom response filtering in docs 2015-08-01 11:10:51 +03:00
Ivan Malison
89cdda86d1 fix generator test. 2015-07-30 14:22:16 -07:00
Ivan 'Goat' Malison
ad48d71897 Merge pull request #180 from abhinav/master
Fix exception catching in coroutines.
2015-07-30 14:21:44 -07:00
Abhinav Gupta
946ce17a97 Fix exception catching in coroutines. 2015-07-30 14:13:58 -07:00
Ivan Malison
4d438dac75 Fix tornado python3 tests. 2015-07-30 04:19:17 -07:00
Ivan Malison
a234ad6b12 Fix all the tests in python 3 2015-07-30 03:39:04 -07:00
Ivan Malison
1d000ac652 Fix all the failing tests 2015-07-30 02:08:42 -07:00
Ivan Malison
21c176ee1e Make cassette active for duration of coroutine/generator
Closes #177.
2015-07-30 01:47:29 -07:00
Ivan 'Goat' Malison
4fb5bef8e1 Merge pull request #179 from graingert/support-ancient-PyPA-tools
Support distribute, Fixes #178
2015-07-29 23:22:16 -07:00
Thomas Grainger
9717596e2c Support distribute, Fixes #178 2015-07-29 22:19:45 +01:00
Ivan 'Goat' Malison
1660cc3a9f Merge pull request #176 from charlax/patch-1
Make setup example pep8 compliant
2015-07-27 10:37:42 -07:00
Charles-Axel Dein
4beb023204 Make setup example pep8 compliant
Pretty minor doc change.
2015-07-27 18:22:51 +02:00
Ivan 'Goat' Malison
72eb5345d6 Merge pull request #175 from gward/issue163-v2
Fix for #163, take 2.
2015-07-26 02:40:27 -07:00
Greg Ward
fe7d193d1a Add several more test cases for issue #163. 2015-07-16 14:49:48 -04:00
Greg Ward
09b7ccf561 Ensure that request bodies are always bytes, not text (fixes #163).
It shouldn't matter whether the request body comes from a file or a
string, or whether it is passed to the Request constructor or assigned
later. It should always be stored internally as bytes.
2015-07-16 14:36:26 -04:00
Kevin McCarthy
a4a80b431b Merge pull request #173 from graingert/patch-2
Fix before_record_reponse doc
2015-07-16 07:28:25 -10:00
Thomas Grainger
025a3b422d Fix before_record_reponse doc 2015-07-16 15:13:19 +01:00
Kevin McCarthy
bb05b2fcf7 Merge pull request #172 from abhinav/patch-1
Add Tornado to list of supported libraries
2015-07-15 10:31:51 -10:00
Abhinav Gupta
f77ef81877 Add Tornado to list of supported libraries 2015-07-15 12:43:36 -07:00
32 changed files with 1475 additions and 245 deletions

3
.gitignore vendored
View File

@@ -7,4 +7,5 @@ dist/
*.egg-info/
pytestdebug.log
fixtures/
fixtures/
/docs/_build

View File

@@ -1,4 +1,5 @@
language: python
sudo: false
before_install: openssl version
env:
global:
@@ -9,6 +10,7 @@ env:
- WITH_LIB="requests2.3"
- WITH_LIB="requests2.4"
- WITH_LIB="requests2.5"
- WITH_LIB="requests2.6"
- WITH_LIB="requests2.7"
- WITH_LIB="requests1.x"
- WITH_LIB="httplib2"
@@ -40,6 +42,7 @@ install:
- if [ $WITH_LIB = "requests2.3" ] ; then pip install requests==2.3.0; fi
- if [ $WITH_LIB = "requests2.4" ] ; then pip install requests==2.4.0; fi
- if [ $WITH_LIB = "requests2.5" ] ; then pip install requests==2.5.0; fi
- if [ $WITH_LIB = "requests2.6" ] ; then pip install requests==2.6.0; fi
- if [ $WITH_LIB = "requests2.7" ] ; then pip install requests==2.7.0; fi
- if [ $WITH_LIB = "httplib2" ] ; then pip install httplib2; fi
- if [ $WITH_LIB = "boto" ] ; then pip install boto; fi

View File

@@ -1,30 +1,29 @@
|Build Status| |Stories in Ready| |Gitter|
VCR.py
======
.. figure:: https://raw.github.com/kevin1024/vcrpy/master/vcr.png
.. image:: https://raw.github.com/kevin1024/vcrpy/master/vcr.png
:alt: vcr.py
vcr.py
This is a Python version of `Ruby's VCR
library <https://github.com/vcr/vcr>`__.
|Build Status| |Stories in Ready|
What it does
------------
VCR.py simplifies and speeds up tests that make HTTP requests. The first
time you run code that is inside a VCR.py context manager or decorated
function, VCR.py records all HTTP interactions that take place through
the libraries it supports and serializes and writes them to a flat file
(in yaml format by default). This flat file is called a cassette. When
the relevant peice of code is executed again, VCR.py will read the
serialized requests and responses from the aforementioned cassette file,
and intercept any HTTP requests that it recognizes from the original
test run and return responses that corresponded to those requests. This
means that the requests will not actually result in HTTP traffic, which
confers several benefits including:
VCR.py simplifies and speeds up tests that make HTTP requests. The
first time you run code that is inside a VCR.py context manager or
decorated function, VCR.py records all HTTP interactions that take
place through the libraries it supports and serializes and writes them
to a flat file (in yaml format by default). This flat file is called a
cassette. When the relevant peice of code is executed again, VCR.py
will read the serialized requests and responses from the
aforementioned cassette file, and intercept any HTTP requests that it
recognizes from the original test run and return the responses that
corresponded to those requests. This means that the requests will not
actually result in HTTP traffic, which confers several benefits
including:
- The ability to work offline
- Completely deterministic tests
@@ -50,6 +49,7 @@ The following http libraries are supported:
- requests (both 1.x and 2.x versions)
- httplib2
- boto
- Tornado's AsyncHTTPClient
Usage
-----
@@ -108,10 +108,10 @@ If you don't like VCR's defaults, you can set options by instantiating a
import vcr
my_vcr = vcr.VCR(
serializer = 'json',
cassette_library_dir = 'fixtures/cassettes',
record_mode = 'once',
match_on = ['uri', 'method'],
serializer='json',
cassette_library_dir='fixtures/cassettes',
record_mode='once',
match_on=['uri', 'method'],
)
with my_vcr.use_cassette('test.json'):
@@ -416,12 +416,13 @@ that of ``before_record``:
.. code:: python
def scrub_string(string, replacement=''):
def before_record_reponse(response):
return response['body']['string'] = response['body']['string'].replace(string, replacement)
return scrub_string
def before_record_response(response):
response['body']['string'] = response['body']['string'].replace(string, replacement)
return response
return before_record_response
my_vcr = vcr.VCR(
before_record=scrub_string(settings.USERNAME, 'username'),
before_record_response=scrub_string(settings.USERNAME, 'username'),
)
with my_vcr.use_cassette('test.yml'):
# your http code here
@@ -572,6 +573,24 @@ If you set the loglevel to DEBUG, you will also get information about
which matchers didn't match. This can help you with debugging custom
matchers.
Speed
-----
VCR.py runs about 10x faster when pyyaml can use the libyaml extensions. However, just installing ``libyaml`` (Mac) or ``libyaml-dev`` (Linux) is not enough, as pyyaml needs to be rebuild with the proper flag. Note that this flag is cached by pip, so clear the cache first.
Are you using libyaml already? This should work:
.. code:: sh
python -c 'from yaml import CLoader'
If not:
.. code:: sh
pip uninstall pyyaml
pip --no-cache-dir install pyyaml
Upgrade
-------
@@ -606,6 +625,20 @@ new API in version 1.0.x
Changelog
---------
- 1.7.4 [#217] Make use_cassette decorated functions actually return a
value (thanks @bcen). [#199] Fix path transfromation defaults.
Better headers dictionary management.
- 1.7.3 [#188] ``additional_matchers`` kwarg on ``use_cassette``.
[#191] Actually support passing multiple before_record_request
functions (thanks @agriffis).
- 1.7.2 [#186] Get effective_url in tornado (thanks @mvschaik), [#187]
Set request_time on Response object in tornado (thanks @abhinav).
- 1.7.1 [#183] Patch ``fetch_impl`` instead of the entire HTTPClient
class for Tornado (thanks @abhinav).
- 1.7.0 [#177] Properly support coroutine/generator decoration. [#178]
Support distribute (thanks @graingert). [#163] Make compatibility
between python2 and python3 recorded cassettes more robust (thanks
@gward).
- 1.6.1 [#169] Support conditional requirements in old versions of
pip, Fix RST parse errors generated by pandoc, [Tornado] Fix
unsupported features exception not being raised, [#166]
@@ -744,3 +777,6 @@ more details
:target: http://travis-ci.org/kevin1024/vcrpy
.. |Stories in Ready| image:: https://badge.waffle.io/kevin1024/vcrpy.png?label=ready&title=Ready
:target: https://waffle.io/kevin1024/vcrpy
.. |Gitter| image:: https://badges.gitter.im/Join%20Chat.svg
:alt: Join the chat at https://gitter.im/kevin1024/vcrpy
:target: https://gitter.im/kevin1024/vcrpy?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge

192
docs/Makefile Normal file
View File

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

293
docs/conf.py Normal file
View File

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

19
docs/index.rst Normal file
View File

@@ -0,0 +1,19 @@
vcrpy
=====
Contents:
.. toctree::
:maxdepth: 2
vcr
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

6
docs/vcr.rst Normal file
View File

@@ -0,0 +1,6 @@
:mod:`~vcr.config`
=================
.. automodule:: vcr.config
:members:
:special-members: __init__

View File

@@ -1,12 +1,15 @@
#!/usr/bin/env python
import sys
import logging
from setuptools import setup, find_packages
from setuptools.command.test import test as TestCommand
import pkg_resources
long_description = open('README.rst', 'r').read()
class PyTest(TestCommand):
def finalize_options(self):
@@ -21,7 +24,7 @@ class PyTest(TestCommand):
sys.exit(errno)
install_requires=['PyYAML', 'wrapt', 'six>=1.5']
install_requires = ['PyYAML', 'wrapt', 'six>=1.5']
extras_require = {
@@ -31,14 +34,24 @@ extras_require = {
}
if 'bdist_wheel' not in sys.argv:
try:
if 'bdist_wheel' not in sys.argv:
for key, value in extras_require.items():
if key.startswith(':') and pkg_resources.evaluate_marker(key[1:]):
install_requires.extend(value)
except Exception:
logging.getLogger(__name__).exception(
'Something went wrong calculating platform specific dependencies, so '
"you're getting them all!"
)
for key, value in extras_require.items():
if key.startswith(':') and pkg_resources.evaluate_marker(key[1:]):
if key.startswith(':'):
install_requires.extend(value)
setup(
name='vcrpy',
version='1.6.1',
version='1.7.4',
description=(
"Automatically mock your HTTP interactions to simplify and "
"speed up testing"

View File

@@ -1,5 +1,6 @@
import pytest
boto = pytest.importorskip("boto")
import boto
import boto.iam
from boto.s3.connection import S3Connection
@@ -7,6 +8,7 @@ from boto.s3.key import Key
from ConfigParser import DuplicateSectionError
import vcr
def test_boto_stubs(tmpdir):
with vcr.use_cassette(str(tmpdir.join('boto-stubs.yml'))):
# Perform the imports within the patched context so that
@@ -17,16 +19,18 @@ def test_boto_stubs(tmpdir):
assert issubclass(CertValidatingHTTPSConnection, VCRCertValidatingHTTPSConnection)
CertValidatingHTTPSConnection('hostname.does.not.matter')
def test_boto_without_vcr():
s3_conn = S3Connection()
s3_bucket = s3_conn.get_bucket('boto-demo-1394171994') # a bucket you can access
s3_bucket = s3_conn.get_bucket('boto-demo-1394171994') # a bucket you can access
k = Key(s3_bucket)
k.key = 'test.txt'
k.set_contents_from_string('hello world i am a string')
def test_boto_medium_difficulty(tmpdir):
s3_conn = S3Connection()
s3_bucket = s3_conn.get_bucket('boto-demo-1394171994') # a bucket you can access
s3_bucket = s3_conn.get_bucket('boto-demo-1394171994') # a bucket you can access
with vcr.use_cassette(str(tmpdir.join('boto-medium.yml'))) as cass:
k = Key(s3_bucket)
k.key = 'test.txt'
@@ -41,18 +45,19 @@ def test_boto_medium_difficulty(tmpdir):
def test_boto_hardcore_mode(tmpdir):
with vcr.use_cassette(str(tmpdir.join('boto-hardcore.yml'))) as cass:
s3_conn = S3Connection()
s3_bucket = s3_conn.get_bucket('boto-demo-1394171994') # a bucket you can access
s3_bucket = s3_conn.get_bucket('boto-demo-1394171994') # a bucket you can access
k = Key(s3_bucket)
k.key = 'test.txt'
k.set_contents_from_string('hello world i am a string')
with vcr.use_cassette(str(tmpdir.join('boto-hardcore.yml'))) as cass:
s3_conn = S3Connection()
s3_bucket = s3_conn.get_bucket('boto-demo-1394171994') # a bucket you can access
s3_bucket = s3_conn.get_bucket('boto-demo-1394171994') # a bucket you can access
k = Key(s3_bucket)
k.key = 'test.txt'
k.set_contents_from_string('hello world i am a string')
def test_boto_iam(tmpdir):
try:
boto.config.add_section('Boto')

View File

@@ -17,11 +17,7 @@ def _request_with_auth(url, username, password):
def _find_header(cassette, header):
for request in cassette.requests:
for k in request.headers:
if header.lower() == k.lower():
return True
return False
return any(header in request.headers for request in cassette.requests)
def test_filter_basic_auth(tmpdir):

View File

@@ -56,6 +56,17 @@ def test_response_headers(scheme, tmpdir):
resp, _ = httplib2.Http().request(url)
assert set(headers) == set(resp.items())
def test_effective_url(scheme, tmpdir):
'''Ensure that the effective_url is captured'''
url = scheme + '://httpbin.org/redirect-to?url=/html'
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))) as cass:
resp, _ = httplib2.Http().request(url)
effective_url = resp['content-location']
assert effective_url == scheme + '://httpbin.org/html'
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))) as cass:
resp, _ = httplib2.Http().request(url)
assert effective_url == resp['content-location']
def test_multiple_requests(scheme, tmpdir):
'''Ensure that we can cache multiple requests'''

View File

@@ -44,6 +44,15 @@ def test_body(tmpdir, scheme):
with vcr.use_cassette(str(tmpdir.join('body.yaml'))):
assert content == requests.get(url).content
def test_effective_url(scheme, tmpdir):
'''Ensure that the effective_url is captured'''
url = scheme + '://httpbin.org/redirect-to?url=/html'
with vcr.use_cassette(str(tmpdir.join('url.yaml'))):
effective_url = requests.get(url).url
assert effective_url == scheme + '://httpbin.org/html'
with vcr.use_cassette(str(tmpdir.join('url.yaml'))):
assert effective_url == requests.get(url).url
def test_auth(tmpdir, scheme):
'''Ensure that we can handle basic auth'''

View File

@@ -81,6 +81,17 @@ def test_body(get_client, tmpdir, scheme):
assert content == (yield get(get_client(), url)).body
assert 1 == cass.play_count
@pytest.mark.gen_test
def test_effective_url(get_client, scheme, tmpdir):
'''Ensure that the effective_url is captured'''
url = scheme + '://httpbin.org/redirect-to?url=/html'
with vcr.use_cassette(str(tmpdir.join('url.yaml'))):
effective_url = (yield get(get_client(), url)).effective_url
assert effective_url == scheme + '://httpbin.org/html'
with vcr.use_cassette(str(tmpdir.join('url.yaml'))) as cass:
assert effective_url == (yield get(get_client(), url)).effective_url
assert 1 == cass.play_count
@pytest.mark.gen_test
def test_auth(get_client, tmpdir, scheme):
@@ -275,3 +286,70 @@ def test_cannot_overwrite_cassette_raise_error_disabled(get_client, tmpdir):
)
assert isinstance(response.error, CannotOverwriteExistingCassetteException)
@pytest.mark.gen_test
@vcr.use_cassette(path_transformer=vcr.default_vcr.ensure_suffix('.yaml'))
def test_tornado_with_decorator_use_cassette(get_client):
response = yield get_client().fetch(
http.HTTPRequest('http://www.google.com/', method='GET')
)
assert response.body.decode('utf-8') == "not actually google"
@pytest.mark.gen_test
@vcr.use_cassette(path_transformer=vcr.default_vcr.ensure_suffix('.yaml'))
def test_tornado_exception_can_be_caught(get_client):
try:
yield get(get_client(), 'http://httpbin.org/status/500')
except http.HTTPError as e:
assert e.code == 500
try:
yield get(get_client(), 'http://httpbin.org/status/404')
except http.HTTPError as e:
assert e.code == 404
@pytest.mark.gen_test
def test_existing_references_get_patched(tmpdir):
from tornado.httpclient import AsyncHTTPClient
with vcr.use_cassette(str(tmpdir.join('data.yaml'))):
client = AsyncHTTPClient()
yield get(client, 'http://httpbin.org/get')
with vcr.use_cassette(str(tmpdir.join('data.yaml'))) as cass:
yield get(client, 'http://httpbin.org/get')
assert cass.play_count == 1
@pytest.mark.gen_test
def test_existing_instances_get_patched(get_client, tmpdir):
'''Ensure that existing instances of AsyncHTTPClient get patched upon
entering VCR context.'''
client = get_client()
with vcr.use_cassette(str(tmpdir.join('data.yaml'))):
yield get(client, 'http://httpbin.org/get')
with vcr.use_cassette(str(tmpdir.join('data.yaml'))) as cass:
yield get(client, 'http://httpbin.org/get')
assert cass.play_count == 1
@pytest.mark.gen_test
def test_request_time_is_set(get_client, tmpdir):
'''Ensures that the request_time on HTTPResponses is set.'''
with vcr.use_cassette(str(tmpdir.join('data.yaml'))):
client = get_client()
response = yield get(client, 'http://httpbin.org/get')
assert response.request_time is not None
with vcr.use_cassette(str(tmpdir.join('data.yaml'))) as cass:
client = get_client()
response = yield get(client, 'http://httpbin.org/get')
assert response.request_time is not None
assert cass.play_count == 1

View File

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

View File

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

View File

@@ -49,6 +49,15 @@ def test_response_headers(scheme, tmpdir):
open2 = urlopen(url).info().items()
assert sorted(open1) == sorted(open2)
def test_effective_url(scheme, tmpdir):
'''Ensure that the effective_url is captured'''
url = scheme + '://httpbin.org/redirect-to?url=/html'
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))) as cass:
effective_url = urlopen(url).geturl()
assert effective_url == scheme + '://httpbin.org/html'
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))) as cass:
assert effective_url == urlopen(url).geturl()
def test_multiple_requests(scheme, tmpdir):
'''Ensure that we can cache multiple requests'''

View File

@@ -10,6 +10,7 @@ from vcr.compat import mock, contextlib
from vcr.cassette import Cassette
from vcr.errors import UnhandledHTTPRequestError
from vcr.patch import force_reset
from vcr.matchers import path, method, query, host
from vcr.stubs import VCRHTTPSConnection
@@ -245,6 +246,13 @@ def test_path_transformer_with_context_manager():
assert cassette._path == 'a'
def test_path_transformer_None():
with Cassette.use(
path='a', path_transformer=None,
) as cassette:
assert cassette._path == 'a'
def test_func_path_generator():
def generator(function):
return os.path.join(os.path.dirname(inspect.getfile(function)),
@@ -253,3 +261,52 @@ def test_func_path_generator():
def function_name(cassette):
assert cassette._path == os.path.join(os.path.dirname(__file__), 'function_name')
function_name()
def test_use_as_decorator_on_coroutine():
original_http_connetion = httplib.HTTPConnection
@Cassette.use(inject=True)
def test_function(cassette):
assert httplib.HTTPConnection.cassette is cassette
assert httplib.HTTPConnection is not original_http_connetion
value = yield 1
assert value == 1
assert httplib.HTTPConnection.cassette is cassette
assert httplib.HTTPConnection is not original_http_connetion
value = yield 2
assert value == 2
coroutine = test_function()
value = next(coroutine)
while True:
try:
value = coroutine.send(value)
except StopIteration:
break
def test_use_as_decorator_on_generator():
original_http_connetion = httplib.HTTPConnection
@Cassette.use(inject=True)
def test_function(cassette):
assert httplib.HTTPConnection.cassette is cassette
assert httplib.HTTPConnection is not original_http_connetion
yield 1
assert httplib.HTTPConnection.cassette is cassette
assert httplib.HTTPConnection is not original_http_connetion
yield 2
assert list(test_function()) == [1, 2]
def test_similar_requests(tmpdir):
# WIP needs to be finished
@Cassette.use(inject=True, match_on=(path, query, host, method))
def test_function(cassette):
conn = httplib.HTTPConnection("www.python.org")
conn.request("GET", "/index.html?test=1")
conn = httplib.HTTPConnection("www.python.org")
conn.request("GET", "/index.html?test=0")
conn = httplib.HTTPConnection("www.cool.org")
conn.request("GET", "/index.html?test=0")
cassette.similar_requests()

View File

@@ -73,7 +73,7 @@ def test_remove_nonexistent_post_data_parameters():
def test_remove_json_post_data_parameters():
body = b'{"id": "secret", "foo": "bar", "baz": "qux"}'
request = Request('POST', 'http://google.com', body, {})
request.add_header('Content-Type', 'application/json')
request.headers['Content-Type'] = 'application/json'
remove_post_data_parameters(request, ['id'])
request_body_json = json.loads(request.body.decode('utf-8'))
expected_json = json.loads(b'{"foo": "bar", "baz": "qux"}'.decode('utf-8'))
@@ -83,7 +83,7 @@ def test_remove_json_post_data_parameters():
def test_remove_all_json_post_data_parameters():
body = b'{"id": "secret", "foo": "bar"}'
request = Request('POST', 'http://google.com', body, {})
request.add_header('Content-Type', 'application/json')
request.headers['Content-Type'] = 'application/json'
remove_post_data_parameters(request, ['id', 'foo'])
assert request.body == b'{}'
@@ -91,6 +91,6 @@ def test_remove_all_json_post_data_parameters():
def test_remove_nonexistent_json_post_data_parameters():
body = b'{}'
request = Request('POST', 'http://google.com', body, {})
request.add_header('Content-Type', 'application/json')
request.headers['Content-Type'] = 'application/json'
remove_post_data_parameters(request, ['id'])
assert request.body == b'{}'

View File

@@ -1,6 +1,6 @@
import pytest
from vcr.request import Request
from vcr.request import Request, HeadersDict
def test_str():
@@ -12,11 +12,16 @@ def test_headers():
headers = {'X-Header1': ['h1'], 'X-Header2': 'h2'}
req = Request('GET', 'http://go.com/', '', headers)
assert req.headers == {'X-Header1': 'h1', 'X-Header2': 'h2'}
req.add_header('X-Header1', 'h11')
req.headers['X-Header1'] = 'h11'
assert req.headers == {'X-Header1': 'h11', 'X-Header2': 'h2'}
def test_add_header_deprecated():
req = Request('GET', 'http://go.com/', '', {})
pytest.deprecated_call(req.add_header, 'foo', 'bar')
assert req.headers == {'foo': 'bar'}
@pytest.mark.parametrize("uri, expected_port", [
('http://go.com/', 80),
('http://go.com:80/', 80),
@@ -36,3 +41,30 @@ def test_uri():
req = Request('GET', 'http://go.com:80/', '', {})
assert req.uri == 'http://go.com:80/'
def test_HeadersDict():
# Simple test of CaseInsensitiveDict
h = HeadersDict()
assert h == {}
h['Content-Type'] = 'application/json'
assert h == {'Content-Type': 'application/json'}
assert h['content-type'] == 'application/json'
assert h['CONTENT-TYPE'] == 'application/json'
# Test feature of HeadersDict: devolve list to first element
h = HeadersDict()
assert h == {}
h['x'] = ['foo', 'bar']
assert h == {'x': 'foo'}
# Test feature of HeadersDict: preserve original key case
h = HeadersDict()
assert h == {}
h['Content-Type'] = 'application/json'
assert h == {'Content-Type': 'application/json'}
h['content-type'] = 'text/plain'
assert h == {'Content-Type': 'text/plain'}
h['CONtent-tyPE'] = 'whoa'
assert h == {'Content-Type': 'whoa'}

View File

@@ -1,3 +1,4 @@
# -*- encoding: utf-8 -*-
import pytest
from vcr.compat import mock
@@ -27,6 +28,55 @@ def test_deserialize_new_json_cassette():
deserialize(f.read(), jsonserializer)
REQBODY_TEMPLATE = u'''\
interactions:
- request:
body: {req_body}
headers:
Content-Type: [application/x-www-form-urlencoded]
Host: [httpbin.org]
method: POST
uri: http://httpbin.org/post
response:
body: {{string: ""}}
headers:
content-length: ['0']
content-type: [application/json]
status: {{code: 200, message: OK}}
'''
# A cassette generated under Python 2 stores the request body as a string,
# but the same cassette generated under Python 3 stores it as "!!binary".
# Make sure we accept both forms, regardless of whether we're running under
# Python 2 or 3.
@pytest.mark.parametrize("req_body, expect", [
# Cassette written under Python 2 (pure ASCII body)
('x=5&y=2', b'x=5&y=2'),
# Cassette written under Python 3 (pure ASCII body)
('!!binary |\n eD01Jnk9Mg==', b'x=5&y=2'),
# Request body has non-ASCII chars (x=föo&y=2), encoded in UTF-8.
('!!python/str "x=f\\xF6o&y=2"', b'x=f\xc3\xb6o&y=2'),
('!!binary |\n eD1mw7ZvJnk9Mg==', b'x=f\xc3\xb6o&y=2'),
# Same request body, this time encoded in UTF-16. In this case, we
# write the same YAML file under both Python 2 and 3, so there's only
# one test case here.
('!!binary |\n //54AD0AZgD2AG8AJgB5AD0AMgA=',
b'\xff\xfex\x00=\x00f\x00\xf6\x00o\x00&\x00y\x00=\x002\x00'),
# Same again, this time encoded in ISO-8859-1.
('!!binary |\n eD1m9m8meT0y', b'x=f\xf6o&y=2'),
])
def test_deserialize_py2py3_yaml_cassette(tmpdir, req_body, expect):
cfile = tmpdir.join('test_cassette.yaml')
cfile.write(REQBODY_TEMPLATE.format(req_body=req_body))
with open(str(cfile)) as f:
(requests, responses) = deserialize(f.read(), yamlserializer)
assert requests[0].body == expect
@mock.patch.object(jsonserializer.json, 'dumps',
side_effect=UnicodeDecodeError('utf-8', b'unicode error in serialization',
0, 10, 'blew up'))

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

@@ -0,0 +1,7 @@
from vcr.stubs import VCRHTTPSConnection
class TestVCRConnection(object):
def test_setting_of_attributes_get_propogated_to_real_connection(self):
vcr_connection = VCRHTTPSConnection('www.examplehost.com')
vcr_connection.ssl_version = 'example_ssl_version'
assert vcr_connection.real_connection.ssl_version == 'example_ssl_version'

View File

@@ -1,11 +1,13 @@
import os
import pytest
from six.moves import http_client as httplib
from vcr import VCR, use_cassette
from vcr.compat import mock
from vcr.request import Request
from vcr.stubs import VCRHTTPSConnection
from vcr.patch import _HTTPConnection, force_reset
def test_vcr_use_cassette():
@@ -15,9 +17,11 @@ def test_vcr_use_cassette():
'vcr.cassette.Cassette.load',
return_value=mock.MagicMock(inject=False)
) as mock_cassette_load:
@test_vcr.use_cassette('test')
def function():
pass
assert mock_cassette_load.call_count == 0
function()
assert mock_cassette_load.call_args[1]['record_mode'] is record_mode
@@ -38,9 +42,11 @@ def test_vcr_use_cassette():
def test_vcr_before_record_request_params():
base_path = 'http://httpbin.org/'
def before_record_cb(request):
if request.path != '/get':
return request
test_vcr = VCR(filter_headers=('cookie',), before_record_request=before_record_cb,
ignore_hosts=('www.test.com',), ignore_localhost=True,
filter_query_parameters=('foo',))
@@ -53,8 +59,12 @@ def test_vcr_before_record_request_params():
assert cassette.filter_request(
Request('GET', base_path + '?foo=bar', '',
{'cookie': 'test', 'other': 'fun'})).headers == {'other': 'fun'}
assert cassette.filter_request(Request('GET', base_path + '?foo=bar', '',
{'cookie': 'test', 'other': 'fun'})).headers == {'other': 'fun'}
assert cassette.filter_request(
Request(
'GET', base_path + '?foo=bar', '',
{'cookie': 'test', 'other': 'fun'}
)
).headers == {'other': 'fun'}
assert cassette.filter_request(Request('GET', 'http://www.test.com' + '?foo=bar', '',
{'cookie': 'test', 'other': 'fun'})) is None
@@ -64,6 +74,69 @@ def test_vcr_before_record_request_params():
assert cassette.filter_request(Request('GET', base_path + 'get', '', {})) is not None
def test_vcr_before_record_response_iterable():
# Regression test for #191
request = Request('GET', '/', '', {})
response = object() # just can't be None
# Prevent actually saving the cassette
with mock.patch('vcr.cassette.save_cassette'):
# Baseline: non-iterable before_record_response should work
mock_filter = mock.Mock()
vcr = VCR(before_record_response=mock_filter)
with vcr.use_cassette('test') as cassette:
assert mock_filter.call_count == 0
cassette.append(request, response)
assert mock_filter.call_count == 1
# Regression test: iterable before_record_response should work too
mock_filter = mock.Mock()
vcr = VCR(before_record_response=(mock_filter,))
with vcr.use_cassette('test') as cassette:
assert mock_filter.call_count == 0
cassette.append(request, response)
assert mock_filter.call_count == 1
def test_before_record_response_as_filter():
request = Request('GET', '/', '', {})
response = object() # just can't be None
# Prevent actually saving the cassette
with mock.patch('vcr.cassette.save_cassette'):
filter_all = mock.Mock(return_value=None)
vcr = VCR(before_record_response=filter_all)
with vcr.use_cassette('test') as cassette:
cassette.append(request, response)
assert cassette.data == []e
assert not cassette.dirty
def test_vcr_path_transformer():
# Regression test for #199
# Prevent actually saving the cassette
with mock.patch('vcr.cassette.save_cassette'):
# Baseline: path should be unchanged
vcr = VCR()
with vcr.use_cassette('test') as cassette:
assert cassette._path == 'test'
# Regression test: path_transformer=None should do the same.
vcr = VCR(path_transformer=None)
with vcr.use_cassette('test') as cassette:
assert cassette._path == 'test'
# and it should still work with cassette_library_dir
vcr = VCR(cassette_library_dir='/foo')
with vcr.use_cassette('test') as cassette:
assert cassette._path == '/foo/test'
@pytest.fixture
def random_fixture():
return 1
@@ -103,6 +176,7 @@ def test_custom_patchers():
def test_inject_cassette():
vcr = VCR(inject_cassette=True)
@vcr.use_cassette('test', record_mode='once')
def with_cassette_injected(cassette):
assert cassette.record_mode == 'once'
@@ -117,9 +191,11 @@ def test_inject_cassette():
def test_with_current_defaults():
vcr = VCR(inject_cassette=True, record_mode='once')
@vcr.use_cassette('test', with_current_defaults=False)
def changing_defaults(cassette, checks):
checks(cassette)
@vcr.use_cassette('test', with_current_defaults=True)
def current_defaults(cassette, checks):
checks(cassette)
@@ -141,27 +217,33 @@ def test_with_current_defaults():
def test_cassette_library_dir_with_decoration_and_no_explicit_path():
library_dir = '/libary_dir'
vcr = VCR(inject_cassette=True, cassette_library_dir=library_dir)
@vcr.use_cassette()
def function_name(cassette):
assert cassette._path == os.path.join(library_dir, 'function_name')
function_name()
def test_cassette_library_dir_with_decoration_and_explicit_path():
library_dir = '/libary_dir'
vcr = VCR(inject_cassette=True, cassette_library_dir=library_dir)
@vcr.use_cassette(path='custom_name')
def function_name(cassette):
assert cassette._path == os.path.join(library_dir, 'custom_name')
function_name()
def test_cassette_library_dir_with_decoration_and_super_explicit_path():
library_dir = '/libary_dir'
vcr = VCR(inject_cassette=True, cassette_library_dir=library_dir)
@vcr.use_cassette(path=os.path.join(library_dir, 'custom_name'))
def function_name(cassette):
assert cassette._path == os.path.join(library_dir, 'custom_name')
function_name()
@@ -169,31 +251,38 @@ def test_cassette_library_dir_with_path_transformer():
library_dir = '/libary_dir'
vcr = VCR(inject_cassette=True, cassette_library_dir=library_dir,
path_transformer=lambda path: path + '.json')
@vcr.use_cassette()
def function_name(cassette):
assert cassette._path == os.path.join(library_dir, 'function_name.json')
function_name()
def test_use_cassette_with_no_extra_invocation():
vcr = VCR(inject_cassette=True, cassette_library_dir='/')
@vcr.use_cassette
def function_name(cassette):
assert cassette._path == os.path.join('/', 'function_name')
function_name()
def test_path_transformer():
vcr = VCR(inject_cassette=True, cassette_library_dir='/',
path_transformer=lambda x: x + '_test')
@vcr.use_cassette
def function_name(cassette):
assert cassette._path == os.path.join('/', 'function_name_test')
function_name()
def test_cassette_name_generator_defaults_to_using_module_function_defined_in():
vcr = VCR(inject_cassette=True)
@vcr.use_cassette
def function_name(cassette):
assert cassette._path == os.path.join(os.path.dirname(__file__),
@@ -203,8 +292,62 @@ def test_cassette_name_generator_defaults_to_using_module_function_defined_in():
def test_ensure_suffix():
vcr = VCR(inject_cassette=True, path_transformer=VCR.ensure_suffix('.yaml'))
@vcr.use_cassette
def function_name(cassette):
assert cassette._path == os.path.join(os.path.dirname(__file__),
'function_name.yaml')
function_name()
def test_additional_matchers():
vcr = VCR(match_on=('uri',), inject_cassette=True)
@vcr.use_cassette
def function_defaults(cassette):
assert set(cassette._match_on) == set([vcr.matchers['uri']])
@vcr.use_cassette(additional_matchers=('body',))
def function_additional(cassette):
assert set(cassette._match_on) == set([vcr.matchers['uri'], vcr.matchers['body']])
function_defaults()
function_additional()
def test_decoration_should_respect_function_return_value():
vcr = VCR()
ret = 'a-return-value'
@vcr.use_cassette
def function_with_return():
return ret
assert ret == function_with_return()
class TestVCRClass(VCR().test_case()):
def no_decoration(self):
assert httplib.HTTPConnection == _HTTPConnection
self.test_dynamically_added()
assert httplib.HTTPConnection == _HTTPConnection
def test_one(self):
with force_reset():
self.no_decoration()
with force_reset():
self.test_two()
assert httplib.HTTPConnection != _HTTPConnection
def test_two(self):
assert httplib.HTTPConnection != _HTTPConnection
def test_dynamically_added(self):
assert httplib.HTTPConnection != _HTTPConnection
TestVCRClass.test_dynamically_added = test_dynamically_added
del test_dynamically_added

View File

@@ -1,11 +1,10 @@
"""The container for recorded requests and responses"""
import functools
import inspect
import logging
import operator
import sys
import wrapt
# Internal imports
from .compat import contextlib, collections
from .errors import UnhandledHTTPRequestError
from .matchers import requests_match, uri, method
@@ -22,10 +21,18 @@ class CassetteContextDecorator(object):
"""Context manager/decorator that handles installing the cassette and
removing cassettes.
This class defers the creation of a new cassette instance until the point at
which it is installed by context manager or decorator. The fact that a new
cassette is used with each application prevents the state of any cassette
from interfering with another.
This class defers the creation of a new cassette instance until
the point at which it is installed by context manager or
decorator. The fact that a new cassette is used with each
application prevents the state of any cassette from interfering
with another.
Instances of this class are NOT reentrant as context managers.
However, functions that are decorated by
``CassetteContextDecorator`` instances ARE reentrant. See the
implementation of ``__call__`` on this class for more details.
There is also a guard against attempts to reenter instances of
this class as a context manager in ``__exit__``.
"""
_non_cassette_arguments = ('path_transformer', 'func_path_generator')
@@ -43,21 +50,18 @@ class CassetteContextDecorator(object):
with contextlib.ExitStack() as exit_stack:
for patcher in CassettePatcherBuilder(cassette).build():
exit_stack.enter_context(patcher)
log.debug('Entered context for cassette at {0}.'.format(cassette._path))
log_format = '{action} context for cassette at {path}.'
log.debug(log_format.format(
action="Entering", path=cassette._path
))
yield cassette
log.debug('Exiting context for cassette at {0}.'.format(cassette._path))
log.debug(log_format.format(
action="Exiting", path=cassette._path
))
# TODO(@IvanMalison): Hmmm. it kind of feels like this should be
# somewhere else.
cassette._save()
@classmethod
def key_predicate(cls, key, value):
return key in cls._non_cassette_arguments
@classmethod
def _split_keys(cls, kwargs):
return partition_dict(cls.key_predicate, kwargs)
def __enter__(self):
# This assertion is here to prevent the dangerous behavior
# that would result from forgetting about a __finish before
@@ -68,8 +72,11 @@ class CassetteContextDecorator(object):
# with context_decorator:
# pass
assert self.__finish is None, "Cassette already open."
other_kwargs, cassette_kwargs = self._split_keys(self._args_getter())
if 'path_transformer' in other_kwargs:
other_kwargs, cassette_kwargs = partition_dict(
lambda key, _: key in self._non_cassette_arguments,
self._args_getter()
)
if other_kwargs.get('path_transformer'):
transformer = other_kwargs['path_transformer']
cassette_kwargs['path'] = transformer(cassette_kwargs['path'])
self.__finish = self._patch_generator(self.cls.load(**cassette_kwargs))
@@ -84,36 +91,88 @@ class CassetteContextDecorator(object):
# This awkward cloning thing is done to ensure that decorated
# functions are reentrant. This is required for thread
# safety and the correct operation of recursive functions.
args_getter = self._build_args_getter_for_decorator(
function, self._args_getter
args_getter = self._build_args_getter_for_decorator(function)
return type(self)(self.cls, args_getter)._execute_function(
function, args, kwargs
)
clone = type(self)(self.cls, args_getter)
with clone as cassette:
if cassette.inject:
return function(cassette, *args, **kwargs)
else:
return function(*args, **kwargs)
def _execute_function(self, function, args, kwargs):
if inspect.isgeneratorfunction(function):
handler = self._handle_coroutine
else:
handler = self._handle_function
return handler(function, args, kwargs)
def _handle_coroutine(self, function, args, kwargs):
"""Wraps a coroutine so that we're inside the cassette context for the
duration of the coroutine.
"""
with self as cassette:
coroutine = self.__handle_function(cassette, function, args, kwargs)
# We don't need to catch StopIteration. The caller (Tornado's
# gen.coroutine, for example) will handle that.
to_yield = next(coroutine)
while True:
try:
to_send = yield to_yield
except Exception:
to_yield = coroutine.throw(*sys.exc_info())
else:
to_yield = coroutine.send(to_send)
def __handle_function(self, cassette, function, args, kwargs):
if cassette.inject:
return function(cassette, *args, **kwargs)
else:
return function(*args, **kwargs)
def _handle_function(self, function, args, kwargs):
with self as cassette:
return self.__handle_function(cassette, function, args, kwargs)
@staticmethod
def get_function_name(function):
return function.__name__
@classmethod
def _build_args_getter_for_decorator(cls, function, args_getter):
def _build_args_getter_for_decorator(self, function):
def new_args_getter():
kwargs = args_getter()
kwargs = self._args_getter()
if 'path' not in kwargs:
name_generator = (kwargs.get('func_path_generator') or
cls.get_function_name)
self.get_function_name)
path = name_generator(function)
kwargs['path'] = path
return kwargs
return new_args_getter
class SimilarityScorer(object):
def __init__(self, matchers, request, ascending=False):
self._matchers = matchers
self._request = request
self._ascending = False
def score(self, candidate, play_count):
value = 1
total = 0
if play_count < 1:
total += value
if self._ascending:
value *= 2
for matcher in self._matchers[::-1]:
if matcher(self._request, candidate):
total += value
if self._ascending:
value *= 2
return total
class Cassette(object):
"""A container for recorded requests and responses"""
max_playcount = 1
@classmethod
def load(cls, **kwargs):
"""Instantiate and load the cassette stored at the specified path."""
@@ -130,15 +189,16 @@ class Cassette(object):
return CassetteContextDecorator.from_args(cls, **kwargs)
def __init__(self, path, serializer=yamlserializer, record_mode='once',
match_on=(uri, method), before_record_request=None,
before_record_response=None, custom_patches=(),
inject=False):
match_on=(uri, method), before_record_request=None,
before_record_response=None, custom_patches=(), inject=False,
similarity_scorer_factory=None):
self._path = path
self._serializer = serializer
self._match_on = match_on
self._before_record_request = before_record_request or (lambda x: x)
self._before_record_response = before_record_response or (lambda x: x)
self._similarity_scorer_factory = similarity_scorer_factory or SimilarityScorer
self.inject = inject
self.record_mode = record_mode
self.custom_patches = custom_patches
@@ -176,8 +236,9 @@ class Cassette(object):
request = self._before_record_request(request)
if not request:
return
if self._before_record_response:
response = self._before_record_response(response)
response = self._before_record_response(response)
if response is None:
return
self.data.append((request, response))
self.dirty = True
@@ -194,6 +255,20 @@ class Cassette(object):
if requests_match(request, stored_request, self._match_on):
yield index, response
def failing_matchers(self, a, b):
return [matcher for matcher in self._match_on if not matcher(a, b)]
def similar_requests(self, request):
scorer = self._similarity_scorer_factory(self._match_on, request).score
scored_requests = [
(
stored_request,
scorer(stored_request, self.play_counts[index])
)
for index, (stored_request, response) in enumerate(self.data)
]
return sorted(scored_requests, key=operator.itemgetter(1), reverse=True)
def can_play_response_for(self, request):
request = self._before_record_request(request)
return request and request in self and \
@@ -206,7 +281,7 @@ class Cassette(object):
hasn't been played back before, and mark it as played
"""
for index, response in self._responses(request):
if self.play_counts[index] == 0:
if self.play_counts[index] < self.max_playcount:
self.play_counts[index] += 1
return response
# The cassette doesn't contain the request asked for.

View File

@@ -2,19 +2,25 @@ import copy
import functools
import inspect
import os
import types
import six
from .compat import collections
from .cassette import Cassette
from .serializers import yamlserializer, jsonserializer
from .util import compose
from .util import compose, auto_decorate
from . import matchers
from . import filters
class VCR(object):
@staticmethod
def is_test_method(method_name, function):
return method_name.startswith('test') and \
isinstance(function, types.FunctionType)
@staticmethod
def ensure_suffix(suffix):
def ensure(path):
@@ -23,7 +29,7 @@ class VCR(object):
return path
return ensure
def __init__(self, path_transformer=lambda x: x, before_record_request=None,
def __init__(self, path_transformer=None, before_record_request=None,
custom_patches=(), filter_query_parameters=(), ignore_hosts=(),
record_mode="once", ignore_localhost=False, filter_headers=(),
before_record_response=None, filter_post_data_parameters=(),
@@ -67,10 +73,11 @@ class VCR(object):
try:
serializer = self.serializers[serializer_name]
except KeyError:
print("Serializer {0} doesn't exist or isn't registered".format(
serializer_name
))
raise KeyError
raise KeyError(
"Serializer {0} doesn't exist or isn't registered".format(
serializer_name
)
)
return serializer
def _get_matchers(self, matcher_names):
@@ -117,12 +124,16 @@ class VCR(object):
'cassette_library_dir',
self.cassette_library_dir
)
additional_matchers = kwargs.get('additional_matchers', ())
if cassette_library_dir:
def add_cassette_library_dir(path):
if not path.startswith(cassette_library_dir):
return os.path.join(cassette_library_dir, path)
return path
path_transformer = compose(add_cassette_library_dir, path_transformer)
path_transformer = compose(
add_cassette_library_dir, path_transformer
)
elif not func_path_generator:
# If we don't have a library dir, use the functions
# location to build a full path for cassettes.
@@ -130,12 +141,12 @@ class VCR(object):
merged_config = {
'serializer': self._get_serializer(serializer_name),
'match_on': self._get_matchers(matcher_names),
'match_on': self._get_matchers(
tuple(matcher_names) + tuple(additional_matchers)
),
'record_mode': kwargs.get('record_mode', self.record_mode),
'before_record_request': self._build_before_record_request(kwargs),
'before_record_response': self._build_before_record_response(
kwargs
),
'before_record_response': self._build_before_record_response(kwargs),
'custom_patches': self._custom_patches + kwargs.get(
'custom_patches', ()
),
@@ -153,11 +164,11 @@ class VCR(object):
'before_record_response', self.before_record_response
)
filter_functions = []
if before_record_response and not isinstance(before_record_response,
collections.Iterable):
before_record_response = (before_record_response,)
for function in before_record_response:
filter_functions.append(function)
if before_record_response:
if not isinstance(before_record_response, collections.Iterable):
before_record_response = (before_record_response,)
filter_functions.extend(before_record_response)
def before_record_response(response):
for function in filter_functions:
if response is None:
@@ -178,7 +189,8 @@ class VCR(object):
'filter_post_data_parameters', self.filter_post_data_parameters
)
before_record_request = options.get(
"before_record_request", options.get("before_record", self.before_record_request)
"before_record_request",
options.get("before_record", self.before_record_request)
)
ignore_hosts = options.get(
'ignore_hosts', self.ignore_hosts
@@ -187,28 +199,36 @@ class VCR(object):
'ignore_localhost', self.ignore_localhost
)
if filter_headers:
filter_functions.append(functools.partial(filters.remove_headers,
headers_to_remove=filter_headers))
filter_functions.append(
functools.partial(
filters.remove_headers,
headers_to_remove=filter_headers
)
)
if filter_query_parameters:
filter_functions.append(functools.partial(filters.remove_query_parameters,
query_parameters_to_remove=filter_query_parameters))
filter_functions.append(functools.partial(
filters.remove_query_parameters,
query_parameters_to_remove=filter_query_parameters
))
if filter_post_data_parameters:
filter_functions.append(functools.partial(filters.remove_post_data_parameters,
post_data_parameters_to_remove=filter_post_data_parameters))
filter_functions.append(
functools.partial(
filters.remove_post_data_parameters,
post_data_parameters_to_remove=filter_post_data_parameters
)
)
hosts_to_ignore = list(ignore_hosts)
hosts_to_ignore = set(ignore_hosts)
if ignore_localhost:
hosts_to_ignore.extend(('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:
hosts_to_ignore = set(hosts_to_ignore)
filter_functions.append(self._build_ignore_hosts(hosts_to_ignore))
if before_record_request:
if not isinstance(before_record_request, collections.Iterable):
before_record_request = (before_record_request,)
for function in before_record_request:
filter_functions.append(function)
filter_functions.extend(before_record_request)
def before_record_request(request):
request = copy.copy(request)
for function in filter_functions:
@@ -216,7 +236,6 @@ class VCR(object):
break
request = function(request)
return request
return before_record_request
@staticmethod
@@ -230,10 +249,14 @@ class VCR(object):
@staticmethod
def _build_path_from_func_using_module(function):
return os.path.join(os.path.dirname(inspect.getfile(function)),
function.__name__)
function.__name__)
def register_serializer(self, name, serializer):
self.serializers[name] = serializer
def register_matcher(self, name, matcher):
self.matchers[name] = matcher
def test_case(self, predicate=None):
predicate = predicate or self.is_test_method
return six.with_metaclass(auto_decorate(self.use_cassette, predicate))

View File

@@ -3,8 +3,5 @@ class CannotOverwriteExistingCassetteException(Exception):
class UnhandledHTTPRequestError(KeyError):
'''
Raised when a cassette does not c
ontain the request we want
'''
"""Raised when a cassette does not contain the request we want."""
pass

View File

@@ -1,19 +1,16 @@
from six import BytesIO, text_type
from six.moves.urllib.parse import urlparse, urlencode, urlunparse
import copy
import json
from .compat import collections
def remove_headers(request, headers_to_remove):
headers = copy.copy(request.headers)
headers_to_remove = [h.lower() for h in headers_to_remove]
keys = [k for k in headers if k.lower() in headers_to_remove]
if keys:
for k in keys:
headers.pop(k)
request.headers = headers
new_headers = request.headers.copy()
for k in headers_to_remove:
if k in new_headers:
del new_headers[k]
request.headers = new_headers
return request
@@ -30,8 +27,7 @@ def remove_query_parameters(request, query_parameters_to_remove):
def remove_post_data_parameters(request, post_data_parameters_to_remove):
if request.method == 'POST' and not isinstance(request.body, BytesIO):
if ('Content-Type' in request.headers and
request.headers['Content-Type'] == 'application/json'):
if request.headers.get('Content-Type') == 'application/json':
json_data = json.loads(request.body.decode('utf-8'))
for k in list(json_data.keys()):
if k in post_data_parameters_to_remove:

View File

@@ -1,7 +1,9 @@
import json
from six.moves import urllib, xmlrpc_client
from .util import CaseInsensitiveDict, read_body
from .util import read_body
import logging
log = logging.getLogger(__name__)
@@ -43,11 +45,18 @@ def _header_checker(value, header='Content-Type'):
return checker
def _transform_json(body):
# Request body is always a byte string, but json.loads() wants a text
# string. RFC 7159 says the default encoding is UTF-8 (although UTF-16
# and UTF-32 are also allowed: hmmmmm).
return json.loads(body.decode('utf-8'))
_xml_header_checker = _header_checker('text/xml')
_xmlrpc_header_checker = _header_checker('xmlrpc', header='User-Agent')
_checker_transformer_pairs = (
(_header_checker('application/x-www-form-urlencoded'), urllib.parse.parse_qs),
(_header_checker('application/json'), json.loads),
(_header_checker('application/json'), _transform_json),
(lambda request: _xml_header_checker(request) and _xmlrpc_header_checker(request), xmlrpc_client.loads),
)
@@ -57,9 +66,8 @@ def _identity(x):
def _get_transformer(request):
headers = CaseInsensitiveDict(request.headers)
for checker, transformer in _checker_transformer_pairs:
if checker(headers): return transformer
if checker(request.headers): return transformer
else:
return _identity

View File

@@ -54,13 +54,12 @@ else:
# Try to save the original types for Tornado
try:
import tornado.httpclient
import tornado.simple_httpclient
except ImportError: # pragma: no cover
pass
else:
_AsyncHTTPClient = tornado.httpclient.AsyncHTTPClient
_SimpleAsyncHTTPClient = tornado.simple_httpclient.SimpleAsyncHTTPClient
_SimpleAsyncHTTPClient_fetch_impl = \
tornado.simple_httpclient.SimpleAsyncHTTPClient.fetch_impl
try:
@@ -68,7 +67,8 @@ try:
except ImportError: # pragma: no cover
pass
else:
_CurlAsyncHTTPClient = tornado.curl_httpclient.CurlAsyncHTTPClient
_CurlAsyncHTTPClient_fetch_impl = \
tornado.curl_httpclient.CurlAsyncHTTPClient.fetch_impl
class CassettePatcherBuilder(object):
@@ -228,23 +228,27 @@ class CassettePatcherBuilder(object):
@_build_patchers_from_mock_triples_decorator
def _tornado(self):
try:
import tornado.httpclient as http
import tornado.simple_httpclient as simple
except ImportError: # pragma: no cover
pass
else:
from .stubs.tornado_stubs import VCRAsyncHTTPClient
from .stubs.tornado_stubs import VCRSimpleAsyncHTTPClient
from .stubs.tornado_stubs import vcr_fetch_impl
yield http, 'AsyncHTTPClient', VCRAsyncHTTPClient
yield simple, 'SimpleAsyncHTTPClient', VCRSimpleAsyncHTTPClient
new_fetch_impl = vcr_fetch_impl(
self._cassette, _SimpleAsyncHTTPClient_fetch_impl
)
yield simple.SimpleAsyncHTTPClient, 'fetch_impl', new_fetch_impl
try:
import tornado.curl_httpclient as curl
except ImportError: # pragma: no cover
pass
else:
from .stubs.tornado_stubs import VCRCurlAsyncHTTPClient
yield curl, 'CurlAsyncHTTPClient', VCRCurlAsyncHTTPClient
from .stubs.tornado_stubs import vcr_fetch_impl
new_fetch_impl = vcr_fetch_impl(
self._cassette, _CurlAsyncHTTPClient_fetch_impl
)
yield curl.CurlAsyncHTTPClient, 'fetch_impl', new_fetch_impl
def _urllib3_patchers(self, cpool, stubs):
http_connection_remover = ConnectionRemover(
@@ -362,19 +366,25 @@ def reset_patchers():
_CertValidatingHTTPSConnection)
try:
import tornado.httpclient as http
import tornado.simple_httpclient as simple
except ImportError: # pragma: no cover
pass
else:
yield mock.patch.object(http, 'AsyncHTTPClient', _AsyncHTTPClient)
yield mock.patch.object(simple, 'SimpleAsyncHTTPClient', _SimpleAsyncHTTPClient)
yield mock.patch.object(
simple.SimpleAsyncHTTPClient,
'fetch_impl',
_SimpleAsyncHTTPClient_fetch_impl,
)
try:
import tornado.curl_httpclient as curl
except ImportError: # pragma: no cover
pass
else:
yield mock.patch.object(curl, 'CurlAsyncHTTPClient', _CurlAsyncHTTPClient)
yield mock.patch.object(
curl.CurlAsyncHTTPClient,
'fetch_impl',
_CurlAsyncHTTPClient_fetch_impl,
)
@contextlib.contextmanager

View File

@@ -1,27 +1,12 @@
from six import BytesIO, binary_type
import warnings
from six import BytesIO, text_type
from six.moves.urllib.parse import urlparse, parse_qsl
from .util import CaseInsensitiveDict
class Request(object):
"""
VCR's representation of a request.
There is a weird quirk in HTTP. You can send the same header twice. For
this reason, headers are represented by a dict, with lists as the values.
However, it appears that HTTPlib is completely incapable of sending the
same header twice. This puts me in a weird position: I want to be able to
accurately represent HTTP headers in cassettes, but I don't want the extra
step of always having to do [0] in the general case, i.e.
request.headers['key'][0]
In addition, some servers sometimes send the same header more than once,
and httplib *can* deal with this situation.
Futhermore, I wanted to keep the request and response cassette format as
similar as possible.
For this reason, in cassettes I keep a dict with lists as keys, but once
deserialized into VCR, I keep them as plain, naked dicts.
VCR's representation of a request.
"""
def __init__(self, method, uri, body, headers):
@@ -29,14 +14,20 @@ class Request(object):
self.uri = uri
self._was_file = hasattr(body, 'read')
if self._was_file:
self._body = body.read()
if not isinstance(self._body, binary_type):
self._body = self._body.encode('utf-8')
self.body = body.read()
else:
self._body = body
self.headers = {}
for key in headers:
self.add_header(key, headers[key])
self.body = body
self.headers = headers
@property
def headers(self):
return self._headers
@headers.setter
def headers(self, value):
if not isinstance(value, HeadersDict):
value = HeadersDict(value)
self._headers = value
@property
def body(self):
@@ -44,14 +35,15 @@ class Request(object):
@body.setter
def body(self, value):
if isinstance(value, text_type):
value = value.encode('utf-8')
self._body = value
def add_header(self, key, value):
# see class docstring for an explanation
if isinstance(value, (tuple, list)):
self.headers[key] = value[0]
else:
self.headers[key] = value
warnings.warn("Request.add_header is deprecated. "
"Please assign to request.headers instead.",
DeprecationWarning)
self.headers[key] = value
@property
def scheme(self):
@@ -105,3 +97,35 @@ class Request(object):
@classmethod
def _from_dict(cls, dct):
return Request(**dct)
class HeadersDict(CaseInsensitiveDict):
"""
There is a weird quirk in HTTP. You can send the same header twice. For
this reason, headers are represented by a dict, with lists as the values.
However, it appears that HTTPlib is completely incapable of sending the
same header twice. This puts me in a weird position: I want to be able to
accurately represent HTTP headers in cassettes, but I don't want the extra
step of always having to do [0] in the general case, i.e.
request.headers['key'][0]
In addition, some servers sometimes send the same header more than once,
and httplib *can* deal with this situation.
Futhermore, I wanted to keep the request and response cassette format as
similar as possible.
For this reason, in cassettes I keep a dict with lists as keys, but once
deserialized into VCR, I keep them as plain, naked dicts.
"""
def __setitem__(self, key, value):
if isinstance(value, (tuple, list)):
value = value[0]
# Preserve the case from the first time this key was set.
old = self._store.get(key.lower())
if old:
key = old[0]
super(HeadersDict, self).__setitem__(key, value)

View File

@@ -9,7 +9,6 @@ import six
from six.moves.http_client import (
HTTPConnection,
HTTPSConnection,
HTTPMessage,
HTTPResponse,
)
from six import BytesIO
@@ -188,8 +187,7 @@ class VCRConnection(object):
log.debug('Got {0}'.format(self._vcr_request))
def putheader(self, header, *values):
for value in values:
self._vcr_request.add_header(header, value)
self._vcr_request.headers[header] = values
def send(self, data):
'''
@@ -229,12 +227,26 @@ class VCRConnection(object):
if self.cassette.write_protected and self.cassette.filter_request(
self._vcr_request
):
most_similar_request = None
failing_matchers = None
most_similar_request_info = None
try:
most_similar_request_info = self.cassette.similar_requests(self._vcr_request)
most_similar_request = most_similar_request_info[0][0]
failing_matchers = self.cassette.failing_matchers(
self._vcr_request, most_similar_request
)
except Exception as err:
print "XXXX {0}".format(err)
import ipdb; ipdb.set_trace()
raise CannotOverwriteExistingCassetteException(
"No match for the request (%r) was found. "
"Can't overwrite existing cassette (%r) in "
"your current record mode (%r)."
"your current record mode (%r). Most similar request was (%r). "
"It differed from the request according to (%r). \n\n\n(%r)"
% (self._vcr_request, self.cassette._path,
self.cassette.record_mode)
self.cassette.record_mode, most_similar_request,
failing_matchers, most_similar_request_info)
)
# Otherwise, we should send the request, then get the response
@@ -307,7 +319,7 @@ class VCRConnection(object):
def __init__(self, *args, **kwargs):
if six.PY3:
kwargs.pop('strict', None) # apparently this is gone in py3
kwargs.pop('strict', None) # apparently this is gone in py3
# need to temporarily reset here because the real connection
# inherits from the thing that we are mocking out. Take out
@@ -316,6 +328,27 @@ class VCRConnection(object):
with force_reset():
self.real_connection = self._baseclass(*args, **kwargs)
def __setattr__(self, name, value):
"""
We need to define this because any attributes that are set on the
VCRConnection need to be propogated to the real connection.
For example, urllib3 will set certain attributes on the connection,
such as 'ssl_version'. These attributes need to get set on the real
connection to have the correct and expected behavior.
TODO: Separately setting the attribute on the two instances is not
ideal. We should switch to a proxying implementation.
"""
try:
setattr(self.real_connection, name, value)
except AttributeError:
# raised if real_connection has not been set yet, such as when
# we're setting the real_connection itself for the first time
pass
super(VCRConnection, self).__setattr__(name, value)
class VCRHTTPConnection(VCRConnection):
'''A Mocked class for HTTP requests'''

View File

@@ -1,49 +1,21 @@
'''Stubs for tornado HTTP clients'''
from __future__ import absolute_import
import functools
from six import BytesIO
from tornado import httputil
from tornado.httpclient import AsyncHTTPClient
from tornado.httpclient import HTTPResponse
from tornado.simple_httpclient import SimpleAsyncHTTPClient
from vcr.errors import CannotOverwriteExistingCassetteException
from vcr.request import Request
class _VCRAsyncClient(object):
cassette = None
def vcr_fetch_impl(cassette, real_fetch_impl):
def __new__(cls, *args, **kwargs):
from vcr.patch import force_reset
with force_reset():
return super(_VCRAsyncClient, cls).__new__(cls, *args, **kwargs)
def initialize(self, *args, **kwargs):
from vcr.patch import force_reset
with force_reset():
self.real_client = self._baseclass(*args, **kwargs)
@property
def io_loop(self):
return self.real_client.io_loop
@property
def _closed(self):
return self.real_client._closed
@property
def defaults(self):
return self.real_client.defaults
def close(self):
from vcr.patch import force_reset
with force_reset():
self.real_client.close()
def fetch_impl(self, request, callback):
headers = dict(request.headers)
@functools.wraps(real_fetch_impl)
def new_fetch_impl(self, request, callback):
headers = request.headers.copy()
if request.user_agent:
headers.setdefault('User-Agent', request.user_agent)
@@ -64,6 +36,7 @@ class _VCRAsyncClient(object):
"that is not yet supported by VCR.py. Please make the "
"request outside a VCR.py context." % repr(request)
),
request_time=self.io_loop.time() - request.start_time,
)
return callback(response)
@@ -74,8 +47,8 @@ class _VCRAsyncClient(object):
headers,
)
if self.cassette.can_play_response_for(vcr_request):
vcr_response = self.cassette.play_response(vcr_request)
if cassette.can_play_response_for(vcr_request):
vcr_response = cassette.play_response(vcr_request)
headers = httputil.HTTPHeaders()
recorded_headers = vcr_response['headers']
@@ -90,10 +63,12 @@ class _VCRAsyncClient(object):
reason=vcr_response['status']['message'],
headers=headers,
buffer=BytesIO(vcr_response['body']['string']),
effective_url=vcr_response.get('url'),
request_time=self.io_loop.time() - request.start_time,
)
return callback(response)
else:
if self.cassette.write_protected and self.cassette.filter_request(
if cassette.write_protected and cassette.filter_request(
vcr_request
):
response = HTTPResponse(
@@ -103,9 +78,9 @@ class _VCRAsyncClient(object):
"No match for the request (%r) was found. "
"Can't overwrite existing cassette (%r) in "
"your current record mode (%r)."
% (vcr_request, self.cassette._path,
self.cassette.record_mode)
% (vcr_request, cassette._path, cassette.record_mode)
),
request_time=self.io_loop.time() - request.start_time,
)
return callback(response)
@@ -122,27 +97,11 @@ class _VCRAsyncClient(object):
},
'headers': headers,
'body': {'string': response.body},
'url': response.effective_url,
}
self.cassette.append(vcr_request, vcr_response)
cassette.append(vcr_request, vcr_response)
return callback(response)
from vcr.patch import force_reset
with force_reset():
self.real_client.fetch_impl(request, new_callback)
real_fetch_impl(self, request, new_callback)
class VCRAsyncHTTPClient(_VCRAsyncClient, AsyncHTTPClient):
_baseclass = AsyncHTTPClient
class VCRSimpleAsyncHTTPClient(_VCRAsyncClient, SimpleAsyncHTTPClient):
_baseclass = SimpleAsyncHTTPClient
try:
from tornado.curl_httpclient import CurlAsyncHTTPClient
except ImportError: # pragma: no cover
VCRCurlAsyncHTTPClient = None
else:
class VCRCurlAsyncHTTPClient(_VCRAsyncClient, CurlAsyncHTTPClient):
_baseclass = CurlAsyncHTTPClient
return new_fetch_impl

View File

@@ -1,4 +1,6 @@
import collections
import types
# Shamelessly stolen from https://github.com/kennethreitz/requests/blob/master/requests/structures.py
class CaseInsensitiveDict(collections.MutableMapping):
@@ -81,8 +83,9 @@ def partition_dict(predicate, dictionary):
def compose(*functions):
def composed(incoming):
res = incoming
for function in functions[::-1]:
res = function(res)
for function in reversed(functions):
if function:
res = function(res)
return res
return composed
@@ -90,3 +93,30 @@ def read_body(request):
if hasattr(request.body, 'read'):
return request.body.read()
return request.body
def auto_decorate(
decorator,
predicate=lambda name, value: isinstance(value, types.FunctionType)
):
def maybe_decorate(attribute, value):
if predicate(attribute, value):
value = decorator(value)
return value
class DecorateAll(type):
def __setattr__(cls, attribute, value):
return super(DecorateAll, cls).__setattr__(
attribute, maybe_decorate(attribute, value)
)
def __new__(cls, name, bases, attributes_dict):
new_attributes_dict = dict(
(attribute, maybe_decorate(attribute, value))
for attribute, value in attributes_dict.items()
)
return super(DecorateAll, cls).__new__(
cls, name, bases, new_attributes_dict
)
return DecorateAll