Skip to content

Commit 2edddd9

Browse files
committed
chore: update all CI files; add ty; fix ruff/mypy errors
1 parent a250175 commit 2edddd9

File tree

15 files changed

+111
-64
lines changed

15 files changed

+111
-64
lines changed

.ci/release-uv

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ from subprocess import check_call
2626

2727
is_ci = os.environ.get('CI') is not None
2828

29+
2930
def main() -> None:
3031
p = argparse.ArgumentParser()
3132
p.add_argument('--use-test-pypi', action='store_true')
@@ -34,19 +35,14 @@ def main() -> None:
3435
publish_url = ['--publish-url', 'https://test.pypi.org/legacy/'] if args.use_test_pypi else []
3536

3637
root = Path(__file__).absolute().parent.parent
37-
os.chdir(root) # just in case
38-
39-
if is_ci:
40-
# see https://github.com/actions/checkout/issues/217
41-
check_call('git fetch --prune --unshallow'.split())
38+
os.chdir(root) # just in case
4239

4340
# TODO ok, for now uv won't remove dist dir if it already exists
4441
# https://github.com/astral-sh/uv/issues/10293
4542
dist = root / 'dist'
4643
if dist.exists():
4744
shutil.rmtree(dist)
4845

49-
# todo what is --force-pep517?
5046
check_call(['uv', 'build'])
5147

5248
if not is_ci:

.github/workflows/main.yml

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,30 @@ on:
66
branches: '*'
77
tags: 'v[0-9]+.*' # only trigger on 'release' tags for PyPi
88
# Ideally I would put this in the pypi job... but github syntax doesn't allow for regexes there :shrug:
9-
pull_request: # needed to trigger on others' PRs
9+
10+
# Needed to trigger on others' PRs.
1011
# Note that people who fork it need to go to "Actions" tab on their fork and click "I understand my workflows, go ahead and enable them".
11-
workflow_dispatch: # needed to trigger workflows manually
12-
# todo cron?
12+
pull_request:
13+
14+
# Needed to trigger workflows manually.
15+
workflow_dispatch:
1316
inputs:
1417
debug_enabled:
1518
type: boolean
1619
description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)'
1720
required: false
1821
default: false
1922

23+
schedule:
24+
- cron: '31 18 * * 5' # run every Friday
25+
2026

2127
jobs:
2228
build:
2329
strategy:
2430
fail-fast: false
2531
matrix:
26-
platform: [ubuntu-latest, macos-latest] # todo windows-latest]
32+
platform: [ubuntu-latest, macos-latest, windows-latest]
2733
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
2834
# vvv just an example of excluding stuff from matrix
2935
# exclude: [{platform: macos-latest, python-version: '3.6'}]
@@ -55,44 +61,57 @@ jobs:
5561

5662
# explicit bash command is necessary for Windows CI runner, otherwise it thinks it's cmd...
5763
- run: bash .ci/run
64+
env:
65+
# only compute lxml coverage on ubuntu; it crashes on windows
66+
CI_MYPY_COVERAGE: ${{ matrix.platform == 'ubuntu-latest' && '--cobertura-xml-report .coverage.mypy' || '' }}
5867

5968
- if: matrix.platform == 'ubuntu-latest' # no need to compute coverage for other platforms
60-
uses: actions/upload-artifact@v4
69+
uses: codecov/codecov-action@v5
6170
with:
62-
include-hidden-files: true
63-
name: .coverage.mypy_${{ matrix.platform }}_${{ matrix.python-version }}
64-
path: .coverage.mypy/
71+
fail_ci_if_error: true # default false
72+
token: ${{ secrets.CODECOV_TOKEN }}
73+
flags: mypy-${{ matrix.python-version }}
74+
files: .coverage.mypy/cobertura.xml
6575

6676

