mirror of
https://github.com/kevin1024/vcrpy.git
synced 2025-12-08 16:53:23 +00:00
Compare commits
358 Commits
v4.1.0
...
d5ba702a1b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d5ba702a1b | ||
|
|
952994b365 | ||
|
|
e2f3240835 | ||
|
|
bb690833bc | ||
|
|
73eed94c47 | ||
|
|
a23fe0333a | ||
|
|
bb743861b6 | ||
|
|
ac70eaa17f | ||
|
|
d50f3385a6 | ||
|
|
14db4de224 | ||
|
|
2c4df79498 | ||
|
|
1456673cb4 | ||
|
|
19bd4e012c | ||
|
|
558c7fc625 | ||
|
|
8217a4c21b | ||
|
|
bd0aa59cd2 | ||
|
|
9a37817a3a | ||
|
|
b4c65bd677 | ||
|
|
93bc59508c | ||
|
|
e313a9cd52 | ||
|
|
5f1b20c4ca | ||
|
|
cd31d71901 | ||
|
|
4607ca1102 | ||
|
|
e3ced4385e | ||
|
|
80099ac6d7 | ||
|
|
440bc20faf | ||
|
|
3ddff27cda | ||
|
|
30b423e8c0 | ||
|
|
752ba0b749 | ||
|
|
c16e526d6a | ||
|
|
d64cdd337b | ||
|
|
ac230b76af | ||
|
|
965f3658d5 | ||
|
|
6465a5995b | ||
|
|
69ca261a88 | ||
|
|
3278619dcc | ||
|
|
3fb62e0f9b | ||
|
|
81978659f1 | ||
|
|
be651bd27c | ||
|
|
a6698ed060 | ||
|
|
48d0a2e453 | ||
|
|
5b858b132d | ||
|
|
c8d99a99ec | ||
|
|
ce27c63685 | ||
|
|
ab8944d3ca | ||
|
|
c6a7f4ae15 | ||
|
|
1d100dda25 | ||
|
|
7275e5d65d | ||
|
|
c6be705fb4 | ||
|
|
10b7f4efb3 | ||
|
|
7a6ef00f4d | ||
|
|
3bf6ac7184 | ||
|
|
983b2202ed | ||
|
|
15a6b71997 | ||
|
|
1ca708dcff | ||
|
|
f5597fa6c1 | ||
|
|
2b3247b3df | ||
|
|
d123a5e8d0 | ||
|
|
e2815fbc88 | ||
|
|
f9d4500c6e | ||
|
|
71eb624708 | ||
|
|
dc449715c1 | ||
|
|
275b9085f3 | ||
|
|
35650b141b | ||
|
|
9c8b679136 | ||
|
|
fab082eff5 | ||
|
|
ffc04f9128 | ||
|
|
4d84da1809 | ||
|
|
241b0bbd91 | ||
|
|
042e16c3e4 | ||
|
|
acef3f49bf | ||
|
|
9cfa6c5173 | ||
|
|
39a86ba3cf | ||
|
|
543c72ba51 | ||
|
|
86b114f2f5 | ||
|
|
4b06f3dba1 | ||
|
|
1c6503526b | ||
|
|
c9c05682cb | ||
|
|
39c8648aa7 | ||
|
|
dfff84d5bb | ||
|
|
40ac0de652 | ||
|
|
f3147f574b | ||
|
|
298a6933ff | ||
|
|
52da776b59 | ||
|
|
8842fb1c3a | ||
|
|
6c4ba172d8 | ||
|
|
c88f2c0dab | ||
|
|
3fd6b1c0b4 | ||
|
|
c6d87309f4 | ||
|
|
1fb9179cf9 | ||
|
|
a58e0d8830 | ||
|
|
acc101412d | ||
|
|
e60dafb8dc | ||
|
|
3ce5979acb | ||
|
|
68038d0559 | ||
|
|
f76289aa78 | ||
|
|
6252b92f50 | ||
|
|
1e3a5ac753 | ||
|
|
b1c45cd249 | ||
|
|
3a5ff1c1ce | ||
|
|
bf80673454 | ||
|
|
8028420cbb | ||
|
|
784b2dcb29 | ||
|
|
42b4a5d2fa | ||
|
|
b7f6c2fce2 | ||
|
|
6d7a842a33 | ||
|
|
db1f5b0dee | ||
|
|
c6667ac56c | ||
|
|
a093fb177d | ||
|
|
666686b542 | ||
|
|
5104b1f462 | ||
|
|
62fe272a8e | ||
|
|
f9b69d8da7 | ||
|
|
cb77cb8f69 | ||
|
|
e37fc9ab6e | ||
|
|
abbb50135f | ||
|
|
0594de9b3e | ||
|
|
53f686aa5b | ||
|
|
1677154f04 | ||
|
|
54bc6467eb | ||
|
|
c5487384ee | ||
|
|
5cf23298ac | ||
|
|
5fa7010712 | ||
|
|
f1e0241673 | ||
|
|
a3a255d606 | ||
|
|
0782382982 | ||
|
|
395d2be295 | ||
|
|
ee6e7905e9 | ||
|
|
cc4d03c62e | ||
|
|
8e13af2ee9 | ||
|
|
b522d3f0a3 | ||
|
|
d39c26b358 | ||
|
|
d76c243513 | ||
|
|
5cff354ec8 | ||
|
|
80614dbd00 | ||
|
|
356ff4122c | ||
|
|
cf765928ac | ||
|
|
73d11e80eb | ||
|
|
97de8a0fce | ||
|
|
895ae205ca | ||
|
|
f075c8b0b4 | ||
|
|
3919cb2573 | ||
|
|
bddec2e62a | ||
|
|
fa789e975b | ||
|
|
556fd0166c | ||
|
|
17c78bff9e | ||
|
|
713cb36d35 | ||
|
|
b0cb8765d5 | ||
|
|
97ad51fe6c | ||
|
|
1dd9cbde8b | ||
|
|
962284072b | ||
|
|
e9102b2bb4 | ||
|
|
957c8bd7a3 | ||
|
|
2d5f8a499e | ||
|
|
e5555a5d5b | ||
|
|
a542567e4a | ||
|
|
3168e7813e | ||
|
|
88cf01aa14 | ||
|
|
85ae012d9c | ||
|
|
db1e9e7180 | ||
|
|
dbf7a3337b | ||
|
|
dd97b02b72 | ||
|
|
e8346ad30e | ||
|
|
6a31904333 | ||
|
|
796dc8de7e | ||
|
|
ecb5d84f0f | ||
|
|
cebdd45849 | ||
|
|
8a8d46f130 | ||
|
|
954a100dfd | ||
|
|
604c0be571 | ||
|
|
0e57182207 | ||
|
|
c062c9f54c | ||
|
|
2abf1188a9 | ||
|
|
2b2935a1e7 | ||
|
|
a8545c89a5 | ||
|
|
5532c0b4cf | ||
|
|
f4467a8d6c | ||
|
|
f5fc7aac22 | ||
|
|
e8e9a4af9f | ||
|
|
7bf8f65815 | ||
|
|
defad28771 | ||
|
|
69621c67fb | ||
|
|
469a10b980 | ||
|
|
d90cea0260 | ||
|
|
c9da7a102f | ||
|
|
f4144359f6 | ||
|
|
69de388649 | ||
|
|
6446d00e27 | ||
|
|
d6bded1820 | ||
|
|
e7c00a4bf9 | ||
|
|
92dd4d00f7 | ||
|
|
cf3ffcad61 | ||
|
|
3ad462e766 | ||
|
|
cdab3fcb30 | ||
|
|
d3a5f4dd6c | ||
|
|
75c8607fd2 | ||
|
|
8c075c7fb3 | ||
|
|
a045a46bb4 | ||
|
|
1d979b078d | ||
|
|
f7d76bd40a | ||
|
|
7e11cfc9e4 | ||
|
|
c95b7264a2 | ||
|
|
8ab8e63e04 | ||
|
|
d2c1da9ab7 | ||
|
|
8336d66976 | ||
|
|
e69b10c2e0 | ||
|
|
a6b9a070a5 | ||
|
|
e35205c5c8 | ||
|
|
05f61ea56c | ||
|
|
943cabb14f | ||
|
|
4f70152e7c | ||
|
|
016a394f2c | ||
|
|
6b2fc182c3 | ||
|
|
a77173c002 | ||
|
|
34d5384318 | ||
|
|
ad1010d0f8 | ||
|
|
d99593bcd3 | ||
|
|
8c03c37df4 | ||
|
|
b827cbe2da | ||
|
|
92ca5a102c | ||
|
|
d2281ab646 | ||
|
|
f21c8f0224 | ||
|
|
8b97fd6551 | ||
|
|
29e42211d7 | ||
|
|
6e511b67fd | ||
|
|
9b6cb1ce23 | ||
|
|
6a12bd1511 | ||
|
|
3411bedc06 | ||
|
|
438a65426b | ||
|
|
8c6b1fdf38 | ||
|
|
15e9f1868c | ||
|
|
7eb235cd9c | ||
|
|
d2f2731481 | ||
|
|
b2a895cb89 | ||
|
|
ffb2f44236 | ||
|
|
d66392a3fb | ||
|
|
b9cab239a7 | ||
|
|
276a41d9b6 | ||
|
|
7007e944ae | ||
|
|
bd112a2385 | ||
|
|
42848285a0 | ||
|
|
e3aae34ef7 | ||
|
|
f4316d2dae | ||
|
|
d613a814d3 | ||
|
|
ce234e503f | ||
|
|
3527d25ce8 | ||
|
|
dedb7ec403 | ||
|
|
59263d6025 | ||
|
|
2842cabec6 | ||
|
|
ad650a7ee1 | ||
|
|
9232915885 | ||
|
|
cbb540029f | ||
|
|
bf30d9a5e5 | ||
|
|
f06f71ece4 | ||
|
|
1070d417b3 | ||
|
|
46726a9a61 | ||
|
|
87db8e69ff | ||
|
|
52701ebca4 | ||
|
|
69679dc3fc | ||
|
|
c13f33b1e0 | ||
|
|
5476dd010c | ||
|
|
0add77d5ae | ||
|
|
96a6e91def | ||
|
|
3b41f0ede3 | ||
|
|
0e06836908 | ||
|
|
69db5c936f | ||
|
|
7c402ae4b0 | ||
|
|
b5c0938d2e | ||
|
|
3ad93fff42 | ||
|
|
89f2005250 | ||
|
|
88c0039089 | ||
|
|
1b3a1235f2 | ||
|
|
fd1aaab3bf | ||
|
|
00da5ac5af | ||
|
|
ac20cd1dd3 | ||
|
|
64d6811eda | ||
|
|
51c99bb9df | ||
|
|
43484e7cff | ||
|
|
199f9f07f8 | ||
|
|
13af8cae43 | ||
|
|
436b62f587 | ||
|
|
5b40a67b3b | ||
|
|
c41bd2bb40 | ||
|
|
62cb151918 | ||
|
|
1a3bc67c7c | ||
|
|
aeff51bd79 | ||
|
|
e9f0ede9dd | ||
|
|
0235eab766 | ||
|
|
31c8dc0a1e | ||
|
|
24af48d468 | ||
|
|
44359bfe43 | ||
|
|
14cef83c15 | ||
|
|
77da67ef0a | ||
|
|
58329f812b | ||
|
|
06913ce21a | ||
|
|
4994c53590 | ||
|
|
1d90853f3b | ||
|
|
36c7465cf7 | ||
|
|
010fa268d1 | ||
|
|
99c0384770 | ||
|
|
42d79b1102 | ||
|
|
cef85a4986 | ||
|
|
964615af25 | ||
|
|
3b6d79fc0b | ||
|
|
f48922ce09 | ||
|
|
2980bfccde | ||
|
|
7599f4d50a | ||
|
|
995020bf06 | ||
|
|
423ccaa40b | ||
|
|
526fdbb194 | ||
|
|
511d0ab855 | ||
|
|
60ac99c907 | ||
|
|
57dee93e11 | ||
|
|
0eece7f96e | ||
|
|
eb59d871b4 | ||
|
|
f3f66086a1 | ||
|
|
c3767c2fdb | ||
|
|
6fff3ab952 | ||
|
|
35378d046f | ||
|
|
9c46831a8e | ||
|
|
fe596447ec | ||
|
|
be1035fd5d | ||
|
|
eb96c590ff | ||
|
|
7add8c0bab | ||
|
|
b1bc5c3a02 | ||
|
|
a79356cf5f | ||
|
|
86806aa9bc | ||
|
|
7e73085331 | ||
|
|
3da66c8dee | ||
|
|
f5ea0304da | ||
|
|
25f715bc42 | ||
|
|
7d7164d7c7 | ||
|
|
fb065751dc | ||
|
|
874cf06407 | ||
|
|
b0e83986f0 | ||
|
|
8c0bb73658 | ||
|
|
43182d97de | ||
|
|
193210de49 | ||
|
|
e05ebca5e5 | ||
|
|
cd72278062 | ||
|
|
3c7b791783 | ||
|
|
7592efb8d9 | ||
|
|
5b2fc2712e | ||
|
|
c596a160b3 | ||
|
|
e68aa84649 | ||
|
|
678d56f608 | ||
|
|
d4927627c3 | ||
|
|
61b83aca7f | ||
|
|
0ac66f4413 | ||
|
|
000f7448a7 | ||
|
|
c79a06f639 | ||
|
|
08ef4a8bc4 | ||
|
|
4991d3d22b | ||
|
|
b2193828ab | ||
|
|
9d414c8841 | ||
|
|
a172207df0 | ||
|
|
dda16ef1e5 | ||
|
|
1d0fdbaa13 |
14
.editorconfig
Normal file
14
.editorconfig
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[Makefile]
|
||||||
|
indent_style = tab
|
||||||
|
|
||||||
|
[*.{yml,yaml}]
|
||||||
|
indent_size = 2
|
||||||
11
.github/dependabot.yml
vendored
Normal file
11
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
version: 2
|
||||||
|
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: pip
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
22
.github/workflows/codespell.yml
vendored
Normal file
22
.github/workflows/codespell.yml
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
name: Codespell
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
pull_request:
|
||||||
|
branches: [master]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
codespell:
|
||||||
|
name: Check for spelling errors
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
- name: Codespell
|
||||||
|
uses: codespell-project/actions-codespell@v2
|
||||||
23
.github/workflows/docs.yml
vendored
Normal file
23
.github/workflows/docs.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
name: Validate docs
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- 'docs/**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
validate:
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v5
|
||||||
|
- uses: actions/setup-python@v6
|
||||||
|
with:
|
||||||
|
python-version: "3.12"
|
||||||
|
|
||||||
|
- name: Install build dependencies
|
||||||
|
run: pip install -r docs/requirements.txt
|
||||||
|
- name: Rendering HTML documentation
|
||||||
|
run: sphinx-build -b html docs/ html
|
||||||
|
- name: Inspect html rendered
|
||||||
|
run: cat html/index.html
|
||||||
61
.github/workflows/main.yml
vendored
Normal file
61
.github/workflows/main.yml
vendored
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
name: Test
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
pull_request:
|
||||||
|
schedule:
|
||||||
|
- cron: "0 16 * * 5" # Every Friday 4pm
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
python-version:
|
||||||
|
- "3.10"
|
||||||
|
- "3.11"
|
||||||
|
- "3.12"
|
||||||
|
- "3.13"
|
||||||
|
- "pypy-3.11"
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v5
|
||||||
|
- name: Install uv
|
||||||
|
uses: astral-sh/setup-uv@v7
|
||||||
|
|
||||||
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
|
uses: actions/setup-python@v6
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
allow-prereleases: true
|
||||||
|
|
||||||
|
- name: Install project dependencies
|
||||||
|
run: |
|
||||||
|
uv pip install --system --upgrade pip setuptools
|
||||||
|
uv pip install --system codecov '.[tests]'
|
||||||
|
uv pip check
|
||||||
|
|
||||||
|
- name: Allow creation of user namespaces (e.g. to the unshare command)
|
||||||
|
run: |
|
||||||
|
# .. so that we don't get error:
|
||||||
|
# unshare: write failed /proc/self/uid_map: Operation not permitted
|
||||||
|
# Idea from https://github.com/YoYoGames/GameMaker-Bugs/issues/6015#issuecomment-2135552784 .
|
||||||
|
sudo sysctl kernel.apparmor_restrict_unprivileged_userns=0
|
||||||
|
|
||||||
|
- name: Run online tests
|
||||||
|
run: ./runtests.sh --cov=./vcr --cov-branch --cov-report=xml --cov-append -m online
|
||||||
|
|
||||||
|
- name: Run offline tests with no access to the Internet
|
||||||
|
run: |
|
||||||
|
# We're using unshare to take Internet access
|
||||||
|
# away so that we'll notice whenever some new test
|
||||||
|
# is missing @pytest.mark.online decoration in the future
|
||||||
|
unshare --map-root-user --net -- \
|
||||||
|
sh -c 'ip link set lo up; ./runtests.sh --cov=./vcr --cov-branch --cov-report=xml --cov-append -m "not online"'
|
||||||
|
|
||||||
|
- name: Run coverage
|
||||||
|
run: codecov
|
||||||
62
.github/workflows/pre-commit-detect-outdated.yml
vendored
Normal file
62
.github/workflows/pre-commit-detect-outdated.yml
vendored
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# Copyright (c) 2023 Sebastian Pipping <sebastian@pipping.org>
|
||||||
|
# Licensed under the MIT license
|
||||||
|
|
||||||
|
name: Detect outdated pre-commit hooks
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 16 * * 5' # Every Friday 4pm
|
||||||
|
|
||||||
|
# NOTE: This will drop all permissions from GITHUB_TOKEN except metadata read,
|
||||||
|
# and then (re)add the ones listed below:
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
pre_commit_detect_outdated:
|
||||||
|
name: Detect outdated pre-commit hooks
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
|
- name: Set up Python 3.12
|
||||||
|
uses: actions/setup-python@v6
|
||||||
|
with:
|
||||||
|
python-version: 3.12
|
||||||
|
|
||||||
|
- name: Install pre-commit
|
||||||
|
run: |-
|
||||||
|
pip install \
|
||||||
|
--disable-pip-version-check \
|
||||||
|
--no-warn-script-location \
|
||||||
|
--user \
|
||||||
|
pre-commit
|
||||||
|
echo "PATH=${HOME}/.local/bin:${PATH}" >> "${GITHUB_ENV}"
|
||||||
|
|
||||||
|
- name: Check for outdated hooks
|
||||||
|
run: |-
|
||||||
|
pre-commit autoupdate
|
||||||
|
git diff -- .pre-commit-config.yaml
|
||||||
|
|
||||||
|
- name: Create pull request from changes (if any)
|
||||||
|
id: create-pull-request
|
||||||
|
uses: peter-evans/create-pull-request@v7
|
||||||
|
with:
|
||||||
|
author: 'pre-commit <pre-commit@tools.invalid>'
|
||||||
|
base: master
|
||||||
|
body: |-
|
||||||
|
For your consideration.
|
||||||
|
|
||||||
|
:warning: Please **CLOSE AND RE-OPEN** this pull request so that [further workflow runs get triggered](https://github.com/peter-evans/create-pull-request/blob/main/docs/concepts-guidelines.md#triggering-further-workflow-runs) for this pull request.
|
||||||
|
branch: precommit-autoupdate
|
||||||
|
commit-message: "pre-commit: Autoupdate"
|
||||||
|
delete-branch: true
|
||||||
|
draft: true
|
||||||
|
labels: enhancement
|
||||||
|
title: "pre-commit: Autoupdate"
|
||||||
|
|
||||||
|
- name: Log pull request URL
|
||||||
|
if: "${{ steps.create-pull-request.outputs.pull-request-url }}"
|
||||||
|
run: |
|
||||||
|
echo "Pull request URL is: ${{ steps.create-pull-request.outputs.pull-request-url }}"
|
||||||
20
.github/workflows/pre-commit.yml
vendored
Normal file
20
.github/workflows/pre-commit.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Copyright (c) 2023 Sebastian Pipping <sebastian@pipping.org>
|
||||||
|
# Licensed under the MIT license
|
||||||
|
|
||||||
|
name: Run pre-commit
|
||||||
|
|
||||||
|
on:
|
||||||
|
- pull_request
|
||||||
|
- push
|
||||||
|
- workflow_dispatch
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
pre-commit:
|
||||||
|
name: Run pre-commit
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v5
|
||||||
|
- uses: actions/setup-python@v6
|
||||||
|
with:
|
||||||
|
python-version: 3.12
|
||||||
|
- uses: pre-commit/action@v3.0.1
|
||||||
17
.pre-commit-config.yaml
Normal file
17
.pre-commit-config.yaml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Copyright (c) 2023 Sebastian Pipping <sebastian@pipping.org>
|
||||||
|
# Licensed under the MIT license
|
||||||
|
|
||||||
|
repos:
|
||||||
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
|
rev: v0.14.5
|
||||||
|
hooks:
|
||||||
|
- id: ruff
|
||||||
|
args: ["--output-format=full"]
|
||||||
|
- id: ruff-format
|
||||||
|
|
||||||
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
rev: v5.0.0
|
||||||
|
hooks:
|
||||||
|
- id: check-merge-conflict
|
||||||
|
- id: end-of-file-fixer
|
||||||
|
- id: trailing-whitespace
|
||||||
24
.readthedocs.yaml
Normal file
24
.readthedocs.yaml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# .readthedocs.yaml
|
||||||
|
# Read the Docs configuration file
|
||||||
|
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
|
||||||
|
|
||||||
|
# Required
|
||||||
|
version: 2
|
||||||
|
|
||||||
|
# Set the version of Python and other tools you might need
|
||||||
|
build:
|
||||||
|
os: ubuntu-24.04
|
||||||
|
tools:
|
||||||
|
python: "3.12"
|
||||||
|
|
||||||
|
# Build documentation in the docs/ directory with Sphinx
|
||||||
|
sphinx:
|
||||||
|
configuration: docs/conf.py
|
||||||
|
|
||||||
|
# We recommend specifying your dependencies to enable reproducible builds:
|
||||||
|
# https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
|
||||||
|
python:
|
||||||
|
install:
|
||||||
|
- requirements: docs/requirements.txt
|
||||||
|
- method: pip
|
||||||
|
path: .
|
||||||
33
.travis.yml
33
.travis.yml
@@ -1,33 +0,0 @@
|
|||||||
dist: xenial
|
|
||||||
language: python
|
|
||||||
before_install: openssl version
|
|
||||||
env:
|
|
||||||
# global:
|
|
||||||
# - secure: AifoKzwhjV94cmcQZrdQmqRu/9rkZZvWpwBv1daeAQpLOKFPGsOm3D+x2cSw9+iCfkgDZDfqQVv1kCaFVxTll8v8jTq5SJdqEY0NmGWbj/UkNtShh609oRDsuzLxAEwtVKYjf/h8K2BRea+bl1tGkwZ2vtmYS6dxNlAijjWOfds=
|
|
||||||
# - secure: LBSEg/gMj4u4Hrpo3zs6Y/1mTpd2RtcN49mZIFgTdbJ9IhpiNPqcEt647Lz94F9Eses2x2WbNuKqZKZZReY7QLbEzU1m0nN5jlaKrjcG5NR5clNABfFFyhgc0jBikyS4abAG8jc2efeaTrFuQwdoF4sE8YiVrkiVj2X5Xoi6sBk=
|
|
||||||
matrix:
|
|
||||||
- TOX_SUFFIX="requests"
|
|
||||||
- TOX_SUFFIX="httplib2"
|
|
||||||
- TOX_SUFFIX="boto3"
|
|
||||||
- TOX_SUFFIX="urllib3"
|
|
||||||
- TOX_SUFFIX="tornado4"
|
|
||||||
- TOX_SUFFIX="aiohttp"
|
|
||||||
- TOX_SUFFIX="httpx"
|
|
||||||
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"
|
|
||||||
install:
|
|
||||||
- pip install tox-travis codecov
|
|
||||||
- if [[ $TOX_SUFFIX != 'lint' ]]; then python setup.py install ; fi
|
|
||||||
script:
|
|
||||||
- tox -e "${TOX_SUFFIX}"
|
|
||||||
after_success:
|
|
||||||
- codecov
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
include README.rst
|
include README.rst
|
||||||
include LICENSE.txt
|
include LICENSE.txt
|
||||||
include tox.ini
|
|
||||||
recursive-include tests *
|
recursive-include tests *
|
||||||
recursive-exclude * __pycache__
|
recursive-exclude * __pycache__
|
||||||
recursive-exclude * *.py[co]
|
recursive-exclude * *.py[co]
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ VCR.py 📼
|
|||||||
###########
|
###########
|
||||||
|
|
||||||
|
|
||||||
|PyPI| |Python versions| |Build Status| |CodeCov| |Gitter| |CodeStyleBlack|
|
|PyPI| |Python versions| |Build Status| |CodeCov| |Gitter|
|
||||||
|
|
||||||
----
|
----
|
||||||
|
|
||||||
@@ -62,14 +62,11 @@ more details
|
|||||||
:target: https://pypi.python.org/pypi/vcrpy
|
:target: https://pypi.python.org/pypi/vcrpy
|
||||||
.. |Python versions| image:: https://img.shields.io/pypi/pyversions/vcrpy.svg
|
.. |Python versions| image:: https://img.shields.io/pypi/pyversions/vcrpy.svg
|
||||||
:target: https://pypi.python.org/pypi/vcrpy
|
:target: https://pypi.python.org/pypi/vcrpy
|
||||||
.. |Build Status| image:: https://secure.travis-ci.org/kevin1024/vcrpy.svg?branch=master
|
.. |Build Status| image:: https://github.com/kevin1024/vcrpy/actions/workflows/main.yml/badge.svg
|
||||||
:target: http://travis-ci.org/kevin1024/vcrpy
|
:target: https://github.com/kevin1024/vcrpy/actions
|
||||||
.. |Gitter| image:: https://badges.gitter.im/Join%20Chat.svg
|
.. |Gitter| image:: https://badges.gitter.im/Join%20Chat.svg
|
||||||
:alt: Join the chat at https://gitter.im/kevin1024/vcrpy
|
:alt: Join the chat at https://gitter.im/kevin1024/vcrpy
|
||||||
:target: https://gitter.im/kevin1024/vcrpy?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
|
:target: https://gitter.im/kevin1024/vcrpy?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
|
||||||
.. |CodeCov| image:: https://codecov.io/gh/kevin1024/vcrpy/branch/master/graph/badge.svg
|
.. |CodeCov| image:: https://codecov.io/gh/kevin1024/vcrpy/branch/master/graph/badge.svg
|
||||||
:target: https://codecov.io/gh/kevin1024/vcrpy
|
:target: https://codecov.io/gh/kevin1024/vcrpy
|
||||||
:alt: Code Coverage Status
|
:alt: Code Coverage Status
|
||||||
.. |CodeStyleBlack| image:: https://img.shields.io/badge/code%20style-black-000000.svg
|
|
||||||
:target: https://github.com/psf/black
|
|
||||||
:alt: Code Style: black
|
|
||||||
|
|||||||
2
docs/_static/vcr.svg
vendored
2
docs/_static/vcr.svg
vendored
@@ -24,4 +24,4 @@
|
|||||||
<stop offset="1" stop-color="#27DDA6"/>
|
<stop offset="1" stop-color="#27DDA6"/>
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.2 KiB |
@@ -16,7 +16,7 @@ a nice addition. Here's an example:
|
|||||||
with vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml') as cass:
|
with vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml') as cass:
|
||||||
response = urllib2.urlopen('http://www.zombo.com/').read()
|
response = urllib2.urlopen('http://www.zombo.com/').read()
|
||||||
# cass should have 1 request inside it
|
# cass should have 1 request inside it
|
||||||
assert len(cass) == 1
|
assert len(cass) == 1
|
||||||
# the request uri should have been http://www.zombo.com/
|
# the request uri should have been http://www.zombo.com/
|
||||||
assert cass.requests[0].uri == 'http://www.zombo.com/'
|
assert cass.requests[0].uri == 'http://www.zombo.com/'
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ Finally, register your class with VCR to use your new serializer.
|
|||||||
|
|
||||||
import vcr
|
import vcr
|
||||||
|
|
||||||
class BogoSerializer(object):
|
class BogoSerializer:
|
||||||
"""
|
"""
|
||||||
Must implement serialize() and deserialize() methods
|
Must implement serialize() and deserialize() methods
|
||||||
"""
|
"""
|
||||||
@@ -136,7 +136,8 @@ Create your own persistence class, see the example below:
|
|||||||
|
|
||||||
Your custom persister must implement both ``load_cassette`` and ``save_cassette``
|
Your custom persister must implement both ``load_cassette`` and ``save_cassette``
|
||||||
methods. The ``load_cassette`` method must return a deserialized cassette or raise
|
methods. The ``load_cassette`` method must return a deserialized cassette or raise
|
||||||
``ValueError`` if no cassette is found.
|
either ``CassetteNotFoundError`` if no cassette is found, or ``CassetteDecodeError``
|
||||||
|
if the cassette cannot be successfully deserialized.
|
||||||
|
|
||||||
Once the persister class is defined, register with VCR like so...
|
Once the persister class is defined, register with VCR like so...
|
||||||
|
|
||||||
@@ -188,7 +189,7 @@ of post data parameters to filter.
|
|||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
with my_vcr.use_cassette('test.yml', filter_post_data_parameters=['client_secret']):
|
with my_vcr.use_cassette('test.yml', filter_post_data_parameters=['api_key']):
|
||||||
requests.post('http://api.com/postdata', data={'api_key': 'secretstring'})
|
requests.post('http://api.com/postdata', data={'api_key': 'secretstring'})
|
||||||
|
|
||||||
Advanced use of filter_headers, filter_query_parameters and filter_post_data_parameters
|
Advanced use of filter_headers, filter_query_parameters and filter_post_data_parameters
|
||||||
@@ -207,7 +208,7 @@ So these two calls are the same:
|
|||||||
|
|
||||||
# original (still works)
|
# original (still works)
|
||||||
vcr = VCR(filter_headers=['authorization'])
|
vcr = VCR(filter_headers=['authorization'])
|
||||||
|
|
||||||
# new
|
# new
|
||||||
vcr = VCR(filter_headers=[('authorization', None)])
|
vcr = VCR(filter_headers=[('authorization', None)])
|
||||||
|
|
||||||
@@ -217,7 +218,7 @@ Here are two examples of the new functionality:
|
|||||||
|
|
||||||
# replace with a static value (most common)
|
# replace with a static value (most common)
|
||||||
vcr = VCR(filter_headers=[('authorization', 'XXXXXX')])
|
vcr = VCR(filter_headers=[('authorization', 'XXXXXX')])
|
||||||
|
|
||||||
# replace with a callable, for example when testing
|
# replace with a callable, for example when testing
|
||||||
# lots of different kinds of authorization.
|
# lots of different kinds of authorization.
|
||||||
def replace_auth(key, value, request):
|
def replace_auth(key, value, request):
|
||||||
@@ -271,7 +272,7 @@ You can also do response filtering with the
|
|||||||
similar to the above ``before_record_request`` - you can
|
similar to the above ``before_record_request`` - you can
|
||||||
mutate the response, or return ``None`` to avoid recording
|
mutate the response, or return ``None`` to avoid recording
|
||||||
the request and response altogether. For example to hide
|
the request and response altogether. For example to hide
|
||||||
sensitive data from the request body:
|
sensitive data from the response body:
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
@@ -285,7 +286,7 @@ sensitive data from the request body:
|
|||||||
before_record_response=scrub_string(settings.USERNAME, 'username'),
|
before_record_response=scrub_string(settings.USERNAME, 'username'),
|
||||||
)
|
)
|
||||||
with my_vcr.use_cassette('test.yml'):
|
with my_vcr.use_cassette('test.yml'):
|
||||||
# your http code here
|
# your http code here
|
||||||
|
|
||||||
|
|
||||||
Decode compressed response
|
Decode compressed response
|
||||||
@@ -404,3 +405,38 @@ the Cassette ``allow_playback_repeats`` option.
|
|||||||
for x in range(10):
|
for x in range(10):
|
||||||
response = urllib2.urlopen('http://www.zombo.com/').read()
|
response = urllib2.urlopen('http://www.zombo.com/').read()
|
||||||
assert cass.all_played
|
assert cass.all_played
|
||||||
|
|
||||||
|
Discards Cassette on Errors
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
By default VCR will save the cassette file even when there is any error inside
|
||||||
|
the enclosing context/test.
|
||||||
|
|
||||||
|
If you want to save the cassette only when the test succeeds, set the Cassette
|
||||||
|
``record_on_exception`` option to ``False``.
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
try:
|
||||||
|
my_vcr = VCR(record_on_exception=False)
|
||||||
|
with my_vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml') as cass:
|
||||||
|
response = urllib2.urlopen('http://www.zombo.com/').read()
|
||||||
|
raise RuntimeError("Oops, something happened")
|
||||||
|
except RuntimeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Since there was an exception, the cassette file hasn't been created.
|
||||||
|
assert not os.path.exists('fixtures/vcr_cassettes/synopsis.yaml')
|
||||||
|
|
||||||
|
Drop unused requests
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
Even if any HTTP request is changed or removed from tests, previously recorded
|
||||||
|
interactions remain in the cassette file. If set the ``drop_unused_requests``
|
||||||
|
option to ``True``, VCR will not save old HTTP interactions if they are not used.
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
my_vcr = VCR(drop_unused_requests=True)
|
||||||
|
with my_vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml'):
|
||||||
|
... # your HTTP interactions here
|
||||||
|
|||||||
@@ -1,12 +1,91 @@
|
|||||||
Changelog
|
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>`_
|
`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.
|
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.
|
||||||
|
|
||||||
|
- Unreleased
|
||||||
|
- Drop support for Python 3.9
|
||||||
|
- Drop support for urllib3 < 2
|
||||||
|
|
||||||
|
- 7.0.0
|
||||||
|
- Drop support for python 3.8 (major version bump) - thanks @jairhenrique
|
||||||
|
- Various linting and test fixes - thanks @jairhenrique
|
||||||
|
- Bugfix for urllib2>=2.3.0 - missing version_string (#888)
|
||||||
|
- Bugfix for asyncio.run - thanks @alekeik1
|
||||||
|
- 6.0.2
|
||||||
|
- Ensure body is consumed only once (#846) - thanks @sathieu
|
||||||
|
- Permit urllib3 2.x for non-PyPy Python >=3.10
|
||||||
|
- Fix typos in test commands - thanks @chuckwondo
|
||||||
|
- Several test and workflow improvements - thanks @hartwork and @graingert
|
||||||
|
- 6.0.1
|
||||||
|
- Bugfix with to Tornado cassette generator (thanks @graingert)
|
||||||
|
- 6.0.0
|
||||||
|
- BREAKING: Fix issue with httpx support (thanks @parkerhancock) in #784. NOTE: You may have to recreate some of your cassettes produced in previous releases due to the binary format being saved incorrectly in previous releases
|
||||||
|
- BREAKING: Drop support for `boto` (vcrpy still supports boto3, but is dropping the deprecated `boto` support in this release. (thanks @jairhenrique)
|
||||||
|
- Fix compatibility issue with Python 3.12 (thanks @hartwork)
|
||||||
|
- Drop simplejson (fixes some compatibility issues) (thanks @jairhenrique)
|
||||||
|
- Run CI on Python 3.12 and PyPy 3.9-3.10 (thanks @mgorny)
|
||||||
|
- Various linting and docs improvements (thanks @jairhenrique)
|
||||||
|
- Tornado fixes (thanks @graingert)
|
||||||
|
- 5.1.0
|
||||||
|
- Use ruff for linting (instead of current flake8/isort/pyflakes) - thanks @jairhenrique
|
||||||
|
- Enable rule B (flake8-bugbear) on ruff - thanks @jairhenrique
|
||||||
|
- Configure read the docs V2 - thanks @jairhenrique
|
||||||
|
- Fix typo in docs - thanks @quasimik
|
||||||
|
- Make json.loads of Python >=3.6 decode bytes by itself - thanks @hartwork
|
||||||
|
- Fix body matcher for chunked requests (fixes #734) - thanks @hartwork
|
||||||
|
- Fix query param filter for aiohttp (fixes #517) - thanks @hartwork and @salomvary
|
||||||
|
- Remove unnecessary dependency on six. - thanks @charettes
|
||||||
|
- build(deps): update sphinx requirement from <7 to <8 - thanks @jairhenrique
|
||||||
|
- Add action to validate docs - thanks @jairhenrique
|
||||||
|
- Add editorconfig file - thanks @jairhenrique
|
||||||
|
- Drop iscoroutinefunction fallback function for unsupported python thanks @jairhenrique
|
||||||
|
- 5.0.0
|
||||||
|
- BREAKING CHANGE: Drop support for Python 3.7. 3.7 is EOL as of 6/27/23 Thanks @jairhenrique
|
||||||
|
- BREAKING CHANGE: Custom Cassette persisters no longer catch ValueError. If you have implemented a custom persister (has anyone implemented a custom persister? Let us know!) then you will need to throw a CassetteNotFoundError when unable to find a cassette. See #681 for discussion and reason for this change. Thanks @amosjyng for the PR and the review from @hartwork
|
||||||
|
|
||||||
|
- 4.4.0
|
||||||
|
- HUGE thanks to @hartwork for all the work done on this release!
|
||||||
|
- Bring vcr/unittest in to vcrpy as a full feature of vcr instead of a separate library. Big thanks to @hartwork for doing this and to @agriffis for originally creating the library
|
||||||
|
- Make decompression robust towards already decompressed input (thanks @hartwork)
|
||||||
|
- Bugfix: Add read1 method (fixes compatibility with biopython), thanks @mghantous
|
||||||
|
- Bugfix: Prevent filters from corrupting request (thanks @abramclark)
|
||||||
|
- Bugfix: Add support for `response.raw.stream()` to fix urllib v2 compat
|
||||||
|
- Bugfix: Replace `assert` with `raise AssertionError`: fixes support for `PYTHONOPTIMIZE=1`
|
||||||
|
- Add pytest.mark.online to run test suite offline, thanks @jspricke
|
||||||
|
- use python3 and pip3 binaries to ease debian packaging (thanks @hartwork)
|
||||||
|
- Add codespell (thanks @mghantous)
|
||||||
|
- 4.3.1
|
||||||
|
- Support urllib3 v1 and v2. NOTE: there is an issue running urllib3 v2 on
|
||||||
|
Python older than 3.10, so this is currently blocked in the requirements.
|
||||||
|
Hopefully we can resolve this situation in the future. Thanks to @shifqu,
|
||||||
|
hartwork, jairhenrique, pquentin, and vEpiphyte for your work on this.
|
||||||
|
- 4.3.0
|
||||||
|
- Add support for Python 3.11 (Thanks @evgeni)
|
||||||
|
- Drop support for botocore <1.11.0 and requests <2.16.2 (thanks @hartwork)
|
||||||
|
- Bugfix: decode_compressed_response raises exception on empty responses. Thanks @CharString
|
||||||
|
- Don't save requests from decorated tests if decorated test fails (thanks @dan-passaro)
|
||||||
|
- Fix not calling all the exit stack when record_on_exception is False (thanks @Terseus)
|
||||||
|
- Various CI, documentation, testing, and formatting improvements (Thanks @jairhenrique, @dan-passaro, @hartwork, and Terseus)
|
||||||
|
- 4.2.1
|
||||||
|
- Fix a bug where the first request in a redirect chain was not being recorded with aiohttp
|
||||||
|
- Various typos and small fixes, thanks @jairhenrique, @timgates42
|
||||||
|
- 4.2.0
|
||||||
|
- Drop support for python < 3.7, thanks @jairhenrique, @IvanMalison, @AthulMuralidhar
|
||||||
|
- Various aiohtt bigfixes (thanks @pauloromeira and boechat107)
|
||||||
|
- Bugfix: filter_post_data_parameters not working with aiohttp. Thank you @vprakashplanview, @scop, @jairhenrique, and @cinemascop89
|
||||||
|
- Bugfix: Some random misspellings (thanks @scop)
|
||||||
|
- Migrate the CI suite to Github Actions from Travis (thanks @jairhenrique and @cclauss)
|
||||||
|
- Various documentation and code misspelling fixes (thanks @scop and @Justintime50)
|
||||||
|
- Bugfix: httpx support (select between allow_redirects/follow_redirects) (thanks @immerrr)
|
||||||
|
- Bugfix: httpx support (select between allow_redirects/follow_redirects) (thanks @immerrr)
|
||||||
|
- 4.1.1
|
||||||
|
- Fix HTTPX support for versions greater than 0.15 (thanks @jairhenrique)
|
||||||
|
- Include a trailing newline on json cassettes (thanks @AaronRobson)
|
||||||
- 4.1.0
|
- 4.1.0
|
||||||
- Add support for httpx!! (thanks @herdigiorgi)
|
- Add support for httpx!! (thanks @herdigiorgi)
|
||||||
- Add the new `allow_playback_repeats` option (thanks @tysonholub)
|
- Add the new `allow_playback_repeats` option (thanks @tysonholub)
|
||||||
@@ -16,8 +95,8 @@ All help in providing PRs to close out bug issues is appreciated. Even if that i
|
|||||||
- Bugfix: Fix test suite by switching to mockbin (thanks @jairhenrique)
|
- Bugfix: Fix test suite by switching to mockbin (thanks @jairhenrique)
|
||||||
- 4.0.2
|
- 4.0.2
|
||||||
- Fix mock imports as reported in #504 by @llybin. Thank you.
|
- Fix mock imports as reported in #504 by @llybin. Thank you.
|
||||||
- 4.0.1
|
- 4.0.1
|
||||||
- Fix logo alignment for PyPI
|
- Fix logo alignment for PyPI
|
||||||
- 4.0.0
|
- 4.0.0
|
||||||
- Remove Python2 support (@hugovk)
|
- Remove Python2 support (@hugovk)
|
||||||
- Add Python 3.8 TravisCI support (@neozenith)
|
- Add Python 3.8 TravisCI support (@neozenith)
|
||||||
@@ -29,7 +108,7 @@ All help in providing PRs to close out bug issues is appreciated. Even if that i
|
|||||||
- Add support for `request_info` on mocked responses in aiohttp stub #495 (@nickdirienzo)
|
- Add support for `request_info` on mocked responses in aiohttp stub #495 (@nickdirienzo)
|
||||||
- doc: fixed variable name (a -> cass) in an example for rewind #492 (@yarikoptic)
|
- doc: fixed variable name (a -> cass) in an example for rewind #492 (@yarikoptic)
|
||||||
|
|
||||||
- 2.1.1
|
- 2.1.1
|
||||||
- Format code with black (@neozenith)
|
- Format code with black (@neozenith)
|
||||||
- Use latest pypy3 in Travis (@hugovk)
|
- Use latest pypy3 in Travis (@hugovk)
|
||||||
- Improve documentation about custom matchers (@gward)
|
- Improve documentation about custom matchers (@gward)
|
||||||
@@ -37,7 +116,7 @@ All help in providing PRs to close out bug issues is appreciated. Even if that i
|
|||||||
- Add `pytest-recording` to the documentation as an alternative Pytest plugin (@Stranger6667)
|
- Add `pytest-recording` to the documentation as an alternative Pytest plugin (@Stranger6667)
|
||||||
- Fix yarl and python3.5 version issue (@neozenith)
|
- Fix yarl and python3.5 version issue (@neozenith)
|
||||||
- Fix header matcher for boto3 - fixes #474 (@simahawk)
|
- Fix header matcher for boto3 - fixes #474 (@simahawk)
|
||||||
- 2.1.0
|
- 2.1.0
|
||||||
- Add a `rewind` method to reset a cassette (thanks @khamidou)
|
- Add a `rewind` method to reset a cassette (thanks @khamidou)
|
||||||
- New error message with more details on why the cassette failed to play a request (thanks @arthurHamon2, @neozenith)
|
- New error message with more details on why the cassette failed to play a request (thanks @arthurHamon2, @neozenith)
|
||||||
- Handle connect tunnel URI (thanks @jeking3)
|
- Handle connect tunnel URI (thanks @jeking3)
|
||||||
@@ -49,9 +128,9 @@ All help in providing PRs to close out bug issues is appreciated. Even if that i
|
|||||||
- Fix bugs on aiohttp integration (thanks @graingert, @steinnes, @stj, @lamenezes, @lmazuel)
|
- Fix bugs on aiohttp integration (thanks @graingert, @steinnes, @stj, @lamenezes, @lmazuel)
|
||||||
- Fix Biopython incompatibility (thanks @rishab121)
|
- Fix Biopython incompatibility (thanks @rishab121)
|
||||||
- Fix Boto3 integration (thanks @1oglop1, @arthurHamon2)
|
- Fix Boto3 integration (thanks @1oglop1, @arthurHamon2)
|
||||||
- 2.0.1
|
- 2.0.1
|
||||||
- Fix bug when using vcrpy with python 3.4
|
- Fix bug when using vcrpy with python 3.4
|
||||||
- 2.0.0
|
- 2.0.0
|
||||||
- Support python 3.7 (fix httplib2 and urllib2, thanks @felixonmars)
|
- Support python 3.7 (fix httplib2 and urllib2, thanks @felixonmars)
|
||||||
- [#356] Fixes `before_record_response` so the original response isn't changed (thanks @kgraves)
|
- [#356] Fixes `before_record_response` so the original response isn't changed (thanks @kgraves)
|
||||||
- Fix requests stub when using proxy (thanks @samuelfekete @daneoshiga)
|
- Fix requests stub when using proxy (thanks @samuelfekete @daneoshiga)
|
||||||
@@ -61,56 +140,56 @@ All help in providing PRs to close out bug issues is appreciated. Even if that i
|
|||||||
- Improve docs (thanks @adamchainz)
|
- Improve docs (thanks @adamchainz)
|
||||||
|
|
||||||
|
|
||||||
- 1.13.0
|
- 1.13.0
|
||||||
- Fix support to latest aiohttp version (3.3.2). Fix content-type bug in aiohttp stub. Save URL with query params properly when using aiohttp.
|
- Fix support to latest aiohttp version (3.3.2). Fix content-type bug in aiohttp stub. Save URL with query params properly when using aiohttp.
|
||||||
- 1.12.0
|
- 1.12.0
|
||||||
- Fix support to latest aiohttp version (3.2.1), Adapted setup to PEP508, Support binary responses on aiohttp, Dropped support for EOL python versions (2.6 and 3.3)
|
- Fix support to latest aiohttp version (3.2.1), Adapted setup to PEP508, Support binary responses on aiohttp, Dropped support for EOL python versions (2.6 and 3.3)
|
||||||
- 1.11.1
|
- 1.11.1
|
||||||
- Fix compatibility with newest requests and urllib3 releases
|
- Fix compatibility with newest requests and urllib3 releases
|
||||||
- 1.11.0
|
- 1.11.0
|
||||||
- Allow injection of persistence methods + bugfixes (thanks @j-funk and @IvanMalison),
|
- Allow injection of persistence methods + bugfixes (thanks @j-funk and @IvanMalison),
|
||||||
- Support python 3.6 + CI tests (thanks @derekbekoe and @graingert),
|
- Support python 3.6 + CI tests (thanks @derekbekoe and @graingert),
|
||||||
- Support pytest-asyncio coroutines (thanks @graingert)
|
- Support pytest-asyncio coroutines (thanks @graingert)
|
||||||
- 1.10.5
|
- 1.10.5
|
||||||
- Added a fix to httplib2 (thanks @carlosds730), Fix an issue with
|
- Added a fix to httplib2 (thanks @carlosds730), Fix an issue with
|
||||||
- aiohttp (thanks @madninja), Add missing requirement yarl (thanks @lamenezes),
|
- aiohttp (thanks @madninja), Add missing requirement yarl (thanks @lamenezes),
|
||||||
- Remove duplicate mock triple (thanks @FooBarQuaxx)
|
- Remove duplicate mock triple (thanks @FooBarQuaxx)
|
||||||
- 1.10.4
|
- 1.10.4
|
||||||
- Fix an issue with asyncio aiohttp (thanks @madninja)
|
- Fix an issue with asyncio aiohttp (thanks @madninja)
|
||||||
- 1.10.3
|
- 1.10.3
|
||||||
- Fix some issues with asyncio and params (thanks @anovikov1984 and @lamenezes)
|
- Fix some issues with asyncio and params (thanks @anovikov1984 and @lamenezes)
|
||||||
- Fix some issues with cassette serialize / deserialize and empty response bodies (thanks @gRoussac and @dz0ny)
|
- Fix some issues with cassette serialize / deserialize and empty response bodies (thanks @gRoussac and @dz0ny)
|
||||||
- 1.10.2
|
- 1.10.2
|
||||||
- Fix 1.10.1 release - add aiohttp support back in
|
- Fix 1.10.1 release - add aiohttp support back in
|
||||||
- 1.10.1
|
- 1.10.1
|
||||||
- [bad release] Fix build for Fedora package + python2 (thanks @puiterwijk and @lamenezes)
|
- [bad release] Fix build for Fedora package + python2 (thanks @puiterwijk and @lamenezes)
|
||||||
- 1.10.0
|
- 1.10.0
|
||||||
- Add support for aiohttp (thanks @lamenezes)
|
- Add support for aiohttp (thanks @lamenezes)
|
||||||
- 1.9.0
|
- 1.9.0
|
||||||
- Add support for boto3 (thanks @desdm, @foorbarna).
|
- Add support for boto3 (thanks @desdm, @foorbarna).
|
||||||
- Fix deepcopy issue for response headers when `decode_compressed_response` is enabled (thanks @nickdirienzo)
|
- Fix deepcopy issue for response headers when `decode_compressed_response` is enabled (thanks @nickdirienzo)
|
||||||
- 1.8.0
|
- 1.8.0
|
||||||
- Fix for Serialization errors with JSON adapter (thanks @aliaksandrb).
|
- Fix for Serialization errors with JSON adapter (thanks @aliaksandrb).
|
||||||
- Avoid concatenating bytes with strings (thanks @jaysonsantos).
|
- Avoid concatenating bytes with strings (thanks @jaysonsantos).
|
||||||
- Exclude __pycache__ dirs & compiled files in sdist (thanks @koobs).
|
- Exclude __pycache__ dirs & compiled files in sdist (thanks @koobs).
|
||||||
- Fix Tornado support behavior for Tornado 3 (thanks @abhinav).
|
- Fix Tornado support behavior for Tornado 3 (thanks @abhinav).
|
||||||
- decode_compressed_response option and filter (thanks @jayvdb).
|
- decode_compressed_response option and filter (thanks @jayvdb).
|
||||||
- 1.7.4 [#217]
|
- 1.7.4 [#217]
|
||||||
- Make use_cassette decorated functions actually return a value (thanks @bcen).
|
- 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.
|
- Better headers dictionary management.
|
||||||
- 1.7.3 [#188]
|
- 1.7.3 [#188]
|
||||||
- ``additional_matchers`` kwarg on ``use_cassette``.
|
- ``additional_matchers`` kwarg on ``use_cassette``.
|
||||||
- [#191] Actually support passing multiple before_record_request functions (thanks @agriffis).
|
- [#191] Actually support passing multiple before_record_request functions (thanks @agriffis).
|
||||||
- 1.7.2
|
- 1.7.2
|
||||||
- [#186] Get effective_url in tornado (thanks @mvschaik)
|
- [#186] Get effective_url in tornado (thanks @mvschaik)
|
||||||
- [#187] Set request_time on Response object in tornado (thanks @abhinav).
|
- [#187] Set request_time on Response object in tornado (thanks @abhinav).
|
||||||
- 1.7.1
|
- 1.7.1
|
||||||
- [#183] Patch ``fetch_impl`` instead of the entire HTTPClient class for Tornado (thanks @abhinav).
|
- [#183] Patch ``fetch_impl`` instead of the entire HTTPClient class for Tornado (thanks @abhinav).
|
||||||
- 1.7.0
|
- 1.7.0
|
||||||
- [#177] Properly support coroutine/generator decoration.
|
- [#177] Properly support coroutine/generator decoration.
|
||||||
- [#178] Support distribute (thanks @graingert). [#163] Make compatibility between python2 and python3 recorded cassettes more robust (thanks @gward).
|
- [#178] Support distribute (thanks @graingert). [#163] Make compatibility between python2 and python3 recorded cassettes more robust (thanks @gward).
|
||||||
- 1.6.1
|
- 1.6.1
|
||||||
- [#169] Support conditional requirements in old versions of pip
|
- [#169] Support conditional requirements in old versions of pip
|
||||||
- Fix RST parse errors generated by pandoc
|
- Fix RST parse errors generated by pandoc
|
||||||
- [Tornado] Fix unsupported features exception not being raised
|
- [Tornado] Fix unsupported features exception not being raised
|
||||||
@@ -124,17 +203,17 @@ All help in providing PRs to close out bug issues is appreciated. Even if that i
|
|||||||
- Fix crash when cassette path contains cassette library directory (thanks @gazpachoking).
|
- Fix crash when cassette path contains cassette library directory (thanks @gazpachoking).
|
||||||
- 1.5.0
|
- 1.5.0
|
||||||
- Automatic cassette naming and 'application/json' post data filtering (thanks @marco-santamaria).
|
- Automatic cassette naming and 'application/json' post data filtering (thanks @marco-santamaria).
|
||||||
- 1.4.2
|
- 1.4.2
|
||||||
- Fix a bug caused by requests 2.7 and chunked transfer encoding
|
- Fix a bug caused by requests 2.7 and chunked transfer encoding
|
||||||
- 1.4.1
|
- 1.4.1
|
||||||
- Include README, tests, LICENSE in package. Thanks @ralphbean.
|
- Include README, tests, LICENSE in package. Thanks @ralphbean.
|
||||||
- 1.4.0
|
- 1.4.0
|
||||||
- Filter post data parameters (thanks @eadmundo)
|
- Filter post data parameters (thanks @eadmundo)
|
||||||
- Support for posting files through requests, inject\_cassette kwarg to access cassette from ``use_cassette`` decorated function, ``with_current_defaults`` actually works (thanks @samstav).
|
- Support for posting files through requests, inject\_cassette kwarg to access cassette from ``use_cassette`` decorated function, ``with_current_defaults`` actually works (thanks @samstav).
|
||||||
- 1.3.0
|
- 1.3.0
|
||||||
- Fix/add support for urllib3 (thanks @aisch)
|
- Fix/add support for urllib3 (thanks @aisch)
|
||||||
- Fix default port for https (thanks @abhinav).
|
- Fix default port for https (thanks @abhinav).
|
||||||
- 1.2.0
|
- 1.2.0
|
||||||
- Add custom\_patches argument to VCR/Cassette objects to allow users to stub custom classes when cassettes become active.
|
- Add custom\_patches argument to VCR/Cassette objects to allow users to stub custom classes when cassettes become active.
|
||||||
- 1.1.4
|
- 1.1.4
|
||||||
- Add force reset around calls to actual connection from stubs, to ensure compatibility with the version of httplib/urlib2 in python 2.7.9.
|
- Add force reset around calls to actual connection from stubs, to ensure compatibility with the version of httplib/urlib2 in python 2.7.9.
|
||||||
@@ -145,22 +224,22 @@ All help in providing PRs to close out bug issues is appreciated. Even if that i
|
|||||||
- fix Windows connectionpool stub bug (thanks @gazpachoking)
|
- fix Windows connectionpool stub bug (thanks @gazpachoking)
|
||||||
- add support for requests 2.5
|
- add support for requests 2.5
|
||||||
- 1.1.2
|
- 1.1.2
|
||||||
- Add urllib==1.7.1 support.
|
- Add urllib==1.7.1 support.
|
||||||
- Make json serialize error handling correct
|
- Make json serialize error handling correct
|
||||||
- Improve logging of match failures.
|
- Improve logging of match failures.
|
||||||
- 1.1.1
|
- 1.1.1
|
||||||
- Use function signature preserving ``wrapt.decorator`` to write the decorator version of use\_cassette in order to ensure compatibility with py.test fixtures and python 2.
|
- Use function signature preserving ``wrapt.decorator`` to write the decorator version of use\_cassette in order to ensure compatibility with py.test fixtures and python 2.
|
||||||
- Move all request filtering into the ``before_record_callable``.
|
- Move all request filtering into the ``before_record_callable``.
|
||||||
- 1.1.0
|
- 1.1.0
|
||||||
- Add ``before_record_response``. Fix several bugs related to the context management of cassettes.
|
- Add ``before_record_response``. Fix several bugs related to the context management of cassettes.
|
||||||
- 1.0.3
|
- 1.0.3
|
||||||
- Fix an issue with requests 2.4 and make sure case sensitivity is consistent across python versions
|
- Fix an issue with requests 2.4 and make sure case sensitivity is consistent across python versions
|
||||||
- 1.0.2
|
- 1.0.2
|
||||||
- Fix an issue with requests 2.3
|
- Fix an issue with requests 2.3
|
||||||
- 1.0.1
|
- 1.0.1
|
||||||
- Fix a bug with the new ignore requests feature and the once record mode
|
- Fix a bug with the new ignore requests feature and the once record mode
|
||||||
- 1.0.0
|
- 1.0.0
|
||||||
- *BACKWARDS INCOMPATIBLE*: Please see the 'upgrade' section in the README. Take a look at the matcher section as well, you might want to update your ``match_on`` settings.
|
- *BACKWARDS INCOMPATIBLE*: Please see the 'upgrade' section in the README. Take a look at the matcher section as well, you might want to update your ``match_on`` settings.
|
||||||
- Add support for filtering sensitive data from requests, matching query strings after the order changes and improving the built-in matchers, (thanks to @mshytikov)
|
- Add support for filtering sensitive data from requests, matching query strings after the order changes and improving the built-in matchers, (thanks to @mshytikov)
|
||||||
- Support for ignoring requests to certain hosts, bump supported Python3 version to 3.4, fix some bugs with Boto support (thanks @marusich)
|
- Support for ignoring requests to certain hosts, bump supported Python3 version to 3.4, fix some bugs with Boto support (thanks @marusich)
|
||||||
- Fix error with URL field capitalization in README (thanks @simon-weber)
|
- Fix error with URL field capitalization in README (thanks @simon-weber)
|
||||||
@@ -168,27 +247,27 @@ All help in providing PRs to close out bug issues is appreciated. Even if that i
|
|||||||
- Added ``all_played`` property on cassette (thanks @mshytikov)
|
- Added ``all_played`` property on cassette (thanks @mshytikov)
|
||||||
|
|
||||||
- 0.7.0
|
- 0.7.0
|
||||||
- VCR.py now supports Python 3! (thanks @asundg)
|
- VCR.py now supports Python 3! (thanks @asundg)
|
||||||
- Also I refactored the stub connections quite a bit to add support for the putrequest and putheader calls.
|
- Also I refactored the stub connections quite a bit to add support for the putrequest and putheader calls.
|
||||||
- This version also adds support for httplib2 (thanks @nilp0inter).
|
- This version also adds support for httplib2 (thanks @nilp0inter).
|
||||||
- I have added a couple tests for boto since it is an http client in its own right.
|
- I have added a couple tests for boto since it is an http client in its own right.
|
||||||
- Finally, this version includes a fix for a bug where requests wasn't being patched properly (thanks @msabramo).
|
- Finally, this version includes a fix for a bug where requests wasn't being patched properly (thanks @msabramo).
|
||||||
- 0.6.0
|
- 0.6.0
|
||||||
- Store response headers as a list since a HTTP response can have the same header twice (happens with set-cookie sometimes).
|
- Store response headers as a list since a HTTP response can have the same header twice (happens with set-cookie sometimes).
|
||||||
- This has the added benefit of preserving the order of headers.
|
- This has the added benefit of preserving the order of headers.
|
||||||
- Thanks @smallcode for the bug report leading to this change.
|
- Thanks @smallcode for the bug report leading to this change.
|
||||||
- I have made an effort to ensure backwards compatibility with the old cassettes' header storage mechanism, but if you want to upgrade to the new header storage, you should delete your cassettes and re-record them.
|
- I have made an effort to ensure backwards compatibility with the old cassettes' header storage mechanism, but if you want to upgrade to the new header storage, you should delete your cassettes and re-record them.
|
||||||
- Also this release adds better error messages (thanks @msabramo)
|
- Also this release adds better error messages (thanks @msabramo)
|
||||||
- and adds support for using VCR as a decorator (thanks @smallcode for the motivation)
|
- and adds support for using VCR as a decorator (thanks @smallcode for the motivation)
|
||||||
- 0.5.0
|
- 0.5.0
|
||||||
- Change the ``response_of`` method to ``responses_of`` since cassettes can now contain more than one response for a request.
|
- Change the ``response_of`` method to ``responses_of`` since cassettes can now contain more than one response for a request.
|
||||||
- Since this changes the API, I'm bumping the version.
|
- Since this changes the API, I'm bumping the version.
|
||||||
- Also includes 2 bugfixes:
|
- Also includes 2 bugfixes:
|
||||||
- a better error message when attempting to overwrite a cassette file,
|
- a better error message when attempting to overwrite a cassette file,
|
||||||
- and a fix for a bug with requests sessions (thanks @msabramo)
|
- and a fix for a bug with requests sessions (thanks @msabramo)
|
||||||
- 0.4.0
|
- 0.4.0
|
||||||
- Change default request recording behavior for multiple requests.
|
- Change default request recording behavior for multiple requests.
|
||||||
- If you make the same request multiple times to the same URL, the response might be different each time (maybe the response has a timestamp in it or something), so this will make the same request multiple times and save them all.
|
- If you make the same request multiple times to the same URL, the response might be different each time (maybe the response has a timestamp in it or something), so this will make the same request multiple times and save them all.
|
||||||
- Then, when you are replaying the cassette, the responses will be played back in the same order in which they were received.
|
- Then, when you are replaying the cassette, the responses will be played back in the same order in which they were received.
|
||||||
- If you were making multiple requests to the same URL in a cassette before version 0.4.0, you might need to regenerate your cassette files.
|
- If you were making multiple requests to the same URL in a cassette before version 0.4.0, you might need to regenerate your cassette files.
|
||||||
- Also, removes support for the cassette.play\_count counter API, since individual requests aren't unique anymore.
|
- Also, removes support for the cassette.play\_count counter API, since individual requests aren't unique anymore.
|
||||||
@@ -200,7 +279,7 @@ All help in providing PRs to close out bug issues is appreciated. Even if that i
|
|||||||
- 0.3.4
|
- 0.3.4
|
||||||
- Bugfix: close file before renaming it. This fixes an issue on Windows. Thanks @smallcode for the fix.
|
- Bugfix: close file before renaming it. This fixes an issue on Windows. Thanks @smallcode for the fix.
|
||||||
- 0.3.3
|
- 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
|
- 0.3.2
|
||||||
- Fix issue with new config syntax and the ``match_on`` parameter. Thanks, @chromy!
|
- Fix issue with new config syntax and the ``match_on`` parameter. Thanks, @chromy!
|
||||||
- 0.3.1
|
- 0.3.1
|
||||||
@@ -208,7 +287,7 @@ All help in providing PRs to close out bug issues is appreciated. Even if that i
|
|||||||
- 0.3.0
|
- 0.3.0
|
||||||
- *Backwards incompatible release*
|
- *Backwards incompatible release*
|
||||||
- Added support for record modes, and changed the default recording behavior to the "once" record mode. Please see the documentation on record modes for more.
|
- Added support for record modes, and changed the default recording behavior to the "once" record mode. Please see the documentation on record modes for more.
|
||||||
- Added support for custom request matching, and changed the default request matching behavior to match only on the URL and method.
|
- Added support for custom request matching, and changed the default request matching behavior to match only on the URL and method.
|
||||||
- Also, improved the httplib mocking to add support for the ``HTTPConnection.send()`` method.
|
- Also, improved the httplib mocking to add support for the ``HTTPConnection.send()`` method.
|
||||||
- This means that requests won't actually be sent until the response is read, since I need to record the entire request in order to match up the appropriate response.
|
- This means that requests won't actually be sent until the response is read, since I need to record the entire request in order to match up the appropriate response.
|
||||||
- I don't think this should cause any issues unless you are sending requests without ever loading the response (which none of the standard httplib wrappers do, as far as I know).
|
- I don't think this should cause any issues unless you are sending requests without ever loading the response (which none of the standard httplib wrappers do, as far as I know).
|
||||||
@@ -216,13 +295,13 @@ All help in providing PRs to close out bug issues is appreciated. Even if that i
|
|||||||
- 0.2.1
|
- 0.2.1
|
||||||
- Fixed missing modules in setup.py
|
- Fixed missing modules in setup.py
|
||||||
- 0.2.0
|
- 0.2.0
|
||||||
- Added configuration API, which lets you configure some settings on VCR (see the README).
|
- Added configuration API, which lets you configure some settings on VCR (see the README).
|
||||||
- Also, VCR no longer saves cassettes if they haven't changed at all and supports JSON as well as YAML (thanks @sirpengi).
|
- Also, VCR no longer saves cassettes if they haven't changed at all and supports JSON as well as YAML (thanks @sirpengi).
|
||||||
- Added amazing new skeumorphic logo, thanks @hairarrow.
|
- Added amazing new skeumorphic logo, thanks @hairarrow.
|
||||||
- 0.1.0
|
- 0.1.0
|
||||||
- *backwards incompatible release - delete your old cassette files*
|
- *backwards incompatible release - delete your old cassette files*
|
||||||
- This release adds the ability to access the cassette to make assertions on it
|
- This release adds the ability to access the cassette to make assertions on it
|
||||||
- as well as a major code refactor thanks to @dlecocq.
|
- as well as a major code refactor thanks to @dlecocq.
|
||||||
- It also fixes a couple longstanding bugs with redirects and HTTPS. [#3 and #4]
|
- It also fixes a couple longstanding bugs with redirects and HTTPS. [#3 and #4]
|
||||||
- 0.0.4
|
- 0.0.4
|
||||||
- If you have libyaml installed, vcrpy will use the c bindings instead. Speed up your tests! Thanks @dlecocq
|
- If you have libyaml installed, vcrpy will use the c bindings instead. Speed up your tests! Thanks @dlecocq
|
||||||
@@ -232,4 +311,3 @@ All help in providing PRs to close out bug issues is appreciated. Even if that i
|
|||||||
- Add support for requests / urllib3
|
- Add support for requests / urllib3
|
||||||
- 0.0.1
|
- 0.0.1
|
||||||
- Initial Release
|
- Initial Release
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# vcrpy documentation build configuration file, created by
|
# vcrpy documentation build configuration file, created by
|
||||||
# sphinx-quickstart on Sun Sep 13 11:18:00 2015.
|
# sphinx-quickstart on Sun Sep 13 11:18:00 2015.
|
||||||
@@ -94,7 +93,7 @@ version = release = find_version("..", "vcr", "__init__.py")
|
|||||||
#
|
#
|
||||||
# This is also used if you do content translation via gettext catalogs.
|
# This is also used if you do content translation via gettext catalogs.
|
||||||
# Usually you set "language" from the command line for these cases.
|
# Usually you set "language" from the command line for these cases.
|
||||||
language = None
|
language = "en"
|
||||||
|
|
||||||
# There are two options for replacing |today|: either, you set today to some
|
# There are two options for replacing |today|: either, you set today to some
|
||||||
# non-false value, then it is used:
|
# non-false value, then it is used:
|
||||||
@@ -317,5 +316,5 @@ texinfo_documents = [
|
|||||||
|
|
||||||
|
|
||||||
# Example configuration for intersphinx: refer to the Python standard library.
|
# Example configuration for intersphinx: refer to the Python standard library.
|
||||||
intersphinx_mapping = {"https://docs.python.org/": None}
|
intersphinx_mapping = {"python": ("https://docs.python.org/3", None)}
|
||||||
html_theme = "alabaster"
|
html_theme = "alabaster"
|
||||||
|
|||||||
@@ -42,7 +42,8 @@ This can be configured by changing the ``match_on`` setting.
|
|||||||
The following options are available :
|
The following options are available :
|
||||||
|
|
||||||
- method (for example, POST or GET)
|
- method (for example, POST or GET)
|
||||||
- uri (the full URI.)
|
- uri (the full URI)
|
||||||
|
- scheme (for example, HTTP or HTTPS)
|
||||||
- host (the hostname of the server receiving the request)
|
- host (the hostname of the server receiving the request)
|
||||||
- port (the port of the server receiving the request)
|
- port (the port of the server receiving the request)
|
||||||
- path (the path of the request)
|
- path (the path of the request)
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ So whilst reporting issues are valuable, please consider:
|
|||||||
- contributing an issue with a toy repo that replicates the issue.
|
- contributing an issue with a toy repo that replicates the issue.
|
||||||
- contributing PRs is a more valuable donation of your time and effort.
|
- contributing PRs is a more valuable donation of your time and effort.
|
||||||
|
|
||||||
Thanks again for your interest and support in VCRpy.
|
Thanks again for your interest and support in VCRpy.
|
||||||
|
|
||||||
We really appreciate it.
|
We really appreciate it.
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ Simply adding these three labels for incoming issues means a lot for maintaining
|
|||||||
- Which library does it affect? ``core``, ``aiohttp``, ``requests``, ``urllib3``, ``tornado4``, ``httplib2``
|
- Which library does it affect? ``core``, ``aiohttp``, ``requests``, ``urllib3``, ``tornado4``, ``httplib2``
|
||||||
- If it is a bug, is it ``Verified Can Replicate`` or ``Requires Help Replicating``
|
- If it is a bug, is it ``Verified Can Replicate`` or ``Requires Help Replicating``
|
||||||
- Thanking people for raising issues. Feedback is always appreciated.
|
- Thanking people for raising issues. Feedback is always appreciated.
|
||||||
- Politely asking if they are able to link to an example repo that replicates the issue if they haven't already. Being able to *clone and go* helps the next person and we like that. 😃
|
- Politely asking if they are able to link to an example repo that replicates the issue if they haven't already. Being able to *clone and go* helps the next person and we like that. 😃
|
||||||
|
|
||||||
**Maintainer:**
|
**Maintainer:**
|
||||||
|
|
||||||
@@ -68,13 +68,13 @@ This involves creating PRs to address bugs and enhancement requests. It also mea
|
|||||||
The PR reviewer is a second set of eyes to see if:
|
The PR reviewer is a second set of eyes to see if:
|
||||||
- Are there tests covering the code paths added/modified?
|
- Are there tests covering the code paths added/modified?
|
||||||
- Do the tests and modifications make sense seem appropriate?
|
- Do the tests and modifications make sense seem appropriate?
|
||||||
- Add specific feedback, even on approvals, why it is accepted. eg "I like how you use a context manager there. 😄 "
|
- Add specific feedback, even on approvals, why it is accepted. eg "I like how you use a context manager there. 😄 "
|
||||||
- Also make sure they add a line to `docs/changelog.rst` to claim credit for their contribution.
|
- Also make sure they add a line to `docs/changelog.rst` to claim credit for their contribution.
|
||||||
|
|
||||||
**Release Manager:**
|
**Release Manager:**
|
||||||
- Ensure CI is passing.
|
- Ensure CI is passing.
|
||||||
- Create a release on github and tag it with the changelog release notes.
|
- Create a release on github and tag it with the changelog release notes.
|
||||||
- ``python setup.py build sdist bdist_wheel``
|
- ``python3 setup.py build sdist bdist_wheel``
|
||||||
- ``twine upload dist/*``
|
- ``twine upload dist/*``
|
||||||
- Go to ReadTheDocs build page and trigger a build https://readthedocs.org/projects/vcrpy/builds/
|
- Go to ReadTheDocs build page and trigger a build https://readthedocs.org/projects/vcrpy/builds/
|
||||||
|
|
||||||
@@ -83,39 +83,21 @@ The PR reviewer is a second set of eyes to see if:
|
|||||||
Running VCR's test suite
|
Running VCR's test suite
|
||||||
------------------------
|
------------------------
|
||||||
|
|
||||||
The tests are all run automatically on `Travis
|
The tests are all run automatically on `Github Actions CI <https://github.com/kevin1024/vcrpy/actions>`__,
|
||||||
CI <https://travis-ci.org/kevin1024/vcrpy>`__, but you can also run them
|
but you can also run them yourself using `pytest <http://pytest.org/>`__.
|
||||||
yourself using `pytest <http://pytest.org/>`__ and
|
|
||||||
`Tox <http://tox.testrun.org/>`__.
|
|
||||||
|
|
||||||
Tox will automatically run them in all environments VCR.py supports if they are available on your `PATH`. Alternatively you can use `tox-pyenv <https://pypi.org/project/tox-pyenv/>`_ with
|
In order for the boto3 tests to run, you will need an AWS key.
|
||||||
`pyenv <https://github.com/pyenv/pyenv>`_.
|
Refer to the `boto3
|
||||||
We recommend you read the documentation for each and see the section further below.
|
documentation <https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/index.html>`__
|
||||||
|
for how to set this up. I have marked the boto3 tests as optional in
|
||||||
The test suite is pretty big and slow, but you can tell tox to only run specific tests like this::
|
|
||||||
|
|
||||||
tox -e {pyNN}-{HTTP_LIBRARY} -- <pytest flags passed through>
|
|
||||||
|
|
||||||
tox -e py36-requests -- -v -k "'test_status_code or test_gzip'"
|
|
||||||
tox -e py37-requests -- -v --last-failed
|
|
||||||
|
|
||||||
This will run only tests that look like ``test_status_code`` or
|
|
||||||
``test_gzip`` in the test suite, and only in the python 3.6 environment
|
|
||||||
that has ``requests`` installed.
|
|
||||||
|
|
||||||
Also, in order for the boto tests to run, you will need an AWS key.
|
|
||||||
Refer to the `boto
|
|
||||||
documentation <https://boto.readthedocs.io/en/latest/getting_started.html>`__
|
|
||||||
for how to set this up. I have marked the boto tests as optional in
|
|
||||||
Travis so you don't have to worry about them failing if you submit a
|
Travis so you don't have to worry about them failing if you submit a
|
||||||
pull request.
|
pull request.
|
||||||
|
|
||||||
Using PyEnv with VCR's test suite
|
Using Pyenv with VCR's test suite
|
||||||
---------------------------------
|
---------------------------------
|
||||||
|
|
||||||
PyEnv is a tool for managing multiple installation of python on your system.
|
Pyenv is a tool for managing multiple installation of python on your system.
|
||||||
See the full documentation at their `github <https://github.com/pyenv/pyenv>`_
|
See the full documentation at their `github <https://github.com/pyenv/pyenv>`_
|
||||||
but we are also going to use `tox-pyenv <https://pypi.org/project/tox-pyenv/>`_
|
|
||||||
in this example::
|
in this example::
|
||||||
|
|
||||||
git clone https://github.com/pyenv/pyenv ~/.pyenv
|
git clone https://github.com/pyenv/pyenv ~/.pyenv
|
||||||
@@ -126,27 +108,21 @@ in this example::
|
|||||||
# Setup shim paths
|
# Setup shim paths
|
||||||
eval "$(pyenv init -)"
|
eval "$(pyenv init -)"
|
||||||
|
|
||||||
# Setup your local system tox tooling
|
|
||||||
pip install tox tox-pyenv
|
|
||||||
|
|
||||||
# Install supported versions (at time of writing), this does not activate them
|
# 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.12.0 pypy3.10
|
||||||
|
|
||||||
# This activates them
|
# 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.12.0 pypy3.10
|
||||||
|
|
||||||
# Run the whole test suite
|
# Run the whole test suite
|
||||||
tox
|
pip install .[tests]
|
||||||
|
./runtests.sh
|
||||||
# Run the whole test suite or just part of it
|
|
||||||
tox -e lint
|
|
||||||
tox -e py37-requests
|
|
||||||
|
|
||||||
|
|
||||||
Troubleshooting on MacOSX
|
Troubleshooting on MacOSX
|
||||||
-------------------------
|
-------------------------
|
||||||
|
|
||||||
If you have this kind of error when running tox :
|
If you have this kind of error when running tests :
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
|
|||||||
@@ -4,25 +4,25 @@ Installation
|
|||||||
VCR.py is a package on `PyPI <https://pypi.python.org>`__, so you can install
|
VCR.py is a package on `PyPI <https://pypi.python.org>`__, so you can install
|
||||||
with pip::
|
with pip::
|
||||||
|
|
||||||
pip install vcrpy
|
pip3 install vcrpy
|
||||||
|
|
||||||
Compatibility
|
Compatibility
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
VCR.py supports Python 3.5+, and `pypy <http://pypy.org>`__.
|
VCR.py supports Python 3.9+, and `pypy <http://pypy.org>`__.
|
||||||
|
|
||||||
The following HTTP libraries are supported:
|
The following HTTP libraries are supported:
|
||||||
|
|
||||||
- ``aiohttp``
|
- ``aiohttp``
|
||||||
- ``boto``
|
|
||||||
- ``boto3``
|
- ``boto3``
|
||||||
- ``http.client``
|
- ``http.client``
|
||||||
- ``httplib2``
|
- ``httplib2``
|
||||||
- ``requests`` (both 1.x and 2.x versions)
|
- ``requests`` (>=2.16.2 versions)
|
||||||
- ``tornado.httpclient``
|
- ``tornado.httpclient``
|
||||||
- ``urllib2``
|
- ``urllib2``
|
||||||
- ``urllib3``
|
- ``urllib3``
|
||||||
- ``httpx``
|
- ``httpx``
|
||||||
|
- ``httpcore``
|
||||||
|
|
||||||
Speed
|
Speed
|
||||||
-----
|
-----
|
||||||
@@ -35,7 +35,7 @@ rebuilding pyyaml.
|
|||||||
|
|
||||||
1. Test if pyyaml is built with libyaml. This should work::
|
1. Test if pyyaml is built with libyaml. This should work::
|
||||||
|
|
||||||
python -c 'from yaml import CLoader'
|
python3 -c 'from yaml import CLoader'
|
||||||
|
|
||||||
2. Install libyaml according to your Linux distribution, or using `Homebrew
|
2. Install libyaml according to your Linux distribution, or using `Homebrew
|
||||||
<http://mxcl.github.com/homebrew/>`__ on Mac::
|
<http://mxcl.github.com/homebrew/>`__ on Mac::
|
||||||
@@ -46,8 +46,8 @@ rebuilding pyyaml.
|
|||||||
|
|
||||||
3. Rebuild pyyaml with libyaml::
|
3. Rebuild pyyaml with libyaml::
|
||||||
|
|
||||||
pip uninstall pyyaml
|
pip3 uninstall pyyaml
|
||||||
pip --no-cache-dir install pyyaml
|
pip3 --no-cache-dir install pyyaml
|
||||||
|
|
||||||
Upgrade
|
Upgrade
|
||||||
-------
|
-------
|
||||||
@@ -61,7 +61,7 @@ is to simply delete your cassettes and re-record all of them. VCR.py
|
|||||||
also provides a migration script that attempts to upgrade your 0.x
|
also provides a migration script that attempts to upgrade your 0.x
|
||||||
cassettes to the new 1.x format. To use it, run the following command::
|
cassettes to the new 1.x format. To use it, run the following command::
|
||||||
|
|
||||||
python -m vcr.migration PATH
|
python3 -m vcr.migration PATH
|
||||||
|
|
||||||
The PATH can be either a path to the directory with cassettes or the
|
The PATH can be either a path to the directory with cassettes or the
|
||||||
path to a single cassette.
|
path to a single cassette.
|
||||||
|
|||||||
2
docs/requirements.txt
Normal file
2
docs/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
sphinx<9
|
||||||
|
sphinx_rtd_theme==3.0.2
|
||||||
@@ -4,11 +4,11 @@ Usage
|
|||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
import vcr
|
import vcr
|
||||||
import urllib2
|
import urllib.request
|
||||||
|
|
||||||
with vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml'):
|
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
|
assert b'Example domains' in response
|
||||||
|
|
||||||
Run this test once, and VCR.py will record the HTTP request to
|
Run this test once, and VCR.py will record the HTTP request to
|
||||||
``fixtures/vcr_cassettes/synopsis.yaml``. Run it again, and VCR.py will
|
``fixtures/vcr_cassettes/synopsis.yaml``. Run it again, and VCR.py will
|
||||||
@@ -25,8 +25,8 @@ look like this:
|
|||||||
|
|
||||||
@vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml')
|
@vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml')
|
||||||
def test_iana():
|
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
|
assert b'Example domains' in response
|
||||||
|
|
||||||
When using the decorator version of ``use_cassette``, it is possible to
|
When using the decorator version of ``use_cassette``, it is possible to
|
||||||
omit the path to the cassette file.
|
omit the path to the cassette file.
|
||||||
@@ -35,8 +35,8 @@ omit the path to the cassette file.
|
|||||||
|
|
||||||
@vcr.use_cassette()
|
@vcr.use_cassette()
|
||||||
def test_iana():
|
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
|
assert b'Example domains' in response
|
||||||
|
|
||||||
In this case, the cassette file will be given the same name as the test
|
In this case, the cassette file will be given the same name as the test
|
||||||
function, and it will be placed in the same directory as the file in
|
function, and it will be placed in the same directory as the file in
|
||||||
@@ -92,9 +92,73 @@ all
|
|||||||
Unittest Integration
|
Unittest Integration
|
||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
While it's possible to use the context manager or decorator forms with unittest,
|
Inherit from ``VCRTestCase`` for automatic recording and playback of HTTP
|
||||||
there's also a ``VCRTestCase`` provided separately by `vcrpy-unittest
|
interactions.
|
||||||
<https://github.com/agriffis/vcrpy-unittest>`__.
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
from vcr.unittest import VCRTestCase
|
||||||
|
import requests
|
||||||
|
|
||||||
|
class MyTestCase(VCRTestCase):
|
||||||
|
def test_something(self):
|
||||||
|
response = requests.get('http://example.com')
|
||||||
|
|
||||||
|
Similar to how VCR.py returns the cassette from the context manager,
|
||||||
|
``VCRTestCase`` makes the cassette available as ``self.cassette``:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
self.assertEqual(len(self.cassette), 1)
|
||||||
|
self.assertEqual(self.cassette.requests[0].uri, 'http://example.com')
|
||||||
|
|
||||||
|
By default cassettes will be placed in the ``cassettes`` subdirectory next to the
|
||||||
|
test, named according to the test class and method. For example, the above test
|
||||||
|
would read from and write to ``cassettes/MyTestCase.test_something.yaml``
|
||||||
|
|
||||||
|
The configuration can be modified by overriding methods on your subclass:
|
||||||
|
``_get_vcr_kwargs``, ``_get_cassette_library_dir`` and ``_get_cassette_name``.
|
||||||
|
To modify the ``VCR`` object after instantiation, for example to add a matcher,
|
||||||
|
you can hook on ``_get_vcr``, for example:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
class MyTestCase(VCRTestCase):
|
||||||
|
def _get_vcr(self, **kwargs):
|
||||||
|
myvcr = super(MyTestCase, self)._get_vcr(**kwargs)
|
||||||
|
myvcr.register_matcher('mymatcher', mymatcher)
|
||||||
|
myvcr.match_on = ['mymatcher']
|
||||||
|
return myvcr
|
||||||
|
|
||||||
|
See
|
||||||
|
`the source
|
||||||
|
<https://github.com/kevin1024/vcrpy/blob/master/vcr/unittest.py>`__
|
||||||
|
for the default implementations of these methods.
|
||||||
|
|
||||||
|
If you implement a ``setUp`` method on your test class then make sure to call
|
||||||
|
the parent version ``super().setUp()`` in your own in order to continue getting
|
||||||
|
the cassettes produced.
|
||||||
|
|
||||||
|
VCRMixin
|
||||||
|
~~~~~~~~
|
||||||
|
|
||||||
|
In case inheriting from ``VCRTestCase`` is difficult because of an existing
|
||||||
|
class hierarchy containing tests in the base classes, inherit from ``VCRMixin``
|
||||||
|
instead.
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
from vcr.unittest import VCRMixin
|
||||||
|
import requests
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
class MyTestMixin(VCRMixin):
|
||||||
|
def test_something(self):
|
||||||
|
response = requests.get(self.url)
|
||||||
|
|
||||||
|
class MyTestCase(MyTestMixin, unittest.TestCase):
|
||||||
|
url = 'http://example.com'
|
||||||
|
|
||||||
|
|
||||||
Pytest Integration
|
Pytest Integration
|
||||||
------------------
|
------------------
|
||||||
|
|||||||
@@ -1,2 +1,32 @@
|
|||||||
[tool.black]
|
[tool.codespell]
|
||||||
line-length=110
|
skip = '.git,*.pdf,*.svg,.tox'
|
||||||
|
ignore-regex = "\\\\[fnrstv]"
|
||||||
|
|
||||||
|
[tool.pytest]
|
||||||
|
addopts = ["--strict-config", "--strict-markers"]
|
||||||
|
asyncio_default_fixture_loop_scope = "session"
|
||||||
|
asyncio_default_test_loop_scope = "session"
|
||||||
|
markers = ["online"]
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
line-length = 110
|
||||||
|
target-version = "py310"
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
select = [
|
||||||
|
"B", # flake8-bugbear
|
||||||
|
"C4", # flake8-comprehensions
|
||||||
|
"COM", # flake8-commas
|
||||||
|
"E", # pycodestyle error
|
||||||
|
"F", # pyflakes
|
||||||
|
"I", # isort
|
||||||
|
"ISC", # flake8-implicit-str-concat
|
||||||
|
"PIE", # flake8-pie
|
||||||
|
"RUF", # Ruff-specific rules
|
||||||
|
"UP", # pyupgrade
|
||||||
|
"W", # pycodestyle warning
|
||||||
|
"SIM",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.ruff.lint.isort]
|
||||||
|
known-first-party = ["vcr"]
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# https://blog.ionelmc.ro/2015/04/14/tox-tricks-and-patterns/#when-it-inevitably-leads-to-shell-scripts
|
# If you are getting an INVOCATION ERROR for this script then there is a good chance you are running on Windows.
|
||||||
# If you are getting an INVOCATION ERROR for this script then there is
|
# You can and should use WSL for running tests on Windows when it calls bash scripts.
|
||||||
# a good chance you are running on Windows.
|
REQUESTS_CA_BUNDLE=`python3 -m pytest_httpbin.certs` exec pytest "$@"
|
||||||
# You can and should use WSL for running tox on Windows when it calls bash scripts.
|
|
||||||
REQUESTS_CA_BUNDLE=`python -m pytest_httpbin.certs` py.test $*
|
|
||||||
|
|||||||
59
setup.py
59
setup.py
@@ -3,12 +3,11 @@
|
|||||||
import codecs
|
import codecs
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
from pathlib import Path
|
||||||
|
|
||||||
from setuptools import setup, find_packages
|
from setuptools import find_packages, setup
|
||||||
from setuptools.command.test import test as TestCommand
|
|
||||||
|
|
||||||
long_description = open("README.rst", "r").read()
|
long_description = Path("README.rst").read_text()
|
||||||
here = os.path.abspath(os.path.dirname(__file__))
|
here = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
|
||||||
|
|
||||||
@@ -28,28 +27,33 @@ def find_version(*file_paths):
|
|||||||
raise RuntimeError("Unable to find version string.")
|
raise RuntimeError("Unable to find version string.")
|
||||||
|
|
||||||
|
|
||||||
class PyTest(TestCommand):
|
|
||||||
def finalize_options(self):
|
|
||||||
TestCommand.finalize_options(self)
|
|
||||||
self.test_args = []
|
|
||||||
self.test_suite = True
|
|
||||||
|
|
||||||
def run_tests(self):
|
|
||||||
# import here, cause outside the eggs aren't loaded
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
errno = pytest.main(self.test_args)
|
|
||||||
sys.exit(errno)
|
|
||||||
|
|
||||||
|
|
||||||
install_requires = [
|
install_requires = [
|
||||||
"PyYAML",
|
"PyYAML",
|
||||||
"wrapt",
|
"wrapt",
|
||||||
"six>=1.5",
|
|
||||||
'yarl; python_version>="3.6"',
|
|
||||||
'yarl<1.4; python_version=="3.5"',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
extras_require = {
|
||||||
|
"tests": [
|
||||||
|
"aiohttp",
|
||||||
|
"boto3",
|
||||||
|
"cryptography",
|
||||||
|
"httpbin",
|
||||||
|
"httpcore",
|
||||||
|
"httplib2",
|
||||||
|
"httpx",
|
||||||
|
"pycurl; platform_python_implementation !='PyPy'",
|
||||||
|
"pytest",
|
||||||
|
"pytest-aiohttp",
|
||||||
|
"pytest-asyncio",
|
||||||
|
"pytest-cov",
|
||||||
|
"pytest-httpbin",
|
||||||
|
"requests>=2.22.0",
|
||||||
|
"tornado",
|
||||||
|
"urllib3",
|
||||||
|
"werkzeug==2.0.3",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="vcrpy",
|
name="vcrpy",
|
||||||
version=find_version("vcr", "__init__.py"),
|
version=find_version("vcr", "__init__.py"),
|
||||||
@@ -60,20 +64,21 @@ setup(
|
|||||||
author_email="me@kevinmccarthy.org",
|
author_email="me@kevinmccarthy.org",
|
||||||
url="https://github.com/kevin1024/vcrpy",
|
url="https://github.com/kevin1024/vcrpy",
|
||||||
packages=find_packages(exclude=["tests*"]),
|
packages=find_packages(exclude=["tests*"]),
|
||||||
python_requires=">=3.5",
|
python_requires=">=3.10",
|
||||||
install_requires=install_requires,
|
install_requires=install_requires,
|
||||||
license="MIT",
|
license="MIT",
|
||||||
tests_require=["pytest", "mock", "pytest-httpbin"],
|
extras_require=extras_require,
|
||||||
|
tests_require=extras_require["tests"],
|
||||||
classifiers=[
|
classifiers=[
|
||||||
"Development Status :: 5 - Production/Stable",
|
"Development Status :: 5 - Production/Stable",
|
||||||
"Environment :: Console",
|
"Environment :: Console",
|
||||||
"Intended Audience :: Developers",
|
"Intended Audience :: Developers",
|
||||||
"Programming Language :: Python",
|
"Programming Language :: Python",
|
||||||
"Programming Language :: Python :: 3",
|
"Programming Language :: Python :: 3",
|
||||||
"Programming Language :: Python :: 3.5",
|
"Programming Language :: Python :: 3.10",
|
||||||
"Programming Language :: Python :: 3.6",
|
"Programming Language :: Python :: 3.11",
|
||||||
"Programming Language :: Python :: 3.7",
|
"Programming Language :: Python :: 3.12",
|
||||||
"Programming Language :: Python :: 3.8",
|
"Programming Language :: Python :: 3.13",
|
||||||
"Programming Language :: Python :: 3 :: Only",
|
"Programming Language :: Python :: 3 :: Only",
|
||||||
"Programming Language :: Python :: Implementation :: CPython",
|
"Programming Language :: Python :: Implementation :: CPython",
|
||||||
"Programming Language :: Python :: Implementation :: PyPy",
|
"Programming Language :: Python :: Implementation :: PyPy",
|
||||||
|
|||||||
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
@@ -11,9 +11,12 @@ def assert_cassette_has_one_response(cass):
|
|||||||
assert cass.play_count == 1
|
assert cass.play_count == 1
|
||||||
|
|
||||||
|
|
||||||
def assert_is_json(a_string):
|
def assert_is_json_bytes(b: bytes):
|
||||||
|
assert isinstance(b, bytes)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
json.loads(a_string.decode("utf-8"))
|
json.loads(b)
|
||||||
except Exception:
|
except Exception as error:
|
||||||
assert False
|
raise AssertionError() from error
|
||||||
|
|
||||||
assert True
|
assert True
|
||||||
|
|||||||
6
tests/fixtures/migration/new_cassette.json
vendored
6
tests/fixtures/migration/new_cassette.json
vendored
@@ -15,9 +15,9 @@
|
|||||||
},
|
},
|
||||||
"response": {
|
"response": {
|
||||||
"status": {
|
"status": {
|
||||||
"message": "OK",
|
"message": "OK",
|
||||||
"code": 200
|
"code": 200
|
||||||
},
|
},
|
||||||
"headers": {
|
"headers": {
|
||||||
"access-control-allow-origin": ["*"],
|
"access-control-allow-origin": ["*"],
|
||||||
"content-type": ["application/json"],
|
"content-type": ["application/json"],
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
"server": ["gunicorn/0.17.4"],
|
"server": ["gunicorn/0.17.4"],
|
||||||
"content-length": ["32"],
|
"content-length": ["32"],
|
||||||
"connection": ["keep-alive"]
|
"connection": ["keep-alive"]
|
||||||
},
|
},
|
||||||
"body": {
|
"body": {
|
||||||
"string": "{\n \"origin\": \"217.122.164.194\"\n}"
|
"string": "{\n \"origin\": \"217.122.164.194\"\n}"
|
||||||
}
|
}
|
||||||
|
|||||||
2
tests/fixtures/migration/new_cassette.yaml
vendored
2
tests/fixtures/migration/new_cassette.yaml
vendored
@@ -2,7 +2,7 @@ version: 1
|
|||||||
interactions:
|
interactions:
|
||||||
- request:
|
- request:
|
||||||
body: null
|
body: null
|
||||||
headers:
|
headers:
|
||||||
accept: ['*/*']
|
accept: ['*/*']
|
||||||
accept-encoding: ['gzip, deflate, compress']
|
accept-encoding: ['gzip, deflate, compress']
|
||||||
user-agent: ['python-requests/2.2.1 CPython/2.6.1 Darwin/10.8.0']
|
user-agent: ['python-requests/2.2.1 CPython/2.6.1 Darwin/10.8.0']
|
||||||
|
|||||||
34
tests/fixtures/migration/old_cassette.json
vendored
34
tests/fixtures/migration/old_cassette.json
vendored
@@ -1,31 +1,31 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"request": {
|
"request": {
|
||||||
"body": null,
|
"body": null,
|
||||||
"protocol": "http",
|
"protocol": "http",
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"headers": {
|
"headers": {
|
||||||
"accept-encoding": "gzip, deflate, compress",
|
"accept-encoding": "gzip, deflate, compress",
|
||||||
"accept": "*/*",
|
"accept": "*/*",
|
||||||
"user-agent": "python-requests/2.2.1 CPython/2.6.1 Darwin/10.8.0"
|
"user-agent": "python-requests/2.2.1 CPython/2.6.1 Darwin/10.8.0"
|
||||||
},
|
},
|
||||||
"host": "httpbin.org",
|
"host": "httpbin.org",
|
||||||
"path": "/ip",
|
"path": "/ip",
|
||||||
"port": 80
|
"port": 80
|
||||||
},
|
},
|
||||||
"response": {
|
"response": {
|
||||||
"status": {
|
"status": {
|
||||||
"message": "OK",
|
"message": "OK",
|
||||||
"code": 200
|
"code": 200
|
||||||
},
|
},
|
||||||
"headers": [
|
"headers": [
|
||||||
"access-control-allow-origin: *\r\n",
|
"access-control-allow-origin: *\r\n",
|
||||||
"content-type: application/json\r\n",
|
"content-type: application/json\r\n",
|
||||||
"date: Mon, 21 Apr 2014 23:13:40 GMT\r\n",
|
"date: Mon, 21 Apr 2014 23:13:40 GMT\r\n",
|
||||||
"server: gunicorn/0.17.4\r\n",
|
"server: gunicorn/0.17.4\r\n",
|
||||||
"content-length: 32\r\n",
|
"content-length: 32\r\n",
|
||||||
"connection: keep-alive\r\n"
|
"connection: keep-alive\r\n"
|
||||||
],
|
],
|
||||||
"body": {
|
"body": {
|
||||||
"string": "{\n \"origin\": \"217.122.164.194\"\n}"
|
"string": "{\n \"origin\": \"217.122.164.194\"\n}"
|
||||||
}
|
}
|
||||||
|
|||||||
2
tests/fixtures/wild/domain_redirect.yaml
vendored
2
tests/fixtures/wild/domain_redirect.yaml
vendored
@@ -10,7 +10,7 @@ interactions:
|
|||||||
uri: http://seomoz.org/
|
uri: http://seomoz.org/
|
||||||
response:
|
response:
|
||||||
body: {string: ''}
|
body: {string: ''}
|
||||||
headers:
|
headers:
|
||||||
Location: ['http://moz.com/']
|
Location: ['http://moz.com/']
|
||||||
Server: ['BigIP']
|
Server: ['BigIP']
|
||||||
Connection: ['Keep-Alive']
|
Connection: ['Keep-Alive']
|
||||||
|
|||||||
@@ -2,28 +2,27 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
from aiohttp.test_utils import TestClient
|
|
||||||
|
|
||||||
|
|
||||||
async def aiohttp_request(loop, method, url, output="text", encoding="utf-8", content_type=None, **kwargs):
|
async def aiohttp_request(loop, method, url, output="text", encoding="utf-8", content_type=None, **kwargs):
|
||||||
session = aiohttp.ClientSession(loop=loop)
|
async with aiohttp.ClientSession(loop=loop) as session:
|
||||||
response_ctx = session.request(method, url, **kwargs)
|
response_ctx = session.request(method, url, **kwargs)
|
||||||
|
|
||||||
response = await response_ctx.__aenter__()
|
response = await response_ctx.__aenter__()
|
||||||
if output == "text":
|
if output == "text":
|
||||||
content = await response.text()
|
content = await response.text()
|
||||||
elif output == "json":
|
elif output == "json":
|
||||||
content_type = content_type or "application/json"
|
content_type = content_type or "application/json"
|
||||||
content = await response.json(encoding=encoding, content_type=content_type)
|
content = await response.json(encoding=encoding, content_type=content_type)
|
||||||
elif output == "raw":
|
elif output == "raw":
|
||||||
content = await response.read()
|
content = await response.read()
|
||||||
elif output == "stream":
|
elif output == "stream":
|
||||||
content = await response.content.read()
|
content = await response.content.read()
|
||||||
|
|
||||||
response_ctx._resp.close()
|
response_ctx._resp.close()
|
||||||
await session.close()
|
await session.close()
|
||||||
|
|
||||||
return response, content
|
return response, content
|
||||||
|
|
||||||
|
|
||||||
def aiohttp_app():
|
def aiohttp_app():
|
||||||
|
|||||||
41
tests/integration/cassettes/gzip_httpx_old_format.yaml
Normal file
41
tests/integration/cassettes/gzip_httpx_old_format.yaml
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
interactions:
|
||||||
|
- request:
|
||||||
|
body: ''
|
||||||
|
headers:
|
||||||
|
accept:
|
||||||
|
- '*/*'
|
||||||
|
accept-encoding:
|
||||||
|
- gzip, deflate, br
|
||||||
|
connection:
|
||||||
|
- keep-alive
|
||||||
|
host:
|
||||||
|
- httpbin.org
|
||||||
|
user-agent:
|
||||||
|
- python-httpx/0.23.0
|
||||||
|
method: GET
|
||||||
|
uri: https://httpbin.org/gzip
|
||||||
|
response:
|
||||||
|
content: "{\n \"gzipped\": true, \n \"headers\": {\n \"Accept\": \"*/*\",
|
||||||
|
\n \"Accept-Encoding\": \"gzip, deflate, br\", \n \"Host\": \"httpbin.org\",
|
||||||
|
\n \"User-Agent\": \"python-httpx/0.23.0\", \n \"X-Amzn-Trace-Id\": \"Root=1-62a62a8d-5f39b5c50c744da821d6ea99\"\n
|
||||||
|
\ }, \n \"method\": \"GET\", \n \"origin\": \"146.200.25.115\"\n}\n"
|
||||||
|
headers:
|
||||||
|
Access-Control-Allow-Credentials:
|
||||||
|
- 'true'
|
||||||
|
Access-Control-Allow-Origin:
|
||||||
|
- '*'
|
||||||
|
Connection:
|
||||||
|
- keep-alive
|
||||||
|
Content-Encoding:
|
||||||
|
- gzip
|
||||||
|
Content-Length:
|
||||||
|
- '230'
|
||||||
|
Content-Type:
|
||||||
|
- application/json
|
||||||
|
Date:
|
||||||
|
- Sun, 12 Jun 2022 18:03:57 GMT
|
||||||
|
Server:
|
||||||
|
- gunicorn/19.9.0
|
||||||
|
http_version: HTTP/1.1
|
||||||
|
status_code: 200
|
||||||
|
version: 1
|
||||||
42
tests/integration/cassettes/gzip_requests.yaml
Normal file
42
tests/integration/cassettes/gzip_requests.yaml
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
interactions:
|
||||||
|
- request:
|
||||||
|
body: null
|
||||||
|
headers:
|
||||||
|
Accept:
|
||||||
|
- '*/*'
|
||||||
|
Accept-Encoding:
|
||||||
|
- gzip, deflate, br
|
||||||
|
Connection:
|
||||||
|
- keep-alive
|
||||||
|
User-Agent:
|
||||||
|
- python-requests/2.28.0
|
||||||
|
method: GET
|
||||||
|
uri: https://httpbin.org/gzip
|
||||||
|
response:
|
||||||
|
body:
|
||||||
|
string: !!binary |
|
||||||
|
H4sIAKwrpmIA/z2OSwrCMBCG956izLIkfQSxkl2RogfQA9R2bIM1iUkqaOndnYDIrGa+/zELDB9l
|
||||||
|
LfYgg5uRwYhtj86DXKDuOrQBJKR5Cuy38kZ3pld6oHu0sqTH29QGZMnVkepgtMYuKKNJcEe0vJ3U
|
||||||
|
C4mcjI9hpaiygqaUW7ETFYGLR8frAXXE9h1Go7nD54w++FxkYp8VsDJ4IBH6E47NmVzGqUHFkn8g
|
||||||
|
rJsvp2omYs8AAAA=
|
||||||
|
headers:
|
||||||
|
Access-Control-Allow-Credentials:
|
||||||
|
- 'true'
|
||||||
|
Access-Control-Allow-Origin:
|
||||||
|
- '*'
|
||||||
|
Connection:
|
||||||
|
- Close
|
||||||
|
Content-Encoding:
|
||||||
|
- gzip
|
||||||
|
Content-Length:
|
||||||
|
- '182'
|
||||||
|
Content-Type:
|
||||||
|
- application/json
|
||||||
|
Date:
|
||||||
|
- Sun, 12 Jun 2022 18:08:44 GMT
|
||||||
|
Server:
|
||||||
|
- Pytest-HTTPBIN/0.1.0
|
||||||
|
status:
|
||||||
|
code: 200
|
||||||
|
message: great
|
||||||
|
version: 1
|
||||||
@@ -13,7 +13,7 @@ interactions:
|
|||||||
user-agent:
|
user-agent:
|
||||||
- python-httpx/0.12.1
|
- python-httpx/0.12.1
|
||||||
method: GET
|
method: GET
|
||||||
uri: https://httpbin.org/headers
|
uri: https://mockbin.org/headers
|
||||||
response:
|
response:
|
||||||
content: "{\n \"headers\": {\n \"Accept\": \"*/*\", \n \"Accept-Encoding\"\
|
content: "{\n \"headers\": {\n \"Accept\": \"*/*\", \n \"Accept-Encoding\"\
|
||||||
: \"gzip, deflate, br\", \n \"Host\": \"httpbin.org\", \n \"User-Agent\"\
|
: \"gzip, deflate, br\", \n \"Host\": \"httpbin.org\", \n \"User-Agent\"\
|
||||||
|
|||||||
@@ -1,21 +1,27 @@
|
|||||||
import contextlib
|
|
||||||
import logging
|
import logging
|
||||||
|
import ssl
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
import pytest_httpbin.certs
|
||||||
|
import yarl
|
||||||
|
|
||||||
|
import vcr
|
||||||
|
|
||||||
asyncio = pytest.importorskip("asyncio")
|
asyncio = pytest.importorskip("asyncio")
|
||||||
aiohttp = pytest.importorskip("aiohttp")
|
aiohttp = pytest.importorskip("aiohttp")
|
||||||
|
|
||||||
import vcr # noqa: E402
|
|
||||||
from .aiohttp_utils import aiohttp_app, aiohttp_request # noqa: E402
|
from .aiohttp_utils import aiohttp_app, aiohttp_request # noqa: E402
|
||||||
|
|
||||||
|
HTTPBIN_SSL_CONTEXT = ssl.create_default_context(cafile=pytest_httpbin.certs.where())
|
||||||
|
|
||||||
|
|
||||||
def run_in_loop(fn):
|
def run_in_loop(fn):
|
||||||
with contextlib.closing(asyncio.new_event_loop()) as loop:
|
async def wrapper():
|
||||||
asyncio.set_event_loop(loop)
|
return await fn(asyncio.get_running_loop())
|
||||||
task = loop.create_task(fn(loop))
|
|
||||||
return loop.run_until_complete(task)
|
return asyncio.run(wrapper())
|
||||||
|
|
||||||
|
|
||||||
def request(method, url, output="text", **kwargs):
|
def request(method, url, output="text", **kwargs):
|
||||||
@@ -33,14 +39,10 @@ def post(url, output="text", **kwargs):
|
|||||||
return request("POST", url, output="text", **kwargs)
|
return request("POST", url, output="text", **kwargs)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(params=["https", "http"])
|
@pytest.mark.online
|
||||||
def scheme(request):
|
def test_status(tmpdir, httpbin):
|
||||||
"""Fixture that returns both http and https."""
|
url = httpbin.url
|
||||||
return request.param
|
|
||||||
|
|
||||||
|
|
||||||
def test_status(tmpdir, scheme):
|
|
||||||
url = scheme + "://httpbin.org"
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("status.yaml"))):
|
with vcr.use_cassette(str(tmpdir.join("status.yaml"))):
|
||||||
response, _ = get(url)
|
response, _ = get(url)
|
||||||
|
|
||||||
@@ -50,9 +52,10 @@ def test_status(tmpdir, scheme):
|
|||||||
assert cassette.play_count == 1
|
assert cassette.play_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.online
|
||||||
@pytest.mark.parametrize("auth", [None, aiohttp.BasicAuth("vcrpy", "test")])
|
@pytest.mark.parametrize("auth", [None, aiohttp.BasicAuth("vcrpy", "test")])
|
||||||
def test_headers(tmpdir, scheme, auth):
|
def test_headers(tmpdir, auth, httpbin):
|
||||||
url = scheme + "://httpbin.org"
|
url = httpbin.url
|
||||||
with vcr.use_cassette(str(tmpdir.join("headers.yaml"))):
|
with vcr.use_cassette(str(tmpdir.join("headers.yaml"))):
|
||||||
response, _ = get(url, auth=auth)
|
response, _ = get(url, auth=auth)
|
||||||
|
|
||||||
@@ -61,14 +64,16 @@ def test_headers(tmpdir, scheme, auth):
|
|||||||
request = cassette.requests[0]
|
request = cassette.requests[0]
|
||||||
assert "AUTHORIZATION" in request.headers
|
assert "AUTHORIZATION" in request.headers
|
||||||
cassette_response, _ = get(url, auth=auth)
|
cassette_response, _ = get(url, auth=auth)
|
||||||
assert dict(cassette_response.headers) == dict(response.headers)
|
assert cassette_response.headers.items() == response.headers.items()
|
||||||
assert cassette.play_count == 1
|
assert cassette.play_count == 1
|
||||||
assert "istr" not in cassette.data[0]
|
assert "istr" not in cassette.data[0]
|
||||||
assert "yarl.URL" not in cassette.data[0]
|
assert "yarl.URL" not in cassette.data[0]
|
||||||
|
|
||||||
|
|
||||||
def test_case_insensitive_headers(tmpdir, scheme):
|
@pytest.mark.online
|
||||||
url = scheme + "://httpbin.org"
|
def test_case_insensitive_headers(tmpdir, httpbin):
|
||||||
|
url = httpbin.url
|
||||||
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("whatever.yaml"))):
|
with vcr.use_cassette(str(tmpdir.join("whatever.yaml"))):
|
||||||
_, _ = get(url)
|
_, _ = get(url)
|
||||||
|
|
||||||
@@ -79,8 +84,10 @@ def test_case_insensitive_headers(tmpdir, scheme):
|
|||||||
assert cassette.play_count == 1
|
assert cassette.play_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_text(tmpdir, scheme):
|
@pytest.mark.online
|
||||||
url = scheme + "://httpbin.org"
|
def test_text(tmpdir, httpbin):
|
||||||
|
url = httpbin.url
|
||||||
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("text.yaml"))):
|
with vcr.use_cassette(str(tmpdir.join("text.yaml"))):
|
||||||
_, response_text = get(url)
|
_, response_text = get(url)
|
||||||
|
|
||||||
@@ -90,8 +97,9 @@ def test_text(tmpdir, scheme):
|
|||||||
assert cassette.play_count == 1
|
assert cassette.play_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_json(tmpdir, scheme):
|
@pytest.mark.online
|
||||||
url = scheme + "://httpbin.org/get"
|
def test_json(tmpdir, httpbin):
|
||||||
|
url = httpbin.url + "/json"
|
||||||
headers = {"Content-Type": "application/json"}
|
headers = {"Content-Type": "application/json"}
|
||||||
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("json.yaml"))):
|
with vcr.use_cassette(str(tmpdir.join("json.yaml"))):
|
||||||
@@ -103,8 +111,9 @@ def test_json(tmpdir, scheme):
|
|||||||
assert cassette.play_count == 1
|
assert cassette.play_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_binary(tmpdir, scheme):
|
@pytest.mark.online
|
||||||
url = scheme + "://httpbin.org/image/png"
|
def test_binary(tmpdir, httpbin):
|
||||||
|
url = httpbin.url + "/image/png"
|
||||||
with vcr.use_cassette(str(tmpdir.join("binary.yaml"))):
|
with vcr.use_cassette(str(tmpdir.join("binary.yaml"))):
|
||||||
_, response_binary = get(url, output="raw")
|
_, response_binary = get(url, output="raw")
|
||||||
|
|
||||||
@@ -114,23 +123,25 @@ def test_binary(tmpdir, scheme):
|
|||||||
assert cassette.play_count == 1
|
assert cassette.play_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_stream(tmpdir, scheme):
|
@pytest.mark.online
|
||||||
url = scheme + "://httpbin.org/get"
|
def test_stream(tmpdir, httpbin):
|
||||||
|
url = httpbin.url
|
||||||
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("stream.yaml"))):
|
with vcr.use_cassette(str(tmpdir.join("stream.yaml"))):
|
||||||
resp, body = get(url, output="raw") # Do not use stream here, as the stream is exhausted by vcr
|
_, body = get(url, output="raw") # Do not use stream here, as the stream is exhausted by vcr
|
||||||
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("stream.yaml"))) as cassette:
|
with vcr.use_cassette(str(tmpdir.join("stream.yaml"))) as cassette:
|
||||||
cassette_resp, cassette_body = get(url, output="stream")
|
_, cassette_body = get(url, output="stream")
|
||||||
assert cassette_body == body
|
assert cassette_body == body
|
||||||
assert cassette.play_count == 1
|
assert cassette.play_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.online
|
||||||
@pytest.mark.parametrize("body", ["data", "json"])
|
@pytest.mark.parametrize("body", ["data", "json"])
|
||||||
def test_post(tmpdir, scheme, body, caplog):
|
def test_post(tmpdir, body, caplog, httpbin):
|
||||||
caplog.set_level(logging.INFO)
|
caplog.set_level(logging.INFO)
|
||||||
data = {"key1": "value1", "key2": "value2"}
|
data = {"key1": "value1", "key2": "value2"}
|
||||||
url = scheme + "://httpbin.org/post"
|
url = httpbin.url
|
||||||
with vcr.use_cassette(str(tmpdir.join("post.yaml"))):
|
with vcr.use_cassette(str(tmpdir.join("post.yaml"))):
|
||||||
_, response_json = post(url, **{body: data})
|
_, response_json = post(url, **{body: data})
|
||||||
|
|
||||||
@@ -145,19 +156,21 @@ def test_post(tmpdir, scheme, body, caplog):
|
|||||||
(
|
(
|
||||||
log
|
log
|
||||||
for log in caplog.records
|
for log in caplog.records
|
||||||
if log.getMessage() == "<Request (POST) {}> not in cassette, sending to real server".format(url)
|
if log.getMessage() == f"<Request (POST) {url}> not in cassette, sending to real server"
|
||||||
),
|
),
|
||||||
None,
|
None,
|
||||||
), "Log message not found."
|
), "Log message not found."
|
||||||
|
|
||||||
|
|
||||||
def test_params(tmpdir, scheme):
|
@pytest.mark.online
|
||||||
url = scheme + "://httpbin.org/get"
|
def test_params(tmpdir, httpbin):
|
||||||
|
url = httpbin.url + "/get?d=d"
|
||||||
headers = {"Content-Type": "application/json"}
|
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:
|
with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
|
||||||
_, response_json = get(url, output="json", params=params, headers=headers)
|
_, response_json = get(url, output="json", params=params, headers=headers)
|
||||||
|
assert response_json["args"] == {"a": "1", "b": "2", "c": "c", "d": "d"}
|
||||||
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
|
with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
|
||||||
_, cassette_response_json = get(url, output="json", params=params, headers=headers)
|
_, cassette_response_json = get(url, output="json", params=params, headers=headers)
|
||||||
@@ -165,10 +178,11 @@ def test_params(tmpdir, scheme):
|
|||||||
assert cassette.play_count == 1
|
assert cassette.play_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_params_same_url_distinct_params(tmpdir, scheme):
|
@pytest.mark.online
|
||||||
url = scheme + "://httpbin.org/get"
|
def test_params_same_url_distinct_params(tmpdir, httpbin):
|
||||||
|
url = httpbin.url + "/json"
|
||||||
headers = {"Content-Type": "application/json"}
|
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:
|
with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
|
||||||
_, response_json = get(url, output="json", params=params, headers=headers)
|
_, response_json = get(url, output="json", params=params, headers=headers)
|
||||||
@@ -179,13 +193,16 @@ def test_params_same_url_distinct_params(tmpdir, scheme):
|
|||||||
assert cassette.play_count == 1
|
assert cassette.play_count == 1
|
||||||
|
|
||||||
other_params = {"other": "params"}
|
other_params = {"other": "params"}
|
||||||
with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
|
with (
|
||||||
with pytest.raises(vcr.errors.CannotOverwriteExistingCassetteException):
|
vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette,
|
||||||
get(url, output="text", params=other_params)
|
pytest.raises(vcr.errors.CannotOverwriteExistingCassetteException),
|
||||||
|
):
|
||||||
|
get(url, output="text", params=other_params)
|
||||||
|
|
||||||
|
|
||||||
def test_params_on_url(tmpdir, scheme):
|
@pytest.mark.online
|
||||||
url = scheme + "://httpbin.org/get?a=1&b=foo"
|
def test_params_on_url(tmpdir, httpbin):
|
||||||
|
url = httpbin.url + "/get?a=1&b=foo"
|
||||||
headers = {"Content-Type": "application/json"}
|
headers = {"Content-Type": "application/json"}
|
||||||
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
|
with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
|
||||||
@@ -249,8 +266,9 @@ def test_aiohttp_test_client_json(aiohttp_client, tmpdir):
|
|||||||
assert cassette.play_count == 1
|
assert cassette.play_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_redirect(aiohttp_client, tmpdir):
|
@pytest.mark.online
|
||||||
url = "https://mockbin.org/redirect/302/2"
|
def test_redirect(tmpdir, httpbin):
|
||||||
|
url = httpbin.url + "/redirect/2"
|
||||||
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("redirect.yaml"))):
|
with vcr.use_cassette(str(tmpdir.join("redirect.yaml"))):
|
||||||
response, _ = get(url)
|
response, _ = get(url)
|
||||||
@@ -267,15 +285,14 @@ def test_redirect(aiohttp_client, tmpdir):
|
|||||||
# looking request_info.
|
# looking request_info.
|
||||||
assert cassette_response.request_info.url == response.request_info.url
|
assert cassette_response.request_info.url == response.request_info.url
|
||||||
assert cassette_response.request_info.method == response.request_info.method
|
assert cassette_response.request_info.method == response.request_info.method
|
||||||
assert {k: v for k, v in cassette_response.request_info.headers.items()} == {
|
assert cassette_response.request_info.headers.items() == response.request_info.headers.items()
|
||||||
k: v for k, v in response.request_info.headers.items()
|
|
||||||
}
|
|
||||||
assert cassette_response.request_info.real_url == response.request_info.real_url
|
assert cassette_response.request_info.real_url == response.request_info.real_url
|
||||||
|
|
||||||
|
|
||||||
def test_not_modified(aiohttp_client, tmpdir):
|
@pytest.mark.online
|
||||||
|
def test_not_modified(tmpdir, httpbin):
|
||||||
"""It doesn't try to redirect on 304"""
|
"""It doesn't try to redirect on 304"""
|
||||||
url = "https://httpbin.org/status/304"
|
url = httpbin.url + "/status/304"
|
||||||
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("not_modified.yaml"))):
|
with vcr.use_cassette(str(tmpdir.join("not_modified.yaml"))):
|
||||||
response, _ = get(url)
|
response, _ = get(url)
|
||||||
@@ -290,13 +307,14 @@ def test_not_modified(aiohttp_client, tmpdir):
|
|||||||
assert cassette.play_count == 1
|
assert cassette.play_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_double_requests(tmpdir):
|
@pytest.mark.online
|
||||||
|
def test_double_requests(tmpdir, httpbin):
|
||||||
"""We should capture, record, and replay all requests and response chains,
|
"""We should capture, record, and replay all requests and response chains,
|
||||||
even if there are duplicate ones.
|
even if there are duplicate ones.
|
||||||
|
|
||||||
We should replay in the order we saw them.
|
We should replay in the order we saw them.
|
||||||
"""
|
"""
|
||||||
url = "https://httpbin.org/get"
|
url = httpbin.url
|
||||||
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("text.yaml"))):
|
with vcr.use_cassette(str(tmpdir.join("text.yaml"))):
|
||||||
_, response_text1 = get(url, output="text")
|
_, response_text1 = get(url, output="text")
|
||||||
@@ -321,31 +339,41 @@ def test_double_requests(tmpdir):
|
|||||||
assert cassette.play_count == 2
|
assert cassette.play_count == 2
|
||||||
|
|
||||||
|
|
||||||
def test_cookies(scheme, tmpdir):
|
def test_cookies(httpbin_both, tmpdir):
|
||||||
async def run(loop):
|
async def run(loop):
|
||||||
cookies_url = scheme + (
|
cookies_url = httpbin_both.url + (
|
||||||
"://httpbin.org/response-headers?"
|
"/response-headers?"
|
||||||
"set-cookie=" + urllib.parse.quote("cookie_1=val_1; Path=/") + "&"
|
"set-cookie=" + urllib.parse.quote("cookie_1=val_1; Path=/") + "&"
|
||||||
"Set-Cookie=" + urllib.parse.quote("Cookie_2=Val_2; Path=/")
|
"Set-Cookie=" + urllib.parse.quote("Cookie_2=Val_2; Path=/")
|
||||||
)
|
)
|
||||||
home_url = scheme + "://httpbin.org/"
|
home_url = httpbin_both.url + "/"
|
||||||
tmp = str(tmpdir.join("cookies.yaml"))
|
tmp = str(tmpdir.join("cookies.yaml"))
|
||||||
req_cookies = {"Cookie_3": "Val_3"}
|
req_cookies = {"Cookie_3": "Val_3"}
|
||||||
req_headers = {"Cookie": "Cookie_4=Val_4"}
|
req_headers = {"Cookie": "Cookie_4=Val_4"}
|
||||||
|
|
||||||
# ------------------------- Record -------------------------- #
|
# ------------------------- Record -------------------------- #
|
||||||
with vcr.use_cassette(tmp) as cassette:
|
with vcr.use_cassette(tmp) as cassette:
|
||||||
async with aiohttp.ClientSession(loop=loop) as session:
|
async with aiohttp.ClientSession(loop=loop, cookie_jar=aiohttp.CookieJar(unsafe=True)) as session:
|
||||||
cookies_resp = await session.get(cookies_url)
|
cookies_resp = await session.get(cookies_url, ssl=HTTPBIN_SSL_CONTEXT)
|
||||||
home_resp = await session.get(home_url, cookies=req_cookies, headers=req_headers)
|
home_resp = await session.get(
|
||||||
|
home_url,
|
||||||
|
cookies=req_cookies,
|
||||||
|
headers=req_headers,
|
||||||
|
ssl=HTTPBIN_SSL_CONTEXT,
|
||||||
|
)
|
||||||
assert cassette.play_count == 0
|
assert cassette.play_count == 0
|
||||||
assert_responses(cookies_resp, home_resp)
|
assert_responses(cookies_resp, home_resp)
|
||||||
|
|
||||||
# -------------------------- Play --------------------------- #
|
# -------------------------- Play --------------------------- #
|
||||||
with vcr.use_cassette(tmp, record_mode=vcr.mode.NONE) as cassette:
|
with vcr.use_cassette(tmp, record_mode=vcr.mode.NONE) as cassette:
|
||||||
async with aiohttp.ClientSession(loop=loop) as session:
|
async with aiohttp.ClientSession(loop=loop, cookie_jar=aiohttp.CookieJar(unsafe=True)) as session:
|
||||||
cookies_resp = await session.get(cookies_url)
|
cookies_resp = await session.get(cookies_url, ssl=HTTPBIN_SSL_CONTEXT)
|
||||||
home_resp = await session.get(home_url, cookies=req_cookies, headers=req_headers)
|
home_resp = await session.get(
|
||||||
|
home_url,
|
||||||
|
cookies=req_cookies,
|
||||||
|
headers=req_headers,
|
||||||
|
ssl=HTTPBIN_SSL_CONTEXT,
|
||||||
|
)
|
||||||
assert cassette.play_count == 2
|
assert cassette.play_count == 2
|
||||||
assert_responses(cookies_resp, home_resp)
|
assert_responses(cookies_resp, home_resp)
|
||||||
|
|
||||||
@@ -361,41 +389,76 @@ def test_cookies(scheme, tmpdir):
|
|||||||
run_in_loop(run)
|
run_in_loop(run)
|
||||||
|
|
||||||
|
|
||||||
def test_cookies_redirect(scheme, tmpdir):
|
def test_cookies_redirect(httpbin_both, tmpdir):
|
||||||
async def run(loop):
|
async def run(loop):
|
||||||
# Sets cookie as provided by the query string and redirects
|
# Sets cookie as provided by the query string and redirects
|
||||||
cookies_url = scheme + "://httpbin.org/cookies/set?Cookie_1=Val_1"
|
cookies_url = httpbin_both.url + "/cookies/set?Cookie_1=Val_1"
|
||||||
tmp = str(tmpdir.join("cookies.yaml"))
|
tmp = str(tmpdir.join("cookies.yaml"))
|
||||||
|
|
||||||
# ------------------------- Record -------------------------- #
|
# ------------------------- Record -------------------------- #
|
||||||
with vcr.use_cassette(tmp) as cassette:
|
with vcr.use_cassette(tmp) as cassette:
|
||||||
async with aiohttp.ClientSession(loop=loop) as session:
|
async with aiohttp.ClientSession(loop=loop, cookie_jar=aiohttp.CookieJar(unsafe=True)) as session:
|
||||||
cookies_resp = await session.get(cookies_url)
|
cookies_resp = await session.get(cookies_url, ssl=HTTPBIN_SSL_CONTEXT)
|
||||||
assert not cookies_resp.cookies
|
assert not cookies_resp.cookies
|
||||||
cookies = session.cookie_jar.filter_cookies(cookies_url)
|
cookies = session.cookie_jar.filter_cookies(yarl.URL(cookies_url))
|
||||||
assert cookies["Cookie_1"].value == "Val_1"
|
assert cookies["Cookie_1"].value == "Val_1"
|
||||||
assert cassette.play_count == 0
|
assert cassette.play_count == 0
|
||||||
cassette.requests[1].headers["Cookie"] == "Cookie_1=Val_1"
|
|
||||||
|
assert cassette.requests[1].headers["Cookie"] == "Cookie_1=Val_1"
|
||||||
|
|
||||||
# -------------------------- Play --------------------------- #
|
# -------------------------- Play --------------------------- #
|
||||||
with vcr.use_cassette(tmp, record_mode=vcr.mode.NONE) as cassette:
|
with vcr.use_cassette(tmp, record_mode=vcr.mode.NONE) as cassette:
|
||||||
async with aiohttp.ClientSession(loop=loop) as session:
|
async with aiohttp.ClientSession(loop=loop, cookie_jar=aiohttp.CookieJar(unsafe=True)) as session:
|
||||||
cookies_resp = await session.get(cookies_url)
|
cookies_resp = await session.get(cookies_url, ssl=HTTPBIN_SSL_CONTEXT)
|
||||||
assert not cookies_resp.cookies
|
assert not cookies_resp.cookies
|
||||||
cookies = session.cookie_jar.filter_cookies(cookies_url)
|
cookies = session.cookie_jar.filter_cookies(yarl.URL(cookies_url))
|
||||||
assert cookies["Cookie_1"].value == "Val_1"
|
assert cookies["Cookie_1"].value == "Val_1"
|
||||||
assert cassette.play_count == 2
|
assert cassette.play_count == 2
|
||||||
cassette.requests[1].headers["Cookie"] == "Cookie_1=Val_1"
|
|
||||||
|
assert cassette.requests[1].headers["Cookie"] == "Cookie_1=Val_1"
|
||||||
|
|
||||||
# Assert that it's ignoring expiration date
|
# Assert that it's ignoring expiration date
|
||||||
with vcr.use_cassette(tmp, record_mode=vcr.mode.NONE) as cassette:
|
with vcr.use_cassette(tmp, record_mode=vcr.mode.NONE) as cassette:
|
||||||
cassette.responses[0]["headers"]["set-cookie"] = [
|
cassette.responses[0]["headers"]["set-cookie"] = [
|
||||||
"Cookie_1=Val_1; Expires=Wed, 21 Oct 2015 07:28:00 GMT"
|
"Cookie_1=Val_1; Expires=Wed, 21 Oct 2015 07:28:00 GMT",
|
||||||
]
|
]
|
||||||
async with aiohttp.ClientSession(loop=loop) as session:
|
async with aiohttp.ClientSession(loop=loop, cookie_jar=aiohttp.CookieJar(unsafe=True)) as session:
|
||||||
cookies_resp = await session.get(cookies_url)
|
cookies_resp = await session.get(cookies_url, ssl=HTTPBIN_SSL_CONTEXT)
|
||||||
assert not cookies_resp.cookies
|
assert not cookies_resp.cookies
|
||||||
cookies = session.cookie_jar.filter_cookies(cookies_url)
|
cookies = session.cookie_jar.filter_cookies(yarl.URL(cookies_url))
|
||||||
assert cookies["Cookie_1"].value == "Val_1"
|
assert cookies["Cookie_1"].value == "Val_1"
|
||||||
|
|
||||||
run_in_loop(run)
|
run_in_loop(run)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.online
|
||||||
|
def test_not_allow_redirects(tmpdir, httpbin):
|
||||||
|
url = httpbin + "/redirect-to?url=.%2F&status_code=308"
|
||||||
|
path = str(tmpdir.join("redirects.yaml"))
|
||||||
|
|
||||||
|
with vcr.use_cassette(path):
|
||||||
|
response, _ = get(url, allow_redirects=False)
|
||||||
|
assert response.url.path == "/redirect-to"
|
||||||
|
assert response.status == 308
|
||||||
|
|
||||||
|
with vcr.use_cassette(path) as cassette:
|
||||||
|
response, _ = get(url, allow_redirects=False)
|
||||||
|
assert response.url.path == "/redirect-to"
|
||||||
|
assert response.status == 308
|
||||||
|
assert cassette.play_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_filter_query_parameters(tmpdir, httpbin):
|
||||||
|
url = httpbin + "?password=secret"
|
||||||
|
path = str(tmpdir.join("query_param_filter.yaml"))
|
||||||
|
|
||||||
|
with vcr.use_cassette(path, filter_query_parameters=["password"]) as cassette:
|
||||||
|
get(url)
|
||||||
|
|
||||||
|
assert "password" not in cassette.requests[0].url
|
||||||
|
assert "secret" not in cassette.requests[0].url
|
||||||
|
|
||||||
|
with open(path) as f:
|
||||||
|
cassette_content = f.read()
|
||||||
|
assert "password" not in cassette_content
|
||||||
|
assert "secret" not in cassette_content
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""Basic tests for cassettes"""
|
"""Basic tests for cassettes"""
|
||||||
|
|
||||||
# External imports
|
# External imports
|
||||||
@@ -11,7 +10,7 @@ import vcr
|
|||||||
|
|
||||||
def test_nonexistent_directory(tmpdir, httpbin):
|
def test_nonexistent_directory(tmpdir, httpbin):
|
||||||
"""If we load a cassette in a nonexistent directory, it can save ok"""
|
"""If we load a cassette in a nonexistent directory, it can save ok"""
|
||||||
# Check to make sure directory doesnt exist
|
# Check to make sure directory doesn't exist
|
||||||
assert not os.path.exists(str(tmpdir.join("nonexistent")))
|
assert not os.path.exists(str(tmpdir.join("nonexistent")))
|
||||||
|
|
||||||
# Run VCR to create dir and cassette file
|
# Run VCR to create dir and cassette file
|
||||||
@@ -40,7 +39,7 @@ def test_basic_json_use(tmpdir, httpbin):
|
|||||||
test_fixture = str(tmpdir.join("synopsis.json"))
|
test_fixture = str(tmpdir.join("synopsis.json"))
|
||||||
with vcr.use_cassette(test_fixture, serializer="json"):
|
with vcr.use_cassette(test_fixture, serializer="json"):
|
||||||
response = urlopen(httpbin.url).read()
|
response = urlopen(httpbin.url).read()
|
||||||
assert b"difficult sometimes" in response
|
assert b"HTTP Request & Response Service" in response
|
||||||
|
|
||||||
|
|
||||||
def test_patched_content(tmpdir, httpbin):
|
def test_patched_content(tmpdir, httpbin):
|
||||||
|
|||||||
@@ -1,79 +0,0 @@
|
|||||||
import pytest
|
|
||||||
|
|
||||||
boto = pytest.importorskip("boto")
|
|
||||||
|
|
||||||
import boto # NOQA
|
|
||||||
import boto.iam # NOQA
|
|
||||||
from boto.s3.connection import S3Connection # NOQA
|
|
||||||
from boto.s3.key import Key # NOQA
|
|
||||||
from configparser import DuplicateSectionError # NOQA
|
|
||||||
import vcr # NOQA
|
|
||||||
|
|
||||||
|
|
||||||
def test_boto_stubs(tmpdir):
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("boto-stubs.yml"))):
|
|
||||||
# Perform the imports within the patched context so that
|
|
||||||
# CertValidatingHTTPSConnection refers to the patched version.
|
|
||||||
from boto.https_connection import CertValidatingHTTPSConnection
|
|
||||||
from vcr.stubs.boto_stubs import VCRCertValidatingHTTPSConnection
|
|
||||||
|
|
||||||
# Prove that the class was patched by the stub and that we can instantiate it.
|
|
||||||
assert issubclass(CertValidatingHTTPSConnection, VCRCertValidatingHTTPSConnection)
|
|
||||||
CertValidatingHTTPSConnection("hostname.does.not.matter")
|
|
||||||
|
|
||||||
|
|
||||||
def test_boto_without_vcr():
|
|
||||||
s3_conn = S3Connection()
|
|
||||||
s3_bucket = s3_conn.get_bucket("boto-demo-1394171994") # a bucket you can access
|
|
||||||
k = Key(s3_bucket)
|
|
||||||
k.key = "test.txt"
|
|
||||||
k.set_contents_from_string("hello world i am a string")
|
|
||||||
|
|
||||||
|
|
||||||
def test_boto_medium_difficulty(tmpdir):
|
|
||||||
s3_conn = S3Connection()
|
|
||||||
s3_bucket = s3_conn.get_bucket("boto-demo-1394171994") # a bucket you can access
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("boto-medium.yml"))):
|
|
||||||
k = Key(s3_bucket)
|
|
||||||
k.key = "test.txt"
|
|
||||||
k.set_contents_from_string("hello world i am a string")
|
|
||||||
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("boto-medium.yml"))):
|
|
||||||
k = Key(s3_bucket)
|
|
||||||
k.key = "test.txt"
|
|
||||||
k.set_contents_from_string("hello world i am a string")
|
|
||||||
|
|
||||||
|
|
||||||
def test_boto_hardcore_mode(tmpdir):
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("boto-hardcore.yml"))):
|
|
||||||
s3_conn = S3Connection()
|
|
||||||
s3_bucket = s3_conn.get_bucket("boto-demo-1394171994") # a bucket you can access
|
|
||||||
k = Key(s3_bucket)
|
|
||||||
k.key = "test.txt"
|
|
||||||
k.set_contents_from_string("hello world i am a string")
|
|
||||||
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("boto-hardcore.yml"))):
|
|
||||||
s3_conn = S3Connection()
|
|
||||||
s3_bucket = s3_conn.get_bucket("boto-demo-1394171994") # a bucket you can access
|
|
||||||
k = Key(s3_bucket)
|
|
||||||
k.key = "test.txt"
|
|
||||||
k.set_contents_from_string("hello world i am a string")
|
|
||||||
|
|
||||||
|
|
||||||
def test_boto_iam(tmpdir):
|
|
||||||
try:
|
|
||||||
boto.config.add_section("Boto")
|
|
||||||
except DuplicateSectionError:
|
|
||||||
pass
|
|
||||||
# Ensure that boto uses HTTPS
|
|
||||||
boto.config.set("Boto", "is_secure", "true")
|
|
||||||
# Ensure that boto uses CertValidatingHTTPSConnection
|
|
||||||
boto.config.set("Boto", "https_validate_certificates", "true")
|
|
||||||
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("boto-iam.yml"))):
|
|
||||||
iam_conn = boto.iam.connect_to_region("universal")
|
|
||||||
iam_conn.get_all_users()
|
|
||||||
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("boto-iam.yml"))):
|
|
||||||
iam_conn = boto.iam.connect_to_region("universal")
|
|
||||||
iam_conn.get_all_users()
|
|
||||||
@@ -1,14 +1,15 @@
|
|||||||
import pytest
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
import vcr
|
||||||
|
|
||||||
boto3 = pytest.importorskip("boto3")
|
boto3 = pytest.importorskip("boto3")
|
||||||
|
|
||||||
import boto3 # NOQA
|
import botocore # noqa
|
||||||
import botocore # NOQA
|
|
||||||
import vcr # NOQA
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from botocore import awsrequest # NOQA
|
from botocore import awsrequest # noqa
|
||||||
|
|
||||||
botocore_awsrequest = True
|
botocore_awsrequest = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
@@ -18,12 +19,12 @@ except ImportError:
|
|||||||
# https://github.com/boto/botocore/pull/1495
|
# https://github.com/boto/botocore/pull/1495
|
||||||
boto3_skip_vendored_requests = pytest.mark.skipif(
|
boto3_skip_vendored_requests = pytest.mark.skipif(
|
||||||
botocore_awsrequest,
|
botocore_awsrequest,
|
||||||
reason="botocore version {ver} does not use vendored requests anymore.".format(ver=botocore.__version__),
|
reason=f"botocore version {botocore.__version__} does not use vendored requests anymore.",
|
||||||
)
|
)
|
||||||
|
|
||||||
boto3_skip_awsrequest = pytest.mark.skipif(
|
boto3_skip_awsrequest = pytest.mark.skipif(
|
||||||
not botocore_awsrequest,
|
not botocore_awsrequest,
|
||||||
reason="botocore version {ver} still uses vendored requests.".format(ver=botocore.__version__),
|
reason=f"botocore version {botocore.__version__} still uses vendored requests.",
|
||||||
)
|
)
|
||||||
|
|
||||||
IAM_USER_NAME = "vcrpy"
|
IAM_USER_NAME = "vcrpy"
|
||||||
@@ -55,24 +56,6 @@ def get_user(iam_client):
|
|||||||
return _get_user
|
return _get_user
|
||||||
|
|
||||||
|
|
||||||
@boto3_skip_vendored_requests
|
|
||||||
def test_boto_vendored_stubs(tmpdir):
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("boto3-stubs.yml"))):
|
|
||||||
# Perform the imports within the patched context so that
|
|
||||||
# HTTPConnection, VerifiedHTTPSConnection refers to the patched version.
|
|
||||||
from botocore.vendored.requests.packages.urllib3.connectionpool import (
|
|
||||||
HTTPConnection,
|
|
||||||
VerifiedHTTPSConnection,
|
|
||||||
)
|
|
||||||
from vcr.stubs.boto3_stubs import VCRRequestsHTTPConnection, VCRRequestsHTTPSConnection
|
|
||||||
|
|
||||||
# Prove that the class was patched by the stub and that we can instantiate it.
|
|
||||||
assert issubclass(HTTPConnection, VCRRequestsHTTPConnection)
|
|
||||||
assert issubclass(VerifiedHTTPSConnection, VCRRequestsHTTPSConnection)
|
|
||||||
HTTPConnection("hostname.does.not.matter")
|
|
||||||
VerifiedHTTPSConnection("hostname.does.not.matter")
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(
|
@pytest.mark.skipif(
|
||||||
os.environ.get("TRAVIS_PULL_REQUEST") != "false",
|
os.environ.get("TRAVIS_PULL_REQUEST") != "false",
|
||||||
reason="Encrypted Environment Variables from Travis Repository Settings"
|
reason="Encrypted Environment Variables from Travis Repository Settings"
|
||||||
@@ -80,7 +63,6 @@ def test_boto_vendored_stubs(tmpdir):
|
|||||||
"https://docs.travis-ci.com/user/pull-requests/#pull-requests-and-security-restrictions",
|
"https://docs.travis-ci.com/user/pull-requests/#pull-requests-and-security-restrictions",
|
||||||
)
|
)
|
||||||
def test_boto_medium_difficulty(tmpdir, get_user):
|
def test_boto_medium_difficulty(tmpdir, get_user):
|
||||||
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("boto3-medium.yml"))):
|
with vcr.use_cassette(str(tmpdir.join("boto3-medium.yml"))):
|
||||||
response = get_user()
|
response = get_user()
|
||||||
assert response["User"]["UserName"] == IAM_USER_NAME
|
assert response["User"]["UserName"] == IAM_USER_NAME
|
||||||
|
|||||||
@@ -1,42 +1,51 @@
|
|||||||
import os
|
|
||||||
import json
|
import json
|
||||||
import pytest
|
import os
|
||||||
import vcr
|
|
||||||
from urllib.request import urlopen
|
from urllib.request import urlopen
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
import vcr
|
||||||
|
from vcr.cassette import Cassette
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.online
|
||||||
def test_set_serializer_default_config(tmpdir, httpbin):
|
def test_set_serializer_default_config(tmpdir, httpbin):
|
||||||
my_vcr = vcr.VCR(serializer="json")
|
my_vcr = vcr.VCR(serializer="json")
|
||||||
|
|
||||||
with my_vcr.use_cassette(str(tmpdir.join("test.json"))):
|
with my_vcr.use_cassette(str(tmpdir.join("test.json"))):
|
||||||
assert my_vcr.serializer == "json"
|
assert my_vcr.serializer == "json"
|
||||||
urlopen(httpbin.url + "/get")
|
urlopen(httpbin.url)
|
||||||
|
|
||||||
with open(str(tmpdir.join("test.json"))) as f:
|
with open(str(tmpdir.join("test.json"))) as f:
|
||||||
assert json.loads(f.read())
|
file_content = f.read()
|
||||||
|
assert file_content.endswith("\n")
|
||||||
|
assert json.loads(file_content)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.online
|
||||||
def test_default_set_cassette_library_dir(tmpdir, httpbin):
|
def test_default_set_cassette_library_dir(tmpdir, httpbin):
|
||||||
my_vcr = vcr.VCR(cassette_library_dir=str(tmpdir.join("subdir")))
|
my_vcr = vcr.VCR(cassette_library_dir=str(tmpdir.join("subdir")))
|
||||||
|
|
||||||
with my_vcr.use_cassette("test.json"):
|
with my_vcr.use_cassette("test.json"):
|
||||||
urlopen(httpbin.url + "/get")
|
urlopen(httpbin.url)
|
||||||
|
|
||||||
assert os.path.exists(str(tmpdir.join("subdir").join("test.json")))
|
assert os.path.exists(str(tmpdir.join("subdir").join("test.json")))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.online
|
||||||
def test_override_set_cassette_library_dir(tmpdir, httpbin):
|
def test_override_set_cassette_library_dir(tmpdir, httpbin):
|
||||||
my_vcr = vcr.VCR(cassette_library_dir=str(tmpdir.join("subdir")))
|
my_vcr = vcr.VCR(cassette_library_dir=str(tmpdir.join("subdir")))
|
||||||
|
|
||||||
cld = str(tmpdir.join("subdir2"))
|
cld = str(tmpdir.join("subdir2"))
|
||||||
|
|
||||||
with my_vcr.use_cassette("test.json", cassette_library_dir=cld):
|
with my_vcr.use_cassette("test.json", cassette_library_dir=cld):
|
||||||
urlopen(httpbin.url + "/get")
|
urlopen(httpbin.url)
|
||||||
|
|
||||||
assert os.path.exists(str(tmpdir.join("subdir2").join("test.json")))
|
assert os.path.exists(str(tmpdir.join("subdir2").join("test.json")))
|
||||||
assert not os.path.exists(str(tmpdir.join("subdir").join("test.json")))
|
assert not os.path.exists(str(tmpdir.join("subdir").join("test.json")))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.online
|
||||||
def test_override_match_on(tmpdir, httpbin):
|
def test_override_match_on(tmpdir, httpbin):
|
||||||
my_vcr = vcr.VCR(match_on=["method"])
|
my_vcr = vcr.VCR(match_on=["method"])
|
||||||
|
|
||||||
@@ -44,7 +53,7 @@ def test_override_match_on(tmpdir, httpbin):
|
|||||||
urlopen(httpbin.url)
|
urlopen(httpbin.url)
|
||||||
|
|
||||||
with my_vcr.use_cassette(str(tmpdir.join("test.json"))) as cass:
|
with my_vcr.use_cassette(str(tmpdir.join("test.json"))) as cass:
|
||||||
urlopen(httpbin.url + "/get")
|
urlopen(httpbin.url)
|
||||||
|
|
||||||
assert len(cass) == 1
|
assert len(cass) == 1
|
||||||
assert cass.play_count == 1
|
assert cass.play_count == 1
|
||||||
@@ -53,6 +62,43 @@ def test_override_match_on(tmpdir, httpbin):
|
|||||||
def test_missing_matcher():
|
def test_missing_matcher():
|
||||||
my_vcr = vcr.VCR()
|
my_vcr = vcr.VCR()
|
||||||
my_vcr.register_matcher("awesome", object)
|
my_vcr.register_matcher("awesome", object)
|
||||||
with pytest.raises(KeyError):
|
with pytest.raises(KeyError), my_vcr.use_cassette("test.yaml", match_on=["notawesome"]):
|
||||||
with my_vcr.use_cassette("test.yaml", match_on=["notawesome"]):
|
pass
|
||||||
pass
|
|
||||||
|
|
||||||
|
@pytest.mark.online
|
||||||
|
def test_dont_record_on_exception(tmpdir, httpbin):
|
||||||
|
my_vcr = vcr.VCR(record_on_exception=False)
|
||||||
|
|
||||||
|
@my_vcr.use_cassette(str(tmpdir.join("dontsave.yml")))
|
||||||
|
def some_test():
|
||||||
|
assert b"Not in content" in urlopen(httpbin.url)
|
||||||
|
|
||||||
|
with pytest.raises(AssertionError):
|
||||||
|
some_test()
|
||||||
|
|
||||||
|
assert not os.path.exists(str(tmpdir.join("dontsave.yml")))
|
||||||
|
|
||||||
|
# Make sure context decorator has the same behavior
|
||||||
|
with pytest.raises(AssertionError), my_vcr.use_cassette(str(tmpdir.join("dontsave2.yml"))):
|
||||||
|
assert b"Not in content" in urlopen(httpbin.url).read()
|
||||||
|
|
||||||
|
assert not os.path.exists(str(tmpdir.join("dontsave2.yml")))
|
||||||
|
|
||||||
|
|
||||||
|
def test_set_drop_unused_requests(tmpdir, httpbin):
|
||||||
|
my_vcr = vcr.VCR(drop_unused_requests=True)
|
||||||
|
file = str(tmpdir.join("test.yaml"))
|
||||||
|
|
||||||
|
with my_vcr.use_cassette(file):
|
||||||
|
urlopen(httpbin.url)
|
||||||
|
urlopen(httpbin.url + "/get")
|
||||||
|
|
||||||
|
cassette = Cassette.load(path=file)
|
||||||
|
assert len(cassette) == 2
|
||||||
|
|
||||||
|
with my_vcr.use_cassette(file):
|
||||||
|
urlopen(httpbin.url)
|
||||||
|
|
||||||
|
cassette = Cassette.load(path=file)
|
||||||
|
assert len(cassette) == 1
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""Basic tests about save behavior"""
|
"""Basic tests about save behavior"""
|
||||||
|
|
||||||
# External imports
|
# External imports
|
||||||
@@ -6,10 +5,13 @@ import os
|
|||||||
import time
|
import time
|
||||||
from urllib.request import urlopen
|
from urllib.request import urlopen
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
# Internal imports
|
# Internal imports
|
||||||
import vcr
|
import vcr
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.online
|
||||||
def test_disk_saver_nowrite(tmpdir, httpbin):
|
def test_disk_saver_nowrite(tmpdir, httpbin):
|
||||||
"""
|
"""
|
||||||
Ensure that when you close a cassette without changing it it doesn't
|
Ensure that when you close a cassette without changing it it doesn't
|
||||||
@@ -30,6 +32,7 @@ def test_disk_saver_nowrite(tmpdir, httpbin):
|
|||||||
assert last_mod == last_mod2
|
assert last_mod == last_mod2
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.online
|
||||||
def test_disk_saver_write(tmpdir, httpbin):
|
def test_disk_saver_write(tmpdir, httpbin):
|
||||||
"""
|
"""
|
||||||
Ensure that when you close a cassette after changing it it does
|
Ensure that when you close a cassette after changing it it does
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import base64
|
import base64
|
||||||
import pytest
|
|
||||||
from urllib.request import urlopen, Request
|
|
||||||
from urllib.parse import urlencode
|
|
||||||
from urllib.error import HTTPError
|
|
||||||
import vcr
|
|
||||||
import json
|
import json
|
||||||
from assertions import assert_cassette_has_one_response, assert_is_json
|
from urllib.error import HTTPError
|
||||||
|
from urllib.parse import urlencode
|
||||||
|
from urllib.request import Request, urlopen
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
import vcr
|
||||||
|
|
||||||
|
from ..assertions import assert_cassette_has_one_response, assert_is_json_bytes
|
||||||
|
|
||||||
|
|
||||||
def _request_with_auth(url, username, password):
|
def _request_with_auth(url, username, password):
|
||||||
@@ -43,13 +46,18 @@ def test_filter_basic_auth(tmpdir, httpbin):
|
|||||||
|
|
||||||
|
|
||||||
def test_filter_querystring(tmpdir, httpbin):
|
def test_filter_querystring(tmpdir, httpbin):
|
||||||
url = httpbin.url + "/?foo=bar"
|
url = httpbin.url + "/?password=secret"
|
||||||
cass_file = str(tmpdir.join("filter_qs.yaml"))
|
cass_file = str(tmpdir.join("filter_qs.yaml"))
|
||||||
with vcr.use_cassette(cass_file, filter_query_parameters=["foo"]):
|
with vcr.use_cassette(cass_file, filter_query_parameters=["password"]):
|
||||||
urlopen(url)
|
urlopen(url)
|
||||||
with vcr.use_cassette(cass_file, filter_query_parameters=["foo"]) as cass:
|
with vcr.use_cassette(cass_file, filter_query_parameters=["password"]) as cass:
|
||||||
urlopen(url)
|
urlopen(url)
|
||||||
assert "foo" not in cass.requests[0].url
|
assert "password" not in cass.requests[0].url
|
||||||
|
assert "secret" not in cass.requests[0].url
|
||||||
|
with open(cass_file) as f:
|
||||||
|
cassette_content = f.read()
|
||||||
|
assert "password" not in cassette_content
|
||||||
|
assert "secret" not in cassette_content
|
||||||
|
|
||||||
|
|
||||||
def test_filter_post_data(tmpdir, httpbin):
|
def test_filter_post_data(tmpdir, httpbin):
|
||||||
@@ -103,7 +111,19 @@ def test_decompress_gzip(tmpdir, httpbin):
|
|||||||
with vcr.use_cassette(cass_file) as cass:
|
with vcr.use_cassette(cass_file) as cass:
|
||||||
decoded_response = urlopen(url).read()
|
decoded_response = urlopen(url).read()
|
||||||
assert_cassette_has_one_response(cass)
|
assert_cassette_has_one_response(cass)
|
||||||
assert_is_json(decoded_response)
|
assert_is_json_bytes(decoded_response)
|
||||||
|
|
||||||
|
|
||||||
|
def test_decomptess_empty_body(tmpdir, httpbin):
|
||||||
|
url = httpbin.url + "/gzip"
|
||||||
|
request = Request(url, headers={"Accept-Encoding": ["gzip, deflate"]}, method="HEAD")
|
||||||
|
cass_file = str(tmpdir.join("gzip_empty_response.yaml"))
|
||||||
|
with vcr.use_cassette(cass_file, decode_compressed_response=True):
|
||||||
|
response = urlopen(request).read()
|
||||||
|
with vcr.use_cassette(cass_file) as cass:
|
||||||
|
decoded_response = urlopen(request).read()
|
||||||
|
assert_cassette_has_one_response(cass)
|
||||||
|
assert decoded_response == response
|
||||||
|
|
||||||
|
|
||||||
def test_decompress_deflate(tmpdir, httpbin):
|
def test_decompress_deflate(tmpdir, httpbin):
|
||||||
@@ -115,7 +135,7 @@ def test_decompress_deflate(tmpdir, httpbin):
|
|||||||
with vcr.use_cassette(cass_file) as cass:
|
with vcr.use_cassette(cass_file) as cass:
|
||||||
decoded_response = urlopen(url).read()
|
decoded_response = urlopen(url).read()
|
||||||
assert_cassette_has_one_response(cass)
|
assert_cassette_has_one_response(cass)
|
||||||
assert_is_json(decoded_response)
|
assert_is_json_bytes(decoded_response)
|
||||||
|
|
||||||
|
|
||||||
def test_decompress_regular(tmpdir, httpbin):
|
def test_decompress_regular(tmpdir, httpbin):
|
||||||
@@ -127,4 +147,25 @@ def test_decompress_regular(tmpdir, httpbin):
|
|||||||
with vcr.use_cassette(cass_file) as cass:
|
with vcr.use_cassette(cass_file) as cass:
|
||||||
resp = urlopen(url).read()
|
resp = urlopen(url).read()
|
||||||
assert_cassette_has_one_response(cass)
|
assert_cassette_has_one_response(cass)
|
||||||
assert_is_json(resp)
|
assert_is_json_bytes(resp)
|
||||||
|
|
||||||
|
|
||||||
|
def test_before_record_request_corruption(tmpdir, httpbin):
|
||||||
|
"""Modifying request in before_record_request should not affect outgoing request"""
|
||||||
|
|
||||||
|
def before_record(request):
|
||||||
|
request.headers.clear()
|
||||||
|
request.body = b""
|
||||||
|
return request
|
||||||
|
|
||||||
|
req = Request(
|
||||||
|
httpbin.url + "/post",
|
||||||
|
data=urlencode({"test": "exists"}).encode(),
|
||||||
|
headers={"X-Test": "exists"},
|
||||||
|
)
|
||||||
|
cass_file = str(tmpdir.join("modified_response.yaml"))
|
||||||
|
with vcr.use_cassette(cass_file, before_record_request=before_record):
|
||||||
|
resp = json.loads(urlopen(req).read())
|
||||||
|
|
||||||
|
assert resp["headers"]["X-Test"] == "exists"
|
||||||
|
assert resp["form"]["test"] == "exists"
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""Integration tests with httplib2"""
|
"""Integration tests with httplib2"""
|
||||||
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import pytest_httpbin.certs
|
import pytest_httpbin.certs
|
||||||
|
|
||||||
import vcr
|
import vcr
|
||||||
|
|
||||||
from assertions import assert_cassette_has_one_response
|
from ..assertions import assert_cassette_has_one_response
|
||||||
|
|
||||||
httplib2 = pytest.importorskip("httplib2")
|
httplib2 = pytest.importorskip("httplib2")
|
||||||
|
|
||||||
@@ -20,8 +18,6 @@ def http():
|
|||||||
with the certificate replaced by the httpbin one.
|
with the certificate replaced by the httpbin one.
|
||||||
"""
|
"""
|
||||||
kwargs = {"ca_certs": pytest_httpbin.certs.where()}
|
kwargs = {"ca_certs": pytest_httpbin.certs.where()}
|
||||||
if sys.version_info[:2] in [(2, 7), (3, 7)]:
|
|
||||||
kwargs["disable_ssl_certificate_validation"] = True
|
|
||||||
return httplib2.Http(**kwargs)
|
return httplib2.Http(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
@@ -61,13 +57,15 @@ def test_response_headers(tmpdir, httpbin_both):
|
|||||||
assert set(headers) == set(resp.items())
|
assert set(headers) == set(resp.items())
|
||||||
|
|
||||||
|
|
||||||
def test_effective_url(tmpdir, httpbin_both):
|
@pytest.mark.online
|
||||||
|
def test_effective_url(tmpdir, httpbin):
|
||||||
"""Ensure that the effective_url is captured"""
|
"""Ensure that the effective_url is captured"""
|
||||||
url = httpbin_both.url + "/redirect-to?url=/html"
|
url = httpbin.url + "/redirect-to?url=.%2F&status_code=301"
|
||||||
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("headers.yaml"))):
|
with vcr.use_cassette(str(tmpdir.join("headers.yaml"))):
|
||||||
resp, _ = http().request(url)
|
resp, _ = http().request(url)
|
||||||
effective_url = resp["content-location"]
|
effective_url = resp["content-location"]
|
||||||
assert effective_url == httpbin_both + "/html"
|
assert effective_url == httpbin.url + "/"
|
||||||
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("headers.yaml"))):
|
with vcr.use_cassette(str(tmpdir.join("headers.yaml"))):
|
||||||
resp, _ = http().request(url)
|
resp, _ = http().request(url)
|
||||||
|
|||||||
@@ -1,50 +1,123 @@
|
|||||||
import pytest
|
|
||||||
import contextlib
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
import vcr
|
||||||
|
|
||||||
|
from ..assertions import assert_is_json_bytes
|
||||||
|
|
||||||
asyncio = pytest.importorskip("asyncio")
|
asyncio = pytest.importorskip("asyncio")
|
||||||
httpx = pytest.importorskip("httpx")
|
httpx = pytest.importorskip("httpx")
|
||||||
|
|
||||||
import vcr # noqa: E402
|
|
||||||
|
@pytest.fixture(params=["https", "http"])
|
||||||
|
def scheme(request):
|
||||||
|
"""Fixture that returns both http and https."""
|
||||||
|
return request.param
|
||||||
|
|
||||||
|
|
||||||
class BaseDoRequest:
|
class BaseDoRequest:
|
||||||
_client_class = None
|
_client_class = None
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self._client = self._client_class(*args, **kwargs)
|
self._client_args = args
|
||||||
|
self._client_kwargs = kwargs
|
||||||
|
self._client_kwargs["follow_redirects"] = self._client_kwargs.get("follow_redirects", True)
|
||||||
|
|
||||||
|
def _make_client(self):
|
||||||
|
return self._client_class(*self._client_args, **self._client_kwargs)
|
||||||
|
|
||||||
|
|
||||||
class DoSyncRequest(BaseDoRequest):
|
class DoSyncRequest(BaseDoRequest):
|
||||||
_client_class = httpx.Client
|
_client_class = httpx.Client
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
self._client = self._make_client()
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, *args):
|
||||||
|
self._client.close()
|
||||||
|
del self._client
|
||||||
|
|
||||||
|
@property
|
||||||
|
def client(self):
|
||||||
|
try:
|
||||||
|
return self._client
|
||||||
|
except AttributeError as e:
|
||||||
|
raise ValueError('To access sync client, use "with do_request() as client"') from e
|
||||||
|
|
||||||
def __call__(self, *args, **kwargs):
|
def __call__(self, *args, **kwargs):
|
||||||
return self._client.request(*args, timeout=60, **kwargs)
|
if hasattr(self, "_client"):
|
||||||
|
return self.client.request(*args, timeout=60, **kwargs)
|
||||||
|
|
||||||
|
# Use one-time context and dispose of the client afterwards
|
||||||
|
with self:
|
||||||
|
return self.client.request(*args, timeout=60, **kwargs)
|
||||||
|
|
||||||
|
def stream(self, *args, **kwargs):
|
||||||
|
if hasattr(self, "_client"):
|
||||||
|
with self.client.stream(*args, **kwargs) as response:
|
||||||
|
return b"".join(response.iter_bytes())
|
||||||
|
|
||||||
|
# Use one-time context and dispose of the client afterwards
|
||||||
|
with self, self.client.stream(*args, **kwargs) as response:
|
||||||
|
return b"".join(response.iter_bytes())
|
||||||
|
|
||||||
|
|
||||||
class DoAsyncRequest(BaseDoRequest):
|
class DoAsyncRequest(BaseDoRequest):
|
||||||
_client_class = httpx.AsyncClient
|
_client_class = httpx.AsyncClient
|
||||||
|
|
||||||
@staticmethod
|
def __enter__(self):
|
||||||
def run_in_loop(coroutine):
|
# Need to manage both loop and client, because client's implementation
|
||||||
with contextlib.closing(asyncio.new_event_loop()) as loop:
|
# will fail if the loop is closed before the client's end of life.
|
||||||
asyncio.set_event_loop(loop)
|
self._loop = asyncio.new_event_loop()
|
||||||
task = loop.create_task(coroutine)
|
asyncio.set_event_loop(self._loop)
|
||||||
return loop.run_until_complete(task)
|
self._client = self._make_client()
|
||||||
|
self._loop.run_until_complete(self._client.__aenter__())
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, *args):
|
||||||
|
try:
|
||||||
|
self._loop.run_until_complete(self._client.__aexit__(*args))
|
||||||
|
finally:
|
||||||
|
del self._client
|
||||||
|
self._loop.close()
|
||||||
|
del self._loop
|
||||||
|
|
||||||
|
@property
|
||||||
|
def client(self):
|
||||||
|
try:
|
||||||
|
return self._client
|
||||||
|
except AttributeError as e:
|
||||||
|
raise ValueError('To access async client, use "with do_request() as client"') from e
|
||||||
|
|
||||||
def __call__(self, *args, **kwargs):
|
def __call__(self, *args, **kwargs):
|
||||||
async def _request():
|
if hasattr(self, "_loop"):
|
||||||
async with self._client as c:
|
return self._loop.run_until_complete(self.client.request(*args, **kwargs))
|
||||||
return await c.request(*args, **kwargs)
|
|
||||||
|
|
||||||
return DoAsyncRequest.run_in_loop(_request())
|
# Use one-time context and dispose of the loop/client afterwards
|
||||||
|
with self:
|
||||||
|
return self._loop.run_until_complete(self.client.request(*args, **kwargs))
|
||||||
|
|
||||||
|
async def _get_stream(self, *args, **kwargs):
|
||||||
|
async with self.client.stream(*args, **kwargs) as response:
|
||||||
|
content = b""
|
||||||
|
async for c in response.aiter_bytes():
|
||||||
|
content += c
|
||||||
|
return content
|
||||||
|
|
||||||
|
def stream(self, *args, **kwargs):
|
||||||
|
if hasattr(self, "_loop"):
|
||||||
|
return self._loop.run_until_complete(self._get_stream(*args, **kwargs))
|
||||||
|
|
||||||
|
# Use one-time context and dispose of the loop/client afterwards
|
||||||
|
with self:
|
||||||
|
return self._loop.run_until_complete(self._get_stream(*args, **kwargs))
|
||||||
|
|
||||||
|
|
||||||
def pytest_generate_tests(metafunc):
|
def pytest_generate_tests(metafunc):
|
||||||
if "do_request" in metafunc.fixturenames:
|
if "do_request" in metafunc.fixturenames:
|
||||||
metafunc.parametrize("do_request", [DoAsyncRequest, DoSyncRequest])
|
metafunc.parametrize("do_request", [DoAsyncRequest, DoSyncRequest])
|
||||||
if "scheme" in metafunc.fixturenames:
|
|
||||||
metafunc.parametrize("scheme", ["http", "https"])
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@@ -52,8 +125,10 @@ def yml(tmpdir, request):
|
|||||||
return str(tmpdir.join(request.function.__name__ + ".yaml"))
|
return str(tmpdir.join(request.function.__name__ + ".yaml"))
|
||||||
|
|
||||||
|
|
||||||
def test_status(tmpdir, scheme, do_request):
|
@pytest.mark.online
|
||||||
url = scheme + "://mockbin.org/request"
|
def test_status(tmpdir, httpbin, do_request):
|
||||||
|
url = httpbin.url
|
||||||
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("status.yaml"))):
|
with vcr.use_cassette(str(tmpdir.join("status.yaml"))):
|
||||||
response = do_request()("GET", url)
|
response = do_request()("GET", url)
|
||||||
|
|
||||||
@@ -63,8 +138,10 @@ def test_status(tmpdir, scheme, do_request):
|
|||||||
assert cassette.play_count == 1
|
assert cassette.play_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_case_insensitive_headers(tmpdir, scheme, do_request):
|
@pytest.mark.online
|
||||||
url = scheme + "://mockbin.org/request"
|
def test_case_insensitive_headers(tmpdir, httpbin, do_request):
|
||||||
|
url = httpbin.url
|
||||||
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("whatever.yaml"))):
|
with vcr.use_cassette(str(tmpdir.join("whatever.yaml"))):
|
||||||
do_request()("GET", url)
|
do_request()("GET", url)
|
||||||
|
|
||||||
@@ -75,8 +152,10 @@ def test_case_insensitive_headers(tmpdir, scheme, do_request):
|
|||||||
assert cassette.play_count == 1
|
assert cassette.play_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_content(tmpdir, scheme, do_request):
|
@pytest.mark.online
|
||||||
url = scheme + "://httpbin.org"
|
def test_content(tmpdir, httpbin, do_request):
|
||||||
|
url = httpbin.url
|
||||||
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("cointent.yaml"))):
|
with vcr.use_cassette(str(tmpdir.join("cointent.yaml"))):
|
||||||
response = do_request()("GET", url)
|
response = do_request()("GET", url)
|
||||||
|
|
||||||
@@ -86,21 +165,22 @@ def test_content(tmpdir, scheme, do_request):
|
|||||||
assert cassette.play_count == 1
|
assert cassette.play_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_json(tmpdir, scheme, do_request):
|
@pytest.mark.online
|
||||||
url = scheme + "://httpbin.org/get"
|
def test_json(tmpdir, httpbin, do_request):
|
||||||
headers = {"Content-Type": "application/json"}
|
url = httpbin.url + "/json"
|
||||||
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("json.yaml"))):
|
with vcr.use_cassette(str(tmpdir.join("json.yaml"))):
|
||||||
response = do_request(headers=headers)("GET", url)
|
response = do_request()("GET", url)
|
||||||
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("json.yaml"))) as cassette:
|
with vcr.use_cassette(str(tmpdir.join("json.yaml"))) as cassette:
|
||||||
cassette_response = do_request(headers=headers)("GET", url)
|
cassette_response = do_request()("GET", url)
|
||||||
assert cassette_response.json() == response.json()
|
assert cassette_response.json() == response.json()
|
||||||
assert cassette.play_count == 1
|
assert cassette.play_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_params_same_url_distinct_params(tmpdir, scheme, do_request):
|
@pytest.mark.online
|
||||||
url = scheme + "://httpbin.org/get"
|
def test_params_same_url_distinct_params(tmpdir, httpbin, do_request):
|
||||||
|
url = httpbin.url + "/get"
|
||||||
headers = {"Content-Type": "application/json"}
|
headers = {"Content-Type": "application/json"}
|
||||||
params = {"a": 1, "b": False, "c": "c"}
|
params = {"a": 1, "b": False, "c": "c"}
|
||||||
|
|
||||||
@@ -114,49 +194,39 @@ def test_params_same_url_distinct_params(tmpdir, scheme, do_request):
|
|||||||
assert cassette.play_count == 1
|
assert cassette.play_count == 1
|
||||||
|
|
||||||
params = {"other": "params"}
|
params = {"other": "params"}
|
||||||
with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
|
with (
|
||||||
with pytest.raises(vcr.errors.CannotOverwriteExistingCassetteException):
|
vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette,
|
||||||
do_request()("GET", url, params=params, headers=headers)
|
pytest.raises(vcr.errors.CannotOverwriteExistingCassetteException),
|
||||||
|
):
|
||||||
|
do_request()("GET", url, params=params, headers=headers)
|
||||||
|
|
||||||
|
|
||||||
def test_redirect(tmpdir, do_request, yml):
|
@pytest.mark.online
|
||||||
url = "https://mockbin.org/redirect/303/2"
|
def test_redirect(httpbin, yml, do_request):
|
||||||
|
url = httpbin.url + "/redirect-to"
|
||||||
|
|
||||||
response = do_request()("GET", url)
|
response = do_request()("GET", url)
|
||||||
with vcr.use_cassette(yml):
|
with vcr.use_cassette(yml):
|
||||||
response = do_request()("GET", url)
|
response = do_request()("GET", url, params={"url": "./get", "status_code": 302})
|
||||||
|
|
||||||
with vcr.use_cassette(yml) as cassette:
|
with vcr.use_cassette(yml) as cassette:
|
||||||
cassette_response = do_request()("GET", url)
|
cassette_response = do_request()("GET", url, params={"url": "./get", "status_code": 302})
|
||||||
|
|
||||||
assert cassette_response.status_code == response.status_code
|
assert cassette_response.status_code == response.status_code
|
||||||
assert len(cassette_response.history) == len(response.history)
|
assert len(cassette_response.history) == len(response.history)
|
||||||
assert len(cassette) == 3
|
assert len(cassette) == 2
|
||||||
assert cassette.play_count == 3
|
assert cassette.play_count == 2
|
||||||
|
|
||||||
# Assert that the real response and the cassette response have a similar
|
# Assert that the real response and the cassette response have a similar
|
||||||
# looking request_info.
|
# looking request_info.
|
||||||
assert cassette_response.request.url == response.request.url
|
assert cassette_response.request.url == response.request.url
|
||||||
assert cassette_response.request.method == response.request.method
|
assert cassette_response.request.method == response.request.method
|
||||||
assert {k: v for k, v in cassette_response.request.headers.items()} == {
|
assert cassette_response.request.headers.items() == response.request.headers.items()
|
||||||
k: v for k, v in response.request.headers.items()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def test_work_with_gzipped_data(tmpdir, do_request, yml):
|
|
||||||
with vcr.use_cassette(yml):
|
|
||||||
do_request()("GET", "https://httpbin.org/gzip")
|
|
||||||
|
|
||||||
with vcr.use_cassette(yml) as cassette:
|
|
||||||
cassette_response = do_request()("GET", "https://httpbin.org/gzip")
|
|
||||||
|
|
||||||
assert "gzip" in cassette_response.json()["headers"]["Accept-Encoding"]
|
|
||||||
assert cassette_response.read()
|
|
||||||
assert cassette.play_count == 1
|
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.online
|
||||||
@pytest.mark.parametrize("url", ["https://github.com/kevin1024/vcrpy/issues/" + str(i) for i in range(3, 6)])
|
@pytest.mark.parametrize("url", ["https://github.com/kevin1024/vcrpy/issues/" + str(i) for i in range(3, 6)])
|
||||||
def test_simple_fetching(tmpdir, do_request, yml, url):
|
def test_simple_fetching(do_request, yml, url):
|
||||||
with vcr.use_cassette(yml):
|
with vcr.use_cassette(yml):
|
||||||
do_request()("GET", url)
|
do_request()("GET", url)
|
||||||
|
|
||||||
@@ -166,90 +236,127 @@ def test_simple_fetching(tmpdir, do_request, yml, url):
|
|||||||
assert cassette.play_count == 1
|
assert cassette.play_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_behind_proxy(do_request):
|
@pytest.mark.online
|
||||||
# This is recorded because otherwise we should have a live proxy somewhere.
|
def test_cookies(tmpdir, httpbin, do_request):
|
||||||
yml = (
|
def client_cookies(client):
|
||||||
os.path.dirname(os.path.realpath(__file__)) + "/cassettes/" + "test_httpx_test_test_behind_proxy.yml"
|
return list(client.client.cookies)
|
||||||
)
|
|
||||||
url = "https://httpbin.org/headers"
|
|
||||||
proxy = "http://localhost:8080"
|
|
||||||
proxies = {"http": proxy, "https": proxy}
|
|
||||||
|
|
||||||
with vcr.use_cassette(yml):
|
def response_cookies(response):
|
||||||
response = do_request(proxies=proxies, verify=False)("GET", url)
|
return list(response.cookies)
|
||||||
|
|
||||||
|
url = httpbin.url + "/cookies/set"
|
||||||
|
params = {"k1": "v1", "k2": "v2"}
|
||||||
|
|
||||||
|
with do_request(params=params, follow_redirects=False) as client:
|
||||||
|
assert client_cookies(client) == []
|
||||||
|
|
||||||
|
testfile = str(tmpdir.join("cookies.yml"))
|
||||||
|
with vcr.use_cassette(testfile):
|
||||||
|
r1 = client("GET", url)
|
||||||
|
|
||||||
|
assert response_cookies(r1) == ["k1", "k2"]
|
||||||
|
|
||||||
|
r2 = client("GET", url)
|
||||||
|
|
||||||
|
assert response_cookies(r2) == ["k1", "k2"]
|
||||||
|
assert client_cookies(client) == ["k1", "k2"]
|
||||||
|
|
||||||
|
with do_request(params=params, follow_redirects=False) as new_client:
|
||||||
|
assert client_cookies(new_client) == []
|
||||||
|
|
||||||
|
with vcr.use_cassette(testfile) as cassette:
|
||||||
|
cassette_response = new_client("GET", url)
|
||||||
|
|
||||||
|
assert cassette.play_count == 1
|
||||||
|
assert response_cookies(cassette_response) == ["k1", "k2"]
|
||||||
|
assert client_cookies(new_client) == ["k1", "k2"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.online
|
||||||
|
def test_stream(tmpdir, httpbin, do_request):
|
||||||
|
url = httpbin.url + "/stream-bytes/512"
|
||||||
|
testfile = str(tmpdir.join("stream.yml"))
|
||||||
|
|
||||||
|
with vcr.use_cassette(testfile):
|
||||||
|
response_content = do_request().stream("GET", url)
|
||||||
|
assert len(response_content) == 512
|
||||||
|
|
||||||
|
with vcr.use_cassette(testfile) as cassette:
|
||||||
|
cassette_content = do_request().stream("GET", url)
|
||||||
|
assert cassette_content == response_content
|
||||||
|
assert len(cassette_content) == 512
|
||||||
|
assert cassette.play_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
# Regular cassette formats support the status reason,
|
||||||
|
# but the old HTTPX cassette format does not.
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"cassette_name,reason",
|
||||||
|
[
|
||||||
|
("requests", "great"),
|
||||||
|
("httpx_old_format", "OK"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_load_cassette_format(do_request, cassette_name, reason):
|
||||||
|
mydir = os.path.dirname(os.path.realpath(__file__))
|
||||||
|
yml = f"{mydir}/cassettes/gzip_{cassette_name}.yaml"
|
||||||
|
url = "https://httpbin.org/gzip"
|
||||||
|
|
||||||
with vcr.use_cassette(yml) as cassette:
|
with vcr.use_cassette(yml) as cassette:
|
||||||
cassette_response = do_request(proxies=proxies, verify=False)("GET", url)
|
cassette_response = do_request()("GET", url)
|
||||||
assert str(cassette_response.request.url) == url
|
assert str(cassette_response.request.url) == url
|
||||||
assert cassette.play_count == 1
|
assert cassette.play_count == 1
|
||||||
|
|
||||||
assert cassette_response.headers["Via"] == "my_own_proxy", str(cassette_response.headers)
|
# Should be able to load up the JSON inside,
|
||||||
assert cassette_response.request.url == response.request.url
|
# regardless whether the content is the gzipped
|
||||||
|
# in the cassette or not.
|
||||||
|
json = cassette_response.json()
|
||||||
|
assert json["method"] == "GET", json
|
||||||
|
assert cassette_response.status_code == 200
|
||||||
|
assert cassette_response.reason_phrase == reason
|
||||||
|
|
||||||
|
|
||||||
def test_cookies(tmpdir, scheme, do_request):
|
def test_gzip__decode_compressed_response_false(tmpdir, httpbin, do_request):
|
||||||
def client_cookies(client):
|
"""
|
||||||
return [c for c in client._client.cookies]
|
Ensure that httpx is able to automatically decompress the response body.
|
||||||
|
"""
|
||||||
def response_cookies(response):
|
for _ in range(2): # one for recording, one for re-playing
|
||||||
return [c for c in response.cookies]
|
with vcr.use_cassette(str(tmpdir.join("gzip.yaml"))) as cassette:
|
||||||
|
response = do_request()("GET", httpbin + "/gzip")
|
||||||
client = do_request()
|
assert response.headers["content-encoding"] == "gzip" # i.e. not removed
|
||||||
assert client_cookies(client) == []
|
# The content stored in the cassette should be gzipped.
|
||||||
|
assert cassette.responses[0]["body"]["string"][:2] == b"\x1f\x8b"
|
||||||
url = scheme + "://httpbin.org"
|
assert_is_json_bytes(response.content) # i.e. uncompressed bytes
|
||||||
testfile = str(tmpdir.join("cookies.yml"))
|
|
||||||
with vcr.use_cassette(testfile):
|
|
||||||
r1 = client("GET", url + "/cookies/set?k1=v1&k2=v2")
|
|
||||||
assert response_cookies(r1.history[0]) == ["k1", "k2"]
|
|
||||||
assert response_cookies(r1) == []
|
|
||||||
|
|
||||||
r2 = client("GET", url + "/cookies")
|
|
||||||
assert len(r2.json()["cookies"]) == 2
|
|
||||||
|
|
||||||
assert client_cookies(client) == ["k1", "k2"]
|
|
||||||
|
|
||||||
new_client = do_request()
|
|
||||||
assert client_cookies(new_client) == []
|
|
||||||
|
|
||||||
with vcr.use_cassette(testfile) as cassette:
|
|
||||||
cassette_response = new_client("GET", url + "/cookies/set?k1=v1&k2=v2")
|
|
||||||
assert response_cookies(cassette_response.history[0]) == ["k1", "k2"]
|
|
||||||
assert response_cookies(cassette_response) == []
|
|
||||||
|
|
||||||
assert cassette.play_count == 2
|
|
||||||
assert client_cookies(new_client) == ["k1", "k2"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_relative_redirects(tmpdir, scheme, do_request):
|
def test_gzip__decode_compressed_response_true(do_request, tmpdir, httpbin):
|
||||||
url = scheme + "://mockbin.com/redirect/301?to=/redirect/301?to=/request"
|
url = httpbin + "/gzip"
|
||||||
testfile = str(tmpdir.join("relative_redirects.yml"))
|
|
||||||
with vcr.use_cassette(testfile):
|
|
||||||
response = do_request()("GET", url)
|
|
||||||
assert len(response.history) == 2, response
|
|
||||||
assert response.json()["url"].endswith("request")
|
|
||||||
|
|
||||||
with vcr.use_cassette(testfile) as cassette:
|
expected_response = do_request()("GET", url)
|
||||||
response = do_request()("GET", url)
|
expected_content = expected_response.content
|
||||||
assert len(response.history) == 2
|
assert expected_response.headers["content-encoding"] == "gzip" # self-test
|
||||||
assert response.json()["url"].endswith("request")
|
|
||||||
|
|
||||||
assert cassette.play_count == 3
|
with vcr.use_cassette(
|
||||||
|
str(tmpdir.join("decode_compressed.yaml")),
|
||||||
|
decode_compressed_response=True,
|
||||||
|
) as cassette:
|
||||||
|
r = do_request()("GET", url)
|
||||||
|
assert r.headers["content-encoding"] == "gzip" # i.e. not removed
|
||||||
|
content_length = r.headers["content-length"]
|
||||||
|
assert r.content == expected_content
|
||||||
|
|
||||||
|
# Has the cassette body been decompressed?
|
||||||
|
cassette_response_body = cassette.responses[0]["body"]["string"]
|
||||||
|
assert isinstance(cassette_response_body, str)
|
||||||
|
|
||||||
def test_redirect_wo_allow_redirects(do_request, yml):
|
# Content should be JSON.
|
||||||
url = "https://mockbin.org/redirect/308/5"
|
assert cassette_response_body[0:1] == "{"
|
||||||
|
|
||||||
with vcr.use_cassette(yml):
|
with vcr.use_cassette(str(tmpdir.join("decode_compressed.yaml")), decode_compressed_response=True):
|
||||||
response = do_request()("GET", url, allow_redirects=False)
|
r = httpx.get(url)
|
||||||
|
assert "content-encoding" not in r.headers # i.e. removed
|
||||||
|
assert r.content == expected_content
|
||||||
|
|
||||||
assert str(response.url).endswith("308/5")
|
# As the content is uncompressed, it should have a bigger
|
||||||
assert response.status_code == 308
|
# length than the compressed version.
|
||||||
|
assert r.headers["content-length"] > content_length
|
||||||
with vcr.use_cassette(yml) as cassette:
|
|
||||||
response = do_request()("GET", url, allow_redirects=False)
|
|
||||||
|
|
||||||
assert str(response.url).endswith("308/5")
|
|
||||||
assert response.status_code == 308
|
|
||||||
|
|
||||||
assert cassette.play_count == 1
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from urllib.request import urlopen
|
|
||||||
import socket
|
import socket
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
from urllib.request import urlopen
|
||||||
|
|
||||||
import vcr
|
import vcr
|
||||||
|
|
||||||
|
|
||||||
@@ -27,9 +28,9 @@ def test_ignore_localhost(tmpdir, httpbin):
|
|||||||
with overridden_dns({"httpbin.org": "127.0.0.1"}):
|
with overridden_dns({"httpbin.org": "127.0.0.1"}):
|
||||||
cass_file = str(tmpdir.join("filter_qs.yaml"))
|
cass_file = str(tmpdir.join("filter_qs.yaml"))
|
||||||
with vcr.use_cassette(cass_file, ignore_localhost=True) as cass:
|
with vcr.use_cassette(cass_file, ignore_localhost=True) as cass:
|
||||||
urlopen("http://localhost:{}/".format(httpbin.port))
|
urlopen(f"http://localhost:{httpbin.port}/")
|
||||||
assert len(cass) == 0
|
assert len(cass) == 0
|
||||||
urlopen("http://httpbin.org:{}/".format(httpbin.port))
|
urlopen(f"http://httpbin.org:{httpbin.port}/")
|
||||||
assert len(cass) == 1
|
assert len(cass) == 1
|
||||||
|
|
||||||
|
|
||||||
@@ -37,9 +38,9 @@ def test_ignore_httpbin(tmpdir, httpbin):
|
|||||||
with overridden_dns({"httpbin.org": "127.0.0.1"}):
|
with overridden_dns({"httpbin.org": "127.0.0.1"}):
|
||||||
cass_file = str(tmpdir.join("filter_qs.yaml"))
|
cass_file = str(tmpdir.join("filter_qs.yaml"))
|
||||||
with vcr.use_cassette(cass_file, ignore_hosts=["httpbin.org"]) as cass:
|
with vcr.use_cassette(cass_file, ignore_hosts=["httpbin.org"]) as cass:
|
||||||
urlopen("http://httpbin.org:{}/".format(httpbin.port))
|
urlopen(f"http://httpbin.org:{httpbin.port}/")
|
||||||
assert len(cass) == 0
|
assert len(cass) == 0
|
||||||
urlopen("http://localhost:{}/".format(httpbin.port))
|
urlopen(f"http://localhost:{httpbin.port}/")
|
||||||
assert len(cass) == 1
|
assert len(cass) == 1
|
||||||
|
|
||||||
|
|
||||||
@@ -47,8 +48,8 @@ def test_ignore_localhost_and_httpbin(tmpdir, httpbin):
|
|||||||
with overridden_dns({"httpbin.org": "127.0.0.1"}):
|
with overridden_dns({"httpbin.org": "127.0.0.1"}):
|
||||||
cass_file = str(tmpdir.join("filter_qs.yaml"))
|
cass_file = str(tmpdir.join("filter_qs.yaml"))
|
||||||
with vcr.use_cassette(cass_file, ignore_hosts=["httpbin.org"], ignore_localhost=True) as cass:
|
with vcr.use_cassette(cass_file, ignore_hosts=["httpbin.org"], ignore_localhost=True) as cass:
|
||||||
urlopen("http://httpbin.org:{}".format(httpbin.port))
|
urlopen(f"http://httpbin.org:{httpbin.port}")
|
||||||
urlopen("http://localhost:{}".format(httpbin.port))
|
urlopen(f"http://localhost:{httpbin.port}")
|
||||||
assert len(cass) == 0
|
assert len(cass) == 0
|
||||||
|
|
||||||
|
|
||||||
@@ -56,12 +57,12 @@ def test_ignore_localhost_twice(tmpdir, httpbin):
|
|||||||
with overridden_dns({"httpbin.org": "127.0.0.1"}):
|
with overridden_dns({"httpbin.org": "127.0.0.1"}):
|
||||||
cass_file = str(tmpdir.join("filter_qs.yaml"))
|
cass_file = str(tmpdir.join("filter_qs.yaml"))
|
||||||
with vcr.use_cassette(cass_file, ignore_localhost=True) as cass:
|
with vcr.use_cassette(cass_file, ignore_localhost=True) as cass:
|
||||||
urlopen("http://localhost:{}".format(httpbin.port))
|
urlopen(f"http://localhost:{httpbin.port}")
|
||||||
assert len(cass) == 0
|
assert len(cass) == 0
|
||||||
urlopen("http://httpbin.org:{}".format(httpbin.port))
|
urlopen(f"http://httpbin.org:{httpbin.port}")
|
||||||
assert len(cass) == 1
|
assert len(cass) == 1
|
||||||
with vcr.use_cassette(cass_file, ignore_localhost=True) as cass:
|
with vcr.use_cassette(cass_file, ignore_localhost=True) as cass:
|
||||||
assert len(cass) == 1
|
assert len(cass) == 1
|
||||||
urlopen("http://localhost:{}".format(httpbin.port))
|
urlopen(f"http://localhost:{httpbin.port}")
|
||||||
urlopen("http://httpbin.org:{}".format(httpbin.port))
|
urlopen(f"http://httpbin.org:{httpbin.port}")
|
||||||
assert len(cass) == 1
|
assert len(cass) == 1
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import vcr
|
|
||||||
import pytest
|
|
||||||
from urllib.request import urlopen
|
from urllib.request import urlopen
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
import vcr
|
||||||
|
|
||||||
DEFAULT_URI = "http://httpbin.org/get?p1=q1&p2=q2" # base uri for testing
|
DEFAULT_URI = "http://httpbin.org/get?p1=q1&p2=q2" # base uri for testing
|
||||||
|
|
||||||
@@ -35,7 +36,6 @@ def cassette(tmpdir, httpbin, httpbin_secure):
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_matchers(httpbin, httpbin_secure, cassette, matcher, matching_uri, not_matching_uri):
|
def test_matchers(httpbin, httpbin_secure, cassette, matcher, matching_uri, not_matching_uri):
|
||||||
|
|
||||||
matching_uri = _replace_httpbin(matching_uri, httpbin, httpbin_secure)
|
matching_uri = _replace_httpbin(matching_uri, httpbin, httpbin_secure)
|
||||||
not_matching_uri = _replace_httpbin(not_matching_uri, httpbin, httpbin_secure)
|
not_matching_uri = _replace_httpbin(not_matching_uri, httpbin, httpbin_secure)
|
||||||
default_uri = _replace_httpbin(DEFAULT_URI, httpbin, httpbin_secure)
|
default_uri = _replace_httpbin(DEFAULT_URI, httpbin, httpbin_secure)
|
||||||
@@ -51,9 +51,11 @@ def test_matchers(httpbin, httpbin_secure, cassette, matcher, matching_uri, not_
|
|||||||
assert cass.play_count == 1
|
assert cass.play_count == 1
|
||||||
|
|
||||||
# play cassette with not matching on uri, it should fail
|
# play cassette with not matching on uri, it should fail
|
||||||
with pytest.raises(vcr.errors.CannotOverwriteExistingCassetteException):
|
with (
|
||||||
with vcr.use_cassette(cassette, match_on=[matcher]) as cass:
|
pytest.raises(vcr.errors.CannotOverwriteExistingCassetteException),
|
||||||
urlopen(not_matching_uri)
|
vcr.use_cassette(cassette, match_on=[matcher]) as cass,
|
||||||
|
):
|
||||||
|
urlopen(not_matching_uri)
|
||||||
|
|
||||||
|
|
||||||
def test_method_matcher(cassette, httpbin, httpbin_secure):
|
def test_method_matcher(cassette, httpbin, httpbin_secure):
|
||||||
@@ -65,17 +67,23 @@ def test_method_matcher(cassette, httpbin, httpbin_secure):
|
|||||||
assert cass.play_count == 1
|
assert cass.play_count == 1
|
||||||
|
|
||||||
# should fail if method does not match
|
# should fail if method does not match
|
||||||
with pytest.raises(vcr.errors.CannotOverwriteExistingCassetteException):
|
with (
|
||||||
with vcr.use_cassette(cassette, match_on=["method"]) as cass:
|
pytest.raises(vcr.errors.CannotOverwriteExistingCassetteException),
|
||||||
# is a POST request
|
vcr.use_cassette(cassette, match_on=["method"]) as cass,
|
||||||
urlopen(default_uri, data=b"")
|
):
|
||||||
|
# is a POST request
|
||||||
|
urlopen(default_uri, data=b"")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"uri", [DEFAULT_URI, "http://httpbin.org/get?p2=q2&p1=q1", "http://httpbin.org/get?p2=q2&p1=q1"]
|
"uri",
|
||||||
|
(
|
||||||
|
DEFAULT_URI,
|
||||||
|
"http://httpbin.org/get?p2=q2&p1=q1",
|
||||||
|
"http://httpbin.org/get?p2=q2&p1=q1",
|
||||||
|
),
|
||||||
)
|
)
|
||||||
def test_default_matcher_matches(cassette, uri, httpbin, httpbin_secure):
|
def test_default_matcher_matches(cassette, uri, httpbin, httpbin_secure):
|
||||||
|
|
||||||
uri = _replace_httpbin(uri, httpbin, httpbin_secure)
|
uri = _replace_httpbin(uri, httpbin, httpbin_secure)
|
||||||
|
|
||||||
with vcr.use_cassette(cassette) as cass:
|
with vcr.use_cassette(cassette) as cass:
|
||||||
@@ -94,14 +102,12 @@ def test_default_matcher_matches(cassette, uri, httpbin, httpbin_secure):
|
|||||||
)
|
)
|
||||||
def test_default_matcher_does_not_match(cassette, uri, httpbin, httpbin_secure):
|
def test_default_matcher_does_not_match(cassette, uri, httpbin, httpbin_secure):
|
||||||
uri = _replace_httpbin(uri, httpbin, httpbin_secure)
|
uri = _replace_httpbin(uri, httpbin, httpbin_secure)
|
||||||
with pytest.raises(vcr.errors.CannotOverwriteExistingCassetteException):
|
with pytest.raises(vcr.errors.CannotOverwriteExistingCassetteException), vcr.use_cassette(cassette):
|
||||||
with vcr.use_cassette(cassette):
|
urlopen(uri)
|
||||||
urlopen(uri)
|
|
||||||
|
|
||||||
|
|
||||||
def test_default_matcher_does_not_match_on_method(cassette, httpbin, httpbin_secure):
|
def test_default_matcher_does_not_match_on_method(cassette, httpbin, httpbin_secure):
|
||||||
default_uri = _replace_httpbin(DEFAULT_URI, httpbin, httpbin_secure)
|
default_uri = _replace_httpbin(DEFAULT_URI, httpbin, httpbin_secure)
|
||||||
with pytest.raises(vcr.errors.CannotOverwriteExistingCassetteException):
|
with pytest.raises(vcr.errors.CannotOverwriteExistingCassetteException), vcr.use_cassette(cassette):
|
||||||
with vcr.use_cassette(cassette):
|
# is a POST request
|
||||||
# is a POST request
|
urlopen(default_uri, data=b"")
|
||||||
urlopen(default_uri, data=b"")
|
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import pytest
|
|
||||||
import vcr
|
|
||||||
from urllib.request import urlopen
|
from urllib.request import urlopen
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
import vcr
|
||||||
|
from vcr.errors import CannotOverwriteExistingCassetteException
|
||||||
|
|
||||||
|
|
||||||
def test_making_extra_request_raises_exception(tmpdir, httpbin):
|
def test_making_extra_request_raises_exception(tmpdir, httpbin):
|
||||||
# make two requests in the first request that are considered
|
# make two requests in the first request that are considered
|
||||||
@@ -16,5 +19,5 @@ def test_making_extra_request_raises_exception(tmpdir, httpbin):
|
|||||||
with vcr.use_cassette(str(tmpdir.join("test.json")), match_on=["method"]):
|
with vcr.use_cassette(str(tmpdir.join("test.json")), match_on=["method"]):
|
||||||
assert urlopen(httpbin.url + "/status/200").getcode() == 200
|
assert urlopen(httpbin.url + "/status/200").getcode() == 200
|
||||||
assert urlopen(httpbin.url + "/status/201").getcode() == 201
|
assert urlopen(httpbin.url + "/status/201").getcode() == 201
|
||||||
with pytest.raises(Exception):
|
with pytest.raises(CannotOverwriteExistingCassetteException):
|
||||||
urlopen(httpbin.url + "/status/200")
|
urlopen(httpbin.url + "/status/200")
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""Test using a proxy."""
|
"""Test using a proxy."""
|
||||||
|
|
||||||
# External imports
|
import asyncio
|
||||||
import multiprocessing
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
import http.server
|
import http.server
|
||||||
import socketserver
|
import socketserver
|
||||||
|
import threading
|
||||||
from urllib.request import urlopen
|
from urllib.request import urlopen
|
||||||
|
|
||||||
# Internal imports
|
import pytest
|
||||||
|
|
||||||
import vcr
|
import vcr
|
||||||
|
|
||||||
# Conditional imports
|
# Conditional imports
|
||||||
@@ -32,20 +30,51 @@ class Proxy(http.server.SimpleHTTPRequestHandler):
|
|||||||
# In Python 2 the response is an addinfourl instance.
|
# In Python 2 the response is an addinfourl instance.
|
||||||
status = upstream_response.code
|
status = upstream_response.code
|
||||||
headers = upstream_response.info().items()
|
headers = upstream_response.info().items()
|
||||||
self.send_response(status, upstream_response.msg)
|
self.log_request(status)
|
||||||
|
self.send_response_only(status, upstream_response.msg)
|
||||||
for header in headers:
|
for header in headers:
|
||||||
self.send_header(*header)
|
self.send_header(*header)
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
self.copyfile(upstream_response, self.wfile)
|
self.copyfile(upstream_response, self.wfile)
|
||||||
|
|
||||||
|
def do_CONNECT(self):
|
||||||
|
host, port = self.path.split(":")
|
||||||
|
|
||||||
@pytest.yield_fixture(scope="session")
|
asyncio.run(self._tunnel(host, port, self.connection))
|
||||||
|
|
||||||
|
async def _tunnel(self, host, port, client_sock):
|
||||||
|
target_r, target_w = await asyncio.open_connection(host=host, port=port)
|
||||||
|
|
||||||
|
self.send_response(http.HTTPStatus.OK)
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
|
source_r, source_w = await asyncio.open_connection(sock=client_sock)
|
||||||
|
|
||||||
|
async def channel(reader, writer):
|
||||||
|
while True:
|
||||||
|
data = await reader.read(1024)
|
||||||
|
if not data:
|
||||||
|
break
|
||||||
|
writer.write(data)
|
||||||
|
await writer.drain()
|
||||||
|
|
||||||
|
writer.close()
|
||||||
|
await writer.wait_closed()
|
||||||
|
|
||||||
|
await asyncio.gather(
|
||||||
|
channel(target_r, source_w),
|
||||||
|
channel(source_r, target_w),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
def proxy_server():
|
def proxy_server():
|
||||||
httpd = socketserver.ThreadingTCPServer(("", 0), Proxy)
|
with socketserver.ThreadingTCPServer(("", 0), Proxy) as httpd:
|
||||||
proxy_process = multiprocessing.Process(target=httpd.serve_forever)
|
proxy_process = threading.Thread(target=httpd.serve_forever)
|
||||||
proxy_process.start()
|
proxy_process.start()
|
||||||
yield "http://{}:{}".format(*httpd.server_address)
|
yield "http://{}:{}".format(*httpd.server_address)
|
||||||
proxy_process.terminate()
|
httpd.shutdown()
|
||||||
|
proxy_process.join()
|
||||||
|
|
||||||
|
|
||||||
def test_use_proxy(tmpdir, httpbin, proxy_server):
|
def test_use_proxy(tmpdir, httpbin, proxy_server):
|
||||||
@@ -53,8 +82,26 @@ def test_use_proxy(tmpdir, httpbin, proxy_server):
|
|||||||
with vcr.use_cassette(str(tmpdir.join("proxy.yaml"))):
|
with vcr.use_cassette(str(tmpdir.join("proxy.yaml"))):
|
||||||
response = requests.get(httpbin.url, proxies={"http": proxy_server})
|
response = requests.get(httpbin.url, proxies={"http": proxy_server})
|
||||||
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("proxy.yaml"))) as cassette:
|
with vcr.use_cassette(str(tmpdir.join("proxy.yaml")), mode="none") as cassette:
|
||||||
cassette_response = requests.get(httpbin.url, proxies={"http": proxy_server})
|
cassette_response = requests.get(httpbin.url, proxies={"http": proxy_server})
|
||||||
|
|
||||||
assert cassette_response.headers == response.headers
|
assert cassette_response.headers == response.headers
|
||||||
assert cassette.play_count == 1
|
assert cassette.play_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_use_https_proxy(tmpdir, httpbin_secure, proxy_server):
|
||||||
|
"""Ensure that it works with an HTTPS proxy."""
|
||||||
|
with vcr.use_cassette(str(tmpdir.join("proxy.yaml"))):
|
||||||
|
response = requests.get(httpbin_secure.url, proxies={"https": proxy_server})
|
||||||
|
|
||||||
|
with vcr.use_cassette(str(tmpdir.join("proxy.yaml")), mode="none") as cassette:
|
||||||
|
cassette_response = requests.get(
|
||||||
|
httpbin_secure.url,
|
||||||
|
proxies={"https": proxy_server},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert cassette_response.headers == response.headers
|
||||||
|
assert cassette.play_count == 1
|
||||||
|
|
||||||
|
# The cassette URL points to httpbin, not the proxy
|
||||||
|
assert cassette.requests[0].url == httpbin_secure.url + "/"
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import pytest
|
|
||||||
import vcr
|
|
||||||
from urllib.request import urlopen
|
from urllib.request import urlopen
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
import vcr
|
||||||
|
from vcr.errors import CannotOverwriteExistingCassetteException
|
||||||
|
|
||||||
|
|
||||||
def test_once_record_mode(tmpdir, httpbin):
|
def test_once_record_mode(tmpdir, httpbin):
|
||||||
testfile = str(tmpdir.join("recordmode.yml"))
|
testfile = str(tmpdir.join("recordmode.yml"))
|
||||||
@@ -16,7 +19,7 @@ def test_once_record_mode(tmpdir, httpbin):
|
|||||||
# the first time, it's played from the cassette.
|
# the first time, it's played from the cassette.
|
||||||
# but, try to access something else from the same cassette, and an
|
# but, try to access something else from the same cassette, and an
|
||||||
# exception is raised.
|
# exception is raised.
|
||||||
with pytest.raises(Exception):
|
with pytest.raises(CannotOverwriteExistingCassetteException):
|
||||||
urlopen(httpbin.url + "/get").read()
|
urlopen(httpbin.url + "/get").read()
|
||||||
|
|
||||||
|
|
||||||
@@ -57,7 +60,7 @@ def test_new_episodes_record_mode(tmpdir, httpbin):
|
|||||||
assert cass.all_played
|
assert cass.all_played
|
||||||
|
|
||||||
# in the "new_episodes" record mode, we can add more requests to
|
# in the "new_episodes" record mode, we can add more requests to
|
||||||
# a cassette without repurcussions.
|
# a cassette without repercussions.
|
||||||
urlopen(httpbin.url + "/get").read()
|
urlopen(httpbin.url + "/get").read()
|
||||||
|
|
||||||
# one of the responses has been played
|
# one of the responses has been played
|
||||||
@@ -92,7 +95,7 @@ def test_new_episodes_record_mode_two_times(tmpdir, httpbin):
|
|||||||
assert urlopen(url).read() == original_second_response
|
assert urlopen(url).read() == original_second_response
|
||||||
# now that we are back in once mode, this should raise
|
# now that we are back in once mode, this should raise
|
||||||
# an error.
|
# an error.
|
||||||
with pytest.raises(Exception):
|
with pytest.raises(CannotOverwriteExistingCassetteException):
|
||||||
urlopen(url).read()
|
urlopen(url).read()
|
||||||
|
|
||||||
|
|
||||||
@@ -108,7 +111,7 @@ def test_all_record_mode(tmpdir, httpbin):
|
|||||||
urlopen(httpbin.url).read()
|
urlopen(httpbin.url).read()
|
||||||
|
|
||||||
# in the "all" record mode, we can add more requests to
|
# in the "all" record mode, we can add more requests to
|
||||||
# a cassette without repurcussions.
|
# a cassette without repercussions.
|
||||||
urlopen(httpbin.url + "/get").read()
|
urlopen(httpbin.url + "/get").read()
|
||||||
|
|
||||||
# The cassette was never actually played, even though it existed.
|
# The cassette was never actually played, even though it existed.
|
||||||
@@ -121,9 +124,11 @@ def test_none_record_mode(tmpdir, httpbin):
|
|||||||
# Cassette file doesn't exist, yet we are trying to make a request.
|
# Cassette file doesn't exist, yet we are trying to make a request.
|
||||||
# raise hell.
|
# raise hell.
|
||||||
testfile = str(tmpdir.join("recordmode.yml"))
|
testfile = str(tmpdir.join("recordmode.yml"))
|
||||||
with vcr.use_cassette(testfile, record_mode=vcr.mode.NONE):
|
with (
|
||||||
with pytest.raises(Exception):
|
vcr.use_cassette(testfile, record_mode=vcr.mode.NONE),
|
||||||
urlopen(httpbin.url).read()
|
pytest.raises(CannotOverwriteExistingCassetteException),
|
||||||
|
):
|
||||||
|
urlopen(httpbin.url).read()
|
||||||
|
|
||||||
|
|
||||||
def test_none_record_mode_with_existing_cassette(tmpdir, httpbin):
|
def test_none_record_mode_with_existing_cassette(tmpdir, httpbin):
|
||||||
@@ -138,5 +143,5 @@ def test_none_record_mode_with_existing_cassette(tmpdir, httpbin):
|
|||||||
urlopen(httpbin.url).read()
|
urlopen(httpbin.url).read()
|
||||||
assert cass.play_count == 1
|
assert cass.play_count == 1
|
||||||
# but if I try to hit the net, raise an exception.
|
# but if I try to hit the net, raise an exception.
|
||||||
with pytest.raises(Exception):
|
with pytest.raises(CannotOverwriteExistingCassetteException):
|
||||||
urlopen(httpbin.url + "/get").read()
|
urlopen(httpbin.url + "/get").read()
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import vcr
|
|
||||||
from urllib.request import urlopen
|
from urllib.request import urlopen
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
import vcr
|
||||||
|
|
||||||
|
|
||||||
def true_matcher(r1, r2):
|
def true_matcher(r1, r2):
|
||||||
return True
|
return True
|
||||||
@@ -10,6 +13,7 @@ def false_matcher(r1, r2):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.online
|
||||||
def test_registered_true_matcher(tmpdir, httpbin):
|
def test_registered_true_matcher(tmpdir, httpbin):
|
||||||
my_vcr = vcr.VCR()
|
my_vcr = vcr.VCR()
|
||||||
my_vcr.register_matcher("true", true_matcher)
|
my_vcr.register_matcher("true", true_matcher)
|
||||||
@@ -21,10 +25,11 @@ def test_registered_true_matcher(tmpdir, httpbin):
|
|||||||
|
|
||||||
with my_vcr.use_cassette(testfile, match_on=["true"]):
|
with my_vcr.use_cassette(testfile, match_on=["true"]):
|
||||||
# I can get the response twice even though I only asked for it once
|
# I can get the response twice even though I only asked for it once
|
||||||
urlopen(httpbin.url + "/get")
|
urlopen(httpbin.url)
|
||||||
urlopen(httpbin.url + "/get")
|
urlopen(httpbin.url)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.online
|
||||||
def test_registered_false_matcher(tmpdir, httpbin):
|
def test_registered_false_matcher(tmpdir, httpbin):
|
||||||
my_vcr = vcr.VCR()
|
my_vcr = vcr.VCR()
|
||||||
my_vcr.register_matcher("false", false_matcher)
|
my_vcr.register_matcher("false", false_matcher)
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""Tests for cassettes with custom persistence"""
|
"""Tests for cassettes with custom persistence"""
|
||||||
|
|
||||||
# External imports
|
# External imports
|
||||||
import os
|
import os
|
||||||
from urllib.request import urlopen
|
from urllib.request import urlopen
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
# Internal imports
|
# Internal imports
|
||||||
import vcr
|
import vcr
|
||||||
from vcr.persisters.filesystem import FilesystemPersister
|
from vcr.persisters.filesystem import CassetteDecodeError, CassetteNotFoundError, FilesystemPersister
|
||||||
|
|
||||||
|
|
||||||
class CustomFilesystemPersister(object):
|
class CustomFilesystemPersister:
|
||||||
"""Behaves just like default FilesystemPersister but adds .test extension
|
"""Behaves just like default FilesystemPersister but adds .test extension
|
||||||
to the cassette file"""
|
to the cassette file"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def load_cassette(cassette_path, serializer):
|
def load_cassette(cassette_path, serializer):
|
||||||
@@ -25,12 +26,25 @@ class CustomFilesystemPersister(object):
|
|||||||
FilesystemPersister.save_cassette(cassette_path, cassette_dict, serializer)
|
FilesystemPersister.save_cassette(cassette_path, cassette_dict, serializer)
|
||||||
|
|
||||||
|
|
||||||
|
class BadPersister(FilesystemPersister):
|
||||||
|
"""A bad persister that raises different errors."""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def load_cassette(cassette_path, serializer):
|
||||||
|
if "nonexistent" in cassette_path:
|
||||||
|
raise CassetteNotFoundError()
|
||||||
|
elif "encoding" in cassette_path:
|
||||||
|
raise CassetteDecodeError()
|
||||||
|
else:
|
||||||
|
raise ValueError("buggy persister")
|
||||||
|
|
||||||
|
|
||||||
def test_save_cassette_with_custom_persister(tmpdir, httpbin):
|
def test_save_cassette_with_custom_persister(tmpdir, httpbin):
|
||||||
"""Ensure you can save a cassette using custom persister"""
|
"""Ensure you can save a cassette using custom persister"""
|
||||||
my_vcr = vcr.VCR()
|
my_vcr = vcr.VCR()
|
||||||
my_vcr.register_persister(CustomFilesystemPersister)
|
my_vcr.register_persister(CustomFilesystemPersister)
|
||||||
|
|
||||||
# Check to make sure directory doesnt exist
|
# Check to make sure directory doesn't exist
|
||||||
assert not os.path.exists(str(tmpdir.join("nonexistent")))
|
assert not os.path.exists(str(tmpdir.join("nonexistent")))
|
||||||
|
|
||||||
# Run VCR to create dir and cassette file using new save_cassette callback
|
# Run VCR to create dir and cassette file using new save_cassette callback
|
||||||
@@ -52,4 +66,22 @@ def test_load_cassette_with_custom_persister(tmpdir, httpbin):
|
|||||||
|
|
||||||
with my_vcr.use_cassette(test_fixture, serializer="json"):
|
with my_vcr.use_cassette(test_fixture, serializer="json"):
|
||||||
response = urlopen(httpbin.url).read()
|
response = urlopen(httpbin.url).read()
|
||||||
assert b"difficult sometimes" in response
|
assert b"HTTP Request & Response Service" in response
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_cassette_persister_exception_handling(tmpdir, httpbin):
|
||||||
|
"""
|
||||||
|
Ensure expected errors from persister are swallowed while unexpected ones
|
||||||
|
are passed up the call stack.
|
||||||
|
"""
|
||||||
|
my_vcr = vcr.VCR()
|
||||||
|
my_vcr.register_persister(BadPersister)
|
||||||
|
|
||||||
|
with my_vcr.use_cassette("bad/nonexistent") as cass:
|
||||||
|
assert len(cass) == 0
|
||||||
|
|
||||||
|
with my_vcr.use_cassette("bad/encoding") as cass:
|
||||||
|
assert len(cass) == 0
|
||||||
|
|
||||||
|
with pytest.raises(ValueError), my_vcr.use_cassette("bad/buggy") as cass:
|
||||||
|
pass
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import vcr
|
|
||||||
from urllib.request import urlopen
|
from urllib.request import urlopen
|
||||||
|
|
||||||
|
import vcr
|
||||||
|
|
||||||
|
|
||||||
def test_recorded_request_uri_with_redirected_request(tmpdir, httpbin):
|
def test_recorded_request_uri_with_redirected_request(tmpdir, httpbin):
|
||||||
with vcr.use_cassette(str(tmpdir.join("test.yml"))) as cass:
|
with vcr.use_cassette(str(tmpdir.join("test.yml"))) as cass:
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""Test requests' interaction with vcr"""
|
"""Test requests' interaction with vcr"""
|
||||||
import platform
|
|
||||||
import pytest
|
import pytest
|
||||||
import sys
|
|
||||||
import vcr
|
import vcr
|
||||||
from assertions import assert_cassette_empty, assert_is_json
|
|
||||||
|
from ..assertions import assert_cassette_empty, assert_is_json_bytes
|
||||||
|
|
||||||
requests = pytest.importorskip("requests")
|
requests = pytest.importorskip("requests")
|
||||||
from requests.exceptions import ConnectionError # noqa E402
|
|
||||||
|
|
||||||
|
|
||||||
def test_status_code(httpbin_both, tmpdir):
|
def test_status_code(httpbin_both, tmpdir):
|
||||||
@@ -116,26 +115,6 @@ def test_post_chunked_binary(tmpdir, httpbin):
|
|||||||
assert req1 == req2
|
assert req1 == req2
|
||||||
|
|
||||||
|
|
||||||
@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"])
|
|
||||||
data2 = iter([b"data", b"to", b"send"])
|
|
||||||
url = httpbin_secure.url + "/post"
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("requests.yaml"))):
|
|
||||||
req1 = requests.post(url, data1).content
|
|
||||||
print(req1)
|
|
||||||
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("requests.yaml"))):
|
|
||||||
req2 = requests.post(url, data2).content
|
|
||||||
|
|
||||||
assert req1 == req2
|
|
||||||
|
|
||||||
|
|
||||||
def test_redirects(tmpdir, httpbin_both):
|
def test_redirects(tmpdir, httpbin_both):
|
||||||
"""Ensure that we can handle redirects"""
|
"""Ensure that we can handle redirects"""
|
||||||
url = httpbin_both + "/redirect-to?url=bytes/1024"
|
url = httpbin_both + "/redirect-to?url=bytes/1024"
|
||||||
@@ -150,6 +129,17 @@ def test_redirects(tmpdir, httpbin_both):
|
|||||||
assert cass.play_count == 2
|
assert cass.play_count == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_raw_stream(tmpdir, httpbin):
|
||||||
|
expected_response = requests.get(httpbin.url, stream=True)
|
||||||
|
expected_content = b"".join(expected_response.raw.stream())
|
||||||
|
|
||||||
|
for _ in range(2): # one for recording, one for cassette reply
|
||||||
|
with vcr.use_cassette(str(tmpdir.join("raw_stream.yaml"))):
|
||||||
|
actual_response = requests.get(httpbin.url, stream=True)
|
||||||
|
actual_content = b"".join(actual_response.raw.stream())
|
||||||
|
assert actual_content == expected_content
|
||||||
|
|
||||||
|
|
||||||
def test_cross_scheme(tmpdir, httpbin_secure, httpbin):
|
def test_cross_scheme(tmpdir, httpbin_secure, httpbin):
|
||||||
"""Ensure that requests between schemes are treated separately"""
|
"""Ensure that requests between schemes are treated separately"""
|
||||||
# First fetch a url under http, and then again under https and then
|
# First fetch a url under http, and then again under https and then
|
||||||
@@ -162,20 +152,41 @@ def test_cross_scheme(tmpdir, httpbin_secure, httpbin):
|
|||||||
assert len(cass) == 2
|
assert len(cass) == 2
|
||||||
|
|
||||||
|
|
||||||
def test_gzip(tmpdir, httpbin_both):
|
def test_gzip__decode_compressed_response_false(tmpdir, httpbin_both):
|
||||||
"""
|
"""
|
||||||
Ensure that requests (actually urllib3) is able to automatically decompress
|
Ensure that requests (actually urllib3) is able to automatically decompress
|
||||||
the response body
|
the response body
|
||||||
"""
|
"""
|
||||||
|
for _ in range(2): # one for recording, one for re-playing
|
||||||
|
with vcr.use_cassette(str(tmpdir.join("gzip.yaml"))):
|
||||||
|
response = requests.get(httpbin_both + "/gzip")
|
||||||
|
assert response.headers["content-encoding"] == "gzip" # i.e. not removed
|
||||||
|
assert_is_json_bytes(response.content) # i.e. uncompressed bytes
|
||||||
|
|
||||||
|
|
||||||
|
def test_gzip__decode_compressed_response_true(tmpdir, httpbin_both):
|
||||||
url = httpbin_both + "/gzip"
|
url = httpbin_both + "/gzip"
|
||||||
response = requests.get(url)
|
|
||||||
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("gzip.yaml"))):
|
expected_response = requests.get(url)
|
||||||
response = requests.get(url)
|
expected_content = expected_response.content
|
||||||
assert_is_json(response.content)
|
assert expected_response.headers["content-encoding"] == "gzip" # self-test
|
||||||
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("gzip.yaml"))):
|
with vcr.use_cassette(
|
||||||
assert_is_json(response.content)
|
str(tmpdir.join("decode_compressed.yaml")),
|
||||||
|
decode_compressed_response=True,
|
||||||
|
) as cassette:
|
||||||
|
r = requests.get(url)
|
||||||
|
assert r.headers["content-encoding"] == "gzip" # i.e. not removed
|
||||||
|
assert r.content == expected_content
|
||||||
|
|
||||||
|
# Has the cassette body been decompressed?
|
||||||
|
cassette_response_body = cassette.responses[0]["body"]["string"]
|
||||||
|
assert isinstance(cassette_response_body, str)
|
||||||
|
|
||||||
|
with vcr.use_cassette(str(tmpdir.join("decode_compressed.yaml")), decode_compressed_response=True):
|
||||||
|
r = requests.get(url)
|
||||||
|
assert "content-encoding" not in r.headers # i.e. removed
|
||||||
|
assert r.content == expected_content
|
||||||
|
|
||||||
|
|
||||||
def test_session_and_connection_close(tmpdir, httpbin):
|
def test_session_and_connection_close(tmpdir, httpbin):
|
||||||
@@ -255,7 +266,7 @@ def test_nested_cassettes_with_session_created_before_nesting(httpbin_both, tmpd
|
|||||||
def test_post_file(tmpdir, httpbin_both):
|
def test_post_file(tmpdir, httpbin_both):
|
||||||
"""Ensure that we handle posting a file."""
|
"""Ensure that we handle posting a file."""
|
||||||
url = httpbin_both + "/post"
|
url = httpbin_both + "/post"
|
||||||
with vcr.use_cassette(str(tmpdir.join("post_file.yaml"))) as cass, open("tox.ini", "rb") as f:
|
with vcr.use_cassette(str(tmpdir.join("post_file.yaml"))) as cass, open(".editorconfig", "rb") as f:
|
||||||
original_response = requests.post(url, f).content
|
original_response = requests.post(url, f).content
|
||||||
|
|
||||||
# This also tests that we do the right thing with matching the body when they are files.
|
# This also tests that we do the right thing with matching the body when they are files.
|
||||||
@@ -263,10 +274,10 @@ def test_post_file(tmpdir, httpbin_both):
|
|||||||
str(tmpdir.join("post_file.yaml")),
|
str(tmpdir.join("post_file.yaml")),
|
||||||
match_on=("method", "scheme", "host", "port", "path", "query", "body"),
|
match_on=("method", "scheme", "host", "port", "path", "query", "body"),
|
||||||
) as cass:
|
) as cass:
|
||||||
with open("tox.ini", "rb") as f:
|
with open(".editorconfig", "rb") as f:
|
||||||
tox_content = f.read()
|
editorconfig = f.read()
|
||||||
assert cass.requests[0].body.read() == tox_content
|
assert cass.requests[0].body.read() == editorconfig
|
||||||
with open("tox.ini", "rb") as f:
|
with open(".editorconfig", "rb") as f:
|
||||||
new_response = requests.post(url, f).content
|
new_response = requests.post(url, f).content
|
||||||
assert original_response == new_response
|
assert original_response == new_response
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import vcr
|
|
||||||
import zlib
|
|
||||||
import json
|
|
||||||
import http.client as httplib
|
import http.client as httplib
|
||||||
|
import json
|
||||||
|
import zlib
|
||||||
|
|
||||||
from assertions import assert_is_json
|
import vcr
|
||||||
|
|
||||||
|
from ..assertions import assert_is_json_bytes
|
||||||
|
|
||||||
|
|
||||||
def _headers_are_case_insensitive(host, port):
|
def _headers_are_case_insensitive(host, port):
|
||||||
@@ -64,8 +65,8 @@ def test_original_decoded_response_is_not_modified(tmpdir, httpbin):
|
|||||||
inside = conn.getresponse()
|
inside = conn.getresponse()
|
||||||
|
|
||||||
# Assert that we do not modify the original response while appending
|
# Assert that we do not modify the original response while appending
|
||||||
# to the casssette.
|
# to the cassette.
|
||||||
assert "gzip" == inside.headers["content-encoding"]
|
assert inside.headers["content-encoding"] == "gzip"
|
||||||
|
|
||||||
# They should effectively be the same response.
|
# They should effectively be the same response.
|
||||||
inside_headers = (h for h in inside.headers.items() if h[0].lower() != "date")
|
inside_headers = (h for h in inside.headers.items() if h[0].lower() != "date")
|
||||||
@@ -83,7 +84,7 @@ def test_original_decoded_response_is_not_modified(tmpdir, httpbin):
|
|||||||
inside = conn.getresponse()
|
inside = conn.getresponse()
|
||||||
|
|
||||||
assert "content-encoding" not in inside.headers
|
assert "content-encoding" not in inside.headers
|
||||||
assert_is_json(inside.read())
|
assert_is_json_bytes(inside.read())
|
||||||
|
|
||||||
|
|
||||||
def _make_before_record_response(fields, replacement="[REDACTED]"):
|
def _make_before_record_response(fields, replacement="[REDACTED]"):
|
||||||
@@ -119,9 +120,9 @@ def test_original_response_is_not_modified_by_before_filter(tmpdir, httpbin):
|
|||||||
|
|
||||||
# The scrubbed field should be the same, because no cassette existed.
|
# The scrubbed field should be the same, because no cassette existed.
|
||||||
# Furthermore, the responses should be identical.
|
# Furthermore, the responses should be identical.
|
||||||
inside_body = json.loads(inside.read().decode("utf-8"))
|
inside_body = json.loads(inside.read())
|
||||||
outside_body = json.loads(outside.read().decode("utf-8"))
|
outside_body = json.loads(outside.read())
|
||||||
assert not inside_body[field_to_scrub] == replacement
|
assert inside_body[field_to_scrub] != replacement
|
||||||
assert inside_body[field_to_scrub] == outside_body[field_to_scrub]
|
assert inside_body[field_to_scrub] == outside_body[field_to_scrub]
|
||||||
|
|
||||||
# Ensure that when a cassette exists, the scrubbed response is returned.
|
# Ensure that when a cassette exists, the scrubbed response is returned.
|
||||||
@@ -130,5 +131,5 @@ def test_original_response_is_not_modified_by_before_filter(tmpdir, httpbin):
|
|||||||
conn.request("GET", "/get")
|
conn.request("GET", "/get")
|
||||||
inside = conn.getresponse()
|
inside = conn.getresponse()
|
||||||
|
|
||||||
inside_body = json.loads(inside.read().decode("utf-8"))
|
inside_body = json.loads(inside.read())
|
||||||
assert inside_body[field_to_scrub] == replacement
|
assert inside_body[field_to_scrub] == replacement
|
||||||
|
|||||||
@@ -1,33 +1,60 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""Test requests' interaction with vcr"""
|
"""Test requests' interaction with vcr"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import functools
|
||||||
|
import inspect
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
|
import ssl
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
import vcr
|
import vcr
|
||||||
from vcr.errors import CannotOverwriteExistingCassetteException
|
from vcr.errors import CannotOverwriteExistingCassetteException
|
||||||
|
|
||||||
from assertions import assert_cassette_empty, assert_is_json
|
from ..assertions import assert_cassette_empty, assert_is_json_bytes
|
||||||
|
|
||||||
tornado = pytest.importorskip("tornado")
|
tornado = pytest.importorskip("tornado")
|
||||||
|
gen = pytest.importorskip("tornado.gen")
|
||||||
http = pytest.importorskip("tornado.httpclient")
|
http = pytest.importorskip("tornado.httpclient")
|
||||||
|
|
||||||
# whether the current version of Tornado supports the raise_error argument for
|
# whether the current version of Tornado supports the raise_error argument for
|
||||||
# fetch().
|
# fetch().
|
||||||
supports_raise_error = tornado.version_info >= (4,)
|
supports_raise_error = tornado.version_info >= (4,)
|
||||||
|
raise_error_for_response_code_only = tornado.version_info >= (6,)
|
||||||
|
|
||||||
|
|
||||||
|
def gen_test(func):
|
||||||
|
@functools.wraps(func)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
async def coro():
|
||||||
|
return await gen.coroutine(func)(*args, **kwargs)
|
||||||
|
|
||||||
|
return asyncio.run(coro())
|
||||||
|
|
||||||
|
# Patch the signature so pytest can inject fixtures
|
||||||
|
# we can't use wrapt.decorator because it returns a generator function
|
||||||
|
wrapper.__signature__ = inspect.signature(func)
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(params=["simple", "curl", "default"])
|
@pytest.fixture(params=["simple", "curl", "default"])
|
||||||
def get_client(request):
|
def get_client(request):
|
||||||
|
ca_bundle_path = os.environ.get("REQUESTS_CA_BUNDLE")
|
||||||
|
ssl_ctx = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
|
||||||
|
ssl_ctx.load_verify_locations(cafile=ca_bundle_path)
|
||||||
|
ssl_ctx.verify_mode = ssl.CERT_REQUIRED
|
||||||
|
|
||||||
if request.param == "simple":
|
if request.param == "simple":
|
||||||
from tornado import simple_httpclient as simple
|
from tornado import simple_httpclient as simple
|
||||||
|
|
||||||
return lambda: simple.SimpleAsyncHTTPClient()
|
return lambda: simple.SimpleAsyncHTTPClient(defaults={"ssl_options": ssl_ctx})
|
||||||
elif request.param == "curl":
|
|
||||||
|
if request.param == "curl":
|
||||||
curl = pytest.importorskip("tornado.curl_httpclient")
|
curl = pytest.importorskip("tornado.curl_httpclient")
|
||||||
return lambda: curl.CurlAsyncHTTPClient()
|
return lambda: curl.CurlAsyncHTTPClient(defaults={"ca_certs": ca_bundle_path})
|
||||||
else:
|
|
||||||
return lambda: http.AsyncHTTPClient()
|
return lambda: http.AsyncHTTPClient(defaults={"ssl_options": ssl_ctx})
|
||||||
|
|
||||||
|
|
||||||
def get(client, url, **kwargs):
|
def get(client, url, **kwargs):
|
||||||
@@ -44,67 +71,65 @@ def post(client, url, data=None, **kwargs):
|
|||||||
return client.fetch(http.HTTPRequest(url, method="POST", **kwargs))
|
return client.fetch(http.HTTPRequest(url, method="POST", **kwargs))
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(params=["https", "http"])
|
@pytest.mark.online
|
||||||
def scheme(request):
|
@gen_test
|
||||||
"""Fixture that returns both http and https."""
|
def test_status_code(get_client, tmpdir, httpbin_both):
|
||||||
return request.param
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gen_test
|
|
||||||
def test_status_code(get_client, scheme, tmpdir):
|
|
||||||
"""Ensure that we can read the status code"""
|
"""Ensure that we can read the status code"""
|
||||||
url = scheme + "://httpbin.org/"
|
url = httpbin_both.url
|
||||||
with vcr.use_cassette(str(tmpdir.join("atts.yaml"))):
|
with vcr.use_cassette(str(tmpdir.join("atts.yaml"))):
|
||||||
status_code = (yield get(get_client(), url)).code
|
status_code = (yield get(get_client(), url)).code
|
||||||
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("atts.yaml"))) as cass:
|
with vcr.use_cassette(str(tmpdir.join("atts.yaml"))) as cass:
|
||||||
assert status_code == (yield get(get_client(), url)).code
|
assert status_code == (yield get(get_client(), url)).code
|
||||||
assert 1 == cass.play_count
|
assert cass.play_count == 1
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gen_test
|
@pytest.mark.online
|
||||||
def test_headers(get_client, scheme, tmpdir):
|
@gen_test
|
||||||
|
def test_headers(get_client, httpbin_both, tmpdir):
|
||||||
"""Ensure that we can read the headers back"""
|
"""Ensure that we can read the headers back"""
|
||||||
url = scheme + "://httpbin.org/"
|
url = httpbin_both.url
|
||||||
with vcr.use_cassette(str(tmpdir.join("headers.yaml"))):
|
with vcr.use_cassette(str(tmpdir.join("headers.yaml"))):
|
||||||
headers = (yield get(get_client(), url)).headers
|
headers = (yield get(get_client(), url)).headers
|
||||||
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("headers.yaml"))) as cass:
|
with vcr.use_cassette(str(tmpdir.join("headers.yaml"))) as cass:
|
||||||
assert headers == (yield get(get_client(), url)).headers
|
assert headers == (yield get(get_client(), url)).headers
|
||||||
assert 1 == cass.play_count
|
assert cass.play_count == 1
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gen_test
|
@pytest.mark.online
|
||||||
def test_body(get_client, tmpdir, scheme):
|
@gen_test
|
||||||
|
def test_body(get_client, tmpdir, httpbin_both):
|
||||||
"""Ensure the responses are all identical enough"""
|
"""Ensure the responses are all identical enough"""
|
||||||
|
|
||||||
url = scheme + "://httpbin.org/bytes/1024"
|
url = httpbin_both.url + "/bytes/1024"
|
||||||
with vcr.use_cassette(str(tmpdir.join("body.yaml"))):
|
with vcr.use_cassette(str(tmpdir.join("body.yaml"))):
|
||||||
content = (yield get(get_client(), url)).body
|
content = (yield get(get_client(), url)).body
|
||||||
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("body.yaml"))) as cass:
|
with vcr.use_cassette(str(tmpdir.join("body.yaml"))) as cass:
|
||||||
assert content == (yield get(get_client(), url)).body
|
assert content == (yield get(get_client(), url)).body
|
||||||
assert 1 == cass.play_count
|
assert cass.play_count == 1
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gen_test
|
@gen_test
|
||||||
def test_effective_url(get_client, scheme, tmpdir):
|
def test_effective_url(get_client, tmpdir, httpbin):
|
||||||
"""Ensure that the effective_url is captured"""
|
"""Ensure that the effective_url is captured"""
|
||||||
url = scheme + "://httpbin.org/redirect-to?url=/html"
|
url = httpbin.url + "/redirect/1"
|
||||||
with vcr.use_cassette(str(tmpdir.join("url.yaml"))):
|
with vcr.use_cassette(str(tmpdir.join("url.yaml"))):
|
||||||
effective_url = (yield get(get_client(), url)).effective_url
|
effective_url = (yield get(get_client(), url)).effective_url
|
||||||
assert effective_url == scheme + "://httpbin.org/html"
|
assert effective_url == httpbin.url + "/get"
|
||||||
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("url.yaml"))) as cass:
|
with vcr.use_cassette(str(tmpdir.join("url.yaml"))) as cass:
|
||||||
assert effective_url == (yield get(get_client(), url)).effective_url
|
assert effective_url == (yield get(get_client(), url)).effective_url
|
||||||
assert 1 == cass.play_count
|
assert cass.play_count == 1
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gen_test
|
@pytest.mark.online
|
||||||
def test_auth(get_client, tmpdir, scheme):
|
@gen_test
|
||||||
|
def test_auth(get_client, tmpdir, httpbin_both):
|
||||||
"""Ensure that we can handle basic auth"""
|
"""Ensure that we can handle basic auth"""
|
||||||
auth = ("user", "passwd")
|
auth = ("user", "passwd")
|
||||||
url = scheme + "://httpbin.org/basic-auth/user/passwd"
|
url = httpbin_both.url + "/basic-auth/user/passwd"
|
||||||
with vcr.use_cassette(str(tmpdir.join("auth.yaml"))):
|
with vcr.use_cassette(str(tmpdir.join("auth.yaml"))):
|
||||||
one = yield get(get_client(), url, auth_username=auth[0], auth_password=auth[1])
|
one = yield get(get_client(), url, auth_username=auth[0], auth_password=auth[1])
|
||||||
|
|
||||||
@@ -112,14 +137,15 @@ def test_auth(get_client, tmpdir, scheme):
|
|||||||
two = yield get(get_client(), url, auth_username=auth[0], auth_password=auth[1])
|
two = yield get(get_client(), url, auth_username=auth[0], auth_password=auth[1])
|
||||||
assert one.body == two.body
|
assert one.body == two.body
|
||||||
assert one.code == two.code
|
assert one.code == two.code
|
||||||
assert 1 == cass.play_count
|
assert cass.play_count == 1
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gen_test
|
@pytest.mark.online
|
||||||
def test_auth_failed(get_client, tmpdir, scheme):
|
@gen_test
|
||||||
|
def test_auth_failed(get_client, tmpdir, httpbin_both):
|
||||||
"""Ensure that we can save failed auth statuses"""
|
"""Ensure that we can save failed auth statuses"""
|
||||||
auth = ("user", "wrongwrongwrong")
|
auth = ("user", "wrongwrongwrong")
|
||||||
url = scheme + "://httpbin.org/basic-auth/user/passwd"
|
url = httpbin_both.url + "/basic-auth/user/passwd"
|
||||||
with vcr.use_cassette(str(tmpdir.join("auth-failed.yaml"))) as cass:
|
with vcr.use_cassette(str(tmpdir.join("auth-failed.yaml"))) as cass:
|
||||||
# Ensure that this is empty to begin with
|
# Ensure that this is empty to begin with
|
||||||
assert_cassette_empty(cass)
|
assert_cassette_empty(cass)
|
||||||
@@ -135,14 +161,15 @@ def test_auth_failed(get_client, tmpdir, scheme):
|
|||||||
assert exc_info.value.code == 401
|
assert exc_info.value.code == 401
|
||||||
assert one.body == two.body
|
assert one.body == two.body
|
||||||
assert one.code == two.code == 401
|
assert one.code == two.code == 401
|
||||||
assert 1 == cass.play_count
|
assert cass.play_count == 1
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gen_test
|
@pytest.mark.online
|
||||||
def test_post(get_client, tmpdir, scheme):
|
@gen_test
|
||||||
|
def test_post(get_client, tmpdir, httpbin_both):
|
||||||
"""Ensure that we can post and cache the results"""
|
"""Ensure that we can post and cache the results"""
|
||||||
data = {"key1": "value1", "key2": "value2"}
|
data = {"key1": "value1", "key2": "value2"}
|
||||||
url = scheme + "://httpbin.org/post"
|
url = httpbin_both.url + "/post"
|
||||||
with vcr.use_cassette(str(tmpdir.join("requests.yaml"))):
|
with vcr.use_cassette(str(tmpdir.join("requests.yaml"))):
|
||||||
req1 = (yield post(get_client(), url, data)).body
|
req1 = (yield post(get_client(), url, data)).body
|
||||||
|
|
||||||
@@ -150,13 +177,13 @@ def test_post(get_client, tmpdir, scheme):
|
|||||||
req2 = (yield post(get_client(), url, data)).body
|
req2 = (yield post(get_client(), url, data)).body
|
||||||
|
|
||||||
assert req1 == req2
|
assert req1 == req2
|
||||||
assert 1 == cass.play_count
|
assert cass.play_count == 1
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gen_test
|
@gen_test
|
||||||
def test_redirects(get_client, tmpdir, scheme):
|
def test_redirects(get_client, tmpdir, httpbin):
|
||||||
"""Ensure that we can handle redirects"""
|
"""Ensure that we can handle redirects"""
|
||||||
url = scheme + "://httpbin.org/redirect-to?url=bytes/1024"
|
url = httpbin + "/redirect-to?url=bytes/1024&status_code=301"
|
||||||
with vcr.use_cassette(str(tmpdir.join("requests.yaml"))):
|
with vcr.use_cassette(str(tmpdir.join("requests.yaml"))):
|
||||||
content = (yield get(get_client(), url)).body
|
content = (yield get(get_client(), url)).body
|
||||||
|
|
||||||
@@ -165,32 +192,38 @@ def test_redirects(get_client, tmpdir, scheme):
|
|||||||
assert cass.play_count == 1
|
assert cass.play_count == 1
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gen_test
|
@pytest.mark.online
|
||||||
def test_cross_scheme(get_client, tmpdir, scheme):
|
@gen_test
|
||||||
|
def test_cross_scheme(get_client, tmpdir, httpbin, httpbin_secure):
|
||||||
"""Ensure that requests between schemes are treated separately"""
|
"""Ensure that requests between schemes are treated separately"""
|
||||||
# First fetch a url under http, and then again under https and then
|
# First fetch a url under http, and then again under https and then
|
||||||
# ensure that we haven't served anything out of cache, and we have two
|
# ensure that we haven't served anything out of cache, and we have two
|
||||||
# requests / response pairs in the cassette
|
# requests / response pairs in the cassette
|
||||||
|
|
||||||
|
url = httpbin.url
|
||||||
|
url_secure = httpbin_secure.url
|
||||||
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("cross_scheme.yaml"))) as cass:
|
with vcr.use_cassette(str(tmpdir.join("cross_scheme.yaml"))) as cass:
|
||||||
yield get(get_client(), "https://httpbin.org/")
|
yield get(get_client(), url)
|
||||||
yield get(get_client(), "http://httpbin.org/")
|
yield get(get_client(), url_secure)
|
||||||
assert cass.play_count == 0
|
assert cass.play_count == 0
|
||||||
assert len(cass) == 2
|
assert len(cass) == 2
|
||||||
|
|
||||||
# Then repeat the same requests and ensure both were replayed.
|
# Then repeat the same requests and ensure both were replayed.
|
||||||
with vcr.use_cassette(str(tmpdir.join("cross_scheme.yaml"))) as cass:
|
with vcr.use_cassette(str(tmpdir.join("cross_scheme.yaml"))) as cass:
|
||||||
yield get(get_client(), "https://httpbin.org/")
|
yield get(get_client(), url)
|
||||||
yield get(get_client(), "http://httpbin.org/")
|
yield get(get_client(), url_secure)
|
||||||
assert cass.play_count == 2
|
assert cass.play_count == 2
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gen_test
|
@pytest.mark.online
|
||||||
def test_gzip(get_client, tmpdir, scheme):
|
@gen_test
|
||||||
|
def test_gzip(get_client, tmpdir, httpbin_both):
|
||||||
"""
|
"""
|
||||||
Ensure that httpclient is able to automatically decompress the response
|
Ensure that httpclient is able to automatically decompress the response
|
||||||
body
|
body
|
||||||
"""
|
"""
|
||||||
url = scheme + "://httpbin.org/gzip"
|
url = httpbin_both + "/gzip"
|
||||||
|
|
||||||
# use_gzip was renamed to decompress_response in 4.0
|
# use_gzip was renamed to decompress_response in 4.0
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
@@ -201,36 +234,39 @@ def test_gzip(get_client, tmpdir, scheme):
|
|||||||
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("gzip.yaml"))):
|
with vcr.use_cassette(str(tmpdir.join("gzip.yaml"))):
|
||||||
response = yield get(get_client(), url, **kwargs)
|
response = yield get(get_client(), url, **kwargs)
|
||||||
assert_is_json(response.body)
|
assert_is_json_bytes(response.body)
|
||||||
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("gzip.yaml"))) as cass:
|
with vcr.use_cassette(str(tmpdir.join("gzip.yaml"))) as cass:
|
||||||
response = yield get(get_client(), url, **kwargs)
|
response = yield get(get_client(), url, **kwargs)
|
||||||
assert_is_json(response.body)
|
assert_is_json_bytes(response.body)
|
||||||
assert 1 == cass.play_count
|
assert cass.play_count == 1
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gen_test
|
@pytest.mark.online
|
||||||
def test_https_with_cert_validation_disabled(get_client, tmpdir):
|
@gen_test
|
||||||
|
def test_https_with_cert_validation_disabled(get_client, tmpdir, httpbin_secure):
|
||||||
cass_path = str(tmpdir.join("cert_validation_disabled.yaml"))
|
cass_path = str(tmpdir.join("cert_validation_disabled.yaml"))
|
||||||
|
|
||||||
|
url = httpbin_secure.url
|
||||||
|
|
||||||
with vcr.use_cassette(cass_path):
|
with vcr.use_cassette(cass_path):
|
||||||
yield get(get_client(), "https://httpbin.org", validate_cert=False)
|
yield get(get_client(), url, validate_cert=False)
|
||||||
|
|
||||||
with vcr.use_cassette(cass_path) as cass:
|
with vcr.use_cassette(cass_path) as cass:
|
||||||
yield get(get_client(), "https://httpbin.org", validate_cert=False)
|
yield get(get_client(), url, validate_cert=False)
|
||||||
assert 1 == cass.play_count
|
assert cass.play_count == 1
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gen_test
|
@gen_test
|
||||||
def test_unsupported_features_raises_in_future(get_client, tmpdir):
|
def test_unsupported_features_raises_in_future(get_client, tmpdir, httpbin):
|
||||||
"""Ensure that the exception for an AsyncHTTPClient feature not being
|
"""Ensure that the exception for an AsyncHTTPClient feature not being
|
||||||
supported is raised inside the future."""
|
supported is raised inside the future."""
|
||||||
|
|
||||||
def callback(chunk):
|
def callback(chunk):
|
||||||
assert False, "Did not expect to be called."
|
raise AssertionError("Did not expect to be called.")
|
||||||
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("invalid.yaml"))):
|
with vcr.use_cassette(str(tmpdir.join("invalid.yaml"))):
|
||||||
future = get(get_client(), "http://httpbin.org", streaming_callback=callback)
|
future = get(get_client(), httpbin.url, streaming_callback=callback)
|
||||||
|
|
||||||
with pytest.raises(Exception) as excinfo:
|
with pytest.raises(Exception) as excinfo:
|
||||||
yield future
|
yield future
|
||||||
@@ -239,60 +275,76 @@ def test_unsupported_features_raises_in_future(get_client, tmpdir):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(not supports_raise_error, reason="raise_error unavailable in tornado <= 3")
|
@pytest.mark.skipif(not supports_raise_error, reason="raise_error unavailable in tornado <= 3")
|
||||||
@pytest.mark.gen_test
|
@pytest.mark.skipif(
|
||||||
|
raise_error_for_response_code_only,
|
||||||
|
reason="raise_error only ignores HTTPErrors due to response code",
|
||||||
|
)
|
||||||
|
@gen_test
|
||||||
def test_unsupported_features_raise_error_disabled(get_client, tmpdir):
|
def test_unsupported_features_raise_error_disabled(get_client, tmpdir):
|
||||||
"""Ensure that the exception for an AsyncHTTPClient feature not being
|
"""Ensure that the exception for an AsyncHTTPClient feature not being
|
||||||
supported is not raised if raise_error=False."""
|
supported is not raised if raise_error=False."""
|
||||||
|
|
||||||
def callback(chunk):
|
def callback(chunk):
|
||||||
assert False, "Did not expect to be called."
|
raise AssertionError("Did not expect to be called.")
|
||||||
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("invalid.yaml"))):
|
with vcr.use_cassette(str(tmpdir.join("invalid.yaml"))):
|
||||||
response = yield get(
|
response = yield get(
|
||||||
get_client(), "http://httpbin.org", streaming_callback=callback, raise_error=False
|
get_client(),
|
||||||
|
"http://httpbin.org",
|
||||||
|
streaming_callback=callback,
|
||||||
|
raise_error=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert "not yet supported by VCR" in str(response.error)
|
assert "not yet supported by VCR" in str(response.error)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gen_test
|
@pytest.mark.online
|
||||||
def test_cannot_overwrite_cassette_raises_in_future(get_client, tmpdir):
|
@gen_test
|
||||||
|
def test_cannot_overwrite_cassette_raises_in_future(get_client, tmpdir, httpbin):
|
||||||
"""Ensure that CannotOverwriteExistingCassetteException is raised inside
|
"""Ensure that CannotOverwriteExistingCassetteException is raised inside
|
||||||
the future."""
|
the future."""
|
||||||
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("overwrite.yaml"))):
|
url = httpbin.url
|
||||||
yield get(get_client(), "http://httpbin.org/get")
|
|
||||||
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("overwrite.yaml"))):
|
with vcr.use_cassette(str(tmpdir.join("overwrite.yaml"))):
|
||||||
future = get(get_client(), "http://httpbin.org/headers")
|
yield get(get_client(), url + "/get")
|
||||||
|
|
||||||
|
with vcr.use_cassette(str(tmpdir.join("overwrite.yaml"))):
|
||||||
|
future = get(get_client(), url + "/headers")
|
||||||
|
|
||||||
with pytest.raises(CannotOverwriteExistingCassetteException):
|
with pytest.raises(CannotOverwriteExistingCassetteException):
|
||||||
yield future
|
yield future
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(not supports_raise_error, reason="raise_error unavailable in tornado <= 3")
|
@pytest.mark.skipif(not supports_raise_error, reason="raise_error unavailable in tornado <= 3")
|
||||||
@pytest.mark.gen_test
|
@pytest.mark.skipif(
|
||||||
def test_cannot_overwrite_cassette_raise_error_disabled(get_client, tmpdir):
|
raise_error_for_response_code_only,
|
||||||
|
reason="raise_error only ignores HTTPErrors due to response code",
|
||||||
|
)
|
||||||
|
@gen_test
|
||||||
|
def test_cannot_overwrite_cassette_raise_error_disabled(get_client, tmpdir, httpbin):
|
||||||
"""Ensure that CannotOverwriteExistingCassetteException is not raised if
|
"""Ensure that CannotOverwriteExistingCassetteException is not raised if
|
||||||
raise_error=False in the fetch() call."""
|
raise_error=False in the fetch() call."""
|
||||||
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("overwrite.yaml"))):
|
url = httpbin.url
|
||||||
yield get(get_client(), "http://httpbin.org/get", raise_error=False)
|
|
||||||
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("overwrite.yaml"))):
|
with vcr.use_cassette(str(tmpdir.join("overwrite.yaml"))):
|
||||||
response = yield get(get_client(), "http://httpbin.org/headers", raise_error=False)
|
yield get(get_client(), url + "/get", raise_error=False)
|
||||||
|
|
||||||
|
with vcr.use_cassette(str(tmpdir.join("overwrite.yaml"))):
|
||||||
|
response = yield get(get_client(), url + "/headers", raise_error=False)
|
||||||
|
|
||||||
assert isinstance(response.error, CannotOverwriteExistingCassetteException)
|
assert isinstance(response.error, CannotOverwriteExistingCassetteException)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gen_test
|
@gen_test
|
||||||
@vcr.use_cassette(path_transformer=vcr.default_vcr.ensure_suffix(".yaml"))
|
@vcr.use_cassette(path_transformer=vcr.default_vcr.ensure_suffix(".yaml"))
|
||||||
def test_tornado_with_decorator_use_cassette(get_client):
|
def test_tornado_with_decorator_use_cassette(get_client):
|
||||||
response = yield get_client().fetch(http.HTTPRequest("http://www.google.com/", method="GET"))
|
response = yield get_client().fetch(http.HTTPRequest("http://www.google.com/", method="GET"))
|
||||||
assert response.body.decode("utf-8") == "not actually google"
|
assert response.body.decode("utf-8") == "not actually google"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gen_test
|
@gen_test
|
||||||
@vcr.use_cassette(path_transformer=vcr.default_vcr.ensure_suffix(".yaml"))
|
@vcr.use_cassette(path_transformer=vcr.default_vcr.ensure_suffix(".yaml"))
|
||||||
def test_tornado_exception_can_be_caught(get_client):
|
def test_tornado_exception_can_be_caught(get_client):
|
||||||
try:
|
try:
|
||||||
@@ -306,45 +358,53 @@ def test_tornado_exception_can_be_caught(get_client):
|
|||||||
assert e.code == 404
|
assert e.code == 404
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gen_test
|
@pytest.mark.online
|
||||||
def test_existing_references_get_patched(tmpdir):
|
@gen_test
|
||||||
|
def test_existing_references_get_patched(tmpdir, httpbin):
|
||||||
from tornado.httpclient import AsyncHTTPClient
|
from tornado.httpclient import AsyncHTTPClient
|
||||||
|
|
||||||
|
url = httpbin.url + "/get"
|
||||||
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("data.yaml"))):
|
with vcr.use_cassette(str(tmpdir.join("data.yaml"))):
|
||||||
client = AsyncHTTPClient()
|
client = AsyncHTTPClient()
|
||||||
yield get(client, "http://httpbin.org/get")
|
yield get(client, url)
|
||||||
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("data.yaml"))) as cass:
|
with vcr.use_cassette(str(tmpdir.join("data.yaml"))) as cass:
|
||||||
yield get(client, "http://httpbin.org/get")
|
yield get(client, url)
|
||||||
assert cass.play_count == 1
|
assert cass.play_count == 1
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gen_test
|
@pytest.mark.online
|
||||||
def test_existing_instances_get_patched(get_client, tmpdir):
|
@gen_test
|
||||||
|
def test_existing_instances_get_patched(get_client, tmpdir, httpbin):
|
||||||
"""Ensure that existing instances of AsyncHTTPClient get patched upon
|
"""Ensure that existing instances of AsyncHTTPClient get patched upon
|
||||||
entering VCR context."""
|
entering VCR context."""
|
||||||
|
|
||||||
|
url = httpbin.url + "/get"
|
||||||
client = get_client()
|
client = get_client()
|
||||||
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("data.yaml"))):
|
with vcr.use_cassette(str(tmpdir.join("data.yaml"))):
|
||||||
yield get(client, "http://httpbin.org/get")
|
yield get(client, url)
|
||||||
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("data.yaml"))) as cass:
|
with vcr.use_cassette(str(tmpdir.join("data.yaml"))) as cass:
|
||||||
yield get(client, "http://httpbin.org/get")
|
yield get(client, url)
|
||||||
assert cass.play_count == 1
|
assert cass.play_count == 1
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gen_test
|
@pytest.mark.online
|
||||||
def test_request_time_is_set(get_client, tmpdir):
|
@gen_test
|
||||||
|
def test_request_time_is_set(get_client, tmpdir, httpbin):
|
||||||
"""Ensures that the request_time on HTTPResponses is set."""
|
"""Ensures that the request_time on HTTPResponses is set."""
|
||||||
|
|
||||||
|
url = httpbin.url + "/get"
|
||||||
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("data.yaml"))):
|
with vcr.use_cassette(str(tmpdir.join("data.yaml"))):
|
||||||
client = get_client()
|
client = get_client()
|
||||||
response = yield get(client, "http://httpbin.org/get")
|
response = yield get(client, url)
|
||||||
assert response.request_time is not None
|
assert response.request_time is not None
|
||||||
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("data.yaml"))) as cass:
|
with vcr.use_cassette(str(tmpdir.join("data.yaml"))) as cass:
|
||||||
client = get_client()
|
client = get_client()
|
||||||
response = yield get(client, "http://httpbin.org/get")
|
response = yield get(client, url)
|
||||||
assert response.request_time is not None
|
assert response.request_time is not None
|
||||||
assert cass.play_count == 1
|
assert cass.play_count == 1
|
||||||
|
|||||||
@@ -1,144 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""Integration tests with urllib2"""
|
|
||||||
|
|
||||||
import ssl
|
|
||||||
from urllib.request import urlopen
|
|
||||||
from urllib.parse import urlencode
|
|
||||||
import pytest_httpbin.certs
|
|
||||||
|
|
||||||
# Internal imports
|
|
||||||
import vcr
|
|
||||||
|
|
||||||
from assertions import assert_cassette_has_one_response
|
|
||||||
|
|
||||||
|
|
||||||
def urlopen_with_cafile(*args, **kwargs):
|
|
||||||
context = ssl.create_default_context(cafile=pytest_httpbin.certs.where())
|
|
||||||
context.check_hostname = False
|
|
||||||
kwargs["context"] = context
|
|
||||||
try:
|
|
||||||
return urlopen(*args, **kwargs)
|
|
||||||
except TypeError:
|
|
||||||
# python2/pypi don't let us override this
|
|
||||||
del kwargs["cafile"]
|
|
||||||
return urlopen(*args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
def test_response_code(httpbin_both, tmpdir):
|
|
||||||
"""Ensure we can read a response code from a fetch"""
|
|
||||||
url = httpbin_both.url
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("atts.yaml"))):
|
|
||||||
code = urlopen_with_cafile(url).getcode()
|
|
||||||
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("atts.yaml"))):
|
|
||||||
assert code == urlopen_with_cafile(url).getcode()
|
|
||||||
|
|
||||||
|
|
||||||
def test_random_body(httpbin_both, tmpdir):
|
|
||||||
"""Ensure we can read the content, and that it's served from cache"""
|
|
||||||
url = httpbin_both.url + "/bytes/1024"
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("body.yaml"))):
|
|
||||||
body = urlopen_with_cafile(url).read()
|
|
||||||
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("body.yaml"))):
|
|
||||||
assert body == urlopen_with_cafile(url).read()
|
|
||||||
|
|
||||||
|
|
||||||
def test_response_headers(httpbin_both, tmpdir):
|
|
||||||
"""Ensure we can get information from the response"""
|
|
||||||
url = httpbin_both.url
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("headers.yaml"))):
|
|
||||||
open1 = urlopen_with_cafile(url).info().items()
|
|
||||||
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("headers.yaml"))):
|
|
||||||
open2 = urlopen_with_cafile(url).info().items()
|
|
||||||
|
|
||||||
assert sorted(open1) == sorted(open2)
|
|
||||||
|
|
||||||
|
|
||||||
def test_effective_url(httpbin_both, tmpdir):
|
|
||||||
"""Ensure that the effective_url is captured"""
|
|
||||||
url = httpbin_both.url + "/redirect-to?url=/html"
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("headers.yaml"))):
|
|
||||||
effective_url = urlopen_with_cafile(url).geturl()
|
|
||||||
assert effective_url == httpbin_both.url + "/html"
|
|
||||||
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("headers.yaml"))):
|
|
||||||
assert effective_url == urlopen_with_cafile(url).geturl()
|
|
||||||
|
|
||||||
|
|
||||||
def test_multiple_requests(httpbin_both, tmpdir):
|
|
||||||
"""Ensure that we can cache multiple requests"""
|
|
||||||
urls = [httpbin_both.url, httpbin_both.url, httpbin_both.url + "/get", httpbin_both.url + "/bytes/1024"]
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("multiple.yaml"))) as cass:
|
|
||||||
[urlopen_with_cafile(url) for url in urls]
|
|
||||||
assert len(cass) == len(urls)
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_data(httpbin_both, tmpdir):
|
|
||||||
"""Ensure that it works with query data"""
|
|
||||||
data = urlencode({"some": 1, "data": "here"})
|
|
||||||
url = httpbin_both.url + "/get?" + data
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("get_data.yaml"))):
|
|
||||||
res1 = urlopen_with_cafile(url).read()
|
|
||||||
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("get_data.yaml"))):
|
|
||||||
res2 = urlopen_with_cafile(url).read()
|
|
||||||
assert res1 == res2
|
|
||||||
|
|
||||||
|
|
||||||
def test_post_data(httpbin_both, tmpdir):
|
|
||||||
"""Ensure that it works when posting data"""
|
|
||||||
data = urlencode({"some": 1, "data": "here"}).encode("utf-8")
|
|
||||||
url = httpbin_both.url + "/post"
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("post_data.yaml"))):
|
|
||||||
res1 = urlopen_with_cafile(url, data).read()
|
|
||||||
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("post_data.yaml"))) as cass:
|
|
||||||
res2 = urlopen_with_cafile(url, data).read()
|
|
||||||
assert len(cass) == 1
|
|
||||||
|
|
||||||
assert res1 == res2
|
|
||||||
assert_cassette_has_one_response(cass)
|
|
||||||
|
|
||||||
|
|
||||||
def test_post_unicode_data(httpbin_both, tmpdir):
|
|
||||||
"""Ensure that it works when posting unicode data"""
|
|
||||||
data = urlencode({"snowman": "☃".encode()}).encode("utf-8")
|
|
||||||
url = httpbin_both.url + "/post"
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("post_data.yaml"))):
|
|
||||||
res1 = urlopen_with_cafile(url, data).read()
|
|
||||||
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("post_data.yaml"))) as cass:
|
|
||||||
res2 = urlopen_with_cafile(url, data).read()
|
|
||||||
assert len(cass) == 1
|
|
||||||
|
|
||||||
assert res1 == res2
|
|
||||||
assert_cassette_has_one_response(cass)
|
|
||||||
|
|
||||||
|
|
||||||
def test_cross_scheme(tmpdir, httpbin_secure, httpbin):
|
|
||||||
"""Ensure that requests between schemes are treated separately"""
|
|
||||||
# First fetch a url under https, and then again under https and then
|
|
||||||
# ensure that we haven't served anything out of cache, and we have two
|
|
||||||
# requests / response pairs in the cassette
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("cross_scheme.yaml"))) as cass:
|
|
||||||
urlopen_with_cafile(httpbin_secure.url)
|
|
||||||
urlopen_with_cafile(httpbin.url)
|
|
||||||
assert len(cass) == 2
|
|
||||||
assert cass.play_count == 0
|
|
||||||
|
|
||||||
|
|
||||||
def test_decorator(httpbin_both, tmpdir):
|
|
||||||
"""Test the decorator version of VCR.py"""
|
|
||||||
url = httpbin_both.url
|
|
||||||
|
|
||||||
@vcr.use_cassette(str(tmpdir.join("atts.yaml")))
|
|
||||||
def inner1():
|
|
||||||
return urlopen_with_cafile(url).getcode()
|
|
||||||
|
|
||||||
@vcr.use_cassette(str(tmpdir.join("atts.yaml")))
|
|
||||||
def inner2():
|
|
||||||
return urlopen_with_cafile(url).getcode()
|
|
||||||
|
|
||||||
assert inner1() == inner2()
|
|
||||||
@@ -4,9 +4,12 @@
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import pytest_httpbin
|
import pytest_httpbin
|
||||||
|
|
||||||
import vcr
|
import vcr
|
||||||
from vcr.patch import force_reset
|
from vcr.patch import force_reset
|
||||||
from assertions import assert_cassette_empty, assert_is_json
|
from vcr.stubs.compat import get_headers
|
||||||
|
|
||||||
|
from ..assertions import assert_cassette_empty, assert_is_json_bytes
|
||||||
|
|
||||||
urllib3 = pytest.importorskip("urllib3")
|
urllib3 = pytest.importorskip("urllib3")
|
||||||
|
|
||||||
@@ -14,7 +17,8 @@ urllib3 = pytest.importorskip("urllib3")
|
|||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="module")
|
||||||
def verify_pool_mgr():
|
def verify_pool_mgr():
|
||||||
return urllib3.PoolManager(
|
return urllib3.PoolManager(
|
||||||
cert_reqs="CERT_REQUIRED", ca_certs=pytest_httpbin.certs.where() # Force certificate check.
|
cert_reqs="CERT_REQUIRED",
|
||||||
|
ca_certs=pytest_httpbin.certs.where(), # Force certificate check.
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -40,7 +44,8 @@ def test_headers(tmpdir, httpbin_both, verify_pool_mgr):
|
|||||||
headers = verify_pool_mgr.request("GET", url).headers
|
headers = verify_pool_mgr.request("GET", url).headers
|
||||||
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("headers.yaml"))):
|
with vcr.use_cassette(str(tmpdir.join("headers.yaml"))):
|
||||||
assert headers == verify_pool_mgr.request("GET", url).headers
|
new_headers = verify_pool_mgr.request("GET", url).headers
|
||||||
|
assert sorted(get_headers(headers)) == sorted(get_headers(new_headers))
|
||||||
|
|
||||||
|
|
||||||
def test_body(tmpdir, httpbin_both, verify_pool_mgr):
|
def test_body(tmpdir, httpbin_both, verify_pool_mgr):
|
||||||
@@ -94,9 +99,11 @@ def test_post(tmpdir, httpbin_both, verify_pool_mgr):
|
|||||||
assert req1 == req2
|
assert req1 == req2
|
||||||
|
|
||||||
|
|
||||||
def test_redirects(tmpdir, httpbin_both, verify_pool_mgr):
|
@pytest.mark.online
|
||||||
|
def test_redirects(tmpdir, verify_pool_mgr, httpbin):
|
||||||
"""Ensure that we can handle redirects"""
|
"""Ensure that we can handle redirects"""
|
||||||
url = httpbin_both.url + "/redirect-to?url=bytes/1024"
|
url = httpbin.url + "/redirect/1"
|
||||||
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("verify_pool_mgr.yaml"))):
|
with vcr.use_cassette(str(tmpdir.join("verify_pool_mgr.yaml"))):
|
||||||
content = verify_pool_mgr.request("GET", url).data
|
content = verify_pool_mgr.request("GET", url).data
|
||||||
|
|
||||||
@@ -104,8 +111,9 @@ def test_redirects(tmpdir, httpbin_both, verify_pool_mgr):
|
|||||||
assert content == verify_pool_mgr.request("GET", url).data
|
assert content == verify_pool_mgr.request("GET", url).data
|
||||||
# Ensure that we've now cached *two* responses. One for the redirect
|
# Ensure that we've now cached *two* responses. One for the redirect
|
||||||
# and one for the final fetch
|
# and one for the final fetch
|
||||||
assert len(cass) == 2
|
|
||||||
assert cass.play_count == 2
|
assert len(cass) == 2
|
||||||
|
assert cass.play_count == 2
|
||||||
|
|
||||||
|
|
||||||
def test_cross_scheme(tmpdir, httpbin, httpbin_secure, verify_pool_mgr):
|
def test_cross_scheme(tmpdir, httpbin, httpbin_secure, verify_pool_mgr):
|
||||||
@@ -130,10 +138,10 @@ def test_gzip(tmpdir, httpbin_both, verify_pool_mgr):
|
|||||||
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("gzip.yaml"))):
|
with vcr.use_cassette(str(tmpdir.join("gzip.yaml"))):
|
||||||
response = verify_pool_mgr.request("GET", url)
|
response = verify_pool_mgr.request("GET", url)
|
||||||
assert_is_json(response.data)
|
assert_is_json_bytes(response.data)
|
||||||
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("gzip.yaml"))):
|
with vcr.use_cassette(str(tmpdir.join("gzip.yaml"))):
|
||||||
assert_is_json(response.data)
|
assert_is_json_bytes(response.data)
|
||||||
|
|
||||||
|
|
||||||
def test_https_with_cert_validation_disabled(tmpdir, httpbin_secure, pool_mgr):
|
def test_https_with_cert_validation_disabled(tmpdir, httpbin_secure, pool_mgr):
|
||||||
@@ -142,18 +150,18 @@ def test_https_with_cert_validation_disabled(tmpdir, httpbin_secure, pool_mgr):
|
|||||||
|
|
||||||
|
|
||||||
def test_urllib3_force_reset():
|
def test_urllib3_force_reset():
|
||||||
cpool = urllib3.connectionpool
|
conn = urllib3.connection
|
||||||
http_original = cpool.HTTPConnection
|
http_original = conn.HTTPConnection
|
||||||
https_original = cpool.HTTPSConnection
|
https_original = conn.HTTPSConnection
|
||||||
verified_https_original = cpool.VerifiedHTTPSConnection
|
verified_https_original = conn.VerifiedHTTPSConnection
|
||||||
with vcr.use_cassette(path="test"):
|
with vcr.use_cassette(path="test"):
|
||||||
first_cassette_HTTPConnection = cpool.HTTPConnection
|
first_cassette_HTTPConnection = conn.HTTPConnection
|
||||||
first_cassette_HTTPSConnection = cpool.HTTPSConnection
|
first_cassette_HTTPSConnection = conn.HTTPSConnection
|
||||||
first_cassette_VerifiedHTTPSConnection = cpool.VerifiedHTTPSConnection
|
first_cassette_VerifiedHTTPSConnection = conn.VerifiedHTTPSConnection
|
||||||
with force_reset():
|
with force_reset():
|
||||||
assert cpool.HTTPConnection is http_original
|
assert conn.HTTPConnection is http_original
|
||||||
assert cpool.HTTPSConnection is https_original
|
assert conn.HTTPSConnection is https_original
|
||||||
assert cpool.VerifiedHTTPSConnection is verified_https_original
|
assert conn.VerifiedHTTPSConnection is verified_https_original
|
||||||
assert cpool.HTTPConnection is first_cassette_HTTPConnection
|
assert conn.HTTPConnection is first_cassette_HTTPConnection
|
||||||
assert cpool.HTTPSConnection is first_cassette_HTTPSConnection
|
assert conn.HTTPSConnection is first_cassette_HTTPSConnection
|
||||||
assert cpool.VerifiedHTTPSConnection is first_cassette_VerifiedHTTPSConnection
|
assert conn.VerifiedHTTPSConnection is first_cassette_VerifiedHTTPSConnection
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import http.client as httplib
|
import http.client as httplib
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
import pytest
|
|
||||||
from xmlrpc.client import ServerProxy
|
from xmlrpc.client import ServerProxy
|
||||||
from xmlrpc.server import SimpleXMLRPCServer
|
from xmlrpc.server import SimpleXMLRPCServer
|
||||||
|
|
||||||
requests = pytest.importorskip("requests")
|
import pytest
|
||||||
|
|
||||||
import vcr # NOQA
|
import vcr
|
||||||
|
|
||||||
|
requests = pytest.importorskip("requests")
|
||||||
|
|
||||||
|
|
||||||
def test_domain_redirect():
|
def test_domain_redirect():
|
||||||
@@ -51,6 +52,7 @@ def test_flickr_multipart_upload(httpbin, tmpdir):
|
|||||||
assert cass.play_count == 1
|
assert cass.play_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.online
|
||||||
def test_flickr_should_respond_with_200(tmpdir):
|
def test_flickr_should_respond_with_200(tmpdir):
|
||||||
testfile = str(tmpdir.join("flickr.yml"))
|
testfile = str(tmpdir.join("flickr.yml"))
|
||||||
with vcr.use_cassette(testfile):
|
with vcr.use_cassette(testfile):
|
||||||
@@ -60,14 +62,15 @@ def test_flickr_should_respond_with_200(tmpdir):
|
|||||||
|
|
||||||
def test_cookies(tmpdir, httpbin):
|
def test_cookies(tmpdir, httpbin):
|
||||||
testfile = str(tmpdir.join("cookies.yml"))
|
testfile = str(tmpdir.join("cookies.yml"))
|
||||||
with vcr.use_cassette(testfile):
|
with vcr.use_cassette(testfile), requests.Session() as s:
|
||||||
s = requests.Session()
|
|
||||||
s.get(httpbin.url + "/cookies/set?k1=v1&k2=v2")
|
s.get(httpbin.url + "/cookies/set?k1=v1&k2=v2")
|
||||||
|
assert s.cookies.keys() == ["k1", "k2"]
|
||||||
|
|
||||||
r2 = s.get(httpbin.url + "/cookies")
|
r2 = s.get(httpbin.url + "/cookies")
|
||||||
assert len(r2.json()["cookies"]) == 2
|
assert sorted(r2.json()["cookies"].keys()) == ["k1", "k2"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.online
|
||||||
def test_amazon_doctype(tmpdir):
|
def test_amazon_doctype(tmpdir):
|
||||||
# amazon gzips its homepage. For some reason, in requests 2.7, it's not
|
# amazon gzips its homepage. For some reason, in requests 2.7, it's not
|
||||||
# getting gunzipped.
|
# getting gunzipped.
|
||||||
@@ -83,7 +86,7 @@ def start_rpc_server(q):
|
|||||||
httpd.serve_forever()
|
httpd.serve_forever()
|
||||||
|
|
||||||
|
|
||||||
@pytest.yield_fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def rpc_server():
|
def rpc_server():
|
||||||
q = multiprocessing.Queue()
|
q = multiprocessing.Queue()
|
||||||
proxy_process = multiprocessing.Process(target=start_rpc_server, args=(q,))
|
proxy_process = multiprocessing.Process(target=start_rpc_server, args=(q,))
|
||||||
|
|||||||
@@ -7,9 +7,11 @@ from unittest import mock
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
from vcr.cassette import Cassette
|
from vcr.cassette import Cassette
|
||||||
from vcr.errors import UnhandledHTTPRequestError
|
from vcr.errors import UnhandledHTTPRequestError
|
||||||
from vcr.patch import force_reset
|
from vcr.patch import force_reset
|
||||||
|
from vcr.request import Request
|
||||||
from vcr.stubs import VCRHTTPSConnection
|
from vcr.stubs import VCRHTTPSConnection
|
||||||
|
|
||||||
|
|
||||||
@@ -19,15 +21,31 @@ def test_cassette_load(tmpdir):
|
|||||||
yaml.dump(
|
yaml.dump(
|
||||||
{
|
{
|
||||||
"interactions": [
|
"interactions": [
|
||||||
{"request": {"body": "", "uri": "foo", "method": "GET", "headers": {}}, "response": "bar"}
|
{
|
||||||
]
|
"request": {"body": "", "uri": "foo", "method": "GET", "headers": {}},
|
||||||
}
|
"response": "bar",
|
||||||
)
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
),
|
||||||
)
|
)
|
||||||
a_cassette = Cassette.load(path=str(a_file))
|
a_cassette = Cassette.load(path=str(a_file))
|
||||||
assert len(a_cassette) == 1
|
assert len(a_cassette) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_cassette_load_nonexistent():
|
||||||
|
a_cassette = Cassette.load(path="something/nonexistent.yml")
|
||||||
|
assert len(a_cassette) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_cassette_load_invalid_encoding(tmpdir):
|
||||||
|
a_file = tmpdir.join("invalid_encoding.yml")
|
||||||
|
with open(a_file, "wb") as fd:
|
||||||
|
fd.write(b"\xda")
|
||||||
|
a_cassette = Cassette.load(path=str(a_file))
|
||||||
|
assert len(a_cassette) == 0
|
||||||
|
|
||||||
|
|
||||||
def test_cassette_not_played():
|
def test_cassette_not_played():
|
||||||
a = Cassette("test")
|
a = Cassette("test")
|
||||||
assert not a.play_count
|
assert not a.play_count
|
||||||
@@ -96,7 +114,7 @@ def make_get_request():
|
|||||||
@mock.patch("vcr.stubs.VCRHTTPResponse")
|
@mock.patch("vcr.stubs.VCRHTTPResponse")
|
||||||
def test_function_decorated_with_use_cassette_can_be_invoked_multiple_times(*args):
|
def test_function_decorated_with_use_cassette_can_be_invoked_multiple_times(*args):
|
||||||
decorated_function = Cassette.use(path="test")(make_get_request)
|
decorated_function = Cassette.use(path="test")(make_get_request)
|
||||||
for i in range(4):
|
for _ in range(4):
|
||||||
decorated_function()
|
decorated_function()
|
||||||
|
|
||||||
|
|
||||||
@@ -142,7 +160,7 @@ def test_cassette_allow_playback_repeats():
|
|||||||
a = Cassette("test", allow_playback_repeats=True)
|
a = Cassette("test", allow_playback_repeats=True)
|
||||||
a.append("foo", "bar")
|
a.append("foo", "bar")
|
||||||
a.append("other", "resp")
|
a.append("other", "resp")
|
||||||
for x in range(10):
|
for _ in range(10):
|
||||||
assert a.play_response("foo") == "bar"
|
assert a.play_response("foo") == "bar"
|
||||||
assert a.play_count == 10
|
assert a.play_count == 10
|
||||||
assert a.all_played is False
|
assert a.all_played is False
|
||||||
@@ -204,14 +222,16 @@ def test_nesting_cassette_context_managers(*args):
|
|||||||
with contextlib.ExitStack() as exit_stack:
|
with contextlib.ExitStack() as exit_stack:
|
||||||
first_cassette = exit_stack.enter_context(Cassette.use(path="test"))
|
first_cassette = exit_stack.enter_context(Cassette.use(path="test"))
|
||||||
exit_stack.enter_context(
|
exit_stack.enter_context(
|
||||||
mock.patch.object(first_cassette, "play_response", return_value=first_response)
|
mock.patch.object(first_cassette, "play_response", return_value=first_response),
|
||||||
)
|
)
|
||||||
assert_get_response_body_is("first_response")
|
assert_get_response_body_is("first_response")
|
||||||
|
|
||||||
# Make sure a second cassette can supercede the first
|
# Make sure a second cassette can supersede the first
|
||||||
with Cassette.use(path="test") as second_cassette:
|
with (
|
||||||
with mock.patch.object(second_cassette, "play_response", return_value=second_response):
|
Cassette.use(path="test") as second_cassette,
|
||||||
assert_get_response_body_is("second_response")
|
mock.patch.object(second_cassette, "play_response", return_value=second_response),
|
||||||
|
):
|
||||||
|
assert_get_response_body_is("second_response")
|
||||||
|
|
||||||
# Now the first cassette should be back in effect
|
# Now the first cassette should be back in effect
|
||||||
assert_get_response_body_is("first_response")
|
assert_get_response_body_is("first_response")
|
||||||
@@ -310,16 +330,16 @@ def test_func_path_generator():
|
|||||||
|
|
||||||
|
|
||||||
def test_use_as_decorator_on_coroutine():
|
def test_use_as_decorator_on_coroutine():
|
||||||
original_http_connetion = httplib.HTTPConnection
|
original_http_connection = httplib.HTTPConnection
|
||||||
|
|
||||||
@Cassette.use(inject=True)
|
@Cassette.use(inject=True)
|
||||||
def test_function(cassette):
|
def test_function(cassette):
|
||||||
assert httplib.HTTPConnection.cassette is 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
|
value = yield 1
|
||||||
assert value == 1
|
assert value == 1
|
||||||
assert httplib.HTTPConnection.cassette is 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 2
|
value = yield 2
|
||||||
assert value == 2
|
assert value == 2
|
||||||
|
|
||||||
@@ -333,15 +353,15 @@ def test_use_as_decorator_on_coroutine():
|
|||||||
|
|
||||||
|
|
||||||
def test_use_as_decorator_on_generator():
|
def test_use_as_decorator_on_generator():
|
||||||
original_http_connetion = httplib.HTTPConnection
|
original_http_connection = httplib.HTTPConnection
|
||||||
|
|
||||||
@Cassette.use(inject=True)
|
@Cassette.use(inject=True)
|
||||||
def test_function(cassette):
|
def test_function(cassette):
|
||||||
assert httplib.HTTPConnection.cassette is 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
|
yield 1
|
||||||
assert httplib.HTTPConnection.cassette is cassette
|
assert httplib.HTTPConnection.cassette is cassette
|
||||||
assert httplib.HTTPConnection is not original_http_connetion
|
assert httplib.HTTPConnection is not original_http_connection
|
||||||
yield 2
|
yield 2
|
||||||
|
|
||||||
assert list(test_function()) == [1, 2]
|
assert list(test_function()) == [1, 2]
|
||||||
@@ -393,3 +413,25 @@ def test_find_requests_with_most_matches_many_similar_requests(mock_get_matchers
|
|||||||
(1, ["method", "path"], [("query", "failed : query")]),
|
(1, ["method", "path"], [("query", "failed : query")]),
|
||||||
(3, ["method", "path"], [("query", "failed : query")]),
|
(3, ["method", "path"], [("query", "failed : query")]),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_used_interactions(tmpdir):
|
||||||
|
interactions = [
|
||||||
|
{"request": {"body": "", "uri": "foo1", "method": "GET", "headers": {}}, "response": "bar1"},
|
||||||
|
{"request": {"body": "", "uri": "foo2", "method": "GET", "headers": {}}, "response": "bar2"},
|
||||||
|
{"request": {"body": "", "uri": "foo3", "method": "GET", "headers": {}}, "response": "bar3"},
|
||||||
|
]
|
||||||
|
file = tmpdir.join("test_cassette.yml")
|
||||||
|
file.write(yaml.dump({"interactions": [interactions[0], interactions[1]]}))
|
||||||
|
|
||||||
|
cassette = Cassette.load(path=str(file))
|
||||||
|
request = Request._from_dict(interactions[1]["request"])
|
||||||
|
cassette.play_response(request)
|
||||||
|
assert len(cassette._played_interactions) < len(cassette._old_interactions)
|
||||||
|
|
||||||
|
request = Request._from_dict(interactions[2]["request"])
|
||||||
|
cassette.append(request, interactions[2]["response"])
|
||||||
|
assert len(cassette._new_interactions()) == 1
|
||||||
|
|
||||||
|
used_interactions = cassette._played_interactions + cassette._new_interactions()
|
||||||
|
assert len(used_interactions) == 2
|
||||||
|
|||||||
@@ -55,15 +55,18 @@ from vcr.cassette import Cassette
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_CannotOverwriteExistingCassetteException_get_message(
|
def test_CannotOverwriteExistingCassetteException_get_message(
|
||||||
mock_find_requests_with_most_matches, most_matches, expected_message
|
mock_find_requests_with_most_matches,
|
||||||
|
most_matches,
|
||||||
|
expected_message,
|
||||||
):
|
):
|
||||||
mock_find_requests_with_most_matches.return_value = most_matches
|
mock_find_requests_with_most_matches.return_value = most_matches
|
||||||
cassette = Cassette("path")
|
cassette = Cassette("path")
|
||||||
failed_request = "request"
|
failed_request = "request"
|
||||||
exception_message = errors.CannotOverwriteExistingCassetteException._get_message(cassette, "request")
|
exception_message = errors.CannotOverwriteExistingCassetteException._get_message(cassette, "request")
|
||||||
expected = (
|
expected = (
|
||||||
"Can't overwrite existing cassette (%r) in your current record mode (%r).\n"
|
f"Can't overwrite existing cassette ({cassette._path!r}) "
|
||||||
"No match for the request (%r) was found.\n"
|
f"in your current record mode ({cassette.record_mode!r}).\n"
|
||||||
"%s" % (cassette._path, cassette.record_mode, failed_request, expected_message)
|
f"No match for the request ({failed_request!r}) was found.\n"
|
||||||
|
f"{expected_message}"
|
||||||
)
|
)
|
||||||
assert exception_message == expected
|
assert exception_message == expected
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
from io import BytesIO
|
|
||||||
from vcr.filters import (
|
|
||||||
remove_headers,
|
|
||||||
replace_headers,
|
|
||||||
remove_query_parameters,
|
|
||||||
replace_query_parameters,
|
|
||||||
remove_post_data_parameters,
|
|
||||||
replace_post_data_parameters,
|
|
||||||
decode_response,
|
|
||||||
)
|
|
||||||
from vcr.request import Request
|
|
||||||
import gzip
|
import gzip
|
||||||
import json
|
import json
|
||||||
from unittest import mock
|
|
||||||
import zlib
|
import zlib
|
||||||
|
from io import BytesIO
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from vcr.filters import (
|
||||||
|
decode_response,
|
||||||
|
remove_headers,
|
||||||
|
remove_post_data_parameters,
|
||||||
|
remove_query_parameters,
|
||||||
|
replace_headers,
|
||||||
|
replace_post_data_parameters,
|
||||||
|
replace_query_parameters,
|
||||||
|
)
|
||||||
|
from vcr.request import Request
|
||||||
|
|
||||||
|
|
||||||
def test_replace_headers():
|
def test_replace_headers():
|
||||||
@@ -196,7 +197,7 @@ def test_replace_json_post_data_parameters():
|
|||||||
("six", "doesntexist"),
|
("six", "doesntexist"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
request_data = json.loads(request.body.decode("utf-8"))
|
request_data = json.loads(request.body)
|
||||||
expected_data = json.loads('{"one": "keep", "three": "tada", "four": "SHOUT"}')
|
expected_data = json.loads('{"one": "keep", "three": "tada", "four": "SHOUT"}')
|
||||||
assert request_data == expected_data
|
assert request_data == expected_data
|
||||||
|
|
||||||
@@ -207,8 +208,8 @@ def test_remove_json_post_data_parameters():
|
|||||||
request = Request("POST", "http://google.com", body, {})
|
request = Request("POST", "http://google.com", body, {})
|
||||||
request.headers["Content-Type"] = "application/json"
|
request.headers["Content-Type"] = "application/json"
|
||||||
remove_post_data_parameters(request, ["id"])
|
remove_post_data_parameters(request, ["id"])
|
||||||
request_body_json = json.loads(request.body.decode("utf-8"))
|
request_body_json = json.loads(request.body)
|
||||||
expected_json = json.loads(b'{"foo": "bar", "baz": "qux"}'.decode("utf-8"))
|
expected_json = json.loads(b'{"foo": "bar", "baz": "qux"}')
|
||||||
assert request_body_json == expected_json
|
assert request_body_json == expected_json
|
||||||
|
|
||||||
|
|
||||||
@@ -220,6 +221,49 @@ def test_remove_all_json_post_data_parameters():
|
|||||||
assert request.body == b"{}"
|
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():
|
def test_decode_response_uncompressed():
|
||||||
recorded_response = {
|
recorded_response = {
|
||||||
"status": {"message": "OK", "code": 200},
|
"status": {"message": "OK", "code": 200},
|
||||||
@@ -254,6 +298,18 @@ def test_decode_response_deflate():
|
|||||||
assert decoded_response["headers"]["content-length"] == [str(len(body))]
|
assert decoded_response["headers"]["content-length"] == [str(len(body))]
|
||||||
|
|
||||||
|
|
||||||
|
def test_decode_response_deflate_already_decompressed():
|
||||||
|
body = b"deflate message"
|
||||||
|
gzip_response = {
|
||||||
|
"body": {"string": body},
|
||||||
|
"headers": {
|
||||||
|
"content-encoding": ["deflate"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
decoded_response = decode_response(gzip_response)
|
||||||
|
assert decoded_response["body"]["string"] == body
|
||||||
|
|
||||||
|
|
||||||
def test_decode_response_gzip():
|
def test_decode_response_gzip():
|
||||||
body = b"gzip message"
|
body = b"gzip message"
|
||||||
|
|
||||||
@@ -281,3 +337,15 @@ def test_decode_response_gzip():
|
|||||||
decoded_response = decode_response(gzip_response)
|
decoded_response = decode_response(gzip_response)
|
||||||
assert decoded_response["body"]["string"] == body
|
assert decoded_response["body"]["string"] == body
|
||||||
assert decoded_response["headers"]["content-length"] == [str(len(body))]
|
assert decoded_response["headers"]["content-length"] == [str(len(body))]
|
||||||
|
|
||||||
|
|
||||||
|
def test_decode_response_gzip_already_decompressed():
|
||||||
|
body = b"gzip message"
|
||||||
|
gzip_response = {
|
||||||
|
"body": {"string": body},
|
||||||
|
"headers": {
|
||||||
|
"content-encoding": ["gzip"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
decoded_response = decode_response(gzip_response)
|
||||||
|
assert decoded_response["body"]["string"] == body
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from vcr.serializers.jsonserializer import serialize
|
|
||||||
from vcr.request import Request
|
from vcr.request import Request
|
||||||
|
from vcr.serializers.jsonserializer import serialize
|
||||||
|
|
||||||
|
|
||||||
def test_serialize_binary():
|
def test_serialize_binary():
|
||||||
|
|||||||
@@ -3,8 +3,7 @@ from unittest import mock
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from vcr import matchers
|
from vcr import matchers, request
|
||||||
from vcr import request
|
|
||||||
|
|
||||||
# the dict contains requests with corresponding to its key difference
|
# the dict contains requests with corresponding to its key difference
|
||||||
# with 'base' request.
|
# with 'base' request.
|
||||||
@@ -64,6 +63,9 @@ boto3_bytes_headers = {
|
|||||||
"Expect": b"100-continue",
|
"Expect": b"100-continue",
|
||||||
"Content-Length": "21",
|
"Content-Length": "21",
|
||||||
}
|
}
|
||||||
|
chunked_headers = {
|
||||||
|
"Transfer-Encoding": "chunked",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
@@ -75,10 +77,16 @@ boto3_bytes_headers = {
|
|||||||
),
|
),
|
||||||
(
|
(
|
||||||
request.Request(
|
request.Request(
|
||||||
"POST", "http://host.com/", "a=1&b=2", {"Content-Type": "application/x-www-form-urlencoded"}
|
"POST",
|
||||||
|
"http://host.com/",
|
||||||
|
"a=1&b=2",
|
||||||
|
{"Content-Type": "application/x-www-form-urlencoded"},
|
||||||
),
|
),
|
||||||
request.Request(
|
request.Request(
|
||||||
"POST", "http://host.com/", "b=2&a=1", {"Content-Type": "application/x-www-form-urlencoded"}
|
"POST",
|
||||||
|
"http://host.com/",
|
||||||
|
"b=2&a=1",
|
||||||
|
{"Content-Type": "application/x-www-form-urlencoded"},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
@@ -87,23 +95,38 @@ boto3_bytes_headers = {
|
|||||||
),
|
),
|
||||||
(
|
(
|
||||||
request.Request(
|
request.Request(
|
||||||
"POST", "http://host.com/", "a=1&b=2", {"Content-Type": "application/x-www-form-urlencoded"}
|
"POST",
|
||||||
|
"http://host.com/",
|
||||||
|
"a=1&b=2",
|
||||||
|
{"Content-Type": "application/x-www-form-urlencoded"},
|
||||||
),
|
),
|
||||||
request.Request(
|
request.Request(
|
||||||
"POST", "http://host.com/", "b=2&a=1", {"Content-Type": "application/x-www-form-urlencoded"}
|
"POST",
|
||||||
|
"http://host.com/",
|
||||||
|
"b=2&a=1",
|
||||||
|
{"Content-Type": "application/x-www-form-urlencoded"},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
request.Request(
|
request.Request(
|
||||||
"POST", "http://host.com/", '{"a": 1, "b": 2}', {"Content-Type": "application/json"}
|
"POST",
|
||||||
|
"http://host.com/",
|
||||||
|
'{"a": 1, "b": 2}',
|
||||||
|
{"Content-Type": "application/json"},
|
||||||
),
|
),
|
||||||
request.Request(
|
request.Request(
|
||||||
"POST", "http://host.com/", '{"b": 2, "a": 1}', {"content-type": "application/json"}
|
"POST",
|
||||||
|
"http://host.com/",
|
||||||
|
'{"b": 2, "a": 1}',
|
||||||
|
{"content-type": "application/json"},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
request.Request(
|
request.Request(
|
||||||
"POST", "http://host.com/", req1_body, {"User-Agent": "xmlrpclib", "Content-Type": "text/xml"}
|
"POST",
|
||||||
|
"http://host.com/",
|
||||||
|
req1_body,
|
||||||
|
{"User-Agent": "xmlrpclib", "Content-Type": "text/xml"},
|
||||||
),
|
),
|
||||||
request.Request(
|
request.Request(
|
||||||
"POST",
|
"POST",
|
||||||
@@ -114,10 +137,16 @@ boto3_bytes_headers = {
|
|||||||
),
|
),
|
||||||
(
|
(
|
||||||
request.Request(
|
request.Request(
|
||||||
"POST", "http://host.com/", '{"a": 1, "b": 2}', {"Content-Type": "application/json"}
|
"POST",
|
||||||
|
"http://host.com/",
|
||||||
|
'{"a": 1, "b": 2}',
|
||||||
|
{"Content-Type": "application/json"},
|
||||||
),
|
),
|
||||||
request.Request(
|
request.Request(
|
||||||
"POST", "http://host.com/", '{"b": 2, "a": 1}', {"content-type": "application/json"}
|
"POST",
|
||||||
|
"http://host.com/",
|
||||||
|
'{"b": 2, "a": 1}',
|
||||||
|
{"content-type": "application/json"},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
@@ -125,6 +154,36 @@ boto3_bytes_headers = {
|
|||||||
request.Request("POST", "http://aws.custom.com/", b"123", boto3_bytes_headers),
|
request.Request("POST", "http://aws.custom.com/", b"123", boto3_bytes_headers),
|
||||||
request.Request("POST", "http://aws.custom.com/", b"123", boto3_bytes_headers),
|
request.Request("POST", "http://aws.custom.com/", b"123", boto3_bytes_headers),
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
# chunked transfer encoding: decoded bytes versus encoded bytes
|
||||||
|
request.Request("POST", "scheme1://host1.test/", b"123456789_123456", chunked_headers),
|
||||||
|
request.Request(
|
||||||
|
"GET",
|
||||||
|
"scheme2://host2.test/",
|
||||||
|
b"10\r\n123456789_123456\r\n0\r\n\r\n",
|
||||||
|
chunked_headers,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
# chunked transfer encoding: bytes iterator versus string iterator
|
||||||
|
request.Request(
|
||||||
|
"POST",
|
||||||
|
"scheme1://host1.test/",
|
||||||
|
iter([b"123456789_", b"123456"]),
|
||||||
|
chunked_headers,
|
||||||
|
),
|
||||||
|
request.Request("GET", "scheme2://host2.test/", iter(["123456789_", "123456"]), chunked_headers),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
# chunked transfer encoding: bytes iterator versus single byte iterator
|
||||||
|
request.Request(
|
||||||
|
"POST",
|
||||||
|
"scheme1://host1.test/",
|
||||||
|
iter([b"123456789_", b"123456"]),
|
||||||
|
chunked_headers,
|
||||||
|
),
|
||||||
|
request.Request("GET", "scheme2://host2.test/", iter(b"123456789_123456"), chunked_headers),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_body_matcher_does_match(r1, r2):
|
def test_body_matcher_does_match(r1, r2):
|
||||||
@@ -140,10 +199,16 @@ def test_body_matcher_does_match(r1, r2):
|
|||||||
),
|
),
|
||||||
(
|
(
|
||||||
request.Request(
|
request.Request(
|
||||||
"POST", "http://host.com/", '{"a": 1, "b": 3}', {"Content-Type": "application/json"}
|
"POST",
|
||||||
|
"http://host.com/",
|
||||||
|
'{"a": 1, "b": 3}',
|
||||||
|
{"Content-Type": "application/json"},
|
||||||
),
|
),
|
||||||
request.Request(
|
request.Request(
|
||||||
"POST", "http://host.com/", '{"b": 2, "a": 1}', {"content-type": "application/json"}
|
"POST",
|
||||||
|
"http://host.com/",
|
||||||
|
'{"b": 2, "a": 1}',
|
||||||
|
{"content-type": "application/json"},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import filecmp
|
import filecmp
|
||||||
import json
|
import json
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
import vcr.migration
|
import vcr.migration
|
||||||
@@ -16,9 +17,9 @@ def test_try_migrate_with_json(tmpdir):
|
|||||||
cassette = tmpdir.join("cassette.json").strpath
|
cassette = tmpdir.join("cassette.json").strpath
|
||||||
shutil.copy("tests/fixtures/migration/old_cassette.json", cassette)
|
shutil.copy("tests/fixtures/migration/old_cassette.json", cassette)
|
||||||
assert vcr.migration.try_migrate(cassette)
|
assert vcr.migration.try_migrate(cassette)
|
||||||
with open("tests/fixtures/migration/new_cassette.json", "r") as f:
|
with open("tests/fixtures/migration/new_cassette.json") as f:
|
||||||
expected_json = json.load(f)
|
expected_json = json.load(f)
|
||||||
with open(cassette, "r") as f:
|
with open(cassette) as f:
|
||||||
actual_json = json.load(f)
|
actual_json = json.load(f)
|
||||||
assert actual_json == expected_json
|
assert actual_json == expected_json
|
||||||
|
|
||||||
@@ -27,9 +28,9 @@ def test_try_migrate_with_yaml(tmpdir):
|
|||||||
cassette = tmpdir.join("cassette.yaml").strpath
|
cassette = tmpdir.join("cassette.yaml").strpath
|
||||||
shutil.copy("tests/fixtures/migration/old_cassette.yaml", cassette)
|
shutil.copy("tests/fixtures/migration/old_cassette.yaml", cassette)
|
||||||
assert vcr.migration.try_migrate(cassette)
|
assert vcr.migration.try_migrate(cassette)
|
||||||
with open("tests/fixtures/migration/new_cassette.yaml", "r") as f:
|
with open("tests/fixtures/migration/new_cassette.yaml") as f:
|
||||||
expected_yaml = yaml.load(f, Loader=Loader)
|
expected_yaml = yaml.load(f, Loader=Loader)
|
||||||
with open(cassette, "r") as f:
|
with open(cassette) as f:
|
||||||
actual_yaml = yaml.load(f, Loader=Loader)
|
actual_yaml = yaml.load(f, Loader=Loader)
|
||||||
assert actual_yaml == expected_yaml
|
assert actual_yaml == expected_yaml
|
||||||
|
|
||||||
@@ -44,4 +45,4 @@ def test_try_migrate_with_invalid_or_new_cassettes(tmpdir):
|
|||||||
for file_path in files:
|
for file_path in files:
|
||||||
shutil.copy(file_path, cassette)
|
shutil.copy(file_path, cassette)
|
||||||
assert not vcr.migration.try_migrate(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
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from vcr.request import Request, HeadersDict
|
from vcr.request import HeadersDict, Request
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
@@ -60,7 +60,6 @@ def test_uri(method, uri):
|
|||||||
|
|
||||||
|
|
||||||
def test_HeadersDict():
|
def test_HeadersDict():
|
||||||
|
|
||||||
# Simple test of CaseInsensitiveDict
|
# Simple test of CaseInsensitiveDict
|
||||||
h = HeadersDict()
|
h = HeadersDict()
|
||||||
assert h == {}
|
assert h == {}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
# coding: UTF-8
|
|
||||||
import io
|
import io
|
||||||
|
|
||||||
from vcr.stubs import VCRHTTPResponse
|
from vcr.stubs import VCRHTTPResponse
|
||||||
@@ -89,11 +88,11 @@ def test_response_parses_correctly_and_fp_attribute_error_is_not_thrown():
|
|||||||
b"different types of cancer cells. Recently, the first HDACi was\n "
|
b"different types of cancer cells. Recently, the first HDACi was\n "
|
||||||
b"approved for the "
|
b"approved for the "
|
||||||
b"treatment of cutaneous T cell lymphomas. Most HDACi currently in\n "
|
b"treatment of cutaneous T cell lymphomas. Most HDACi currently in\n "
|
||||||
b"clinical "
|
b"clinical ",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
vcr_response = VCRHTTPResponse(recorded_response)
|
vcr_response = VCRHTTPResponse(recorded_response)
|
||||||
handle = io.TextIOWrapper(io.BufferedReader(vcr_response), encoding="utf-8")
|
handle = io.TextIOWrapper(vcr_response, encoding="utf-8")
|
||||||
handle = iter(handle)
|
handle = iter(handle)
|
||||||
articles = [line for line in handle]
|
articles = list(handle)
|
||||||
assert len(articles) > 1
|
assert len(articles) > 1
|
||||||
|
|||||||
@@ -1,32 +1,29 @@
|
|||||||
# -*- encoding: utf-8 -*-
|
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from vcr.request import Request
|
from vcr.request import Request
|
||||||
from vcr.serialize import deserialize, serialize
|
from vcr.serialize import deserialize, serialize
|
||||||
from vcr.serializers import yamlserializer, jsonserializer, compat
|
from vcr.serializers import compat, jsonserializer, yamlserializer
|
||||||
|
|
||||||
|
|
||||||
def test_deserialize_old_yaml_cassette():
|
def test_deserialize_old_yaml_cassette():
|
||||||
with open("tests/fixtures/migration/old_cassette.yaml", "r") as f:
|
with open("tests/fixtures/migration/old_cassette.yaml") as f, pytest.raises(ValueError):
|
||||||
with pytest.raises(ValueError):
|
deserialize(f.read(), yamlserializer)
|
||||||
deserialize(f.read(), yamlserializer)
|
|
||||||
|
|
||||||
|
|
||||||
def test_deserialize_old_json_cassette():
|
def test_deserialize_old_json_cassette():
|
||||||
with open("tests/fixtures/migration/old_cassette.json", "r") as f:
|
with open("tests/fixtures/migration/old_cassette.json") as f, pytest.raises(ValueError):
|
||||||
with pytest.raises(ValueError):
|
deserialize(f.read(), jsonserializer)
|
||||||
deserialize(f.read(), jsonserializer)
|
|
||||||
|
|
||||||
|
|
||||||
def test_deserialize_new_yaml_cassette():
|
def test_deserialize_new_yaml_cassette():
|
||||||
with open("tests/fixtures/migration/new_cassette.yaml", "r") as f:
|
with open("tests/fixtures/migration/new_cassette.yaml") as f:
|
||||||
deserialize(f.read(), yamlserializer)
|
deserialize(f.read(), yamlserializer)
|
||||||
|
|
||||||
|
|
||||||
def test_deserialize_new_json_cassette():
|
def test_deserialize_new_json_cassette():
|
||||||
with open("tests/fixtures/migration/new_cassette.json", "r") as f:
|
with open("tests/fixtures/migration/new_cassette.json") as f:
|
||||||
deserialize(f.read(), jsonserializer)
|
deserialize(f.read(), jsonserializer)
|
||||||
|
|
||||||
|
|
||||||
@@ -77,7 +74,7 @@ def test_deserialize_py2py3_yaml_cassette(tmpdir, req_body, expect):
|
|||||||
cfile = tmpdir.join("test_cassette.yaml")
|
cfile = tmpdir.join("test_cassette.yaml")
|
||||||
cfile.write(REQBODY_TEMPLATE.format(req_body=req_body))
|
cfile.write(REQBODY_TEMPLATE.format(req_body=req_body))
|
||||||
with open(str(cfile)) as f:
|
with open(str(cfile)) as f:
|
||||||
(requests, responses) = deserialize(f.read(), yamlserializer)
|
(requests, _) = deserialize(f.read(), yamlserializer)
|
||||||
assert requests[0].body == expect
|
assert requests[0].body == expect
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,75 @@
|
|||||||
|
import contextlib
|
||||||
|
import http.client as httplib
|
||||||
|
from io import BytesIO
|
||||||
|
from tempfile import NamedTemporaryFile
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
from vcr import mode
|
from pytest import mark
|
||||||
from vcr.stubs import VCRHTTPSConnection
|
|
||||||
|
from vcr import mode, use_cassette
|
||||||
from vcr.cassette import Cassette
|
from vcr.cassette import Cassette
|
||||||
|
from vcr.stubs import VCRHTTPSConnection
|
||||||
|
|
||||||
|
|
||||||
class TestVCRConnection:
|
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 = VCRHTTPSConnection("www.examplehost.com")
|
||||||
vcr_connection.ssl_version = "example_ssl_version"
|
vcr_connection.ssl_version = "example_ssl_version"
|
||||||
assert vcr_connection.real_connection.ssl_version == "example_ssl_version"
|
assert vcr_connection.real_connection.ssl_version == "example_ssl_version"
|
||||||
|
|
||||||
|
@mark.online
|
||||||
@mock.patch("vcr.cassette.Cassette.can_play_response_for", return_value=False)
|
@mock.patch("vcr.cassette.Cassette.can_play_response_for", return_value=False)
|
||||||
def testing_connect(*args):
|
def testing_connect(*args):
|
||||||
vcr_connection = VCRHTTPSConnection("www.google.com")
|
with contextlib.closing(VCRHTTPSConnection("www.google.com")) as vcr_connection:
|
||||||
vcr_connection.cassette = Cassette("test", record_mode=mode.ALL)
|
vcr_connection.cassette = Cassette("test", record_mode=mode.ALL)
|
||||||
vcr_connection.real_connection.connect()
|
vcr_connection.real_connection.connect()
|
||||||
assert vcr_connection.real_connection.sock is not None
|
assert vcr_connection.real_connection.sock is not None
|
||||||
|
|
||||||
|
def test_body_consumed_once_stream(self, tmpdir, httpbin):
|
||||||
|
self._test_body_consumed_once(
|
||||||
|
tmpdir,
|
||||||
|
httpbin,
|
||||||
|
BytesIO(b"1234567890"),
|
||||||
|
BytesIO(b"9876543210"),
|
||||||
|
BytesIO(b"9876543210"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_body_consumed_once_iterator(self, tmpdir, httpbin):
|
||||||
|
self._test_body_consumed_once(
|
||||||
|
tmpdir,
|
||||||
|
httpbin,
|
||||||
|
iter([b"1234567890"]),
|
||||||
|
iter([b"9876543210"]),
|
||||||
|
iter([b"9876543210"]),
|
||||||
|
)
|
||||||
|
|
||||||
|
# data2 and data3 should serve the same data, potentially as iterators
|
||||||
|
def _test_body_consumed_once(
|
||||||
|
self,
|
||||||
|
tmpdir,
|
||||||
|
httpbin,
|
||||||
|
data1,
|
||||||
|
data2,
|
||||||
|
data3,
|
||||||
|
):
|
||||||
|
with NamedTemporaryFile(dir=tmpdir, suffix=".yml") as f:
|
||||||
|
testpath = f.name
|
||||||
|
# NOTE: ``use_cassette`` is not okay with the file existing
|
||||||
|
# already. So we using ``.close()`` to not only
|
||||||
|
# close but also delete the empty file, before we start.
|
||||||
|
f.close()
|
||||||
|
host, port = httpbin.host, httpbin.port
|
||||||
|
match_on = ["method", "uri", "body"]
|
||||||
|
with use_cassette(testpath, match_on=match_on):
|
||||||
|
conn1 = httplib.HTTPConnection(host, port)
|
||||||
|
conn1.request("POST", "/anything", body=data1)
|
||||||
|
conn1.getresponse()
|
||||||
|
conn2 = httplib.HTTPConnection(host, port)
|
||||||
|
conn2.request("POST", "/anything", body=data2)
|
||||||
|
conn2.getresponse()
|
||||||
|
with use_cassette(testpath, match_on=match_on) as cass:
|
||||||
|
conn3 = httplib.HTTPConnection(host, port)
|
||||||
|
conn3.request("POST", "/anything", body=data3)
|
||||||
|
conn3.getresponse()
|
||||||
|
assert cass.play_counts[0] == 0
|
||||||
|
assert cass.play_counts[1] == 1
|
||||||
|
|||||||
199
tests/unit/test_unittest.py
Normal file
199
tests/unit/test_unittest.py
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
import os
|
||||||
|
from unittest import TextTestRunner, defaultTestLoader
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
from urllib.request import urlopen
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from vcr.unittest import VCRTestCase
|
||||||
|
|
||||||
|
|
||||||
|
def test_defaults():
|
||||||
|
class MyTest(VCRTestCase):
|
||||||
|
def test_foo(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
test = run_testcase(MyTest)[0][0]
|
||||||
|
expected_path = os.path.join(os.path.dirname(__file__), "cassettes")
|
||||||
|
expected_name = "MyTest.test_foo.yaml"
|
||||||
|
assert os.path.dirname(test.cassette._path) == expected_path
|
||||||
|
assert os.path.basename(test.cassette._path) == expected_name
|
||||||
|
|
||||||
|
|
||||||
|
def test_disabled():
|
||||||
|
# Baseline vcr_enabled = True
|
||||||
|
class MyTest(VCRTestCase):
|
||||||
|
def test_foo(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
test = run_testcase(MyTest)[0][0]
|
||||||
|
assert hasattr(test, "cassette")
|
||||||
|
|
||||||
|
# Test vcr_enabled = False
|
||||||
|
class MyTest(VCRTestCase):
|
||||||
|
vcr_enabled = False
|
||||||
|
|
||||||
|
def test_foo(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
test = run_testcase(MyTest)[0][0]
|
||||||
|
assert not hasattr(test, "cassette")
|
||||||
|
|
||||||
|
|
||||||
|
def test_cassette_library_dir():
|
||||||
|
class MyTest(VCRTestCase):
|
||||||
|
def test_foo(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _get_cassette_library_dir(self):
|
||||||
|
return "/testing"
|
||||||
|
|
||||||
|
test = run_testcase(MyTest)[0][0]
|
||||||
|
assert test.cassette._path.startswith("/testing/")
|
||||||
|
|
||||||
|
|
||||||
|
def test_cassette_name():
|
||||||
|
class MyTest(VCRTestCase):
|
||||||
|
def test_foo(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _get_cassette_name(self):
|
||||||
|
return "my-custom-name"
|
||||||
|
|
||||||
|
test = run_testcase(MyTest)[0][0]
|
||||||
|
assert os.path.basename(test.cassette._path) == "my-custom-name"
|
||||||
|
|
||||||
|
|
||||||
|
def test_vcr_kwargs_overridden():
|
||||||
|
class MyTest(VCRTestCase):
|
||||||
|
def test_foo(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _get_vcr_kwargs(self):
|
||||||
|
kwargs = super()._get_vcr_kwargs()
|
||||||
|
kwargs["record_mode"] = "new_episodes"
|
||||||
|
return kwargs
|
||||||
|
|
||||||
|
test = run_testcase(MyTest)[0][0]
|
||||||
|
assert test.cassette.record_mode == "new_episodes"
|
||||||
|
|
||||||
|
|
||||||
|
def test_vcr_kwargs_passed():
|
||||||
|
class MyTest(VCRTestCase):
|
||||||
|
def test_foo(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _get_vcr_kwargs(self):
|
||||||
|
return super()._get_vcr_kwargs(
|
||||||
|
record_mode="new_episodes",
|
||||||
|
)
|
||||||
|
|
||||||
|
test = run_testcase(MyTest)[0][0]
|
||||||
|
assert test.cassette.record_mode == "new_episodes"
|
||||||
|
|
||||||
|
|
||||||
|
def test_vcr_kwargs_cassette_dir():
|
||||||
|
# Test that _get_cassette_library_dir applies if cassette_library_dir
|
||||||
|
# is absent from vcr kwargs.
|
||||||
|
class MyTest(VCRTestCase):
|
||||||
|
def test_foo(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _get_vcr_kwargs(self):
|
||||||
|
return {
|
||||||
|
"record_mode": "new_episodes",
|
||||||
|
}
|
||||||
|
|
||||||
|
_get_cassette_library_dir = MagicMock(return_value="/testing")
|
||||||
|
|
||||||
|
test = run_testcase(MyTest)[0][0]
|
||||||
|
assert test.cassette._path.startswith("/testing/")
|
||||||
|
assert test._get_cassette_library_dir.call_count == 1
|
||||||
|
|
||||||
|
# Test that _get_cassette_library_dir is ignored if cassette_library_dir
|
||||||
|
# is present in vcr kwargs.
|
||||||
|
class MyTest(VCRTestCase):
|
||||||
|
def test_foo(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _get_vcr_kwargs(self):
|
||||||
|
return {
|
||||||
|
"cassette_library_dir": "/testing",
|
||||||
|
}
|
||||||
|
|
||||||
|
_get_cassette_library_dir = MagicMock(return_value="/ignored")
|
||||||
|
|
||||||
|
test = run_testcase(MyTest)[0][0]
|
||||||
|
assert test.cassette._path.startswith("/testing/")
|
||||||
|
assert test._get_cassette_library_dir.call_count == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.online
|
||||||
|
def test_get_vcr_with_matcher(tmpdir):
|
||||||
|
cassette_dir = tmpdir.mkdir("cassettes")
|
||||||
|
assert len(cassette_dir.listdir()) == 0
|
||||||
|
|
||||||
|
mock_matcher = MagicMock(return_value=True, __name__="MockMatcher")
|
||||||
|
|
||||||
|
class MyTest(VCRTestCase):
|
||||||
|
def test_foo(self):
|
||||||
|
self.response = urlopen("http://example.com").read()
|
||||||
|
|
||||||
|
def _get_vcr(self):
|
||||||
|
myvcr = super()._get_vcr()
|
||||||
|
myvcr.register_matcher("mymatcher", mock_matcher)
|
||||||
|
myvcr.match_on = ["mymatcher"]
|
||||||
|
return myvcr
|
||||||
|
|
||||||
|
def _get_cassette_library_dir(self):
|
||||||
|
return str(cassette_dir)
|
||||||
|
|
||||||
|
# First run to fill cassette.
|
||||||
|
test = run_testcase(MyTest)[0][0]
|
||||||
|
assert len(test.cassette.requests) == 1
|
||||||
|
assert not mock_matcher.called # nothing in cassette
|
||||||
|
|
||||||
|
# Second run to call matcher.
|
||||||
|
test = run_testcase(MyTest)[0][0]
|
||||||
|
assert len(test.cassette.requests) == 1
|
||||||
|
assert mock_matcher.called
|
||||||
|
assert (
|
||||||
|
repr(mock_matcher.mock_calls[0])
|
||||||
|
== "call(<Request (GET) http://example.com>, <Request (GET) http://example.com>)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.online
|
||||||
|
def test_testcase_playback(tmpdir):
|
||||||
|
cassette_dir = tmpdir.mkdir("cassettes")
|
||||||
|
assert len(cassette_dir.listdir()) == 0
|
||||||
|
|
||||||
|
# First test actually reads from the web.
|
||||||
|
|
||||||
|
class MyTest(VCRTestCase):
|
||||||
|
def test_foo(self):
|
||||||
|
self.response = urlopen("http://example.com").read()
|
||||||
|
|
||||||
|
def _get_cassette_library_dir(self):
|
||||||
|
return str(cassette_dir)
|
||||||
|
|
||||||
|
test = run_testcase(MyTest)[0][0]
|
||||||
|
assert b"Example Domain" in test.response
|
||||||
|
assert len(test.cassette.requests) == 1
|
||||||
|
assert test.cassette.play_count == 0
|
||||||
|
|
||||||
|
# Second test reads from cassette.
|
||||||
|
|
||||||
|
test2 = run_testcase(MyTest)[0][0]
|
||||||
|
assert test.cassette is not test2.cassette
|
||||||
|
assert b"Example Domain" in test.response
|
||||||
|
assert len(test2.cassette.requests) == 1
|
||||||
|
assert test2.cassette.play_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
def run_testcase(testcase_class):
|
||||||
|
"""Run all the tests in a TestCase and return them."""
|
||||||
|
suite = defaultTestLoader.loadTestsFromTestCase(testcase_class)
|
||||||
|
tests = list(suite._tests)
|
||||||
|
result = TextTestRunner().run(suite)
|
||||||
|
return tests, result
|
||||||
33
tests/unit/test_util.py
Normal file
33
tests/unit/test_util.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
from io import BytesIO, StringIO
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from vcr import request
|
||||||
|
from vcr.util import read_body
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"input_, expected_output",
|
||||||
|
[
|
||||||
|
(BytesIO(b"Stream"), b"Stream"),
|
||||||
|
(StringIO("Stream"), b"Stream"),
|
||||||
|
(iter(["StringIter"]), b"StringIter"),
|
||||||
|
(iter(["String", "Iter"]), b"StringIter"),
|
||||||
|
(iter([b"BytesIter"]), b"BytesIter"),
|
||||||
|
(iter([b"Bytes", b"Iter"]), b"BytesIter"),
|
||||||
|
(iter([70, 111, 111]), b"Foo"),
|
||||||
|
(iter([]), b""),
|
||||||
|
("String", b"String"),
|
||||||
|
(b"Bytes", b"Bytes"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_read_body(input_, expected_output):
|
||||||
|
r = request.Request("POST", "http://host.com/", input_, {})
|
||||||
|
assert read_body(r) == expected_output
|
||||||
|
|
||||||
|
|
||||||
|
def test_unsupported_read_body():
|
||||||
|
r = request.Request("POST", "http://host.com/", iter([[]]), {})
|
||||||
|
with pytest.raises(ValueError) as excinfo:
|
||||||
|
assert read_body(r)
|
||||||
|
assert excinfo.value.args == ("Body type <class 'list'> not supported",)
|
||||||
@@ -1,20 +1,22 @@
|
|||||||
from unittest import mock
|
import http.client as httplib
|
||||||
import os
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import http.client as httplib
|
|
||||||
|
|
||||||
from vcr import VCR, mode, use_cassette
|
from vcr import VCR, mode, use_cassette
|
||||||
|
from vcr.patch import _HTTPConnection, force_reset
|
||||||
from vcr.request import Request
|
from vcr.request import Request
|
||||||
from vcr.stubs import VCRHTTPSConnection
|
from vcr.stubs import VCRHTTPSConnection
|
||||||
from vcr.patch import _HTTPConnection, force_reset
|
|
||||||
|
|
||||||
|
|
||||||
def test_vcr_use_cassette():
|
def test_vcr_use_cassette():
|
||||||
record_mode = mock.Mock()
|
record_mode = mock.Mock()
|
||||||
test_vcr = VCR(record_mode=record_mode)
|
test_vcr = VCR(record_mode=record_mode)
|
||||||
with mock.patch(
|
with mock.patch(
|
||||||
"vcr.cassette.Cassette.load", return_value=mock.MagicMock(inject=False)
|
"vcr.cassette.Cassette.load",
|
||||||
|
return_value=mock.MagicMock(inject=False),
|
||||||
) as mock_cassette_load:
|
) as mock_cassette_load:
|
||||||
|
|
||||||
@test_vcr.use_cassette("test")
|
@test_vcr.use_cassette("test")
|
||||||
@@ -31,7 +33,7 @@ def test_vcr_use_cassette():
|
|||||||
function()
|
function()
|
||||||
assert mock_cassette_load.call_args[1]["record_mode"] == test_vcr.record_mode
|
assert mock_cassette_load.call_args[1]["record_mode"] == test_vcr.record_mode
|
||||||
|
|
||||||
# Ensure that explicitly provided arguments still supercede
|
# Ensure that explicitly provided arguments still supersede
|
||||||
# those on the vcr.
|
# those on the vcr.
|
||||||
new_record_mode = mock.Mock()
|
new_record_mode = mock.Mock()
|
||||||
|
|
||||||
@@ -40,7 +42,7 @@ def test_vcr_use_cassette():
|
|||||||
|
|
||||||
|
|
||||||
def test_vcr_before_record_request_params():
|
def test_vcr_before_record_request_params():
|
||||||
base_path = "http://httpbin.org/"
|
base_path = "http://whatever.test/"
|
||||||
|
|
||||||
def before_record_cb(request):
|
def before_record_cb(request):
|
||||||
if request.path != "/get":
|
if request.path != "/get":
|
||||||
@@ -70,16 +72,19 @@ def test_vcr_before_record_request_params():
|
|||||||
|
|
||||||
# Test filter_headers
|
# Test filter_headers
|
||||||
request = Request(
|
request = Request(
|
||||||
"GET", base_path + "?foo=bar", "", {"cookie": "test", "other": "fun", "bert": "nobody"}
|
"GET",
|
||||||
|
base_path + "?foo=bar",
|
||||||
|
"",
|
||||||
|
{"cookie": "test", "other": "fun", "bert": "nobody"},
|
||||||
)
|
)
|
||||||
assert cassette.filter_request(request).headers == {"other": "fun", "bert": "ernie"}
|
assert cassette.filter_request(request).headers == {"other": "fun", "bert": "ernie"}
|
||||||
|
|
||||||
# Test ignore_hosts
|
# Test ignore_hosts
|
||||||
request = Request("GET", "http://www.test.com" + "?foo=bar", "", {"cookie": "test", "other": "fun"})
|
request = Request("GET", "http://www.test.com?foo=bar", "", {"cookie": "test", "other": "fun"})
|
||||||
assert cassette.filter_request(request) is None
|
assert cassette.filter_request(request) is None
|
||||||
|
|
||||||
# Test ignore_localhost
|
# Test ignore_localhost
|
||||||
request = Request("GET", "http://localhost:8000" + "?foo=bar", "", {"cookie": "test", "other": "fun"})
|
request = Request("GET", "http://localhost:8000?foo=bar", "", {"cookie": "test", "other": "fun"})
|
||||||
assert cassette.filter_request(request) is None
|
assert cassette.filter_request(request) is None
|
||||||
|
|
||||||
with test_vcr.use_cassette("test", before_record_request=None) as cassette:
|
with test_vcr.use_cassette("test", before_record_request=None) as cassette:
|
||||||
@@ -95,7 +100,6 @@ def test_vcr_before_record_response_iterable():
|
|||||||
|
|
||||||
# Prevent actually saving the cassette
|
# Prevent actually saving the cassette
|
||||||
with mock.patch("vcr.cassette.FilesystemPersister.save_cassette"):
|
with mock.patch("vcr.cassette.FilesystemPersister.save_cassette"):
|
||||||
|
|
||||||
# Baseline: non-iterable before_record_response should work
|
# Baseline: non-iterable before_record_response should work
|
||||||
mock_filter = mock.Mock()
|
mock_filter = mock.Mock()
|
||||||
vcr = VCR(before_record_response=mock_filter)
|
vcr = VCR(before_record_response=mock_filter)
|
||||||
@@ -119,7 +123,6 @@ def test_before_record_response_as_filter():
|
|||||||
|
|
||||||
# Prevent actually saving the cassette
|
# Prevent actually saving the cassette
|
||||||
with mock.patch("vcr.cassette.FilesystemPersister.save_cassette"):
|
with mock.patch("vcr.cassette.FilesystemPersister.save_cassette"):
|
||||||
|
|
||||||
filter_all = mock.Mock(return_value=None)
|
filter_all = mock.Mock(return_value=None)
|
||||||
vcr = VCR(before_record_response=filter_all)
|
vcr = VCR(before_record_response=filter_all)
|
||||||
with vcr.use_cassette("test") as cassette:
|
with vcr.use_cassette("test") as cassette:
|
||||||
@@ -133,7 +136,6 @@ def test_vcr_path_transformer():
|
|||||||
|
|
||||||
# Prevent actually saving the cassette
|
# Prevent actually saving the cassette
|
||||||
with mock.patch("vcr.cassette.FilesystemPersister.save_cassette"):
|
with mock.patch("vcr.cassette.FilesystemPersister.save_cassette"):
|
||||||
|
|
||||||
# Baseline: path should be unchanged
|
# Baseline: path should be unchanged
|
||||||
vcr = VCR()
|
vcr = VCR()
|
||||||
with vcr.use_cassette("test") as cassette:
|
with vcr.use_cassette("test") as cassette:
|
||||||
@@ -226,7 +228,7 @@ def test_with_current_defaults():
|
|||||||
|
|
||||||
|
|
||||||
def test_cassette_library_dir_with_decoration_and_no_explicit_path():
|
def test_cassette_library_dir_with_decoration_and_no_explicit_path():
|
||||||
library_dir = "/libary_dir"
|
library_dir = "/library_dir"
|
||||||
vcr = VCR(inject_cassette=True, cassette_library_dir=library_dir)
|
vcr = VCR(inject_cassette=True, cassette_library_dir=library_dir)
|
||||||
|
|
||||||
@vcr.use_cassette()
|
@vcr.use_cassette()
|
||||||
@@ -237,7 +239,7 @@ def test_cassette_library_dir_with_decoration_and_no_explicit_path():
|
|||||||
|
|
||||||
|
|
||||||
def test_cassette_library_dir_with_decoration_and_explicit_path():
|
def test_cassette_library_dir_with_decoration_and_explicit_path():
|
||||||
library_dir = "/libary_dir"
|
library_dir = "/library_dir"
|
||||||
vcr = VCR(inject_cassette=True, cassette_library_dir=library_dir)
|
vcr = VCR(inject_cassette=True, cassette_library_dir=library_dir)
|
||||||
|
|
||||||
@vcr.use_cassette(path="custom_name")
|
@vcr.use_cassette(path="custom_name")
|
||||||
@@ -248,7 +250,7 @@ def test_cassette_library_dir_with_decoration_and_explicit_path():
|
|||||||
|
|
||||||
|
|
||||||
def test_cassette_library_dir_with_decoration_and_super_explicit_path():
|
def test_cassette_library_dir_with_decoration_and_super_explicit_path():
|
||||||
library_dir = "/libary_dir"
|
library_dir = "/library_dir"
|
||||||
vcr = VCR(inject_cassette=True, cassette_library_dir=library_dir)
|
vcr = VCR(inject_cassette=True, cassette_library_dir=library_dir)
|
||||||
|
|
||||||
@vcr.use_cassette(path=os.path.join(library_dir, "custom_name"))
|
@vcr.use_cassette(path=os.path.join(library_dir, "custom_name"))
|
||||||
@@ -259,9 +261,11 @@ def test_cassette_library_dir_with_decoration_and_super_explicit_path():
|
|||||||
|
|
||||||
|
|
||||||
def test_cassette_library_dir_with_path_transformer():
|
def test_cassette_library_dir_with_path_transformer():
|
||||||
library_dir = "/libary_dir"
|
library_dir = "/library_dir"
|
||||||
vcr = VCR(
|
vcr = VCR(
|
||||||
inject_cassette=True, cassette_library_dir=library_dir, path_transformer=lambda path: path + ".json"
|
inject_cassette=True,
|
||||||
|
cassette_library_dir=library_dir,
|
||||||
|
path_transformer=lambda path: path + ".json",
|
||||||
)
|
)
|
||||||
|
|
||||||
@vcr.use_cassette()
|
@vcr.use_cassette()
|
||||||
@@ -360,3 +364,27 @@ def test_dynamically_added(self):
|
|||||||
|
|
||||||
TestVCRClass.test_dynamically_added = test_dynamically_added
|
TestVCRClass.test_dynamically_added = test_dynamically_added
|
||||||
del test_dynamically_added
|
del test_dynamically_added
|
||||||
|
|
||||||
|
|
||||||
|
def test_path_class_as_cassette():
|
||||||
|
path = Path(__file__).parent.parent.joinpath(
|
||||||
|
"integration/cassettes/test_httpx_test_test_behind_proxy.yml",
|
||||||
|
)
|
||||||
|
with use_cassette(path):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def test_use_cassette_generator_return():
|
||||||
|
ret_val = object()
|
||||||
|
|
||||||
|
vcr = VCR()
|
||||||
|
|
||||||
|
@vcr.use_cassette("test")
|
||||||
|
def gen():
|
||||||
|
return ret_val
|
||||||
|
yield
|
||||||
|
|
||||||
|
with pytest.raises(StopIteration) as exc_info:
|
||||||
|
next(gen())
|
||||||
|
|
||||||
|
assert exc_info.value.value is ret_val
|
||||||
|
|||||||
@@ -2,15 +2,10 @@ import sys
|
|||||||
|
|
||||||
|
|
||||||
def test_vcr_import_deprecation(recwarn):
|
def test_vcr_import_deprecation(recwarn):
|
||||||
|
|
||||||
if "vcr" in sys.modules:
|
if "vcr" in sys.modules:
|
||||||
# Remove imported module entry if already loaded in another test
|
# Remove imported module entry if already loaded in another test
|
||||||
del sys.modules["vcr"]
|
del sys.modules["vcr"]
|
||||||
|
|
||||||
import vcr # noqa: F401
|
import vcr # noqa: F401
|
||||||
|
|
||||||
if sys.version_info[0] == 2:
|
assert len(recwarn) == 0
|
||||||
assert len(recwarn) == 1
|
|
||||||
assert issubclass(recwarn[0].category, DeprecationWarning)
|
|
||||||
else:
|
|
||||||
assert len(recwarn) == 0
|
|
||||||
|
|||||||
94
tox.ini
94
tox.ini
@@ -1,94 +0,0 @@
|
|||||||
[tox]
|
|
||||||
skip_missing_interpreters=true
|
|
||||||
envlist =
|
|
||||||
cov-clean,
|
|
||||||
lint,
|
|
||||||
{py35,py36,py37,py38}-{requests,httplib2,urllib3,tornado4,boto3,aiohttp},
|
|
||||||
{py36,py37,py38}-{httpx}
|
|
||||||
{pypy3}-{requests,httplib2,urllib3,tornado4,boto3},
|
|
||||||
cov-report
|
|
||||||
|
|
||||||
|
|
||||||
# Coverage environment tasks: cov-clean and cov-report
|
|
||||||
# https://pytest-cov.readthedocs.io/en/latest/tox.html
|
|
||||||
[testenv:cov-clean]
|
|
||||||
deps = coverage
|
|
||||||
skip_install=true
|
|
||||||
commands = coverage erase
|
|
||||||
|
|
||||||
[testenv:cov-report]
|
|
||||||
deps = coverage
|
|
||||||
skip_install=true
|
|
||||||
commands =
|
|
||||||
coverage html
|
|
||||||
coverage report --fail-under=90
|
|
||||||
|
|
||||||
[testenv:lint]
|
|
||||||
skipsdist = True
|
|
||||||
commands =
|
|
||||||
black --version
|
|
||||||
black --check --diff .
|
|
||||||
flake8 --version
|
|
||||||
flake8 --exclude=./docs/conf.py,./.tox/
|
|
||||||
pyflakes ./docs/conf.py
|
|
||||||
deps =
|
|
||||||
flake8
|
|
||||||
black
|
|
||||||
|
|
||||||
[testenv:docs]
|
|
||||||
# Running sphinx from inside the "docs" directory
|
|
||||||
# ensures it will not pick up any stray files that might
|
|
||||||
# get into a virtual environment under the top-level directory
|
|
||||||
# or other artifacts under build/
|
|
||||||
changedir = docs
|
|
||||||
# The only dependency is sphinx
|
|
||||||
# If we were using extensions packaged separately,
|
|
||||||
# we would specify them here.
|
|
||||||
# A better practice is to specify a specific version of sphinx.
|
|
||||||
deps =
|
|
||||||
sphinx
|
|
||||||
sphinx_rtd_theme
|
|
||||||
# This is the sphinx command to generate HTML.
|
|
||||||
# In other circumstances, we might want to generate a PDF or an ebook
|
|
||||||
commands =
|
|
||||||
sphinx-build -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html
|
|
||||||
# We use Python 3.7. Tox sometimes tries to autodetect it based on the name of
|
|
||||||
# the testenv, but "docs" does not give useful clues so we have to be explicit.
|
|
||||||
basepython = python3.7
|
|
||||||
|
|
||||||
[testenv]
|
|
||||||
# Need to use develop install so that paths
|
|
||||||
# for aggregate code coverage combine
|
|
||||||
usedevelop=true
|
|
||||||
commands =
|
|
||||||
./runtests.sh --cov=./vcr --cov-branch --cov-report=xml --cov-append {posargs}
|
|
||||||
deps =
|
|
||||||
Flask
|
|
||||||
pytest
|
|
||||||
pytest-httpbin
|
|
||||||
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
|
|
||||||
{py36,py37,py38}-{httpx}: httpx
|
|
||||||
{py36,py37,py38}-{httpx}: pytest-asyncio
|
|
||||||
depends =
|
|
||||||
lint,{py35,py36,py37,py38,pypy3}-{requests,httplib2,urllib3,tornado4,boto3},{py35,py36,py37,py38}-{aiohttp},{py36,py37,py38}-{httpx}: cov-clean
|
|
||||||
cov-report: lint,{py35,py36,py37,py38,pypy3}-{requests,httplib2,urllib3,tornado4,boto3},{py35,py36,py37,py38}-{aiohttp}
|
|
||||||
passenv =
|
|
||||||
AWS_ACCESS_KEY_ID
|
|
||||||
AWS_DEFAULT_REGION
|
|
||||||
AWS_SECRET_ACCESS_KEY
|
|
||||||
|
|
||||||
[flake8]
|
|
||||||
max_line_length = 110
|
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import logging
|
import logging
|
||||||
from .config import VCR
|
|
||||||
from logging import NullHandler
|
from logging import NullHandler
|
||||||
from .record_mode import RecordMode as mode # noqa import is not used in this file
|
|
||||||
|
|
||||||
__version__ = "4.1.0"
|
from .config import VCR
|
||||||
|
from .record_mode import RecordMode as mode # noqa: F401
|
||||||
|
|
||||||
|
__version__ = "7.0.0"
|
||||||
|
|
||||||
logging.getLogger(__name__).addHandler(NullHandler())
|
logging.getLogger(__name__).addHandler(NullHandler())
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
async def handle_coroutine(vcr, fn): # noqa: E999
|
async def handle_coroutine(vcr, fn):
|
||||||
with vcr as cassette:
|
with vcr as cassette:
|
||||||
return await fn(cassette) # noqa: E999
|
return await fn(cassette)
|
||||||
|
|||||||
118
vcr/cassette.py
118
vcr/cassette.py
@@ -1,28 +1,20 @@
|
|||||||
import collections
|
import collections
|
||||||
import contextlib
|
import contextlib
|
||||||
import copy
|
import copy
|
||||||
import sys
|
|
||||||
import inspect
|
import inspect
|
||||||
import logging
|
import logging
|
||||||
|
from inspect import iscoroutinefunction
|
||||||
|
|
||||||
import wrapt
|
import wrapt
|
||||||
|
|
||||||
from .errors import UnhandledHTTPRequestError
|
|
||||||
from .matchers import requests_match, uri, method, get_matchers_results
|
|
||||||
from .patch import CassettePatcherBuilder
|
|
||||||
from .serializers import yamlserializer
|
|
||||||
from .persisters.filesystem import FilesystemPersister
|
|
||||||
from .util import partition_dict
|
|
||||||
from ._handle_coroutine import handle_coroutine
|
from ._handle_coroutine import handle_coroutine
|
||||||
|
from .errors import UnhandledHTTPRequestError
|
||||||
|
from .matchers import get_matchers_results, method, requests_match, uri
|
||||||
|
from .patch import CassettePatcherBuilder
|
||||||
|
from .persisters.filesystem import CassetteDecodeError, CassetteNotFoundError, FilesystemPersister
|
||||||
from .record_mode import RecordMode
|
from .record_mode import RecordMode
|
||||||
|
from .serializers import yamlserializer
|
||||||
try:
|
from .util import partition_dict
|
||||||
from asyncio import iscoroutinefunction
|
|
||||||
except ImportError:
|
|
||||||
|
|
||||||
def iscoroutinefunction(*args, **kwargs):
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -45,7 +37,11 @@ class CassetteContextDecorator:
|
|||||||
this class as a context manager in ``__exit__``.
|
this class as a context manager in ``__exit__``.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_non_cassette_arguments = ("path_transformer", "func_path_generator")
|
_non_cassette_arguments = (
|
||||||
|
"path_transformer",
|
||||||
|
"func_path_generator",
|
||||||
|
"record_on_exception",
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_args(cls, cassette_class, **kwargs):
|
def from_args(cls, cassette_class, **kwargs):
|
||||||
@@ -55,6 +51,7 @@ class CassetteContextDecorator:
|
|||||||
self.cls = cls
|
self.cls = cls
|
||||||
self._args_getter = args_getter
|
self._args_getter = args_getter
|
||||||
self.__finish = None
|
self.__finish = None
|
||||||
|
self.__cassette = None
|
||||||
|
|
||||||
def _patch_generator(self, cassette):
|
def _patch_generator(self, cassette):
|
||||||
with contextlib.ExitStack() as exit_stack:
|
with contextlib.ExitStack() as exit_stack:
|
||||||
@@ -64,9 +61,6 @@ class CassetteContextDecorator:
|
|||||||
log.debug(log_format.format(action="Entering", path=cassette._path))
|
log.debug(log_format.format(action="Entering", path=cassette._path))
|
||||||
yield cassette
|
yield cassette
|
||||||
log.debug(log_format.format(action="Exiting", path=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()
|
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
# This assertion is here to prevent the dangerous behavior
|
# This assertion is here to prevent the dangerous behavior
|
||||||
@@ -79,15 +73,28 @@ class CassetteContextDecorator:
|
|||||||
# pass
|
# pass
|
||||||
assert self.__finish is None, "Cassette already open."
|
assert self.__finish is None, "Cassette already open."
|
||||||
other_kwargs, cassette_kwargs = partition_dict(
|
other_kwargs, cassette_kwargs = partition_dict(
|
||||||
lambda key, _: key in self._non_cassette_arguments, self._args_getter()
|
lambda key, _: key in self._non_cassette_arguments,
|
||||||
|
self._args_getter(),
|
||||||
)
|
)
|
||||||
if other_kwargs.get("path_transformer"):
|
if other_kwargs.get("path_transformer"):
|
||||||
transformer = other_kwargs["path_transformer"]
|
transformer = other_kwargs["path_transformer"]
|
||||||
cassette_kwargs["path"] = transformer(cassette_kwargs["path"])
|
cassette_kwargs["path"] = transformer(cassette_kwargs["path"])
|
||||||
self.__finish = self._patch_generator(self.cls.load(**cassette_kwargs))
|
self.__cassette = self.cls.load(**cassette_kwargs)
|
||||||
|
self.__finish = self._patch_generator(self.__cassette)
|
||||||
return next(self.__finish)
|
return next(self.__finish)
|
||||||
|
|
||||||
def __exit__(self, *args):
|
def __exit__(self, *exc_info):
|
||||||
|
exception_was_raised = any(exc_info)
|
||||||
|
record_on_exception = self._args_getter().get("record_on_exception", True)
|
||||||
|
if record_on_exception or not exception_was_raised:
|
||||||
|
self.__cassette._save()
|
||||||
|
self.__cassette = None
|
||||||
|
# Fellow programmer, don't remove this `next`, if `self.__finish` is
|
||||||
|
# not consumed the unpatcher functions accumulated in the `exit_stack`
|
||||||
|
# object created in `_patch_generator` will not be called until
|
||||||
|
# `exit_stack` is not garbage collected.
|
||||||
|
# This works in CPython but not in Pypy, where the unpatchers will not
|
||||||
|
# be called until much later.
|
||||||
next(self.__finish, None)
|
next(self.__finish, None)
|
||||||
self.__finish = None
|
self.__finish = None
|
||||||
|
|
||||||
@@ -118,20 +125,7 @@ class CassetteContextDecorator:
|
|||||||
duration of the generator.
|
duration of the generator.
|
||||||
"""
|
"""
|
||||||
with self as cassette:
|
with self as cassette:
|
||||||
coroutine = fn(cassette)
|
return (yield from fn(cassette))
|
||||||
# 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:
|
|
||||||
try:
|
|
||||||
to_yield = coroutine.send(to_send)
|
|
||||||
except StopIteration:
|
|
||||||
break
|
|
||||||
|
|
||||||
def _handle_function(self, fn):
|
def _handle_function(self, fn):
|
||||||
with self as cassette:
|
with self as cassette:
|
||||||
@@ -183,6 +177,7 @@ class Cassette:
|
|||||||
custom_patches=(),
|
custom_patches=(),
|
||||||
inject=False,
|
inject=False,
|
||||||
allow_playback_repeats=False,
|
allow_playback_repeats=False,
|
||||||
|
drop_unused_requests=False,
|
||||||
):
|
):
|
||||||
self._persister = persister or FilesystemPersister
|
self._persister = persister or FilesystemPersister
|
||||||
self._path = path
|
self._path = path
|
||||||
@@ -195,6 +190,7 @@ class Cassette:
|
|||||||
self.record_mode = record_mode
|
self.record_mode = record_mode
|
||||||
self.custom_patches = custom_patches
|
self.custom_patches = custom_patches
|
||||||
self.allow_playback_repeats = allow_playback_repeats
|
self.allow_playback_repeats = allow_playback_repeats
|
||||||
|
self.drop_unused_requests = drop_unused_requests
|
||||||
|
|
||||||
# self.data is the list of (req, resp) tuples
|
# self.data is the list of (req, resp) tuples
|
||||||
self.data = []
|
self.data = []
|
||||||
@@ -202,6 +198,10 @@ class Cassette:
|
|||||||
self.dirty = False
|
self.dirty = False
|
||||||
self.rewound = False
|
self.rewound = False
|
||||||
|
|
||||||
|
# Subsets of self.data to store old and played interactions
|
||||||
|
self._old_interactions = []
|
||||||
|
self._played_interactions = []
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def play_count(self):
|
def play_count(self):
|
||||||
return sum(self.play_counts.values())
|
return sum(self.play_counts.values())
|
||||||
@@ -221,14 +221,14 @@ class Cassette:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def write_protected(self):
|
def write_protected(self):
|
||||||
return self.rewound and self.record_mode == RecordMode.ONCE or self.record_mode == RecordMode.NONE
|
return (self.rewound and self.record_mode == RecordMode.ONCE) or self.record_mode == RecordMode.NONE
|
||||||
|
|
||||||
def append(self, request, response):
|
def append(self, request, response):
|
||||||
"""Add a request, response pair to this cassette"""
|
"""Add a request, response pair to this cassette"""
|
||||||
log.info("Appending request %s and response %s", request, response)
|
|
||||||
request = self._before_record_request(request)
|
request = self._before_record_request(request)
|
||||||
if not request:
|
if not request:
|
||||||
return
|
return
|
||||||
|
log.info("Appending request %s and response %s", request, response)
|
||||||
# Deepcopy is here because mutation of `response` will corrupt the
|
# Deepcopy is here because mutation of `response` will corrupt the
|
||||||
# real response.
|
# real response.
|
||||||
response = copy.deepcopy(response)
|
response = copy.deepcopy(response)
|
||||||
@@ -263,10 +263,11 @@ class Cassette:
|
|||||||
for index, response in self._responses(request):
|
for index, response in self._responses(request):
|
||||||
if self.play_counts[index] == 0 or self.allow_playback_repeats:
|
if self.play_counts[index] == 0 or self.allow_playback_repeats:
|
||||||
self.play_counts[index] += 1
|
self.play_counts[index] += 1
|
||||||
|
self._played_interactions.append((request, response))
|
||||||
return response
|
return response
|
||||||
# The cassette doesn't contain the request asked for.
|
# The cassette doesn't contain the request asked for.
|
||||||
raise UnhandledHTTPRequestError(
|
raise UnhandledHTTPRequestError(
|
||||||
"The cassette (%r) doesn't contain the request (%r) asked for" % (self._path, request)
|
f"The cassette ({self._path!r}) doesn't contain the request ({request!r}) asked for",
|
||||||
)
|
)
|
||||||
|
|
||||||
def responses_of(self, request):
|
def responses_of(self, request):
|
||||||
@@ -281,7 +282,7 @@ class Cassette:
|
|||||||
return responses
|
return responses
|
||||||
# The cassette doesn't contain the request asked for.
|
# The cassette doesn't contain the request asked for.
|
||||||
raise UnhandledHTTPRequestError(
|
raise UnhandledHTTPRequestError(
|
||||||
"The cassette (%r) doesn't contain the request (%r) asked for" % (self._path, request)
|
f"The cassette ({self._path!r}) doesn't contain the request ({request!r}) asked for",
|
||||||
)
|
)
|
||||||
|
|
||||||
def rewind(self):
|
def rewind(self):
|
||||||
@@ -300,7 +301,7 @@ class Cassette:
|
|||||||
"""
|
"""
|
||||||
best_matches = []
|
best_matches = []
|
||||||
request = self._before_record_request(request)
|
request = self._before_record_request(request)
|
||||||
for index, (stored_request, response) in enumerate(self.data):
|
for _, (stored_request, _) in enumerate(self.data):
|
||||||
successes, fails = get_matchers_results(request, stored_request, self._match_on)
|
successes, fails = get_matchers_results(request, stored_request, self._match_on)
|
||||||
best_matches.append((len(successes), stored_request, successes, fails))
|
best_matches.append((len(successes), stored_request, successes, fails))
|
||||||
best_matches.sort(key=lambda t: t[0], reverse=True)
|
best_matches.sort(key=lambda t: t[0], reverse=True)
|
||||||
@@ -323,26 +324,51 @@ class Cassette:
|
|||||||
|
|
||||||
return final_best_matches
|
return final_best_matches
|
||||||
|
|
||||||
|
def _new_interactions(self):
|
||||||
|
"""List of new HTTP interactions (request/response tuples)"""
|
||||||
|
new_interactions = []
|
||||||
|
for request, response in self.data:
|
||||||
|
if all(
|
||||||
|
not requests_match(request, old_request, self._match_on)
|
||||||
|
for old_request, _ in self._old_interactions
|
||||||
|
):
|
||||||
|
new_interactions.append((request, response))
|
||||||
|
return new_interactions
|
||||||
|
|
||||||
def _as_dict(self):
|
def _as_dict(self):
|
||||||
return {"requests": self.requests, "responses": self.responses}
|
return {"requests": self.requests, "responses": self.responses}
|
||||||
|
|
||||||
|
def _build_used_interactions_dict(self):
|
||||||
|
interactions = self._played_interactions + self._new_interactions()
|
||||||
|
cassete_dict = {
|
||||||
|
"requests": [request for request, _ in interactions],
|
||||||
|
"responses": [response for _, response in interactions],
|
||||||
|
}
|
||||||
|
return cassete_dict
|
||||||
|
|
||||||
def _save(self, force=False):
|
def _save(self, force=False):
|
||||||
|
if self.drop_unused_requests and len(self._played_interactions) < len(self._old_interactions):
|
||||||
|
cassete_dict = self._build_used_interactions_dict()
|
||||||
|
force = True
|
||||||
|
else:
|
||||||
|
cassete_dict = self._as_dict()
|
||||||
if force or self.dirty:
|
if force or self.dirty:
|
||||||
self._persister.save_cassette(self._path, self._as_dict(), serializer=self._serializer)
|
self._persister.save_cassette(self._path, cassete_dict, serializer=self._serializer)
|
||||||
self.dirty = False
|
self.dirty = False
|
||||||
|
|
||||||
def _load(self):
|
def _load(self):
|
||||||
try:
|
try:
|
||||||
requests, responses = self._persister.load_cassette(self._path, serializer=self._serializer)
|
requests, responses = self._persister.load_cassette(self._path, serializer=self._serializer)
|
||||||
for request, response in zip(requests, responses):
|
for request, response in zip(requests, responses, strict=False):
|
||||||
self.append(request, response)
|
self.append(request, response)
|
||||||
|
self._old_interactions.append((request, response))
|
||||||
self.dirty = False
|
self.dirty = False
|
||||||
self.rewound = True
|
self.rewound = True
|
||||||
except ValueError:
|
except (CassetteDecodeError, CassetteNotFoundError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "<Cassette containing {} recorded response(s)>".format(len(self))
|
return f"<Cassette containing {len(self)} recorded response(s)>"
|
||||||
|
|
||||||
def __len__(self):
|
def __len__(self):
|
||||||
"""Return the number of request,response pairs stored in here"""
|
"""Return the number of request,response pairs stored in here"""
|
||||||
@@ -350,7 +376,7 @@ class Cassette:
|
|||||||
|
|
||||||
def __contains__(self, request):
|
def __contains__(self, request):
|
||||||
"""Return whether or not a request has been stored"""
|
"""Return whether or not a request has been stored"""
|
||||||
for index, response in self._responses(request):
|
for index, _ in self._responses(request):
|
||||||
if self.play_counts[index] == 0 or self.allow_playback_repeats:
|
if self.play_counts[index] == 0 or self.allow_playback_repeats:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -1,20 +1,17 @@
|
|||||||
import copy
|
import copy
|
||||||
|
|
||||||
from collections import abc as collections_abc
|
|
||||||
import functools
|
import functools
|
||||||
import inspect
|
import inspect
|
||||||
import os
|
import os
|
||||||
import types
|
import types
|
||||||
|
from collections import abc as collections_abc
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import six
|
from . import filters, matchers
|
||||||
|
|
||||||
from .cassette import Cassette
|
from .cassette import Cassette
|
||||||
from .serializers import yamlserializer, jsonserializer
|
|
||||||
from .persisters.filesystem import FilesystemPersister
|
from .persisters.filesystem import FilesystemPersister
|
||||||
from .util import compose, auto_decorate
|
|
||||||
from .record_mode import RecordMode
|
from .record_mode import RecordMode
|
||||||
from . import matchers
|
from .serializers import jsonserializer, yamlserializer
|
||||||
from . import filters
|
from .util import auto_decorate, compose
|
||||||
|
|
||||||
|
|
||||||
class VCR:
|
class VCR:
|
||||||
@@ -50,6 +47,8 @@ class VCR:
|
|||||||
cassette_library_dir=None,
|
cassette_library_dir=None,
|
||||||
func_path_generator=None,
|
func_path_generator=None,
|
||||||
decode_compressed_response=False,
|
decode_compressed_response=False,
|
||||||
|
record_on_exception=True,
|
||||||
|
drop_unused_requests=False,
|
||||||
):
|
):
|
||||||
self.serializer = serializer
|
self.serializer = serializer
|
||||||
self.match_on = match_on
|
self.match_on = match_on
|
||||||
@@ -81,13 +80,15 @@ class VCR:
|
|||||||
self.path_transformer = path_transformer
|
self.path_transformer = path_transformer
|
||||||
self.func_path_generator = func_path_generator
|
self.func_path_generator = func_path_generator
|
||||||
self.decode_compressed_response = decode_compressed_response
|
self.decode_compressed_response = decode_compressed_response
|
||||||
|
self.record_on_exception = record_on_exception
|
||||||
self._custom_patches = tuple(custom_patches)
|
self._custom_patches = tuple(custom_patches)
|
||||||
|
self.drop_unused_requests = drop_unused_requests
|
||||||
|
|
||||||
def _get_serializer(self, serializer_name):
|
def _get_serializer(self, serializer_name):
|
||||||
try:
|
try:
|
||||||
serializer = self.serializers[serializer_name]
|
serializer = self.serializers[serializer_name]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise KeyError("Serializer {} doesn't exist or isn't registered".format(serializer_name))
|
raise KeyError(f"Serializer {serializer_name} doesn't exist or isn't registered") from None
|
||||||
return serializer
|
return serializer
|
||||||
|
|
||||||
def _get_matchers(self, matcher_names):
|
def _get_matchers(self, matcher_names):
|
||||||
@@ -96,11 +97,11 @@ class VCR:
|
|||||||
for m in matcher_names:
|
for m in matcher_names:
|
||||||
matchers.append(self.matchers[m])
|
matchers.append(self.matchers[m])
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise KeyError("Matcher {} doesn't exist or isn't registered".format(m))
|
raise KeyError(f"Matcher {m} doesn't exist or isn't registered") from None
|
||||||
return matchers
|
return matchers
|
||||||
|
|
||||||
def use_cassette(self, path=None, **kwargs):
|
def use_cassette(self, path=None, **kwargs):
|
||||||
if path is not None and not isinstance(path, str):
|
if path is not None and not isinstance(path, (str, Path)):
|
||||||
function = path
|
function = path
|
||||||
# Assume this is an attempt to decorate a function
|
# Assume this is an attempt to decorate a function
|
||||||
return self._use_cassette(**kwargs)(function)
|
return self._use_cassette(**kwargs)(function)
|
||||||
@@ -124,6 +125,7 @@ class VCR:
|
|||||||
func_path_generator = kwargs.get("func_path_generator", self.func_path_generator)
|
func_path_generator = kwargs.get("func_path_generator", self.func_path_generator)
|
||||||
cassette_library_dir = kwargs.get("cassette_library_dir", self.cassette_library_dir)
|
cassette_library_dir = kwargs.get("cassette_library_dir", self.cassette_library_dir)
|
||||||
additional_matchers = kwargs.get("additional_matchers", ())
|
additional_matchers = kwargs.get("additional_matchers", ())
|
||||||
|
record_on_exception = kwargs.get("record_on_exception", self.record_on_exception)
|
||||||
|
|
||||||
if cassette_library_dir:
|
if cassette_library_dir:
|
||||||
|
|
||||||
@@ -150,6 +152,8 @@ class VCR:
|
|||||||
"path_transformer": path_transformer,
|
"path_transformer": path_transformer,
|
||||||
"func_path_generator": func_path_generator,
|
"func_path_generator": func_path_generator,
|
||||||
"allow_playback_repeats": kwargs.get("allow_playback_repeats", False),
|
"allow_playback_repeats": kwargs.get("allow_playback_repeats", False),
|
||||||
|
"record_on_exception": record_on_exception,
|
||||||
|
"drop_unused_requests": kwargs.get("drop_unused_requests", self.drop_unused_requests),
|
||||||
}
|
}
|
||||||
path = kwargs.get("path")
|
path = kwargs.get("path")
|
||||||
if path:
|
if path:
|
||||||
@@ -159,7 +163,8 @@ class VCR:
|
|||||||
def _build_before_record_response(self, options):
|
def _build_before_record_response(self, options):
|
||||||
before_record_response = options.get("before_record_response", self.before_record_response)
|
before_record_response = options.get("before_record_response", self.before_record_response)
|
||||||
decode_compressed_response = options.get(
|
decode_compressed_response = options.get(
|
||||||
"decode_compressed_response", self.decode_compressed_response
|
"decode_compressed_response",
|
||||||
|
self.decode_compressed_response,
|
||||||
)
|
)
|
||||||
filter_functions = []
|
filter_functions = []
|
||||||
if decode_compressed_response:
|
if decode_compressed_response:
|
||||||
@@ -183,10 +188,12 @@ class VCR:
|
|||||||
filter_headers = options.get("filter_headers", self.filter_headers)
|
filter_headers = options.get("filter_headers", self.filter_headers)
|
||||||
filter_query_parameters = options.get("filter_query_parameters", self.filter_query_parameters)
|
filter_query_parameters = options.get("filter_query_parameters", self.filter_query_parameters)
|
||||||
filter_post_data_parameters = options.get(
|
filter_post_data_parameters = options.get(
|
||||||
"filter_post_data_parameters", self.filter_post_data_parameters
|
"filter_post_data_parameters",
|
||||||
|
self.filter_post_data_parameters,
|
||||||
)
|
)
|
||||||
before_record_request = options.get(
|
before_record_request = options.get(
|
||||||
"before_record_request", 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)
|
ignore_hosts = options.get("ignore_hosts", self.ignore_hosts)
|
||||||
ignore_localhost = options.get("ignore_localhost", self.ignore_localhost)
|
ignore_localhost = options.get("ignore_localhost", self.ignore_localhost)
|
||||||
@@ -196,12 +203,12 @@ class VCR:
|
|||||||
if filter_query_parameters:
|
if filter_query_parameters:
|
||||||
replacements = [p if isinstance(p, tuple) else (p, None) for p in filter_query_parameters]
|
replacements = [p if isinstance(p, tuple) else (p, None) for p in filter_query_parameters]
|
||||||
filter_functions.append(
|
filter_functions.append(
|
||||||
functools.partial(filters.replace_query_parameters, replacements=replacements)
|
functools.partial(filters.replace_query_parameters, replacements=replacements),
|
||||||
)
|
)
|
||||||
if filter_post_data_parameters:
|
if filter_post_data_parameters:
|
||||||
replacements = [p if isinstance(p, tuple) else (p, None) for p in filter_post_data_parameters]
|
replacements = [p if isinstance(p, tuple) else (p, None) for p in filter_post_data_parameters]
|
||||||
filter_functions.append(
|
filter_functions.append(
|
||||||
functools.partial(filters.replace_post_data_parameters, replacements=replacements)
|
functools.partial(filters.replace_post_data_parameters, replacements=replacements),
|
||||||
)
|
)
|
||||||
|
|
||||||
hosts_to_ignore = set(ignore_hosts)
|
hosts_to_ignore = set(ignore_hosts)
|
||||||
@@ -216,7 +223,7 @@ class VCR:
|
|||||||
filter_functions.extend(before_record_request)
|
filter_functions.extend(before_record_request)
|
||||||
|
|
||||||
def before_record_request(request):
|
def before_record_request(request):
|
||||||
request = copy.copy(request)
|
request = copy.deepcopy(request)
|
||||||
for function in filter_functions:
|
for function in filter_functions:
|
||||||
if request is None:
|
if request is None:
|
||||||
break
|
break
|
||||||
@@ -250,5 +257,5 @@ class VCR:
|
|||||||
|
|
||||||
def test_case(self, predicate=None):
|
def test_case(self, predicate=None):
|
||||||
predicate = predicate or self.is_test_method
|
predicate = predicate or self.is_test_method
|
||||||
# TODO: Remove this reference to `six` in favor of the Python3 equivalent
|
metaclass = auto_decorate(self.use_cassette, predicate)
|
||||||
return six.with_metaclass(auto_decorate(self.use_cassette, predicate))
|
return metaclass("temporary_class", (), {})
|
||||||
|
|||||||
@@ -13,30 +13,29 @@ class CannotOverwriteExistingCassetteException(Exception):
|
|||||||
best_matches = cassette.find_requests_with_most_matches(failed_request)
|
best_matches = cassette.find_requests_with_most_matches(failed_request)
|
||||||
if best_matches:
|
if best_matches:
|
||||||
# Build a comprehensible message to put in the exception.
|
# Build a comprehensible message to put in the exception.
|
||||||
best_matches_msg = "Found {} similar requests with {} different matcher(s) :\n".format(
|
best_matches_msg = (
|
||||||
len(best_matches), len(best_matches[0][2])
|
f"Found {len(best_matches)} similar requests "
|
||||||
|
f"with {len(best_matches[0][2])} different matcher(s) :\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
for idx, best_match in enumerate(best_matches, start=1):
|
for idx, best_match in enumerate(best_matches, start=1):
|
||||||
request, succeeded_matchers, failed_matchers_assertion_msgs = best_match
|
request, succeeded_matchers, failed_matchers_assertion_msgs = best_match
|
||||||
best_matches_msg += (
|
best_matches_msg += (
|
||||||
"\n%s - (%r).\n"
|
f"\n{idx} - ({request!r}).\n"
|
||||||
"Matchers succeeded : %s\n"
|
f"Matchers succeeded : {succeeded_matchers}\n"
|
||||||
"Matchers failed :\n" % (idx, request, succeeded_matchers)
|
"Matchers failed :\n"
|
||||||
)
|
)
|
||||||
for failed_matcher, assertion_msg in failed_matchers_assertion_msgs:
|
for failed_matcher, assertion_msg in failed_matchers_assertion_msgs:
|
||||||
best_matches_msg += "%s - assertion failure :\n" "%s\n" % (failed_matcher, assertion_msg)
|
best_matches_msg += f"{failed_matcher} - assertion failure :\n{assertion_msg}\n"
|
||||||
else:
|
else:
|
||||||
best_matches_msg = "No similar requests, that have not been played, found."
|
best_matches_msg = "No similar requests, that have not been played, found."
|
||||||
return (
|
return (
|
||||||
"Can't overwrite existing cassette (%r) in "
|
f"Can't overwrite existing cassette ({cassette._path!r}) in "
|
||||||
"your current record mode (%r).\n"
|
f"your current record mode ({cassette.record_mode!r}).\n"
|
||||||
"No match for the request (%r) was found.\n"
|
f"No match for the request ({failed_request!r}) was found.\n"
|
||||||
"%s" % (cassette._path, cassette.record_mode, failed_request, best_matches_msg)
|
f"{best_matches_msg}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class UnhandledHTTPRequestError(KeyError):
|
class UnhandledHTTPRequestError(KeyError):
|
||||||
"""Raised when a cassette does not contain the request we want."""
|
"""Raised when a cassette does not contain the request we want."""
|
||||||
|
|
||||||
pass
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
from io import BytesIO
|
|
||||||
from urllib.parse import urlparse, urlencode, urlunparse
|
|
||||||
import copy
|
import copy
|
||||||
import json
|
import json
|
||||||
import zlib
|
import zlib
|
||||||
|
from io import BytesIO
|
||||||
|
from urllib.parse import urlencode, urlparse, urlunparse
|
||||||
|
|
||||||
from .util import CaseInsensitiveDict
|
from .util import CaseInsensitiveDict
|
||||||
|
|
||||||
@@ -84,8 +84,18 @@ def replace_post_data_parameters(request, replacements):
|
|||||||
|
|
||||||
replacements = dict(replacements)
|
replacements = dict(replacements)
|
||||||
if request.method == "POST" and not isinstance(request.body, BytesIO):
|
if request.method == "POST" and not isinstance(request.body, BytesIO):
|
||||||
if request.headers.get("Content-Type") == "application/json":
|
if isinstance(request.body, dict):
|
||||||
json_data = json.loads(request.body.decode("utf-8"))
|
new_body = request.body.copy()
|
||||||
|
for k, rv in replacements.items():
|
||||||
|
if k in new_body:
|
||||||
|
ov = new_body.pop(k)
|
||||||
|
if callable(rv):
|
||||||
|
rv = rv(key=k, value=ov, request=request)
|
||||||
|
if rv is not None:
|
||||||
|
new_body[k] = rv
|
||||||
|
request.body = new_body
|
||||||
|
elif request.headers.get("Content-Type") == "application/json":
|
||||||
|
json_data = json.loads(request.body)
|
||||||
for k, rv in replacements.items():
|
for k, rv in replacements.items():
|
||||||
if k in json_data:
|
if k in json_data:
|
||||||
ov = json_data.pop(k)
|
ov = json_data.pop(k)
|
||||||
@@ -140,10 +150,18 @@ def decode_response(response):
|
|||||||
"""Returns decompressed body according to encoding using zlib.
|
"""Returns decompressed body according to encoding using zlib.
|
||||||
to (de-)compress gzip format, use wbits = zlib.MAX_WBITS | 16
|
to (de-)compress gzip format, use wbits = zlib.MAX_WBITS | 16
|
||||||
"""
|
"""
|
||||||
|
if not body:
|
||||||
|
return ""
|
||||||
if encoding == "gzip":
|
if encoding == "gzip":
|
||||||
return zlib.decompress(body, zlib.MAX_WBITS | 16)
|
try:
|
||||||
|
return zlib.decompress(body, zlib.MAX_WBITS | 16)
|
||||||
|
except zlib.error:
|
||||||
|
return body # assumes that the data was already decompressed
|
||||||
else: # encoding == 'deflate'
|
else: # encoding == 'deflate'
|
||||||
return zlib.decompress(body)
|
try:
|
||||||
|
return zlib.decompress(body)
|
||||||
|
except zlib.error:
|
||||||
|
return body # assumes that the data was already decompressed
|
||||||
|
|
||||||
# Deepcopy here in case `headers` contain objects that could
|
# Deepcopy here in case `headers` contain objects that could
|
||||||
# be mutated by a shallow copy and corrupt the real response.
|
# be mutated by a shallow copy and corrupt the real response.
|
||||||
|
|||||||
127
vcr/matchers.py
127
vcr/matchers.py
@@ -1,55 +1,74 @@
|
|||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
import urllib
|
import urllib
|
||||||
import xmlrpc.client
|
import xmlrpc.client
|
||||||
from .util import read_body
|
from string import hexdigits
|
||||||
import logging
|
|
||||||
|
|
||||||
|
from .util import read_body
|
||||||
|
|
||||||
|
_HEXDIG_CODE_POINTS: set[int] = {ord(s.encode("ascii")) for s in hexdigits}
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def method(r1, r2):
|
def method(r1, r2):
|
||||||
assert r1.method == r2.method, "{} != {}".format(r1.method, r2.method)
|
if r1.method != r2.method:
|
||||||
|
raise AssertionError(f"{r1.method} != {r2.method}")
|
||||||
|
|
||||||
|
|
||||||
def uri(r1, r2):
|
def uri(r1, r2):
|
||||||
assert r1.uri == r2.uri, "{} != {}".format(r1.uri, r2.uri)
|
if r1.uri != r2.uri:
|
||||||
|
raise AssertionError(f"{r1.uri} != {r2.uri}")
|
||||||
|
|
||||||
|
|
||||||
def host(r1, r2):
|
def host(r1, r2):
|
||||||
assert r1.host == r2.host, "{} != {}".format(r1.host, r2.host)
|
if r1.host != r2.host:
|
||||||
|
raise AssertionError(f"{r1.host} != {r2.host}")
|
||||||
|
|
||||||
|
|
||||||
def scheme(r1, r2):
|
def scheme(r1, r2):
|
||||||
assert r1.scheme == r2.scheme, "{} != {}".format(r1.scheme, r2.scheme)
|
if r1.scheme != r2.scheme:
|
||||||
|
raise AssertionError(f"{r1.scheme} != {r2.scheme}")
|
||||||
|
|
||||||
|
|
||||||
def port(r1, r2):
|
def port(r1, r2):
|
||||||
assert r1.port == r2.port, "{} != {}".format(r1.port, r2.port)
|
if r1.port != r2.port:
|
||||||
|
raise AssertionError(f"{r1.port} != {r2.port}")
|
||||||
|
|
||||||
|
|
||||||
def path(r1, r2):
|
def path(r1, r2):
|
||||||
assert r1.path == r2.path, "{} != {}".format(r1.path, r2.path)
|
if r1.path != r2.path:
|
||||||
|
raise AssertionError(f"{r1.path} != {r2.path}")
|
||||||
|
|
||||||
|
|
||||||
def query(r1, r2):
|
def query(r1, r2):
|
||||||
assert r1.query == r2.query, "{} != {}".format(r1.query, r2.query)
|
if r1.query != r2.query:
|
||||||
|
raise AssertionError(f"{r1.query} != {r2.query}")
|
||||||
|
|
||||||
|
|
||||||
def raw_body(r1, r2):
|
def raw_body(r1, r2):
|
||||||
assert read_body(r1) == read_body(r2)
|
if read_body(r1) != read_body(r2):
|
||||||
|
raise AssertionError
|
||||||
|
|
||||||
|
|
||||||
def body(r1, r2):
|
def body(r1, r2):
|
||||||
transformer = _get_transformer(r1)
|
transformers = list(_get_transformers(r1))
|
||||||
r2_transformer = _get_transformer(r2)
|
if transformers != list(_get_transformers(r2)):
|
||||||
if transformer != r2_transformer:
|
transformers = []
|
||||||
transformer = _identity
|
|
||||||
assert transformer(read_body(r1)) == transformer(read_body(r2))
|
b1 = read_body(r1)
|
||||||
|
b2 = read_body(r2)
|
||||||
|
for transform in transformers:
|
||||||
|
b1 = transform(b1)
|
||||||
|
b2 = transform(b2)
|
||||||
|
|
||||||
|
if b1 != b2:
|
||||||
|
raise AssertionError
|
||||||
|
|
||||||
|
|
||||||
def headers(r1, r2):
|
def headers(r1, r2):
|
||||||
assert r1.headers == r2.headers, "{} != {}".format(r1.headers, r2.headers)
|
if r1.headers != r2.headers:
|
||||||
|
raise AssertionError(f"{r1.headers} != {r2.headers}")
|
||||||
|
|
||||||
|
|
||||||
def _header_checker(value, header="Content-Type"):
|
def _header_checker(value, header="Content-Type"):
|
||||||
@@ -62,17 +81,71 @@ def _header_checker(value, header="Content-Type"):
|
|||||||
return checker
|
return checker
|
||||||
|
|
||||||
|
|
||||||
|
def _dechunk(body):
|
||||||
|
if isinstance(body, str):
|
||||||
|
body = body.encode("utf-8")
|
||||||
|
elif isinstance(body, bytearray):
|
||||||
|
body = bytes(body)
|
||||||
|
elif hasattr(body, "__iter__"):
|
||||||
|
body = list(body)
|
||||||
|
if body:
|
||||||
|
if isinstance(body[0], str):
|
||||||
|
body = ("".join(body)).encode("utf-8")
|
||||||
|
elif isinstance(body[0], bytes):
|
||||||
|
body = b"".join(body)
|
||||||
|
elif isinstance(body[0], int):
|
||||||
|
body = bytes(body)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Body chunk type {type(body[0])} not supported")
|
||||||
|
else:
|
||||||
|
body = None
|
||||||
|
|
||||||
|
if not isinstance(body, bytes):
|
||||||
|
return body
|
||||||
|
|
||||||
|
# Now decode chunked data format (https://en.wikipedia.org/wiki/Chunked_transfer_encoding)
|
||||||
|
# Example input: b"45\r\n<69 bytes>\r\n0\r\n\r\n" where int(b"45", 16) == 69.
|
||||||
|
CHUNK_GAP = b"\r\n"
|
||||||
|
BODY_LEN: int = len(body)
|
||||||
|
|
||||||
|
chunks: list[bytes] = []
|
||||||
|
pos: int = 0
|
||||||
|
|
||||||
|
while True:
|
||||||
|
for i in range(pos, BODY_LEN):
|
||||||
|
if body[i] not in _HEXDIG_CODE_POINTS:
|
||||||
|
break
|
||||||
|
|
||||||
|
if i == 0 or body[i : i + len(CHUNK_GAP)] != CHUNK_GAP:
|
||||||
|
if pos == 0:
|
||||||
|
return body # i.e. assume non-chunk data
|
||||||
|
raise ValueError("Malformed chunked data")
|
||||||
|
|
||||||
|
size_bytes = int(body[pos:i], 16)
|
||||||
|
if size_bytes == 0: # i.e. well-formed ending
|
||||||
|
return b"".join(chunks)
|
||||||
|
|
||||||
|
chunk_data_first = i + len(CHUNK_GAP)
|
||||||
|
chunk_data_after_last = chunk_data_first + size_bytes
|
||||||
|
|
||||||
|
if body[chunk_data_after_last : chunk_data_after_last + len(CHUNK_GAP)] != CHUNK_GAP:
|
||||||
|
raise ValueError("Malformed chunked data")
|
||||||
|
|
||||||
|
chunk_data = body[chunk_data_first:chunk_data_after_last]
|
||||||
|
chunks.append(chunk_data)
|
||||||
|
|
||||||
|
pos = chunk_data_after_last + len(CHUNK_GAP)
|
||||||
|
|
||||||
|
|
||||||
def _transform_json(body):
|
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).
|
|
||||||
if body:
|
if body:
|
||||||
return json.loads(body.decode("utf-8"))
|
return json.loads(body)
|
||||||
|
|
||||||
|
|
||||||
_xml_header_checker = _header_checker("text/xml")
|
_xml_header_checker = _header_checker("text/xml")
|
||||||
_xmlrpc_header_checker = _header_checker("xmlrpc", header="User-Agent")
|
_xmlrpc_header_checker = _header_checker("xmlrpc", header="User-Agent")
|
||||||
_checker_transformer_pairs = (
|
_checker_transformer_pairs = (
|
||||||
|
(_header_checker("chunked", header="Transfer-Encoding"), _dechunk),
|
||||||
(
|
(
|
||||||
_header_checker("application/x-www-form-urlencoded"),
|
_header_checker("application/x-www-form-urlencoded"),
|
||||||
lambda body: urllib.parse.parse_qs(body.decode("ascii")),
|
lambda body: urllib.parse.parse_qs(body.decode("ascii")),
|
||||||
@@ -82,22 +155,16 @@ _checker_transformer_pairs = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _identity(x):
|
def _get_transformers(request):
|
||||||
return x
|
|
||||||
|
|
||||||
|
|
||||||
def _get_transformer(request):
|
|
||||||
for checker, transformer in _checker_transformer_pairs:
|
for checker, transformer in _checker_transformer_pairs:
|
||||||
if checker(request.headers):
|
if checker(request.headers):
|
||||||
return transformer
|
yield transformer
|
||||||
else:
|
|
||||||
return _identity
|
|
||||||
|
|
||||||
|
|
||||||
def requests_match(r1, r2, matchers):
|
def requests_match(r1, r2, matchers):
|
||||||
successes, failures = get_matchers_results(r1, r2, matchers)
|
_, failures = get_matchers_results(r1, r2, matchers)
|
||||||
if failures:
|
if failures:
|
||||||
log.debug("Requests {} and {} differ.\n" "Failure details:\n" "{}".format(r1, r2, failures))
|
log.debug(f"Requests {r1} and {r2} differ.\nFailure details:\n{failures}")
|
||||||
return len(failures) == 0
|
return len(failures) == 0
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ It merges and deletes the request obsolete keys (protocol, host, port, path)
|
|||||||
into new 'uri' key.
|
into new 'uri' key.
|
||||||
Usage::
|
Usage::
|
||||||
|
|
||||||
python -m vcr.migration PATH
|
python3 -m vcr.migration PATH
|
||||||
|
|
||||||
The PATH can be path to the directory with cassettes or cassette itself
|
The PATH can be path to the directory with cassettes or cassette itself
|
||||||
"""
|
"""
|
||||||
@@ -17,11 +17,12 @@ import os
|
|||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
from .serializers import yamlserializer, jsonserializer
|
|
||||||
from .serialize import serialize
|
|
||||||
from . import request
|
from . import request
|
||||||
|
from .serialize import serialize
|
||||||
|
from .serializers import jsonserializer, yamlserializer
|
||||||
from .stubs.compat import get_httpmessage
|
from .stubs.compat import get_httpmessage
|
||||||
|
|
||||||
# Use the libYAML versions if possible
|
# Use the libYAML versions if possible
|
||||||
@@ -54,7 +55,7 @@ def build_uri(**parts):
|
|||||||
port = parts["port"]
|
port = parts["port"]
|
||||||
scheme = parts["protocol"]
|
scheme = parts["protocol"]
|
||||||
default_port = {"https": 443, "http": 80}[scheme]
|
default_port = {"https": 443, "http": 80}[scheme]
|
||||||
parts["port"] = ":{}".format(port) if port != default_port else ""
|
parts["port"] = f":{port}" if port != default_port else ""
|
||||||
return "{protocol}://{host}{port}{path}".format(**parts)
|
return "{protocol}://{host}{port}{path}".format(**parts)
|
||||||
|
|
||||||
|
|
||||||
@@ -91,7 +92,7 @@ def migrate_json(in_fp, out_fp):
|
|||||||
|
|
||||||
|
|
||||||
def _list_of_tuples_to_dict(fs):
|
def _list_of_tuples_to_dict(fs):
|
||||||
return {k: v for k, v in fs[0]}
|
return dict(fs[0])
|
||||||
|
|
||||||
|
|
||||||
def _already_migrated(data):
|
def _already_migrated(data):
|
||||||
@@ -117,7 +118,7 @@ def migrate(file_path, migration_fn):
|
|||||||
# because we assume that original files can be reverted
|
# because we assume that original files can be reverted
|
||||||
# we will try to copy the content. (os.rename not needed)
|
# we will try to copy the content. (os.rename not needed)
|
||||||
with tempfile.TemporaryFile(mode="w+") as out_fp:
|
with tempfile.TemporaryFile(mode="w+") as out_fp:
|
||||||
with open(file_path, "r") as in_fp:
|
with open(file_path) as in_fp:
|
||||||
if not migration_fn(in_fp, out_fp):
|
if not migration_fn(in_fp, out_fp):
|
||||||
return False
|
return False
|
||||||
with open(file_path, "w") as in_fp:
|
with open(file_path, "w") as in_fp:
|
||||||
@@ -129,7 +130,7 @@ def migrate(file_path, migration_fn):
|
|||||||
def try_migrate(path):
|
def try_migrate(path):
|
||||||
if path.endswith(".json"):
|
if path.endswith(".json"):
|
||||||
return migrate(path, migrate_json)
|
return migrate(path, migrate_json)
|
||||||
elif path.endswith(".yaml") or path.endswith(".yml"):
|
elif path.endswith((".yaml", ".yml")):
|
||||||
return migrate(path, migrate_yml)
|
return migrate(path, migrate_yml)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -137,7 +138,7 @@ def try_migrate(path):
|
|||||||
def main():
|
def main():
|
||||||
if len(sys.argv) != 2:
|
if len(sys.argv) != 2:
|
||||||
raise SystemExit(
|
raise SystemExit(
|
||||||
"Please provide path to cassettes directory or file. " "Usage: python -m vcr.migration PATH"
|
"Please provide path to cassettes directory or file. Usage: python3 -m vcr.migration PATH",
|
||||||
)
|
)
|
||||||
|
|
||||||
path = sys.argv[1]
|
path = sys.argv[1]
|
||||||
@@ -149,7 +150,7 @@ def main():
|
|||||||
for file_path in files:
|
for file_path in files:
|
||||||
migrated = try_migrate(file_path)
|
migrated = try_migrate(file_path)
|
||||||
status = "OK" if migrated else "FAIL"
|
status = "OK" if migrated else "FAIL"
|
||||||
sys.stderr.write("[{}] {}\n".format(status, file_path))
|
sys.stderr.write(f"[{status}] {file_path}\n")
|
||||||
sys.stderr.write("Done.\n")
|
sys.stderr.write("Done.\n")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
209
vcr/patch.py
209
vcr/patch.py
@@ -1,13 +1,13 @@
|
|||||||
"""Utilities for patching in cassettes"""
|
"""Utilities for patching in cassettes"""
|
||||||
|
|
||||||
import contextlib
|
import contextlib
|
||||||
import functools
|
import functools
|
||||||
|
import http.client as httplib
|
||||||
import itertools
|
import itertools
|
||||||
|
import logging
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
from .stubs import VCRHTTPConnection, VCRHTTPSConnection
|
from .stubs import VCRHTTPConnection, VCRHTTPSConnection
|
||||||
import http.client as httplib
|
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
# Save some of the original types for the purposes of unpatching
|
# Save some of the original types for the purposes of unpatching
|
||||||
@@ -16,42 +16,47 @@ _HTTPSConnection = httplib.HTTPSConnection
|
|||||||
|
|
||||||
# Try to save the original types for boto3
|
# Try to save the original types for boto3
|
||||||
try:
|
try:
|
||||||
from botocore.awsrequest import AWSHTTPSConnection, AWSHTTPConnection
|
from botocore.awsrequest import AWSHTTPConnection, AWSHTTPSConnection
|
||||||
except ImportError:
|
except ImportError as e:
|
||||||
try:
|
try:
|
||||||
import botocore.vendored.requests.packages.urllib3.connectionpool as cpool
|
import botocore.vendored.requests # noqa: F401
|
||||||
except ImportError: # pragma: no cover
|
except ImportError: # pragma: no cover
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
_Boto3VerifiedHTTPSConnection = cpool.VerifiedHTTPSConnection
|
raise RuntimeError(
|
||||||
_cpoolBoto3HTTPConnection = cpool.HTTPConnection
|
"vcrpy >=4.2.2 and botocore <1.11.0 are not compatible"
|
||||||
_cpoolBoto3HTTPSConnection = cpool.HTTPSConnection
|
"; please upgrade botocore (or downgrade vcrpy)",
|
||||||
|
) from e
|
||||||
else:
|
else:
|
||||||
_Boto3VerifiedHTTPSConnection = AWSHTTPSConnection
|
_Boto3VerifiedHTTPSConnection = AWSHTTPSConnection
|
||||||
_cpoolBoto3HTTPConnection = AWSHTTPConnection
|
_cpoolBoto3HTTPConnection = AWSHTTPConnection
|
||||||
_cpoolBoto3HTTPSConnection = AWSHTTPSConnection
|
_cpoolBoto3HTTPSConnection = AWSHTTPSConnection
|
||||||
|
|
||||||
cpool = None
|
cpool = None
|
||||||
|
conn = None
|
||||||
# Try to save the original types for urllib3
|
# Try to save the original types for urllib3
|
||||||
try:
|
try:
|
||||||
|
import urllib3.connection as conn
|
||||||
import urllib3.connectionpool as cpool
|
import urllib3.connectionpool as cpool
|
||||||
except ImportError: # pragma: no cover
|
except ImportError: # pragma: no cover
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
_VerifiedHTTPSConnection = cpool.VerifiedHTTPSConnection
|
_VerifiedHTTPSConnection = conn.VerifiedHTTPSConnection
|
||||||
_cpoolHTTPConnection = cpool.HTTPConnection
|
_connHTTPConnection = conn.HTTPConnection
|
||||||
_cpoolHTTPSConnection = cpool.HTTPSConnection
|
_connHTTPSConnection = conn.HTTPSConnection
|
||||||
|
|
||||||
# Try to save the original types for requests
|
# Try to save the original types for requests
|
||||||
try:
|
try:
|
||||||
if not cpool:
|
import requests
|
||||||
import requests.packages.urllib3.connectionpool as cpool
|
|
||||||
except ImportError: # pragma: no cover
|
except ImportError: # pragma: no cover
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
_VerifiedHTTPSConnection = cpool.VerifiedHTTPSConnection
|
if requests.__build__ < 0x021602:
|
||||||
_cpoolHTTPConnection = cpool.HTTPConnection
|
raise RuntimeError(
|
||||||
_cpoolHTTPSConnection = cpool.HTTPSConnection
|
"vcrpy >=4.2.2 and requests <2.16.2 are not compatible"
|
||||||
|
"; please upgrade requests (or downgrade vcrpy)",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# Try to save the original types for httplib2
|
# Try to save the original types for httplib2
|
||||||
try:
|
try:
|
||||||
@@ -63,14 +68,6 @@ else:
|
|||||||
_HTTPSConnectionWithTimeout = httplib2.HTTPSConnectionWithTimeout
|
_HTTPSConnectionWithTimeout = httplib2.HTTPSConnectionWithTimeout
|
||||||
_SCHEME_TO_CONNECTION = httplib2.SCHEME_TO_CONNECTION
|
_SCHEME_TO_CONNECTION = httplib2.SCHEME_TO_CONNECTION
|
||||||
|
|
||||||
# Try to save the original types for boto
|
|
||||||
try:
|
|
||||||
import boto.https_connection
|
|
||||||
except ImportError: # pragma: no cover
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
_CertValidatingHTTPSConnection = boto.https_connection.CertValidatingHTTPSConnection
|
|
||||||
|
|
||||||
# Try to save the original types for Tornado
|
# Try to save the original types for Tornado
|
||||||
try:
|
try:
|
||||||
import tornado.simple_httpclient
|
import tornado.simple_httpclient
|
||||||
@@ -95,12 +92,12 @@ else:
|
|||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import httpx
|
import httpcore
|
||||||
except ImportError: # pragma: no cover
|
except ImportError: # pragma: no cover
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
_HttpxSyncClient_send = httpx.Client.send
|
_HttpcoreConnectionPool_handle_request = httpcore.ConnectionPool.handle_request
|
||||||
_HttpxAsyncClient_send = httpx.AsyncClient.send
|
_HttpcoreAsyncConnectionPool_handle_async_request = httpcore.AsyncConnectionPool.handle_async_request
|
||||||
|
|
||||||
|
|
||||||
class CassettePatcherBuilder:
|
class CassettePatcherBuilder:
|
||||||
@@ -122,10 +119,9 @@ class CassettePatcherBuilder:
|
|||||||
self._boto3(),
|
self._boto3(),
|
||||||
self._urllib3(),
|
self._urllib3(),
|
||||||
self._httplib2(),
|
self._httplib2(),
|
||||||
self._boto(),
|
|
||||||
self._tornado(),
|
self._tornado(),
|
||||||
self._aiohttp(),
|
self._aiohttp(),
|
||||||
self._httpx(),
|
self._httpcore(),
|
||||||
self._build_patchers_from_mock_triples(self._cassette.custom_patches),
|
self._build_patchers_from_mock_triples(self._cassette.custom_patches),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -140,7 +136,9 @@ class CassettePatcherBuilder:
|
|||||||
return
|
return
|
||||||
|
|
||||||
return mock.patch.object(
|
return mock.patch.object(
|
||||||
obj, patched_attribute, self._recursively_apply_get_cassette_subclass(replacement_class)
|
obj,
|
||||||
|
patched_attribute,
|
||||||
|
self._recursively_apply_get_cassette_subclass(replacement_class),
|
||||||
)
|
)
|
||||||
|
|
||||||
def _recursively_apply_get_cassette_subclass(self, replacement_dict_or_obj):
|
def _recursively_apply_get_cassette_subclass(self, replacement_dict_or_obj):
|
||||||
@@ -182,9 +180,7 @@ class CassettePatcherBuilder:
|
|||||||
bases = (base_class,)
|
bases = (base_class,)
|
||||||
if not issubclass(base_class, object): # Check for old style class
|
if not issubclass(base_class, object): # Check for old style class
|
||||||
bases += (object,)
|
bases += (object,)
|
||||||
return type(
|
return type(f"{base_class.__name__}{self._cassette._path}", bases, {"cassette": self._cassette})
|
||||||
"{}{}".format(base_class.__name__, self._cassette._path), bases, dict(cassette=self._cassette)
|
|
||||||
)
|
|
||||||
|
|
||||||
@_build_patchers_from_mock_triples_decorator
|
@_build_patchers_from_mock_triples_decorator
|
||||||
def _httplib(self):
|
def _httplib(self):
|
||||||
@@ -196,24 +192,15 @@ class CassettePatcherBuilder:
|
|||||||
from .stubs import requests_stubs
|
from .stubs import requests_stubs
|
||||||
except ImportError: # pragma: no cover
|
except ImportError: # pragma: no cover
|
||||||
return ()
|
return ()
|
||||||
return self._urllib3_patchers(cpool, requests_stubs)
|
return self._urllib3_patchers(cpool, conn, requests_stubs)
|
||||||
|
|
||||||
@_build_patchers_from_mock_triples_decorator
|
@_build_patchers_from_mock_triples_decorator
|
||||||
def _boto3(self):
|
def _boto3(self):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# botocore using awsrequest
|
# botocore using awsrequest
|
||||||
import botocore.awsrequest as cpool
|
import botocore.awsrequest as cpool
|
||||||
except ImportError: # pragma: no cover
|
except ImportError: # pragma: no cover
|
||||||
try:
|
pass
|
||||||
# botocore using vendored requests
|
|
||||||
import botocore.vendored.requests.packages.urllib3.connectionpool as cpool
|
|
||||||
except ImportError: # pragma: no cover
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
from .stubs import boto3_stubs
|
|
||||||
|
|
||||||
yield self._urllib3_patchers(cpool, boto3_stubs)
|
|
||||||
else:
|
else:
|
||||||
from .stubs import boto3_stubs
|
from .stubs import boto3_stubs
|
||||||
|
|
||||||
@@ -255,12 +242,13 @@ class CassettePatcherBuilder:
|
|||||||
|
|
||||||
def _urllib3(self):
|
def _urllib3(self):
|
||||||
try:
|
try:
|
||||||
|
import urllib3.connection as conn
|
||||||
import urllib3.connectionpool as cpool
|
import urllib3.connectionpool as cpool
|
||||||
except ImportError: # pragma: no cover
|
except ImportError: # pragma: no cover
|
||||||
return ()
|
return ()
|
||||||
from .stubs import urllib3_stubs
|
from .stubs import urllib3_stubs
|
||||||
|
|
||||||
return self._urllib3_patchers(cpool, urllib3_stubs)
|
return self._urllib3_patchers(cpool, conn, urllib3_stubs)
|
||||||
|
|
||||||
@_build_patchers_from_mock_triples_decorator
|
@_build_patchers_from_mock_triples_decorator
|
||||||
def _httplib2(self):
|
def _httplib2(self):
|
||||||
@@ -269,26 +257,18 @@ class CassettePatcherBuilder:
|
|||||||
except ImportError: # pragma: no cover
|
except ImportError: # pragma: no cover
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
from .stubs.httplib2_stubs import VCRHTTPConnectionWithTimeout
|
from .stubs.httplib2_stubs import VCRHTTPConnectionWithTimeout, VCRHTTPSConnectionWithTimeout
|
||||||
from .stubs.httplib2_stubs import VCRHTTPSConnectionWithTimeout
|
|
||||||
|
|
||||||
yield cpool, "HTTPConnectionWithTimeout", VCRHTTPConnectionWithTimeout
|
yield cpool, "HTTPConnectionWithTimeout", VCRHTTPConnectionWithTimeout
|
||||||
yield cpool, "HTTPSConnectionWithTimeout", VCRHTTPSConnectionWithTimeout
|
yield cpool, "HTTPSConnectionWithTimeout", VCRHTTPSConnectionWithTimeout
|
||||||
yield cpool, "SCHEME_TO_CONNECTION", {
|
yield (
|
||||||
"http": VCRHTTPConnectionWithTimeout,
|
cpool,
|
||||||
"https": VCRHTTPSConnectionWithTimeout,
|
"SCHEME_TO_CONNECTION",
|
||||||
}
|
{
|
||||||
|
"http": VCRHTTPConnectionWithTimeout,
|
||||||
@_build_patchers_from_mock_triples_decorator
|
"https": VCRHTTPSConnectionWithTimeout,
|
||||||
def _boto(self):
|
},
|
||||||
try:
|
)
|
||||||
import boto.https_connection as cpool
|
|
||||||
except ImportError: # pragma: no cover
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
from .stubs.boto_stubs import VCRCertValidatingHTTPSConnection
|
|
||||||
|
|
||||||
yield cpool, "CertValidatingHTTPSConnection", VCRCertValidatingHTTPSConnection
|
|
||||||
|
|
||||||
@_build_patchers_from_mock_triples_decorator
|
@_build_patchers_from_mock_triples_decorator
|
||||||
def _tornado(self):
|
def _tornado(self):
|
||||||
@@ -324,31 +304,34 @@ class CassettePatcherBuilder:
|
|||||||
yield client.ClientSession, "_request", new_request
|
yield client.ClientSession, "_request", new_request
|
||||||
|
|
||||||
@_build_patchers_from_mock_triples_decorator
|
@_build_patchers_from_mock_triples_decorator
|
||||||
def _httpx(self):
|
def _httpcore(self):
|
||||||
try:
|
try:
|
||||||
import httpx
|
import httpcore
|
||||||
except ImportError: # pragma: no cover
|
except ImportError: # pragma: no cover
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
from .stubs.httpx_stubs import async_vcr_send, sync_vcr_send
|
from .stubs.httpcore_stubs import vcr_handle_async_request, vcr_handle_request
|
||||||
|
|
||||||
new_async_client_send = async_vcr_send(self._cassette, _HttpxAsyncClient_send)
|
new_handle_async_request = vcr_handle_async_request(
|
||||||
yield httpx.AsyncClient, "send", new_async_client_send
|
self._cassette,
|
||||||
|
_HttpcoreAsyncConnectionPool_handle_async_request,
|
||||||
|
)
|
||||||
|
yield httpcore.AsyncConnectionPool, "handle_async_request", new_handle_async_request
|
||||||
|
|
||||||
new_sync_client_send = sync_vcr_send(self._cassette, _HttpxSyncClient_send)
|
new_handle_request = vcr_handle_request(self._cassette, _HttpcoreConnectionPool_handle_request)
|
||||||
yield httpx.Client, "send", new_sync_client_send
|
yield httpcore.ConnectionPool, "handle_request", new_handle_request
|
||||||
|
|
||||||
def _urllib3_patchers(self, cpool, stubs):
|
def _urllib3_patchers(self, cpool, conn, stubs):
|
||||||
http_connection_remover = ConnectionRemover(
|
http_connection_remover = ConnectionRemover(
|
||||||
self._get_cassette_subclass(stubs.VCRRequestsHTTPConnection)
|
self._get_cassette_subclass(stubs.VCRRequestsHTTPConnection),
|
||||||
)
|
)
|
||||||
https_connection_remover = ConnectionRemover(
|
https_connection_remover = ConnectionRemover(
|
||||||
self._get_cassette_subclass(stubs.VCRRequestsHTTPSConnection)
|
self._get_cassette_subclass(stubs.VCRRequestsHTTPSConnection),
|
||||||
)
|
)
|
||||||
mock_triples = (
|
mock_triples = (
|
||||||
(cpool, "VerifiedHTTPSConnection", stubs.VCRRequestsHTTPSConnection),
|
(conn, "VerifiedHTTPSConnection", stubs.VCRRequestsHTTPSConnection),
|
||||||
(cpool, "HTTPConnection", stubs.VCRRequestsHTTPConnection),
|
(conn, "HTTPConnection", stubs.VCRRequestsHTTPConnection),
|
||||||
(cpool, "HTTPSConnection", stubs.VCRRequestsHTTPSConnection),
|
(conn, "HTTPSConnection", stubs.VCRRequestsHTTPSConnection),
|
||||||
(cpool, "is_connection_dropped", mock.Mock(return_value=False)), # Needed on Windows only
|
(cpool, "is_connection_dropped", mock.Mock(return_value=False)), # Needed on Windows only
|
||||||
(cpool.HTTPConnectionPool, "ConnectionCls", stubs.VCRRequestsHTTPConnection),
|
(cpool.HTTPConnectionPool, "ConnectionCls", stubs.VCRRequestsHTTPConnection),
|
||||||
(cpool.HTTPSConnectionPool, "ConnectionCls", stubs.VCRRequestsHTTPSConnection),
|
(cpool.HTTPSConnectionPool, "ConnectionCls", stubs.VCRRequestsHTTPSConnection),
|
||||||
@@ -393,10 +376,6 @@ class ConnectionRemover:
|
|||||||
if isinstance(connection, self._connection_class):
|
if isinstance(connection, self._connection_class):
|
||||||
self._connection_pool_to_connections.setdefault(pool, set()).add(connection)
|
self._connection_pool_to_connections.setdefault(pool, set()).add(connection)
|
||||||
|
|
||||||
def remove_connection_to_pool_entry(self, pool, connection):
|
|
||||||
if isinstance(connection, self._connection_class):
|
|
||||||
self._connection_pool_to_connections[self._connection_class].remove(connection)
|
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
return self
|
return self
|
||||||
|
|
||||||
@@ -407,10 +386,13 @@ class ConnectionRemover:
|
|||||||
connection = pool.pool.get()
|
connection = pool.pool.get()
|
||||||
if isinstance(connection, self._connection_class):
|
if isinstance(connection, self._connection_class):
|
||||||
connections.remove(connection)
|
connections.remove(connection)
|
||||||
|
connection.close()
|
||||||
else:
|
else:
|
||||||
readd_connections.append(connection)
|
readd_connections.append(connection)
|
||||||
for connection in readd_connections:
|
for connection in readd_connections:
|
||||||
pool._put_conn(connection)
|
pool._put_conn(connection)
|
||||||
|
for connection in connections:
|
||||||
|
connection.close()
|
||||||
|
|
||||||
|
|
||||||
def reset_patchers():
|
def reset_patchers():
|
||||||
@@ -418,69 +400,23 @@ def reset_patchers():
|
|||||||
yield mock.patch.object(httplib, "HTTPSConnection", _HTTPSConnection)
|
yield mock.patch.object(httplib, "HTTPSConnection", _HTTPSConnection)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import requests
|
import urllib3.connection as conn
|
||||||
|
|
||||||
if requests.__build__ < 0x021603:
|
|
||||||
# Avoid double unmock if requests 2.16.3
|
|
||||||
# First, this is pointless, requests.packages.urllib3 *IS* urllib3 (see packages.py)
|
|
||||||
# Second, this is unmocking twice the same classes with different namespaces
|
|
||||||
# and is creating weird issues and bugs:
|
|
||||||
# > AssertionError: assert <class 'urllib3.connection.HTTPConnection'>
|
|
||||||
# > is <class 'requests.packages.urllib3.connection.HTTPConnection'>
|
|
||||||
# This assert should work!!!
|
|
||||||
# Note that this also means that now, requests.packages is never imported
|
|
||||||
# if requests 2.16.3 or greater is used with VCRPy.
|
|
||||||
import requests.packages.urllib3.connectionpool as cpool
|
|
||||||
else:
|
|
||||||
raise ImportError("Skip requests not vendored anymore")
|
|
||||||
except ImportError: # pragma: no cover
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
# unpatch requests v1.x
|
|
||||||
yield mock.patch.object(cpool, "VerifiedHTTPSConnection", _VerifiedHTTPSConnection)
|
|
||||||
yield mock.patch.object(cpool, "HTTPConnection", _cpoolHTTPConnection)
|
|
||||||
# unpatch requests v2.x
|
|
||||||
if hasattr(cpool.HTTPConnectionPool, "ConnectionCls"):
|
|
||||||
yield mock.patch.object(cpool.HTTPConnectionPool, "ConnectionCls", _cpoolHTTPConnection)
|
|
||||||
yield mock.patch.object(cpool.HTTPSConnectionPool, "ConnectionCls", _cpoolHTTPSConnection)
|
|
||||||
|
|
||||||
if hasattr(cpool, "HTTPSConnection"):
|
|
||||||
yield mock.patch.object(cpool, "HTTPSConnection", _cpoolHTTPSConnection)
|
|
||||||
|
|
||||||
try:
|
|
||||||
import urllib3.connectionpool as cpool
|
import urllib3.connectionpool as cpool
|
||||||
except ImportError: # pragma: no cover
|
except ImportError: # pragma: no cover
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
yield mock.patch.object(cpool, "VerifiedHTTPSConnection", _VerifiedHTTPSConnection)
|
yield mock.patch.object(conn, "VerifiedHTTPSConnection", _VerifiedHTTPSConnection)
|
||||||
yield mock.patch.object(cpool, "HTTPConnection", _cpoolHTTPConnection)
|
yield mock.patch.object(conn, "HTTPConnection", _connHTTPConnection)
|
||||||
yield mock.patch.object(cpool, "HTTPSConnection", _cpoolHTTPSConnection)
|
yield mock.patch.object(conn, "HTTPSConnection", _connHTTPSConnection)
|
||||||
if hasattr(cpool.HTTPConnectionPool, "ConnectionCls"):
|
if hasattr(cpool.HTTPConnectionPool, "ConnectionCls"):
|
||||||
yield mock.patch.object(cpool.HTTPConnectionPool, "ConnectionCls", _cpoolHTTPConnection)
|
yield mock.patch.object(cpool.HTTPConnectionPool, "ConnectionCls", _connHTTPConnection)
|
||||||
yield mock.patch.object(cpool.HTTPSConnectionPool, "ConnectionCls", _cpoolHTTPSConnection)
|
yield mock.patch.object(cpool.HTTPSConnectionPool, "ConnectionCls", _connHTTPSConnection)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# unpatch botocore with awsrequest
|
# unpatch botocore with awsrequest
|
||||||
import botocore.awsrequest as cpool
|
import botocore.awsrequest as cpool
|
||||||
except ImportError: # pragma: no cover
|
except ImportError: # pragma: no cover
|
||||||
try:
|
pass
|
||||||
# unpatch botocore with vendored requests
|
|
||||||
import botocore.vendored.requests.packages.urllib3.connectionpool as cpool
|
|
||||||
except ImportError: # pragma: no cover
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
# unpatch requests v1.x
|
|
||||||
yield mock.patch.object(cpool, "VerifiedHTTPSConnection", _Boto3VerifiedHTTPSConnection)
|
|
||||||
yield mock.patch.object(cpool, "HTTPConnection", _cpoolBoto3HTTPConnection)
|
|
||||||
# unpatch requests v2.x
|
|
||||||
if hasattr(cpool.HTTPConnectionPool, "ConnectionCls"):
|
|
||||||
yield mock.patch.object(cpool.HTTPConnectionPool, "ConnectionCls", _cpoolBoto3HTTPConnection)
|
|
||||||
yield mock.patch.object(
|
|
||||||
cpool.HTTPSConnectionPool, "ConnectionCls", _cpoolBoto3HTTPSConnection
|
|
||||||
)
|
|
||||||
|
|
||||||
if hasattr(cpool, "HTTPSConnection"):
|
|
||||||
yield mock.patch.object(cpool, "HTTPSConnection", _cpoolBoto3HTTPSConnection)
|
|
||||||
else:
|
else:
|
||||||
if hasattr(cpool.AWSHTTPConnectionPool, "ConnectionCls"):
|
if hasattr(cpool.AWSHTTPConnectionPool, "ConnectionCls"):
|
||||||
yield mock.patch.object(cpool.AWSHTTPConnectionPool, "ConnectionCls", _cpoolBoto3HTTPConnection)
|
yield mock.patch.object(cpool.AWSHTTPConnectionPool, "ConnectionCls", _cpoolBoto3HTTPConnection)
|
||||||
@@ -498,13 +434,6 @@ def reset_patchers():
|
|||||||
yield mock.patch.object(cpool, "HTTPSConnectionWithTimeout", _HTTPSConnectionWithTimeout)
|
yield mock.patch.object(cpool, "HTTPSConnectionWithTimeout", _HTTPSConnectionWithTimeout)
|
||||||
yield mock.patch.object(cpool, "SCHEME_TO_CONNECTION", _SCHEME_TO_CONNECTION)
|
yield mock.patch.object(cpool, "SCHEME_TO_CONNECTION", _SCHEME_TO_CONNECTION)
|
||||||
|
|
||||||
try:
|
|
||||||
import boto.https_connection as cpool
|
|
||||||
except ImportError: # pragma: no cover
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
yield mock.patch.object(cpool, "CertValidatingHTTPSConnection", _CertValidatingHTTPSConnection)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import tornado.simple_httpclient as simple
|
import tornado.simple_httpclient as simple
|
||||||
except ImportError: # pragma: no cover
|
except ImportError: # pragma: no cover
|
||||||
|
|||||||
@@ -1,25 +1,40 @@
|
|||||||
# .. _persister_example:
|
# .. _persister_example:
|
||||||
|
|
||||||
import os
|
from pathlib import Path
|
||||||
from ..serialize import serialize, deserialize
|
|
||||||
|
from ..serialize import deserialize, serialize
|
||||||
|
|
||||||
|
|
||||||
|
class CassetteNotFoundError(FileNotFoundError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class CassetteDecodeError(ValueError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class FilesystemPersister:
|
class FilesystemPersister:
|
||||||
@classmethod
|
@classmethod
|
||||||
def load_cassette(cls, cassette_path, serializer):
|
def load_cassette(cls, cassette_path, serializer):
|
||||||
|
cassette_path = Path(cassette_path) # if cassette path is already Path this is no operation
|
||||||
|
if not cassette_path.is_file():
|
||||||
|
raise CassetteNotFoundError()
|
||||||
try:
|
try:
|
||||||
with open(cassette_path) as f:
|
with cassette_path.open() as f:
|
||||||
cassette_content = f.read()
|
data = f.read()
|
||||||
except OSError:
|
except UnicodeDecodeError as err:
|
||||||
raise ValueError("Cassette not found.")
|
raise CassetteDecodeError("Can't read Cassette, Encoding is broken") from err
|
||||||
cassette = deserialize(cassette_content, serializer)
|
|
||||||
return cassette
|
return deserialize(data, serializer)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def save_cassette(cassette_path, cassette_dict, serializer):
|
def save_cassette(cassette_path, cassette_dict, serializer):
|
||||||
data = serialize(cassette_dict, serializer)
|
data = serialize(cassette_dict, serializer)
|
||||||
dirname, filename = os.path.split(cassette_path)
|
cassette_path = Path(cassette_path) # if cassette path is already Path this is no operation
|
||||||
if dirname and not os.path.exists(dirname):
|
|
||||||
os.makedirs(dirname)
|
cassette_folder = cassette_path.parent
|
||||||
with open(cassette_path, "w") as f:
|
if not cassette_folder.exists():
|
||||||
|
cassette_folder.mkdir(parents=True)
|
||||||
|
|
||||||
|
with cassette_path.open("w") as f:
|
||||||
f.write(data)
|
f.write(data)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from enum import Enum
|
|||||||
|
|
||||||
class RecordMode(str, 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`)
|
Can be declared by either using the enumerated value (`vcr.mode.ONCE`)
|
||||||
or by simply using the defined string (`once`).
|
or by simply using the defined string (`once`).
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import warnings
|
|
||||||
from io import BytesIO
|
|
||||||
from urllib.parse import urlparse, parse_qsl
|
|
||||||
from .util import CaseInsensitiveDict
|
|
||||||
import logging
|
import logging
|
||||||
|
import warnings
|
||||||
|
from contextlib import suppress
|
||||||
|
from io import BytesIO
|
||||||
|
from urllib.parse import parse_qsl, urlparse
|
||||||
|
|
||||||
|
from .util import CaseInsensitiveDict, _is_nonsequence_iterator
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -16,13 +18,25 @@ class Request:
|
|||||||
self.method = method
|
self.method = method
|
||||||
self.uri = uri
|
self.uri = uri
|
||||||
self._was_file = hasattr(body, "read")
|
self._was_file = hasattr(body, "read")
|
||||||
|
self._was_iter = _is_nonsequence_iterator(body)
|
||||||
if self._was_file:
|
if self._was_file:
|
||||||
self.body = body.read()
|
self.body = body.read()
|
||||||
|
elif self._was_iter:
|
||||||
|
self.body = list(body)
|
||||||
else:
|
else:
|
||||||
self.body = body
|
self.body = body
|
||||||
self.headers = headers
|
self.headers = headers
|
||||||
log.debug("Invoking Request %s", self.uri)
|
log.debug("Invoking Request %s", self.uri)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def uri(self):
|
||||||
|
return self._uri
|
||||||
|
|
||||||
|
@uri.setter
|
||||||
|
def uri(self, uri):
|
||||||
|
self._uri = uri
|
||||||
|
self.parsed_uri = urlparse(uri)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def headers(self):
|
def headers(self):
|
||||||
return self._headers
|
return self._headers
|
||||||
@@ -35,7 +49,11 @@ class Request:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def body(self):
|
def body(self):
|
||||||
return BytesIO(self._body) if self._was_file else self._body
|
if self._was_file:
|
||||||
|
return BytesIO(self._body)
|
||||||
|
if self._was_iter:
|
||||||
|
return iter(self._body)
|
||||||
|
return self._body
|
||||||
|
|
||||||
@body.setter
|
@body.setter
|
||||||
def body(self, value):
|
def body(self, value):
|
||||||
@@ -45,37 +63,36 @@ class Request:
|
|||||||
|
|
||||||
def add_header(self, key, value):
|
def add_header(self, key, value):
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
"Request.add_header is deprecated. " "Please assign to request.headers instead.",
|
"Request.add_header is deprecated. Please assign to request.headers instead.",
|
||||||
DeprecationWarning,
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
)
|
)
|
||||||
self.headers[key] = value
|
self.headers[key] = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def scheme(self):
|
def scheme(self):
|
||||||
return urlparse(self.uri).scheme
|
return self.parsed_uri.scheme
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def host(self):
|
def host(self):
|
||||||
return urlparse(self.uri).hostname
|
return self.parsed_uri.hostname
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def port(self):
|
def port(self):
|
||||||
parse_uri = urlparse(self.uri)
|
port = self.parsed_uri.port
|
||||||
port = parse_uri.port
|
|
||||||
if port is None:
|
if port is None:
|
||||||
try:
|
with suppress(KeyError):
|
||||||
port = {"https": 443, "http": 80}[parse_uri.scheme]
|
port = {"https": 443, "http": 80}[self.parsed_uri.scheme]
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
return port
|
return port
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def path(self):
|
def path(self):
|
||||||
return urlparse(self.uri).path
|
return self.parsed_uri.path
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def query(self):
|
def query(self):
|
||||||
q = urlparse(self.uri).query
|
q = self.parsed_uri.query
|
||||||
return sorted(parse_qsl(q))
|
return sorted(parse_qsl(q))
|
||||||
|
|
||||||
# alias for backwards compatibility
|
# alias for backwards compatibility
|
||||||
@@ -89,7 +106,7 @@ class Request:
|
|||||||
return self.scheme
|
return self.scheme
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "<Request ({}) {}>".format(self.method, self.uri)
|
return f"<Request ({self.method}) {self.uri}>"
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return self.__str__()
|
return self.__str__()
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
from vcr.serializers import compat
|
|
||||||
from vcr.request import Request
|
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
|
from vcr.request import Request
|
||||||
|
from vcr.serializers import compat
|
||||||
|
|
||||||
# version 1 cassettes started with VCR 1.0.x.
|
# version 1 cassettes started with VCR 1.0.x.
|
||||||
# Before 1.0.x, there was no versioning.
|
# Before 1.0.x, there was no versioning.
|
||||||
CASSETTE_FORMAT_VERSION = 1
|
CASSETTE_FORMAT_VERSION = 1
|
||||||
@@ -27,7 +28,7 @@ def _warn_about_old_cassette_format():
|
|||||||
raise ValueError(
|
raise ValueError(
|
||||||
"Your cassette files were generated in an older version "
|
"Your cassette files were generated in an older version "
|
||||||
"of VCR. Delete your cassettes or run the migration script."
|
"of VCR. Delete your cassettes or run the migration script."
|
||||||
"See http://git.io/mHhLBg for more details."
|
"See http://git.io/mHhLBg for more details.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -52,7 +53,7 @@ def serialize(cassette_dict, serializer):
|
|||||||
"request": compat.convert_to_unicode(request._to_dict()),
|
"request": compat.convert_to_unicode(request._to_dict()),
|
||||||
"response": compat.convert_to_unicode(response),
|
"response": compat.convert_to_unicode(response),
|
||||||
}
|
}
|
||||||
for request, response in zip(cassette_dict["requests"], cassette_dict["responses"])
|
for request, response in zip(cassette_dict["requests"], cassette_dict["responses"], strict=False)
|
||||||
]
|
]
|
||||||
data = {"version": CASSETTE_FORMAT_VERSION, "interactions": interactions}
|
data = {"version": CASSETTE_FORMAT_VERSION, "interactions": interactions}
|
||||||
return serializer.serialize(data)
|
return serializer.serialize(data)
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ def convert_body_to_unicode(resp):
|
|||||||
If the request or responses body is bytes, decode it to a string
|
If the request or responses body is bytes, decode it to a string
|
||||||
(for python3 support)
|
(for python3 support)
|
||||||
"""
|
"""
|
||||||
if type(resp) is not dict:
|
if not isinstance(resp, dict):
|
||||||
# Some of the tests just serialize and deserialize a string.
|
# Some of the tests just serialize and deserialize a string.
|
||||||
return _convert_string_to_unicode(resp)
|
return _convert_string_to_unicode(resp)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
try:
|
import json
|
||||||
import simplejson as json
|
|
||||||
except ImportError:
|
|
||||||
import json
|
|
||||||
|
|
||||||
|
|
||||||
def deserialize(cassette_string):
|
def deserialize(cassette_string):
|
||||||
@@ -16,14 +13,6 @@ def serialize(cassette_dict):
|
|||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return json.dumps(cassette_dict, indent=4)
|
return json.dumps(cassette_dict, indent=4) + "\n"
|
||||||
except UnicodeDecodeError as original: # py2
|
except TypeError:
|
||||||
raise UnicodeDecodeError(
|
raise TypeError(error_message) from None
|
||||||
original.encoding,
|
|
||||||
b"Error serializing cassette to JSON",
|
|
||||||
original.start,
|
|
||||||
original.end,
|
|
||||||
original.args[-1] + error_message,
|
|
||||||
)
|
|
||||||
except TypeError: # py3
|
|
||||||
raise TypeError(error_message)
|
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ import yaml
|
|||||||
|
|
||||||
# Use the libYAML versions if possible
|
# Use the libYAML versions if possible
|
||||||
try:
|
try:
|
||||||
from yaml import CLoader as Loader, CDumper as Dumper
|
from yaml import CDumper as Dumper
|
||||||
|
from yaml import CLoader as Loader
|
||||||
except ImportError:
|
except ImportError:
|
||||||
from yaml import Loader, Dumper
|
from yaml import Dumper, Loader
|
||||||
|
|
||||||
|
|
||||||
def deserialize(cassette_string):
|
def deserialize(cassette_string):
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
"""Stubs for patching HTTP and HTTPS requests"""
|
"""Stubs for patching HTTP and HTTPS requests"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from http.client import (
|
from contextlib import suppress
|
||||||
HTTPConnection,
|
from http.client import HTTPConnection, HTTPResponse, HTTPSConnection
|
||||||
HTTPSConnection,
|
|
||||||
HTTPResponse,
|
|
||||||
)
|
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from vcr.request import Request
|
|
||||||
from vcr.errors import CannotOverwriteExistingCassetteException
|
from vcr.errors import CannotOverwriteExistingCassetteException
|
||||||
|
from vcr.request import Request
|
||||||
|
|
||||||
from . import compat
|
from . import compat
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
@@ -49,8 +48,9 @@ def parse_headers(header_list):
|
|||||||
|
|
||||||
|
|
||||||
def serialize_headers(response):
|
def serialize_headers(response):
|
||||||
|
headers = response.headers if response.msg is None else response.msg
|
||||||
out = {}
|
out = {}
|
||||||
for key, values in compat.get_headers(response.msg):
|
for key, values in compat.get_headers(headers):
|
||||||
out.setdefault(key, [])
|
out.setdefault(key, [])
|
||||||
out[key].extend(values)
|
out[key].extend(values)
|
||||||
return out
|
return out
|
||||||
@@ -67,8 +67,10 @@ class VCRHTTPResponse(HTTPResponse):
|
|||||||
self.reason = recorded_response["status"]["message"]
|
self.reason = recorded_response["status"]["message"]
|
||||||
self.status = self.code = recorded_response["status"]["code"]
|
self.status = self.code = recorded_response["status"]["code"]
|
||||||
self.version = None
|
self.version = None
|
||||||
|
self.version_string = None
|
||||||
self._content = BytesIO(self.recorded_response["body"]["string"])
|
self._content = BytesIO(self.recorded_response["body"]["string"])
|
||||||
self._closed = False
|
self._closed = False
|
||||||
|
self._original_response = self # for requests.session.Session cookie extraction
|
||||||
|
|
||||||
headers = self.recorded_response["headers"]
|
headers = self.recorded_response["headers"]
|
||||||
# Since we are loading a response that has already been serialized, our
|
# Since we are loading a response that has already been serialized, our
|
||||||
@@ -76,7 +78,7 @@ class VCRHTTPResponse(HTTPResponse):
|
|||||||
# libraries trying to process a chunked response. By removing the
|
# libraries trying to process a chunked response. By removing the
|
||||||
# transfer-encoding: chunked header, this should cause the downstream
|
# transfer-encoding: chunked header, this should cause the downstream
|
||||||
# libraries to process this as a non-chunked response.
|
# libraries to process this as a non-chunked response.
|
||||||
te_key = [h for h in headers.keys() if h.upper() == "TRANSFER-ENCODING"]
|
te_key = [h for h in headers if h.upper() == "TRANSFER-ENCODING"]
|
||||||
if te_key:
|
if te_key:
|
||||||
del headers[te_key[0]]
|
del headers[te_key[0]]
|
||||||
self.headers = self.msg = parse_headers(headers)
|
self.headers = self.msg = parse_headers(headers)
|
||||||
@@ -87,12 +89,15 @@ class VCRHTTPResponse(HTTPResponse):
|
|||||||
def closed(self):
|
def closed(self):
|
||||||
# in python3, I can't change the value of self.closed. So I'
|
# in python3, I can't change the value of self.closed. So I'
|
||||||
# twiddling self._closed and using this property to shadow the real
|
# twiddling self._closed and using this property to shadow the real
|
||||||
# self.closed from the superclas
|
# self.closed from the superclass
|
||||||
return self._closed
|
return self._closed
|
||||||
|
|
||||||
def read(self, *args, **kwargs):
|
def read(self, *args, **kwargs):
|
||||||
return self._content.read(*args, **kwargs)
|
return self._content.read(*args, **kwargs)
|
||||||
|
|
||||||
|
def read1(self, *args, **kwargs):
|
||||||
|
return self._content.read1(*args, **kwargs)
|
||||||
|
|
||||||
def readall(self):
|
def readall(self):
|
||||||
return self._content.readall()
|
return self._content.readall()
|
||||||
|
|
||||||
@@ -145,6 +150,35 @@ class VCRHTTPResponse(HTTPResponse):
|
|||||||
def readable(self):
|
def readable(self):
|
||||||
return self._content.readable()
|
return self._content.readable()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def length_remaining(self):
|
||||||
|
return self._content.getbuffer().nbytes - self._content.tell()
|
||||||
|
|
||||||
|
def get_redirect_location(self):
|
||||||
|
"""
|
||||||
|
Returns (a) redirect location string if we got a redirect
|
||||||
|
status code and valid location, (b) None if redirect status and
|
||||||
|
no location, (c) False if not a redirect status code.
|
||||||
|
See https://urllib3.readthedocs.io/en/stable/reference/urllib3.response.html .
|
||||||
|
"""
|
||||||
|
if not (300 <= self.status <= 399):
|
||||||
|
return False
|
||||||
|
return self.getheader("Location")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def data(self):
|
||||||
|
return self._content.getbuffer().tobytes()
|
||||||
|
|
||||||
|
def drain_conn(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def stream(self, amt=65536, decode_content=None):
|
||||||
|
while True:
|
||||||
|
b = self._content.read(amt)
|
||||||
|
yield b
|
||||||
|
if not b:
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
class VCRConnection:
|
class VCRConnection:
|
||||||
# A reference to the cassette that's currently being patched in
|
# A reference to the cassette that's currently being patched in
|
||||||
@@ -154,28 +188,40 @@ class VCRConnection:
|
|||||||
"""
|
"""
|
||||||
Returns empty string for the default port and ':port' otherwise
|
Returns empty string for the default port and ':port' otherwise
|
||||||
"""
|
"""
|
||||||
port = self.real_connection.port
|
port = (
|
||||||
|
self.real_connection.port
|
||||||
|
if not self.real_connection._tunnel_host
|
||||||
|
else self.real_connection._tunnel_port
|
||||||
|
)
|
||||||
default_port = {"https": 443, "http": 80}[self._protocol]
|
default_port = {"https": 443, "http": 80}[self._protocol]
|
||||||
return ":{}".format(port) if port != default_port else ""
|
return f":{port}" if port != default_port else ""
|
||||||
|
|
||||||
|
def _real_host(self):
|
||||||
|
"""Returns the request host"""
|
||||||
|
if self.real_connection._tunnel_host:
|
||||||
|
# The real connection is to an HTTPS proxy
|
||||||
|
return self.real_connection._tunnel_host
|
||||||
|
else:
|
||||||
|
return self.real_connection.host
|
||||||
|
|
||||||
def _uri(self, url):
|
def _uri(self, url):
|
||||||
"""Returns request absolute URI"""
|
"""Returns request absolute URI"""
|
||||||
if url and not url.startswith("/"):
|
if url and not url.startswith("/"):
|
||||||
# Then this must be a proxy request.
|
# Then this must be a proxy request.
|
||||||
return url
|
return url
|
||||||
uri = "{}://{}{}{}".format(self._protocol, self.real_connection.host, self._port_postfix(), url)
|
uri = f"{self._protocol}://{self._real_host()}{self._port_postfix()}{url}"
|
||||||
log.debug("Absolute URI: %s", uri)
|
log.debug("Absolute URI: %s", uri)
|
||||||
return uri
|
return uri
|
||||||
|
|
||||||
def _url(self, uri):
|
def _url(self, uri):
|
||||||
"""Returns request selector url from absolute URI"""
|
"""Returns request selector url from absolute URI"""
|
||||||
prefix = "{}://{}{}".format(self._protocol, self.real_connection.host, self._port_postfix())
|
prefix = f"{self._protocol}://{self._real_host()}{self._port_postfix()}"
|
||||||
return uri.replace(prefix, "", 1)
|
return uri.replace(prefix, "", 1)
|
||||||
|
|
||||||
def request(self, method, url, body=None, headers=None, *args, **kwargs):
|
def request(self, method, url, body=None, headers=None, *args, **kwargs):
|
||||||
"""Persist the request metadata in self._vcr_request"""
|
"""Persist the request metadata in self._vcr_request"""
|
||||||
self._vcr_request = Request(method=method, uri=self._uri(url), body=body, headers=headers or {})
|
self._vcr_request = Request(method=method, uri=self._uri(url), body=body, headers=headers or {})
|
||||||
log.debug("Got {}".format(self._vcr_request))
|
log.debug(f"Got {self._vcr_request}")
|
||||||
|
|
||||||
# Note: The request may not actually be finished at this point, so
|
# Note: The request may not actually be finished at this point, so
|
||||||
# I'm not sending the actual request until getresponse(). This
|
# I'm not sending the actual request until getresponse(). This
|
||||||
@@ -191,7 +237,7 @@ class VCRConnection:
|
|||||||
of putheader() calls.
|
of putheader() calls.
|
||||||
"""
|
"""
|
||||||
self._vcr_request = Request(method=method, uri=self._uri(url), body="", headers={})
|
self._vcr_request = Request(method=method, uri=self._uri(url), body="", headers={})
|
||||||
log.debug("Got {}".format(self._vcr_request))
|
log.debug(f"Got {self._vcr_request}")
|
||||||
|
|
||||||
def putheader(self, header, *values):
|
def putheader(self, header, *values):
|
||||||
self._vcr_request.headers[header] = values
|
self._vcr_request.headers[header] = values
|
||||||
@@ -223,19 +269,20 @@ class VCRConnection:
|
|||||||
# Check to see if the cassette has a response for this request. If so,
|
# Check to see if the cassette has a response for this request. If so,
|
||||||
# then return it
|
# then return it
|
||||||
if self.cassette.can_play_response_for(self._vcr_request):
|
if self.cassette.can_play_response_for(self._vcr_request):
|
||||||
log.info("Playing response for {} from cassette".format(self._vcr_request))
|
log.info(f"Playing response for {self._vcr_request} from cassette")
|
||||||
response = self.cassette.play_response(self._vcr_request)
|
response = self.cassette.play_response(self._vcr_request)
|
||||||
return VCRHTTPResponse(response)
|
return VCRHTTPResponse(response)
|
||||||
else:
|
else:
|
||||||
if self.cassette.write_protected and self.cassette.filter_request(self._vcr_request):
|
if self.cassette.write_protected and self.cassette.filter_request(self._vcr_request):
|
||||||
raise CannotOverwriteExistingCassetteException(
|
raise CannotOverwriteExistingCassetteException(
|
||||||
cassette=self.cassette, failed_request=self._vcr_request
|
cassette=self.cassette,
|
||||||
|
failed_request=self._vcr_request,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Otherwise, we should send the request, then get the response
|
# Otherwise, we should send the request, then get the response
|
||||||
# and return it.
|
# and return it.
|
||||||
|
|
||||||
log.info("{} not in cassette, sending to real server".format(self._vcr_request))
|
log.info(f"{self._vcr_request} not in cassette, sending to real server")
|
||||||
# This is imported here to avoid circular import.
|
# This is imported here to avoid circular import.
|
||||||
# TODO(@IvanMalison): Refactor to allow normal import.
|
# TODO(@IvanMalison): Refactor to allow normal import.
|
||||||
from vcr.patch import force_reset
|
from vcr.patch import force_reset
|
||||||
@@ -250,12 +297,13 @@ class VCRConnection:
|
|||||||
|
|
||||||
# get the response
|
# get the response
|
||||||
response = self.real_connection.getresponse()
|
response = self.real_connection.getresponse()
|
||||||
|
response_data = response.data if hasattr(response, "data") else response.read()
|
||||||
|
|
||||||
# put the response into the cassette
|
# put the response into the cassette
|
||||||
response = {
|
response = {
|
||||||
"status": {"code": response.status, "message": response.reason},
|
"status": {"code": response.status, "message": response.reason},
|
||||||
"headers": serialize_headers(response),
|
"headers": serialize_headers(response),
|
||||||
"body": {"string": response.read()},
|
"body": {"string": response_data},
|
||||||
}
|
}
|
||||||
self.cassette.append(self._vcr_request, response)
|
self.cassette.append(self._vcr_request, response)
|
||||||
return VCRHTTPResponse(response)
|
return VCRHTTPResponse(response)
|
||||||
@@ -314,7 +362,7 @@ class VCRConnection:
|
|||||||
def __setattr__(self, name, value):
|
def __setattr__(self, name, value):
|
||||||
"""
|
"""
|
||||||
We need to define this because any attributes that are set on the
|
We need to define this because any attributes that are set on the
|
||||||
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,
|
For example, urllib3 will set certain attributes on the connection,
|
||||||
such as 'ssl_version'. These attributes need to get set on the real
|
such as 'ssl_version'. These attributes need to get set on the real
|
||||||
@@ -323,12 +371,8 @@ class VCRConnection:
|
|||||||
TODO: Separately setting the attribute on the two instances is not
|
TODO: Separately setting the attribute on the two instances is not
|
||||||
ideal. We should switch to a proxying implementation.
|
ideal. We should switch to a proxying implementation.
|
||||||
"""
|
"""
|
||||||
try:
|
with suppress(AttributeError):
|
||||||
setattr(self.real_connection, name, value)
|
setattr(self.real_connection, name, value)
|
||||||
except AttributeError:
|
|
||||||
# raised if real_connection has not been set yet, such as when
|
|
||||||
# we're setting the real_connection itself for the first time
|
|
||||||
pass
|
|
||||||
|
|
||||||
super().__setattr__(name, value)
|
super().__setattr__(name, value)
|
||||||
|
|
||||||
@@ -355,6 +399,8 @@ class VCRHTTPConnection(VCRConnection):
|
|||||||
|
|
||||||
_baseclass = HTTPConnection
|
_baseclass = HTTPConnection
|
||||||
_protocol = "http"
|
_protocol = "http"
|
||||||
|
debuglevel = _baseclass.debuglevel
|
||||||
|
_http_vsn = _baseclass._http_vsn
|
||||||
|
|
||||||
|
|
||||||
class VCRHTTPSConnection(VCRConnection):
|
class VCRHTTPSConnection(VCRConnection):
|
||||||
@@ -363,3 +409,5 @@ class VCRHTTPSConnection(VCRConnection):
|
|||||||
_baseclass = HTTPSConnection
|
_baseclass = HTTPSConnection
|
||||||
_protocol = "https"
|
_protocol = "https"
|
||||||
is_verified = True
|
is_verified = True
|
||||||
|
debuglevel = _baseclass.debuglevel
|
||||||
|
_http_vsn = _baseclass._http_vsn
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
"""Stubs for aiohttp HTTP clients"""
|
"""Stubs for aiohttp HTTP clients"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import functools
|
import functools
|
||||||
import logging
|
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
from aiohttp import ClientConnectionError, ClientResponse, RequestInfo, streams
|
from collections.abc import Mapping
|
||||||
from aiohttp import hdrs, CookieJar
|
|
||||||
from http.cookies import CookieError, Morsel, SimpleCookie
|
from http.cookies import CookieError, Morsel, SimpleCookie
|
||||||
|
|
||||||
|
from aiohttp import ClientConnectionError, ClientResponse, CookieJar, RequestInfo, hdrs, streams
|
||||||
from aiohttp.helpers import strip_auth_from_url
|
from aiohttp.helpers import strip_auth_from_url
|
||||||
from multidict import CIMultiDict, CIMultiDictProxy
|
from multidict import CIMultiDict, CIMultiDictProxy, MultiDict
|
||||||
from yarl import URL
|
from yarl import URL
|
||||||
|
|
||||||
from vcr.errors import CannotOverwriteExistingCassetteException
|
from vcr.errors import CannotOverwriteExistingCassetteException
|
||||||
@@ -35,7 +36,7 @@ class MockClientResponse(ClientResponse):
|
|||||||
session=None,
|
session=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def json(self, *, encoding="utf-8", loads=json.loads, **kwargs): # NOQA: E999
|
async def json(self, *, encoding="utf-8", loads=json.loads, **kwargs):
|
||||||
stripped = self._body.strip()
|
stripped = self._body.strip()
|
||||||
if not stripped:
|
if not stripped:
|
||||||
return None
|
return None
|
||||||
@@ -66,7 +67,7 @@ def build_response(vcr_request, vcr_response, history):
|
|||||||
headers=_deserialize_headers(vcr_request.headers),
|
headers=_deserialize_headers(vcr_request.headers),
|
||||||
real_url=URL(vcr_request.url),
|
real_url=URL(vcr_request.url),
|
||||||
)
|
)
|
||||||
response = MockClientResponse(vcr_request.method, URL(vcr_response.get("url")), request_info=request_info)
|
response = MockClientResponse(vcr_request.method, URL(vcr_request.url), request_info=request_info)
|
||||||
response.status = vcr_response["status"]["code"]
|
response.status = vcr_response["status"]["code"]
|
||||||
response._body = vcr_response["body"].get("string", b"")
|
response._body = vcr_response["body"].get("string", b"")
|
||||||
response.reason = vcr_response["status"]["message"]
|
response.reason = vcr_response["status"]["message"]
|
||||||
@@ -91,9 +92,9 @@ def build_response(vcr_request, vcr_response, history):
|
|||||||
|
|
||||||
def _serialize_headers(headers):
|
def _serialize_headers(headers):
|
||||||
"""Serialize CIMultiDictProxy to a pickle-able dict because proxy
|
"""Serialize CIMultiDictProxy to a pickle-able dict because proxy
|
||||||
objects forbid pickling:
|
objects forbid pickling:
|
||||||
|
|
||||||
https://github.com/aio-libs/multidict/issues/340
|
https://github.com/aio-libs/multidict/issues/340
|
||||||
"""
|
"""
|
||||||
# Mark strings as keys so 'istr' types don't show up in
|
# Mark strings as keys so 'istr' types don't show up in
|
||||||
# the cassettes as comments.
|
# the cassettes as comments.
|
||||||
@@ -116,14 +117,15 @@ def _deserialize_headers(headers):
|
|||||||
return CIMultiDictProxy(deserialized_headers)
|
return CIMultiDictProxy(deserialized_headers)
|
||||||
|
|
||||||
|
|
||||||
def play_responses(cassette, vcr_request):
|
def play_responses(cassette, vcr_request, kwargs):
|
||||||
history = []
|
history = []
|
||||||
|
allow_redirects = kwargs.get("allow_redirects", True)
|
||||||
vcr_response = cassette.play_response(vcr_request)
|
vcr_response = cassette.play_response(vcr_request)
|
||||||
response = build_response(vcr_request, vcr_response, history)
|
response = build_response(vcr_request, vcr_response, history)
|
||||||
|
|
||||||
# If we're following redirects, continue playing until we reach
|
# If we're following redirects, continue playing until we reach
|
||||||
# our final destination.
|
# our final destination.
|
||||||
while 300 <= response.status <= 399:
|
while allow_redirects and 300 <= response.status <= 399:
|
||||||
if "location" not in response.headers:
|
if "location" not in response.headers:
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -161,8 +163,7 @@ async def record_response(cassette, vcr_request, response):
|
|||||||
vcr_response = {
|
vcr_response = {
|
||||||
"status": {"code": response.status, "message": response.reason},
|
"status": {"code": response.status, "message": response.reason},
|
||||||
"headers": _serialize_headers(response.headers),
|
"headers": _serialize_headers(response.headers),
|
||||||
"body": body, # NOQA: E999
|
"body": body,
|
||||||
"url": str(response.url),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cassette.append(vcr_request, vcr_response)
|
cassette.append(vcr_request, vcr_response)
|
||||||
@@ -170,19 +171,19 @@ async def record_response(cassette, vcr_request, response):
|
|||||||
|
|
||||||
async def record_responses(cassette, vcr_request, response):
|
async def record_responses(cassette, vcr_request, response):
|
||||||
"""Because aiohttp follows redirects by default, we must support
|
"""Because aiohttp follows redirects by default, we must support
|
||||||
them by default. This method is used to write individual
|
them by default. This method is used to write individual
|
||||||
request-response chains that were implicitly followed to get
|
request-response chains that were implicitly followed to get
|
||||||
to the final destination.
|
to the final destination.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
for past_response in response.history:
|
for i, past_response in enumerate(response.history):
|
||||||
aiohttp_request = past_response.request_info
|
aiohttp_request = past_response.request_info
|
||||||
|
|
||||||
# No data because it's following a redirect.
|
|
||||||
past_request = Request(
|
past_request = Request(
|
||||||
aiohttp_request.method,
|
aiohttp_request.method,
|
||||||
str(aiohttp_request.url),
|
str(aiohttp_request.url),
|
||||||
None,
|
# Record body of first request, rest are following a redirect.
|
||||||
|
None if i else vcr_request.body,
|
||||||
_serialize_headers(aiohttp_request.headers),
|
_serialize_headers(aiohttp_request.headers),
|
||||||
)
|
)
|
||||||
await record_response(cassette, past_request, past_response)
|
await record_response(cassette, past_request, past_response)
|
||||||
@@ -228,6 +229,16 @@ def _build_cookie_header(session, cookies, cookie_header, url):
|
|||||||
return c.output(header="", sep=";").strip()
|
return c.output(header="", sep=";").strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _build_url_with_params(url_str: str, params: Mapping[str, 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):
|
def vcr_request(cassette, real_request):
|
||||||
@functools.wraps(real_request)
|
@functools.wraps(real_request)
|
||||||
async def new_request(self, method, url, **kwargs):
|
async def new_request(self, method, url, **kwargs):
|
||||||
@@ -241,12 +252,7 @@ def vcr_request(cassette, real_request):
|
|||||||
if auth is not None:
|
if auth is not None:
|
||||||
headers["AUTHORIZATION"] = auth.encode()
|
headers["AUTHORIZATION"] = auth.encode()
|
||||||
|
|
||||||
request_url = URL(url)
|
request_url = URL(url) if not params else _build_url_with_params(url, params)
|
||||||
if params:
|
|
||||||
for k, v in params.items():
|
|
||||||
params[k] = str(v)
|
|
||||||
request_url = URL(url).with_query(params)
|
|
||||||
|
|
||||||
c_header = headers.pop(hdrs.COOKIE, None)
|
c_header = headers.pop(hdrs.COOKIE, None)
|
||||||
cookie_header = _build_cookie_header(self, cookies, c_header, request_url)
|
cookie_header = _build_cookie_header(self, cookies, c_header, request_url)
|
||||||
if cookie_header:
|
if cookie_header:
|
||||||
@@ -255,8 +261,8 @@ def vcr_request(cassette, real_request):
|
|||||||
vcr_request = Request(method, str(request_url), data, _serialize_headers(headers))
|
vcr_request = Request(method, str(request_url), data, _serialize_headers(headers))
|
||||||
|
|
||||||
if cassette.can_play_response_for(vcr_request):
|
if cassette.can_play_response_for(vcr_request):
|
||||||
log.info("Playing response for {} from cassette".format(vcr_request))
|
log.info(f"Playing response for {vcr_request} from cassette")
|
||||||
response = play_responses(cassette, vcr_request)
|
response = play_responses(cassette, vcr_request, kwargs)
|
||||||
for redirect in response.history:
|
for redirect in response.history:
|
||||||
self._cookie_jar.update_cookies(redirect.cookies, redirect.url)
|
self._cookie_jar.update_cookies(redirect.cookies, redirect.url)
|
||||||
self._cookie_jar.update_cookies(response.cookies, response.url)
|
self._cookie_jar.update_cookies(response.cookies, response.url)
|
||||||
@@ -267,7 +273,7 @@ def vcr_request(cassette, real_request):
|
|||||||
|
|
||||||
log.info("%s not in cassette, sending to real server", vcr_request)
|
log.info("%s not in cassette, sending to real server", vcr_request)
|
||||||
|
|
||||||
response = await real_request(self, method, url, **kwargs) # NOQA: E999
|
response = await real_request(self, method, url, **kwargs)
|
||||||
await record_responses(cassette, vcr_request, response)
|
await record_responses(cassette, vcr_request, response)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,7 @@
|
|||||||
"""Stubs for boto3"""
|
"""Stubs for boto3"""
|
||||||
try:
|
|
||||||
# boto using awsrequest
|
|
||||||
from botocore.awsrequest import AWSHTTPConnection as HTTPConnection
|
|
||||||
from botocore.awsrequest import AWSHTTPSConnection as VerifiedHTTPSConnection
|
|
||||||
|
|
||||||
except ImportError: # pragma: nocover
|
from botocore.awsrequest import AWSHTTPConnection as HTTPConnection
|
||||||
# boto using vendored requests
|
from botocore.awsrequest import AWSHTTPSConnection as VerifiedHTTPSConnection
|
||||||
# urllib3 defines its own HTTPConnection classes, which boto3 goes ahead and assumes
|
|
||||||
# you're using. It includes some polyfills for newer features missing in older pythons.
|
|
||||||
try:
|
|
||||||
from urllib3.connectionpool import HTTPConnection, VerifiedHTTPSConnection
|
|
||||||
except ImportError: # pragma: nocover
|
|
||||||
from requests.packages.urllib3.connectionpool import HTTPConnection, VerifiedHTTPSConnection
|
|
||||||
|
|
||||||
from ..stubs import VCRHTTPConnection, VCRHTTPSConnection
|
from ..stubs import VCRHTTPConnection, VCRHTTPSConnection
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
"""Stubs for boto"""
|
|
||||||
|
|
||||||
from boto.https_connection import CertValidatingHTTPSConnection
|
|
||||||
from ..stubs import VCRHTTPSConnection
|
|
||||||
|
|
||||||
|
|
||||||
class VCRCertValidatingHTTPSConnection(VCRHTTPSConnection):
|
|
||||||
_baseclass = CertValidatingHTTPSConnection
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
from io import BytesIO
|
|
||||||
import http.client
|
import http.client
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
"""
|
"""
|
||||||
The python3 http.client api moved some stuff around, so this is an abstraction
|
The python3 http.client api moved some stuff around, so this is an abstraction
|
||||||
@@ -13,7 +12,7 @@ def get_header(message, name):
|
|||||||
|
|
||||||
|
|
||||||
def get_header_items(message):
|
def get_header_items(message):
|
||||||
for (key, values) in get_headers(message):
|
for key, values in get_headers(message):
|
||||||
for value in values:
|
for value in values:
|
||||||
yield key, value
|
yield key, value
|
||||||
|
|
||||||
|
|||||||
215
vcr/stubs/httpcore_stubs.py
Normal file
215
vcr/stubs/httpcore_stubs.py
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
import asyncio
|
||||||
|
import functools
|
||||||
|
import logging
|
||||||
|
from collections import defaultdict
|
||||||
|
from collections.abc import AsyncIterable, Iterable
|
||||||
|
|
||||||
|
from httpcore import Response
|
||||||
|
from httpcore._models import ByteStream
|
||||||
|
|
||||||
|
from vcr.errors import CannotOverwriteExistingCassetteException
|
||||||
|
from vcr.filters import decode_response
|
||||||
|
from vcr.request import Request as VcrRequest
|
||||||
|
from vcr.serializers.compat import convert_body_to_bytes
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def _convert_byte_stream(stream):
|
||||||
|
if isinstance(stream, Iterable):
|
||||||
|
return list(stream)
|
||||||
|
|
||||||
|
if isinstance(stream, AsyncIterable):
|
||||||
|
return [part async for part in stream]
|
||||||
|
|
||||||
|
raise TypeError(
|
||||||
|
f"_convert_byte_stream: stream must be Iterable or AsyncIterable, got {type(stream).__name__}",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _serialize_headers(real_response):
|
||||||
|
"""
|
||||||
|
Some headers can appear multiple times, like "Set-Cookie".
|
||||||
|
Therefore serialize every header key to a list of values.
|
||||||
|
"""
|
||||||
|
|
||||||
|
headers = defaultdict(list)
|
||||||
|
|
||||||
|
for name, value in real_response.headers:
|
||||||
|
headers[name.decode("ascii")].append(value.decode("ascii"))
|
||||||
|
|
||||||
|
return dict(headers)
|
||||||
|
|
||||||
|
|
||||||
|
async def _serialize_response(real_response):
|
||||||
|
# The reason_phrase may not exist
|
||||||
|
try:
|
||||||
|
reason_phrase = real_response.extensions["reason_phrase"].decode("ascii")
|
||||||
|
except KeyError:
|
||||||
|
reason_phrase = None
|
||||||
|
|
||||||
|
# Reading the response stream consumes the iterator, so we need to restore it afterwards
|
||||||
|
content = b"".join(await _convert_byte_stream(real_response.stream))
|
||||||
|
real_response.stream = ByteStream(content)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": {"code": real_response.status, "message": reason_phrase},
|
||||||
|
"headers": _serialize_headers(real_response),
|
||||||
|
"body": {"string": content},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _deserialize_headers(headers):
|
||||||
|
"""
|
||||||
|
httpcore accepts headers as list of tuples of header key and value.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return [
|
||||||
|
(name.encode("ascii"), value.encode("ascii")) for name, values in headers.items() for value in values
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _deserialize_response(vcr_response):
|
||||||
|
# Cassette format generated for HTTPX requests by older versions of
|
||||||
|
# vcrpy. We restructure the content to resemble what a regular
|
||||||
|
# cassette looks like.
|
||||||
|
if "status_code" in vcr_response:
|
||||||
|
vcr_response = decode_response(
|
||||||
|
convert_body_to_bytes(
|
||||||
|
{
|
||||||
|
"headers": vcr_response["headers"],
|
||||||
|
"body": {"string": vcr_response["content"]},
|
||||||
|
"status": {"code": vcr_response["status_code"]},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
extensions = None
|
||||||
|
else:
|
||||||
|
extensions = (
|
||||||
|
{"reason_phrase": vcr_response["status"]["message"].encode("ascii")}
|
||||||
|
if vcr_response["status"]["message"]
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
vcr_response["status"]["code"],
|
||||||
|
headers=_deserialize_headers(vcr_response["headers"]),
|
||||||
|
content=vcr_response["body"]["string"],
|
||||||
|
extensions=extensions,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _make_vcr_request(real_request):
|
||||||
|
# Reading the request stream consumes the iterator, so we need to restore it afterwards
|
||||||
|
body = b"".join(await _convert_byte_stream(real_request.stream))
|
||||||
|
real_request.stream = ByteStream(body)
|
||||||
|
|
||||||
|
uri = bytes(real_request.url).decode("ascii")
|
||||||
|
|
||||||
|
# As per HTTPX: If there are multiple headers with the same key, then we concatenate them with commas
|
||||||
|
headers = defaultdict(list)
|
||||||
|
|
||||||
|
for name, value in real_request.headers:
|
||||||
|
headers[name.decode("ascii")].append(value.decode("ascii"))
|
||||||
|
|
||||||
|
headers = {name: ", ".join(values) for name, values in headers.items()}
|
||||||
|
|
||||||
|
return VcrRequest(real_request.method.decode("ascii"), uri, body, headers)
|
||||||
|
|
||||||
|
|
||||||
|
async def _vcr_request(cassette, real_request):
|
||||||
|
vcr_request = await _make_vcr_request(real_request)
|
||||||
|
|
||||||
|
if cassette.can_play_response_for(vcr_request):
|
||||||
|
return vcr_request, _play_responses(cassette, vcr_request)
|
||||||
|
|
||||||
|
if cassette.write_protected and cassette.filter_request(vcr_request):
|
||||||
|
raise CannotOverwriteExistingCassetteException(
|
||||||
|
cassette=cassette,
|
||||||
|
failed_request=vcr_request,
|
||||||
|
)
|
||||||
|
|
||||||
|
_logger.info("%s not in cassette, sending to real server", vcr_request)
|
||||||
|
|
||||||
|
return vcr_request, None
|
||||||
|
|
||||||
|
|
||||||
|
async def _record_responses(cassette, vcr_request, real_response):
|
||||||
|
cassette.append(vcr_request, await _serialize_response(real_response))
|
||||||
|
|
||||||
|
|
||||||
|
def _play_responses(cassette, vcr_request):
|
||||||
|
vcr_response = cassette.play_response(vcr_request)
|
||||||
|
real_response = _deserialize_response(vcr_response)
|
||||||
|
|
||||||
|
return real_response
|
||||||
|
|
||||||
|
|
||||||
|
async def _vcr_handle_async_request(
|
||||||
|
cassette,
|
||||||
|
real_handle_async_request,
|
||||||
|
self,
|
||||||
|
real_request,
|
||||||
|
):
|
||||||
|
vcr_request, vcr_response = await _vcr_request(cassette, real_request)
|
||||||
|
|
||||||
|
if vcr_response:
|
||||||
|
return vcr_response
|
||||||
|
|
||||||
|
real_response = await real_handle_async_request(self, real_request)
|
||||||
|
await _record_responses(cassette, vcr_request, real_response)
|
||||||
|
|
||||||
|
return real_response
|
||||||
|
|
||||||
|
|
||||||
|
def vcr_handle_async_request(cassette, real_handle_async_request):
|
||||||
|
@functools.wraps(real_handle_async_request)
|
||||||
|
def _inner_handle_async_request(self, real_request):
|
||||||
|
return _vcr_handle_async_request(
|
||||||
|
cassette,
|
||||||
|
real_handle_async_request,
|
||||||
|
self,
|
||||||
|
real_request,
|
||||||
|
)
|
||||||
|
|
||||||
|
return _inner_handle_async_request
|
||||||
|
|
||||||
|
|
||||||
|
def _run_async_function(sync_func, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Safely run an asynchronous function from a synchronous context.
|
||||||
|
Handles both cases:
|
||||||
|
- An event loop is already running.
|
||||||
|
- No event loop exists yet.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
asyncio.get_running_loop()
|
||||||
|
except RuntimeError:
|
||||||
|
return asyncio.run(sync_func(*args, **kwargs))
|
||||||
|
else:
|
||||||
|
# If inside a running loop, create a task and wait for it
|
||||||
|
return asyncio.ensure_future(sync_func(*args, **kwargs))
|
||||||
|
|
||||||
|
|
||||||
|
def _vcr_handle_request(cassette, real_handle_request, self, real_request):
|
||||||
|
vcr_request, vcr_response = _run_async_function(
|
||||||
|
_vcr_request,
|
||||||
|
cassette,
|
||||||
|
real_request,
|
||||||
|
)
|
||||||
|
|
||||||
|
if vcr_response:
|
||||||
|
return vcr_response
|
||||||
|
|
||||||
|
real_response = real_handle_request(self, real_request)
|
||||||
|
_run_async_function(_record_responses, cassette, vcr_request, real_response)
|
||||||
|
|
||||||
|
return real_response
|
||||||
|
|
||||||
|
|
||||||
|
def vcr_handle_request(cassette, real_handle_request):
|
||||||
|
@functools.wraps(real_handle_request)
|
||||||
|
def _inner_handle_request(self, real_request):
|
||||||
|
return _vcr_handle_request(cassette, real_handle_request, self, real_request)
|
||||||
|
|
||||||
|
return _inner_handle_request
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
"""Stubs for httplib2"""
|
"""Stubs for httplib2"""
|
||||||
|
|
||||||
from httplib2 import HTTPConnectionWithTimeout, HTTPSConnectionWithTimeout
|
from httplib2 import HTTPConnectionWithTimeout, HTTPSConnectionWithTimeout
|
||||||
|
|
||||||
from ..stubs import VCRHTTPConnection, VCRHTTPSConnection
|
from ..stubs import VCRHTTPConnection, VCRHTTPSConnection
|
||||||
|
|
||||||
|
|
||||||
@@ -27,7 +28,6 @@ class VCRHTTPSConnectionWithTimeout(VCRHTTPSConnection, HTTPSConnectionWithTimeo
|
|||||||
_baseclass = HTTPSConnectionWithTimeout
|
_baseclass = HTTPSConnectionWithTimeout
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
|
||||||
# Delete the keyword arguments that HTTPSConnection would not recognize
|
# Delete the keyword arguments that HTTPSConnection would not recognize
|
||||||
safe_keys = {
|
safe_keys = {
|
||||||
"host",
|
"host",
|
||||||
|
|||||||
@@ -1,159 +0,0 @@
|
|||||||
import functools
|
|
||||||
import logging
|
|
||||||
from unittest.mock import patch, MagicMock
|
|
||||||
|
|
||||||
import httpx
|
|
||||||
from vcr.request import Request as VcrRequest
|
|
||||||
from vcr.errors import CannotOverwriteExistingCassetteException
|
|
||||||
|
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def _transform_headers(httpx_reponse):
|
|
||||||
"""
|
|
||||||
Some headers can appear multiple times, like "Set-Cookie".
|
|
||||||
Therefore transform to every header key to list of values.
|
|
||||||
"""
|
|
||||||
|
|
||||||
out = {}
|
|
||||||
for key, var in httpx_reponse.headers.raw:
|
|
||||||
decoded_key = key.decode("utf-8")
|
|
||||||
out.setdefault(decoded_key, [])
|
|
||||||
out[decoded_key].append(var.decode("utf-8"))
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
def _to_serialized_response(httpx_reponse):
|
|
||||||
return {
|
|
||||||
"status_code": httpx_reponse.status_code,
|
|
||||||
"http_version": httpx_reponse.http_version,
|
|
||||||
"headers": _transform_headers(httpx_reponse),
|
|
||||||
"content": httpx_reponse.content.decode("utf-8", "ignore"),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _from_serialized_headers(headers):
|
|
||||||
"""
|
|
||||||
httpx accepts headers as list of tuples of header key and value.
|
|
||||||
"""
|
|
||||||
|
|
||||||
header_list = []
|
|
||||||
for key, values in headers.items():
|
|
||||||
for v in values:
|
|
||||||
header_list.append((key, v))
|
|
||||||
return header_list
|
|
||||||
|
|
||||||
|
|
||||||
@patch("httpx.Response.close", MagicMock())
|
|
||||||
@patch("httpx.Response.read", MagicMock())
|
|
||||||
def _from_serialized_response(request, serialized_response, history=None):
|
|
||||||
content = serialized_response.get("content").encode()
|
|
||||||
response = httpx.Response(
|
|
||||||
status_code=serialized_response.get("status_code"),
|
|
||||||
request=request,
|
|
||||||
http_version=serialized_response.get("http_version"),
|
|
||||||
headers=_from_serialized_headers(serialized_response.get("headers")),
|
|
||||||
content=content,
|
|
||||||
history=history or [],
|
|
||||||
)
|
|
||||||
response._content = content
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
def _make_vcr_request(httpx_request, **kwargs):
|
|
||||||
body = httpx_request.read().decode("utf-8")
|
|
||||||
uri = str(httpx_request.url)
|
|
||||||
headers = dict(httpx_request.headers)
|
|
||||||
return VcrRequest(httpx_request.method, uri, body, headers)
|
|
||||||
|
|
||||||
|
|
||||||
def _shared_vcr_send(cassette, real_send, *args, **kwargs):
|
|
||||||
real_request = args[1]
|
|
||||||
|
|
||||||
vcr_request = _make_vcr_request(real_request, **kwargs)
|
|
||||||
|
|
||||||
if cassette.can_play_response_for(vcr_request):
|
|
||||||
return vcr_request, _play_responses(cassette, real_request, vcr_request, args[0], kwargs)
|
|
||||||
|
|
||||||
if cassette.write_protected and cassette.filter_request(vcr_request):
|
|
||||||
raise CannotOverwriteExistingCassetteException(cassette=cassette, failed_request=vcr_request)
|
|
||||||
|
|
||||||
_logger.info("%s not in cassette, sending to real server", vcr_request)
|
|
||||||
return vcr_request, None
|
|
||||||
|
|
||||||
|
|
||||||
def _record_responses(cassette, vcr_request, real_response):
|
|
||||||
for past_real_response in real_response.history:
|
|
||||||
past_vcr_request = _make_vcr_request(past_real_response.request)
|
|
||||||
cassette.append(past_vcr_request, _to_serialized_response(past_real_response))
|
|
||||||
|
|
||||||
if real_response.history:
|
|
||||||
# If there was a redirection keep we want the request which will hold the
|
|
||||||
# final redirect value
|
|
||||||
vcr_request = _make_vcr_request(real_response.request)
|
|
||||||
|
|
||||||
cassette.append(vcr_request, _to_serialized_response(real_response))
|
|
||||||
return real_response
|
|
||||||
|
|
||||||
|
|
||||||
def _play_responses(cassette, request, vcr_request, client, kwargs):
|
|
||||||
history = []
|
|
||||||
allow_redirects = kwargs.get("allow_redirects", True)
|
|
||||||
vcr_response = cassette.play_response(vcr_request)
|
|
||||||
response = _from_serialized_response(request, vcr_response)
|
|
||||||
|
|
||||||
while allow_redirects and 300 <= response.status_code <= 399:
|
|
||||||
next_url = response.headers.get("location")
|
|
||||||
if not next_url:
|
|
||||||
break
|
|
||||||
|
|
||||||
vcr_request = VcrRequest("GET", next_url, None, dict(response.headers))
|
|
||||||
vcr_request = cassette.find_requests_with_most_matches(vcr_request)[0][0]
|
|
||||||
|
|
||||||
history.append(response)
|
|
||||||
# add cookies from response to session cookie store
|
|
||||||
client.cookies.extract_cookies(response)
|
|
||||||
|
|
||||||
vcr_response = cassette.play_response(vcr_request)
|
|
||||||
response = _from_serialized_response(vcr_request, vcr_response, history)
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
async def _async_vcr_send(cassette, real_send, *args, **kwargs):
|
|
||||||
vcr_request, response = _shared_vcr_send(cassette, real_send, *args, **kwargs)
|
|
||||||
if response:
|
|
||||||
# add cookies from response to session cookie store
|
|
||||||
args[0].cookies.extract_cookies(response)
|
|
||||||
return response
|
|
||||||
|
|
||||||
real_response = await real_send(*args, **kwargs)
|
|
||||||
return _record_responses(cassette, vcr_request, real_response)
|
|
||||||
|
|
||||||
|
|
||||||
def async_vcr_send(cassette, real_send):
|
|
||||||
@functools.wraps(real_send)
|
|
||||||
def _inner_send(*args, **kwargs):
|
|
||||||
return _async_vcr_send(cassette, real_send, *args, **kwargs)
|
|
||||||
|
|
||||||
return _inner_send
|
|
||||||
|
|
||||||
|
|
||||||
def _sync_vcr_send(cassette, real_send, *args, **kwargs):
|
|
||||||
vcr_request, response = _shared_vcr_send(cassette, real_send, *args, **kwargs)
|
|
||||||
if response:
|
|
||||||
# add cookies from response to session cookie store
|
|
||||||
args[0].cookies.extract_cookies(response)
|
|
||||||
return response
|
|
||||||
|
|
||||||
real_response = real_send(*args, **kwargs)
|
|
||||||
return _record_responses(cassette, vcr_request, real_response)
|
|
||||||
|
|
||||||
|
|
||||||
def sync_vcr_send(cassette, real_send):
|
|
||||||
@functools.wraps(real_send)
|
|
||||||
def _inner_send(*args, **kwargs):
|
|
||||||
return _sync_vcr_send(cassette, real_send, *args, **kwargs)
|
|
||||||
|
|
||||||
return _inner_send
|
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
"""Stubs for requests"""
|
"""Stubs for requests"""
|
||||||
|
|
||||||
try:
|
from urllib3.connection import HTTPConnection, VerifiedHTTPSConnection
|
||||||
from urllib3.connectionpool import HTTPConnection, VerifiedHTTPSConnection
|
|
||||||
except ImportError:
|
|
||||||
from requests.packages.urllib3.connectionpool import HTTPConnection, VerifiedHTTPSConnection
|
|
||||||
|
|
||||||
from ..stubs import VCRHTTPConnection, VCRHTTPSConnection
|
from ..stubs import VCRHTTPConnection, VCRHTTPSConnection
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
"""Stubs for tornado HTTP clients"""
|
"""Stubs for tornado HTTP clients"""
|
||||||
|
|
||||||
import functools
|
import functools
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
@@ -29,9 +30,9 @@ def vcr_fetch_impl(cassette, real_fetch_impl):
|
|||||||
request,
|
request,
|
||||||
599,
|
599,
|
||||||
error=Exception(
|
error=Exception(
|
||||||
"The request (%s) uses AsyncHTTPClient functionality "
|
f"The request ({request!r}) uses AsyncHTTPClient functionality "
|
||||||
"that is not yet supported by VCR.py. Please make the "
|
"that is not yet supported by VCR.py. Please make the "
|
||||||
"request outside a VCR.py context." % repr(request)
|
"request outside a VCR.py context.",
|
||||||
),
|
),
|
||||||
request_time=self.io_loop.time() - request.start_time,
|
request_time=self.io_loop.time() - request.start_time,
|
||||||
)
|
)
|
||||||
@@ -65,14 +66,15 @@ def vcr_fetch_impl(cassette, real_fetch_impl):
|
|||||||
request,
|
request,
|
||||||
599,
|
599,
|
||||||
error=CannotOverwriteExistingCassetteException(
|
error=CannotOverwriteExistingCassetteException(
|
||||||
cassette=cassette, failed_request=vcr_request
|
cassette=cassette,
|
||||||
|
failed_request=vcr_request,
|
||||||
),
|
),
|
||||||
request_time=self.io_loop.time() - request.start_time,
|
request_time=self.io_loop.time() - request.start_time,
|
||||||
)
|
)
|
||||||
return callback(response)
|
return callback(response)
|
||||||
|
|
||||||
def new_callback(response):
|
def new_callback(response):
|
||||||
headers = [(k, response.headers.get_list(k)) for k in response.headers.keys()]
|
headers = [(k, response.headers.get_list(k)) for k in response.headers]
|
||||||
|
|
||||||
vcr_response = {
|
vcr_response = {
|
||||||
"status": {"code": response.code, "message": response.reason},
|
"status": {"code": response.code, "message": response.reason},
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"""Stubs for urllib3"""
|
"""Stubs for urllib3"""
|
||||||
|
|
||||||
from urllib3.connectionpool import HTTPConnection, VerifiedHTTPSConnection
|
from urllib3.connection import HTTPConnection, VerifiedHTTPSConnection
|
||||||
|
|
||||||
from ..stubs import VCRHTTPConnection, VCRHTTPSConnection
|
from ..stubs import VCRHTTPConnection, VCRHTTPSConnection
|
||||||
|
|
||||||
# urllib3 defines its own HTTPConnection classes. It includes some polyfills
|
# urllib3 defines its own HTTPConnection classes. It includes some polyfills
|
||||||
|
|||||||
39
vcr/unittest.py
Normal file
39
vcr/unittest.py
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import inspect
|
||||||
|
import os
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from .config import VCR
|
||||||
|
|
||||||
|
|
||||||
|
class VCRMixin:
|
||||||
|
"""A TestCase mixin that provides VCR integration."""
|
||||||
|
|
||||||
|
vcr_enabled = True
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
if self.vcr_enabled:
|
||||||
|
kwargs = self._get_vcr_kwargs()
|
||||||
|
myvcr = self._get_vcr(**kwargs)
|
||||||
|
cm = myvcr.use_cassette(self._get_cassette_name())
|
||||||
|
self.cassette = cm.__enter__()
|
||||||
|
self.addCleanup(cm.__exit__, None, None, None)
|
||||||
|
|
||||||
|
def _get_vcr(self, **kwargs):
|
||||||
|
if "cassette_library_dir" not in kwargs:
|
||||||
|
kwargs["cassette_library_dir"] = self._get_cassette_library_dir()
|
||||||
|
return VCR(**kwargs)
|
||||||
|
|
||||||
|
def _get_vcr_kwargs(self, **kwargs):
|
||||||
|
return kwargs
|
||||||
|
|
||||||
|
def _get_cassette_library_dir(self):
|
||||||
|
testdir = os.path.dirname(inspect.getfile(self.__class__))
|
||||||
|
return os.path.join(testdir, "cassettes")
|
||||||
|
|
||||||
|
def _get_cassette_name(self):
|
||||||
|
return f"{self.__class__.__name__}.{self._testMethodName}.yaml"
|
||||||
|
|
||||||
|
|
||||||
|
class VCRTestCase(VCRMixin, unittest.TestCase):
|
||||||
|
pass
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user