Skip to content

Commit 6da2410

Browse files
authored
Merge pull request #39 from cachedjdk/type-hints
Use type checking
2 parents 989ee27 + e51756e commit 6da2410

File tree

17 files changed

+379
-196
lines changed

17 files changed

+379
-196
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ jobs:
2222
with:
2323
python-version: "3.x"
2424
- uses: pre-commit/[email protected]
25+
- uses: astral-sh/setup-uv@v5
26+
- run: uv run ty check # Until they add a pre-commit hook
2527

2628
test:
2729
strategy:

docs/development.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,18 @@ uv tool install pre-commit
2121
pre-commit install
2222
```
2323

24+
We use [ty](https://docs.astral.sh/ty/) for type checking. This will be added
25+
to the pre-commit hook in the future (when an official ty hook is available),
26+
but for now, you can run it manually:
27+
28+
```sh
29+
uv run ty check
30+
```
31+
2432
To run the tests:
2533

2634
```sh
27-
uv run test pytest
35+
uv run pytest
2836
```
2937

3038
To build the documentation with [Jupyter Book](https://jupyterbook.org/):

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ dependencies = [
3333
"click >= 8.0",
3434
"progressbar2 >= 4.0",
3535
"requests >= 2.24",
36+
"typing-extensions>=4.15.0",
3637
]
3738

3839
[project.urls]
@@ -50,6 +51,7 @@ test = [
5051
]
5152
dev = [
5253
{include-group = "test"},
54+
"ty >= 0.0.9",
5355
]
5456
docs = [
5557
"jgo >= 1.0",

src/cjdk/__main__.py

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# This file is part of cjdk.
22
# Copyright 2022-25 Board of Regents of the University of Wisconsin System
33
# SPDX-License-Identifier: MIT
4+
from __future__ import annotations
45

56
import os
67
import subprocess
@@ -44,7 +45,16 @@
4445
help="Show or do not show progress bars.",
4546
)
4647
@click.version_option(version=__version__)
47-
def _cli(ctx, jdk, cache_dir, index_url, index_ttl, os, arch, progress):
48+
def _cli(
49+
ctx: click.Context,
50+
jdk: str | None,
51+
cache_dir: str | None,
52+
index_url: str | None,
53+
index_ttl: int | None,
54+
os: str | None,
55+
arch: str | None,
56+
progress: bool,
57+
) -> None:
4858
"""
4959
Download, cache, and run JDK or JRE distributions.
5060
@@ -67,7 +77,7 @@ def _cli(ctx, jdk, cache_dir, index_url, index_ttl, os, arch, progress):
6777

6878
@click.command(short_help="List available JDK vendors.")
6979
@click.pass_context
70-
def ls_vendors(ctx):
80+
def ls_vendors(ctx: click.Context) -> None:
7181
"""
7282
Print the list of available JDK vendors.
7383
"""
@@ -83,7 +93,7 @@ def ls_vendors(ctx):
8393
default=True,
8494
help="Show only already-cached JDKs, or show all available JDKs from the index (default cached only).",
8595
)
86-
def ls(ctx, cached: bool = False):
96+
def ls(ctx: click.Context, cached: bool) -> None:
8797
"""
8898
Print the list of JDKs matching the given criteria.
8999
@@ -96,7 +106,7 @@ def ls(ctx, cached: bool = False):
96106

97107
@click.command(short_help="Ensure the requested JDK is cached.")
98108
@click.pass_context
99-
def cache(ctx):
109+
def cache(ctx: click.Context) -> None:
100110
"""
101111
Download and extract the requested JDK if it is not already cached.
102112
@@ -112,7 +122,7 @@ def cache(ctx):
112122

113123
@click.command(hidden=True)
114124
@click.pass_context
115-
def cache_jdk(ctx):
125+
def cache_jdk(ctx: click.Context) -> None:
116126
"""
117127
Deprecated. Use cache function instead.
118128
"""
@@ -123,7 +133,7 @@ def cache_jdk(ctx):
123133
short_help="Print the Java home directory for the requested JDK."
124134
)
125135
@click.pass_context
126-
def java_home(ctx):
136+
def java_home(ctx: click.Context) -> None:
127137
"""
128138
Print the path that is suitable as the value of JAVA_HOME for the requested
129139
JDK.
@@ -143,7 +153,7 @@ def java_home(ctx):
143153
@click.pass_context
144154
@click.argument("prog", nargs=1)
145155
@click.argument("args", nargs=-1, type=click.UNPROCESSED)
146-
def exec(ctx, prog, args):
156+
def exec(ctx: click.Context, prog: str, args: tuple[str, ...]) -> None:
147157
"""
148158
Run PROG with the environment variables set for the requested JDK.
149159
@@ -191,7 +201,16 @@ def exec(ctx, prog, args):
191201
metavar="HASH",
192202
help="Check the downloaded file against the given SHA-512 hash.",
193203
)
194-
def cache_file(ctx, url, filename, name, ttl, sha1, sha256, sha512):
204+
def cache_file(
205+
ctx: click.Context,
206+
url: str,
207+
filename: str,
208+
name: str | None,
209+
ttl: int | None,
210+
sha1: str | None,
211+
sha256: str | None,
212+
sha512: str | None,
213+
) -> None:
195214
"""
196215
Download and store an arbitrary file if it is not already cached.
197216
@@ -236,7 +255,14 @@ def cache_file(ctx, url, filename, name, ttl, sha1, sha256, sha512):
236255
metavar="HASH",
237256
help="Check the downloaded file against the given SHA-512 hash.",
238257
)
239-
def cache_package(ctx, url, name, sha1, sha256, sha512):
258+
def cache_package(
259+
ctx: click.Context,
260+
url: str,
261+
name: str | None,
262+
sha1: str | None,
263+
sha256: str | None,
264+
sha512: str | None,
265+
) -> None:
240266
"""
241267
Download, extract, and store an arbitrary .zip or .tar.gz package if it is
242268
not already cached.
@@ -262,7 +288,7 @@ def cache_package(ctx, url, name, sha1, sha256, sha512):
262288

263289
@click.command(short_help="Remove all cached files.")
264290
@click.pass_context
265-
def clear_cache(ctx):
291+
def clear_cache(ctx: click.Context) -> None:
266292
"""
267293
Remove all cached JDKs, files, and packages from the cache directory.
268294
@@ -292,7 +318,7 @@ def clear_cache(ctx):
292318
_cli.add_command(cache_jdk)
293319

294320

295-
def main():
321+
def main() -> None:
296322
try:
297323
_cli()
298324
except CjdkError as e:

src/cjdk/_api.py

Lines changed: 54 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
if TYPE_CHECKING:
2121
from collections.abc import Callable, Iterator
2222
from pathlib import Path
23-
from typing import Any, Unpack
23+
24+
from typing_extensions import Unpack
2425

2526
from ._conf import ConfigKwargs
2627

@@ -46,6 +47,8 @@ def list_vendors(**kwargs: Unpack[ConfigKwargs]) -> list[str]:
4647
4748
Other Parameters
4849
----------------
50+
cache_dir : pathlib.Path or str, optional
51+
Override the root cache directory.
4952
index_url : str, optional
5053
Alternative URL for the JDK index.
5154
@@ -122,8 +125,8 @@ def clear_cache(**kwargs: Unpack[ConfigKwargs]) -> Path:
122125
This should not be called when other processes may be using cjdk or the
123126
JDKs and files installed by cjdk.
124127
125-
Parameters
126-
----------
128+
Other Parameters
129+
----------------
127130
cache_dir : pathlib.Path or str, optional
128131
Override the root cache directory.
129132
@@ -280,7 +283,7 @@ def cache_file(
280283
name: str,
281284
url: str,
282285
filename: str,
283-
ttl: int | None = None,
286+
ttl: float | None = None,
284287
*,
285288
sha1: str | None = None,
286289
sha256: str | None = None,
@@ -301,13 +304,13 @@ def cache_file(
301304
The URL of the file resource. The scheme must be https.
302305
filename : str
303306
The filename under which the file will be stored.
304-
ttl : int
307+
ttl : int or float, optional
305308
Time to live (in seconds) for the cached file resource.
306-
sha1 : str
309+
sha1 : str, optional
307310
SHA-1 hash that the downloaded file must match.
308-
sha256 : str
311+
sha256 : str, optional
309312
SHA-256 hash that the downloaded file must match.
310-
sha512 : str
313+
sha512 : str, optional
311314
SHA-512 hash that the downloaded file must match.
312315
313316
Returns
@@ -328,6 +331,9 @@ def cache_file(
328331
The check for SHA-1/SHA-256/SHA-512 hashes is only performed after a
329332
download; it is not performed if the file already exists in the cache.
330333
"""
334+
_conf.check_str("name", name)
335+
_conf.check_str("url", url, allow_empty=False)
336+
_conf.check_str("filename", filename, allow_empty=False)
331337
if ttl is None:
332338
ttl = 2**63
333339
check_hashes = _make_hash_checker(
@@ -369,11 +375,11 @@ def cache_package(
369375
url : str
370376
The URL of the file resource. The scheme must be tgz+https or
371377
zip+https.
372-
sha1 : str
378+
sha1 : str, optional
373379
SHA-1 hash that the downloaded file must match.
374-
sha256 : str
380+
sha256 : str, optional
375381
SHA-256 hash that the downloaded file must match.
376-
sha512 : str
382+
sha512 : str, optional
377383
SHA-512 hash that the downloaded file must match.
378384
379385
Returns
@@ -394,6 +400,8 @@ def cache_package(
394400
unextracted archive) after a download; it is not performed if the directory
395401
already exists in the cache.
396402
"""
403+
_conf.check_str("name", name)
404+
_conf.check_str("url", url, allow_empty=False)
397405
check_hashes = _make_hash_checker(
398406
dict(sha1=sha1, sha256=sha256, sha512=sha512)
399407
)
@@ -428,34 +436,41 @@ def _get_vendors(**kwargs: Unpack[ConfigKwargs]) -> set[str]:
428436
}
429437

430438

431-
def _get_jdks(*, vendor=None, version=None, cached_only=True, **kwargs):
432-
conf = _conf.configure(
433-
vendor=vendor,
434-
version=version,
435-
fallback_to_default_vendor=False,
436-
**kwargs,
437-
)
438-
if conf.vendor is None:
439-
# Search across all vendors.
440-
kwargs.pop("jdk", None) # It was already parsed.
439+
def _get_jdks(
440+
*,
441+
vendor: str | None = None,
442+
version: str | None = None,
443+
cached_only: bool = True,
444+
**kwargs: Unpack[ConfigKwargs],
445+
) -> list[str]:
446+
jdk = kwargs.pop("jdk", None)
447+
if jdk:
448+
parsed_vendor, parsed_version = _conf.parse_vendor_version(jdk)
449+
vendor = vendor or parsed_vendor or None
450+
version = version or parsed_version or None
451+
452+
# Handle "all vendors" before creating Configuration.
453+
if vendor is None:
441454
return [
442455
jdk
443456
for v in sorted(_get_vendors())
444457
for jdk in _get_jdks(
445458
vendor=v,
446-
version=conf.version,
459+
version=version,
447460
cached_only=cached_only,
448461
**kwargs,
449462
)
450463
]
464+
465+
conf = _conf.configure(vendor=vendor, version=version, **kwargs)
451466
index = _index.jdk_index(conf)
452467
jdks = _index.available_jdks(index, conf)
453468
versions = _index._get_versions(jdks, conf)
454469
matched = _index._match_versions(conf.vendor, versions, conf.version)
455470

456471
if cached_only:
457472
# Filter matches by existing key directories.
458-
def is_cached(v):
473+
def is_cached(v: str) -> bool:
459474
url = _index.jdk_url(index, conf, v)
460475
key = (_jdk._JDK_KEY_PREFIX, _cache._key_for_url(url))
461476
keydir = _cache._key_directory(conf.cache_dir, key)
@@ -464,21 +479,22 @@ def is_cached(v):
464479
matched = {k: v for k, v in matched.items() if is_cached(v)}
465480

466481
class VersionElement:
467-
def __init__(self, value):
482+
def __init__(self, value: int | str) -> None:
468483
self.value = value
469-
self.is_int = isinstance(value, int)
470484

471-
def __eq__(self, other):
472-
if self.is_int and other.is_int:
485+
def __eq__(self, other: VersionElement) -> bool: # type: ignore[override]
486+
if isinstance(self.value, int) and isinstance(other.value, int):
473487
return self.value == other.value
474488
return str(self.value) == str(other.value)
475489

476-
def __lt__(self, other):
477-
if self.is_int and other.is_int:
490+
def __lt__(self, other: VersionElement) -> bool:
491+
if isinstance(self.value, int) and isinstance(other.value, int):
478492
return self.value < other.value
479493
return str(self.value) < str(other.value)
480494

481-
def version_key(version_tuple):
495+
def version_key(
496+
version_tuple: tuple[tuple[int | str, ...], str],
497+
) -> tuple[VersionElement, ...]:
482498
return tuple(VersionElement(elem) for elem in version_tuple[0])
483499

484500
return [
@@ -487,24 +503,26 @@ def version_key(version_tuple):
487503
]
488504

489505

490-
def _make_hash_checker(hashes: dict) -> Callable[[Any], None]:
506+
def _make_hash_checker(
507+
hashes: dict[str, str | None],
508+
) -> Callable[[Path], None]:
491509
checks = [
492510
(hashes.pop("sha1", None), hashlib.sha1),
493511
(hashes.pop("sha256", None), hashlib.sha256),
494512
(hashes.pop("sha512", None), hashlib.sha512),
495513
]
496514

497-
def check(filepath: Any) -> None:
515+
def check(filepath: Path) -> None:
498516
for hash, hasher in checks:
499517
if hash:
500518
_hasher = hasher()
501519
try:
502520
with open(filepath, "rb") as infile:
503521
while True:
504-
bytes = infile.read(16384)
505-
if not len(bytes):
522+
chunk = infile.read(16384)
523+
if not len(chunk):
506524
break
507-
_hasher.update(bytes)
525+
_hasher.update(chunk)
508526
except OSError as e:
509527
raise InstallError(
510528
f"Failed to read file for hash verification: {e}"
@@ -516,7 +534,7 @@ def check(filepath: Any) -> None:
516534

517535

518536
@contextmanager
519-
def _env_var_set(name, value):
537+
def _env_var_set(name: str, value: str) -> Iterator[None]:
520538
old_value = os.environ.get(name, None)
521539
os.environ[name] = value
522540
try:

0 commit comments

Comments
 (0)