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

Compare commits

..

32 Commits

Author SHA1 Message Date
Kevin McCarthy
9c46831a8e release 4.2.0 2022-06-29 16:53:45 -05:00
dependabot[bot]
fe596447ec build(deps): bump actions/setup-python from 3 to 4
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 3 to 4.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v3...v4)

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

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

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

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

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

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

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

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

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-03 08:09:20 -03:00
Jair Henrique
5b2fc2712e Change ci badge to github actions 2021-11-02 14:57:19 -10:00
Jair Henrique
c596a160b3 Move from travis to github actions 2021-11-02 10:31:00 -10:00
Ivan Malison
e68aa84649 Merge pull request #602 from scop/spelling
Spelling fixes
2021-08-16 04:04:29 +00:00
Ville Skyttä
678d56f608 Spelling fixes 2021-08-16 06:56:36 +03:00
Ivan Malison
d4927627c3 Merge pull request #582 from scop/fix/filter-dict-post-data-parameters
fix(filters): make work with dict body parameters, such as aiohttp
2021-08-15 20:22:31 +00:00
Ivan Malison
61b83aca7f Merge pull request #554 from AthulMuralidhar/patch-1
Change urllib2 to ulrlib in accordance with Py 3.5
2021-08-15 20:17:23 +00:00
Ivan Malison
0ac66f4413 Merge pull request #564 from jairhenrique/drop-35
Drop support to deprecated Python (<3.6)
2021-08-15 20:16:21 +00:00
Ville Skyttä
000f7448a7 fix(filters): make work with dict body parameters, such as aiohttp
Closes https://github.com/kevin1024/vcrpy/issues/398
2021-04-15 22:40:41 +03:00
Jair Henrique
08ef4a8bc4 Drop support to deprecated Python (<3.6) 2020-10-06 10:17:07 -03:00
Athul Muralidhar
dda16ef1e5 Change urllib2 to ulrlib
the change is required because the urllib2 is now moved to urllib in python 3.5
2020-08-19 11:31:59 +02:00
30 changed files with 329 additions and 156 deletions

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

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

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

@@ -0,0 +1,40 @@
name: Test
on:
push:
branches:
- master
pull_request:
branches:
- "*"
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.7", "3.8", "3.9", "3.10", "pypy-3.8"]
steps:
- name: Install libgnutls28-dev
run: |
sudo apt update -q
sudo apt install -q -y libgnutls28-dev libcurl4-gnutls-dev
- uses: actions/checkout@v3.0.2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install project dependencies
run: |
pip install --upgrade pip
pip install codecov tox tox-gh-actions
- name: Run tests with tox
run: tox
- name: Run coverage
run: codecov

View File

@@ -1,26 +0,0 @@
dist: xenial
language: python
matrix:
include:
# Only run lint on a single 3.x
- env: TOX_SUFFIX="lint"
python: "3.7"
python:
- "3.5"
- "3.6"
- "3.7"
- "3.8"
- "pypy3"
before_install:
- openssl version
- sudo apt-get install libgnutls28-dev
install:
- pip install tox-travis codecov
- if [[ $TOX_SUFFIX != 'lint' ]]; then python setup.py install ; fi
script:
- tox
after_success:
- codecov

View File

@@ -62,8 +62,8 @@ more details
:target: https://pypi.python.org/pypi/vcrpy
.. |Python versions| image:: https://img.shields.io/pypi/pyversions/vcrpy.svg
:target: https://pypi.python.org/pypi/vcrpy
.. |Build Status| image:: https://secure.travis-ci.org/kevin1024/vcrpy.svg?branch=master
:target: http://travis-ci.org/kevin1024/vcrpy
.. |Build Status| image:: https://github.com/kevin1024/vcrpy/actions/workflows/main.yml/badge.svg
:target: https://github.com/kevin1024/vcrpy/actions
.. |Gitter| image:: https://badges.gitter.im/Join%20Chat.svg
:alt: Join the chat at https://gitter.im/kevin1024/vcrpy
:target: https://gitter.im/kevin1024/vcrpy?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge

View File

@@ -271,7 +271,7 @@ You can also do response filtering with the
similar to the above ``before_record_request`` - you can
mutate the response, or return ``None`` to avoid recording
the request and response altogether. For example to hide
sensitive data from the request body:
sensitive data from the response body:
.. code:: python

View File

