Skip to content

Commit 22cbc6b

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 22cbc6b

File tree

11 files changed

+100
-96
lines changed

11 files changed

+100
-96
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]"

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])

src/cachew/logging_helper.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,24 @@ def test() -> None:
2121

2222
M(" Logging module's defaults are not great:")
2323
l = logging.getLogger('default_logger')
24-
l.error("For example, this should be logged as error. But it's not even formatted properly, doesn't have logger name or level")
24+
l.error(
25+
"For example, this should be logged as error. But it's not even formatted properly, doesn't have logger name or level"
26+
)
2527

2628
M("\n The reason is that you need to remember to call basicConfig() first. Let's do it now:")
2729
logging.basicConfig()
28-
l.error("OK, this is better. But the default format kinda sucks, I prefer having timestamps and the file/line number")
30+
l.error(
31+
"OK, this is better. But the default format kinda sucks, I prefer having timestamps and the file/line number"
32+
)
2933

30-
M("\n Also exception logging is kinda lame, doesn't print traceback by default unless you remember to pass exc_info:")
34+
M(
35+
"\n Also exception logging is kinda lame, doesn't print traceback by default unless you remember to pass exc_info:"
36+
)
3137
l.exception(ex) # type: ignore[possibly-undefined] # pylint: disable=used-before-assignment
3238

33-
M("\n\n With make_logger you get a reasonable logging format, colours (via colorlog library) and other neat things:")
39+
M(
40+
"\n\n With make_logger you get a reasonable logging format, colours (via colorlog library) and other neat things:"
41+
)
3442

3543
ll = make_logger('test') # No need for basicConfig!
3644
ll.info("default level is INFO")
@@ -40,7 +48,9 @@ def test() -> None:
4048
M("\n Exceptions print traceback by default now:")
4149
ll.exception(ex)
4250

43-
M("\n You can (and should) use it via regular logging.getLogger after that, e.g. let's set logging level to DEBUG now")
51+
M(
52+
"\n You can (and should) use it via regular logging.getLogger after that, e.g. let's set logging level to DEBUG now"
53+
)
4454
logging.getLogger('test').setLevel(logging.DEBUG)
4555
ll.debug("... now debug messages are also displayed")
4656

0 commit comments

Comments
 (0)