Skip to content

Commit 6b40c2b

Browse files
committed
chore: deprecate python 3.9 support, it's EOL soon
1 parent 09c517c commit 6b40c2b

16 files changed

Lines changed: 125 additions & 166 deletions

File tree

.github/workflows/main.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ jobs:
3030
fail-fast: false
3131
matrix:
3232
platform: [ubuntu-latest, macos-latest] # windows-latest
33-
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
33+
python-version: ['3.10', '3.11', '3.12', '3.13']
3434
# vvv just an example of excluding stuff from matrix
3535
# exclude: [{platform: macos-latest, python-version: '3.6'}]
3636

README.md

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -125,10 +125,10 @@ Cachew gives the best of two worlds and makes it both **easy and efficient**. Th
125125

126126
# How it works
127127

128-
- first your objects get [converted](src/cachew/marshall/cachew.py#L31) into a simpler JSON-like representation
128+
- first your objects get [converted](src/cachew/marshall/cachew.py#L32) into a simpler JSON-like representation
129129
- after that, they are mapped into byte blobs via [`orjson`](https://github.com/ijl/orjson).
130130

131-
When the function is called, cachew [computes the hash of your function's arguments ](src/cachew/__init__.py:#L590)
131+
When the function is called, cachew [computes the hash of your function's arguments ](src/cachew/__init__.py:#L575)
132132
and compares it against the previously stored hash value.
133133

134134
- If they match, it would deserialize and yield whatever is stored in the cache database
@@ -140,18 +140,18 @@ and compares it against the previously stored hash value.
140140

141141

142142

143-
* automatic schema inference: [1](src/cachew/tests/test_cachew.py#L386), [2](src/cachew/tests/test_cachew.py#L400)
143+
* automatic schema inference: [1](src/cachew/tests/test_cachew.py#L384), [2](src/cachew/tests/test_cachew.py#L398)
144144
* supported types:
145145

146146
* primitive: `str`, `int`, `float`, `bool`, `datetime`, `date`, `Exception`
147147

148-
See [tests.test_types](src/cachew/tests/test_cachew.py#L686), [tests.test_primitive](src/cachew/tests/test_cachew.py#L724), [tests.test_dates](src/cachew/tests/test_cachew.py#L636), [tests.test_exceptions](src/cachew/tests/test_cachew.py#L1124)
149-
* [@dataclass and NamedTuple](src/cachew/tests/test_cachew.py#L601)
150-
* [Optional](src/cachew/tests/test_cachew.py#L530) types
151-
* [Union](src/cachew/tests/test_cachew.py#L831) types
152-
* [nested datatypes](src/cachew/tests/test_cachew.py#L446)
148+
See [tests.test_types](src/cachew/tests/test_cachew.py#L684), [tests.test_primitive](src/cachew/tests/test_cachew.py#L722), [tests.test_dates](src/cachew/tests/test_cachew.py#L634), [tests.test_exceptions](src/cachew/tests/test_cachew.py#L1122)
149+
* [@dataclass and NamedTuple](src/cachew/tests/test_cachew.py#L599)
150+
* [Optional](src/cachew/tests/test_cachew.py#L528) types
151+
* [Union](src/cachew/tests/test_cachew.py#L829) types
152+
* [nested datatypes](src/cachew/tests/test_cachew.py#L444)
153153

154-
* detects [datatype schema changes](src/cachew/tests/test_cachew.py#L476) and discards old data automatically
154+
* detects [datatype schema changes](src/cachew/tests/test_cachew.py#L474) and discards old data automatically
155155

156156

157157
# Performance
@@ -165,20 +165,20 @@ You can find some of my performance tests in [benchmarks/](benchmarks) dir, and
165165

166166

167167
# Using
168-
See [docstring](src/cachew/__init__.py#L292) for up-to-date documentation on parameters and return types.
168+
See [docstring](src/cachew/__init__.py#L277) for up-to-date documentation on parameters and return types.
169169
You can also use [extensive unit tests](src/cachew/tests/test_cachew.py) as a reference.
170170

171171
Some useful (but optional) arguments of `@cachew` decorator:
172172

173-
* `cache_path` can be a directory, or a callable that [returns a path](src/cachew/tests/test_cachew.py#L423) and depends on function's arguments.
173+
* `cache_path` can be a directory, or a callable that [returns a path](src/cachew/tests/test_cachew.py#L421) and depends on function's arguments.
174174

175175
By default, `settings.DEFAULT_CACHEW_DIR` is used.
176176

177177
* `depends_on` is a function which determines whether your inputs have changed, and the cache needs to be invalidated.
178178

179179
By default it just uses string representation of the arguments, you can also specify a custom callable.
180180

181-
For instance, it can be used to [discard cache](src/cachew/tests/test_cachew.py#L120) if the input file was modified.
181+
For instance, it can be used to [discard cache](src/cachew/tests/test_cachew.py#L118) if the input file was modified.
182182

183183
* `cls` is the type that would be serialized.
184184

@@ -258,7 +258,10 @@ def mcachew(*args, **kwargs):
258258
except ModuleNotFoundError:
259259
import warnings
260260

261-
warnings.warn('cachew library not found. You might want to install it to speed things up. See https://github.com/karlicoss/cachew')
261+
warnings.warn(
262+
'cachew library not found. You might want to install it to speed things up. See https://github.com/karlicoss/cachew',
263+
stacklevel=2,
264+
)
262265
return lambda orig_func: orig_func
263266
else:
264267
return cachew.cachew(*args, **kwargs)
@@ -271,9 +274,9 @@ Now you can use `@mcachew` in place of `@cachew`, and be certain things don't br
271274
## Settings
272275

273276

274-
[cachew.settings](src/cachew/__init__.py#L64) exposes some parameters that allow you to control `cachew` behaviour:
277+
[cachew.settings](src/cachew/__init__.py#L61) exposes some parameters that allow you to control `cachew` behaviour:
275278
- `ENABLE`: set to `False` if you want to disable caching for without removing the decorators (useful for testing and debugging).
276-
You can also use [cachew.extra.disabled_cachew](src/cachew/extra.py#L21) context manager to do it temporarily.
279+
You can also use [cachew.extra.disabled_cachew](src/cachew/extra.py#L24) context manager to do it temporarily.
277280
- `DEFAULT_CACHEW_DIR`: override to set a different base directory. The default is the "user cache directory" (see [platformdirs docs](https://github.com/tox-dev/platformdirs?tab=readme-ov-file#example-output)).
278281
- `THROW_ON_ERROR`: by default, cachew is defensive and simply attemps to cause the original function on caching issues.
279282
Set to `True` to catch errors earlier.

conftest.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
import os
66
import pathlib
7-
from typing import Optional
87

98
import _pytest.main
109
import _pytest.pathlib
@@ -22,7 +21,7 @@
2221
resolve_pkg_path_orig = _pytest.pathlib.resolve_package_path
2322

2423

25-
def resolve_package_path(path: pathlib.Path) -> Optional[pathlib.Path]:
24+
def resolve_package_path(path: pathlib.Path) -> pathlib.Path | None:
2625
result = path # search from the test file upwards
2726
for parent in result.parents:
2827
if str(parent) in namespace_pkg_dirs:

doc/test_serialization.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ class Country:
4949
@dataclass_json
5050
@dataclass
5151
class WithUnion:
52-
union: Union[City, Country]
52+
union: Union[City, Country] # noqa: UP007
5353

5454
objs = [
5555
WithUnion(union=City(name='London')),
@@ -87,7 +87,7 @@ class Country:
8787

8888
@dataclass
8989
class WithUnion:
90-
union: Union[City, Country]
90+
union: Union[City, Country] # noqa: UP007
9191

9292
objs = [
9393
WithUnion(union=City(name="London")),
@@ -123,7 +123,7 @@ class Country:
123123

124124
@dataclass
125125
class WithUnion:
126-
union: Union[City, Country]
126+
union: Union[City, Country] # noqa: UP007
127127

128128
objs = [
129129
WithUnion(union=City(name="London")),
@@ -177,7 +177,7 @@ class Country:
177177

178178
@dataclass
179179
class WithUnion:
180-
union: Union[City, Country]
180+
union: Union[City, Country] # noqa: UP007
181181

182182
objs = [
183183
WithUnion(union=City(name="London")),

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ dependencies = [
88
"orjson", # fast json serialization
99
"typing-extensions",# for depreceated decorator
1010
]
11-
requires-python = ">=3.9"
11+
requires-python = ">=3.10"
1212

1313
## these need to be set if you're planning to upload to pypi
1414
# description = "TODO"

ruff.toml

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,6 @@ lint.extend-select = [
44
"ALL",
55
]
66

7-
# Preserve types, even if a file imports `from __future__ import annotations`
8-
# we need this for cachew to work with HPI types on 3.9
9-
# can probably remove after 3.10?
10-
lint.pyupgrade.keep-runtime-typing = true
11-
127
lint.ignore = [
138
"D", # annoying nags about docstrings
149
"N", # pep naming
@@ -106,4 +101,5 @@ lint.ignore = [
106101

107102

108103
extend-exclude = [
104+
"src/cachew/legacy.py", # TODO dunno, remove it for good?
109105
]

src/cachew/__init__.py

Lines changed: 27 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,17 @@
66
import logging
77
import os
88
import stat
9-
import sys
109
import warnings
11-
from collections.abc import Iterable
10+
from collections.abc import Callable, Iterable
1211
from dataclasses import dataclass
1312
from pathlib import Path
1413
from typing import (
1514
TYPE_CHECKING,
1615
Any,
17-
Callable,
1816
Generic,
1917
Literal,
20-
Optional,
18+
ParamSpec,
2119
TypeVar,
22-
Union,
2320
cast,
2421
get_args,
2522
get_origin,
@@ -56,7 +53,7 @@ def orjson_dumps(*args, **kwargs): # type: ignore[misc]
5653
# in case of changes in the way cachew stores data, this should be changed to discard old caches
5754
CACHEW_VERSION: str = importlib.metadata.version(__name__)
5855

59-
PathIsh = Union[Path, str]
56+
PathIsh = Path | str
6057

6158
Backend = Literal['sqlite', 'file']
6259

@@ -92,22 +89,10 @@ def get_logger() -> logging.Logger:
9289

9390

9491
R = TypeVar('R')
95-
# ugh. python < 3.10 doesn't have ParamSpec and it seems tricky to backport it in compatible manner
96-
if sys.version_info[:2] >= (3, 10) or TYPE_CHECKING:
97-
if sys.version_info[:2] >= (3, 10):
98-
from typing import ParamSpec
99-
else:
100-
from typing_extensions import ParamSpec
101-
P = ParamSpec('P')
102-
CC = Callable[P, R] # need to give it a name, if inlined into bound=, mypy runs in a bug
103-
PathProvider = Union[PathIsh, Callable[P, PathIsh]]
104-
HashFunction = Callable[P, SourceHash]
105-
else:
106-
# just use some dummy types so runtime is happy
107-
P = TypeVar('P')
108-
CC = Any
109-
PathProvider = Union[P, Any]
110-
HashFunction = Union[P, Any]
92+
P = ParamSpec('P')
93+
CC = Callable[P, R] # need to give it a name, if inlined into bound=, mypy runs in a bug
94+
PathProvider = PathIsh | Callable[P, PathIsh]
95+
HashFunction = Callable[P, SourceHash]
11196

11297
F = TypeVar('F', bound=CC)
11398

@@ -129,7 +114,7 @@ def mtime_hash(path: Path, *args, **kwargs) -> SourceHash:
129114
Inferred = tuple[Kind, type[Any]]
130115

131116

132-
def infer_return_type(func) -> Union[Failure, Inferred]:
117+
def infer_return_type(func) -> Failure | Inferred:
133118
"""
134119
>>> def const() -> int:
135120
... return 123
@@ -292,21 +277,21 @@ def cachew_error(e: Exception, *, logger: logging.Logger) -> None:
292277
@doublewrap
293278
def cachew_impl(
294279
func=None, # TODO should probably type it after switch to python 3.10/proper paramspec
295-
cache_path: Optional[PathProvider[P]] = use_default_path,
280+
cache_path: PathProvider[P] | None = use_default_path,
296281
*,
297282
force_file: bool = False,
298-
cls: Optional[Union[type, tuple[Kind, type]]] = None,
283+
cls: type | tuple[Kind, type] | None = None,
299284
depends_on: HashFunction[P] = default_hash,
300-
logger: Optional[logging.Logger] = None,
285+
logger: logging.Logger | None = None,
301286
chunk_by: int = 100,
302287
# NOTE: allowed values for chunk_by depend on the system.
303288
# some systems (to be more specific, sqlite builds), it might be too large and cause issues
304289
# ideally this would be more defensive/autodetected, maybe with a warning?
305290
# you can use 'test_many' to experiment
306291
# - too small values (e.g. 10) are slower than 100 (presumably, too many sql statements)
307292
# - too large values (e.g. 10K) are slightly slower as well (not sure why?)
308-
synthetic_key: Optional[str] = None,
309-
backend: Optional[Backend] = None,
293+
synthetic_key: str | None = None,
294+
backend: Backend | None = None,
310295
**kwargs,
311296
):
312297
r"""
@@ -383,8 +368,8 @@ def process(self, msg, kwargs):
383368
cache_path = settings.DEFAULT_CACHEW_DIR
384369
logger.debug(f'no cache_path specified, using the default {cache_path}')
385370

386-
use_kind: Optional[Kind] = None
387-
use_cls: Optional[type] = None
371+
use_kind: Kind | None = None
372+
use_cls: type | None = None
388373
if cls is not None:
389374
# defensive here since typing. objects passed as cls might fail on isinstance
390375
try:
@@ -469,15 +454,15 @@ def cachew(fun: F) -> F: ...
469454
# but at least it works for checking that cachew_path and depdns_on have the same args :shrug:
470455
@overload
471456
def cachew(
472-
cache_path: Optional[PathProvider[P]] = ...,
457+
cache_path: PathProvider[P] | None = ...,
473458
*,
474459
force_file: bool = ...,
475-
cls: Optional[Union[type, tuple[Kind, type]]] = ...,
460+
cls: type | tuple[Kind, type] | None = ...,
476461
depends_on: HashFunction[P] = ...,
477-
logger: Optional[logging.Logger] = ...,
462+
logger: logging.Logger | None = ...,
478463
chunk_by: int = ...,
479-
synthetic_key: Optional[str] = ...,
480-
backend: Optional[Backend] = ...,
464+
synthetic_key: str | None = ...,
465+
backend: Backend | None = ...,
481466
) -> Callable[[F], F]: ...
482467

483468
def cachew(*args, **kwargs): # make ty happy
@@ -492,12 +477,12 @@ def callable_name(func: Callable) -> str:
492477
return f'{mod}:{getattr(func, "__qualname__")}'
493478

494479

495-
def callable_module_name(func: Callable) -> Optional[str]:
480+
def callable_module_name(func: Callable) -> str | None:
496481
return getattr(func, '__module__', None)
497482

498483

499484
# could cache this, but might be worth not to, so the user can change it on the fly?
500-
def _parse_disabled_modules(logger: Optional[logging.Logger] = None) -> list[str]:
485+
def _parse_disabled_modules(logger: logging.Logger | None = None) -> list[str]:
501486
# e.g. CACHEW_DISABLE=my.browser:my.reddit
502487
if 'CACHEW_DISABLE' not in os.environ:
503488
return []
@@ -550,7 +535,7 @@ def _matches_disabled_module(module_name: str, pattern: str) -> bool:
550535
if len(module_parts) < len(pattern_parts):
551536
return False
552537

553-
for mp, pp in zip(module_parts, pattern_parts):
538+
for mp, pp in zip(module_parts, pattern_parts, strict=False):
554539
if fnmatch.fnmatch(mp, pp):
555540
continue
556541
return False
@@ -584,8 +569,8 @@ class Context(Generic[P]):
584569
depends_on : HashFunction[P]
585570
logger : logging.Logger
586571
chunk_by : int
587-
synthetic_key: Optional[str]
588-
backend : Optional[Backend]
572+
synthetic_key: str | None
573+
backend : Backend | None
589574

590575
def composite_hash(self, *args, **kwargs) -> dict[str, Any]:
591576
fsig = inspect.signature(self.func)
@@ -650,7 +635,7 @@ def cachew_wrapper(
650635
yield from func(*args, **kwargs)
651636
return
652637

653-
def get_db_path() -> Optional[Path]:
638+
def get_db_path() -> Path | None:
654639
db_path: Path
655640
if callable(cache_path):
656641
pp = cache_path(*args, **kwargs)
@@ -708,7 +693,7 @@ def try_use_synthetic_key() -> None:
708693
if not cache_compatible:
709694
return
710695

711-
def missing_keys(cached: list[str], wanted: list[str]) -> Optional[list[str]]:
696+
def missing_keys(cached: list[str], wanted: list[str]) -> list[str] | None:
712697
# FIXME assert both cached and wanted are sorted? since we rely on it
713698
# if not, then the user could use some custom key for caching (e.g. normalise filenames etc)
714699
# although in this case passing it into the function wouldn't make sense?

src/cachew/backend/common.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,6 @@
22
from abc import abstractmethod
33
from collections.abc import Iterator, Sequence
44
from pathlib import Path
5-
from typing import (
6-
Optional,
7-
)
85

96
from ..common import SourceHash
107

@@ -21,10 +18,10 @@ def __enter__(self):
2118
def __exit__(self, *args) -> None:
2219
raise NotImplementedError
2320

24-
def get_old_hash(self) -> Optional[SourceHash]:
21+
def get_old_hash(self) -> SourceHash | None:
2522
raise NotImplementedError
2623

27-
def cached_blobs_total(self) -> Optional[int]:
24+
def cached_blobs_total(self) -> int | None:
2825
raise NotImplementedError
2926

3027
def cached_blobs(self) -> Iterator[bytes]:

0 commit comments

Comments
 (0)