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,246 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
import sys
|
|
5
|
+
from copy import deepcopy
|
|
6
|
+
from importlib import util as importlib_util
|
|
7
|
+
from typing import Any, Callable, ClassVar, Union
|
|
8
|
+
|
|
9
|
+
from duty._internal.context import Context
|
|
10
|
+
|
|
11
|
+
DutyListType = list[Union[str, Callable, "Duty"]]
|
|
12
|
+
"""Type of a list of duties, which can be a list of strings, callables, or Duty instances."""
|
|
13
|
+
default_duties_file = "duties.py"
|
|
14
|
+
"""Default path to the duties file, relative to the current working directory."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Duty:
|
|
18
|
+
"""The main duty class."""
|
|
19
|
+
|
|
20
|
+
default_options: ClassVar[dict[str, Any]] = {}
|
|
21
|
+
"""Default options used to create the context instance."""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
name: str,
|
|
26
|
+
description: str,
|
|
27
|
+
function: Callable,
|
|
28
|
+
collection: Collection | None = None,
|
|
29
|
+
aliases: set | None = None,
|
|
30
|
+
pre: DutyListType | None = None,
|
|
31
|
+
post: DutyListType | None = None,
|
|
32
|
+
opts: dict[str, Any] | None = None,
|
|
33
|
+
) -> None:
|
|
34
|
+
"""Initialize the duty.
|
|
35
|
+
|
|
36
|
+
Parameters:
|
|
37
|
+
name: The duty name.
|
|
38
|
+
description: The duty description.
|
|
39
|
+
function: The duty function.
|
|
40
|
+
collection: The collection on which to attach this duty.
|
|
41
|
+
aliases: A list of aliases for this duty.
|
|
42
|
+
pre: A list of duties to run before this one.
|
|
43
|
+
post: A list of duties to run after this one.
|
|
44
|
+
opts: Options used to create the context instance.
|
|
45
|
+
"""
|
|
46
|
+
self.name = name
|
|
47
|
+
"""The duty name."""
|
|
48
|
+
self.description = description
|
|
49
|
+
"""The duty description."""
|
|
50
|
+
self.function = function
|
|
51
|
+
"""The duty function."""
|
|
52
|
+
self.aliases = aliases or set()
|
|
53
|
+
"""A set of aliases for this duty."""
|
|
54
|
+
self.pre = pre or []
|
|
55
|
+
"""A list of duties to run before this one."""
|
|
56
|
+
self.post = post or []
|
|
57
|
+
"""A list of duties to run after this one."""
|
|
58
|
+
self.options = opts or self.default_options
|
|
59
|
+
"""Options used to create the context instance."""
|
|
60
|
+
self.options_override: dict = {}
|
|
61
|
+
"""Options that override `run` and `@duty` options."""
|
|
62
|
+
|
|
63
|
+
self.collection: Collection | None = None
|
|
64
|
+
"""The collection on which this duty is attached."""
|
|
65
|
+
if collection:
|
|
66
|
+
collection.add(self)
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def context(self) -> Context:
|
|
70
|
+
"""Return a new context instance.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
A new context instance.
|
|
74
|
+
"""
|
|
75
|
+
return Context(self.options, self.options_override)
|
|
76
|
+
|
|
77
|
+
def run(self, *args: Any, **kwargs: Any) -> None:
|
|
78
|
+
"""Run the duty.
|
|
79
|
+
|
|
80
|
+
This is just a shortcut for `duty(duty.context, *args, **kwargs)`.
|
|
81
|
+
|
|
82
|
+
Parameters:
|
|
83
|
+
args: Positional arguments passed to the function.
|
|
84
|
+
kwargs: Keyword arguments passed to the function.
|
|
85
|
+
"""
|
|
86
|
+
self(self.context, *args, **kwargs)
|
|
87
|
+
|
|
88
|
+
def run_duties(self, context: Context, duties_list: DutyListType) -> None:
|
|
89
|
+
"""Run a list of duties.
|
|
90
|
+
|
|
91
|
+
Parameters:
|
|
92
|
+
context: The context to use.
|
|
93
|
+
duties_list: The list of duties to run.
|
|
94
|
+
|
|
95
|
+
Raises:
|
|
96
|
+
RuntimeError: When a duty name is given to pre or post duties.
|
|
97
|
+
Indeed, without a parent collection, it is impossible
|
|
98
|
+
to find another duty by its name.
|
|
99
|
+
"""
|
|
100
|
+
for duty_item in duties_list:
|
|
101
|
+
if callable(duty_item):
|
|
102
|
+
# Item is a proper duty, or a callable: run it.
|
|
103
|
+
duty_item(context)
|
|
104
|
+
elif isinstance(duty_item, str):
|
|
105
|
+
# Item is a reference to a duty.
|
|
106
|
+
if self.collection is None:
|
|
107
|
+
raise RuntimeError(f"Can't find duty by name without a collection ({duty_item})")
|
|
108
|
+
# Get the duty and run it.
|
|
109
|
+
self.collection.get(duty_item)(context)
|
|
110
|
+
|
|
111
|
+
def __call__(self, context: Context, *args: Any, **kwargs: Any) -> None:
|
|
112
|
+
"""Run the duty function.
|
|
113
|
+
|
|
114
|
+
Parameters:
|
|
115
|
+
context: The context to use.
|
|
116
|
+
args: Positional arguments passed to the function.
|
|
117
|
+
kwargs: Keyword arguments passed to the function.
|
|
118
|
+
"""
|
|
119
|
+
self.run_duties(context, self.pre)
|
|
120
|
+
self.function(context, *args, **kwargs)
|
|
121
|
+
self.run_duties(context, self.post)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class Collection:
|
|
125
|
+
"""A collection of duties.
|
|
126
|
+
|
|
127
|
+
Attributes:
|
|
128
|
+
path: The path to the duties file.
|
|
129
|
+
duties: The list of duties.
|
|
130
|
+
aliases: A dictionary of aliases pointing to their respective duties.
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
def __init__(self, path: str = default_duties_file) -> None:
|
|
134
|
+
"""Initialize the collection.
|
|
135
|
+
|
|
136
|
+
Parameters:
|
|
137
|
+
path: The path to the duties file.
|
|
138
|
+
"""
|
|
139
|
+
self.path = path
|
|
140
|
+
"""The path to the duties file."""
|
|
141
|
+
self.duties: dict[str, Duty] = {}
|
|
142
|
+
"""The list of duties."""
|
|
143
|
+
self.aliases: dict[str, Duty] = {}
|
|
144
|
+
"""A dictionary of aliases pointing to their respective duties."""
|
|
145
|
+
|
|
146
|
+
def clear(self) -> None:
|
|
147
|
+
"""Clear the collection."""
|
|
148
|
+
self.duties.clear()
|
|
149
|
+
self.aliases.clear()
|
|
150
|
+
|
|
151
|
+
def names(self) -> list[str]:
|
|
152
|
+
"""Return the list of duties names and aliases.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
The list of duties names and aliases.
|
|
156
|
+
"""
|
|
157
|
+
return list(self.duties.keys()) + list(self.aliases.keys())
|
|
158
|
+
|
|
159
|
+
def completion_candidates(self, args: tuple[str, ...]) -> list[str]:
|
|
160
|
+
"""Find shell completion candidates within this collection.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
The list of shell completion candidates, sorted alphabetically.
|
|
164
|
+
"""
|
|
165
|
+
# Find last duty name in args.
|
|
166
|
+
name = None
|
|
167
|
+
names = set(self.names())
|
|
168
|
+
for arg in reversed(args):
|
|
169
|
+
if arg in names:
|
|
170
|
+
name = arg
|
|
171
|
+
break
|
|
172
|
+
|
|
173
|
+
completion_names = sorted(names)
|
|
174
|
+
|
|
175
|
+
# If no duty found, return names.
|
|
176
|
+
if name is None:
|
|
177
|
+
return completion_names
|
|
178
|
+
|
|
179
|
+
params = [
|
|
180
|
+
f"{param.name}="
|
|
181
|
+
for param in inspect.signature(self.get(name).function).parameters.values()
|
|
182
|
+
if param.kind is not param.VAR_POSITIONAL
|
|
183
|
+
][1:]
|
|
184
|
+
|
|
185
|
+
# If duty found, return names *and* duty parameters.
|
|
186
|
+
return completion_names + sorted(params)
|
|
187
|
+
|
|
188
|
+
def get(self, name_or_alias: str) -> Duty:
|
|
189
|
+
"""Get a duty by its name or alias.
|
|
190
|
+
|
|
191
|
+
Parameters:
|
|
192
|
+
name_or_alias: The name or alias of the duty.
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
A duty.
|
|
196
|
+
"""
|
|
197
|
+
try:
|
|
198
|
+
return self.duties[name_or_alias]
|
|
199
|
+
except KeyError:
|
|
200
|
+
return self.aliases[name_or_alias]
|
|
201
|
+
|
|
202
|
+
def format_help(self) -> str:
|
|
203
|
+
"""Format a message listing the duties.
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
A string listing the duties and their summary.
|
|
207
|
+
"""
|
|
208
|
+
lines = []
|
|
209
|
+
# 20 makes the summary aligned with options description
|
|
210
|
+
longest_name = max(*(len(name) for name in self.duties), 20)
|
|
211
|
+
for name, duty in self.duties.items():
|
|
212
|
+
description = duty.description.split("\n")[0]
|
|
213
|
+
lines.append(f"{name:{longest_name}} {description}")
|
|
214
|
+
return "\n".join(lines)
|
|
215
|
+
|
|
216
|
+
def add(self, duty: Duty) -> None:
|
|
217
|
+
"""Add a duty to the collection.
|
|
218
|
+
|
|
219
|
+
Parameters:
|
|
220
|
+
duty: The duty to add.
|
|
221
|
+
"""
|
|
222
|
+
if duty.collection is not None:
|
|
223
|
+
# we must copy the duty to be able to add it
|
|
224
|
+
# in multiple collections
|
|
225
|
+
duty = deepcopy(duty)
|
|
226
|
+
duty.collection = self
|
|
227
|
+
self.duties[duty.name] = duty
|
|
228
|
+
for alias in duty.aliases:
|
|
229
|
+
self.aliases[alias] = duty
|
|
230
|
+
|
|
231
|
+
def load(self, path: str | None = None) -> None:
|
|
232
|
+
"""Load duties from a Python file.
|
|
233
|
+
|
|
234
|
+
Parameters:
|
|
235
|
+
path: The path to the Python file to load.
|
|
236
|
+
Uses the collection's path by default.
|
|
237
|
+
"""
|
|
238
|
+
path = path or self.path
|
|
239
|
+
spec = importlib_util.spec_from_file_location("duty.duties", path)
|
|
240
|
+
if spec:
|
|
241
|
+
duties = importlib_util.module_from_spec(spec)
|
|
242
|
+
sys.modules["duty.duties"] = duties
|
|
243
|
+
spec.loader.exec_module(duties) # type: ignore[union-attr]
|
|
244
|
+
declared_duties = inspect.getmembers(duties, lambda member: isinstance(member, Duty))
|
|
245
|
+
for _, duty in declared_duties:
|
|
246
|
+
self.add(duty)
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from contextlib import contextmanager, suppress
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Callable, Union
|
|
6
|
+
|
|
7
|
+
from failprint import run as failprint_run
|
|
8
|
+
|
|
9
|
+
from duty._internal.exceptions import DutyFailure
|
|
10
|
+
from duty._internal.tools._base import Tool
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from collections.abc import Iterator
|
|
14
|
+
|
|
15
|
+
CmdType = Union[str, list[str], Callable]
|
|
16
|
+
"""Type of a command that can be run in a subprocess or as a Python callable."""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Context:
|
|
20
|
+
"""A simple context class.
|
|
21
|
+
|
|
22
|
+
Context instances are passed to functions decorated with `duty`.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, options: dict[str, Any], options_override: dict[str, Any] | None = None) -> None:
|
|
26
|
+
"""Initialize the context.
|
|
27
|
+
|
|
28
|
+
Parameters:
|
|
29
|
+
options: Base options specified in `@duty(**options)`.
|
|
30
|
+
options_override: Options that override `run` and `@duty` options.
|
|
31
|
+
This argument is used to allow users to override options from the CLI or environment.
|
|
32
|
+
"""
|
|
33
|
+
self._options = options
|
|
34
|
+
self._option_stack: list[dict[str, Any]] = []
|
|
35
|
+
self._options_override = options_override or {}
|
|
36
|
+
|
|
37
|
+
@contextmanager
|
|
38
|
+
def cd(self, directory: str) -> Iterator:
|
|
39
|
+
"""Change working directory as a context manager.
|
|
40
|
+
|
|
41
|
+
Parameters:
|
|
42
|
+
directory: The directory to go into.
|
|
43
|
+
|
|
44
|
+
Yields:
|
|
45
|
+
Nothing.
|
|
46
|
+
"""
|
|
47
|
+
if not directory:
|
|
48
|
+
yield
|
|
49
|
+
return
|
|
50
|
+
old_wd = os.getcwd()
|
|
51
|
+
os.chdir(directory)
|
|
52
|
+
try:
|
|
53
|
+
yield
|
|
54
|
+
finally:
|
|
55
|
+
os.chdir(old_wd)
|
|
56
|
+
|
|
57
|
+
def run(self, cmd: CmdType, **options: Any) -> str:
|
|
58
|
+
"""Run a command in a subprocess or a Python callable.
|
|
59
|
+
|
|
60
|
+
Parameters:
|
|
61
|
+
cmd: A command or a Python callable.
|
|
62
|
+
options: Options passed to `failprint` functions.
|
|
63
|
+
|
|
64
|
+
Raises:
|
|
65
|
+
DutyFailure: When the exit code / function result is greather than 0.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
The output of the command.
|
|
69
|
+
"""
|
|
70
|
+
final_options = dict(self._options)
|
|
71
|
+
final_options.update(options)
|
|
72
|
+
|
|
73
|
+
if "command" not in final_options and isinstance(cmd, Tool):
|
|
74
|
+
with suppress(ValueError):
|
|
75
|
+
final_options["command"] = cmd.cli_command
|
|
76
|
+
|
|
77
|
+
allow_overrides = final_options.pop("allow_overrides", True)
|
|
78
|
+
workdir = final_options.pop("workdir", None)
|
|
79
|
+
|
|
80
|
+
if allow_overrides:
|
|
81
|
+
final_options.update(self._options_override)
|
|
82
|
+
|
|
83
|
+
with self.cd(workdir):
|
|
84
|
+
try:
|
|
85
|
+
result = failprint_run(cmd, **final_options)
|
|
86
|
+
except KeyboardInterrupt as ki:
|
|
87
|
+
raise DutyFailure(130) from ki
|
|
88
|
+
|
|
89
|
+
if result.code:
|
|
90
|
+
raise DutyFailure(result.code)
|
|
91
|
+
|
|
92
|
+
return result.output
|
|
93
|
+
|
|
94
|
+
@contextmanager
|
|
95
|
+
def options(self, **opts: Any) -> Iterator:
|
|
96
|
+
"""Change options as a context manager.
|
|
97
|
+
|
|
98
|
+
Can be nested as will, previous options will pop once out of the with clause.
|
|
99
|
+
|
|
100
|
+
Parameters:
|
|
101
|
+
**opts: Options used in `run`.
|
|
102
|
+
|
|
103
|
+
Yields:
|
|
104
|
+
Nothing.
|
|
105
|
+
"""
|
|
106
|
+
self._option_stack.append(self._options)
|
|
107
|
+
self._options = {**self._options, **opts}
|
|
108
|
+
try:
|
|
109
|
+
yield
|
|
110
|
+
finally:
|
|
111
|
+
self._options = self._option_stack.pop()
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
"""Debugging utilities."""
|
|
2
|
-
|
|
3
1
|
from __future__ import annotations
|
|
4
2
|
|
|
5
3
|
import os
|
|
@@ -10,7 +8,7 @@ from importlib import metadata
|
|
|
10
8
|
|
|
11
9
|
|
|
12
10
|
@dataclass
|
|
13
|
-
class
|
|
11
|
+
class _Variable:
|
|
14
12
|
"""Dataclass describing an environment variable."""
|
|
15
13
|
|
|
16
14
|
name: str
|
|
@@ -20,7 +18,7 @@ class Variable:
|
|
|
20
18
|
|
|
21
19
|
|
|
22
20
|
@dataclass
|
|
23
|
-
class
|
|
21
|
+
class _Package:
|
|
24
22
|
"""Dataclass describing a Python package."""
|
|
25
23
|
|
|
26
24
|
name: str
|
|
@@ -30,7 +28,7 @@ class Package:
|
|
|
30
28
|
|
|
31
29
|
|
|
32
30
|
@dataclass
|
|
33
|
-
class
|
|
31
|
+
class _Environment:
|
|
34
32
|
"""Dataclass to store environment information."""
|
|
35
33
|
|
|
36
34
|
interpreter_name: str
|
|
@@ -41,9 +39,9 @@ class Environment:
|
|
|
41
39
|
"""Path to Python executable."""
|
|
42
40
|
platform: str
|
|
43
41
|
"""Operating System."""
|
|
44
|
-
packages: list[
|
|
42
|
+
packages: list[_Package]
|
|
45
43
|
"""Installed packages."""
|
|
46
|
-
variables: list[
|
|
44
|
+
variables: list[_Variable]
|
|
47
45
|
"""Environment variables."""
|
|
48
46
|
|
|
49
47
|
|
|
@@ -58,7 +56,7 @@ def _interpreter_name_version() -> tuple[str, str]:
|
|
|
58
56
|
return "", "0.0.0"
|
|
59
57
|
|
|
60
58
|
|
|
61
|
-
def
|
|
59
|
+
def _get_version(dist: str = "duty") -> str:
|
|
62
60
|
"""Get version of the given distribution.
|
|
63
61
|
|
|
64
62
|
Parameters:
|
|
@@ -73,7 +71,7 @@ def get_version(dist: str = "duty") -> str:
|
|
|
73
71
|
return "0.0.0"
|
|
74
72
|
|
|
75
73
|
|
|
76
|
-
def
|
|
74
|
+
def _get_debug_info() -> _Environment:
|
|
77
75
|
"""Get debug/environment information.
|
|
78
76
|
|
|
79
77
|
Returns:
|
|
@@ -82,19 +80,19 @@ def get_debug_info() -> Environment:
|
|
|
82
80
|
py_name, py_version = _interpreter_name_version()
|
|
83
81
|
packages = ["duty"]
|
|
84
82
|
variables = ["PYTHONPATH", *[var for var in os.environ if var.startswith("DUTY")]]
|
|
85
|
-
return
|
|
83
|
+
return _Environment(
|
|
86
84
|
interpreter_name=py_name,
|
|
87
85
|
interpreter_version=py_version,
|
|
88
86
|
interpreter_path=sys.executable,
|
|
89
87
|
platform=platform.platform(),
|
|
90
|
-
variables=[
|
|
91
|
-
packages=[
|
|
88
|
+
variables=[_Variable(var, val) for var in variables if (val := os.getenv(var))], # ty: ignore[invalid-argument-type]
|
|
89
|
+
packages=[_Package(pkg, _get_version(pkg)) for pkg in packages],
|
|
92
90
|
)
|
|
93
91
|
|
|
94
92
|
|
|
95
|
-
def
|
|
93
|
+
def _print_debug_info() -> None:
|
|
96
94
|
"""Print debug/environment information."""
|
|
97
|
-
info =
|
|
95
|
+
info = _get_debug_info()
|
|
98
96
|
print(f"- __System__: {info.platform}")
|
|
99
97
|
print(f"- __Python__: {info.interpreter_name} {info.interpreter_version} ({info.interpreter_path})")
|
|
100
98
|
print("- __Environment variables__:")
|
|
@@ -106,4 +104,4 @@ def print_debug_info() -> None:
|
|
|
106
104
|
|
|
107
105
|
|
|
108
106
|
if __name__ == "__main__":
|
|
109
|
-
|
|
107
|
+
_print_debug_info()
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
from functools import wraps
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Callable, overload
|
|
6
|
+
|
|
7
|
+
from duty._internal.collection import Duty, DutyListType
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from collections.abc import Iterable
|
|
11
|
+
|
|
12
|
+
from duty._internal.context import Context
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _skip(func: Callable, reason: str) -> Callable:
|
|
16
|
+
@wraps(func)
|
|
17
|
+
def wrapper(ctx: Context, *args, **kwargs) -> None: # noqa: ARG001,ANN002,ANN003
|
|
18
|
+
ctx.run(lambda: True, title=reason)
|
|
19
|
+
|
|
20
|
+
return wrapper
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def create_duty(
|
|
24
|
+
func: Callable,
|
|
25
|
+
*,
|
|
26
|
+
name: str | None = None,
|
|
27
|
+
aliases: Iterable[str] | None = None,
|
|
28
|
+
pre: DutyListType | None = None,
|
|
29
|
+
post: DutyListType | None = None,
|
|
30
|
+
skip_if: bool = False,
|
|
31
|
+
skip_reason: str | None = None,
|
|
32
|
+
**opts: Any,
|
|
33
|
+
) -> Duty:
|
|
34
|
+
"""Register a duty in the collection.
|
|
35
|
+
|
|
36
|
+
Parameters:
|
|
37
|
+
func: The callable to register as a duty.
|
|
38
|
+
name: The duty name.
|
|
39
|
+
aliases: A set of aliases for this duty.
|
|
40
|
+
pre: Pre-duties.
|
|
41
|
+
post: Post-duties.
|
|
42
|
+
skip_if: Skip running the duty if the given condition is met.
|
|
43
|
+
skip_reason: Custom message when skipping.
|
|
44
|
+
opts: Options passed to the context.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
The registered duty.
|
|
48
|
+
"""
|
|
49
|
+
aliases = set(aliases) if aliases else set()
|
|
50
|
+
name = name or func.__name__
|
|
51
|
+
dash_name = name.replace("_", "-")
|
|
52
|
+
if name != dash_name:
|
|
53
|
+
aliases.add(name)
|
|
54
|
+
name = dash_name
|
|
55
|
+
description = inspect.getdoc(func) or ""
|
|
56
|
+
if skip_if:
|
|
57
|
+
func = _skip(func, skip_reason or f"{dash_name}: skipped")
|
|
58
|
+
duty = Duty(name, description, func, aliases=aliases, pre=pre, post=post, opts=opts)
|
|
59
|
+
duty.__name__ = name # type: ignore[attr-defined]
|
|
60
|
+
duty.__doc__ = description
|
|
61
|
+
duty.__wrapped__ = func # type: ignore[attr-defined]
|
|
62
|
+
return duty
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@overload
|
|
66
|
+
def duty(**kwargs: Any) -> Callable[[Callable], Duty]: ...
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@overload
|
|
70
|
+
def duty(func: Callable) -> Duty: ...
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def duty(*args: Any, **kwargs: Any) -> Callable | Duty:
|
|
74
|
+
"""Decorate a callable to transform it and register it as a duty.
|
|
75
|
+
|
|
76
|
+
Parameters:
|
|
77
|
+
args: One callable.
|
|
78
|
+
kwargs: Context options.
|
|
79
|
+
|
|
80
|
+
Raises:
|
|
81
|
+
ValueError: When the decorator is misused.
|
|
82
|
+
|
|
83
|
+
Examples:
|
|
84
|
+
Decorate a function:
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
@duty
|
|
88
|
+
def clean(ctx):
|
|
89
|
+
ctx.run("rm -rf build", silent=True)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Pass options to the context:
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
@duty(silent=True)
|
|
96
|
+
def clean(ctx):
|
|
97
|
+
ctx.run("rm -rf build") # silent=True is implied
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
A duty when used without parentheses, a decorator otherwise.
|
|
102
|
+
"""
|
|
103
|
+
if args:
|
|
104
|
+
if len(args) > 1:
|
|
105
|
+
raise ValueError("The duty decorator accepts only one positional argument")
|
|
106
|
+
return create_duty(args[0], **kwargs)
|
|
107
|
+
|
|
108
|
+
def decorator(func: Callable) -> Duty:
|
|
109
|
+
return create_duty(func, **kwargs)
|
|
110
|
+
|
|
111
|
+
return decorator
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
class DutyFailure(Exception): # noqa: N818
|
|
2
|
+
"""An exception raised when a duty fails."""
|
|
3
|
+
|
|
4
|
+
def __init__(self, code: int) -> None:
|
|
5
|
+
"""Initialize the object.
|
|
6
|
+
|
|
7
|
+
Parameters:
|
|
8
|
+
code: The exit code of a command.
|
|
9
|
+
"""
|
|
10
|
+
super().__init__(self)
|
|
11
|
+
self.code = code
|
|
12
|
+
"""The exit code of the command that failed."""
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from duty._internal.tools._autoflake import autoflake
|
|
4
|
+
from duty._internal.tools._black import black
|
|
5
|
+
from duty._internal.tools._blacken_docs import blacken_docs
|
|
6
|
+
from duty._internal.tools._build import build
|
|
7
|
+
from duty._internal.tools._coverage import coverage
|
|
8
|
+
from duty._internal.tools._flake8 import flake8
|
|
9
|
+
from duty._internal.tools._git_changelog import git_changelog
|
|
10
|
+
from duty._internal.tools._griffe import griffe
|
|
11
|
+
from duty._internal.tools._interrogate import interrogate
|
|
12
|
+
from duty._internal.tools._isort import isort
|
|
13
|
+
from duty._internal.tools._mkdocs import mkdocs
|
|
14
|
+
from duty._internal.tools._mypy import mypy
|
|
15
|
+
from duty._internal.tools._pytest import pytest
|
|
16
|
+
from duty._internal.tools._ruff import ruff
|
|
17
|
+
from duty._internal.tools._safety import safety
|
|
18
|
+
from duty._internal.tools._ssort import ssort
|
|
19
|
+
from duty._internal.tools._twine import twine
|
|
20
|
+
from duty._internal.tools._yore import yore
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"autoflake",
|
|
24
|
+
"black",
|
|
25
|
+
"blacken_docs",
|
|
26
|
+
"build",
|
|
27
|
+
"coverage",
|
|
28
|
+
"flake8",
|
|
29
|
+
"git_changelog",
|
|
30
|
+
"griffe",
|
|
31
|
+
"interrogate",
|
|
32
|
+
"isort",
|
|
33
|
+
"mkdocs",
|
|
34
|
+
"mypy",
|
|
35
|
+
"pytest",
|
|
36
|
+
"ruff",
|
|
37
|
+
"safety",
|
|
38
|
+
"ssort",
|
|
39
|
+
"twine",
|
|
40
|
+
"yore",
|
|
41
|
+
]
|
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
"""Callable for [autoflake](https://github.com/PyCQA/autoflake)."""
|
|
2
|
-
|
|
3
1
|
from __future__ import annotations
|
|
4
2
|
|
|
5
|
-
from duty.tools._base import LazyStderr, LazyStdout, Tool
|
|
3
|
+
from duty._internal.tools._base import LazyStderr, LazyStdout, Tool
|
|
6
4
|
|
|
7
5
|
|
|
8
6
|
class autoflake(Tool): # noqa: N801
|
|
9
7
|
"""Call [autoflake](https://github.com/PyCQA/autoflake)."""
|
|
10
8
|
|
|
11
9
|
cli_name = "autoflake"
|
|
10
|
+
"""The name of the executable on PATH."""
|
|
12
11
|
|
|
13
12
|
def __init__(
|
|
14
13
|
self,
|
|
@@ -129,7 +128,12 @@ class autoflake(Tool): # noqa: N801
|
|
|
129
128
|
super().__init__(cli_args)
|
|
130
129
|
|
|
131
130
|
def __call__(self) -> int:
|
|
132
|
-
|
|
131
|
+
"""Run the command.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
The exit code of the command.
|
|
135
|
+
"""
|
|
136
|
+
from autoflake import _main as run_autoflake # noqa: PLC0415
|
|
133
137
|
|
|
134
138
|
return run_autoflake(
|
|
135
139
|
self.cli_args,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
# Utilities for creating tools.
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -21,6 +21,7 @@ class LazyStdout(StringIO):
|
|
|
21
21
|
"""
|
|
22
22
|
|
|
23
23
|
def write(self, value: str) -> int:
|
|
24
|
+
"""Write a string to the stdout buffer."""
|
|
24
25
|
return sys.stdout.write(value)
|
|
25
26
|
|
|
26
27
|
def __repr__(self) -> str:
|
|
@@ -35,6 +36,7 @@ class LazyStderr(StringIO):
|
|
|
35
36
|
"""
|
|
36
37
|
|
|
37
38
|
def write(self, value: str) -> int:
|
|
39
|
+
"""Write a string to the stderr buffer."""
|
|
38
40
|
return sys.stderr.write(value)
|
|
39
41
|
|
|
40
42
|
def __repr__(self) -> str:
|
|
@@ -45,22 +47,33 @@ class Tool:
|
|
|
45
47
|
"""Base class for tools."""
|
|
46
48
|
|
|
47
49
|
cli_name: str = ""
|
|
50
|
+
"""The name of the executable on PATH."""
|
|
48
51
|
|
|
49
52
|
def __init__(
|
|
50
53
|
self,
|
|
51
54
|
cli_args: list[str] | None = None,
|
|
52
55
|
py_args: dict[str, Any] | None = None,
|
|
53
56
|
) -> None:
|
|
57
|
+
"""Initialize the tool.
|
|
58
|
+
|
|
59
|
+
Parameters:
|
|
60
|
+
cli_args: Initial command-line arguments. Use `add_args()` to add more.
|
|
61
|
+
py_args: Python arguments. Your `__call__` method will be able to access
|
|
62
|
+
these arguments as `self.py_args`.
|
|
63
|
+
"""
|
|
54
64
|
self.cli_args: list[str] = cli_args or []
|
|
65
|
+
"""Registered command-line arguments."""
|
|
55
66
|
self.py_args: dict[str, Any] = py_args or {}
|
|
67
|
+
"""Registered Python arguments."""
|
|
56
68
|
|
|
57
69
|
def add_args(self, *args: str) -> Self:
|
|
58
|
-
"""
|
|
70
|
+
"""Append CLI arguments."""
|
|
59
71
|
self.cli_args.extend(args)
|
|
60
72
|
return self
|
|
61
73
|
|
|
62
74
|
@property
|
|
63
75
|
def cli_command(self) -> str:
|
|
76
|
+
"""The equivalent CLI command."""
|
|
64
77
|
if not self.cli_name:
|
|
65
78
|
raise ValueError("This tool does not provide a CLI.")
|
|
66
79
|
return shlex.join([self.cli_name, *self.cli_args])
|