6777
pypi:
68-
runs-on: ubuntu-latest
78+
# Do not run it for PRs/cron schedule etc.
79+
# NOTE: release tags are guarded by on: push: tags on the top.
80+
if: github.event_name == 'push' && (startsWith(github.event.ref, 'refs/tags/') || (github.event.ref == format('refs/heads/{0}', github.event.repository.master_branch)))
81+
# Ugh, I tried using matrix or something to explicitly generate only test pypi or prod pypi pipelines.
82+
# But github actions is so shit, it's impossible to do any logic at all, e.g. doesn't support conditional matrix, if/else statements for variables etc.
83+
6984
needs: [build] # add all other jobs here
85+
86+
runs-on: ubuntu-latest
87+
7088
permissions:
7189
# necessary for Trusted Publishing
7290
id-token: write
91+
7392
steps:
7493
# ugh https://github.com/actions/toolkit/blob/main/docs/commands.md#path-manipulation
7594
- run: echo "$HOME/.local/bin" >> $GITHUB_PATH
7695

7796
- uses: actions/checkout@v4
7897
with:
7998
submodules: recursive
99+
fetch-depth: 0 # pull all commits to correctly infer vcs version
80100

81101
- uses: actions/setup-python@v5
82102
with:
83-
python-version: '3.10'
103+
python-version: '3.12'
84104

85105
- uses: astral-sh/setup-uv@v5
86106
with:
87107
enable-cache: false # we don't have lock files, so can't use them as cache key
88108

89109
- name: 'release to test pypi'
90110
# always deploy merged master to test pypi
91-
if: github.event_name != 'pull_request' && github.event.ref == 'refs/heads/master'
111+
if: github.event.ref == format('refs/heads/{0}', github.event.repository.master_branch)
92112
run: .ci/release-uv --use-test-pypi
93113

94-
- name: 'release to pypi'
114+
- name: 'release to prod pypi'
95115
# always deploy tags to release pypi
96-
# NOTE: release tags are guarded by on: push: tags on the top
97-
if: github.event_name != 'pull_request' && startsWith(github.event.ref, 'refs/tags')
116+
if: startsWith(github.event.ref, 'refs/tags/')
98117
run: .ci/release-uv

