Skip to content

Commit 5b8ed03

Browse files
committed
general: set min version to 3.12; update ci to run 3.14
It's a bit of a toll to support so many versions, especially with new 3.12 type syntax and other goodies. With pyenv/uv these days using custom python version is far easier than before and even preferred.
1 parent d905463 commit 5b8ed03

File tree

12 files changed

+113
-109
lines changed

12 files changed

+113
-109
lines changed

.github/workflows/main.yml

Lines changed: 7 additions & 7 deletions
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.10', '3.11', '3.12', '3.13']
33+
python-version: ['3.12', '3.13', '3.14']
3434
# vvv just an example of excluding stuff from matrix
3535
# exclude: [{platform: macos-latest, python-version: '3.6'}]
3636

@@ -43,16 +43,16 @@ jobs:
4343
# ugh https://github.com/actions/toolkit/blob/main/docs/commands.md#path-manipulation
4444
- run: echo "$HOME/.local/bin" >> $GITHUB_PATH
4545

46-
- uses: actions/checkout@v4
46+
- uses: actions/checkout@v5
4747
with:
4848
submodules: recursive
4949
fetch-depth: 0 # nicer to have all git history when debugging/for tests
5050

51-
- uses: actions/setup-python@v5
51+
- uses: actions/setup-python@v6
5252
with:
5353
python-version: ${{ matrix.python-version }}
5454

55-
- uses: astral-sh/setup-uv@v5
55+
- uses: astral-sh/setup-uv@v7
5656
with:
5757
enable-cache: false # we don't have lock files, so can't use them as cache key
5858

@@ -93,16 +93,16 @@ jobs:
9393
# ugh https://github.com/actions/toolkit/blob/main/docs/commands.md#path-manipulation
9494
- run: echo "$HOME/.local/bin" >> $GITHUB_PATH
9595

96-
- uses: actions/checkout@v4
96+
- uses: actions/checkout@v5
9797
with:
9898
submodules: recursive
9999
fetch-depth: 0 # pull all commits to correctly infer vcs version
100100

101-
- uses: actions/setup-python@v5
101+
- uses: actions/setup-python@v6
102102
with:
103103
python-version: '3.12'
104104

105-
- uses: astral-sh/setup-uv@v5
105+
- uses: astral-sh/setup-uv@v7
106106
with:
107107
enable-cache: false # we don't have lock files, so can't use them as cache key
108108

