diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 829e7806..01611fb9 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -18,11 +18,6 @@ jobs: matrix: python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] os: ["macos-latest", "windows-latest", "ubuntu-latest"] - include: - - python-version: "3.6" - os: "ubuntu-20.04" - - python-version: "3.6" - os: "windows-latest" steps: - uses: "actions/checkout@v3" diff --git a/cachecontrol/_cmd.py b/cachecontrol/_cmd.py index 9d668a47..684a4a8b 100644 --- a/cachecontrol/_cmd.py +++ b/cachecontrol/_cmd.py @@ -1,6 +1,7 @@ # SPDX-FileCopyrightText: 2015 Eric Larson # # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations import logging from argparse import ArgumentParser @@ -36,7 +37,7 @@ def get_session() -> requests.Session: return sess -def get_args() -> "Namespace": +def get_args() -> Namespace: parser = ArgumentParser() parser.add_argument("url", help="The URL to try and cache") return parser.parse_args() @@ -53,7 +54,7 @@ def main() -> None: setup_logging() # try setting the cache - cache_controller: "CacheController" = ( + cache_controller: CacheController = ( sess.cache_controller # type: ignore[attr-defined] ) cache_controller.cache_response(resp.request, resp.raw) diff --git a/cachecontrol/adapter.py b/cachecontrol/adapter.py index c0fed6ff..bf4a23dd 100644 --- a/cachecontrol/adapter.py +++ b/cachecontrol/adapter.py @@ -1,11 +1,12 @@ # SPDX-FileCopyrightText: 2015 Eric Larson # # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations import functools import types import zlib -from typing import TYPE_CHECKING, Any, Collection, Mapping, Optional, Tuple, Type, Union +from typing import TYPE_CHECKING, Any, Collection, Mapping from requests.adapters import HTTPAdapter @@ -27,16 +28,16 @@ class CacheControlAdapter(HTTPAdapter): def __init__( self, - cache: Optional["BaseCache"] = None, + cache: BaseCache | None = None, cache_etags: bool = True, - controller_class: Optional[Type[CacheController]] = None, - serializer: Optional["Serializer"] = None, - heuristic: Optional["BaseHeuristic"] = None, - cacheable_methods: Optional[Collection[str]] = None, + controller_class: type[CacheController] | None = None, + serializer: Serializer | None = None, + heuristic: BaseHeuristic | None = None, + cacheable_methods: Collection[str] | None = None, *args: Any, **kw: Any, ) -> None: - super(CacheControlAdapter, self).__init__(*args, **kw) + super().__init__(*args, **kw) self.cache = DictCache() if cache is None else cache self.heuristic = heuristic self.cacheable_methods = cacheable_methods or ("GET",) @@ -48,16 +49,14 @@ def __init__( def send( self, - request: "PreparedRequest", + request: PreparedRequest, stream: bool = False, - timeout: Union[None, float, Tuple[float, float], Tuple[float, None]] = None, - verify: Union[bool, str] = True, - cert: Union[ - None, bytes, str, Tuple[Union[bytes, str], Union[bytes, str]] - ] = None, - proxies: Optional[Mapping[str, str]] = None, - cacheable_methods: Optional[Collection[str]] = None, - ) -> "Response": + timeout: None | float | tuple[float, float] | tuple[float, None] = None, + verify: bool | str = True, + cert: (None | bytes | str | tuple[bytes | str, bytes | str]) = None, + proxies: Mapping[str, str] | None = None, + cacheable_methods: Collection[str] | None = None, + ) -> Response: """ Send a request. Use the request information to see if it exists in the cache and cache the response if we need to and can. @@ -74,19 +73,17 @@ def send( # check for etags and add headers if appropriate request.headers.update(self.controller.conditional_headers(request)) - resp = super(CacheControlAdapter, self).send( - request, stream, timeout, verify, cert, proxies - ) + resp = super().send(request, stream, timeout, verify, cert, proxies) return resp def build_response( self, - request: "PreparedRequest", - response: "HTTPResponse", + request: PreparedRequest, + response: HTTPResponse, from_cache: bool = False, - cacheable_methods: Optional[Collection[str]] = None, - ) -> "Response": + cacheable_methods: Collection[str] | None = None, + ) -> Response: """ Build a response by making a request or using the cache. @@ -137,7 +134,7 @@ def build_response( if response.chunked: super_update_chunk_length = response._update_chunk_length # type: ignore[attr-defined] - def _update_chunk_length(self: "HTTPResponse") -> None: + def _update_chunk_length(self: HTTPResponse) -> None: super_update_chunk_length() if self.chunk_left == 0: self._fp._close() # type: ignore[attr-defined] @@ -146,9 +143,7 @@ def _update_chunk_length(self: "HTTPResponse") -> None: _update_chunk_length, response ) - resp: "Response" = super( # type: ignore[no-untyped-call] - CacheControlAdapter, self - ).build_response(request, response) + resp: Response = super().build_response(request, response) # type: ignore[no-untyped-call] # See if we should invalidate the cache. if request.method in self.invalidating_methods and resp.ok: @@ -163,4 +158,4 @@ def _update_chunk_length(self: "HTTPResponse") -> None: def close(self) -> None: self.cache.close() - super(CacheControlAdapter, self).close() # type: ignore[no-untyped-call] + super().close() # type: ignore[no-untyped-call] diff --git a/cachecontrol/cache.py b/cachecontrol/cache.py index 61031d23..3293b005 100644 --- a/cachecontrol/cache.py +++ b/cachecontrol/cache.py @@ -6,19 +6,21 @@ The cache object API for implementing caches. The default is a thread safe in-memory dictionary. """ +from __future__ import annotations + from threading import Lock -from typing import IO, TYPE_CHECKING, MutableMapping, Optional, Union +from typing import IO, TYPE_CHECKING, MutableMapping if TYPE_CHECKING: from datetime import datetime -class BaseCache(object): - def get(self, key: str) -> Optional[bytes]: +class BaseCache: + def get(self, key: str) -> bytes | None: raise NotImplementedError() def set( - self, key: str, value: bytes, expires: Optional[Union[int, "datetime"]] = None + self, key: str, value: bytes, expires: int | datetime | None = None ) -> None: raise NotImplementedError() @@ -30,15 +32,15 @@ def close(self) -> None: class DictCache(BaseCache): - def __init__(self, init_dict: Optional[MutableMapping[str, bytes]] = None) -> None: + def __init__(self, init_dict: MutableMapping[str, bytes] | None = None) -> None: self.lock = Lock() self.data = init_dict or {} - def get(self, key: str) -> Optional[bytes]: + def get(self, key: str) -> bytes | None: return self.data.get(key, None) def set( - self, key: str, value: bytes, expires: Optional[Union[int, "datetime"]] = None + self, key: str, value: bytes, expires: int | datetime | None = None ) -> None: with self.lock: self.data.update({key: value}) @@ -65,7 +67,7 @@ class SeparateBodyBaseCache(BaseCache): def set_body(self, key: str, body: bytes) -> None: raise NotImplementedError() - def get_body(self, key: str) -> Optional["IO[bytes]"]: + def get_body(self, key: str) -> IO[bytes] | None: """ Return the body as file-like object. """ diff --git a/cachecontrol/caches/file_cache.py b/cachecontrol/caches/file_cache.py index 08759b54..a4ddb5e4 100644 --- a/cachecontrol/caches/file_cache.py +++ b/cachecontrol/caches/file_cache.py @@ -1,11 +1,12 @@ # SPDX-FileCopyrightText: 2015 Eric Larson # # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations import hashlib import os from textwrap import dedent -from typing import IO, TYPE_CHECKING, Optional, Type, Union +from typing import IO, TYPE_CHECKING from cachecontrol.cache import BaseCache, SeparateBodyBaseCache from cachecontrol.controller import CacheController @@ -16,7 +17,7 @@ from filelock import BaseFileLock -def _secure_open_write(filename: str, fmode: int) -> "IO[bytes]": +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 @@ -39,7 +40,7 @@ def _secure_open_write(filename: str, fmode: int) -> "IO[bytes]": # there try: os.remove(filename) - except (IOError, OSError): + except OSError: # The file must not exist already, so we can just skip ahead to opening pass @@ -66,7 +67,7 @@ def __init__( forever: bool = False, filemode: int = 0o0600, dirmode: int = 0o0700, - lock_class: Optional[Type["BaseFileLock"]] = None, + lock_class: type[BaseFileLock] | None = None, ) -> None: try: if lock_class is None: @@ -100,7 +101,7 @@ def _fn(self, name: str) -> str: parts = list(hashed[:5]) + [hashed] return os.path.join(self.directory, *parts) - def get(self, key: str) -> Optional[bytes]: + def get(self, key: str) -> bytes | None: name = self._fn(key) try: with open(name, "rb") as fh: @@ -110,7 +111,7 @@ def get(self, key: str) -> Optional[bytes]: return None def set( - self, key: str, value: bytes, expires: Optional[Union[int, "datetime"]] = None + self, key: str, value: bytes, expires: int | datetime | None = None ) -> None: name = self._fn(key) self._write(name, value) @@ -122,7 +123,7 @@ def _write(self, path: str, data: bytes) -> None: # Make sure the directory exists try: os.makedirs(os.path.dirname(path), self.dirmode) - except (IOError, OSError): + except OSError: pass with self.lock_class(path + ".lock"): @@ -155,7 +156,7 @@ class SeparateBodyFileCache(_FileCacheMixin, SeparateBodyBaseCache): peak memory usage. """ - def get_body(self, key: str) -> Optional["IO[bytes]"]: + def get_body(self, key: str) -> IO[bytes] | None: name = self._fn(key) + ".body" try: return open(name, "rb") diff --git a/cachecontrol/caches/redis_cache.py b/cachecontrol/caches/redis_cache.py index ee037646..f859e719 100644 --- a/cachecontrol/caches/redis_cache.py +++ b/cachecontrol/caches/redis_cache.py @@ -1,11 +1,11 @@ # SPDX-FileCopyrightText: 2015 Eric Larson # # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations -from __future__ import division from datetime import datetime, timezone -from typing import TYPE_CHECKING, Optional, Union +from typing import TYPE_CHECKING from cachecontrol.cache import BaseCache @@ -14,14 +14,14 @@ class RedisCache(BaseCache): - def __init__(self, conn: "Redis[bytes]") -> None: + def __init__(self, conn: Redis[bytes]) -> None: self.conn = conn - def get(self, key: str) -> Optional[bytes]: + def get(self, key: str) -> bytes | None: return self.conn.get(key) def set( - self, key: str, value: bytes, expires: Optional[Union[int, datetime]] = None + self, key: str, value: bytes, expires: int | datetime | None = None ) -> None: if not expires: self.conn.set(key, value) diff --git a/cachecontrol/controller.py b/cachecontrol/controller.py index 86bb5a52..1de50ce2 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 from email.utils import parsedate_tz -from typing import TYPE_CHECKING, Collection, Dict, Mapping, Optional, Tuple, Union +from typing import TYPE_CHECKING, Collection, Mapping from requests.structures import CaseInsensitiveDict @@ -32,7 +34,7 @@ PERMANENT_REDIRECT_STATUSES = (301, 308) -def parse_uri(uri: str) -> Tuple[str, str, str, str, str]: +def parse_uri(uri: str) -> tuple[str, str, str, str, str]: """Parses a URI using the regex given in Appendix B of RFC 3986. (scheme, authority, path, query, fragment) = parse_uri(uri) @@ -43,15 +45,15 @@ def parse_uri(uri: str) -> Tuple[str, str, str, str, str]: return (groups[1], groups[3], groups[4], groups[6], groups[8]) -class CacheController(object): +class CacheController: """An interface to see if request should cached or not.""" def __init__( self, - cache: Optional["BaseCache"] = None, + cache: BaseCache | None = None, cache_etags: bool = True, - serializer: Optional[Serializer] = None, - status_codes: Optional[Collection[int]] = None, + serializer: Serializer | None = None, + status_codes: Collection[int] | None = None, ): self.cache = DictCache() if cache is None else cache self.cache_etags = cache_etags @@ -82,9 +84,7 @@ def _urlnorm(cls, uri: str) -> str: def cache_url(cls, uri: str) -> str: return cls._urlnorm(uri) - def parse_cache_control( - self, headers: Mapping[str, str] - ) -> Dict[str, Optional[int]]: + def parse_cache_control(self, headers: Mapping[str, str]) -> dict[str, int | None]: known_directives = { # https://tools.ietf.org/html/rfc7234#section-5.2 "max-age": (int, True), @@ -103,7 +103,7 @@ def parse_cache_control( cc_headers = headers.get("cache-control", headers.get("Cache-Control", "")) - retval: Dict[str, Optional[int]] = {} + retval: dict[str, int | None] = {} for cc_directive in cc_headers.split(","): if not cc_directive.strip(): @@ -138,7 +138,7 @@ def parse_cache_control( return retval - def _load_from_cache(self, request: "PreparedRequest") -> Optional["HTTPResponse"]: + def _load_from_cache(self, request: PreparedRequest) -> HTTPResponse | None: """ Load a cached response, or return None if it's not available. """ @@ -159,9 +159,7 @@ def _load_from_cache(self, request: "PreparedRequest") -> Optional["HTTPResponse logger.warning("Cache entry deserialization failed, entry ignored") return result - def cached_request( - self, request: "PreparedRequest" - ) -> Union["HTTPResponse", "Literal[False]"]: + def cached_request(self, request: PreparedRequest) -> HTTPResponse | Literal[False]: """ Return a cached response if it exists in the cache, otherwise return False. @@ -271,7 +269,7 @@ def cached_request( # return the original handler return False - def conditional_headers(self, request: "PreparedRequest") -> Dict[str, str]: + def conditional_headers(self, request: PreparedRequest) -> dict[str, str]: resp = self._load_from_cache(request) new_headers = {} @@ -289,10 +287,10 @@ def conditional_headers(self, request: "PreparedRequest") -> Dict[str, str]: def _cache_set( self, cache_url: str, - request: "PreparedRequest", - response: "HTTPResponse", - body: Optional[bytes] = None, - expires_time: Optional[int] = None, + request: PreparedRequest, + response: HTTPResponse, + body: bytes | None = None, + expires_time: int | None = None, ) -> None: """ Store the data in the cache. @@ -318,10 +316,10 @@ def _cache_set( def cache_response( self, - request: "PreparedRequest", - response: "HTTPResponse", - body: Optional[bytes] = None, - status_codes: Optional[Collection[int]] = None, + request: PreparedRequest, + response: HTTPResponse, + body: bytes | None = None, + status_codes: Collection[int] | None = None, ) -> None: """ Algorithm for caching requests. @@ -400,7 +398,7 @@ def cache_response( expires_time = max(expires_time, 14 * 86400) - logger.debug("etag object cached for {0} seconds".format(expires_time)) + logger.debug(f"etag object cached for {expires_time} seconds") logger.debug("Caching due to etag") self._cache_set(cache_url, request, response, body, expires_time) @@ -441,7 +439,7 @@ def cache_response( expires_time = None logger.debug( - "Caching b/c of expires header. expires in {0} seconds".format( + "Caching b/c of expires header. expires in {} seconds".format( expires_time ) ) @@ -454,8 +452,8 @@ def cache_response( ) def update_cached_response( - self, request: "PreparedRequest", response: "HTTPResponse" - ) -> "HTTPResponse": + self, request: PreparedRequest, response: HTTPResponse + ) -> HTTPResponse: """On a 304 we will get a new set of headers that we want to update our cached value with, assuming we have one. @@ -480,11 +478,11 @@ def update_cached_response( excluded_headers = ["content-length"] cached_response.headers.update( - dict( - (k, v) + { + k: v for k, v in response.headers.items() # type: ignore[no-untyped-call] if k.lower() not in excluded_headers - ) + } ) # we want a 200 b/c we have content via the cache diff --git a/cachecontrol/filewrapper.py b/cachecontrol/filewrapper.py index 472ba600..25143902 100644 --- a/cachecontrol/filewrapper.py +++ b/cachecontrol/filewrapper.py @@ -1,16 +1,17 @@ # SPDX-FileCopyrightText: 2015 Eric Larson # # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations import mmap from tempfile import NamedTemporaryFile -from typing import TYPE_CHECKING, Any, Callable, Optional +from typing import TYPE_CHECKING, Any, Callable if TYPE_CHECKING: from http.client import HTTPResponse -class CallbackFileWrapper(object): +class CallbackFileWrapper: """ Small wrapper around a fp object which will tee everything read into a buffer, and when that file is closed it will execute a callback with the @@ -30,7 +31,7 @@ class CallbackFileWrapper(object): """ def __init__( - self, fp: "HTTPResponse", callback: Optional[Callable[[bytes], None]] + self, fp: HTTPResponse, callback: Callable[[bytes], None] | None ) -> None: self.__buf = NamedTemporaryFile("rb+", delete=True) self.__fp = fp @@ -93,7 +94,7 @@ def _close(self) -> None: # Important when caching big files. self.__buf.close() - def read(self, amt: Optional[int] = None) -> bytes: + def read(self, amt: int | None = None) -> bytes: data: bytes = self.__fp.read(amt) if data: # We may be dealing with b'', a sign that things are over: diff --git a/cachecontrol/heuristics.py b/cachecontrol/heuristics.py index 5188c661..323262be 100644 --- a/cachecontrol/heuristics.py +++ b/cachecontrol/heuristics.py @@ -1,12 +1,13 @@ # SPDX-FileCopyrightText: 2015 Eric Larson # # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations import calendar import time from datetime import datetime, timedelta, timezone from email.utils import formatdate, parsedate, parsedate_tz -from typing import TYPE_CHECKING, Any, Dict, Mapping, Optional +from typing import TYPE_CHECKING, Any, Mapping if TYPE_CHECKING: from urllib3 import HTTPResponse @@ -14,7 +15,7 @@ TIME_FMT = "%a, %d %b %Y %H:%M:%S GMT" -def expire_after(delta: timedelta, date: Optional[datetime] = None) -> datetime: +def expire_after(delta: timedelta, date: datetime | None = None) -> datetime: date = date or datetime.now(timezone.utc) return date + delta @@ -23,8 +24,8 @@ def datetime_to_header(dt: datetime) -> str: return formatdate(calendar.timegm(dt.timetuple())) -class BaseHeuristic(object): - def warning(self, response: "HTTPResponse") -> Optional[str]: +class BaseHeuristic: + def warning(self, response: HTTPResponse) -> str | None: """ Return a valid 1xx warning header value describing the cache adjustments. @@ -35,7 +36,7 @@ def warning(self, response: "HTTPResponse") -> Optional[str]: """ return '110 - "Response is Stale"' - def update_headers(self, response: "HTTPResponse") -> Dict[str, str]: + def update_headers(self, response: HTTPResponse) -> dict[str, str]: """Update the response headers with any new headers. NOTE: This SHOULD always include some Warning header to @@ -44,7 +45,7 @@ def update_headers(self, response: "HTTPResponse") -> Dict[str, str]: """ return {} - def apply(self, response: "HTTPResponse") -> "HTTPResponse": + def apply(self, response: HTTPResponse) -> HTTPResponse: updated_headers = self.update_headers(response) if updated_headers: @@ -62,7 +63,7 @@ class OneDayCache(BaseHeuristic): future. """ - def update_headers(self, response: "HTTPResponse") -> Dict[str, str]: + def update_headers(self, response: HTTPResponse) -> dict[str, str]: headers = {} if "expires" not in response.headers: @@ -81,11 +82,11 @@ class ExpiresAfter(BaseHeuristic): def __init__(self, **kw: Any) -> None: self.delta = timedelta(**kw) - def update_headers(self, response: "HTTPResponse") -> Dict[str, str]: + def update_headers(self, response: HTTPResponse) -> dict[str, str]: expires = expire_after(self.delta) return {"expires": datetime_to_header(expires), "cache-control": "public"} - def warning(self, response: "HTTPResponse") -> Optional[str]: + def warning(self, response: HTTPResponse) -> str | None: tmpl = "110 - Automatically cached for %s. Response might be stale" return tmpl % self.delta @@ -117,7 +118,7 @@ class LastModified(BaseHeuristic): 501, } - def update_headers(self, resp: "HTTPResponse") -> Dict[str, str]: + def update_headers(self, resp: HTTPResponse) -> dict[str, str]: headers: Mapping[str, str] = resp.headers if "expires" in headers: @@ -149,5 +150,5 @@ def update_headers(self, resp: "HTTPResponse") -> Dict[str, str]: expires = date + freshness_lifetime return {"expires": time.strftime(TIME_FMT, time.gmtime(expires))} - def warning(self, resp: "HTTPResponse") -> Optional[str]: + def warning(self, resp: HTTPResponse) -> str | None: return None diff --git a/cachecontrol/serialize.py b/cachecontrol/serialize.py index ace07f8e..51e04a6c 100644 --- a/cachecontrol/serialize.py +++ b/cachecontrol/serialize.py @@ -1,9 +1,10 @@ # SPDX-FileCopyrightText: 2015 Eric Larson # # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations import io -from typing import IO, TYPE_CHECKING, Any, Dict, Mapping, Optional, cast +from typing import IO, TYPE_CHECKING, Any, Mapping, cast import msgpack from requests.structures import CaseInsensitiveDict @@ -18,9 +19,9 @@ class Serializer(object): def dumps( self, - request: "PreparedRequest", + request: PreparedRequest, response: HTTPResponse, - body: Optional[bytes] = None, + body: bytes | None = None, ) -> bytes: response_headers: CaseInsensitiveDict[str] = CaseInsensitiveDict( response.headers @@ -37,7 +38,7 @@ def dumps( data = { "response": { "body": body, # Empty bytestring if body is stored separately - "headers": dict((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()}, # type: ignore[no-untyped-call] "status": response.status, "version": response.version, "reason": str(response.reason), @@ -58,15 +59,15 @@ def dumps( return b",".join([f"cc={self.serde_version}".encode(), self.serialize(data)]) - def serialize(self, data: Dict[str, Any]) -> bytes: + def serialize(self, data: dict[str, Any]) -> bytes: return cast(bytes, msgpack.dumps(data, use_bin_type=True)) def loads( self, - request: "PreparedRequest", + request: PreparedRequest, data: bytes, - body_file: Optional["IO[bytes]"] = None, - ) -> Optional[HTTPResponse]: + body_file: IO[bytes] | None = None, + ) -> HTTPResponse | None: # Short circuit if we've been given an empty set of data if not data: return None @@ -89,7 +90,7 @@ def loads( # Dispatch to the actual load method for the given version try: - return getattr(self, "_loads_v{}".format(verstr))(request, data, body_file) # type: ignore[no-any-return] + 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 @@ -98,10 +99,10 @@ def loads( def prepare_response( self, - request: "Request", + request: Request, cached: Mapping[str, Any], - body_file: Optional["IO[bytes]"] = None, - ) -> Optional[HTTPResponse]: + body_file: IO[bytes] | None = None, + ) -> HTTPResponse | None: """Verify our vary headers match and construct a real urllib3 HTTPResponse object. """ @@ -129,7 +130,7 @@ def prepare_response( cached["response"]["headers"] = headers try: - body: "IO[bytes]" + body: IO[bytes] if body_file is None: body = io.BytesIO(body_raw) else: @@ -150,9 +151,9 @@ def prepare_response( def _loads_v0( self, - request: "Request", + request: Request, data: bytes, - body_file: Optional["IO[bytes]"] = None, + 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 @@ -161,20 +162,20 @@ def _loads_v0( def _loads_v1( self, - request: "Request", + request: Request, data: bytes, - body_file: Optional["IO[bytes]"] = None, - ) -> Optional[HTTPResponse]: + 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: "Request", + request: Request, data: bytes, - body_file: Optional["IO[bytes]"] = None, - ) -> Optional[HTTPResponse]: + 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. @@ -182,9 +183,9 @@ def _loads_v2( def _loads_v3( self, - request: "Request", + request: Request, data: bytes, - body_file: Optional["IO[bytes]"] = None, + 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 @@ -193,10 +194,10 @@ def _loads_v3( def _loads_v4( self, - request: "Request", + request: Request, data: bytes, - body_file: Optional["IO[bytes]"] = None, - ) -> Optional[HTTPResponse]: + body_file: IO[bytes] | None = None, + ) -> HTTPResponse | None: try: cached = msgpack.loads(data, raw=False) except ValueError: diff --git a/cachecontrol/wrapper.py b/cachecontrol/wrapper.py index 724438aa..37ee07c7 100644 --- a/cachecontrol/wrapper.py +++ b/cachecontrol/wrapper.py @@ -1,8 +1,9 @@ # SPDX-FileCopyrightText: 2015 Eric Larson # # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations -from typing import TYPE_CHECKING, Collection, Optional, Type +from typing import TYPE_CHECKING, Collection from cachecontrol.adapter import CacheControlAdapter from cachecontrol.cache import DictCache @@ -17,15 +18,15 @@ def CacheControl( - sess: "requests.Session", - cache: Optional["BaseCache"] = None, + sess: requests.Session, + cache: BaseCache | None = None, cache_etags: bool = True, - serializer: Optional["Serializer"] = None, - heuristic: Optional["BaseHeuristic"] = None, - controller_class: Optional[Type["CacheController"]] = None, - adapter_class: Optional[Type[CacheControlAdapter]] = None, - cacheable_methods: Optional[Collection[str]] = None, -) -> "requests.Session": + serializer: Serializer | None = None, + heuristic: BaseHeuristic | None = None, + controller_class: type[CacheController] | None = None, + adapter_class: type[CacheControlAdapter] | None = None, + cacheable_methods: Collection[str] | None = None, +) -> requests.Session: cache = DictCache() if cache is None else cache adapter_class = adapter_class or CacheControlAdapter adapter = adapter_class( diff --git a/docs/release_notes.rst b/docs/release_notes.rst index a5a9c9b8..4a6d55cb 100644 --- a/docs/release_notes.rst +++ b/docs/release_notes.rst @@ -12,6 +12,7 @@ Unreleased * Support for old serialization formats has been removed. * Move the serialization implementation into own method. +* Drop support for Python older than 3.7. 0.13.0 ====== diff --git a/setup.py b/setup.py index 6f84263e..a1b408b5 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ install_requires=["requests>=2.16.0", "msgpack>=0.5.2"], extras_require={"filecache": ["filelock>=3.8.0"], "redis": ["redis>=2.10.5"]}, entry_points={"console_scripts": ["doesitcache = cachecontrol._cmd:main"]}, - python_requires=">=3.6", + python_requires=">=3.7", classifiers=[ "Development Status :: 4 - Beta", "Environment :: Web Environment", diff --git a/tests/conftest.py b/tests/conftest.py index d363d909..e6231e59 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,7 +12,7 @@ import cherrypy -class SimpleApp(object): +class SimpleApp: def __init__(self): self.etag_count = 0 @@ -53,7 +53,7 @@ def vary_accept(self, env, start_response): def update_etag_string(self): self.etag_count += 1 - self.etag_string = '"ETAG-{}"'.format(self.etag_count) + self.etag_string = f'"ETAG-{self.etag_count}"' def update_etag(self, env, start_response): self.update_etag_string() @@ -86,16 +86,16 @@ def no_cache(self, env, start_response): def permanent_redirect(self, env, start_response): headers = [("Location", "/permalink")] start_response("301 Moved Permanently", headers) - return ["See: /permalink".encode("utf-8")] + return [b"See: /permalink"] def permalink(self, env, start_response): start_response("200 OK", [("Content-Type", "text/plain")]) - return ["The permanent resource".encode("utf-8")] + return [b"The permanent resource"] def multiple_choices(self, env, start_response): headers = [("Link", "/permalink")] start_response("300 Multiple Choices", headers) - return ["See: /permalink".encode("utf-8")] + return [b"See: /permalink"] def stream(self, env, start_response): headers = [("Content-Type", "text/plain"), ("Cache-Control", "max-age=5000")] diff --git a/tests/test_adapter.py b/tests/test_adapter.py index ab21f38f..fcac6831 100644 --- a/tests/test_adapter.py +++ b/tests/test_adapter.py @@ -34,7 +34,7 @@ def sess(url, request): sess.close() -class TestSessionActions(object): +class TestSessionActions: def test_get_caches(self, url, sess): r2 = sess.get(url) diff --git a/tests/test_cache_control.py b/tests/test_cache_control.py index bb9afbd0..7d893cd9 100644 --- a/tests/test_cache_control.py +++ b/tests/test_cache_control.py @@ -20,7 +20,7 @@ TIME_FMT = "%a, %d %b %Y %H:%M:%S GMT" -class TestCacheControllerResponse(object): +class TestCacheControllerResponse: url = "http://url.com/" def req(self, headers=None): @@ -221,7 +221,7 @@ def update_cached_response_with_valid_headers_test(self, cache): assert r.read() == b"my body" -class TestCacheControlRequest(object): +class TestCacheControlRequest: url = "http://foo.com/bar" def setup_method(self): diff --git a/tests/test_chunked_response.py b/tests/test_chunked_response.py index 46840870..f0be8023 100644 --- a/tests/test_chunked_response.py +++ b/tests/test_chunked_response.py @@ -4,7 +4,6 @@ """ Test for supporting streamed responses (Transfer-Encoding: chunked) """ -from __future__ import print_function, unicode_literals import pytest import requests @@ -21,7 +20,7 @@ def sess(): sess.close() -class TestChunkedResponses(object): +class TestChunkedResponses: def test_cache_chunked_response(self, url, sess): """ diff --git a/tests/test_etag.py b/tests/test_etag.py index 3df6614d..b4963118 100644 --- a/tests/test_etag.py +++ b/tests/test_etag.py @@ -13,7 +13,7 @@ from tests.utils import NullSerializer -class TestETag(object): +class TestETag: """Test our equal priority caching with ETags Equal Priority Caching is a term I've defined to describe when @@ -80,7 +80,7 @@ def test_etags_get_example(self, sess, server): assert self.cache.get(self.etag_url) == resp.raw -class TestDisabledETags(object): +class TestDisabledETags: """Test our use of ETags when the response is stale and the response has an ETag. """ @@ -117,7 +117,7 @@ def test_expired_etags_if_none_match_response(self, sess): assert r.status_code == 200 -class TestReleaseConnection(object): +class TestReleaseConnection: """ On 304s we still make a request using our connection pool, yet we do not call the parent adapter, which releases the connection diff --git a/tests/test_expires_heuristics.py b/tests/test_expires_heuristics.py index c0895efc..2a5fd23b 100644 --- a/tests/test_expires_heuristics.py +++ b/tests/test_expires_heuristics.py @@ -23,7 +23,7 @@ from .utils import DummyResponse -class TestHeuristicWithoutWarning(object): +class TestHeuristicWithoutWarning: def setup_method(self): class NoopHeuristic(BaseHeuristic): warning = Mock() @@ -41,7 +41,7 @@ def test_no_header_change_means_no_warning_header(self, url): assert not self.heuristic.warning.called -class TestHeuristicWith3xxResponse(object): +class TestHeuristicWith3xxResponse: def setup_method(self): class DummyHeuristic(BaseHeuristic): def update_headers(self, resp): @@ -60,14 +60,14 @@ def test_heuristic_applies_to_304(self, url): assert "x-dummy-header" in resp.headers -class TestUseExpiresHeuristic(object): +class TestUseExpiresHeuristic: def test_expires_heuristic_arg(self): sess = Session() cached_sess = CacheControl(sess, heuristic=Mock()) assert cached_sess -class TestOneDayCache(object): +class TestOneDayCache: def setup_method(self): self.sess = Session() self.cached_sess = CacheControl(self.sess, heuristic=OneDayCache()) @@ -86,7 +86,7 @@ def test_cache_for_one_day(self, url): assert r.from_cache -class TestExpiresAfter(object): +class TestExpiresAfter: def setup_method(self): self.sess = Session() self.cache_sess = CacheControl(self.sess, heuristic=ExpiresAfter(days=1)) @@ -106,7 +106,7 @@ def test_expires_after_one_day(self, url): assert r.from_cache -class TestLastModified(object): +class TestLastModified: def setup_method(self): self.sess = Session() self.cached_sess = CacheControl(self.sess, heuristic=LastModified()) @@ -129,7 +129,7 @@ def datetime_to_header(dt): return formatdate(calendar.timegm(dt.timetuple())) -class TestModifiedUnitTests(object): +class TestModifiedUnitTests: def last_modified(self, period): return time.strftime(TIME_FMT, time.gmtime(self.time_now - period)) diff --git a/tests/test_max_age.py b/tests/test_max_age.py index 4755c573..09a00cea 100644 --- a/tests/test_max_age.py +++ b/tests/test_max_age.py @@ -2,7 +2,6 @@ # # SPDX-License-Identifier: Apache-2.0 -from __future__ import print_function import pytest from requests import Session @@ -11,7 +10,7 @@ from .utils import NullSerializer -class TestMaxAge(object): +class TestMaxAge: @pytest.fixture() def sess(self, url): diff --git a/tests/test_redirects.py b/tests/test_redirects.py index af9723f2..a37bb2b2 100644 --- a/tests/test_redirects.py +++ b/tests/test_redirects.py @@ -10,7 +10,7 @@ from cachecontrol import CacheControl -class TestPermanentRedirects(object): +class TestPermanentRedirects: def setup_method(self): self.sess = CacheControl(requests.Session()) @@ -31,7 +31,7 @@ def test_bust_cache_on_redirect(self, url): assert not resp.from_cache -class TestMultipleChoicesRedirects(object): +class TestMultipleChoicesRedirects: def setup_method(self): self.sess = CacheControl(requests.Session()) diff --git a/tests/test_regressions.py b/tests/test_regressions.py index a072fd73..7109c2a9 100644 --- a/tests/test_regressions.py +++ b/tests/test_regressions.py @@ -12,7 +12,7 @@ from requests import Session -class Test39(object): +class Test39: @pytest.mark.skipif( sys.version.startswith("2"), reason="Only run this for python 3.x" diff --git a/tests/test_serialization.py b/tests/test_serialization.py index cc2b0b20..1ae4f00a 100644 --- a/tests/test_serialization.py +++ b/tests/test_serialization.py @@ -11,14 +11,14 @@ from cachecontrol.serialize import Serializer -class TestSerializer(object): +class TestSerializer: def setup_method(self): self.serializer = Serializer() self.response_data = { "response": { # Encode the body as bytes b/c it will eventually be # converted back into a BytesIO object. - "body": "Hello World".encode("utf-8"), + "body": b"Hello World", "headers": { "Content-Type": "text/plain", "Expires": "87654", @@ -60,7 +60,7 @@ def test_read_version_v4(self): req = Mock() resp = self.serializer._loads_v4(req, msgpack.dumps(self.response_data)) # We have to decode our urllib3 data back into a unicode string. - assert resp.data == "Hello World".encode("utf-8") + assert resp.data == b"Hello World" def test_read_latest_version_streamable(self, url): original_resp = requests.get(url, stream=True) diff --git a/tests/test_storage_filecache.py b/tests/test_storage_filecache.py index 8c93284a..f194deb8 100644 --- a/tests/test_storage_filecache.py +++ b/tests/test_storage_filecache.py @@ -21,10 +21,10 @@ def randomdata(): """Plain random http data generator:""" key = "".join(sample(string.ascii_lowercase, randint(2, 4))) val = "".join(sample(string.ascii_lowercase + string.digits, randint(2, 10))) - return "&{}={}".format(key, val) + return f"&{key}={val}" -class FileCacheTestsMixin(object): +class FileCacheTestsMixin: FileCacheClass = None # Either FileCache or SeparateBodyFileCache diff --git a/tests/test_storage_redis.py b/tests/test_storage_redis.py index ac5b22c0..5e794b65 100644 --- a/tests/test_storage_redis.py +++ b/tests/test_storage_redis.py @@ -8,7 +8,7 @@ from cachecontrol.caches import RedisCache -class TestRedisCache(object): +class TestRedisCache: def setup_method(self): self.conn = Mock() self.cache = RedisCache(self.conn) diff --git a/tests/test_vary.py b/tests/test_vary.py index b1fc0c7a..c8e0cecc 100644 --- a/tests/test_vary.py +++ b/tests/test_vary.py @@ -12,7 +12,7 @@ from cachecontrol.cache import DictCache -class TestVary(object): +class TestVary: @pytest.fixture() def sess(self, url): self.url = urljoin(url, "/vary_accept") diff --git a/tox.ini b/tox.ini index c8025374..b945e7cd 100644 --- a/tox.ini +++ b/tox.ini @@ -7,7 +7,6 @@ envlist = py{36,37,38,39,310,311}, mypy [gh-actions] python = - 3.6: py36 3.7: py37 3.8: py38 3.9: py39 @@ -17,8 +16,8 @@ python = [testenv] deps = pytest cherrypy - redis - filelock + redis>=2.10.5 + filelock>=3.8.0 commands = py.test {posargs:tests/} [testenv:mypy]