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.
Files changed (82) hide show
  1. duty/__init__.py +49 -2
  2. duty/__main__.py +1 -1
  3. duty/_internal/__init__.py +0 -0
  4. duty/_internal/callables/__init__.py +34 -0
  5. duty/{callables → _internal/callables}/_io.py +2 -0
  6. duty/_internal/callables/autoflake.py +132 -0
  7. duty/_internal/callables/black.py +176 -0
  8. duty/_internal/callables/blacken_docs.py +92 -0
  9. duty/_internal/callables/build.py +76 -0
  10. duty/_internal/callables/coverage.py +716 -0
  11. duty/_internal/callables/flake8.py +222 -0
  12. duty/_internal/callables/git_changelog.py +178 -0
  13. duty/_internal/callables/griffe.py +227 -0
  14. duty/_internal/callables/interrogate.py +152 -0
  15. duty/_internal/callables/isort.py +573 -0
  16. duty/_internal/callables/mkdocs.py +256 -0
  17. duty/_internal/callables/mypy.py +496 -0
  18. duty/_internal/callables/pytest.py +475 -0
  19. duty/_internal/callables/ruff.py +399 -0
  20. duty/_internal/callables/safety.py +82 -0
  21. duty/_internal/callables/ssort.py +38 -0
  22. duty/_internal/callables/twine.py +284 -0
  23. duty/_internal/cli.py +322 -0
  24. duty/_internal/collection.py +246 -0
  25. duty/_internal/context.py +111 -0
  26. duty/{debug.py → _internal/debug.py} +13 -15
  27. duty/_internal/decorator.py +111 -0
  28. duty/_internal/exceptions.py +12 -0
  29. duty/_internal/tools/__init__.py +41 -0
  30. duty/{tools → _internal/tools}/_autoflake.py +8 -4
  31. duty/{tools → _internal/tools}/_base.py +15 -2
  32. duty/{tools → _internal/tools}/_black.py +5 -5
  33. duty/{tools → _internal/tools}/_blacken_docs.py +10 -5
  34. duty/{tools → _internal/tools}/_build.py +4 -4
  35. duty/{tools → _internal/tools}/_coverage.py +8 -4
  36. duty/{tools → _internal/tools}/_flake8.py +10 -12
  37. duty/{tools → _internal/tools}/_git_changelog.py +8 -4
  38. duty/{tools → _internal/tools}/_griffe.py +8 -4
  39. duty/{tools → _internal/tools}/_interrogate.py +4 -4
  40. duty/{tools → _internal/tools}/_isort.py +8 -6
  41. duty/{tools → _internal/tools}/_mkdocs.py +8 -4
  42. duty/{tools → _internal/tools}/_mypy.py +5 -5
  43. duty/{tools → _internal/tools}/_pytest.py +8 -4
  44. duty/{tools → _internal/tools}/_ruff.py +11 -5
  45. duty/{tools → _internal/tools}/_safety.py +13 -8
  46. duty/{tools → _internal/tools}/_ssort.py +10 -6
  47. duty/{tools → _internal/tools}/_twine.py +11 -5
  48. duty/_internal/tools/_yore.py +96 -0
  49. duty/_internal/validation.py +266 -0
  50. duty/callables/__init__.py +4 -4
  51. duty/callables/autoflake.py +11 -126
  52. duty/callables/black.py +12 -171
  53. duty/callables/blacken_docs.py +11 -86
  54. duty/callables/build.py +12 -71
  55. duty/callables/coverage.py +12 -711
  56. duty/callables/flake8.py +12 -217
  57. duty/callables/git_changelog.py +12 -173
  58. duty/callables/griffe.py +12 -222
  59. duty/callables/interrogate.py +12 -147
  60. duty/callables/isort.py +12 -568
  61. duty/callables/mkdocs.py +12 -251
  62. duty/callables/mypy.py +11 -490
  63. duty/callables/pytest.py +12 -470
  64. duty/callables/ruff.py +12 -394
  65. duty/callables/safety.py +11 -76
  66. duty/callables/ssort.py +12 -33
  67. duty/callables/twine.py +12 -279
  68. duty/cli.py +10 -316
  69. duty/collection.py +12 -228
  70. duty/context.py +12 -107
  71. duty/decorator.py +12 -108
  72. duty/exceptions.py +13 -10
  73. duty/tools.py +63 -0
  74. duty/validation.py +12 -262
  75. {duty-1.6.0.dist-info → duty-1.6.2.dist-info}/METADATA +5 -4
  76. duty-1.6.2.dist-info/RECORD +81 -0
  77. {duty-1.6.0.dist-info → duty-1.6.2.dist-info}/WHEEL +1 -1
  78. {duty-1.6.0.dist-info → duty-1.6.2.dist-info}/entry_points.txt +1 -1
  79. duty/tools/__init__.py +0 -50
  80. duty/tools/_yore.py +0 -54
  81. duty-1.6.0.dist-info/RECORD +0 -55
  82. {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 Variable:
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 Package:
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 Environment:
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[Package]
42
+ packages: list[_Package]
45
43
  """Installed packages."""
46
- variables: list[Variable]
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 get_version(dist: str = "duty") -> str:
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 get_debug_info() -> Environment:
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 Environment(
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=[Variable(var, val) for var in variables if (val := os.getenv(var))],
91
- packages=[Package(pkg, get_version(pkg)) for pkg in 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 print_debug_info() -> None:
93
+ def _print_debug_info() -> None:
96
94
  """Print debug/environment information."""
97
- info = get_debug_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
- print_debug_info()
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
- from autoflake import _main as run_autoflake
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
- """Utilities for creating tools."""
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
- """Add arguments."""
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])