README.ipynb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"\n",
1818
" path_s = subprocess.check_output(['git', 'rev-parse', '--show-toplevel'], text=True).strip()\n",
1919
" path = Path(path_s)\n",
20-
" assert path.is_absolute(), path # just in case\n",
20+
" assert path.is_absolute(), path # just in case\n",
2121
" return path\n",
2222
"\n",
2323
"\n",
@@ -49,6 +49,7 @@
4949
" rpath = Path(c.module_path).relative_to(src_dir)\n",
5050
" return f'src/{rpath}#L{c.line}'\n",
5151
"\n",
52+
"\n",
5253
"# TODO ugh.. annoying, seems like Jedi can't get the functions source?\n",
5354
"# maybe because it's doing partial parsing or something?\n",
5455
"# there is c._get_module_context().code_lines, but it returns all lines in a source file??\n",
@@ -67,7 +68,7 @@
6768
" raise RuntimeError(f'Function not found: {symbol}')\n",
6869
"\n",
6970
" # ugh lineno is 1-indexed, and seems like a closed interval?\n",
70-
" return ''.join(src_lines[x.lineno - 1: x.end_lineno])\n",
71+
" return ''.join(src_lines[x.lineno - 1 : x.end_lineno])\n",
7172
"\n",
7273
"\n",
7374
"def getdoc(symbol: str) -> str:\n",
@@ -100,7 +101,6 @@
100101
"metadata": {},
101102
"outputs": [],
102103
"source": [
103-
"\n",
104104
"from IPython.display import Markdown as md # ty: ignore[unresolved-import]\n",
105105
"\n",
106106
"dmd = lambda x: display(md(x.strip())) # ty: ignore[unresolved-reference]"

README.md

Lines changed: 13 additions & 13 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#L32) into a simpler JSON-like representation
128+
- first your objects get [converted](src/cachew/marshall/cachew.py#L28) 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#L575)
131+
When the function is called, cachew [computes the hash of your function's arguments ](src/cachew/__init__.py#L571)
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#L384), [2](src/cachew/tests/test_cachew.py#L398)
143+
* automatic schema inference: [1](src/cachew/tests/test_cachew.py#L381), [2](src/cachew/tests/test_cachew.py#L395)
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#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)
148+
See [tests.test_types](src/cachew/tests/test_cachew.py#L683), [tests.test_primitive](src/cachew/tests/test_cachew.py#L721), [tests.test_dates](src/cachew/tests/test_cachew.py#L633), [tests.test_exceptions](src/cachew/tests/test_cachew.py#L1125)
149+
* [@dataclass and NamedTuple](src/cachew/tests/test_cachew.py#L598)
150+
* [Optional](src/cachew/tests/test_cachew.py#L525) types
151+
* [Union](src/cachew/tests/test_cachew.py#L828) types
152+
* [nested datatypes](src/cachew/tests/test_cachew.py#L441)
153153

154-
* detects [datatype schema changes](src/cachew/tests/test_cachew.py#L474) and discards old data automatically
154+
* detects [datatype schema changes](src/cachew/tests/test_cachew.py#L471) 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#L278) for up-to-date documentation on parameters and return types.
168+
See [docstring](src/cachew/__init__.py#L270) for up-to-date documentation on parameters and return types.
169169
You can also use [extensive unit tests](src/cachew/tests/test_cachew.py#L1) 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#L421) 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#L418) 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#L118) if the input file was modified.
181+
For instance, it can be used to [discard cache](src/cachew/tests/test_cachew.py#L115) if the input file was modified.
182182

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

@@ -274,7 +274,7 @@ Now you can use `@mcachew` in place of `@cachew`, and be certain things don't br
274274
## Settings
275275

276276

277-
[cachew.settings](src/cachew/__init__.py#L61) exposes some parameters that allow you to control `cachew` behaviour:
277+
[cachew.settings](src/cachew/__init__.py#L58) exposes some parameters that allow you to control `cachew` behaviour:
278278
- `ENABLE`: set to `False` if you want to disable caching for without removing the decorators (useful for testing and debugging).
279279
You can also use [cachew.extra.disabled_cachew](src/cachew/extra.py#L25) context manager to do it temporarily.
280280
- `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)).

pyproject.toml

Lines changed: 12 additions & 8 deletions
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.10"
11+
requires-python = ">=3.12"
1212

1313
## these need to be set if you're planning to upload to pypi
1414
# description = "TODO"
@@ -39,23 +39,27 @@ optional = [
3939
# On the other hand, it's a bit annoying that it's always included by default?
4040
# To make sure it's not included, need to use `uv run --exact --no-default-groups ...`
4141
testing = [
42-
"pytz", "types-pytz", # optional runtime only dependency
43-
4442
"pytest",
43+
"ruff",
44+
45+
"pytz",
46+
4547
"more-itertools",
4648
"patchy", # for injecting sleeps and testing concurrent behaviour
4749
"enlighten", # used in logging helper, but not really required
4850
"cattrs", # benchmarking alternative marshalling implementation
4951
"pyinstrument", # for profiling from within tests
5052
"codetiming", # Timer context manager
51-
52-
"ruff",
53-
"mypy",
54-
"lxml", # for mypy html coverage
55-
"ty>=0.0.1a19",
5653
]
5754
typecheck = [
5855
{ include-group = "testing" },
56+
57+
"mypy",
58+
"lxml", # for mypy coverage
59+
"ty>=0.0.1a22",
60+
61+
"types-pytz", # optional runtime only dependency
62+
5963
"cachew[optional]",
6064
]
6165

ruff.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
line-length = 140 # impacts import sorting
1+
line-length = 120 # impacts import sorting
22

33
lint.extend-select = [
44
"ALL",
@@ -72,6 +72,7 @@ lint.ignore = [
7272

7373
"TRY003", # suggests defining exception messages in exception class -- kinda annoying
7474
"TRY201", # raise without specifying exception name -- sometimes hurts readability
75+
"TRY400", # a bit dumb, and results in false positives (see https://github.com/astral-sh/ruff/issues/18070)
7576
"TRY401", # redundant exception in logging.exception call? TODO double check, might result in excessive logging
7677

7778
"TID252", # Prefer absolute imports over relative imports from parent modules

src/cachew/__init__.py

Lines changed: 26 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,7 @@
1313
from typing import (
1414
TYPE_CHECKING,
1515
Any,
16-
Generic,
1716
Literal,
18-
ParamSpec,
19-
TypeVar,
2017
cast,
2118
get_args,
2219
get_origin,
@@ -88,13 +85,8 @@ def get_logger() -> logging.Logger:
8885
}
8986

9087

91-
R = TypeVar('R')
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]
96-
97-
F = TypeVar('F', bound=CC)
88+
type PathProvider[**P] = PathIsh | Callable[P, PathIsh]
89+
type HashFunction[**P] = Callable[P, SourceHash]
9890

9991

10092
def default_hash(*args, **kwargs) -> SourceHash:
@@ -109,9 +101,9 @@ def mtime_hash(path: Path, *args, **kwargs) -> SourceHash:
109101
return default_hash(f'{path}.{mt}', *args, **kwargs)
110102

111103

112-
Failure = str
113-
Kind = Literal['single', 'multiple']
114-
Inferred = tuple[Kind, type[Any]]
104+
Failure = str # deliberately not a type =, used in type checks
105+
type Kind = Literal['single', 'multiple']
106+
type Inferred = tuple[Kind, type[Any]]
115107

116108

117109
def infer_return_type(func) -> Failure | Inferred:
@@ -161,12 +153,12 @@ def infer_return_type(func) -> Failure | Inferred:
161153
>>> infer_return_type(int_provider)
162154
('multiple', <class 'int'>)
163155
164-
>>> from typing import Iterator, Union
165-
>>> def union_provider() -> Iterator[Union[str, int]]:
156+
>>> from typing import Iterator
157+
>>> def union_provider() -> Iterator[str | int]:
166158
... yield 1
167159
... yield 'aaa'
168160
>>> infer_return_type(union_provider)
169-
('multiple', typing.Union[str, int])
161+
('multiple', str | int)
170162
171163
# a bit of an edge case
172164
>>> from typing import Tuple
@@ -275,13 +267,13 @@ def cachew_error(e: Exception, *, logger: logging.Logger) -> None:
275267

276268
# using cachew_impl here just to use different signatures during type checking (see below)
277269
@doublewrap
278-
def cachew_impl(
270+
def cachew_impl[**P](
279271
func=None, # TODO should probably type it after switch to python 3.10/proper paramspec
280-
cache_path: PathProvider[P] | None = use_default_path,
272+
cache_path: PathProvider[P] | None = use_default_path, # ty: ignore[too-many-positional-arguments] # see https://github.com/astral-sh/ty/issues/157
281273
*,
282274
force_file: bool = False,
283275
cls: type | tuple[Kind, type] | None = None,
284-
depends_on: HashFunction[P] = default_hash,
276+
depends_on: HashFunction[P] = default_hash, # ty: ignore[too-many-positional-arguments]
285277
logger: logging.Logger | None = None,
286278
chunk_by: int = 100,
287279
# NOTE: allowed values for chunk_by depend on the system.
@@ -402,7 +394,9 @@ def process(self, msg, kwargs):
402394
else:
403395
assert use_kind is not None
404396
if (use_kind, use_cls) != inference_res:
405-
logger.warning(f"inferred type {inference_res} mismatches explicitly specified type {(use_kind, use_cls)}")
397+
logger.warning(
398+
f"inferred type {inference_res} mismatches explicitly specified type {(use_kind, use_cls)}"
399+
)
406400
# TODO not sure if should be more serious error...
407401

408402
if use_kind == 'single':
@@ -447,18 +441,18 @@ def binder(*args, **kwargs):
447441
# we need two versions due to @doublewrap
448442
# this is when we just annotate as @cachew without any args
449443
@overload
450-
def cachew(fun: F) -> F: ...
444+
def cachew[F: Callable](fun: F) -> F: ...
451445

452446
# NOTE: we won't really be able to make sure the args of cache_path are the same as args of the wrapped function
453447
# because when cachew() is called, we don't know anything about the wrapped function yet
454448
# but at least it works for checking that cachew_path and depdns_on have the same args :shrug:
455449
@overload
456-
def cachew(
457-
cache_path: PathProvider[P] | None = ...,
450+
def cachew[F, **P](
451+
cache_path: PathProvider[P] | None = ..., # ty: ignore[too-many-positional-arguments]
458452
*,
459453
force_file: bool = ...,
460454
cls: type | tuple[Kind, type] | None = ...,
461-
depends_on: HashFunction[P] = ...,
455+
depends_on: HashFunction[P] = ..., # ty: ignore[too-many-positional-arguments]
462456
logger: logging.Logger | None = ...,
463457
chunk_by: int = ...,
464458
synthetic_key: str | None = ...,
@@ -546,7 +540,9 @@ def _module_is_disabled(module_name: str, logger: logging.Logger) -> bool:
546540
disabled_modules = _parse_disabled_modules(logger)
547541
for pat in disabled_modules:
548542
if _matches_disabled_module(module_name, pat):
549-
logger.debug(f"caching disabled for {module_name} (matched '{pat}' from 'CACHEW_DISABLE={os.environ['CACHEW_DISABLE']})'")
543+
logger.debug(
544+
f"caching disabled for {module_name} (matched '{pat}' from 'CACHEW_DISABLE={os.environ['CACHEW_DISABLE']})'"
545+
)
550546
return True
551547
return False
552548

@@ -560,13 +556,13 @@ def _module_is_disabled(module_name: str, logger: logging.Logger) -> bool:
560556

561557

562558
@dataclass
563-
class Context(Generic[P]):
559+
class Context[**P]:
564560
# fmt: off
565561
func : Callable
566-
cache_path : PathProvider[P]
562+
cache_path : PathProvider[P] # ty: ignore[too-many-positional-arguments]
567563
force_file : bool
568564
cls_ : type
569-
depends_on : HashFunction[P]
565+
depends_on : HashFunction[P] # ty: ignore[too-many-positional-arguments]
570566
logger : logging.Logger
571567
chunk_by : int
572568
synthetic_key: str | None
@@ -605,9 +601,9 @@ def composite_hash(self, *args, **kwargs) -> dict[str, Any]:
605601
# fmt: on
606602

607603

608-
def cachew_wrapper(
604+
def cachew_wrapper[**P](
609605
*args,
610-
_cachew_context: Context[P],
606+
_cachew_context: Context[P], # ty: ignore[too-many-positional-arguments]
611607
**kwargs,
612608
):
613609
C = _cachew_context

src/cachew/backend/sqlite.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,8 @@ def cached_blobs(self) -> Iterator[bytes]:
120120
raw_row_iterator = getattr(rows, '_raw_row_iterator', None)
121121
if raw_row_iterator is None:
122122
warnings.warn(
123-
"CursorResult._raw_row_iterator method isn't found. This could lead to degraded cache reading performance.", stacklevel=2
123+
"CursorResult._raw_row_iterator method isn't found. This could lead to degraded cache reading performance.",
124+
stacklevel=2,
124125
)
125126
row_iterator = rows
126127
else:
@@ -168,7 +169,9 @@ def flush_blobs(self, chunk: Sequence[bytes]) -> None:
168169
# uhh. this gives a huge speedup for inserting
169170
# since we don't have to create intermediate dictionaries
170171
# TODO move this to __init__?
171-
insert_into_table_cache_tmp_raw = str(self.table_cache_tmp.insert().compile(dialect=sqlite.dialect(paramstyle='qmark')))
172+
insert_into_table_cache_tmp_raw = str(
173+
self.table_cache_tmp.insert().compile(dialect=sqlite.dialect(paramstyle='qmark'))
174+
)
172175
# I also tried setting paramstyle='qmark' in create_engine, but it seems to be ignored :(
173176
# idk what benefit sqlalchemy gives at this point, seems to just complicate things
174177
self.connection.exec_driver_sql(insert_into_table_cache_tmp_raw, [(c,) for c in chunk])

0 commit comments

Comments
 (0)