@@ -1,12 +1,21 @@
Changelog
---------
For a full list of triaged issues, bugs and PRs and what release they are targetted for please see the following link.
For a full list of triaged issues, bugs and PRs and what release they are targeted for please see the following link.
`ROADMAP MILESTONES <https://github.com/kevin1024/vcrpy/milestones>`_
All help in providing PRs to close out bug issues is appreciated. Even if that is providing a repo that fully replicates issues. We have very generous contributors that have added these to bug issues which meant another contributor picked up the bug and closed it out.
- 4.2.0
- Drop support for python < 3.7, thanks @jairhenrique, @IvanMalison, @AthulMuralidhar
- Various aiohtt bigfixes (thanks @pauloromeira and boechat107)
- Bugfix: filter_post_data_parameters not working with aiohttp. Thank you @vprakashplanview, @scop, @jairhenrique, and @cinemascop89
- Bugfix: Some random misspellings (thanks @scop)
- Migrate the CI suite to Github Actions from Travis (thanks @jairhenrique and @cclauss)
- Various documentation and code misspelling fixes (thanks @scop and @Justintime50)
- Bugfix: httpx support (select between allow_redirects/follow_redirects) (thanks @immerrr)
- Bugfix: httpx support (select between allow_redirects/follow_redirects) (thanks @immerrr)
- 4.1.1
- Fix HTTPX support for versions greater than 0.15 (thanks @jairhenrique)
- Include a trailing newline on json cassettes (thanks @AaronRobson)
@@ -100,7 +109,7 @@ All help in providing PRs to close out bug issues is appreciated. Even if that i
- decode_compressed_response option and filter (thanks @jayvdb).
- 1.7.4 [#217]
- Make use_cassette decorated functions actually return a value (thanks @bcen).
- [#199] Fix path transfromation defaults.
- [#199] Fix path transformation defaults.
- Better headers dictionary management.
- 1.7.3 [#188]
- ``additional_matchers`` kwarg on ``use_cassette``.
@@ -203,7 +212,7 @@ All help in providing PRs to close out bug issues is appreciated. Even if that i
- 0.3.4
- Bugfix: close file before renaming it. This fixes an issue on Windows. Thanks @smallcode for the fix.
- 0.3.3
- Bugfix for error message when an unreigstered custom matcher was used
- Bugfix for error message when an unregistered custom matcher was used
- 0.3.2
- Fix issue with new config syntax and the ``match_on`` parameter. Thanks, @chromy!
- 0.3.1

View File

@@ -96,11 +96,11 @@ The test suite is pretty big and slow, but you can tell tox to only run specific
tox -e {pyNN}-{HTTP_LIBRARY} -- <pytest flags passed through>
tox -e py36-requests -- -v -k "'test_status_code or test_gzip'"
tox -e py37-requests -- -v -k "'test_status_code or test_gzip'"
tox -e py37-requests -- -v --last-failed
This will run only tests that look like ``test_status_code`` or
``test_gzip`` in the test suite, and only in the python 3.6 environment
``test_gzip`` in the test suite, and only in the python 3.7 environment
that has ``requests`` installed.
Also, in order for the boto tests to run, you will need an AWS key.
@@ -130,10 +130,10 @@ in this example::
pip install tox tox-pyenv
# Install supported versions (at time of writing), this does not activate them
pyenv install 3.5.9 3.6.9 3.7.5 3.8.0 pypy3.6-7.2.0
pyenv install 3.7.5 3.8.0 pypy3.8
# This activates them
pyenv local 3.5.9 3.6.9 3.7.5 3.8.0 pypy3.6-7.2.0
pyenv local 3.7.5 3.8.0 pypy3.8
# Run the whole test suite
tox

View File

@@ -9,7 +9,7 @@ with pip::
Compatibility
-------------
VCR.py supports Python 3.5+, and `pypy <http://pypy.org>`__.
VCR.py supports Python 3.7+, and `pypy <http://pypy.org>`__.
The following HTTP libraries are supported:

View File

@@ -4,10 +4,10 @@ Usage
.. code:: python
import vcr
import urllib2
import urllib
with vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml'):
response = urllib2.urlopen('http://www.iana.org/domains/reserved').read()
response = urllib.request.urlopen('http://www.iana.org/domains/reserved').read()
assert 'Example domains' in response
Run this test once, and VCR.py will record the HTTP request to
@@ -25,7 +25,7 @@ look like this:
@vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml')
def test_iana():
response = urllib2.urlopen('http://www.iana.org/domains/reserved').read()
response = urllib.request.urlopen('http://www.iana.org/domains/reserved').read()
assert 'Example domains' in response
When using the decorator version of ``use_cassette``, it is possible to
@@ -35,7 +35,7 @@ omit the path to the cassette file.
@vcr.use_cassette()
def test_iana():
response = urllib2.urlopen('http://www.iana.org/domains/reserved').read()
response = urllib.request.urlopen('http://www.iana.org/domains/reserved').read()
assert 'Example domains' in response
In this case, the cassette file will be given the same name as the test

View File

@@ -46,8 +46,7 @@ install_requires = [
"PyYAML",
"wrapt",
"six>=1.5",
'yarl; python_version>="3.6"',
'yarl<1.4; python_version=="3.5"',
"yarl",
]
setup(
@@ -60,7 +59,7 @@ setup(
author_email="me@kevinmccarthy.org",
url="https://github.com/kevin1024/vcrpy",
packages=find_packages(exclude=["tests*"]),
python_requires=">=3.5",
python_requires=">=3.7",
install_requires=install_requires,
license="MIT",
tests_require=["pytest", "mock", "pytest-httpbin"],
@@ -70,10 +69,10 @@ setup(
"Intended Audience :: Developers",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",

View File

@@ -152,12 +152,13 @@ def test_post(tmpdir, scheme, body, caplog):
def test_params(tmpdir, scheme):
url = scheme + "://httpbin.org/get"
url = scheme + "://httpbin.org/get?d=d"
headers = {"Content-Type": "application/json"}
params = {"a": 1, "b": False, "c": "c"}
params = {"a": 1, "b": 2, "c": "c"}
with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
_, response_json = get(url, output="json", params=params, headers=headers)
assert response_json["args"] == {"a": "1", "b": "2", "c": "c", "d": "d"}
with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
_, cassette_response_json = get(url, output="json", params=params, headers=headers)
@@ -168,7 +169,7 @@ def test_params(tmpdir, scheme):
def test_params_same_url_distinct_params(tmpdir, scheme):
url = scheme + "://httpbin.org/get"
headers = {"Content-Type": "application/json"}
params = {"a": 1, "b": False, "c": "c"}
params = {"a": 1, "b": 2, "c": "c"}
with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
_, response_json = get(url, output="json", params=params, headers=headers)
@@ -399,3 +400,19 @@ def test_cookies_redirect(scheme, tmpdir):
assert cookies["Cookie_1"].value == "Val_1"
run_in_loop(run)
def test_not_allow_redirects(tmpdir):
url = "https://mockbin.org/redirect/308/5"
path = str(tmpdir.join("redirects.yaml"))
with vcr.use_cassette(path):
response, _ = get(url, allow_redirects=False)
assert response.url.path == "/redirect/308/5"
assert response.status == 308
with vcr.use_cassette(path) as cassette:
response, _ = get(url, allow_redirects=False)
assert response.url.path == "/redirect/308/5"
assert response.status == 308
assert cassette.play_count == 1

View File

@@ -11,7 +11,7 @@ import vcr
def test_nonexistent_directory(tmpdir, httpbin):
"""If we load a cassette in a nonexistent directory, it can save ok"""
# Check to make sure directory doesnt exist
# Check to make sure directory doesn't exist
assert not os.path.exists(str(tmpdir.join("nonexistent")))
# Run VCR to create dir and cassette file

View File

@@ -61,13 +61,14 @@ def test_response_headers(tmpdir, httpbin_both):
assert set(headers) == set(resp.items())
def test_effective_url(tmpdir, httpbin_both):
def test_effective_url(tmpdir):
"""Ensure that the effective_url is captured"""
url = httpbin_both.url + "/redirect-to?url=/html"
url = "http://mockbin.org/redirect/301"
with vcr.use_cassette(str(tmpdir.join("headers.yaml"))):
resp, _ = http().request(url)
effective_url = resp["content-location"]
assert effective_url == httpbin_both + "/html"
assert effective_url == "http://mockbin.org/redirect/301/0"
with vcr.use_cassette(str(tmpdir.join("headers.yaml"))):
resp, _ = http().request(url)

View File

@@ -1,43 +1,79 @@
import pytest
import contextlib
import os
asyncio = pytest.importorskip("asyncio")
httpx = pytest.importorskip("httpx")
import vcr # noqa: E402
from vcr.stubs.httpx_stubs import HTTPX_REDIRECT_PARAM # noqa: E402
class BaseDoRequest:
_client_class = None
def __init__(self, *args, **kwargs):
self._client = self._client_class(*args, **kwargs)
self._client_args = args
self._client_kwargs = kwargs
def _make_client(self):
return self._client_class(*self._client_args, **self._client_kwargs)
class DoSyncRequest(BaseDoRequest):
_client_class = httpx.Client
def __enter__(self):
return self
def __exit__(self, *args):
pass
@property
def client(self):
try:
return self._client
except AttributeError:
self._client = self._make_client()
return self._client
def __call__(self, *args, **kwargs):
return self._client.request(*args, timeout=60, **kwargs)
return self.client.request(*args, timeout=60, **kwargs)
class DoAsyncRequest(BaseDoRequest):
_client_class = httpx.AsyncClient
@staticmethod
def run_in_loop(coroutine):
with contextlib.closing(asyncio.new_event_loop()) as loop:
asyncio.set_event_loop(loop)
task = loop.create_task(coroutine)
return loop.run_until_complete(task)
def __enter__(self):
# Need to manage both loop and client, because client's implementation
# will fail if the loop is closed before the client's end of life.
self._loop = asyncio.new_event_loop()
asyncio.set_event_loop(self._loop)
self._client = self._make_client()
self._loop.run_until_complete(self._client.__aenter__())
return self
def __exit__(self, *args):
try:
self._loop.run_until_complete(self._client.__aexit__(*args))
finally:
del self._client
self._loop.close()
del self._loop
@property
def client(self):
try:
return self._client
except AttributeError:
raise ValueError('To access async client, use "with do_request() as client"')
def __call__(self, *args, **kwargs):
async def _request():
async with self._client as c:
return await c.request(*args, **kwargs)
if hasattr(self, "_loop"):
return self._loop.run_until_complete(self.client.request(*args, **kwargs))
return DoAsyncRequest.run_in_loop(_request())
# Use one-time context and dispose of the loop/client afterwards
with self:
return self(*args, **kwargs)
def pytest_generate_tests(metafunc):
@@ -122,12 +158,14 @@ def test_params_same_url_distinct_params(tmpdir, scheme, do_request):
def test_redirect(tmpdir, do_request, yml):
url = "https://mockbin.org/redirect/303/2"
response = do_request()("GET", url)
redirect_kwargs = {HTTPX_REDIRECT_PARAM.name: True}
response = do_request()("GET", url, **redirect_kwargs)
with vcr.use_cassette(yml):
response = do_request()("GET", url)
response = do_request()("GET", url, **redirect_kwargs)
with vcr.use_cassette(yml) as cassette:
cassette_response = do_request()("GET", url)
cassette_response = do_request()("GET", url, **redirect_kwargs)
assert cassette_response.status_code == response.status_code
assert len(cassette_response.history) == len(response.history)
@@ -173,7 +211,7 @@ def test_behind_proxy(do_request):
)
url = "https://httpbin.org/headers"
proxy = "http://localhost:8080"
proxies = {"http": proxy, "https": proxy}
proxies = {"http://": proxy, "https://": proxy}
with vcr.use_cassette(yml):
response = do_request(proxies=proxies, verify=False)("GET", url)
@@ -189,48 +227,52 @@ def test_behind_proxy(do_request):
def test_cookies(tmpdir, scheme, do_request):
def client_cookies(client):
return [c for c in client._client.cookies]
return [c for c in client.client.cookies]
def response_cookies(response):
return [c for c in response.cookies]
client = do_request()
assert client_cookies(client) == []
with do_request() as client:
assert client_cookies(client) == []
url = scheme + "://httpbin.org"
testfile = str(tmpdir.join("cookies.yml"))
with vcr.use_cassette(testfile):
r1 = client("GET", url + "/cookies/set?k1=v1&k2=v2")
assert response_cookies(r1.history[0]) == ["k1", "k2"]
assert response_cookies(r1) == []
redirect_kwargs = {HTTPX_REDIRECT_PARAM.name: True}
r2 = client("GET", url + "/cookies")
assert len(r2.json()["cookies"]) == 2
url = scheme + "://httpbin.org"
testfile = str(tmpdir.join("cookies.yml"))
with vcr.use_cassette(testfile):
r1 = client("GET", url + "/cookies/set?k1=v1&k2=v2", **redirect_kwargs)
assert response_cookies(r1.history[0]) == ["k1", "k2"]
assert response_cookies(r1) == []
assert client_cookies(client) == ["k1", "k2"]
r2 = client("GET", url + "/cookies", **redirect_kwargs)
assert len(r2.json()["cookies"]) == 2
new_client = do_request()
assert client_cookies(new_client) == []
assert client_cookies(client) == ["k1", "k2"]
with vcr.use_cassette(testfile) as cassette:
cassette_response = new_client("GET", url + "/cookies/set?k1=v1&k2=v2")
assert response_cookies(cassette_response.history[0]) == ["k1", "k2"]
assert response_cookies(cassette_response) == []
with do_request() as new_client:
assert client_cookies(new_client) == []
assert cassette.play_count == 2
assert client_cookies(new_client) == ["k1", "k2"]
with vcr.use_cassette(testfile) as cassette:
cassette_response = new_client("GET", url + "/cookies/set?k1=v1&k2=v2")
assert response_cookies(cassette_response.history[0]) == ["k1", "k2"]
assert response_cookies(cassette_response) == []
assert cassette.play_count == 2
assert client_cookies(new_client) == ["k1", "k2"]
def test_relative_redirects(tmpdir, scheme, do_request):
redirect_kwargs = {HTTPX_REDIRECT_PARAM.name: True}
url = scheme + "://mockbin.com/redirect/301?to=/redirect/301?to=/request"
testfile = str(tmpdir.join("relative_redirects.yml"))
with vcr.use_cassette(testfile):
response = do_request()("GET", url)
response = do_request()("GET", url, **redirect_kwargs)
assert len(response.history) == 2, response
assert response.json()["url"].endswith("request")
with vcr.use_cassette(testfile) as cassette:
response = do_request()("GET", url)
response = do_request()("GET", url, **redirect_kwargs)
assert len(response.history) == 2
assert response.json()["url"].endswith("request")
@@ -240,14 +282,16 @@ def test_relative_redirects(tmpdir, scheme, do_request):
def test_redirect_wo_allow_redirects(do_request, yml):
url = "https://mockbin.org/redirect/308/5"
redirect_kwargs = {HTTPX_REDIRECT_PARAM.name: False}
with vcr.use_cassette(yml):
response = do_request()("GET", url, allow_redirects=False)
response = do_request()("GET", url, **redirect_kwargs)
assert str(response.url).endswith("308/5")
assert response.status_code == 308
with vcr.use_cassette(yml) as cassette:
response = do_request()("GET", url, allow_redirects=False)
response = do_request()("GET", url, **redirect_kwargs)
assert str(response.url).endswith("308/5")
assert response.status_code == 308

View File

@@ -30,7 +30,7 @@ def test_save_cassette_with_custom_persister(tmpdir, httpbin):
my_vcr = vcr.VCR()
my_vcr.register_persister(CustomFilesystemPersister)
# Check to make sure directory doesnt exist
# Check to make sure directory doesn't exist
assert not os.path.exists(str(tmpdir.join("nonexistent")))
# Run VCR to create dir and cassette file using new save_cassette callback

View File

@@ -1,8 +1,5 @@
# -*- coding: utf-8 -*-
"""Test requests' interaction with vcr"""
import platform
import pytest
import sys
import vcr
from assertions import assert_cassette_empty, assert_is_json
@@ -117,10 +114,6 @@ def test_post_chunked_binary(tmpdir, httpbin):
@pytest.mark.skipif("sys.version_info >= (3, 6)", strict=True, raises=ConnectionError)
@pytest.mark.skipif(
(3, 5) < sys.version_info < (3, 6) and platform.python_implementation() == "CPython",
reason="Fails on CPython 3.5",
)
def test_post_chunked_binary_secure(tmpdir, httpbin_secure):
"""Ensure that we can send chunked binary without breaking while trying to concatenate bytes with str."""
data1 = iter([b"data", b"to", b"send"])

View File

@@ -56,12 +56,13 @@ def test_response_headers(httpbin_both, tmpdir):
assert sorted(open1) == sorted(open2)
def test_effective_url(httpbin_both, tmpdir):
def test_effective_url(tmpdir):
"""Ensure that the effective_url is captured"""
url = httpbin_both.url + "/redirect-to?url=/html"
url = "http://mockbin.org/redirect/301"
with vcr.use_cassette(str(tmpdir.join("headers.yaml"))):
effective_url = urlopen_with_cafile(url).geturl()
assert effective_url == httpbin_both.url + "/html"
assert effective_url == "http://mockbin.org/redirect/301/0"
with vcr.use_cassette(str(tmpdir.join("headers.yaml"))):
assert effective_url == urlopen_with_cafile(url).geturl()

View File

@@ -94,9 +94,10 @@ def test_post(tmpdir, httpbin_both, verify_pool_mgr):
assert req1 == req2
def test_redirects(tmpdir, httpbin_both, verify_pool_mgr):
def test_redirects(tmpdir, verify_pool_mgr):
"""Ensure that we can handle redirects"""
url = httpbin_both.url + "/redirect-to?url=bytes/1024"
url = "http://mockbin.org/redirect/301"
with vcr.use_cassette(str(tmpdir.join("verify_pool_mgr.yaml"))):
content = verify_pool_mgr.request("GET", url).data
@@ -104,8 +105,9 @@ def test_redirects(tmpdir, httpbin_both, verify_pool_mgr):
assert content == verify_pool_mgr.request("GET", url).data
# Ensure that we've now cached *two* responses. One for the redirect
# and one for the final fetch
assert len(cass) == 2
assert cass.play_count == 2
assert len(cass) == 2
assert cass.play_count == 2
def test_cross_scheme(tmpdir, httpbin, httpbin_secure, verify_pool_mgr):

View File

@@ -208,7 +208,7 @@ def test_nesting_cassette_context_managers(*args):
)
assert_get_response_body_is("first_response")
# Make sure a second cassette can supercede the first
# Make sure a second cassette can supersede the first
with Cassette.use(path="test") as second_cassette:
with mock.patch.object(second_cassette, "play_response", return_value=second_response):
assert_get_response_body_is("second_response")
@@ -310,16 +310,16 @@ def test_func_path_generator():
def test_use_as_decorator_on_coroutine():
original_http_connetion = httplib.HTTPConnection
original_http_connection = httplib.HTTPConnection
@Cassette.use(inject=True)
def test_function(cassette):
assert httplib.HTTPConnection.cassette is cassette
assert httplib.HTTPConnection is not original_http_connetion
assert httplib.HTTPConnection is not original_http_connection
value = yield 1
assert value == 1
assert httplib.HTTPConnection.cassette is cassette
assert httplib.HTTPConnection is not original_http_connetion
assert httplib.HTTPConnection is not original_http_connection
value = yield 2
assert value == 2
@@ -333,15 +333,15 @@ def test_use_as_decorator_on_coroutine():
def test_use_as_decorator_on_generator():
original_http_connetion = httplib.HTTPConnection
original_http_connection = httplib.HTTPConnection
@Cassette.use(inject=True)
def test_function(cassette):
assert httplib.HTTPConnection.cassette is cassette
assert httplib.HTTPConnection is not original_http_connetion
assert httplib.HTTPConnection is not original_http_connection
yield 1
assert httplib.HTTPConnection.cassette is cassette
assert httplib.HTTPConnection is not original_http_connetion
assert httplib.HTTPConnection is not original_http_connection
yield 2
assert list(test_function()) == [1, 2]

View File

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

View File

@@ -44,4 +44,4 @@ def test_try_migrate_with_invalid_or_new_cassettes(tmpdir):
for file_path in files:
shutil.copy(file_path, cassette)
assert not vcr.migration.try_migrate(cassette)
assert filecmp.cmp(cassette, file_path) # shold not change file
assert filecmp.cmp(cassette, file_path) # should not change file

View File

@@ -6,7 +6,7 @@ from vcr.cassette import Cassette
class TestVCRConnection:
def test_setting_of_attributes_get_propogated_to_real_connection(self):
def test_setting_of_attributes_get_propagated_to_real_connection(self):
vcr_connection = VCRHTTPSConnection("www.examplehost.com")
vcr_connection.ssl_version = "example_ssl_version"
assert vcr_connection.real_connection.ssl_version == "example_ssl_version"

View File

@@ -31,7 +31,7 @@ def test_vcr_use_cassette():
function()
assert mock_cassette_load.call_args[1]["record_mode"] == test_vcr.record_mode
# Ensure that explicitly provided arguments still supercede
# Ensure that explicitly provided arguments still supersede
# those on the vcr.
new_record_mode = mock.Mock()
@@ -226,7 +226,7 @@ def test_with_current_defaults():
def test_cassette_library_dir_with_decoration_and_no_explicit_path():
library_dir = "/libary_dir"
library_dir = "/library_dir"
vcr = VCR(inject_cassette=True, cassette_library_dir=library_dir)
@vcr.use_cassette()
@@ -237,7 +237,7 @@ def test_cassette_library_dir_with_decoration_and_no_explicit_path():
def test_cassette_library_dir_with_decoration_and_explicit_path():
library_dir = "/libary_dir"
library_dir = "/library_dir"
vcr = VCR(inject_cassette=True, cassette_library_dir=library_dir)
@vcr.use_cassette(path="custom_name")
@@ -248,7 +248,7 @@ def test_cassette_library_dir_with_decoration_and_explicit_path():
def test_cassette_library_dir_with_decoration_and_super_explicit_path():
library_dir = "/libary_dir"
library_dir = "/library_dir"
vcr = VCR(inject_cassette=True, cassette_library_dir=library_dir)
@vcr.use_cassette(path=os.path.join(library_dir, "custom_name"))
@@ -259,7 +259,7 @@ def test_cassette_library_dir_with_decoration_and_super_explicit_path():
def test_cassette_library_dir_with_path_transformer():
library_dir = "/libary_dir"
library_dir = "/library_dir"
vcr = VCR(
inject_cassette=True, cassette_library_dir=library_dir, path_transformer=lambda path: path + ".json"
)

32
tox.ini
View File

@@ -3,12 +3,20 @@ skip_missing_interpreters=true
envlist =
cov-clean,
lint,
{py35,py36,py37,py38}-{requests,httplib2,urllib3,tornado4,boto3,aiohttp},
{py36,py37,py38}-{httpx}
{py37,py38,py39,py310}-{requests,httplib2,urllib3,tornado4,boto3,aiohttp,httpx},
{pypy3}-{requests,httplib2,urllib3,tornado4,boto3},
{py310}-httpx019,
cov-report
[gh-actions]
python =
3.7: py37, lint
3.8: py38
3.9: py39
3.10: py310
pypy-3: pypy3
# Coverage environment tasks: cov-clean and cov-report
# https://pytest-cov.readthedocs.io/en/latest/tox.html
[testenv:cov-clean]
@@ -34,6 +42,7 @@ commands =
deps =
flake8
black
basepython = python3.7
[testenv:docs]
# Running sphinx from inside the "docs" directory
@@ -63,29 +72,30 @@ usedevelop=true
commands =
./runtests.sh --cov=./vcr --cov-branch --cov-report=xml --cov-append {posargs}
deps =
Flask
Werkzeug==2.0.3
pytest
pytest-httpbin
git+https://github.com/immerrr/pytest-httpbin@fix-redirect-location-scheme-for-secure-server
pytest-cov
PyYAML
ipaddress
requests: requests>=2.22.0
httplib2: httplib2
urllib3: urllib3
{py35,py36}-tornado4: tornado>=4,<5
{py35,py36}-tornado4: pytest-tornado
{py35,py36}-tornado4: pycurl
boto3: boto3
boto3: urllib3
aiohttp: aiohttp
aiohttp: pytest-asyncio
aiohttp: pytest-aiohttp
httpx: httpx
{py36,py37,py38}-{httpx}: httpx
{py36,py37,py38}-{httpx}: pytest-asyncio
{py37,py38,py39,py310}-{httpx}: httpx
{py37,py38,py39,py310}-{httpx}: pytest-asyncio
httpx: httpx>0.19
# httpx==0.19 is the latest version that supports allow_redirects, newer versions use follow_redirects
httpx019: httpx==0.19
{py37,py38,py39,py310}-{httpx}: pytest-asyncio
depends =
lint,{py35,py36,py37,py38,pypy3}-{requests,httplib2,urllib3,tornado4,boto3},{py35,py36,py37,py38}-{aiohttp},{py36,py37,py38}-{httpx}: cov-clean
cov-report: lint,{py35,py36,py37,py38,pypy3}-{requests,httplib2,urllib3,tornado4,boto3},{py35,py36,py37,py38}-{aiohttp}
lint,{py37,py38,py39,py310,pypy3}-{requests,httplib2,urllib3,tornado4,boto3},{py37,py38,py39,py310}-{aiohttp},{py37,py38,py39,py310}-{httpx}: cov-clean
cov-report: lint,{py37,py38,py39,py310,pypy3}-{requests,httplib2,urllib3,tornado4,boto3},{py37,py38,py39,py310}-{aiohttp}
passenv =
AWS_ACCESS_KEY_ID
AWS_DEFAULT_REGION

View File

@@ -3,7 +3,7 @@ from .config import VCR
from logging import NullHandler
from .record_mode import RecordMode as mode # noqa import is not used in this file
__version__ = "4.1.1"
__version__ = "4.2.0"
logging.getLogger(__name__).addHandler(NullHandler())

View File

@@ -84,7 +84,17 @@ def replace_post_data_parameters(request, replacements):
replacements = dict(replacements)
if request.method == "POST" and not isinstance(request.body, BytesIO):
if request.headers.get("Content-Type") == "application/json":
if isinstance(request.body, dict):
new_body = request.body.copy()
for k, rv in replacements.items():
if k in new_body:
ov = new_body.pop(k)
if callable(rv):
rv = rv(key=k, value=ov, request=request)
if rv is not None:
new_body[k] = rv
request.body = new_body
elif request.headers.get("Content-Type") == "application/json":
json_data = json.loads(request.body.decode("utf-8"))
for k, rv in replacements.items():
if k in json_data:

View File

@@ -3,7 +3,7 @@ from enum import Enum
class RecordMode(str, Enum):
"""
Configues when VCR will record to the cassette.
Configures when VCR will record to the cassette.
Can be declared by either using the enumerated value (`vcr.mode.ONCE`)
or by simply using the defined string (`once`).

View File

@@ -314,7 +314,7 @@ class VCRConnection:
def __setattr__(self, name, value):
"""
We need to define this because any attributes that are set on the
VCRConnection need to be propogated to the real connection.
VCRConnection need to be propagated to the real connection.
For example, urllib3 will set certain attributes on the connection,
such as 'ssl_version'. These attributes need to get set on the real

View File

@@ -8,7 +8,8 @@ from aiohttp import ClientConnectionError, ClientResponse, RequestInfo, streams
from aiohttp import hdrs, CookieJar
from http.cookies import CookieError, Morsel, SimpleCookie
from aiohttp.helpers import strip_auth_from_url
from multidict import CIMultiDict, CIMultiDictProxy
from multidict import CIMultiDict, CIMultiDictProxy, MultiDict
from typing import Union, Mapping
from yarl import URL
from vcr.errors import CannotOverwriteExistingCassetteException
@@ -116,14 +117,15 @@ def _deserialize_headers(headers):
return CIMultiDictProxy(deserialized_headers)
def play_responses(cassette, vcr_request):
def play_responses(cassette, vcr_request, kwargs):
history = []
allow_redirects = kwargs.get("allow_redirects", True)
vcr_response = cassette.play_response(vcr_request)
response = build_response(vcr_request, vcr_response, history)
# If we're following redirects, continue playing until we reach
# our final destination.
while 300 <= response.status <= 399:
while allow_redirects and 300 <= response.status <= 399:
if "location" not in response.headers:
break
@@ -228,6 +230,16 @@ def _build_cookie_header(session, cookies, cookie_header, url):
return c.output(header="", sep=";").strip()
def _build_url_with_params(url_str: str, params: Mapping[str, Union[str, int, float]]) -> URL:
# This code is basically a copy&paste of aiohttp.
# https://github.com/aio-libs/aiohttp/blob/master/aiohttp/client_reqrep.py#L225
url = URL(url_str)
q = MultiDict(url.query)
url2 = url.with_query(params)
q.extend(url2.query)
return url.with_query(q)
def vcr_request(cassette, real_request):
@functools.wraps(real_request)
async def new_request(self, method, url, **kwargs):
@@ -241,12 +253,7 @@ def vcr_request(cassette, real_request):
if auth is not None:
headers["AUTHORIZATION"] = auth.encode()
request_url = URL(url)
if params:
for k, v in params.items():
params[k] = str(v)
request_url = URL(url).with_query(params)
request_url = URL(url) if not params else _build_url_with_params(url, params)
c_header = headers.pop(hdrs.COOKIE, None)
cookie_header = _build_cookie_header(self, cookies, c_header, request_url)
if cookie_header:
@@ -256,7 +263,7 @@ def vcr_request(cassette, real_request):
if cassette.can_play_response_for(vcr_request):
log.info("Playing response for {} from cassette".format(vcr_request))
response = play_responses(cassette, vcr_request)
response = play_responses(cassette, vcr_request, kwargs)
for redirect in response.history:
self._cookie_jar.update_cookies(redirect.cookies, redirect.url)
self._cookie_jar.update_cookies(response.cookies, response.url)

View File

@@ -5,31 +5,39 @@ from unittest.mock import patch, MagicMock
import httpx
from vcr.request import Request as VcrRequest
from vcr.errors import CannotOverwriteExistingCassetteException
import inspect
_httpx_signature = inspect.signature(httpx.Client.request)
try:
HTTPX_REDIRECT_PARAM = _httpx_signature.parameters["follow_redirects"]
except KeyError:
HTTPX_REDIRECT_PARAM = _httpx_signature.parameters["allow_redirects"]
_logger = logging.getLogger(__name__)
def _transform_headers(httpx_reponse):
def _transform_headers(httpx_response):
"""
Some headers can appear multiple times, like "Set-Cookie".
Therefore transform to every header key to list of values.
"""
out = {}
for key, var in httpx_reponse.headers.raw:
for key, var in httpx_response.headers.raw:
decoded_key = key.decode("utf-8")
out.setdefault(decoded_key, [])
out[decoded_key].append(var.decode("utf-8"))
return out
def _to_serialized_response(httpx_reponse):
def _to_serialized_response(httpx_response):
return {
"status_code": httpx_reponse.status_code,
"http_version": httpx_reponse.http_version,
"headers": _transform_headers(httpx_reponse),
"content": httpx_reponse.content.decode("utf-8", "ignore"),
"status_code": httpx_response.status_code,
"http_version": httpx_response.http_version,
"headers": _transform_headers(httpx_response),
"content": httpx_response.content.decode("utf-8", "ignore"),
}
@@ -98,7 +106,11 @@ def _record_responses(cassette, vcr_request, real_response):
def _play_responses(cassette, request, vcr_request, client, kwargs):
history = []
allow_redirects = kwargs.get("allow_redirects", True)
allow_redirects = kwargs.get(
HTTPX_REDIRECT_PARAM.name,
HTTPX_REDIRECT_PARAM.default,
)
vcr_response = cassette.play_response(vcr_request)
response = _from_serialized_response(request, vcr_response)