duty 1.6.0__py3-none-any.whl → 1.6.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.
- duty/__init__.py +49 -2
- duty/__main__.py +1 -1
- duty/_internal/__init__.py +0 -0
- duty/_internal/callables/__init__.py +34 -0
- duty/{callables → _internal/callables}/_io.py +2 -0
- duty/_internal/callables/autoflake.py +132 -0
- duty/_internal/callables/black.py +176 -0
- duty/_internal/callables/blacken_docs.py +92 -0
- duty/_internal/callables/build.py +76 -0
- duty/_internal/callables/coverage.py +716 -0
- duty/_internal/callables/flake8.py +222 -0
- duty/_internal/callables/git_changelog.py +178 -0
- duty/_internal/callables/griffe.py +227 -0
- duty/_internal/callables/interrogate.py +152 -0
- duty/_internal/callables/isort.py +573 -0
- duty/_internal/callables/mkdocs.py +256 -0
- duty/_internal/callables/mypy.py +496 -0
- duty/_internal/callables/pytest.py +475 -0
- duty/_internal/callables/ruff.py +399 -0
- duty/_internal/callables/safety.py +82 -0
- duty/_internal/callables/ssort.py +38 -0
- duty/_internal/callables/twine.py +284 -0
- duty/_internal/cli.py +322 -0
- duty/_internal/collection.py +246 -0
- duty/_internal/context.py +111 -0
- duty/{debug.py → _internal/debug.py} +13 -15
- duty/_internal/decorator.py +111 -0
- duty/_internal/exceptions.py +12 -0
- duty/_internal/tools/__init__.py +41 -0
- duty/{tools → _internal/tools}/_autoflake.py +8 -4
- duty/{tools → _internal/tools}/_base.py +15 -2
- duty/{tools → _internal/tools}/_black.py +5 -5
- duty/{tools → _internal/tools}/_blacken_docs.py +10 -5
- duty/{tools → _internal/tools}/_build.py +4 -4
- duty/{tools → _internal/tools}/_coverage.py +8 -4
- duty/{tools → _internal/tools}/_flake8.py +10 -12
- duty/{tools → _internal/tools}/_git_changelog.py +8 -4
- duty/{tools → _internal/tools}/_griffe.py +8 -4
- duty/{tools → _internal/tools}/_interrogate.py +4 -4
- duty/{tools → _internal/tools}/_isort.py +8 -6
- duty/{tools → _internal/tools}/_mkdocs.py +8 -4
- duty/{tools → _internal/tools}/_mypy.py +5 -5
- duty/{tools → _internal/tools}/_pytest.py +8 -4
- duty/{tools → _internal/tools}/_ruff.py +11 -5
- duty/{tools → _internal/tools}/_safety.py +13 -8
- duty/{tools → _internal/tools}/_ssort.py +10 -6
- duty/{tools → _internal/tools}/_twine.py +11 -5
- duty/_internal/tools/_yore.py +96 -0
- duty/_internal/validation.py +266 -0
- duty/callables/__init__.py +4 -4
- duty/callables/autoflake.py +11 -126
- duty/callables/black.py +12 -171
- duty/callables/blacken_docs.py +11 -86
- duty/callables/build.py +12 -71
- duty/callables/coverage.py +12 -711
- duty/callables/flake8.py +12 -217
- duty/callables/git_changelog.py +12 -173
- duty/callables/griffe.py +12 -222
- duty/callables/interrogate.py +12 -147
- duty/callables/isort.py +12 -568
- duty/callables/mkdocs.py +12 -251
- duty/callables/mypy.py +11 -490
- duty/callables/pytest.py +12 -470
- duty/callables/ruff.py +12 -394
- duty/callables/safety.py +11 -76
- duty/callables/ssort.py +12 -33
- duty/callables/twine.py +12 -279
- duty/cli.py +10 -316
- duty/collection.py +12 -228
- duty/context.py +12 -107
- duty/decorator.py +12 -108
- duty/exceptions.py +13 -10
- duty/tools.py +63 -0
- duty/validation.py +12 -262
- {duty-1.6.0.dist-info → duty-1.6.2.dist-info}/METADATA +5 -4
- duty-1.6.2.dist-info/RECORD +81 -0
- {duty-1.6.0.dist-info → duty-1.6.2.dist-info}/WHEEL +1 -1
- {duty-1.6.0.dist-info → duty-1.6.2.dist-info}/entry_points.txt +1 -1
- duty/tools/__init__.py +0 -50
- duty/tools/_yore.py +0 -54
- duty-1.6.0.dist-info/RECORD +0 -55
- {duty-1.6.0.dist-info → duty-1.6.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
# YORE: Bump 2: Remove file.
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from failprint import lazy
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def run(*args: str, version: bool = False, no_color: bool = False) -> None:
|
|
9
|
+
"""Run `twine`.
|
|
10
|
+
|
|
11
|
+
Parameters:
|
|
12
|
+
*args: CLI arguments.
|
|
13
|
+
version: Show program's version number and exit.
|
|
14
|
+
no_color: Disable colored output.
|
|
15
|
+
"""
|
|
16
|
+
from twine.cli import dispatch as twine # noqa: PLC0415
|
|
17
|
+
|
|
18
|
+
cli_args = []
|
|
19
|
+
|
|
20
|
+
# --version and --no-color must appear first.
|
|
21
|
+
if version:
|
|
22
|
+
cli_args.append("--version")
|
|
23
|
+
|
|
24
|
+
if no_color:
|
|
25
|
+
cli_args.append("--no-color")
|
|
26
|
+
|
|
27
|
+
cli_args.extend(args)
|
|
28
|
+
|
|
29
|
+
twine(cli_args)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@lazy(name="twine.check")
|
|
33
|
+
def check(
|
|
34
|
+
*dists: str,
|
|
35
|
+
strict: bool = False,
|
|
36
|
+
version: bool = False,
|
|
37
|
+
no_color: bool = False,
|
|
38
|
+
) -> None:
|
|
39
|
+
"""Checks whether your distribution's long description will render correctly on PyPI.
|
|
40
|
+
|
|
41
|
+
Parameters:
|
|
42
|
+
dists: The distribution files to check, usually `dist/*`.
|
|
43
|
+
strict: Fail on warnings.
|
|
44
|
+
version: Show program's version number and exit.
|
|
45
|
+
no_color: Disable colored output.
|
|
46
|
+
"""
|
|
47
|
+
cli_args = list(dists)
|
|
48
|
+
|
|
49
|
+
if strict is True:
|
|
50
|
+
cli_args.append("--strict")
|
|
51
|
+
|
|
52
|
+
run("check", *cli_args, version=version, no_color=no_color)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@lazy(name="twine.register")
|
|
56
|
+
def register(
|
|
57
|
+
package: str,
|
|
58
|
+
*,
|
|
59
|
+
repository: str = "pypi",
|
|
60
|
+
repository_url: str | None = None,
|
|
61
|
+
attestations: bool = False,
|
|
62
|
+
sign: bool = False,
|
|
63
|
+
sign_with: str | None = None,
|
|
64
|
+
identity: str | None = None,
|
|
65
|
+
username: str | None = None,
|
|
66
|
+
password: str | None = None,
|
|
67
|
+
non_interactive: bool = False,
|
|
68
|
+
comment: str | None = None,
|
|
69
|
+
config_file: str | None = None,
|
|
70
|
+
skip_existing: bool = False,
|
|
71
|
+
cert: str | None = None,
|
|
72
|
+
client_cert: str | None = None,
|
|
73
|
+
verbose: bool = False,
|
|
74
|
+
disable_progress_bar: bool = False,
|
|
75
|
+
version: bool = False,
|
|
76
|
+
no_color: bool = False,
|
|
77
|
+
) -> None:
|
|
78
|
+
"""Pre-register a name with a repository before uploading a distribution.
|
|
79
|
+
|
|
80
|
+
Pre-registration is not supported on PyPI, so the register command
|
|
81
|
+
is only necessary if you are using a different repository that requires it.
|
|
82
|
+
|
|
83
|
+
Parameters:
|
|
84
|
+
package: File from which we read the package metadata.
|
|
85
|
+
repository: The repository (package index) to upload the package to.
|
|
86
|
+
Should be a section in the config file (default: `pypi`).
|
|
87
|
+
Can also be set via `TWINE_REPOSITORY` environment variable.
|
|
88
|
+
repository_url: The repository (package index) URL to upload the package to. This overrides `--repository`.
|
|
89
|
+
Can also be set via `TWINE_REPOSITORY_URL` environment variable.
|
|
90
|
+
attestations: Upload each file's associated attestations.
|
|
91
|
+
sign: Sign files to upload using GPG.
|
|
92
|
+
sign_with: GPG program used to sign uploads (default: `gpg`).
|
|
93
|
+
identity: GPG identity used to sign files.
|
|
94
|
+
username: The username to authenticate to the repository (package index) as.
|
|
95
|
+
Can also be set via `TWINE_USERNAME` environment variable.
|
|
96
|
+
password: The password to authenticate to the repository (package index) with.
|
|
97
|
+
Can also be set via `TWINE_PASSWORD` environment variable.
|
|
98
|
+
non_interactive: Do not interactively prompt for username/password if the required credentials are missing.
|
|
99
|
+
Can also be set via `TWINE_NON_INTERACTIVE` environment variable.
|
|
100
|
+
comment: The comment to include with the distribution file.
|
|
101
|
+
config_file: The `.pypirc` config file to use.
|
|
102
|
+
skip_existing: Continue uploading files if one already exists.
|
|
103
|
+
Only valid when uploading to PyPI. Other implementations may not support this.
|
|
104
|
+
cert: Path to alternate CA bundle (can also be set via `TWINE_CERT` environment variable).
|
|
105
|
+
client_cert: Path to SSL client certificate, a single file containing the private key and the certificate in PEM format.
|
|
106
|
+
verbose: Show verbose output.
|
|
107
|
+
disable_progress_bar: Disable the progress bar.
|
|
108
|
+
version: Show program's version number and exit.
|
|
109
|
+
no_color: Disable colored output.
|
|
110
|
+
"""
|
|
111
|
+
cli_args = [package]
|
|
112
|
+
|
|
113
|
+
if repository:
|
|
114
|
+
cli_args.append("--repository")
|
|
115
|
+
cli_args.append(repository)
|
|
116
|
+
|
|
117
|
+
if repository_url:
|
|
118
|
+
cli_args.append("--repository-url")
|
|
119
|
+
cli_args.append(repository_url)
|
|
120
|
+
|
|
121
|
+
if attestations:
|
|
122
|
+
cli_args.append("--attestations")
|
|
123
|
+
|
|
124
|
+
if sign:
|
|
125
|
+
cli_args.append("--sign")
|
|
126
|
+
|
|
127
|
+
if sign_with:
|
|
128
|
+
cli_args.append("--sign-with")
|
|
129
|
+
cli_args.append(sign_with)
|
|
130
|
+
|
|
131
|
+
if identity:
|
|
132
|
+
cli_args.append("--identity")
|
|
133
|
+
cli_args.append(identity)
|
|
134
|
+
|
|
135
|
+
if username:
|
|
136
|
+
cli_args.append("--username")
|
|
137
|
+
cli_args.append(username)
|
|
138
|
+
|
|
139
|
+
if password:
|
|
140
|
+
cli_args.append("--password")
|
|
141
|
+
cli_args.append(password)
|
|
142
|
+
|
|
143
|
+
if non_interactive:
|
|
144
|
+
cli_args.append("--non-interactive")
|
|
145
|
+
|
|
146
|
+
if comment:
|
|
147
|
+
cli_args.append("--repository")
|
|
148
|
+
|
|
149
|
+
if config_file:
|
|
150
|
+
cli_args.append("--config-file")
|
|
151
|
+
cli_args.append(config_file)
|
|
152
|
+
|
|
153
|
+
if skip_existing:
|
|
154
|
+
cli_args.append("--skip-existing")
|
|
155
|
+
|
|
156
|
+
if cert:
|
|
157
|
+
cli_args.append("--cert")
|
|
158
|
+
cli_args.append(cert)
|
|
159
|
+
|
|
160
|
+
if client_cert:
|
|
161
|
+
cli_args.append("--client-cert")
|
|
162
|
+
cli_args.append(client_cert)
|
|
163
|
+
|
|
164
|
+
if verbose:
|
|
165
|
+
cli_args.append("--verbose")
|
|
166
|
+
|
|
167
|
+
if disable_progress_bar:
|
|
168
|
+
cli_args.append("--disable-progress-bar")
|
|
169
|
+
|
|
170
|
+
run("check", *cli_args, version=version, no_color=no_color)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@lazy(name="twine.upload")
|
|
174
|
+
def upload(
|
|
175
|
+
*dists: str,
|
|
176
|
+
repository: str = "pypi",
|
|
177
|
+
repository_url: str | None = None,
|
|
178
|
+
attestations: bool = False,
|
|
179
|
+
sign: bool = False,
|
|
180
|
+
sign_with: str | None = None,
|
|
181
|
+
identity: str | None = None,
|
|
182
|
+
username: str | None = None,
|
|
183
|
+
password: str | None = None,
|
|
184
|
+
non_interactive: bool = False,
|
|
185
|
+
comment: str | None = None,
|
|
186
|
+
config_file: str | None = None,
|
|
187
|
+
skip_existing: bool = False,
|
|
188
|
+
cert: str | None = None,
|
|
189
|
+
client_cert: str | None = None,
|
|
190
|
+
verbose: bool = False,
|
|
191
|
+
disable_progress_bar: bool = False,
|
|
192
|
+
version: bool = False,
|
|
193
|
+
no_color: bool = False,
|
|
194
|
+
) -> None:
|
|
195
|
+
"""Uploads one or more distributions to a repository.
|
|
196
|
+
|
|
197
|
+
Parameters:
|
|
198
|
+
dists: The distribution files to check, usually `dist/*`.
|
|
199
|
+
repository: The repository (package index) to upload the package to.
|
|
200
|
+
Should be a section in the config file (default: `pypi`).
|
|
201
|
+
Can also be set via `TWINE_REPOSITORY` environment variable.
|
|
202
|
+
repository_url: The repository (package index) URL to upload the package to. This overrides `--repository`.
|
|
203
|
+
Can also be set via `TWINE_REPOSITORY_URL` environment variable.
|
|
204
|
+
attestations: Upload each file's associated attestations.
|
|
205
|
+
sign: Sign files to upload using GPG.
|
|
206
|
+
sign_with: GPG program used to sign uploads (default: `gpg`).
|
|
207
|
+
identity: GPG identity used to sign files.
|
|
208
|
+
username: The username to authenticate to the repository (package index) as.
|
|
209
|
+
Can also be set via `TWINE_USERNAME` environment variable.
|
|
210
|
+
password: The password to authenticate to the repository (package index) with.
|
|
211
|
+
Can also be set via `TWINE_PASSWORD` environment variable.
|
|
212
|
+
non_interactive: Do not interactively prompt for username/password if the required credentials are missing.
|
|
213
|
+
Can also be set via `TWINE_NON_INTERACTIVE` environment variable.
|
|
214
|
+
comment: The comment to include with the distribution file.
|
|
215
|
+
config_file: The `.pypirc` config file to use.
|
|
216
|
+
skip_existing: Continue uploading files if one already exists.
|
|
217
|
+
Only valid when uploading to PyPI. Other implementations may not support this.
|
|
218
|
+
cert: Path to alternate CA bundle (can also be set via `TWINE_CERT` environment variable).
|
|
219
|
+
client_cert: Path to SSL client certificate, a single file containing the private key and the certificate in PEM format.
|
|
220
|
+
verbose: Show verbose output.
|
|
221
|
+
disable_progress_bar: Disable the progress bar.
|
|
222
|
+
version: Show program's version number and exit.
|
|
223
|
+
no_color: Disable colored output.
|
|
224
|
+
"""
|
|
225
|
+
cli_args = list(dists)
|
|
226
|
+
|
|
227
|
+
if repository:
|
|
228
|
+
cli_args.append("--repository")
|
|
229
|
+
cli_args.append(repository)
|
|
230
|
+
|
|
231
|
+
if repository_url:
|
|
232
|
+
cli_args.append("--repository-url")
|
|
233
|
+
cli_args.append(repository_url)
|
|
234
|
+
|
|
235
|
+
if attestations:
|
|
236
|
+
cli_args.append("--attestations")
|
|
237
|
+
|
|
238
|
+
if sign:
|
|
239
|
+
cli_args.append("--sign")
|
|
240
|
+
|
|
241
|
+
if sign_with:
|
|
242
|
+
cli_args.append("--sign-with")
|
|
243
|
+
cli_args.append(sign_with)
|
|
244
|
+
|
|
245
|
+
if identity:
|
|
246
|
+
cli_args.append("--identity")
|
|
247
|
+
cli_args.append(identity)
|
|
248
|
+
|
|
249
|
+
if username:
|
|
250
|
+
cli_args.append("--username")
|
|
251
|
+
cli_args.append(username)
|
|
252
|
+
|
|
253
|
+
if password:
|
|
254
|
+
cli_args.append("--password")
|
|
255
|
+
cli_args.append(password)
|
|
256
|
+
|
|
257
|
+
if non_interactive:
|
|
258
|
+
cli_args.append("--non-interactive")
|
|
259
|
+
|
|
260
|
+
if comment:
|
|
261
|
+
cli_args.append("--repository")
|
|
262
|
+
|
|
263
|
+
if config_file:
|
|
264
|
+
cli_args.append("--config-file")
|
|
265
|
+
cli_args.append(config_file)
|
|
266
|
+
|
|
267
|
+
if skip_existing:
|
|
268
|
+
cli_args.append("--skip-existing")
|
|
269
|
+
|
|
270
|
+
if cert:
|
|
271
|
+
cli_args.append("--cert")
|
|
272
|
+
cli_args.append(cert)
|
|
273
|
+
|
|
274
|
+
if client_cert:
|
|
275
|
+
cli_args.append("--client-cert")
|
|
276
|
+
cli_args.append(client_cert)
|
|
277
|
+
|
|
278
|
+
if verbose:
|
|
279
|
+
cli_args.append("--verbose")
|
|
280
|
+
|
|
281
|
+
if disable_progress_bar:
|
|
282
|
+
cli_args.append("--disable-progress-bar")
|
|
283
|
+
|
|
284
|
+
run("upload", *cli_args, version=version, no_color=no_color)
|
duty/_internal/cli.py
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
# Why does this file exist, and why not put this in `__main__`?
|
|
2
|
+
#
|
|
3
|
+
# You might be tempted to import things from `__main__` later,
|
|
4
|
+
# but that will cause problems: the code will get executed twice:
|
|
5
|
+
#
|
|
6
|
+
# - When you run `python -m duty` python will execute
|
|
7
|
+
# `__main__.py` as a script. That means there won't be any
|
|
8
|
+
# `duty.__main__` in `sys.modules`.
|
|
9
|
+
# - When you import `__main__` it will get executed again (as a module) because
|
|
10
|
+
# there's no `duty.__main__` in `sys.modules`.
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import inspect
|
|
16
|
+
import sys
|
|
17
|
+
import textwrap
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from failprint import ArgParser, add_flags
|
|
22
|
+
|
|
23
|
+
from duty._internal import debug
|
|
24
|
+
from duty._internal.collection import Collection, Duty
|
|
25
|
+
from duty._internal.exceptions import DutyFailure
|
|
26
|
+
from duty._internal.validation import validate
|
|
27
|
+
|
|
28
|
+
empty = inspect.Signature.empty
|
|
29
|
+
"""Empty value for a parameter's default value."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class _DebugInfo(argparse.Action):
|
|
33
|
+
def __init__(self, nargs: int | str | None = 0, **kwargs: Any) -> None:
|
|
34
|
+
super().__init__(nargs=nargs, **kwargs)
|
|
35
|
+
|
|
36
|
+
def __call__(self, *args: Any, **kwargs: Any) -> None: # noqa: ARG002
|
|
37
|
+
debug._print_debug_info()
|
|
38
|
+
sys.exit(0)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_parser() -> ArgParser:
|
|
42
|
+
"""Return the CLI argument parser.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
An argparse parser.
|
|
46
|
+
"""
|
|
47
|
+
usage = "duty [GLOBAL_OPTS...] [DUTY [DUTY_OPTS...] [DUTY_PARAMS...]...]"
|
|
48
|
+
description = "A simple task runner."
|
|
49
|
+
parser = ArgParser(add_help=False, usage=usage, description=description)
|
|
50
|
+
|
|
51
|
+
parser.add_argument(
|
|
52
|
+
"-d",
|
|
53
|
+
"--duties-file",
|
|
54
|
+
default="duties.py",
|
|
55
|
+
help="Python file where the duties are defined.",
|
|
56
|
+
)
|
|
57
|
+
parser.add_argument(
|
|
58
|
+
"-l",
|
|
59
|
+
"--list",
|
|
60
|
+
action="store_true",
|
|
61
|
+
dest="list",
|
|
62
|
+
help="List the available duties.",
|
|
63
|
+
)
|
|
64
|
+
parser.add_argument(
|
|
65
|
+
"-h",
|
|
66
|
+
"--help",
|
|
67
|
+
dest="help",
|
|
68
|
+
nargs="*",
|
|
69
|
+
metavar="DUTY",
|
|
70
|
+
help="Show this help message and exit. Pass duties names to print their help.",
|
|
71
|
+
)
|
|
72
|
+
parser.add_argument(
|
|
73
|
+
"--completion",
|
|
74
|
+
dest="completion",
|
|
75
|
+
action="store_true",
|
|
76
|
+
help=argparse.SUPPRESS,
|
|
77
|
+
)
|
|
78
|
+
parser.add_argument(
|
|
79
|
+
"--complete",
|
|
80
|
+
dest="complete",
|
|
81
|
+
action="store_true",
|
|
82
|
+
help=argparse.SUPPRESS,
|
|
83
|
+
)
|
|
84
|
+
parser.add_argument("-V", "--version", action="version", version=f"%(prog)s {debug._get_version()}")
|
|
85
|
+
parser.add_argument("--debug-info", action=_DebugInfo, help="Print debug information.")
|
|
86
|
+
|
|
87
|
+
add_flags(parser, set_defaults=False)
|
|
88
|
+
parser.add_argument("remainder", nargs=argparse.REMAINDER)
|
|
89
|
+
|
|
90
|
+
parser._optionals.title = "Global options"
|
|
91
|
+
|
|
92
|
+
return parser
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def split_args(args: list[str], names: list[str]) -> list[list[str]]:
|
|
96
|
+
"""Split command line arguments into duty commands.
|
|
97
|
+
|
|
98
|
+
Parameters:
|
|
99
|
+
args: The CLI arguments.
|
|
100
|
+
names: The known duty names.
|
|
101
|
+
|
|
102
|
+
Raises:
|
|
103
|
+
ValueError: When a duty name is missing before an argument,
|
|
104
|
+
or when the duty name is unknown.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
The split commands.
|
|
108
|
+
"""
|
|
109
|
+
arg_lists = []
|
|
110
|
+
current_arg_list: list[str] = []
|
|
111
|
+
|
|
112
|
+
for arg in args:
|
|
113
|
+
if arg in names:
|
|
114
|
+
# We found a duty name.
|
|
115
|
+
if current_arg_list:
|
|
116
|
+
# Append the previous arg list to the result and reset it.
|
|
117
|
+
arg_lists.append(current_arg_list)
|
|
118
|
+
current_arg_list = []
|
|
119
|
+
current_arg_list.append(arg)
|
|
120
|
+
elif current_arg_list:
|
|
121
|
+
# We found an argument.
|
|
122
|
+
current_arg_list.append(arg)
|
|
123
|
+
else:
|
|
124
|
+
# We found an argument but no duty name.
|
|
125
|
+
raise ValueError(f"> Missing duty name before argument '{arg}', or unknown duty name")
|
|
126
|
+
|
|
127
|
+
# Don't forget the last arg list.
|
|
128
|
+
if current_arg_list:
|
|
129
|
+
arg_lists.append(current_arg_list)
|
|
130
|
+
|
|
131
|
+
return arg_lists
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def get_duty_parser(duty: Duty) -> ArgParser:
|
|
135
|
+
"""Get a duty-specific options parser.
|
|
136
|
+
|
|
137
|
+
Parameters:
|
|
138
|
+
duty: The duty to parse for.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
A duty-specific parser.
|
|
142
|
+
"""
|
|
143
|
+
parser = ArgParser(
|
|
144
|
+
prog=f"duty {duty.name}",
|
|
145
|
+
add_help=False,
|
|
146
|
+
description=duty.description,
|
|
147
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
148
|
+
)
|
|
149
|
+
add_flags(parser, set_defaults=False)
|
|
150
|
+
return parser
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def specified_options(opts: argparse.Namespace, exclude: set[str] | None = None) -> dict:
|
|
154
|
+
"""Cast an argparse Namespace into a dictionary of options.
|
|
155
|
+
|
|
156
|
+
Remove all options that were not specified (equal to None).
|
|
157
|
+
|
|
158
|
+
Parameters:
|
|
159
|
+
opts: The namespace to cast.
|
|
160
|
+
exclude: Names of options to exclude from the result.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
A dictionary of specified-only options.
|
|
164
|
+
"""
|
|
165
|
+
exclude = exclude or set()
|
|
166
|
+
options = opts.__dict__.items()
|
|
167
|
+
return {opt: value for opt, value in options if value is not None and opt not in exclude}
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def parse_options(duty: Duty, args: list[str]) -> tuple[dict, list[str]]:
|
|
171
|
+
"""Parse options for a duty.
|
|
172
|
+
|
|
173
|
+
Parameters:
|
|
174
|
+
duty: The duty to parse for.
|
|
175
|
+
args: The CLI args passed for this duty.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
The parsed opts, and the remaining arguments.
|
|
179
|
+
"""
|
|
180
|
+
parser = get_duty_parser(duty)
|
|
181
|
+
opts, remainder = parser.parse_known_args(args)
|
|
182
|
+
return specified_options(opts), remainder
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def parse_args(duty: Duty, args: list[str]) -> tuple:
|
|
186
|
+
"""Parse the positional and keyword arguments of a duty.
|
|
187
|
+
|
|
188
|
+
Parameters:
|
|
189
|
+
duty: The duty to parse for.
|
|
190
|
+
args: The list of arguments.
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
The positional and keyword arguments.
|
|
194
|
+
"""
|
|
195
|
+
posargs = []
|
|
196
|
+
kwargs = {}
|
|
197
|
+
|
|
198
|
+
for arg in args:
|
|
199
|
+
if "=" in arg:
|
|
200
|
+
# we found a keyword argument
|
|
201
|
+
arg_name, arg_value = arg.split("=", 1)
|
|
202
|
+
kwargs[arg_name] = arg_value
|
|
203
|
+
else:
|
|
204
|
+
# we found a positional argument
|
|
205
|
+
posargs.append(arg)
|
|
206
|
+
|
|
207
|
+
return validate(duty.function, *posargs, **kwargs)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def parse_commands(arg_lists: list[list[str]], global_opts: dict[str, Any], collection: Collection) -> list[tuple]:
|
|
211
|
+
"""Parse argument lists into ready-to-run duties.
|
|
212
|
+
|
|
213
|
+
Parameters:
|
|
214
|
+
arg_lists: Lists of arguments lists.
|
|
215
|
+
global_opts: The global options.
|
|
216
|
+
collection: The duties collection.
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
A list of tuples composed of:
|
|
220
|
+
|
|
221
|
+
- a duty
|
|
222
|
+
- its positional arguments
|
|
223
|
+
- its keyword arguments
|
|
224
|
+
"""
|
|
225
|
+
commands = []
|
|
226
|
+
for arg_list in arg_lists:
|
|
227
|
+
duty = collection.get(arg_list[0])
|
|
228
|
+
opts, remainder = parse_options(duty, arg_list[1:])
|
|
229
|
+
if remainder and remainder[0] == "--":
|
|
230
|
+
remainder = remainder[1:]
|
|
231
|
+
duty.options_override = {**global_opts, **opts}
|
|
232
|
+
commands.append((duty, *parse_args(duty, remainder)))
|
|
233
|
+
return commands
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def print_help(parser: ArgParser, opts: argparse.Namespace, collection: Collection) -> None:
|
|
237
|
+
"""Print general help or duties help.
|
|
238
|
+
|
|
239
|
+
Parameters:
|
|
240
|
+
parser: The main parser.
|
|
241
|
+
opts: The main parsed options.
|
|
242
|
+
collection: A collection of duties.
|
|
243
|
+
"""
|
|
244
|
+
if opts.help:
|
|
245
|
+
for duty_name in opts.help:
|
|
246
|
+
try:
|
|
247
|
+
duty = collection.get(duty_name)
|
|
248
|
+
except KeyError:
|
|
249
|
+
print(f"> Unknown duty '{duty_name}'")
|
|
250
|
+
else:
|
|
251
|
+
print(get_duty_parser(duty).format_help())
|
|
252
|
+
else:
|
|
253
|
+
print(parser.format_help())
|
|
254
|
+
print("Available duties:")
|
|
255
|
+
print(textwrap.indent(collection.format_help(), prefix=" "))
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def main(args: list[str] | None = None) -> int:
|
|
259
|
+
"""Run the main program.
|
|
260
|
+
|
|
261
|
+
This function is executed when you type `duty` or `python -m duty`.
|
|
262
|
+
|
|
263
|
+
Parameters:
|
|
264
|
+
args: Arguments passed from the command line.
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
An exit code.
|
|
268
|
+
"""
|
|
269
|
+
parser = get_parser()
|
|
270
|
+
opts = parser.parse_args(args=args)
|
|
271
|
+
remainder = opts.remainder
|
|
272
|
+
|
|
273
|
+
collection = Collection(opts.duties_file)
|
|
274
|
+
collection.load()
|
|
275
|
+
|
|
276
|
+
if opts.completion:
|
|
277
|
+
print(Path(__file__).parent.joinpath("completions.bash").read_text())
|
|
278
|
+
return 0
|
|
279
|
+
|
|
280
|
+
if opts.complete:
|
|
281
|
+
words = collection.completion_candidates(remainder)
|
|
282
|
+
words += sorted(
|
|
283
|
+
opt for opt, action in parser._option_string_actions.items() if action.help != argparse.SUPPRESS
|
|
284
|
+
)
|
|
285
|
+
print(*words, sep="\n")
|
|
286
|
+
return 0
|
|
287
|
+
|
|
288
|
+
if opts.help is not None:
|
|
289
|
+
print_help(parser, opts, collection)
|
|
290
|
+
return 0
|
|
291
|
+
|
|
292
|
+
if opts.list:
|
|
293
|
+
print(textwrap.indent(collection.format_help(), prefix=" "))
|
|
294
|
+
return 0
|
|
295
|
+
|
|
296
|
+
try:
|
|
297
|
+
arg_lists = split_args(remainder, collection.names())
|
|
298
|
+
except ValueError as error:
|
|
299
|
+
print(error, file=sys.stderr)
|
|
300
|
+
return 1
|
|
301
|
+
|
|
302
|
+
if not arg_lists:
|
|
303
|
+
print_help(parser, opts, collection)
|
|
304
|
+
return 1
|
|
305
|
+
|
|
306
|
+
global_opts = specified_options(
|
|
307
|
+
opts,
|
|
308
|
+
exclude={"duties_file", "list", "help", "remainder", "complete", "completion"},
|
|
309
|
+
)
|
|
310
|
+
try:
|
|
311
|
+
commands = parse_commands(arg_lists, global_opts, collection)
|
|
312
|
+
except TypeError as error:
|
|
313
|
+
print(f"> {error}", file=sys.stderr)
|
|
314
|
+
return 1
|
|
315
|
+
|
|
316
|
+
for duty, posargs, kwargs in commands:
|
|
317
|
+
try:
|
|
318
|
+
duty.run(*posargs, **kwargs)
|
|
319
|
+
except DutyFailure as failure:
|
|
320
|
+
return failure.code
|
|
321
|
+
|
|
322
|
+
return 0
|