mirror of
https://github.com/kevin1024/vcrpy.git
synced 2025-12-10 09:35:34 +00:00
Compare commits
95 Commits
v6.0.0
...
04fc6cf626
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
04fc6cf626 | ||
|
|
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 | ||
|
|
b1c45cd249 | ||
|
|
36c7465cf7 | ||
|
|
010fa268d1 | ||
|
|
99c0384770 |
4
.github/workflows/codespell.yml
vendored
4
.github/workflows/codespell.yml
vendored
@@ -13,10 +13,10 @@ permissions:
|
|||||||
jobs:
|
jobs:
|
||||||
codespell:
|
codespell:
|
||||||
name: Check for spelling errors
|
name: Check for spelling errors
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-24.04
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
- name: Codespell
|
- name: Codespell
|
||||||
uses: codespell-project/actions-codespell@v2
|
uses: codespell-project/actions-codespell@v2
|
||||||
|
|||||||
4
.github/workflows/docs.yml
vendored
4
.github/workflows/docs.yml
vendored
@@ -7,10 +7,10 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
validate:
|
validate:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-24.04
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: "3.12"
|
python-version: "3.12"
|
||||||
|
|||||||
29
.github/workflows/main.yml
vendored
29
.github/workflows/main.yml
vendored
@@ -5,21 +5,22 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
pull_request:
|
pull_request:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 16 * * 5' # Every Friday 4pm
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-24.04
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
python-version:
|
python-version:
|
||||||
- "3.8"
|
|
||||||
- "3.9"
|
- "3.9"
|
||||||
- "3.10"
|
- "3.10"
|
||||||
- "3.11"
|
- "3.11"
|
||||||
- "3.12"
|
- "3.12"
|
||||||
- "pypy-3.8"
|
- "3.13"
|
||||||
- "pypy-3.9"
|
- "pypy-3.9"
|
||||||
- "pypy-3.10"
|
- "pypy-3.10"
|
||||||
urllib3-requirement:
|
urllib3-requirement:
|
||||||
@@ -27,10 +28,6 @@ jobs:
|
|||||||
- "urllib3<2"
|
- "urllib3<2"
|
||||||
|
|
||||||
exclude:
|
exclude:
|
||||||
- python-version: "3.8"
|
|
||||||
urllib3-requirement: "urllib3>=2"
|
|
||||||
- python-version: "pypy-3.8"
|
|
||||||
urllib3-requirement: "urllib3>=2"
|
|
||||||
- python-version: "3.9"
|
- python-version: "3.9"
|
||||||
urllib3-requirement: "urllib3>=2"
|
urllib3-requirement: "urllib3>=2"
|
||||||
- python-version: "pypy-3.9"
|
- python-version: "pypy-3.9"
|
||||||
@@ -39,19 +36,29 @@ jobs:
|
|||||||
urllib3-requirement: "urllib3>=2"
|
urllib3-requirement: "urllib3>=2"
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
|
- name: Install uv
|
||||||
|
uses: astral-sh/setup-uv@v6
|
||||||
|
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
cache: pip
|
cache: pip
|
||||||
|
allow-prereleases: true
|
||||||
|
|
||||||
- name: Install project dependencies
|
- name: Install project dependencies
|
||||||
run: |
|
run: |
|
||||||
pip install --upgrade pip
|
uv pip install --system --upgrade pip setuptools
|
||||||
pip install codecov '.[tests]' '${{ matrix.urllib3-requirement }}'
|
uv pip install --system codecov '.[tests]' '${{ matrix.urllib3-requirement }}'
|
||||||
pip check
|
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
|
- name: Run online tests
|
||||||
run: ./runtests.sh --cov=./vcr --cov-branch --cov-report=xml --cov-append -m online
|
run: ./runtests.sh --cov=./vcr --cov-branch --cov-report=xml --cov-append -m online
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ permissions:
|
|||||||
jobs:
|
jobs:
|
||||||
pre_commit_detect_outdated:
|
pre_commit_detect_outdated:
|
||||||
name: Detect outdated pre-commit hooks
|
name: Detect outdated pre-commit hooks
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Set up Python 3.12
|
- name: Set up Python 3.12
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
@@ -41,7 +41,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Create pull request from changes (if any)
|
- name: Create pull request from changes (if any)
|
||||||
id: create-pull-request
|
id: create-pull-request
|
||||||
uses: peter-evans/create-pull-request@v5
|
uses: peter-evans/create-pull-request@v7
|
||||||
with:
|
with:
|
||||||
author: 'pre-commit <pre-commit@tools.invalid>'
|
author: 'pre-commit <pre-commit@tools.invalid>'
|
||||||
base: master
|
base: master
|
||||||
|
|||||||
6
.github/workflows/pre-commit.yml
vendored
6
.github/workflows/pre-commit.yml
vendored
@@ -11,10 +11,10 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
pre-commit:
|
pre-commit:
|
||||||
name: Run pre-commit
|
name: Run pre-commit
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: 3.12
|
python-version: 3.12
|
||||||
- uses: pre-commit/action@v3.0.0
|
- uses: pre-commit/action@v3.0.1
|
||||||
|
|||||||
@@ -3,14 +3,14 @@
|
|||||||
|
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v0.1.13
|
rev: v0.14.5
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
args: ["--show-source"]
|
args: ["--output-format=full"]
|
||||||
- id: ruff-format
|
- id: ruff-format
|
||||||
|
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v4.5.0
|
rev: v6.0.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: check-merge-conflict
|
- id: check-merge-conflict
|
||||||
- id: end-of-file-fixer
|
- id: end-of-file-fixer
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ version: 2
|
|||||||
|
|
||||||
# Set the version of Python and other tools you might need
|
# Set the version of Python and other tools you might need
|
||||||
build:
|
build:
|
||||||
os: ubuntu-22.04
|
os: ubuntu-24.04
|
||||||
tools:
|
tools:
|
||||||
python: "3.12"
|
python: "3.12"
|
||||||
|
|
||||||
|
|||||||
@@ -427,3 +427,16 @@ If you want to save the cassette only when the test succeeds, set the Cassette
|
|||||||
|
|
||||||
# Since there was an exception, the cassette file hasn't been created.
|
# Since there was an exception, the cassette file hasn't been created.
|
||||||
assert not os.path.exists('fixtures/vcr_cassettes/synopsis.yaml')
|
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
|
||||||
|
|||||||
@@ -7,6 +7,18 @@ For a full list of triaged issues, bugs and PRs and what release they are target
|
|||||||
|
|
||||||
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.
|
||||||
|
|
||||||
|
- 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
|
- 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: 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)
|
- BREAKING: Drop support for `boto` (vcrpy still supports boto3, but is dropping the deprecated `boto` support in this release. (thanks @jairhenrique)
|
||||||
|
|||||||
@@ -316,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"
|
||||||
|
|||||||
@@ -115,8 +115,8 @@ in this example::
|
|||||||
pyenv local 3.12.0 pypy3.10
|
pyenv local 3.12.0 pypy3.10
|
||||||
|
|
||||||
# Run the whole test suite
|
# Run the whole test suite
|
||||||
pip install .[test]
|
pip install .[tests]
|
||||||
./run_tests.sh
|
./runtests.sh
|
||||||
|
|
||||||
|
|
||||||
Troubleshooting on MacOSX
|
Troubleshooting on MacOSX
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ with pip::
|
|||||||
Compatibility
|
Compatibility
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
VCR.py supports Python 3.8+, 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:
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
sphinx<8
|
sphinx<9
|
||||||
sphinx_rtd_theme==2.0.0
|
sphinx_rtd_theme==3.0.2
|
||||||
|
|||||||
@@ -1,21 +1,17 @@
|
|||||||
[tool.codespell]
|
[tool.codespell]
|
||||||
skip = '.git,*.pdf,*.svg,.tox'
|
skip = '.git,*.pdf,*.svg,.tox'
|
||||||
ignore-regex = "\\\\[fnrstv]"
|
ignore-regex = "\\\\[fnrstv]"
|
||||||
#
|
|
||||||
# ignore-words-list = ''
|
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
addopts = [
|
addopts = ["--strict-config", "--strict-markers"]
|
||||||
"--strict-config",
|
asyncio_default_fixture_loop_scope = "function"
|
||||||
"--strict-markers",
|
|
||||||
]
|
|
||||||
markers = ["online"]
|
markers = ["online"]
|
||||||
filterwarnings = [
|
|
||||||
"error",
|
|
||||||
'''ignore:datetime\.datetime\.utcfromtimestamp\(\) is deprecated and scheduled for removal in a future version.*:DeprecationWarning''',
|
|
||||||
]
|
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
|
line-length = 110
|
||||||
|
target-version = "py39"
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
select = [
|
select = [
|
||||||
"B", # flake8-bugbear
|
"B", # flake8-bugbear
|
||||||
"C4", # flake8-comprehensions
|
"C4", # flake8-comprehensions
|
||||||
@@ -29,8 +25,6 @@ select = [
|
|||||||
"UP", # pyupgrade
|
"UP", # pyupgrade
|
||||||
"W", # pycodestyle warning
|
"W", # pycodestyle warning
|
||||||
]
|
]
|
||||||
line-length = 110
|
|
||||||
target-version = "py38"
|
|
||||||
|
|
||||||
[tool.ruff.isort]
|
[tool.ruff.lint.isort]
|
||||||
known-first-party = ["vcr"]
|
known-first-party = ["vcr"]
|
||||||
|
|||||||
23
setup.py
23
setup.py
@@ -3,10 +3,8 @@
|
|||||||
import codecs
|
import codecs
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
|
||||||
|
|
||||||
from setuptools import find_packages, setup
|
from setuptools import find_packages, setup
|
||||||
from setuptools.command.test import test as TestCommand
|
|
||||||
|
|
||||||
long_description = open("README.rst").read()
|
long_description = open("README.rst").read()
|
||||||
here = os.path.abspath(os.path.dirname(__file__))
|
here = os.path.abspath(os.path.dirname(__file__))
|
||||||
@@ -28,20 +26,6 @@ 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",
|
||||||
@@ -55,6 +39,9 @@ install_requires = [
|
|||||||
"urllib3 <2; python_version <'3.10'",
|
"urllib3 <2; python_version <'3.10'",
|
||||||
# https://github.com/kevin1024/vcrpy/pull/775#issuecomment-1847849962
|
# https://github.com/kevin1024/vcrpy/pull/775#issuecomment-1847849962
|
||||||
"urllib3 <2; platform_python_implementation =='PyPy'",
|
"urllib3 <2; platform_python_implementation =='PyPy'",
|
||||||
|
# Workaround for Poetry with CPython >= 3.10, problem description at:
|
||||||
|
# https://github.com/kevin1024/vcrpy/pull/826
|
||||||
|
"urllib3; platform_python_implementation !='PyPy' and python_version >='3.10'",
|
||||||
]
|
]
|
||||||
|
|
||||||
extras_require = {
|
extras_require = {
|
||||||
@@ -91,7 +78,7 @@ 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.8",
|
python_requires=">=3.9",
|
||||||
install_requires=install_requires,
|
install_requires=install_requires,
|
||||||
license="MIT",
|
license="MIT",
|
||||||
extras_require=extras_require,
|
extras_require=extras_require,
|
||||||
@@ -102,11 +89,11 @@ setup(
|
|||||||
"Intended Audience :: Developers",
|
"Intended Audience :: Developers",
|
||||||
"Programming Language :: Python",
|
"Programming Language :: Python",
|
||||||
"Programming Language :: Python :: 3",
|
"Programming Language :: Python :: 3",
|
||||||
"Programming Language :: Python :: 3.8",
|
|
||||||
"Programming Language :: Python :: 3.9",
|
"Programming Language :: Python :: 3.9",
|
||||||
"Programming Language :: Python :: 3.10",
|
"Programming Language :: Python :: 3.10",
|
||||||
"Programming Language :: Python :: 3.11",
|
"Programming Language :: Python :: 3.11",
|
||||||
"Programming Language :: Python :: 3.12",
|
"Programming Language :: Python :: 3.12",
|
||||||
|
"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",
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
import os
|
|
||||||
import ssl
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def httpbin_ssl_context():
|
|
||||||
ssl_ca_location = os.environ["REQUESTS_CA_BUNDLE"]
|
|
||||||
ssl_cert_location = os.environ["REQUESTS_CA_BUNDLE"].replace("cacert.pem", "cert.pem")
|
|
||||||
ssl_key_location = os.environ["REQUESTS_CA_BUNDLE"].replace("cacert.pem", "key.pem")
|
|
||||||
|
|
||||||
ssl_context = ssl.create_default_context(cafile=ssl_ca_location)
|
|
||||||
ssl_context.load_cert_chain(ssl_cert_location, ssl_key_location)
|
|
||||||
|
|
||||||
return ssl_context
|
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
import logging
|
import logging
|
||||||
|
import ssl
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
import pytest_httpbin.certs
|
||||||
|
import yarl
|
||||||
|
|
||||||
import vcr
|
import vcr
|
||||||
|
|
||||||
@@ -11,6 +14,8 @@ aiohttp = pytest.importorskip("aiohttp")
|
|||||||
|
|
||||||
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):
|
||||||
async def wrapper():
|
async def wrapper():
|
||||||
@@ -338,7 +343,7 @@ def test_double_requests(tmpdir, httpbin):
|
|||||||
assert cassette.play_count == 2
|
assert cassette.play_count == 2
|
||||||
|
|
||||||
|
|
||||||
def test_cookies(httpbin_both, httpbin_ssl_context, tmpdir):
|
def test_cookies(httpbin_both, tmpdir):
|
||||||
async def run(loop):
|
async def run(loop):
|
||||||
cookies_url = httpbin_both.url + (
|
cookies_url = httpbin_both.url + (
|
||||||
"/response-headers?"
|
"/response-headers?"
|
||||||
@@ -353,12 +358,12 @@ def test_cookies(httpbin_both, httpbin_ssl_context, tmpdir):
|
|||||||
# ------------------------- Record -------------------------- #
|
# ------------------------- Record -------------------------- #
|
||||||
with vcr.use_cassette(tmp) as cassette:
|
with vcr.use_cassette(tmp) as cassette:
|
||||||
async with aiohttp.ClientSession(loop=loop, cookie_jar=aiohttp.CookieJar(unsafe=True)) as session:
|
async with aiohttp.ClientSession(loop=loop, cookie_jar=aiohttp.CookieJar(unsafe=True)) as session:
|
||||||
cookies_resp = await session.get(cookies_url, ssl=httpbin_ssl_context)
|
cookies_resp = await session.get(cookies_url, ssl=HTTPBIN_SSL_CONTEXT)
|
||||||
home_resp = await session.get(
|
home_resp = await session.get(
|
||||||
home_url,
|
home_url,
|
||||||
cookies=req_cookies,
|
cookies=req_cookies,
|
||||||
headers=req_headers,
|
headers=req_headers,
|
||||||
ssl=httpbin_ssl_context,
|
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)
|
||||||
@@ -366,12 +371,12 @@ def test_cookies(httpbin_both, httpbin_ssl_context, tmpdir):
|
|||||||
# -------------------------- 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, cookie_jar=aiohttp.CookieJar(unsafe=True)) as session:
|
async with aiohttp.ClientSession(loop=loop, cookie_jar=aiohttp.CookieJar(unsafe=True)) as session:
|
||||||
cookies_resp = await session.get(cookies_url, ssl=httpbin_ssl_context)
|
cookies_resp = await session.get(cookies_url, ssl=HTTPBIN_SSL_CONTEXT)
|
||||||
home_resp = await session.get(
|
home_resp = await session.get(
|
||||||
home_url,
|
home_url,
|
||||||
cookies=req_cookies,
|
cookies=req_cookies,
|
||||||
headers=req_headers,
|
headers=req_headers,
|
||||||
ssl=httpbin_ssl_context,
|
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)
|
||||||
@@ -388,7 +393,7 @@ def test_cookies(httpbin_both, httpbin_ssl_context, tmpdir):
|
|||||||
run_in_loop(run)
|
run_in_loop(run)
|
||||||
|
|
||||||
|
|
||||||
def test_cookies_redirect(httpbin_both, httpbin_ssl_context, 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 = httpbin_both.url + "/cookies/set?Cookie_1=Val_1"
|
cookies_url = httpbin_both.url + "/cookies/set?Cookie_1=Val_1"
|
||||||
@@ -397,9 +402,9 @@ def test_cookies_redirect(httpbin_both, httpbin_ssl_context, tmpdir):
|
|||||||
# ------------------------- Record -------------------------- #
|
# ------------------------- Record -------------------------- #
|
||||||
with vcr.use_cassette(tmp) as cassette:
|
with vcr.use_cassette(tmp) as cassette:
|
||||||
async with aiohttp.ClientSession(loop=loop, cookie_jar=aiohttp.CookieJar(unsafe=True)) as session:
|
async with aiohttp.ClientSession(loop=loop, cookie_jar=aiohttp.CookieJar(unsafe=True)) as session:
|
||||||
cookies_resp = await session.get(cookies_url, ssl=httpbin_ssl_context)
|
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
|
||||||
|
|
||||||
@@ -408,9 +413,9 @@ def test_cookies_redirect(httpbin_both, httpbin_ssl_context, tmpdir):
|
|||||||
# -------------------------- 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, cookie_jar=aiohttp.CookieJar(unsafe=True)) as session:
|
async with aiohttp.ClientSession(loop=loop, cookie_jar=aiohttp.CookieJar(unsafe=True)) as session:
|
||||||
cookies_resp = await session.get(cookies_url, ssl=httpbin_ssl_context)
|
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
|
||||||
|
|
||||||
@@ -422,9 +427,9 @@ def test_cookies_redirect(httpbin_both, httpbin_ssl_context, tmpdir):
|
|||||||
"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, cookie_jar=aiohttp.CookieJar(unsafe=True)) as session:
|
async with aiohttp.ClientSession(loop=loop, cookie_jar=aiohttp.CookieJar(unsafe=True)) as session:
|
||||||
cookies_resp = await session.get(cookies_url, ssl=httpbin_ssl_context)
|
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)
|
||||||
|
|||||||
@@ -39,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"A simple HTTP Request & Response Service." in response
|
assert b"HTTP Request & Response Service" in response
|
||||||
|
|
||||||
|
|
||||||
def test_patched_content(tmpdir, httpbin):
|
def test_patched_content(tmpdir, httpbin):
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from urllib.request import urlopen
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
import vcr
|
import vcr
|
||||||
|
from vcr.cassette import Cassette
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.online
|
@pytest.mark.online
|
||||||
@@ -85,3 +86,21 @@ def test_dont_record_on_exception(tmpdir, httpbin):
|
|||||||
assert b"Not in content" in urlopen(httpbin.url).read()
|
assert b"Not in content" in urlopen(httpbin.url).read()
|
||||||
|
|
||||||
assert not os.path.exists(str(tmpdir.join("dontsave2.yml")))
|
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,5 @@
|
|||||||
"""Integration tests with httplib2"""
|
"""Integration tests with httplib2"""
|
||||||
|
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"""Test using a proxy."""
|
"""Test using a proxy."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import http.server
|
import http.server
|
||||||
import socketserver
|
import socketserver
|
||||||
import threading
|
import threading
|
||||||
@@ -36,6 +37,35 @@ class Proxy(http.server.SimpleHTTPRequestHandler):
|
|||||||
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(":")
|
||||||
|
|
||||||
|
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")
|
@pytest.fixture(scope="session")
|
||||||
def proxy_server():
|
def proxy_server():
|
||||||
@@ -52,10 +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")), mode="once") 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})
|
||||||
|
|
||||||
for key in set(cassette_response.headers.keys()) & set(response.headers.keys()):
|
|
||||||
assert cassette_response.headers[key] == response.headers[key]
|
|
||||||
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 + "/"
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ 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"A simple HTTP Request & Response Service." in response
|
assert b"HTTP Request & Response Service" in response
|
||||||
|
|
||||||
|
|
||||||
def test_load_cassette_persister_exception_handling(tmpdir, httpbin):
|
def test_load_cassette_persister_exception_handling(tmpdir, httpbin):
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
"""Test requests' interaction with vcr"""
|
"""Test requests' interaction with vcr"""
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
import vcr
|
import vcr
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
"""Test requests' interaction with vcr"""
|
"""Test requests' interaction with vcr"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import functools
|
import functools
|
||||||
import inspect
|
import inspect
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ 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
|
||||||
|
|
||||||
|
|
||||||
@@ -410,3 +411,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
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import contextlib
|
import contextlib
|
||||||
|
import http.client as httplib
|
||||||
|
from io import BytesIO
|
||||||
|
from tempfile import NamedTemporaryFile
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
from pytest import mark
|
from pytest import mark
|
||||||
|
|
||||||
from vcr import mode
|
from vcr import mode, use_cassette
|
||||||
from vcr.cassette import Cassette
|
from vcr.cassette import Cassette
|
||||||
from vcr.stubs import VCRHTTPSConnection
|
from vcr.stubs import VCRHTTPSConnection
|
||||||
|
|
||||||
@@ -21,3 +24,52 @@ class TestVCRConnection:
|
|||||||
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
|
||||||
|
|||||||
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",)
|
||||||
@@ -372,3 +372,19 @@ def test_path_class_as_cassette():
|
|||||||
)
|
)
|
||||||
with use_cassette(path):
|
with use_cassette(path):
|
||||||
pass
|
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
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from logging import NullHandler
|
|||||||
from .config import VCR
|
from .config import VCR
|
||||||
from .record_mode import RecordMode as mode # noqa: F401
|
from .record_mode import RecordMode as mode # noqa: F401
|
||||||
|
|
||||||
__version__ = "6.0.0"
|
__version__ = "7.0.0"
|
||||||
|
|
||||||
logging.getLogger(__name__).addHandler(NullHandler())
|
logging.getLogger(__name__).addHandler(NullHandler())
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import contextlib
|
|||||||
import copy
|
import copy
|
||||||
import inspect
|
import inspect
|
||||||
import logging
|
import logging
|
||||||
from asyncio import iscoroutinefunction
|
from inspect import iscoroutinefunction
|
||||||
|
|
||||||
import wrapt
|
import wrapt
|
||||||
|
|
||||||
@@ -125,7 +125,7 @@ class CassetteContextDecorator:
|
|||||||
duration of the generator.
|
duration of the generator.
|
||||||
"""
|
"""
|
||||||
with self as cassette:
|
with self as cassette:
|
||||||
yield from fn(cassette)
|
return (yield from fn(cassette))
|
||||||
|
|
||||||
def _handle_function(self, fn):
|
def _handle_function(self, fn):
|
||||||
with self as cassette:
|
with self as cassette:
|
||||||
@@ -177,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
|
||||||
@@ -189,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 = []
|
||||||
@@ -196,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())
|
||||||
@@ -215,7 +221,7 @@ 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"""
|
||||||
@@ -257,6 +263,7 @@ 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(
|
||||||
@@ -317,12 +324,36 @@ 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):
|
||||||
@@ -330,6 +361,7 @@ class Cassette:
|
|||||||
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):
|
||||||
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 (CassetteDecodeError, CassetteNotFoundError):
|
except (CassetteDecodeError, CassetteNotFoundError):
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ class VCR:
|
|||||||
func_path_generator=None,
|
func_path_generator=None,
|
||||||
decode_compressed_response=False,
|
decode_compressed_response=False,
|
||||||
record_on_exception=True,
|
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,6 +82,7 @@ class VCR:
|
|||||||
self.decode_compressed_response = decode_compressed_response
|
self.decode_compressed_response = decode_compressed_response
|
||||||
self.record_on_exception = record_on_exception
|
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:
|
||||||
@@ -151,6 +153,7 @@ class VCR:
|
|||||||
"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,
|
"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:
|
||||||
|
|||||||
@@ -3,11 +3,10 @@ import logging
|
|||||||
import urllib
|
import urllib
|
||||||
import xmlrpc.client
|
import xmlrpc.client
|
||||||
from string import hexdigits
|
from string import hexdigits
|
||||||
from typing import List, Set
|
|
||||||
|
|
||||||
from .util import read_body
|
from .util import read_body
|
||||||
|
|
||||||
_HEXDIG_CODE_POINTS: Set[int] = {ord(s.encode("ascii")) for s in hexdigits}
|
_HEXDIG_CODE_POINTS: set[int] = {ord(s.encode("ascii")) for s in hexdigits}
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -109,7 +108,7 @@ def _dechunk(body):
|
|||||||
CHUNK_GAP = b"\r\n"
|
CHUNK_GAP = b"\r\n"
|
||||||
BODY_LEN: int = len(body)
|
BODY_LEN: int = len(body)
|
||||||
|
|
||||||
chunks: List[bytes] = []
|
chunks: list[bytes] = []
|
||||||
pos: int = 0
|
pos: int = 0
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
"""Utilities for patching in cassettes"""
|
"""Utilities for patching in cassettes"""
|
||||||
|
|
||||||
import contextlib
|
import contextlib
|
||||||
import functools
|
import functools
|
||||||
import http.client as httplib
|
import http.client as httplib
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import warnings
|
|||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from urllib.parse import parse_qsl, urlparse
|
from urllib.parse import parse_qsl, urlparse
|
||||||
|
|
||||||
from .util import CaseInsensitiveDict
|
from .util import CaseInsensitiveDict, _is_nonsequence_iterator
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -17,13 +17,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
|
||||||
@@ -36,7 +48,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):
|
||||||
@@ -54,30 +70,29 @@ class Request:
|
|||||||
|
|
||||||
@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:
|
try:
|
||||||
port = {"https": 443, "http": 80}[parse_uri.scheme]
|
port = {"https": 443, "http": 80}[self.parsed_uri.scheme]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
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
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ 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
|
self._original_response = self # for requests.session.Session cookie extraction
|
||||||
@@ -186,22 +187,34 @@ 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 f":{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 = f"{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 = f"{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):
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
"""Stubs for aiohttp HTTP clients"""
|
"""Stubs for aiohttp HTTP clients"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import functools
|
import functools
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
from collections.abc import Mapping
|
||||||
from http.cookies import CookieError, Morsel, SimpleCookie
|
from http.cookies import CookieError, Morsel, SimpleCookie
|
||||||
from typing import Mapping, Union
|
from typing import Union
|
||||||
|
|
||||||
from aiohttp import ClientConnectionError, ClientResponse, CookieJar, RequestInfo, hdrs, streams
|
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
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
"""Stubs for boto3"""
|
"""Stubs for boto3"""
|
||||||
|
|
||||||
from botocore.awsrequest import AWSHTTPConnection as HTTPConnection
|
from botocore.awsrequest import AWSHTTPConnection as HTTPConnection
|
||||||
from botocore.awsrequest import AWSHTTPSConnection as VerifiedHTTPSConnection
|
from botocore.awsrequest import AWSHTTPSConnection as VerifiedHTTPSConnection
|
||||||
|
|
||||||
|
|||||||
@@ -166,6 +166,22 @@ def async_vcr_send(cassette, real_send):
|
|||||||
return _inner_send
|
return _inner_send
|
||||||
|
|
||||||
|
|
||||||
|
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 _sync_vcr_send(cassette, real_send, *args, **kwargs):
|
def _sync_vcr_send(cassette, real_send, *args, **kwargs):
|
||||||
vcr_request, response = _shared_vcr_send(cassette, real_send, *args, **kwargs)
|
vcr_request, response = _shared_vcr_send(cassette, real_send, *args, **kwargs)
|
||||||
if response:
|
if response:
|
||||||
@@ -174,7 +190,7 @@ def _sync_vcr_send(cassette, real_send, *args, **kwargs):
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
real_response = real_send(*args, **kwargs)
|
real_response = real_send(*args, **kwargs)
|
||||||
asyncio.run(_record_responses(cassette, vcr_request, real_response, aread=False))
|
_run_async_function(_record_responses, cassette, vcr_request, real_response, aread=False)
|
||||||
return real_response
|
return real_response
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
19
vcr/util.py
19
vcr/util.py
@@ -89,9 +89,28 @@ def compose(*functions):
|
|||||||
return composed
|
return composed
|
||||||
|
|
||||||
|
|
||||||
|
def _is_nonsequence_iterator(obj):
|
||||||
|
return hasattr(obj, "__iter__") and not isinstance(
|
||||||
|
obj,
|
||||||
|
(bytearray, bytes, dict, list, str),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def read_body(request):
|
def read_body(request):
|
||||||
if hasattr(request.body, "read"):
|
if hasattr(request.body, "read"):
|
||||||
return request.body.read()
|
return request.body.read()
|
||||||
|
if _is_nonsequence_iterator(request.body):
|
||||||
|
body = list(request.body)
|
||||||
|
if body:
|
||||||
|
if isinstance(body[0], str):
|
||||||
|
return "".join(body).encode("utf-8")
|
||||||
|
elif isinstance(body[0], (bytes, bytearray)):
|
||||||
|
return b"".join(body)
|
||||||
|
elif isinstance(body[0], int):
|
||||||
|
return bytes(body)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Body type {type(body[0])} not supported")
|
||||||
|
return b""
|
||||||
return request.body
|
return request.body
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user