From 06894cc93d103e73328cba3984841ca031fb58db Mon Sep 17 00:00:00 2001 From: "deepin-community-bot[bot]" <156989552+deepin-community-bot[bot]@users.noreply.github.com> Date: Mon, 5 Jan 2026 02:48:31 +0000 Subject: [PATCH] feat: update python-cachecontrol to 0.14.3-1 --- .bumpversion.cfg | 5 -- Makefile | 14 ++-- README.rst | 2 +- cachecontrol/__init__.py | 3 +- cachecontrol/adapter.py | 29 +++++---- cachecontrol/cache.py | 1 + cachecontrol/caches/file_cache.py | 62 ++++-------------- cachecontrol/controller.py | 21 +++++- cachecontrol/filewrapper.py | 4 +- cachecontrol/heuristics.py | 5 +- cachecontrol/serialize.py | 76 +++------------------- debian/changelog | 35 ++++++++++ debian/control | 3 +- debian/copyright | 4 +- debian/patches/no-doesitcache-script.patch | 4 +- debian/tests/control | 1 - docs/conf.py | 13 ++-- docs/etags.rst | 2 +- docs/index.rst | 21 ++++-- docs/release_notes.rst | 34 ++++++++++ docs/requirements.txt | 1 + docs/storage.rst | 4 +- docs/tips.rst | 2 +- examples/benchmark.py | 8 +-- pyproject.toml | 26 ++++---- tests/conftest.py | 5 +- tests/issue_263.py | 3 - tests/test_adapter.py | 32 ++++++++- tests/test_cache_control.py | 9 +-- tests/test_chunked_response.py | 1 - tests/test_etag.py | 25 +++++++ tests/test_max_age.py | 1 - tests/test_redirects.py | 1 + tests/test_regressions.py | 5 -- tests/test_serialization.py | 1 - tests/test_storage_filecache.py | 2 +- tests/test_storage_redis.py | 3 +- tests/utils.py | 2 +- tox.ini | 5 +- 39 files changed, 262 insertions(+), 213 deletions(-) delete mode 100644 .bumpversion.cfg create mode 100644 docs/requirements.txt diff --git a/.bumpversion.cfg b/.bumpversion.cfg deleted file mode 100644 index 437b22e..0000000 --- a/.bumpversion.cfg +++ /dev/null @@ -1,5 +0,0 @@ -[bumpversion] -current_version = 0.13.0 -files = cachecontrol/__init__.py docs/conf.py -commit = True -tag = True diff --git a/Makefile b/Makefile index 26b3831..62feb92 100644 --- a/Makefile +++ b/Makefile @@ -5,19 +5,17 @@ VENV=.venv VENV_CMD=python3 -m venv ACTIVATE = $(VENV)/bin/activate -CHEESE=https://pypi.python.org/pypi -BUMPTYPE=patch -BUMPPRE=0 - -$(VENV)/bin/pip3: +$(VENV)/bin/pip: $(VENV_CMD) $(VENV) -bootstrap: $(VENV)/bin/pip3 - $(VENV)/bin/pip3 install -e .[dev] +bootstrap: $(VENV)/bin/pip + $(VENV)/bin/pip install -e .[dev] format: - $(VENV)/bin/black . + $(VENV)/bin/codespell + $(VENV)/bin/ruff check --fix + $(VENV)/bin/ruff format doc: $(VENV)/bin/sphinx-build . $(ACTIVATE); diff --git a/README.rst b/README.rst index 37f70db..df5e7b4 100644 --- a/README.rst +++ b/README.rst @@ -35,7 +35,7 @@ Quickstart sess = requests.session() cached_sess = CacheControl(sess) - response = cached_sess.get('http://google.com') + response = cached_sess.get('https://google.com') If the URL contains any caching based headers, it will cache the result in a simple dictionary. diff --git a/cachecontrol/__init__.py b/cachecontrol/__init__.py index abbf8c0..62e00b1 100644 --- a/cachecontrol/__init__.py +++ b/cachecontrol/__init__.py @@ -6,9 +6,10 @@ Make it easy to import from cachecontrol without long namespaces. """ + __author__ = "Eric Larson" __email__ = "eric@ionrock.org" -__version__ = "0.13.1" +__version__ = "0.14.3" from cachecontrol.adapter import CacheControlAdapter from cachecontrol.controller import CacheController diff --git a/cachecontrol/adapter.py b/cachecontrol/adapter.py index bf4a23d..9a32634 100644 --- a/cachecontrol/adapter.py +++ b/cachecontrol/adapter.py @@ -5,6 +5,7 @@ import functools import types +import weakref import zlib from typing import TYPE_CHECKING, Any, Collection, Mapping @@ -77,7 +78,7 @@ def send( return resp - def build_response( + def build_response( # type: ignore[override] self, request: PreparedRequest, response: HTTPResponse, @@ -125,25 +126,31 @@ 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 + self.controller.cache_response, request, weakref.ref(response) ), ) if response.chunked: - super_update_chunk_length = response._update_chunk_length # type: ignore[attr-defined] + super_update_chunk_length = response.__class__._update_chunk_length - def _update_chunk_length(self: HTTPResponse) -> None: - super_update_chunk_length() + def _update_chunk_length( + weak_self: weakref.ReferenceType[HTTPResponse], + ) -> None: + self = weak_self() + if self is None: + return + + super_update_chunk_length(self) 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] - _update_chunk_length, response + response._update_chunk_length = functools.partial( # type: ignore[method-assign] + _update_chunk_length, weakref.ref(response) ) - resp: Response = super().build_response(request, response) # type: ignore[no-untyped-call] + resp: Response = super().build_response(request, response) # See if we should invalidate the cache. if request.method in self.invalidating_methods and resp.ok: diff --git a/cachecontrol/cache.py b/cachecontrol/cache.py index 3293b00..91598e9 100644 --- a/cachecontrol/cache.py +++ b/cachecontrol/cache.py @@ -6,6 +6,7 @@ The cache object API for implementing caches. The default is a thread safe in-memory dictionary. """ + from __future__ import annotations from threading import Lock diff --git a/cachecontrol/caches/file_cache.py b/cachecontrol/caches/file_cache.py index a4ddb5e..b0bf5bf 100644 --- a/cachecontrol/caches/file_cache.py +++ b/cachecontrol/caches/file_cache.py @@ -5,8 +5,10 @@ import hashlib import os +import tempfile from textwrap import dedent from typing import IO, TYPE_CHECKING +from pathlib import Path from cachecontrol.cache import BaseCache, SeparateBodyBaseCache from cachecontrol.controller import CacheController @@ -17,53 +19,12 @@ from filelock import BaseFileLock -def _secure_open_write(filename: str, fmode: int) -> IO[bytes]: - # We only want to write to this file, so open it in write only mode - flags = os.O_WRONLY - - # os.O_CREAT | os.O_EXCL will fail if the file already exists, so we only - # will open *new* files. - # We specify this because we want to ensure that the mode we pass is the - # mode of the file. - flags |= os.O_CREAT | os.O_EXCL - - # Do not follow symlinks to prevent someone from making a symlink that - # we follow and insecurely open a cache file. - if hasattr(os, "O_NOFOLLOW"): - flags |= os.O_NOFOLLOW - - # On Windows we'll mark this file as binary - if hasattr(os, "O_BINARY"): - flags |= os.O_BINARY - - # Before we open our file, we want to delete any existing file that is - # there - try: - os.remove(filename) - except OSError: - # The file must not exist already, so we can just skip ahead to opening - pass - - # Open our file, the use of os.O_CREAT | os.O_EXCL will ensure that if a - # race condition happens between the os.remove and this line, that an - # error will be raised. Because we utilize a lockfile this should only - # happen if someone is attempting to attack us. - fd = os.open(filename, flags, fmode) - try: - return os.fdopen(fd, "wb") - - except: - # An error occurred wrapping our FD in a file object - os.close(fd) - raise - - class _FileCacheMixin: """Shared implementation for both FileCache variants.""" def __init__( self, - directory: str, + directory: str | Path, forever: bool = False, filemode: int = 0o0600, dirmode: int = 0o0700, @@ -79,7 +40,7 @@ def __init__( """ NOTE: In order to use the FileCache you must have filelock installed. You can install it via pip: - pip install filelock + pip install cachecontrol[filecache] """ ) raise ImportError(notice) @@ -121,15 +82,18 @@ def _write(self, path: str, data: bytes) -> None: Safely write the data to the given path. """ # Make sure the directory exists - try: - os.makedirs(os.path.dirname(path), self.dirmode) - except OSError: - pass + dirname = os.path.dirname(path) + os.makedirs(dirname, self.dirmode, exist_ok=True) with self.lock_class(path + ".lock"): # Write our actual file - with _secure_open_write(path, self.filemode) as fh: - fh.write(data) + (fd, name) = tempfile.mkstemp(dir=dirname) + try: + os.write(fd, data) + finally: + os.close(fd) + os.chmod(name, self.filemode) + os.replace(name, path) def _delete(self, key: str, suffix: str) -> None: name = self._fn(key) + suffix diff --git a/cachecontrol/controller.py b/cachecontrol/controller.py index 1de50ce..4e251c8 100644 --- a/cachecontrol/controller.py +++ b/cachecontrol/controller.py @@ -5,12 +5,14 @@ """ The httplib2 algorithms ported for use with requests. """ + from __future__ import annotations import calendar import logging import re import time +import weakref from email.utils import parsedate_tz from typing import TYPE_CHECKING, Collection, Mapping @@ -142,6 +144,11 @@ def _load_from_cache(self, request: PreparedRequest) -> HTTPResponse | None: """ Load a cached response, or return None if it's not available. """ + # We do not support caching of partial content: so if the request contains a + # Range header then we don't want to load anything from the cache. + if "Range" in request.headers: + return None + cache_url = request.url assert cache_url is not None cache_data = self.cache.get(cache_url) @@ -317,7 +324,7 @@ def _cache_set( def cache_response( self, request: PreparedRequest, - response: HTTPResponse, + response_or_ref: HTTPResponse | weakref.ReferenceType[HTTPResponse], body: bytes | None = None, status_codes: Collection[int] | None = None, ) -> None: @@ -326,6 +333,16 @@ def cache_response( This assumes a requests Response object. """ + if isinstance(response_or_ref, weakref.ReferenceType): + response = response_or_ref() + if response is None: + # The weakref can be None only in case the user used streamed request + # and did not consume or close it, and holds no reference to requests.Response. + # In such case, we don't want to cache the response. + return + else: + response = response_or_ref + # From httplib2: Don't cache 206's since we aren't going to # handle byte range requests cacheable_status_codes = status_codes or self.cacheable_status_codes @@ -480,7 +497,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/filewrapper.py b/cachecontrol/filewrapper.py index 2514390..37d2fa5 100644 --- a/cachecontrol/filewrapper.py +++ b/cachecontrol/filewrapper.py @@ -38,10 +38,10 @@ def __init__( self.__callback = callback def __getattr__(self, name: str) -> Any: - # The vaguaries of garbage collection means that self.__fp is + # The vagaries of garbage collection means that self.__fp is # not always set. By using __getattribute__ and the private # name[0] allows looking up the attribute value and raising an - # AttributeError when it doesn't exist. This stop thigns from + # AttributeError when it doesn't exist. This stop things from # infinitely recursing calls to getattr in the case where # self.__fp hasn't been set. # diff --git a/cachecontrol/heuristics.py b/cachecontrol/heuristics.py index 323262b..d95e78a 100644 --- a/cachecontrol/heuristics.py +++ b/cachecontrol/heuristics.py @@ -68,7 +68,10 @@ 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 28d3dc9..83bce07 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), @@ -72,31 +72,14 @@ def loads( if not data: return None - # Determine what version of the serializer the data was serialized - # with - try: - ver, data = data.split(b",", 1) - except ValueError: - ver = b"cc=0" - - # Make sure that our "ver" is actually a version and isn't a false - # positive from a , being in the data stream. - if ver[:3] != b"cc=": - data = ver + data - ver = b"cc=0" - - # Get the version number out of the cc=N - verstr = ver.split(b"=", 1)[-1].decode("ascii") - - # Dispatch to the actual load method for the given version - try: - return getattr(self, f"_loads_v{verstr}")(request, data, body_file) # type: ignore[no-any-return] - - except AttributeError: - # This is a version we don't have a loads function for, so we'll - # just treat it as a miss and return None + # Previous versions of this library supported other serialization + # formats, but these have all been removed. + if not data.startswith(f"cc={self.serde_version},".encode()): return None + data = data[5:] + return self._loads_v4(request, data, body_file) + def prepare_response( self, request: PreparedRequest, @@ -149,49 +132,6 @@ def prepare_response( return HTTPResponse(body=body, preload_content=False, **cached["response"]) - def _loads_v0( - self, - request: PreparedRequest, - data: bytes, - body_file: IO[bytes] | None = None, - ) -> None: - # The original legacy cache data. This doesn't contain enough - # information to construct everything we need, so we'll treat this as - # a miss. - return None - - def _loads_v1( - self, - request: PreparedRequest, - data: bytes, - body_file: IO[bytes] | None = None, - ) -> HTTPResponse | None: - # The "v1" pickled cache format. This is no longer supported - # for security reasons, so we treat it as a miss. - return None - - def _loads_v2( - self, - request: PreparedRequest, - data: bytes, - body_file: IO[bytes] | None = None, - ) -> HTTPResponse | None: - # The "v2" compressed base64 cache format. - # This has been removed due to age and poor size/performance - # characteristics, so we treat it as a miss. - return None - - def _loads_v3( - self, - request: PreparedRequest, - data: bytes, - body_file: IO[bytes] | None = None, - ) -> None: - # Due to Python 2 encoding issues, it's impossible to know for sure - # exactly how to load v3 entries, thus we'll treat these as a miss so - # that they get rewritten out as v4 entries. - return None - def _loads_v4( self, request: PreparedRequest, diff --git a/debian/changelog b/debian/changelog index 46c040f..313ba35 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,38 @@ +python-cachecontrol (0.14.3-1) unstable; urgency=medium + + * New upstream point release. + * Refresh patches. + * Bump Standards-Version to 4.7.2, no changes needed. + + -- Stefano Rivera Sat, 03 May 2025 13:02:45 -0400 + +python-cachecontrol (0.14.2-1) unstable; urgency=medium + + * New upstream point release. + * Refresh patches. + * Bump copyright years. + + -- Stefano Rivera Sat, 11 Jan 2025 13:23:44 -0400 + +python-cachecontrol (0.14.1-1) unstable; urgency=medium + + * New upstream point release. + * Bump Standards-Version to 4.7.0, no changes needed. + + -- Stefano Rivera Thu, 07 Nov 2024 17:37:09 -0800 + +python-cachecontrol (0.14.0-1) unstable; urgency=medium + + [ Stefano Rivera ] + * New upstream release. + * Refresh patches. + * Bump copyright years. + + [ Alexandre Detiste ] + * Remove extraneous dependency on python3-mock. + + -- Stefano Rivera Wed, 07 Feb 2024 16:11:27 -0400 + python-cachecontrol (0.13.1-1) unstable; urgency=medium * New upstream release. (Closes: #1019705) diff --git a/debian/control b/debian/control index f2a6573..4054adb 100644 --- a/debian/control +++ b/debian/control @@ -12,12 +12,11 @@ Build-Depends: python3-all, python3-cherrypy3 , python3-filelock , - python3-mock , python3-msgpack , python3-pytest , python3-redis , python3-requests , -Standards-Version: 4.6.2 +Standards-Version: 4.7.2 Rules-Requires-Root: no Vcs-Git: https://salsa.debian.org/python-team/packages/python-cachecontrol.git Vcs-Browser: https://salsa.debian.org/python-team/packages/python-cachecontrol diff --git a/debian/copyright b/debian/copyright index 01e3cce..3a131b8 100644 --- a/debian/copyright +++ b/debian/copyright @@ -3,7 +3,7 @@ Upstream-Name: CacheControl Source: https://github.com/ionrock/cachecontrol Files: * -Copyright: 2015-2022 Eric Larson +Copyright: 2015-2025 Eric Larson License: Apache-2.0 Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -24,7 +24,7 @@ License: Apache-2.0 Files: debian/* Copyright: 2015 Barry Warsaw - 2021-2022 Stefano Rivera + 2021-2025 Stefano Rivera License: Expat Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/debian/patches/no-doesitcache-script.patch b/debian/patches/no-doesitcache-script.patch index 0206c0c..ed616ff 100644 --- a/debian/patches/no-doesitcache-script.patch +++ b/debian/patches/no-doesitcache-script.patch @@ -8,10 +8,10 @@ Don't install the undocumented and unnecessary doesitcache script. 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml -index ef55536..e44db2a 100644 +index d8890eb..8fd3861 100644 --- a/pyproject.toml +++ b/pyproject.toml -@@ -62,7 +62,7 @@ dev = [ +@@ -64,7 +64,7 @@ dev = [ ] [project.scripts] diff --git a/debian/tests/control b/debian/tests/control index 6a3fa4f..55a6a2e 100644 --- a/debian/tests/control +++ b/debian/tests/control @@ -4,7 +4,6 @@ Depends: python3-cachecontrol, python3-cherrypy3, python3-filelock, - python3-mock, python3-pytest, python3-redis Restrictions: allow-stderr diff --git a/docs/conf.py b/docs/conf.py index 957a728..a8f7edd 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 @@ -30,7 +28,12 @@ # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ["sphinx.ext.autodoc", "sphinx.ext.todo", "sphinx.ext.viewcode"] +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.todo", + "sphinx.ext.viewcode", + "sphinx_copybutton", +] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] @@ -86,7 +89,7 @@ # show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = "sphinx" +# pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] @@ -96,7 +99,7 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = "default" +html_theme = "furo" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the diff --git a/docs/etags.rst b/docs/etags.rst index 0128ece..6c9688c 100644 --- a/docs/etags.rst +++ b/docs/etags.rst @@ -18,7 +18,7 @@ expired. In CacheControl the default behavior when an ETag is sent by the server is to cache the response. We'll refer to this pattern as a **Equal Priority** cache as the decision to cache is either time base or -due to the presense of an ETag. +due to the presence of an ETag. The spec is not explicit what takes priority when caching with both ETags and time based headers. Therefore, CacheControl supports the diff --git a/docs/index.rst b/docs/index.rst index ca0d0e2..4e068df 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -66,20 +66,20 @@ effort to faithfully port the tests from httplib2 to CacheControl, but there is a decent chance that I've missed something. Please file bugs if you find any issues! -With that in mind, CacheControl has been used sucessfully in +With that in mind, CacheControl has been used successfully in production environments, replacing httplib2's usage. If you give it a try, please let me know of any issues. .. _httplib2: https://github.com/httplib2/httplib2 -.. _requests: http://docs.python-requests.org/ -.. _Editing the Web: http://www.w3.org/1999/04/Editing/ -.. _PyPI: https://pypi.python.org/pypi/CacheControl/ -.. _pip: http://www.pip-installer.org/ +.. _requests: https://requests.readthedocs.io/en/latest/ +.. _PyPI: https://pypi.org/project/CacheControl/ +.. _pip: https://pip.pypa.io/en/stable/ -Contents: +Contents +======== .. toctree:: :maxdepth: 2 @@ -89,8 +89,15 @@ Contents: etags custom_heuristics tips - release_notes +.. toctree:: + :hidden: + :caption: Development + :maxdepth: 2 + + release_notes + GitHub + PyPI Indices and tables diff --git a/docs/release_notes.rst b/docs/release_notes.rst index be9e939..155dd1c 100644 --- a/docs/release_notes.rst +++ b/docs/release_notes.rst @@ -7,6 +7,28 @@ Release Notes =============== +0.14.3 +====== + +* Drop support for Python 3.8. + +0.14.2 +====== + +* Eliminate a race condition when overwriting cache entries. +* Fix a ``requests`` object leak when doing caching on a streamed response + +0.14.1 +====== + +* Explicitly support Python 3.13. + +0.14.0 +====== + +* Explicitly allow ``pathlib.Path`` as a type for ``FileCache.directory``. +* Don't use the cache to return a full response if a request has a Range header. + 0.13.1 ====== @@ -29,6 +51,18 @@ The project has been moved to the `PSF `_ organization. * Add type annotations. * Exclude the ``tests`` directory from the wheel. +0.12.14 +======= + +* Revert the change "switch lockfile to filelock" to fix the compatibility issue. + +0.12.13 +======= + +* Discard the ``strict`` attribute when serializing and deserializing responses. +* Fix the IncompleteRead error thrown by ``urllib3`` 2.0. +* Exclude the tests directory from the wheel. + 0.12.11 ======= diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..e7ab444 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1 @@ +.[dev] diff --git a/docs/storage.rst b/docs/storage.rst index a2fc3fc..d3e7177 100644 --- a/docs/storage.rst +++ b/docs/storage.rst @@ -77,10 +77,10 @@ Here is an example of using the cache:: import requests from cachecontrol import CacheControl - from cachecontrol.caches SeparateBodyFileCache + from cachecontrol.caches import SeparateBodyFileCache sess = CacheControl(requests.Session(), - cache=SeparatedBodyFileCache('.web_cache')) + cache=SeparateBodyFileCache('.web_cache')) ``SeparateBodyFileCache`` supports the same options as ``FileCache``. diff --git a/docs/tips.rst b/docs/tips.rst index af00bd9..e67668e 100644 --- a/docs/tips.rst +++ b/docs/tips.rst @@ -43,7 +43,7 @@ response. With that in mind, you should be aware that if you try to cache a very large response on a network store, you still might have some latency -tranferring the data from the network store to your +transferring the data from the network store to your application. Another consideration is storing large responses in a `FileCache`. If you are caching using ETags and the server is extremely specific as to what constitutes an equivalent request, it diff --git a/examples/benchmark.py b/examples/benchmark.py index b036f78..5aa0df3 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 ef55536..d8890eb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["flit_core >=3.2,<4"] +requires = ["flit_core >=3.11,<4"] build-backend = "flit_core.buildapi" [tool.flit.module] @@ -13,7 +13,8 @@ name = "CacheControl" dynamic = ["version"] description = "httplib2 caching for requests" readme = "README.rst" -license = { file = "LICENSE.txt" } +license = "Apache-2.0" +license-files = ["LICENSE.txt"] authors = [ { name = "Eric Larson", email = "ericlarson@ionrock.com" }, { name = "Frost Ming", email = "me@frostming.com" }, @@ -22,19 +23,17 @@ authors = [ classifiers = [ "Development Status :: 4 - Beta", "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", + "Programming Language :: Python :: 3.13", "Topic :: Internet :: WWW/HTTP", ] keywords = ["requests", "http", "caching", "web"] -dependencies = ["requests >= 2.16.0", "msgpack >= 0.5.2"] -requires-python = ">=3.7" +dependencies = ["requests >= 2.16.0", "msgpack >= 0.5.2, < 2.0.0"] +requires-python = ">=3.9" [project.urls] Homepage = "https://pypi.org/project/CacheControl/" @@ -50,13 +49,16 @@ redis = ["redis>=2.10.5"] dev = [ "CacheControl[filecache,redis]", "build", + "cherrypy", + "codespell[tomli]", + "furo", "mypy", - "tox", - "pytest-cov", "pytest", - "cherrypy", + "pytest-cov", + "ruff", "sphinx", - "black", + "sphinx-copybutton", + "tox", "types-redis", "types-requests", ] diff --git a/tests/conftest.py b/tests/conftest.py index e6231e5..02dd341 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] @@ -161,5 +160,5 @@ def pytest_configure(config): def pytest_unconfigure(config): try: cherrypy.server.stop() - except: + except: # noqa: E722 pass diff --git a/tests/issue_263.py b/tests/issue_263.py index 66075fe..b049e48 100644 --- a/tests/issue_263.py +++ b/tests/issue_263.py @@ -13,9 +13,6 @@ clogger.setLevel(logging.DEBUG) -from pprint import pprint - - class NoAgeHeuristic(BaseHeuristic): def update_headers(self, response): if "cache-control" in response.headers: diff --git a/tests/test_adapter.py b/tests/test_adapter.py index fcac683..7b299a2 100644 --- a/tests/test_adapter.py +++ b/tests/test_adapter.py @@ -2,10 +2,14 @@ # # SPDX-License-Identifier: Apache-2.0 +import gc +import platform +import weakref from unittest import mock -import pytest +import pytest from requests import Session + from cachecontrol.adapter import CacheControlAdapter from cachecontrol.cache import DictCache from cachecontrol.wrapper import CacheControl @@ -35,7 +39,6 @@ def sess(url, request): class TestSessionActions: - def test_get_caches(self, url, sess): r2 = sess.get(url) assert r2.from_cache is True @@ -66,3 +69,28 @@ def test_close(self): sess.close() assert cache.close.called + + def test_do_not_leak_response(self, url, sess): + resp = sess.get(url + "stream", stream=True) + resp.raise_for_status() + r1_weak = weakref.ref(resp.raw) + + # This is a mis-use of requests, becase we should either consume + # the body, or call .close(). + # But requests without cachecontrol handle this anyway, because + # urllib3.response.HTTPResponse has a __del__ finalizer on it that closes it + # once there are no more references to it. + # We should not break this. + + resp = None + if platform.python_implementation() == "PyPy": + # NOTE: Need to explicitly tell PyPy to collect at this point. + # See: https://github.com/psf/cachecontrol/issues/351 + # See: https://doc.pypy.org/en/latest/cpython_differences.html#differences-related-to-garbage-collection-strategies + gc.collect() + + # Below this point, it should be closed because there are no more references + # to the session response. + + r1 = r1_weak() + assert r1 is None or r1.closed diff --git a/tests/test_cache_control.py b/tests/test_cache_control.py index 7d893cd..40cac5b 100644 --- a/tests/test_cache_control.py +++ b/tests/test_cache_control.py @@ -5,8 +5,9 @@ """ Unit tests that verify our caching methods work correctly. """ + +import os import time -from tempfile import mkdtemp from unittest.mock import ANY, Mock import pytest @@ -142,7 +143,7 @@ def test_update_cached_response_no_local_cache(self): result = cc.update_cached_response(req, resp) assert result is resp - def test_update_cached_response_with_valid_headers_separate_body(self): + def test_update_cached_response_with_valid_headers_separate_body(self, tmp_path): """ If the local cache has the given URL ``update_cached_response()`` will: @@ -151,7 +152,7 @@ def test_update_cached_response_with_valid_headers_separate_body(self): This is the version for a cache that stores a separate body. """ - cache = SeparateBodyFileCache(mkdtemp()) + cache = SeparateBodyFileCache(os.fsdecode(tmp_path)) self.update_cached_response_with_valid_headers_test(cache) def test_update_cached_response_with_valid_headers(self): @@ -175,7 +176,7 @@ def update_cached_response_with_valid_headers_test(self, cache): This is the shared utility for any cache object. """ - # Cache starts out prepopulated wih an entry: + # Cache starts out prepopulated with an entry: etag = "jfd9094r808" cc = CacheController(cache) url = "http://localhost:123/x" diff --git a/tests/test_chunked_response.py b/tests/test_chunked_response.py index f0be802..8cc4496 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_etag.py b/tests/test_etag.py index b496311..2ea8ba1 100644 --- a/tests/test_etag.py +++ b/tests/test_etag.py @@ -79,6 +79,31 @@ def test_etags_get_example(self, sess, server): # Make sure we updated our cache with the new etag'd response. assert self.cache.get(self.etag_url) == resp.raw + def test_etags_get_no_cache(self, sess, server): + """A 'Cache-Control: no-cache' header stops us from using the cache directly, + but not from using the 'If-None-Match' header on the request.""" + # get our response + r = sess.get(self.etag_url) + assert "if-none-match" not in r.request.headers + + r = sess.get(self.etag_url, headers={"Cache-Control": "no-cache"}) + assert "if-none-match" in r.request.headers + assert r.status_code == 200 + + # This response does come from the cache, but only after the 304 response from + # the server told us that was fine. + assert r.from_cache + + def test_etags_get_with_range(self, sess, server): + """A 'Range' header stops us from using the cache altogether.""" + # get our response + r = sess.get(self.etag_url) + + r = sess.get(self.etag_url, headers={"Range": "0-10"}) + assert "if-none-match" not in r.request.headers + assert r.status_code == 200 + assert not r.from_cache + class TestDisabledETags: """Test our use of ETags when the response is stale and the diff --git a/tests/test_max_age.py b/tests/test_max_age.py index 09a00ce..18fe18d 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_redirects.py b/tests/test_redirects.py index a37bb2b..9d6932b 100644 --- a/tests/test_redirects.py +++ b/tests/test_redirects.py @@ -5,6 +5,7 @@ """ Test for supporting redirect caches as needed. """ + import requests from cachecontrol import CacheControl diff --git a/tests/test_regressions.py b/tests/test_regressions.py index 7109c2a..78e8aaa 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_serialization.py b/tests/test_serialization.py index 1ae4f00..eab1c13 100644 --- a/tests/test_serialization.py +++ b/tests/test_serialization.py @@ -2,7 +2,6 @@ # # SPDX-License-Identifier: Apache-2.0 -import pickle from unittest.mock import Mock import msgpack diff --git a/tests/test_storage_filecache.py b/tests/test_storage_filecache.py index f194deb..df74a3b 100644 --- a/tests/test_storage_filecache.py +++ b/tests/test_storage_filecache.py @@ -5,6 +5,7 @@ """ Unit tests that verify FileCache storage works correctly. """ + import os import string @@ -25,7 +26,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 5e794b6..d1c64b4 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 99e67ec..747420d 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 d9ceacf..c7516b4 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