From 7d8689ba0e8affa8678e7af8cf7dd26a254d4592 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 18 Oct 2023 03:24:18 +0300 Subject: [PATCH] Add support for Python 3.12 and drop EOL 3.6 and 3.7 (#323) * Add support for Python 3.12 * Drop support for EOL Python 3.6 * Upgrade Python syntax with pyupgrade --py37-plus * examples, tests: reformat * cachecontrol, tox: run mypy on 3.12 and tweak ignores. --------- Co-authored-by: William Woodruff --- .github/workflows/release.yml | 6 +++--- .github/workflows/tests.yml | 5 +++-- cachecontrol/adapter.py | 10 +++++----- cachecontrol/caches/file_cache.py | 2 +- cachecontrol/controller.py | 2 +- cachecontrol/heuristics.py | 2 +- cachecontrol/serialize.py | 4 ++-- docs/conf.py | 2 -- examples/benchmark.py | 8 ++++---- pyproject.toml | 2 +- tests/conftest.py | 3 +-- tests/test_adapter.py | 1 - tests/test_chunked_response.py | 1 - tests/test_max_age.py | 1 - tests/test_regressions.py | 5 ----- tests/test_storage_filecache.py | 1 - tests/test_storage_redis.py | 3 +-- tests/utils.py | 2 +- tox.ini | 5 +++-- 19 files changed, 27 insertions(+), 38 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 08c05fb2..12fdb682 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,11 +19,11 @@ jobs: contents: write steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-python@v4 with: - python-version: ">= 3.6" + python-version: "3.x" - name: deps run: python -m pip install -U build @@ -35,7 +35,7 @@ jobs: uses: pypa/gh-action-pypi-publish@release/v1 - name: sign - uses: sigstore/gh-action-sigstore-python@v1.2.3 + uses: sigstore/gh-action-sigstore-python@v2.0.1 with: inputs: ./dist/*.tar.gz ./dist/*.whl release-signing-artifacts: true diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 01611fb9..2b6c56f4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -16,14 +16,15 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] os: ["macos-latest", "windows-latest", "ubuntu-latest"] steps: - - uses: "actions/checkout@v3" + - uses: "actions/checkout@v4" - uses: "actions/setup-python@v4" with: python-version: "${{ matrix.python-version }}" + allow-prereleases: true - name: "Install dependencies" run: | python -VV diff --git a/cachecontrol/adapter.py b/cachecontrol/adapter.py index bf4a23dd..d2e65ff0 100644 --- a/cachecontrol/adapter.py +++ b/cachecontrol/adapter.py @@ -125,21 +125,21 @@ def build_response( else: # Wrap the response file with a wrapper that will cache the # response when the stream has been consumed. - response._fp = CallbackFileWrapper( # type: ignore[attr-defined] - response._fp, # type: ignore[attr-defined] + response._fp = CallbackFileWrapper( # type: ignore[assignment] + response._fp, # type: ignore[arg-type] functools.partial( self.controller.cache_response, request, response ), ) if response.chunked: - super_update_chunk_length = response._update_chunk_length # type: ignore[attr-defined] + super_update_chunk_length = response._update_chunk_length def _update_chunk_length(self: HTTPResponse) -> None: super_update_chunk_length() if self.chunk_left == 0: - self._fp._close() # type: ignore[attr-defined] + self._fp._close() # type: ignore[union-attr] - response._update_chunk_length = types.MethodType( # type: ignore[attr-defined] + response._update_chunk_length = types.MethodType( # type: ignore[method-assign] _update_chunk_length, response ) diff --git a/cachecontrol/caches/file_cache.py b/cachecontrol/caches/file_cache.py index 76af2cbb..0f941713 100644 --- a/cachecontrol/caches/file_cache.py +++ b/cachecontrol/caches/file_cache.py @@ -64,7 +64,7 @@ class _FileCacheMixin: def __init__( self, - directory: Union[str, Path], + directory: str | Path, forever: bool = False, filemode: int = 0o0600, dirmode: int = 0o0700, diff --git a/cachecontrol/controller.py b/cachecontrol/controller.py index 1de50ce2..ba602f1d 100644 --- a/cachecontrol/controller.py +++ b/cachecontrol/controller.py @@ -480,7 +480,7 @@ def update_cached_response( cached_response.headers.update( { k: v - for k, v in response.headers.items() # type: ignore[no-untyped-call] + for k, v in response.headers.items() if k.lower() not in excluded_headers } ) diff --git a/cachecontrol/heuristics.py b/cachecontrol/heuristics.py index 323262be..ea1badca 100644 --- a/cachecontrol/heuristics.py +++ b/cachecontrol/heuristics.py @@ -68,7 +68,7 @@ def update_headers(self, response: HTTPResponse) -> dict[str, str]: if "expires" not in response.headers: date = parsedate(response.headers["date"]) - expires = expire_after(timedelta(days=1), date=datetime(*date[:6], tzinfo=timezone.utc)) # type: ignore[misc] + expires = expire_after(timedelta(days=1), date=datetime(*date[:6], tzinfo=timezone.utc)) # type: ignore[index,misc] headers["expires"] = datetime_to_header(expires) headers["cache-control"] = "public" return headers diff --git a/cachecontrol/serialize.py b/cachecontrol/serialize.py index 99045a0a..83bce073 100644 --- a/cachecontrol/serialize.py +++ b/cachecontrol/serialize.py @@ -32,13 +32,13 @@ def dumps( # also update the response with a new file handler to be # sure it acts as though it was never read. body = response.read(decode_content=False) - response._fp = io.BytesIO(body) # type: ignore[attr-defined] + response._fp = io.BytesIO(body) # type: ignore[assignment] response.length_remaining = len(body) data = { "response": { "body": body, # Empty bytestring if body is stored separately - "headers": {str(k): str(v) for k, v in response.headers.items()}, # type: ignore[no-untyped-call] + "headers": {str(k): str(v) for k, v in response.headers.items()}, "status": response.status, "version": response.version, "reason": str(response.reason), diff --git a/docs/conf.py b/docs/conf.py index 170b7920..a8f7edd9 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - # SPDX-FileCopyrightText: 2015 Eric Larson # # SPDX-License-Identifier: Apache-2.0 diff --git a/examples/benchmark.py b/examples/benchmark.py index b036f788..5aa0df35 100644 --- a/examples/benchmark.py +++ b/examples/benchmark.py @@ -13,16 +13,16 @@ HOST = "localhost" PORT = 8050 -URL = "http://{}:{}/".format(HOST, PORT) +URL = f"http://{HOST}:{PORT}/" -class Server(object): - +class Server: def __call__(self, env, sr): body = "Hello World!" status = "200 OK" headers = [ - ("Cache-Control", "max-age=%i" % (60 * 10)), ("Content-Type", "text/plain") + ("Cache-Control", "max-age=%i" % (60 * 10)), + ("Content-Type", "text/plain"), ] sr(status, headers) return body diff --git a/pyproject.toml b/pyproject.toml index 62fddedc..b600b34d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,12 +24,12 @@ classifiers = [ "Environment :: Web Environment", "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", - "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Topic :: Internet :: WWW/HTTP", ] keywords = ["requests", "http", "caching", "web"] diff --git a/tests/conftest.py b/tests/conftest.py index e6231e59..89359cc3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,7 +13,6 @@ class SimpleApp: - def __init__(self): self.etag_count = 0 self.update_etag_string() @@ -109,7 +108,7 @@ def fixed_length(self, env, start_response): headers = [ ("Content-Type", "text/plain"), ("Cache-Control", "max-age=5000"), - ("Content-Length", str(len(body))) + ("Content-Length", str(len(body))), ] start_response("200 OK", headers) return [body] diff --git a/tests/test_adapter.py b/tests/test_adapter.py index fcac6831..2614e098 100644 --- a/tests/test_adapter.py +++ b/tests/test_adapter.py @@ -35,7 +35,6 @@ def sess(url, request): class TestSessionActions: - def test_get_caches(self, url, sess): r2 = sess.get(url) assert r2.from_cache is True diff --git a/tests/test_chunked_response.py b/tests/test_chunked_response.py index f0be8023..8cc44964 100644 --- a/tests/test_chunked_response.py +++ b/tests/test_chunked_response.py @@ -21,7 +21,6 @@ def sess(): class TestChunkedResponses: - def test_cache_chunked_response(self, url, sess): """ Verify that an otherwise cacheable response is cached when the diff --git a/tests/test_max_age.py b/tests/test_max_age.py index 09a00cea..18fe18d9 100644 --- a/tests/test_max_age.py +++ b/tests/test_max_age.py @@ -11,7 +11,6 @@ class TestMaxAge: - @pytest.fixture() def sess(self, url): self.url = url diff --git a/tests/test_regressions.py b/tests/test_regressions.py index 7109c2a9..78e8aaad 100644 --- a/tests/test_regressions.py +++ b/tests/test_regressions.py @@ -2,7 +2,6 @@ # # SPDX-License-Identifier: Apache-2.0 -import sys import pytest @@ -13,10 +12,6 @@ class Test39: - - @pytest.mark.skipif( - sys.version.startswith("2"), reason="Only run this for python 3.x" - ) def test_file_cache_recognizes_consumed_file_handle(self, url): s = CacheControl(Session(), FileCache("web_cache")) the_url = url + "cache_60" diff --git a/tests/test_storage_filecache.py b/tests/test_storage_filecache.py index f194deb8..d9dd2419 100644 --- a/tests/test_storage_filecache.py +++ b/tests/test_storage_filecache.py @@ -25,7 +25,6 @@ def randomdata(): class FileCacheTestsMixin: - FileCacheClass = None # Either FileCache or SeparateBodyFileCache @pytest.fixture() diff --git a/tests/test_storage_redis.py b/tests/test_storage_redis.py index 5e794b65..d1c64b45 100644 --- a/tests/test_storage_redis.py +++ b/tests/test_storage_redis.py @@ -18,8 +18,7 @@ def test_set_expiration_datetime(self): assert self.conn.setex.called def test_set_expiration_datetime_aware(self): - self.cache.set("foo", "bar", - expires=datetime(2014, 2, 2, tzinfo=timezone.utc)) + self.cache.set("foo", "bar", expires=datetime(2014, 2, 2, tzinfo=timezone.utc)) assert self.conn.setex.called def test_set_expiration_int(self): diff --git a/tests/utils.py b/tests/utils.py index 99e67ecb..747420da 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -8,7 +8,6 @@ class NullSerializer(Serializer): - def dumps(self, request, response, body=None): return response @@ -20,6 +19,7 @@ def loads(self, request, data, body_file=None): class DummyResponse: """Match a ``urllib3.response.HTTPResponse``.""" + version = "1.1" reason = b"Because" strict = 0 diff --git a/tox.ini b/tox.ini index d9ceacfd..c7516b46 100644 --- a/tox.ini +++ b/tox.ini @@ -4,15 +4,16 @@ [tox] isolated_build = True -envlist = py{36,37,38,39,310,311}, mypy +envlist = py{37,38,39,310,311,312}, mypy [gh-actions] python = 3.7: py37 3.8: py38 3.9: py39 - 3.10: py310, mypy + 3.10: py310 3.11: py311 + 3.12: py312, mypy [testenv] deps = pytest