mirror of
https://github.com/kevin1024/vcrpy.git
synced 2025-12-08 16:53:23 +00:00
Compare commits
60 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
efe6744eda | ||
|
|
58f4b98f7f | ||
|
|
3305f0ca7d | ||
|
|
7f02d65dd9 | ||
|
|
3e5553c56a | ||
|
|
a569dd4dc8 | ||
|
|
eb1cdad03a | ||
|
|
08bb3bd187 | ||
|
|
ae5580c8f9 | ||
|
|
f342f92f03 | ||
|
|
be3bf39161 | ||
|
|
29d37e410a | ||
|
|
8b7e6c0ab8 | ||
|
|
bd7c6ed03f | ||
|
|
1e414826e7 | ||
|
|
1e1c093b3c | ||
|
|
bb8f563135 | ||
|
|
ca3200d96e | ||
|
|
04b5978adc | ||
|
|
01f1f9fdc1 | ||
|
|
a82e8628c2 | ||
|
|
7d68f0577a | ||
|
|
d0aa5fddb7 | ||
|
|
e54aeadc68 | ||
|
|
c4a33d1cff | ||
|
|
8b59d73f25 | ||
|
|
eb394b90d9 | ||
|
|
14931dd47a | ||
|
|
89cdda86d1 | ||
|
|
ad48d71897 | ||
|
|
946ce17a97 | ||
|
|
4d438dac75 | ||
|
|
a234ad6b12 | ||
|
|
1d000ac652 | ||
|
|
21c176ee1e | ||
|
|
4fb5bef8e1 | ||
|
|
9717596e2c | ||
|
|
1660cc3a9f | ||
|
|
4beb023204 | ||
|
|
72eb5345d6 | ||
|
|
fe7d193d1a | ||
|
|
09b7ccf561 | ||
|
|
a4a80b431b | ||
|
|
025a3b422d | ||
|
|
bb05b2fcf7 | ||
|
|
f77ef81877 | ||
|
|
80ece7750f | ||
|
|
8a86d75dc5 | ||
|
|
33a4fb98c6 | ||
|
|
a046697567 | ||
|
|
c0286dfd97 | ||
|
|
cc9af1d5fb | ||
|
|
5f8407a8a1 | ||
|
|
c789c82c1d | ||
|
|
16b5b77bcd | ||
|
|
0a093786ed | ||
|
|
3986caf182 | ||
|
|
cc6c26646c | ||
|
|
3846a4ccef | ||
|
|
aae4ae255b |
63
README.rst
63
README.rst
@@ -5,6 +5,7 @@ VCR.py
|
||||
:alt: vcr.py
|
||||
|
||||
vcr.py
|
||||
|
||||
This is a Python version of `Ruby's VCR
|
||||
library <https://github.com/vcr/vcr>`__.
|
||||
|
||||
@@ -13,17 +14,18 @@ library <https://github.com/vcr/vcr>`__.
|
||||
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
|
||||
@@ -49,6 +51,7 @@ The following http libraries are supported:
|
||||
- requests (both 1.x and 2.x versions)
|
||||
- httplib2
|
||||
- boto
|
||||
- Tornado's AsyncHTTPClient
|
||||
|
||||
Usage
|
||||
-----
|
||||
@@ -107,10 +110,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'):
|
||||
@@ -144,7 +147,9 @@ The following options are available :
|
||||
- port (the port of the server receiving the request)
|
||||
- path (the path of the request)
|
||||
- query (the query string of the request)
|
||||
- body (the entire request body)
|
||||
- raw\_body (the entire request body as is)
|
||||
- body (the entire request body unmarshalled by content-type
|
||||
i.e. xmlrpc, json, form-urlencoded, falling back on raw\_body)
|
||||
- headers (the headers of the request)
|
||||
|
||||
Backwards compatible matchers:
|
||||
@@ -413,12 +418,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
|
||||
@@ -603,7 +609,22 @@ new API in version 1.0.x
|
||||
|
||||
Changelog
|
||||
---------
|
||||
- 1.6.0 [#120] Tornado support thanks (thanks @abhinav), [#147] packaging fixes
|
||||
- 1.7.3 [#188] ``additional_matchers`` kwarg on ``use_casstte``.
|
||||
[#191] Actually support passing multiple before_record_request
|
||||
functions (thanks @agriffis).
|
||||
- 1.7.2 [#186] Get effective_url in tornado (thanks @mvschaik), [#187]
|
||||
Set request_time on Response object in tornado (thanks @abhinav).
|
||||
- 1.7.1 [#183] Patch ``fetch_impl`` instead of the entire HTTPClient
|
||||
class for Tornado (thanks @abhinav).
|
||||
- 1.7.0 [#177] Properly support coroutine/generator decoration. [#178]
|
||||
Support distribute (thanks @graingert). [#163] Make compatibility
|
||||
between python2 and python3 recorded cassettes more robust (thanks
|
||||
@gward).
|
||||
- 1.6.1 [#169] Support conditional requirements in old versions of
|
||||
pip, Fix RST parse errors generated by pandoc, [Tornado] Fix
|
||||
unsupported features exception not being raised, [#166]
|
||||
content-aware body matcher.
|
||||
- 1.6.0 [#120] Tornado support (thanks @abhinav), [#147] packaging fixes
|
||||
(thanks @graingert), [#158] allow filtering post params in requests
|
||||
(thanks @MrJohz), [#140] add xmlrpclib support (thanks @Diaoul).
|
||||
- 1.5.2 Fix crash when cassette path contains cassette library
|
||||
|
||||
39
setup.py
39
setup.py
@@ -1,11 +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):
|
||||
@@ -20,9 +24,34 @@ class PyTest(TestCommand):
|
||||
sys.exit(errno)
|
||||
|
||||
|
||||
install_requires = ['PyYAML', 'wrapt', 'six>=1.5']
|
||||
|
||||
|
||||
extras_require = {
|
||||
':python_version in "2.4, 2.5, 2.6"':
|
||||
['contextlib2', 'backport_collections', 'mock'],
|
||||
':python_version in "2.7, 3.1, 3.2"': ['contextlib2', 'mock'],
|
||||
}
|
||||
|
||||
|
||||
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(':'):
|
||||
install_requires.extend(value)
|
||||
|
||||
|
||||
setup(
|
||||
name='vcrpy',
|
||||
version='1.6.0',
|
||||
version='1.7.3',
|
||||
description=(
|
||||
"Automatically mock your HTTP interactions to simplify and "
|
||||
"speed up testing"
|
||||
@@ -32,12 +61,8 @@ setup(
|
||||
author_email='me@kevinmccarthy.org',
|
||||
url='https://github.com/kevin1024/vcrpy',
|
||||
packages=find_packages(exclude=("tests*",)),
|
||||
install_requires=['PyYAML', 'wrapt', 'six>=1.5'],
|
||||
extras_require = {
|
||||
':python_version in "2.4, 2.5, 2.6"':
|
||||
['contextlib2', 'backport_collections', 'mock'],
|
||||
':python_version in "2.7, 3.1, 3.2"': ['contextlib2', 'mock'],
|
||||
},
|
||||
install_requires=install_requires,
|
||||
extras_require=extras_require,
|
||||
license='MIT',
|
||||
tests_require=['pytest', 'mock', 'pytest-localserver'],
|
||||
cmdclass={'test': PyTest},
|
||||
|
||||
@@ -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'''
|
||||
|
||||
@@ -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'''
|
||||
|
||||
@@ -5,6 +5,7 @@ import json
|
||||
|
||||
import pytest
|
||||
import vcr
|
||||
from vcr.errors import CannotOverwriteExistingCassetteException
|
||||
|
||||
from assertions import assert_cassette_empty, assert_is_json
|
||||
|
||||
@@ -80,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):
|
||||
@@ -203,3 +215,141 @@ def test_https_with_cert_validation_disabled(get_client, tmpdir):
|
||||
with vcr.use_cassette(cass_path) as cass:
|
||||
yield get(get_client(), 'https://httpbin.org', validate_cert=False)
|
||||
assert 1 == cass.play_count
|
||||
|
||||
|
||||
@pytest.mark.gen_test
|
||||
def test_unsupported_features_raises_in_future(get_client, tmpdir):
|
||||
'''Ensure that the exception for an AsyncHTTPClient feature not being
|
||||
supported is raised inside the future.'''
|
||||
|
||||
def callback(chunk):
|
||||
assert False, "Did not expect to be called."
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('invalid.yaml'))):
|
||||
future = get(
|
||||
get_client(), 'http://httpbin.org', streaming_callback=callback
|
||||
)
|
||||
|
||||
with pytest.raises(Exception) as excinfo:
|
||||
yield future
|
||||
|
||||
assert "not yet supported by VCR" in str(excinfo)
|
||||
|
||||
|
||||
@pytest.mark.gen_test
|
||||
def test_unsupported_features_raise_error_disabled(get_client, tmpdir):
|
||||
'''Ensure that the exception for an AsyncHTTPClient feature not being
|
||||
supported is not raised if raise_error=False.'''
|
||||
|
||||
def callback(chunk):
|
||||
assert False, "Did not expect to be called."
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('invalid.yaml'))):
|
||||
response = yield get(
|
||||
get_client(),
|
||||
'http://httpbin.org',
|
||||
streaming_callback=callback,
|
||||
raise_error=False,
|
||||
)
|
||||
|
||||
assert "not yet supported by VCR" in str(response.error)
|
||||
|
||||
|
||||
@pytest.mark.gen_test
|
||||
def test_cannot_overwrite_cassette_raises_in_future(get_client, tmpdir):
|
||||
'''Ensure that CannotOverwriteExistingCassetteException is raised inside
|
||||
the future.'''
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('overwrite.yaml'))):
|
||||
yield get(get_client(), 'http://httpbin.org/get')
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('overwrite.yaml'))):
|
||||
future = get(get_client(), 'http://httpbin.org/headers')
|
||||
|
||||
with pytest.raises(CannotOverwriteExistingCassetteException):
|
||||
yield future
|
||||
|
||||
|
||||
@pytest.mark.gen_test
|
||||
def test_cannot_overwrite_cassette_raise_error_disabled(get_client, tmpdir):
|
||||
'''Ensure that CannotOverwriteExistingCassetteException is not raised if
|
||||
raise_error=False in the fetch() call.'''
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('overwrite.yaml'))):
|
||||
yield get(
|
||||
get_client(), 'http://httpbin.org/get', raise_error=False
|
||||
)
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('overwrite.yaml'))):
|
||||
response = yield get(
|
||||
get_client(), 'http://httpbin.org/headers', raise_error=False
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
62
tests/integration/test_tornado_exception_can_be_caught.yaml
Normal file
62
tests/integration/test_tornado_exception_can_be_caught.yaml
Normal 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
|
||||
@@ -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
|
||||
@@ -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'''
|
||||
|
||||
@@ -253,3 +253,37 @@ 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]
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import itertools
|
||||
|
||||
import pytest
|
||||
|
||||
from vcr import matchers
|
||||
from vcr import request
|
||||
|
||||
@@ -35,6 +37,107 @@ def test_uri_matcher():
|
||||
assert matched
|
||||
|
||||
|
||||
req1_body = (b"<?xml version='1.0'?><methodCall><methodName>test</methodName>"
|
||||
b"<params><param><value><array><data><value><struct>"
|
||||
b"<member><name>a</name><value><string>1</string></value></member>"
|
||||
b"<member><name>b</name><value><string>2</string></value></member>"
|
||||
b"</struct></value></data></array></value></param></params></methodCall>")
|
||||
req2_body = (b"<?xml version='1.0'?><methodCall><methodName>test</methodName>"
|
||||
b"<params><param><value><array><data><value><struct>"
|
||||
b"<member><name>b</name><value><string>2</string></value></member>"
|
||||
b"<member><name>a</name><value><string>1</string></value></member>"
|
||||
b"</struct></value></data></array></value></param></params></methodCall>")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("r1, r2", [
|
||||
(
|
||||
request.Request('POST', 'http://host.com/', '123', {}),
|
||||
request.Request('POST', 'http://another-host.com/',
|
||||
'123', {'Some-Header': 'value'})
|
||||
),
|
||||
(
|
||||
request.Request('POST', 'http://host.com/', 'a=1&b=2',
|
||||
{'Content-Type': 'application/x-www-form-urlencoded'}),
|
||||
request.Request('POST', 'http://host.com/', 'b=2&a=1',
|
||||
{'Content-Type': 'application/x-www-form-urlencoded'})
|
||||
),
|
||||
(
|
||||
request.Request('POST', 'http://host.com/', '123', {}),
|
||||
request.Request('POST', 'http://another-host.com/', '123', {'Some-Header': 'value'})
|
||||
),
|
||||
(
|
||||
request.Request(
|
||||
'POST', 'http://host.com/', 'a=1&b=2',
|
||||
{'Content-Type': 'application/x-www-form-urlencoded'}
|
||||
),
|
||||
request.Request(
|
||||
'POST', 'http://host.com/', 'b=2&a=1',
|
||||
{'Content-Type': 'application/x-www-form-urlencoded'}
|
||||
)
|
||||
),
|
||||
(
|
||||
request.Request(
|
||||
'POST', 'http://host.com/', '{"a": 1, "b": 2}',
|
||||
{'Content-Type': 'application/json'}
|
||||
),
|
||||
request.Request(
|
||||
'POST', 'http://host.com/', '{"b": 2, "a": 1}',
|
||||
{'content-type': 'application/json'}
|
||||
)
|
||||
),
|
||||
(
|
||||
request.Request(
|
||||
'POST', 'http://host.com/', req1_body,
|
||||
{'User-Agent': 'xmlrpclib', 'Content-Type': 'text/xml'}
|
||||
),
|
||||
request.Request(
|
||||
'POST', 'http://host.com/', req2_body,
|
||||
{'user-agent': 'somexmlrpc', 'content-type': 'text/xml'}
|
||||
)
|
||||
),
|
||||
(
|
||||
request.Request(
|
||||
'POST', 'http://host.com/',
|
||||
'{"a": 1, "b": 2}', {'Content-Type': 'application/json'}
|
||||
),
|
||||
request.Request(
|
||||
'POST', 'http://host.com/',
|
||||
'{"b": 2, "a": 1}', {'content-type': 'application/json'}
|
||||
)
|
||||
)
|
||||
])
|
||||
def test_body_matcher_does_match(r1, r2):
|
||||
assert matchers.body(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": 3}', {'Content-Type': 'application/json'}
|
||||
),
|
||||
request.Request(
|
||||
'POST', 'http://host.com/',
|
||||
'{"b": 2, "a": 1}', {'content-type': 'application/json'}
|
||||
)
|
||||
),
|
||||
(
|
||||
request.Request(
|
||||
'POST', 'http://host.com/', req1_body, {'Content-Type': 'text/xml'}
|
||||
),
|
||||
request.Request(
|
||||
'POST', 'http://host.com/', req2_body, {'content-type': 'text/xml'}
|
||||
)
|
||||
)
|
||||
])
|
||||
def test_body_match_does_not_match(r1, r2):
|
||||
assert not matchers.body(r1, r2)
|
||||
|
||||
|
||||
def test_query_matcher():
|
||||
req1 = request.Request('GET', 'http://host.com/?a=b&c=d', '', {})
|
||||
req2 = request.Request('GET', 'http://host.com/?c=d&a=b', '', {})
|
||||
|
||||
@@ -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'))
|
||||
|
||||
@@ -15,9 +15,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 +40,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 +57,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 +72,32 @@ 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
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def random_fixture():
|
||||
return 1
|
||||
@@ -103,6 +137,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 +152,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 +178,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,26 +212,32 @@ 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()
|
||||
|
||||
|
||||
@@ -203,8 +252,25 @@ 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()
|
||||
|
||||
101
vcr/cassette.py
101
vcr/cassette.py
@@ -1,11 +1,9 @@
|
||||
"""The container for recorded requests and responses"""
|
||||
import functools
|
||||
import sys
|
||||
import inspect
|
||||
import logging
|
||||
|
||||
|
||||
import wrapt
|
||||
|
||||
# Internal imports
|
||||
from .compat import contextlib, collections
|
||||
from .errors import UnhandledHTTPRequestError
|
||||
from .matchers import requests_match, uri, method
|
||||
@@ -22,10 +20,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 +49,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,7 +71,10 @@ 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())
|
||||
other_kwargs, cassette_kwargs = partition_dict(
|
||||
lambda key, _: key in self._non_cassette_arguments,
|
||||
self._args_getter()
|
||||
)
|
||||
if 'path_transformer' in other_kwargs:
|
||||
transformer = other_kwargs['path_transformer']
|
||||
cassette_kwargs['path'] = transformer(cassette_kwargs['path'])
|
||||
@@ -84,27 +90,55 @@ 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:
|
||||
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
|
||||
@@ -130,7 +164,7 @@ 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,
|
||||
match_on=(uri, method), before_record_request=None,
|
||||
before_record_response=None, custom_patches=(),
|
||||
inject=False):
|
||||
|
||||
@@ -176,8 +210,7 @@ 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)
|
||||
self.data.append((request, response))
|
||||
self.dirty = True
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ class VCR(object):
|
||||
'path': matchers.path,
|
||||
'query': matchers.query,
|
||||
'headers': matchers.headers,
|
||||
'raw_body': matchers.raw_body,
|
||||
'body': matchers.body,
|
||||
}
|
||||
self.record_mode = record_mode
|
||||
@@ -66,10 +67,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):
|
||||
@@ -106,7 +108,7 @@ class VCR(object):
|
||||
matcher_names = kwargs.get('match_on', self.match_on)
|
||||
path_transformer = kwargs.get(
|
||||
'path_transformer',
|
||||
self.path_transformer
|
||||
self.path_transformer or self.ensure_suffix('.yaml')
|
||||
)
|
||||
func_path_generator = kwargs.get(
|
||||
'func_path_generator',
|
||||
@@ -116,12 +118,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.
|
||||
@@ -129,12 +135,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', ()
|
||||
),
|
||||
@@ -152,11 +158,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:
|
||||
@@ -177,7 +183,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
|
||||
@@ -186,28 +193,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:
|
||||
@@ -215,7 +230,6 @@ class VCR(object):
|
||||
break
|
||||
request = function(request)
|
||||
return request
|
||||
|
||||
return before_record_request
|
||||
|
||||
@staticmethod
|
||||
@@ -229,7 +243,7 @@ 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import json
|
||||
from six.moves import urllib, xmlrpc_client
|
||||
from .util import CaseInsensitiveDict, read_body
|
||||
import logging
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -30,10 +35,50 @@ def query(r1, r2):
|
||||
return r1.query == r2.query
|
||||
|
||||
|
||||
def raw_body(r1, r2):
|
||||
return read_body(r1) == read_body(r2)
|
||||
|
||||
|
||||
def _header_checker(value, header='Content-Type'):
|
||||
def checker(headers):
|
||||
return value in headers.get(header, '').lower()
|
||||
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'), _transform_json),
|
||||
(lambda request: _xml_header_checker(request) and _xmlrpc_header_checker(request), xmlrpc_client.loads),
|
||||
)
|
||||
|
||||
|
||||
def _identity(x):
|
||||
return x
|
||||
|
||||
|
||||
def _get_transformer(request):
|
||||
headers = CaseInsensitiveDict(request.headers)
|
||||
for checker, transformer in _checker_transformer_pairs:
|
||||
if checker(headers): return transformer
|
||||
else:
|
||||
return _identity
|
||||
|
||||
|
||||
def body(r1, r2):
|
||||
if hasattr(r1.body, 'read') and hasattr(r2.body, 'read'):
|
||||
return r1.body.read() == r2.body.read()
|
||||
return r1.body == r2.body
|
||||
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):
|
||||
|
||||
40
vcr/patch.py
40
vcr/patch.py
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from six import BytesIO, binary_type
|
||||
from six import BytesIO, text_type
|
||||
from six.moves.urllib.parse import urlparse, parse_qsl
|
||||
|
||||
|
||||
@@ -29,11 +29,9 @@ 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.body = body
|
||||
self.headers = {}
|
||||
for key in headers:
|
||||
self.add_header(key, headers[key])
|
||||
@@ -44,6 +42,8 @@ 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):
|
||||
|
||||
@@ -1,48 +1,20 @@
|
||||
'''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):
|
||||
@functools.wraps(real_fetch_impl)
|
||||
def new_fetch_impl(self, request, callback):
|
||||
headers = dict(request.headers)
|
||||
if request.user_agent:
|
||||
headers.setdefault('User-Agent', request.user_agent)
|
||||
@@ -64,7 +36,9 @@ 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)
|
||||
|
||||
vcr_request = Request(
|
||||
request.method,
|
||||
@@ -73,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']
|
||||
@@ -89,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,
|
||||
)
|
||||
callback(response)
|
||||
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(
|
||||
@@ -102,11 +78,11 @@ 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,
|
||||
)
|
||||
callback(response)
|
||||
return callback(response)
|
||||
|
||||
def new_callback(response):
|
||||
headers = [
|
||||
@@ -121,27 +97,11 @@ class _VCRAsyncClient(object):
|
||||
},
|
||||
'headers': headers,
|
||||
'body': {'string': response.body},
|
||||
'url': response.effective_url,
|
||||
}
|
||||
self.cassette.append(vcr_request, vcr_response)
|
||||
callback(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
|
||||
|
||||
76
vcr/util.py
76
vcr/util.py
@@ -1,3 +1,74 @@
|
||||
import collections
|
||||
|
||||
# Shamelessly stolen from https://github.com/kennethreitz/requests/blob/master/requests/structures.py
|
||||
class CaseInsensitiveDict(collections.MutableMapping):
|
||||
"""
|
||||
A case-insensitive ``dict``-like object.
|
||||
Implements all methods and operations of
|
||||
``collections.MutableMapping`` as well as dict's ``copy``. Also
|
||||
provides ``lower_items``.
|
||||
All keys are expected to be strings. The structure remembers the
|
||||
case of the last key to be set, and ``iter(instance)``,
|
||||
``keys()``, ``items()``, ``iterkeys()``, and ``iteritems()``
|
||||
will contain case-sensitive keys. However, querying and contains
|
||||
testing is case insensitive::
|
||||
cid = CaseInsensitiveDict()
|
||||
cid['Accept'] = 'application/json'
|
||||
cid['aCCEPT'] == 'application/json' # True
|
||||
list(cid) == ['Accept'] # True
|
||||
For example, ``headers['content-encoding']`` will return the
|
||||
value of a ``'Content-Encoding'`` response header, regardless
|
||||
of how the header name was originally stored.
|
||||
If the constructor, ``.update``, or equality comparison
|
||||
operations are given keys that have equal ``.lower()``s, the
|
||||
behavior is undefined.
|
||||
"""
|
||||
def __init__(self, data=None, **kwargs):
|
||||
self._store = dict()
|
||||
if data is None:
|
||||
data = {}
|
||||
self.update(data, **kwargs)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
# Use the lowercased key for lookups, but store the actual
|
||||
# key alongside the value.
|
||||
self._store[key.lower()] = (key, value)
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self._store[key.lower()][1]
|
||||
|
||||
def __delitem__(self, key):
|
||||
del self._store[key.lower()]
|
||||
|
||||
def __iter__(self):
|
||||
return (casedkey for casedkey, mappedvalue in self._store.values())
|
||||
|
||||
def __len__(self):
|
||||
return len(self._store)
|
||||
|
||||
def lower_items(self):
|
||||
"""Like iteritems(), but with all lowercase keys."""
|
||||
return (
|
||||
(lowerkey, keyval[1])
|
||||
for (lowerkey, keyval)
|
||||
in self._store.items()
|
||||
)
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, collections.Mapping):
|
||||
other = CaseInsensitiveDict(other)
|
||||
else:
|
||||
return NotImplemented
|
||||
# Compare insensitively
|
||||
return dict(self.lower_items()) == dict(other.lower_items())
|
||||
|
||||
# Copy is required
|
||||
def copy(self):
|
||||
return CaseInsensitiveDict(self._store.values())
|
||||
|
||||
def __repr__(self):
|
||||
return str(dict(self.items()))
|
||||
|
||||
def partition_dict(predicate, dictionary):
|
||||
true_dict = {}
|
||||
false_dict = {}
|
||||
@@ -14,3 +85,8 @@ def compose(*functions):
|
||||
res = function(res)
|
||||
return res
|
||||
return composed
|
||||
|
||||
def read_body(request):
|
||||
if hasattr(request.body, 'read'):
|
||||
return request.body.read()
|
||||
return request.body
|
||||
|
||||
Reference in New Issue
Block a user