From ab550d9a7a07c3c88c05f39b6fb3b022840786de Mon Sep 17 00:00:00 2001 From: Jair Henrique Date: Wed, 19 Nov 2025 07:11:10 -0300 Subject: [PATCH] Drops Python 3.9 support --- .github/workflows/main.yml | 19 +--- .pre-commit-config.yaml | 2 +- docs/changelog.rst | 4 + pyproject.toml | 7 +- setup.py | 25 +---- tests/integration/test_aiohttp.py | 6 -- tests/integration/test_tornado.py | 14 +-- tests/integration/test_urllib2.py | 147 ------------------------------ tests/unit/test_serialize.py | 2 +- tests/unit/test_unittest.py | 4 +- vcr/cassette.py | 2 +- vcr/matchers.py | 2 +- vcr/serialize.py | 2 +- vcr/stubs/aiohttp_stubs.py | 3 +- 14 files changed, 27 insertions(+), 212 deletions(-) delete mode 100644 tests/integration/test_urllib2.py diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8586435..4133226 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -6,7 +6,7 @@ on: - master pull_request: schedule: - - cron: '0 16 * * 5' # Every Friday 4pm + - cron: "0 16 * * 5" # Every Friday 4pm workflow_dispatch: jobs: @@ -16,24 +16,12 @@ jobs: fail-fast: false matrix: python-version: - - "3.9" - "3.10" - "3.11" - "3.12" - "3.13" - - "pypy-3.9" - "pypy-3.10" - urllib3-requirement: - - "urllib3>=2" - - "urllib3<2" - - exclude: - - python-version: "3.9" - urllib3-requirement: "urllib3>=2" - - python-version: "pypy-3.9" - urllib3-requirement: "urllib3>=2" - - python-version: "pypy-3.10" - urllib3-requirement: "urllib3>=2" + - "pypy-3.11" steps: - uses: actions/checkout@v5 @@ -44,13 +32,12 @@ jobs: uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - cache: pip allow-prereleases: true - name: Install project dependencies run: | uv pip install --system --upgrade pip setuptools - uv pip install --system codecov '.[tests]' '${{ matrix.urllib3-requirement }}' + uv pip install --system codecov '.[tests]' uv pip check - name: Allow creation of user namespaces (e.g. to the unshare command) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ada33d3..91947b0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.7 + rev: v0.14.5 hooks: - id: ruff args: ["--output-format=full"] diff --git a/docs/changelog.rst b/docs/changelog.rst index 8947493..7547633 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -7,6 +7,10 @@ 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. +- 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 diff --git a/pyproject.toml b/pyproject.toml index cc4bab1..c69082d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,14 +2,15 @@ skip = '.git,*.pdf,*.svg,.tox' ignore-regex = "\\\\[fnrstv]" -[tool.pytest.ini_options] +[tool.pytest] addopts = ["--strict-config", "--strict-markers"] -asyncio_default_fixture_loop_scope = "function" +asyncio_default_fixture_loop_scope = "session" +asyncio_default_test_loop_scope = "session" markers = ["online"] [tool.ruff] line-length = 110 -target-version = "py39" +target-version = "py310" [tool.ruff.lint] select = [ diff --git a/setup.py b/setup.py index f3f06c1..cb55cdf 100644 --- a/setup.py +++ b/setup.py @@ -30,18 +30,6 @@ install_requires = [ "PyYAML", "wrapt", "yarl", - # Support for urllib3 >=2 needs CPython >=3.10 - # so we need to block urllib3 >=2 for Python <3.10 and PyPy for now. - # Note that vcrpy would work fine without any urllib3 around, - # so this block and the dependency can be dropped at some point - # in the future. For more Details: - # https://github.com/kevin1024/vcrpy/pull/699#issuecomment-1551439663 - "urllib3 <2; python_version <'3.10'", - # https://github.com/kevin1024/vcrpy/pull/775#issuecomment-1847849962 - "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 = { @@ -49,22 +37,16 @@ extras_require = { "aiohttp", "boto3", "httplib2", + "httpbin", "httpx", + "pytest", "pytest-aiohttp", "pytest-asyncio", "pytest-cov", "pytest-httpbin", - "pytest", "requests>=2.22.0", "tornado", "urllib3", - # Needed to un-break httpbin 0.7.0. For httpbin >=0.7.1 and after, - # this pin and the dependency itself can be removed, provided - # that the related bug in httpbin has been fixed: - # https://github.com/kevin1024/vcrpy/issues/645#issuecomment-1562489489 - # https://github.com/postmanlabs/httpbin/issues/673 - # https://github.com/postmanlabs/httpbin/pull/674 - "Werkzeug==2.0.3", ], } @@ -78,7 +60,7 @@ setup( author_email="me@kevinmccarthy.org", url="https://github.com/kevin1024/vcrpy", packages=find_packages(exclude=["tests*"]), - python_requires=">=3.9", + python_requires=">=3.10", install_requires=install_requires, license="MIT", extras_require=extras_require, @@ -89,7 +71,6 @@ setup( "Intended Audience :: Developers", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", diff --git a/tests/integration/test_aiohttp.py b/tests/integration/test_aiohttp.py index c1ae748..eaa2fd1 100644 --- a/tests/integration/test_aiohttp.py +++ b/tests/integration/test_aiohttp.py @@ -264,12 +264,6 @@ def test_aiohttp_test_client_json(aiohttp_client, tmpdir): assert cassette.play_count == 1 -def test_cleanup_from_pytest_asyncio(): - # work around https://github.com/pytest-dev/pytest-asyncio/issues/724 - asyncio.get_event_loop().close() - asyncio.set_event_loop(None) - - @pytest.mark.online def test_redirect(tmpdir, httpbin): url = httpbin.url + "/redirect/2" diff --git a/tests/integration/test_tornado.py b/tests/integration/test_tornado.py index 8de8d3d..205825a 100644 --- a/tests/integration/test_tornado.py +++ b/tests/integration/test_tornado.py @@ -42,17 +42,13 @@ def scheme(request): return request.param -@pytest.fixture(params=["simple", "curl", "default"]) +@pytest.fixture(params=["curl", "default"]) def get_client(request): - if request.param == "simple": - from tornado import simple_httpclient as simple - - return lambda: simple.SimpleAsyncHTTPClient() - elif request.param == "curl": + if request.param == "curl": curl = pytest.importorskip("tornado.curl_httpclient") return lambda: curl.CurlAsyncHTTPClient() - else: - return lambda: http.AsyncHTTPClient() + + return lambda: http.AsyncHTTPClient() def get(client, url, **kwargs): @@ -192,7 +188,7 @@ def test_redirects(get_client, tmpdir, httpbin): @pytest.mark.online @gen_test -def test_cross_scheme(get_client, tmpdir, scheme): +def test_cross_scheme(get_client, tmpdir): """Ensure that requests between schemes are treated separately""" # 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 diff --git a/tests/integration/test_urllib2.py b/tests/integration/test_urllib2.py deleted file mode 100644 index 078c14a..0000000 --- a/tests/integration/test_urllib2.py +++ /dev/null @@ -1,147 +0,0 @@ -"""Integration tests with urllib2""" - -import ssl -from urllib.parse import urlencode -from urllib.request import urlopen - -import pytest_httpbin.certs -from pytest import mark - -# 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) - - -@mark.online -def test_effective_url(tmpdir, httpbin): - """Ensure that the effective_url is captured""" - url = httpbin.url + "/redirect-to?url=.%2F&status_code=301" - - with vcr.use_cassette(str(tmpdir.join("headers.yaml"))): - effective_url = urlopen_with_cafile(url).geturl() - assert effective_url == httpbin.url + "/" - - 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() diff --git a/tests/unit/test_serialize.py b/tests/unit/test_serialize.py index 7dd8a45..270cd12 100644 --- a/tests/unit/test_serialize.py +++ b/tests/unit/test_serialize.py @@ -76,7 +76,7 @@ def test_deserialize_py2py3_yaml_cassette(tmpdir, req_body, expect): cfile = tmpdir.join("test_cassette.yaml") cfile.write(REQBODY_TEMPLATE.format(req_body=req_body)) with open(str(cfile)) as f: - (requests, responses) = deserialize(f.read(), yamlserializer) + (requests, _) = deserialize(f.read(), yamlserializer) assert requests[0].body == expect diff --git a/tests/unit/test_unittest.py b/tests/unit/test_unittest.py index 1ecdee3..85baf70 100644 --- a/tests/unit/test_unittest.py +++ b/tests/unit/test_unittest.py @@ -178,7 +178,7 @@ def test_testcase_playback(tmpdir): return str(cassette_dir) test = run_testcase(MyTest)[0][0] - assert b"illustrative examples" in test.response + assert b"Example Domain" in test.response assert len(test.cassette.requests) == 1 assert test.cassette.play_count == 0 @@ -186,7 +186,7 @@ def test_testcase_playback(tmpdir): test2 = run_testcase(MyTest)[0][0] assert test.cassette is not test2.cassette - assert b"illustrative examples" in test.response + assert b"Example Domain" in test.response assert len(test2.cassette.requests) == 1 assert test2.cassette.play_count == 1 diff --git a/vcr/cassette.py b/vcr/cassette.py index 1ac06dd..3e5081e 100644 --- a/vcr/cassette.py +++ b/vcr/cassette.py @@ -359,7 +359,7 @@ class Cassette: def _load(self): try: 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._old_interactions.append((request, response)) self.dirty = False diff --git a/vcr/matchers.py b/vcr/matchers.py index cbdeabe..1295f21 100644 --- a/vcr/matchers.py +++ b/vcr/matchers.py @@ -162,7 +162,7 @@ def _get_transformers(request): def requests_match(r1, r2, matchers): - successes, failures = get_matchers_results(r1, r2, matchers) + _, failures = get_matchers_results(r1, r2, matchers) if failures: log.debug(f"Requests {r1} and {r2} differ.\nFailure details:\n{failures}") return len(failures) == 0 diff --git a/vcr/serialize.py b/vcr/serialize.py index 0eec264..7707e22 100644 --- a/vcr/serialize.py +++ b/vcr/serialize.py @@ -53,7 +53,7 @@ def serialize(cassette_dict, serializer): "request": compat.convert_to_unicode(request._to_dict()), "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} return serializer.serialize(data) diff --git a/vcr/stubs/aiohttp_stubs.py b/vcr/stubs/aiohttp_stubs.py index 88be90a..dc4bfa5 100644 --- a/vcr/stubs/aiohttp_stubs.py +++ b/vcr/stubs/aiohttp_stubs.py @@ -6,7 +6,6 @@ import json import logging from collections.abc import Mapping from http.cookies import CookieError, Morsel, SimpleCookie -from typing import Union from aiohttp import ClientConnectionError, ClientResponse, CookieJar, RequestInfo, hdrs, streams from aiohttp.helpers import strip_auth_from_url @@ -230,7 +229,7 @@ def _build_cookie_header(session, cookies, cookie_header, url): return c.output(header="", sep=";").strip() -def _build_url_with_params(url_str: str, params: Mapping[str, Union[str, int, float]]) -> URL: +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)