typecheck-runner 0.1.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- typecheck_runner/__init__.py +21 -0
- typecheck_runner/__main__.py +7 -0
- typecheck_runner/py.typed +0 -0
- typecheck_runner/typecheck_runner.py +426 -0
- typecheck_runner-0.1.2.dist-info/METADATA +260 -0
- typecheck_runner-0.1.2.dist-info/RECORD +9 -0
- typecheck_runner-0.1.2.dist-info/WHEEL +4 -0
- typecheck_runner-0.1.2.dist-info/entry_points.txt +3 -0
- typecheck_runner-0.1.2.dist-info/licenses/LICENSE +24 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Top level API (:mod:`typecheck_runner`)
|
|
3
|
+
=======================================
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from importlib.metadata import PackageNotFoundError
|
|
7
|
+
from importlib.metadata import version as _version
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
__version__ = _version("typecheck-runner")
|
|
11
|
+
except PackageNotFoundError: # pragma: no cover
|
|
12
|
+
__version__ = "999"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
__author__ = """William P. Krekelberg"""
|
|
16
|
+
__email__ = "wpk@nist.gov"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"__version__",
|
|
21
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Interface to type checkers (mypy, (based)pyright, ty, pyrefly).
|
|
3
|
+
|
|
4
|
+
This handles locating python-version and python-executable. This allows for
|
|
5
|
+
running centrally installed (or via uvx) type checkers against a given virtual
|
|
6
|
+
environment.
|
|
7
|
+
"""
|
|
8
|
+
# pylint: disable=duplicate-code
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
import os
|
|
14
|
+
import shlex
|
|
15
|
+
import shutil
|
|
16
|
+
import subprocess
|
|
17
|
+
import sys
|
|
18
|
+
from argparse import ArgumentParser
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from time import perf_counter
|
|
21
|
+
from typing import TYPE_CHECKING
|
|
22
|
+
|
|
23
|
+
from packaging.requirements import Requirement
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from collections.abc import Sequence
|
|
27
|
+
from logging import Logger
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
FORMAT = "[typecheck-runner %(levelname)s] %(message)s"
|
|
31
|
+
logger: Logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# * Utilities -----------------------------------------------------------------
|
|
35
|
+
def _setup_logging(
|
|
36
|
+
verbosity: int = 0,
|
|
37
|
+
stdout: bool = False,
|
|
38
|
+
) -> None: # pragma: no cover
|
|
39
|
+
"""Setup logging."""
|
|
40
|
+
level_number = max(0, logging.WARNING - 10 * verbosity)
|
|
41
|
+
logger.setLevel(level_number)
|
|
42
|
+
|
|
43
|
+
# Silence noisy loggers
|
|
44
|
+
logging.getLogger("sh").setLevel(logging.WARNING)
|
|
45
|
+
|
|
46
|
+
if stdout:
|
|
47
|
+
handler = logging.StreamHandler(sys.stdout)
|
|
48
|
+
logging.basicConfig(level=logging.WARNING, format=FORMAT, handlers=[handler])
|
|
49
|
+
else:
|
|
50
|
+
logging.basicConfig(level=logging.WARNING, format=FORMAT)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# * Runner --------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _infer_venv_location() -> Path:
|
|
57
|
+
for var in ("VIRTUAL_ENV", "CONDA_PREFIX"):
|
|
58
|
+
if venv := os.getenv(var, ""):
|
|
59
|
+
logger.debug("Inferred venv location %s", venv)
|
|
60
|
+
return Path(venv).absolute()
|
|
61
|
+
|
|
62
|
+
for d in (".venv",):
|
|
63
|
+
if (path := Path(d)).is_dir():
|
|
64
|
+
logger.debug("Inferred venv location %s", path)
|
|
65
|
+
return path.absolute()
|
|
66
|
+
|
|
67
|
+
msg = "Could not infer virtual environment"
|
|
68
|
+
raise ValueError(msg)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _get_python_executable_from_venv(
|
|
72
|
+
location: Path,
|
|
73
|
+
) -> Path:
|
|
74
|
+
executable = "python.exe" if sys.platform.startswith("win") else "python"
|
|
75
|
+
for d in ("bin", "Scripts"):
|
|
76
|
+
if (path := location / d / executable).exists():
|
|
77
|
+
logger.debug("Inferred python-executable %s", path)
|
|
78
|
+
return path.absolute()
|
|
79
|
+
|
|
80
|
+
msg = f"No virtual environment found under {location}"
|
|
81
|
+
raise ValueError(msg)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _get_python_executable(
|
|
85
|
+
python_executable: Path | None, venv: Path | None, infer_venv: bool
|
|
86
|
+
) -> Path | None:
|
|
87
|
+
if venv is None and infer_venv:
|
|
88
|
+
venv = _infer_venv_location()
|
|
89
|
+
|
|
90
|
+
if venv is not None:
|
|
91
|
+
return _get_python_executable_from_venv(venv)
|
|
92
|
+
|
|
93
|
+
return python_executable
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _get_python_values(
|
|
97
|
+
python_version: str | None,
|
|
98
|
+
python_executable: Path | None,
|
|
99
|
+
no_python_version: bool,
|
|
100
|
+
no_python_executable: bool,
|
|
101
|
+
) -> tuple[str | None, Path | None]:
|
|
102
|
+
if python_version is None and not no_python_version:
|
|
103
|
+
if python_executable is not None:
|
|
104
|
+
# infer python version from python_executable
|
|
105
|
+
logger.debug(
|
|
106
|
+
"Calculate python-version from executable %s", python_executable
|
|
107
|
+
)
|
|
108
|
+
script = "import sys; info = sys.version_info; print(f'{info.major}.{info.minor}')"
|
|
109
|
+
python_version = (
|
|
110
|
+
subprocess.check_output([python_executable, "-c", script])
|
|
111
|
+
.decode("utf-8")
|
|
112
|
+
.strip()
|
|
113
|
+
)
|
|
114
|
+
else:
|
|
115
|
+
python_version = f"{sys.version_info.major}.{sys.version_info.minor}"
|
|
116
|
+
|
|
117
|
+
if python_executable is None and not no_python_executable:
|
|
118
|
+
python_executable = Path(sys.executable)
|
|
119
|
+
|
|
120
|
+
return python_version, python_executable
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _maybe_add_check_argument(checker: str, args: list[str]) -> list[str]:
|
|
124
|
+
if checker in {"ty", "pyrefly"} and "check" not in args:
|
|
125
|
+
return ["check", *args]
|
|
126
|
+
return args
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _parse_command(
|
|
130
|
+
command: str,
|
|
131
|
+
no_uvx: bool,
|
|
132
|
+
uvx_delimiter: str,
|
|
133
|
+
uvx_options: Sequence[str],
|
|
134
|
+
) -> tuple[str, list[str]]:
|
|
135
|
+
command, *args = shlex.split(command)
|
|
136
|
+
|
|
137
|
+
if no_uvx:
|
|
138
|
+
path = Path(command).expanduser()
|
|
139
|
+
checker = path.name
|
|
140
|
+
path_str = str(path)
|
|
141
|
+
return checker, [
|
|
142
|
+
shutil.which(path_str) or path_str,
|
|
143
|
+
*_maybe_add_check_argument(checker, args),
|
|
144
|
+
]
|
|
145
|
+
|
|
146
|
+
req = Requirement(command)
|
|
147
|
+
checker = req.name
|
|
148
|
+
|
|
149
|
+
idx = args.index(uvx_delimiter) if uvx_delimiter in args else len(args)
|
|
150
|
+
checker_args = args[:idx]
|
|
151
|
+
uvx_args = args[idx + 1 :]
|
|
152
|
+
|
|
153
|
+
args = [
|
|
154
|
+
"uvx",
|
|
155
|
+
*uvx_options,
|
|
156
|
+
*uvx_args,
|
|
157
|
+
command,
|
|
158
|
+
*_maybe_add_check_argument(checker, checker_args),
|
|
159
|
+
]
|
|
160
|
+
return checker, args
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
PYRIGHT_LIKE_CHECKERS = {"pyright", "basedpyright"}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _get_python_flags(
|
|
167
|
+
checker: str,
|
|
168
|
+
python_version: str | None,
|
|
169
|
+
python_executable: str | Path | None,
|
|
170
|
+
) -> list[str]:
|
|
171
|
+
out: list[str] = []
|
|
172
|
+
if python_version is not None:
|
|
173
|
+
version_flag = (
|
|
174
|
+
"pythonversion" if checker in PYRIGHT_LIKE_CHECKERS else "python-version"
|
|
175
|
+
)
|
|
176
|
+
out.append(f"--{version_flag}={python_version}")
|
|
177
|
+
|
|
178
|
+
if python_executable is not None:
|
|
179
|
+
if checker in PYRIGHT_LIKE_CHECKERS:
|
|
180
|
+
python_flag = "pythonpath"
|
|
181
|
+
elif checker == "ty":
|
|
182
|
+
python_flag = "python"
|
|
183
|
+
elif checker == "pyrefly":
|
|
184
|
+
python_flag = "python-interpreter-path"
|
|
185
|
+
elif checker == "mypy":
|
|
186
|
+
# default to mypy
|
|
187
|
+
python_flag = "python-executable"
|
|
188
|
+
else:
|
|
189
|
+
msg = f"Unknown checker {checker}"
|
|
190
|
+
raise ValueError(msg)
|
|
191
|
+
out.append(f"--{python_flag}={python_executable}")
|
|
192
|
+
|
|
193
|
+
return out
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _run_checker(
|
|
197
|
+
*args: str,
|
|
198
|
+
dry_run: bool = False,
|
|
199
|
+
) -> int:
|
|
200
|
+
cleaned_args = [os.fsdecode(arg) for arg in args]
|
|
201
|
+
full_cmd = shlex.join(cleaned_args)
|
|
202
|
+
logger.info("Command: %s", full_cmd)
|
|
203
|
+
|
|
204
|
+
if dry_run:
|
|
205
|
+
return 0
|
|
206
|
+
|
|
207
|
+
start_time = perf_counter()
|
|
208
|
+
returncode = subprocess.call(cleaned_args)
|
|
209
|
+
logger.info("Execution time: %s", perf_counter() - start_time)
|
|
210
|
+
if returncode:
|
|
211
|
+
logger.error("Failed with exit code: %s", returncode)
|
|
212
|
+
|
|
213
|
+
return returncode
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
# * Application ---------------------------------------------------------------
|
|
217
|
+
def get_parser() -> ArgumentParser:
|
|
218
|
+
"""Get argparser."""
|
|
219
|
+
parser = ArgumentParser(description="Run executable using uvx.")
|
|
220
|
+
_ = parser.add_argument("--version", action="store_true", help="Display version.")
|
|
221
|
+
|
|
222
|
+
_ = parser.add_argument(
|
|
223
|
+
"-c",
|
|
224
|
+
"--check",
|
|
225
|
+
dest="checkers",
|
|
226
|
+
default=[],
|
|
227
|
+
action="append",
|
|
228
|
+
help="""
|
|
229
|
+
Checker to run. This can be a string with options to the checker. For
|
|
230
|
+
example, ``--check "mypy --verbose"`` runs the checker the command
|
|
231
|
+
``mypy --verbose``. Options after ``uvx_delimiter`` (default ``"--"``,
|
|
232
|
+
see ``--uvx-delimiter`` options) are treated as ``uvx`` options. For
|
|
233
|
+
example, passing ``--check "mypy --verbose -- --reinstall"`` will run
|
|
234
|
+
``uvx --reinstall mypy --verbose``. Can be specified multiple times.
|
|
235
|
+
""",
|
|
236
|
+
)
|
|
237
|
+
_ = parser.add_argument(
|
|
238
|
+
"--python-executable",
|
|
239
|
+
dest="python_executable",
|
|
240
|
+
default=None,
|
|
241
|
+
type=Path,
|
|
242
|
+
help="""
|
|
243
|
+
Path to python executable. Defaults to ``sys.executable``. This is
|
|
244
|
+
passed to ``--python-executable`` (mypy), ``--pythonpath`` in
|
|
245
|
+
((based)pyright), ``--python`` (ty), ``--python-interpreter-path``
|
|
246
|
+
(pyrefly), and ignored for pylint.
|
|
247
|
+
""",
|
|
248
|
+
)
|
|
249
|
+
_ = parser.add_argument(
|
|
250
|
+
"--python-version",
|
|
251
|
+
dest="python_version",
|
|
252
|
+
default=None,
|
|
253
|
+
type=str,
|
|
254
|
+
help="""
|
|
255
|
+
Python version (x.y) to typecheck against. Defaults to
|
|
256
|
+
``{sys.version_info.major}.{sys.version_info.minor}``. This is passed
|
|
257
|
+
to ``--pythonversion`` in pyright and ``--python-version`` otherwise.
|
|
258
|
+
""",
|
|
259
|
+
)
|
|
260
|
+
_ = parser.add_argument(
|
|
261
|
+
"--no-python-executable",
|
|
262
|
+
action="store_true",
|
|
263
|
+
help="""
|
|
264
|
+
Do not infer ``python_executable``
|
|
265
|
+
""",
|
|
266
|
+
)
|
|
267
|
+
_ = parser.add_argument(
|
|
268
|
+
"--no-python-version",
|
|
269
|
+
action="store_true",
|
|
270
|
+
help="""
|
|
271
|
+
Do not infer ``python_version``.
|
|
272
|
+
""",
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
_ = parser.add_argument(
|
|
276
|
+
"--venv",
|
|
277
|
+
type=Path,
|
|
278
|
+
help="""
|
|
279
|
+
Use specified vitualenvironment location
|
|
280
|
+
""",
|
|
281
|
+
)
|
|
282
|
+
_ = parser.add_argument(
|
|
283
|
+
"--infer-venv",
|
|
284
|
+
action="store_true",
|
|
285
|
+
help="""
|
|
286
|
+
Infer virtual environment location. Checks in order environment variables ``VIRTUAL_ENV``, ``CONDA_PREFIX``, directory ``.venv``.
|
|
287
|
+
""",
|
|
288
|
+
)
|
|
289
|
+
_ = parser.add_argument(
|
|
290
|
+
"--constraints",
|
|
291
|
+
dest="constraints",
|
|
292
|
+
default=[],
|
|
293
|
+
action="append",
|
|
294
|
+
type=Path,
|
|
295
|
+
help="""
|
|
296
|
+
Constraints (requirements.txt) specs for checkers. Can specify multiple
|
|
297
|
+
times. Passed to ``uvx --constraints=...``.
|
|
298
|
+
""",
|
|
299
|
+
)
|
|
300
|
+
_ = parser.add_argument(
|
|
301
|
+
"-v",
|
|
302
|
+
"--verbose",
|
|
303
|
+
dest="verbosity",
|
|
304
|
+
action="count",
|
|
305
|
+
default=0,
|
|
306
|
+
help="Set verbosity level. Pass multiple times to up level.",
|
|
307
|
+
)
|
|
308
|
+
_ = parser.add_argument(
|
|
309
|
+
"--stdout", action="store_true", help="logger information to stdout"
|
|
310
|
+
)
|
|
311
|
+
_ = parser.add_argument(
|
|
312
|
+
"--allow-errors",
|
|
313
|
+
action="store_true",
|
|
314
|
+
help="""
|
|
315
|
+
If passed, return ``0`` regardless of checker status.
|
|
316
|
+
""",
|
|
317
|
+
)
|
|
318
|
+
_ = parser.add_argument(
|
|
319
|
+
"--fail-fast",
|
|
320
|
+
action="store_true",
|
|
321
|
+
help="""
|
|
322
|
+
Exit on first failed checker. Default is to run all checkers, even if
|
|
323
|
+
they fail.
|
|
324
|
+
""",
|
|
325
|
+
)
|
|
326
|
+
_ = parser.add_argument(
|
|
327
|
+
"--dry-run", action="store_true", help="""Perform dry run."""
|
|
328
|
+
)
|
|
329
|
+
_ = parser.add_argument(
|
|
330
|
+
"--no-uvx",
|
|
331
|
+
dest="no_uvx",
|
|
332
|
+
action="store_true",
|
|
333
|
+
help="""
|
|
334
|
+
If ``--no-uvx`` is passed, assume typecheckers are in the current
|
|
335
|
+
python environment. Default is to invoke typecheckers using `uvx`.
|
|
336
|
+
""",
|
|
337
|
+
)
|
|
338
|
+
_ = parser.add_argument(
|
|
339
|
+
"--uvx-options",
|
|
340
|
+
default="",
|
|
341
|
+
help="""
|
|
342
|
+
Extra options to pass to ``uvx``. Note that you may have to escape the
|
|
343
|
+
first option. For example, ``--uvx-options "\\--verbose --reinstall"
|
|
344
|
+
""",
|
|
345
|
+
)
|
|
346
|
+
_ = parser.add_argument(
|
|
347
|
+
"--uvx-delimiter",
|
|
348
|
+
default="--",
|
|
349
|
+
help="""
|
|
350
|
+
Delimiter between typechecker command arguments and ``uvx`` arguments.
|
|
351
|
+
See ``--check`` option.
|
|
352
|
+
""",
|
|
353
|
+
)
|
|
354
|
+
_ = parser.add_argument(
|
|
355
|
+
"args",
|
|
356
|
+
type=str,
|
|
357
|
+
nargs="*",
|
|
358
|
+
default=[],
|
|
359
|
+
help="Extra files/arguments passed to all checkers.",
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
return parser
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def main(args: Sequence[str] | None = None) -> int:
|
|
366
|
+
"""Main script."""
|
|
367
|
+
parser = get_parser()
|
|
368
|
+
options = parser.parse_args(args)
|
|
369
|
+
|
|
370
|
+
if options.version:
|
|
371
|
+
from typecheck_runner import __version__
|
|
372
|
+
|
|
373
|
+
print("typecheck-runner", __version__) # noqa: T201
|
|
374
|
+
return 0
|
|
375
|
+
|
|
376
|
+
if not options.checkers:
|
|
377
|
+
parser.print_help()
|
|
378
|
+
return 2
|
|
379
|
+
|
|
380
|
+
_setup_logging(options.verbosity, options.stdout)
|
|
381
|
+
|
|
382
|
+
# possibly infer python executable from location
|
|
383
|
+
python_version, python_executable = _get_python_values(
|
|
384
|
+
python_version=options.python_version,
|
|
385
|
+
python_executable=_get_python_executable(
|
|
386
|
+
options.python_executable, options.venv, options.infer_venv
|
|
387
|
+
),
|
|
388
|
+
no_python_version=options.no_python_version,
|
|
389
|
+
no_python_executable=options.no_python_executable,
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
logger.info("python_executable %s", python_executable)
|
|
393
|
+
logger.info("python_version %s", python_version)
|
|
394
|
+
logger.debug("checkers: %s", options.checkers)
|
|
395
|
+
logger.debug("args: %s", options.args)
|
|
396
|
+
|
|
397
|
+
uvx_options = [
|
|
398
|
+
*shlex.split(options.uvx_options),
|
|
399
|
+
*(f"--constraints={c}" for c in options.constraints),
|
|
400
|
+
]
|
|
401
|
+
|
|
402
|
+
code = 0
|
|
403
|
+
for command in options.checkers:
|
|
404
|
+
checker, args = _parse_command(
|
|
405
|
+
command,
|
|
406
|
+
no_uvx=options.no_uvx,
|
|
407
|
+
uvx_delimiter=options.uvx_delimiter,
|
|
408
|
+
uvx_options=uvx_options,
|
|
409
|
+
)
|
|
410
|
+
logger.info("Checker: %s", checker)
|
|
411
|
+
checker_code = _run_checker(
|
|
412
|
+
*args,
|
|
413
|
+
*_get_python_flags(checker, python_version, python_executable),
|
|
414
|
+
*options.args,
|
|
415
|
+
dry_run=options.dry_run,
|
|
416
|
+
)
|
|
417
|
+
if options.fail_fast and checker_code:
|
|
418
|
+
return checker_code
|
|
419
|
+
|
|
420
|
+
code += checker_code
|
|
421
|
+
|
|
422
|
+
return 0 if options.allow_errors else code
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
if __name__ == "__main__":
|
|
426
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: typecheck-runner
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: Unified api to multiple typecheckers
|
|
5
|
+
Keywords: typecheck-runner
|
|
6
|
+
Author: William P. Krekelberg
|
|
7
|
+
Author-email: William P. Krekelberg <wpk@nist.gov>
|
|
8
|
+
License-Expression: NIST-PD
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python
|
|
13
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
20
|
+
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
21
|
+
Classifier: Topic :: Utilities
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Dist: packaging>=25.0
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Project-URL: Homepage, https://github.com/wpk-nist-gov/typecheck-runner
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
<!-- markdownlint-disable MD041 -->
|
|
29
|
+
|
|
30
|
+
<!-- prettier-ignore-start -->
|
|
31
|
+
[![Repo][repo-badge]][repo-link]
|
|
32
|
+
[![PyPI license][license-badge]][license-link]
|
|
33
|
+
[![PyPI version][pypi-badge]][pypi-link]
|
|
34
|
+
[![Code style: ruff][ruff-badge]][ruff-link]
|
|
35
|
+
[![uv][uv-badge]][uv-link]
|
|
36
|
+
|
|
37
|
+
<!--
|
|
38
|
+
For more badges, see
|
|
39
|
+
https://shields.io/category/other
|
|
40
|
+
https://naereen.github.io/badges/
|
|
41
|
+
[pypi-badge]: https://badge.fury.io/py/typecheck-runner
|
|
42
|
+
-->
|
|
43
|
+
|
|
44
|
+
[ruff-badge]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json
|
|
45
|
+
[ruff-link]: https://github.com/astral-sh/ruff
|
|
46
|
+
[uv-badge]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json
|
|
47
|
+
[uv-link]: https://github.com/astral-sh/uv
|
|
48
|
+
[pypi-badge]: https://img.shields.io/pypi/v/typecheck-runner
|
|
49
|
+
[pypi-link]: https://pypi.org/project/typecheck-runner
|
|
50
|
+
[repo-badge]: https://img.shields.io/badge/--181717?logo=github&logoColor=ffffff
|
|
51
|
+
[repo-link]: https://github.com/wpk-nist-gov/typecheck-runner
|
|
52
|
+
[license-badge]: https://img.shields.io/pypi/l/typecheck-runner?color=informational
|
|
53
|
+
[license-link]: https://github.com/wpk-nist-gov/typecheck-runner/blob/main/LICENSE
|
|
54
|
+
[changelog-link]: https://github.com/wpk-nist-gov/typecheck-runner/blob/main/CHANGELOG.md
|
|
55
|
+
|
|
56
|
+
<!-- other links -->
|
|
57
|
+
|
|
58
|
+
[mypy]: https://github.com/python/mypy
|
|
59
|
+
[pyright]: https://github.com/microsoft/pyright
|
|
60
|
+
[basedpyright]: https://github.com/DetachHead/basedpyright
|
|
61
|
+
[ty]: https://github.com/astral-sh/ty
|
|
62
|
+
[pyrefly]: https://github.com/microsoft/pyright
|
|
63
|
+
<!-- [pre-commit]: https://pre-commit.com/ -->
|
|
64
|
+
<!-- [prek]: https://github.com/j178/prek -->
|
|
65
|
+
|
|
66
|
+
<!-- prettier-ignore-end -->
|
|
67
|
+
|
|
68
|
+
# `typecheck-runner`
|
|
69
|
+
|
|
70
|
+
A unified way to run globally installed typecheckers against a specified virtual
|
|
71
|
+
environment.
|
|
72
|
+
|
|
73
|
+
## Overview
|
|
74
|
+
|
|
75
|
+
I prefer to invoke globally managed type checkers against specified virtual
|
|
76
|
+
environments. For cases where python versions are checked against (with, for
|
|
77
|
+
example tox or nox), this prevents each virtual environment from having to
|
|
78
|
+
contain a type checker. Each type checker ([mypy], [pyright], [basedpyright],
|
|
79
|
+
[ty], and [pyrefly]) has it's own particular flags to specify the python
|
|
80
|
+
executable and the python version. `typecheck-runner` unifies these flags. Also,
|
|
81
|
+
by default, `typecheck-runner` invokes the type checker using
|
|
82
|
+
[`uvx`](https://docs.astral.sh/uv/guides/tools/), which installs the type
|
|
83
|
+
checker if needed.
|
|
84
|
+
|
|
85
|
+
## Usage
|
|
86
|
+
|
|
87
|
+
### Install into virtual environment
|
|
88
|
+
|
|
89
|
+
The easiest way to use `typecheck-runner` is to install it into the virtual
|
|
90
|
+
environment you'd like to test against using something like
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
pip install typecheck-runner
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
from the virtual environment of interest. To invoke a type checker against the
|
|
97
|
+
virtual environment, assuming the python executable of the virtual environment
|
|
98
|
+
is located at `/path/to/venv/bin` with python version `3.13`, use
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
typecheck-runner --check mypy
|
|
102
|
+
# runs: uvx mypy --python-version=3.13 --python-executable=/path/to/venv/bin
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Where the commented line shows the command run. Specifying `--no-uvx` will
|
|
106
|
+
instead invoke the type checker without `uvx`, so the type checker must already
|
|
107
|
+
be installed.
|
|
108
|
+
|
|
109
|
+
You can specify multiple checkers with multiple `--check` flags. To specify
|
|
110
|
+
options to `uvx` for each checker, pass options after `--uvx-delimiter` which
|
|
111
|
+
defaults to `--`. For example:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
typecheck-runner --check "mypy --verbose -- --reinstall"
|
|
115
|
+
# runs: uvx --reinstall mypy --verbose
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
You can specify `uvx` options to all checkers using the `--uvx-options` flag.
|
|
119
|
+
|
|
120
|
+
### Specify virtual environment
|
|
121
|
+
|
|
122
|
+
You can also use a globally installed `typecheck-runner` and specify which
|
|
123
|
+
virtual environment to test over using `--venv` or `--infer-venv` options. For
|
|
124
|
+
example, you can use:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
uvx typecheck-runner --venv .venv --check mypy
|
|
128
|
+
# run for example (if .venv current directory with version 3.14)
|
|
129
|
+
# uvx mypy --python-version=3.14 --python-executable=.venv/bin/python
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Using `--infer-venv` will attempt to infer the virtual environment from, in
|
|
133
|
+
order, environment variables `VIRTUAL_ENV`, `CONDA_PREFIX`, and finally `.venv`
|
|
134
|
+
in current directory.
|
|
135
|
+
|
|
136
|
+
## Options
|
|
137
|
+
|
|
138
|
+
<!-- markdownlint-disable-next-line MD013 -->
|
|
139
|
+
<!-- [[[cog
|
|
140
|
+
import sys
|
|
141
|
+
sys.path.insert(0, ".")
|
|
142
|
+
from tools.cog_utils import wrap_command, get_pyproject, run_command, cat_lines
|
|
143
|
+
sys.path.pop(0)
|
|
144
|
+
]]] -->
|
|
145
|
+
<!-- [[[end]]] -->
|
|
146
|
+
|
|
147
|
+
<!-- prettier-ignore-start -->
|
|
148
|
+
<!-- markdownlint-disable MD013 -->
|
|
149
|
+
<!-- [[[cog run_command("typecheck-runner --help", include_cmd=False, wrapper="restructuredtext")]]] -->
|
|
150
|
+
|
|
151
|
+
```restructuredtext
|
|
152
|
+
usage: typecheck-runner [-h] [--version] [-c CHECKERS]
|
|
153
|
+
[--python-executable PYTHON_EXECUTABLE]
|
|
154
|
+
[--python-version PYTHON_VERSION] [--no-python-executable]
|
|
155
|
+
[--no-python-version] [--venv VENV] [--infer-venv]
|
|
156
|
+
[--constraints CONSTRAINTS] [-v] [--stdout] [--allow-errors]
|
|
157
|
+
[--fail-fast] [--dry-run] [--no-uvx] [--uvx-options UVX_OPTIONS]
|
|
158
|
+
[--uvx-delimiter UVX_DELIMITER]
|
|
159
|
+
[args ...]
|
|
160
|
+
|
|
161
|
+
Run executable using uvx.
|
|
162
|
+
|
|
163
|
+
positional arguments:
|
|
164
|
+
args Extra files/arguments passed to all checkers.
|
|
165
|
+
|
|
166
|
+
options:
|
|
167
|
+
-h, --help show this help message and exit
|
|
168
|
+
--version Display version.
|
|
169
|
+
-c, --check CHECKERS Checker to run. This can be a string with options to the
|
|
170
|
+
checker. For example, ``--check "mypy --verbose"`` runs the
|
|
171
|
+
checker the command ``mypy --verbose``. Options after
|
|
172
|
+
``uvx_delimiter`` (default ``"--"``, see ``--uvx-delimiter``
|
|
173
|
+
options) are treated as ``uvx`` options. For example, passing
|
|
174
|
+
``--check "mypy --verbose -- --reinstall"`` will run ``uvx
|
|
175
|
+
--reinstall mypy --verbose``. Can be specified multiple times.
|
|
176
|
+
--python-executable PYTHON_EXECUTABLE
|
|
177
|
+
Path to python executable. Defaults to ``sys.executable``. This
|
|
178
|
+
is passed to ``--python-executable`` (mypy), ``--pythonpath`` in
|
|
179
|
+
((based)pyright), ``--python`` (ty), ``--python-interpreter-
|
|
180
|
+
path`` (pyrefly), and ignored for pylint.
|
|
181
|
+
--python-version PYTHON_VERSION
|
|
182
|
+
Python version (x.y) to typecheck against. Defaults to
|
|
183
|
+
``{sys.version_info.major}.{sys.version_info.minor}``. This is
|
|
184
|
+
passed to ``--pythonversion`` in pyright and ``--python-
|
|
185
|
+
version`` otherwise.
|
|
186
|
+
--no-python-executable
|
|
187
|
+
Do not infer ``python_executable``
|
|
188
|
+
--no-python-version Do not infer ``python_version``.
|
|
189
|
+
--venv VENV Use specified vitualenvironment location
|
|
190
|
+
--infer-venv Infer virtual environment location. Checks in order environment
|
|
191
|
+
variables ``VIRTUAL_ENV``, ``CONDA_PREFIX``, directory
|
|
192
|
+
``.venv``.
|
|
193
|
+
--constraints CONSTRAINTS
|
|
194
|
+
Constraints (requirements.txt) specs for checkers. Can specify
|
|
195
|
+
multiple times. Passed to ``uvx --constraints=...``.
|
|
196
|
+
-v, --verbose Set verbosity level. Pass multiple times to up level.
|
|
197
|
+
--stdout logger information to stdout
|
|
198
|
+
--allow-errors If passed, return ``0`` regardless of checker status.
|
|
199
|
+
--fail-fast Exit on first failed checker. Default is to run all checkers,
|
|
200
|
+
even if they fail.
|
|
201
|
+
--dry-run Perform dry run.
|
|
202
|
+
--no-uvx If ``--no-uvx`` is passed, assume typecheckers are in the
|
|
203
|
+
current python environment. Default is to invoke typecheckers
|
|
204
|
+
using `uvx`.
|
|
205
|
+
--uvx-options UVX_OPTIONS
|
|
206
|
+
Extra options to pass to ``uvx``. Note that you may have to
|
|
207
|
+
escape the first option. For example, ``--uvx-options
|
|
208
|
+
"\--verbose --reinstall"
|
|
209
|
+
--uvx-delimiter UVX_DELIMITER
|
|
210
|
+
Delimiter between typechecker command arguments and ``uvx``
|
|
211
|
+
arguments. See ``--check`` option.
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
<!-- [[[end]]] -->
|
|
215
|
+
<!-- prettier-ignore-end -->
|
|
216
|
+
|
|
217
|
+
## Status
|
|
218
|
+
|
|
219
|
+
This package is actively used by the author. Please feel free to create a pull
|
|
220
|
+
request for wanted features and suggestions!
|
|
221
|
+
|
|
222
|
+
<!-- end-docs -->
|
|
223
|
+
|
|
224
|
+
## Installation
|
|
225
|
+
|
|
226
|
+
<!-- start-installation -->
|
|
227
|
+
|
|
228
|
+
Use one of the following
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
pip install typecheck-runner
|
|
232
|
+
uv pip install typecheck-runner
|
|
233
|
+
uv add typecheck-runner
|
|
234
|
+
...
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
<!-- end-installation -->
|
|
238
|
+
|
|
239
|
+
## What's new?
|
|
240
|
+
|
|
241
|
+
See [changelog][changelog-link].
|
|
242
|
+
|
|
243
|
+
## License
|
|
244
|
+
|
|
245
|
+
This is free software. See [LICENSE][license-link].
|
|
246
|
+
|
|
247
|
+
## Related work
|
|
248
|
+
|
|
249
|
+
Any other stuff to mention....
|
|
250
|
+
|
|
251
|
+
## Contact
|
|
252
|
+
|
|
253
|
+
The author can be reached at <wpk@nist.gov>.
|
|
254
|
+
|
|
255
|
+
## Credits
|
|
256
|
+
|
|
257
|
+
This package was created using
|
|
258
|
+
[Cookiecutter](https://github.com/audreyr/cookiecutter) with the
|
|
259
|
+
[usnistgov/cookiecutter-nist-python](https://github.com/usnistgov/cookiecutter-nist-python)
|
|
260
|
+
template.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
typecheck_runner/__init__.py,sha256=bN5LuK_PNk5YFr2rdgX1CwJWHRMtspp30M1SHd2uhbE,423
|
|
2
|
+
typecheck_runner/__main__.py,sha256=rRzOwUCzMPhYnn4hruIKPM_vSOddm9rCZ6dF5SW4_m0,167
|
|
3
|
+
typecheck_runner/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
typecheck_runner/typecheck_runner.py,sha256=o21akaFiaacdkki2dRbZ7fdTvZ2hqIUzEd1Yg2JmNF8,12588
|
|
5
|
+
typecheck_runner-0.1.2.dist-info/licenses/LICENSE,sha256=j_sUyRmuUbF_c9KR7g_uTdRv-cBn65wjYQpmusAzyT0,1645
|
|
6
|
+
typecheck_runner-0.1.2.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
|
|
7
|
+
typecheck_runner-0.1.2.dist-info/entry_points.txt,sha256=DnC_eOsN1KeMB9CKwGKR4DJ7vcbAExz1JlNBy1x9PPE,77
|
|
8
|
+
typecheck_runner-0.1.2.dist-info/METADATA,sha256=wU2JMhkwg1oECleZwgSm_mJpVs3JUk-9hi7_8kRsSmM,10094
|
|
9
|
+
typecheck_runner-0.1.2.dist-info/RECORD,,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
This software was developed by employees of the National Institute of Standards
|
|
2
|
+
and Technology (NIST), an agency of the Federal Government. Pursuant to title 17
|
|
3
|
+
United States Code Section 105, works of NIST employees are not subject to
|
|
4
|
+
copyright protection in the United States and are considered to be in the public
|
|
5
|
+
domain. Permission to freely use, copy, modify, and distribute this software and
|
|
6
|
+
its documentation without fee is hereby granted, provided that this notice and
|
|
7
|
+
disclaimer of warranty appears in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED 'AS IS' WITHOUT ANY WARRANTY OF ANY KIND, EITHER
|
|
10
|
+
EXPRESSED, IMPLIED, OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY
|
|
11
|
+
THAT THE SOFTWARE WILL CONFORM TO SPECIFICATIONS, ANY IMPLIED WARRANTIES OF
|
|
12
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND FREEDOM FROM
|
|
13
|
+
INFRINGEMENT, AND ANY WARRANTY THAT THE DOCUMENTATION WILL CONFORM TO THE
|
|
14
|
+
SOFTWARE, OR ANY WARRANTY THAT THE SOFTWARE WILL BE ERROR FREE. IN NO EVENT
|
|
15
|
+
SHALL NIST BE LIABLE FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO, DIRECT,
|
|
16
|
+
INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES, ARISING OUT OF, RESULTING FROM, OR
|
|
17
|
+
IN ANY WAY CONNECTED WITH THIS SOFTWARE, WHETHER OR NOT BASED UPON WARRANTY,
|
|
18
|
+
CONTRACT, TORT, OR OTHERWISE, WHETHER OR NOT INJURY WAS SUSTAINED BY PERSONS OR
|
|
19
|
+
PROPERTY OR OTHERWISE, AND WHETHER OR NOT LOSS WAS SUSTAINED FROM, OR AROSE OUT
|
|
20
|
+
OF THE RESULTS OF, OR USE OF, THE SOFTWARE OR SERVICES PROVIDED HEREUNDER.
|
|
21
|
+
|
|
22
|
+
Distributions of NIST software should also include copyright and licensing
|
|
23
|
+
statements of any third-party software that are legally bundled with the code in
|
|
24
|
+
compliance with the conditions of those licenses.
|