README.ipynb

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,15 @@
2323
"import sys\n",
2424
"from pathlib import Path\n",
2525
"\n",
26-
"from IPython.display import Markdown as md\n",
26+
"from IPython.display import Markdown as md # ty: ignore[unresolved-import]\n",
2727
"\n",
2828
"##\n",
2929
"sys.path.insert(0, str(Path('src').absolute()))\n",
3030
"import cachew # isort: skip\n",
3131
"import cachew.extra # isort: skip\n",
3232
"import cachew.marshall.cachew # isort: skip\n",
3333
"import cachew.tests.test_cachew as tests # isort: skip\n",
34+
"\n",
3435
"sys.modules['tests'] = tests # meh\n",
3536
"##\n",
3637
"\n",
@@ -62,7 +63,7 @@
6263
" return f'[{title}]({file}{numbers})'\n",
6364
"\n",
6465
"\n",
65-
"dmd = lambda x: display(md(x.strip()))"
66+
"dmd = lambda x: display(md(x.strip())) # ty: ignore[unresolved-reference]"
6667
]
6768
},
6869
{
@@ -148,6 +149,7 @@
148149
"outputs": [],
149150
"source": [
150151
"doc = inspect.getdoc(cachew.cachew)\n",
152+
"assert doc is not None\n",
151153
"doc = doc.split('Usage example:')[-1].lstrip()\n",
152154
"dmd(f\"\"\"```python\n",
153155
"{doc}\n",
@@ -229,7 +231,7 @@
229231
"[composite] = [x\n",
230232
" for x in ast.walk(ast.parse(inspect.getsource(cachew)))\n",
231233
" if isinstance(x, ast.FunctionDef) and x.name == 'composite_hash'\n",
232-
"]\n",
234+
"] # fmt: skip\n",
233235
"\n",
234236
"link = f'{Path(cachew.__file__).relative_to(_CWD)}:#L{composite.lineno}'\n",
235237
"\n",

README.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ Cachew gives the best of two worlds and makes it both **easy and efficient**. Th
128128
- first your objects get [converted](src/cachew/marshall/cachew.py#L30) 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:#L587)
131+
When the function is called, cachew [computes the hash of your function's arguments ](src/cachew/__init__.py:#L590)
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#L387), [2](src/cachew/tests/test_cachew.py#L401)
143+
* automatic schema inference: [1](src/cachew/tests/test_cachew.py#L385), [2](src/cachew/tests/test_cachew.py#L399)
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#L687), [tests.test_primitive](src/cachew/tests/test_cachew.py#L725), [tests.test_dates](src/cachew/tests/test_cachew.py#L637), [tests.test_exceptions](src/cachew/tests/test_cachew.py#L1124)
149-
* [@dataclass and NamedTuple](src/cachew/tests/test_cachew.py#L602)
150-
* [Optional](src/cachew/tests/test_cachew.py#L531) types
151-
* [Union](src/cachew/tests/test_cachew.py#L832) types
152-
* [nested datatypes](src/cachew/tests/test_cachew.py#L447)
148+
See [tests.test_types](src/cachew/tests/test_cachew.py#L685), [tests.test_primitive](src/cachew/tests/test_cachew.py#L723), [tests.test_dates](src/cachew/tests/test_cachew.py#L635), [tests.test_exceptions](src/cachew/tests/test_cachew.py#L1122)
149+
* [@dataclass and NamedTuple](src/cachew/tests/test_cachew.py#L600)
150+
* [Optional](src/cachew/tests/test_cachew.py#L529) types
151+
* [Union](src/cachew/tests/test_cachew.py#L830) types
152+
* [nested datatypes](src/cachew/tests/test_cachew.py#L445)
153153

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

156156

157157
# Performance
@@ -170,7 +170,7 @@ You can also use [extensive unit tests](src/cachew/tests/test_cachew.py) as a re
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#L424) 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#L422) and depends on function's arguments.
174174

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

conftest.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,10 @@ def resolve_package_path(path: pathlib.Path) -> Optional[pathlib.Path]:
3434
raise RuntimeError("Couldn't determine path for ", path)
3535

3636

37-
_pytest.pathlib.resolve_package_path = resolve_package_path
37+
# NOTE: seems like it's not necessary anymore?
38+
# keeping it for now just in case
39+
# after https://github.com/pytest-dev/pytest/pull/13426 we should be able to remove the whole conftest
40+
# _pytest.pathlib.resolve_package_path = resolve_package_path
3841

3942

4043
# without patching, the orig function returns just a package name for some reason
@@ -52,4 +55,4 @@ def search_pypath(module_name: str) -> str:
5255
return str(mpath)
5356

5457

55-
_pytest.main.search_pypath = search_pypath
58+
_pytest.main.search_pypath = search_pypath # ty: ignore[invalid-assignment]

misc/test_redis/test.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
#!/usr/bin/env python3
22
from time import time
33

4-
import redis
5-
from loguru import logger
4+
import redis # ty: ignore[unresolved-import]
5+
from loguru import logger # ty: ignore[unresolved-import]
66
from more_itertools import ilen
77

88
r = redis.Redis(host='localhost', port=6379, db=0)

pyproject.toml

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ optional = [
3434
"colorlog",
3535
]
3636
[dependency-groups]
37+
# TODO: not sure, on the one hand could just use 'standard' dev dependency group
38+
# On the other hand, it's a bit annoying that it's always included by default?
39+
# To make sure it's not included, need to use `uv run --exact --no-default-groups ...`
3740
testing = [
3841
"pytz", "types-pytz", # optional runtime only dependency
3942

@@ -47,16 +50,11 @@ testing = [
4750

4851
"ruff",
4952
"mypy",
50-
"lxml", # for mypy html coverage
53+
"lxml", # for mypy html coverage
54+
"ty>=0.0.1a16",
5155
]
5256

5357

54-
# workaround for error during uv publishing
55-
# see https://github.com/astral-sh/uv/issues/9513#issuecomment-2519527822
56-
[tool.setuptools]
57-
license-files = []
58-
59-
6058
[build-system]
6159
requires = ["hatchling", "hatch-vcs"]
6260
build-backend = "hatchling.build"

ruff.toml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ lint.extend-select = [
2929
"ARG", # unused argument checks
3030
"A", # builtin shadowing
3131
"G", # logging stuff
32-
# "EM", # TODO hmm could be helpful to prevent duplicate err msg in traceback.. but kinda annoying
3332

3433
# "ALL", # uncomment this to check for new rules!
3534
]
@@ -48,6 +47,7 @@ lint.ignore = [
4847
"FIX", # complains about fixmes/todos -- annoying
4948
"TD", # complains about todo formatting -- too annoying
5049
"ANN", # missing type annotations? seems way to strict though
50+
"EM" , # suggests assigning all exception messages into a variable first... pretty annoying
5151

5252
### too opinionated style checks
5353
"E501", # too long lines
@@ -137,4 +137,10 @@ lint.ignore = [
137137
"SIM", # some if statements crap
138138
"RSE102", # complains about missing parens in exceptions
139139
##
140+
141+
"PLC0415", # "imports should be at the top level" -- not realistic
142+
]
143+
144+
145+
extend-exclude = [
140146
]

src/cachew/__init__.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ def orjson_dumps(*args, **kwargs): # type: ignore[misc]
3838
# sqlite needs a blob
3939
return json.dumps(*args, **kwargs).encode('utf8')
4040

41-
orjson_loads = json.loads
41+
orjson_loads = json.loads # ty: ignore[invalid-assignment]
4242

4343
import platformdirs
4444

@@ -291,7 +291,7 @@ def cachew_error(e: Exception, *, logger: logging.Logger) -> None:
291291
# using cachew_impl here just to use different signatures during type checking (see below)
292292
@doublewrap
293293
def cachew_impl(
294-
func=None,
294+
func=None, # TODO should probably type it after switch to python 3.10/proper paramspec
295295
cache_path: Optional[PathProvider[P]] = use_default_path,
296296
*,
297297
force_file: bool = False,
@@ -364,6 +364,7 @@ def process(self, msg, kwargs):
364364
func_name = extra['func_name']
365365
return f'[{func_name}] {msg}', kwargs
366366

367+
assert func is not None
367368
func_name = callable_name(func)
368369
adapter = AddFuncName(logger, {'func_name': func_name})
369370
logger = cast(logging.Logger, adapter)
@@ -428,7 +429,8 @@ def _func(*args, **kwargs):
428429
else:
429430
_func = func
430431

431-
# fmt: off
432+
assert use_cls is not None
433+
432434
ctx = Context(
433435
func =_func,
434436
cache_path =cache_path,
@@ -439,14 +441,13 @@ def _func(*args, **kwargs):
439441
chunk_by =chunk_by,
440442
synthetic_key=synthetic_key,
441443
backend =backend,
442-
)
443-
# fmt: on
444+
) # fmt: skip
444445

445446
# hack to avoid extra stack frame (see test_recursive*)
446447
@functools.wraps(func)
447448
def binder(*args, **kwargs):
448449
kwargs['_cachew_context'] = ctx
449-
res = cachew_wrapper(*args, **kwargs)
450+
res = cachew_wrapper(*args, **kwargs) # ty: ignore[missing-argument]
450451

451452
if use_kind == 'single':
452453
lres = list(res)
@@ -460,7 +461,7 @@ def binder(*args, **kwargs):
460461
if TYPE_CHECKING:
461462
# we need two versions due to @doublewrap
462463
# this is when we just annotate as @cachew without any args
463-
@overload # type: ignore[no-overload-impl]
464+
@overload
464465
def cachew(fun: F) -> F: ...
465466

466467
# NOTE: we won't really be able to make sure the args of cache_path are the same as args of the wrapped function
@@ -479,14 +480,16 @@ def cachew(
479480
backend: Optional[Backend] = ...,
480481
) -> Callable[[F], F]: ...
481482

483+
def cachew(*args, **kwargs): # make ty happy
484+
raise NotImplementedError
482485
else:
483486
cachew = cachew_impl
484487

485488

486489
def callable_name(func: Callable) -> str:
487490
# some functions don't have __module__
488491
mod = getattr(func, '__module__', None) or ''
489-
return f'{mod}:{func.__qualname__}'
492+
return f'{mod}:{getattr(func, "__qualname__")}'
490493

491494

492495
def callable_module_name(func: Callable) -> Optional[str]:
@@ -572,7 +575,7 @@ def _module_is_disabled(module_name: str, logger: logging.Logger) -> bool:
572575

573576

574577
@dataclass
575-
class Context(Generic[P]):
578+
class Context(Generic[P]): # ty: ignore[invalid-argument-type]
576579
# fmt: off
577580
func : Callable
578581
cache_path : PathProvider[P]

0 commit comments

Comments
 (0)