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,96 @@
1
+ from __future__ import annotations
2
+
3
+ from duty._internal.tools._base import Tool
4
+
5
+
6
+ class yore(Tool): # noqa: N801
7
+ """Call [Yore](https://github.com/pawamoy/yore)."""
8
+
9
+ cli_name = "yore"
10
+ """The name of the executable on PATH."""
11
+
12
+ @classmethod
13
+ def check(
14
+ cls,
15
+ *paths: str,
16
+ bump: str | None = None,
17
+ eol_within: str | None = None,
18
+ bol_within: str | None = None,
19
+ ) -> yore:
20
+ """Check Yore comments against Python EOL dates or the provided next version of your project.
21
+
22
+ Parameters:
23
+ paths: Path to files or directories to check.
24
+ bump: The next version of your project.
25
+ eol_within: The time delta to start checking before the End of Life of a Python version.
26
+ It is provided in a human-readable format, like `2 weeks` or `1 month`.
27
+ Spaces are optional, and the unit can be shortened to a single letter:
28
+ `d` for days, `w` for weeks, `m` for months, and `y` for years.
29
+ bol_within: The time delta to start checking before the Beginning of Life of a Python version.
30
+ It is provided in a human-readable format, like `2 weeks` or `1 month`.
31
+ Spaces are optional, and the unit can be shortened to a single letter:
32
+ `d` for days, `w` for weeks, `m` for months, and `y` for years.
33
+ """
34
+ cli_args = ["check", *paths]
35
+
36
+ if bump:
37
+ cli_args.append("--bump")
38
+ cli_args.append(bump)
39
+
40
+ if eol_within:
41
+ cli_args.append("--eol-within")
42
+ cli_args.append(eol_within)
43
+
44
+ if bol_within:
45
+ cli_args.append("--bol-within")
46
+ cli_args.append(bol_within)
47
+
48
+ return cls(cli_args)
49
+
50
+ @classmethod
51
+ def fix(
52
+ cls,
53
+ *paths: str,
54
+ bump: str | None = None,
55
+ eol_within: str | None = None,
56
+ bol_within: str | None = None,
57
+ ) -> yore:
58
+ """Fix your code by transforming it according to the Yore comments.
59
+
60
+ Parameters:
61
+ paths: Path to files or directories to fix.
62
+ bump: The next version of your project.
63
+ eol_within: The time delta to start fixing before the End of Life of a Python version.
64
+ It is provided in a human-readable format, like `2 weeks` or `1 month`.
65
+ Spaces are optional, and the unit can be shortened to a single letter:
66
+ `d` for days, `w` for weeks, `m` for months, and `y` for years.
67
+ bol_within: The time delta to start fixing before the Beginning of Life of a Python version.
68
+ It is provided in a human-readable format, like `2 weeks` or `1 month`.
69
+ Spaces are optional, and the unit can be shortened to a single letter:
70
+ `d` for days, `w` for weeks, `m` for months, and `y` for years.
71
+ """
72
+ cli_args = ["fix", *paths]
73
+
74
+ if bump:
75
+ cli_args.append("--bump")
76
+ cli_args.append(bump)
77
+
78
+ if eol_within:
79
+ cli_args.append("--eol-within")
80
+ cli_args.append(eol_within)
81
+
82
+ if bol_within:
83
+ cli_args.append("--bol-within")
84
+ cli_args.append(bol_within)
85
+
86
+ return cls(cli_args)
87
+
88
+ def __call__(self) -> int:
89
+ """Run the command.
90
+
91
+ Returns:
92
+ The exit code of the command.
93
+ """
94
+ from yore import main as run_yore # noqa: PLC0415
95
+
96
+ return run_yore(self.cli_args)
@@ -0,0 +1,266 @@
1
+ # This module contains logic used to validate parameters passed to duties.
2
+ #
3
+ # We validate the parameters before running the duties,
4
+ # effectively checking all CLI arguments and failing early
5
+ # if they are incorrect.
6
+
7
+ from __future__ import annotations
8
+
9
+ import sys
10
+ import textwrap
11
+ from contextlib import suppress
12
+ from functools import cached_property, partial
13
+ from inspect import Parameter, Signature, signature
14
+ from typing import TYPE_CHECKING, Any, Callable, ForwardRef, Union, get_args, get_origin
15
+
16
+ if TYPE_CHECKING:
17
+ from collections.abc import Sequence
18
+
19
+ # YORE: EOL 3.9: Replace block with lines 6-13.
20
+ if sys.version_info < (3, 10):
21
+ from eval_type_backport import eval_type_backport as _eval_type
22
+
23
+ _union_types = (Union,)
24
+ else:
25
+ from types import UnionType
26
+ from typing import _eval_type # type: ignore[attr-defined]
27
+
28
+ if sys.version_info >= (3, 13):
29
+ _eval_type = partial(_eval_type, type_params=None)
30
+ _union_types = (Union, UnionType)
31
+
32
+
33
+ def to_bool(value: str) -> bool:
34
+ """Convert a string to a boolean.
35
+
36
+ Parameters:
37
+ value: The string to convert.
38
+
39
+ Returns:
40
+ True or False.
41
+ """
42
+ return value.lower() not in {"", "0", "no", "n", "false", "off"}
43
+
44
+
45
+ def cast_arg(arg: Any, annotation: Any) -> Any:
46
+ """Cast an argument using a type annotation.
47
+
48
+ Parameters:
49
+ arg: The argument value.
50
+ annotation: A type annotation.
51
+
52
+ Returns:
53
+ The cast value.
54
+ """
55
+ if annotation is Parameter.empty:
56
+ return arg
57
+ if annotation is bool:
58
+ annotation = to_bool
59
+ if get_origin(annotation) in _union_types:
60
+ for sub_annotation in get_args(annotation):
61
+ if sub_annotation is type(None):
62
+ continue
63
+ with suppress(Exception):
64
+ return cast_arg(arg, sub_annotation)
65
+ try:
66
+ return annotation(arg)
67
+ except Exception: # noqa: BLE001
68
+ return arg
69
+
70
+
71
+ class ParamsCaster:
72
+ """A helper class to cast parameters based on a function's signature annotations."""
73
+
74
+ def __init__(self, signature: Signature) -> None:
75
+ """Initialize the object.
76
+
77
+ Parameters:
78
+ signature: The signature to use to cast arguments.
79
+ """
80
+ self.params_dict = signature.parameters
81
+ """A dictionary of parameters, indexed by their name."""
82
+ self.params_list = list(self.params_dict.values())
83
+ """A list of parameters, in the order they appear in the signature."""
84
+
85
+ @cached_property
86
+ def var_positional_position(self) -> int:
87
+ """Give the position of the variable positional parameter in the signature.
88
+
89
+ Returns:
90
+ The position of the variable positional parameter.
91
+ """
92
+ for pos, param in enumerate(self.params_list):
93
+ if param.kind is Parameter.VAR_POSITIONAL:
94
+ return pos
95
+ return -1
96
+
97
+ @cached_property
98
+ def has_var_positional(self) -> bool:
99
+ """Tell if there is a variable positional parameter.
100
+
101
+ Returns:
102
+ True or False.
103
+ """
104
+ return self.var_positional_position >= 0
105
+
106
+ @cached_property
107
+ def var_positional_annotation(self) -> Any:
108
+ """Give the variable positional parameter (`*args`) annotation if any.
109
+
110
+ Returns:
111
+ The variable positional parameter annotation.
112
+ """
113
+ return self.params_list[self.var_positional_position].annotation
114
+
115
+ @cached_property
116
+ def var_keyword_annotation(self) -> Any:
117
+ """Give the variable keyword parameter (`**kwargs`) annotation if any.
118
+
119
+ Returns:
120
+ The variable keyword parameter annotation.
121
+ """
122
+ for param in self.params_list:
123
+ if param.kind is Parameter.VAR_KEYWORD:
124
+ return param.annotation
125
+ return Parameter.empty
126
+
127
+ def annotation_at_pos(self, pos: int) -> Any:
128
+ """Give the annotation for the parameter at the given position.
129
+
130
+ Parameters:
131
+ pos: The position of the parameter.
132
+
133
+ Returns:
134
+ The positional parameter annotation.
135
+ """
136
+ return self.params_list[pos].annotation
137
+
138
+ def eaten_by_var_positional(self, pos: int) -> bool:
139
+ """Tell if the parameter at this position is eaten by a variable positional parameter.
140
+
141
+ Parameters:
142
+ pos: The position of the parameter.
143
+
144
+ Returns:
145
+ Whether the parameter is eaten.
146
+ """
147
+ return self.has_var_positional and pos >= self.var_positional_position
148
+
149
+ def cast_posarg(self, pos: int, arg: Any) -> Any:
150
+ """Cast a positional argument.
151
+
152
+ Parameters:
153
+ pos: The position of the argument in the signature.
154
+ arg: The argument value.
155
+
156
+ Returns:
157
+ The cast value.
158
+ """
159
+ if self.eaten_by_var_positional(pos):
160
+ return cast_arg(arg, self.var_positional_annotation)
161
+ return cast_arg(arg, self.annotation_at_pos(pos))
162
+
163
+ def cast_kwarg(self, name: str, value: Any) -> Any:
164
+ """Cast a keyword argument.
165
+
166
+ Parameters:
167
+ name: The name of the argument in the signature.
168
+ value: The argument value.
169
+
170
+ Returns:
171
+ The cast value.
172
+ """
173
+ if name in self.params_dict:
174
+ return cast_arg(value, self.params_dict[name].annotation)
175
+ return cast_arg(value, self.var_keyword_annotation)
176
+
177
+ def cast(self, *args: Any, **kwargs: Any) -> tuple[Sequence, dict[str, Any]]:
178
+ """Cast all positional and keyword arguments.
179
+
180
+ Parameters:
181
+ *args: The positional arguments.
182
+ **kwargs: The keyword arguments.
183
+
184
+ Returns:
185
+ The cast arguments.
186
+ """
187
+ positional = tuple(self.cast_posarg(pos, arg) for pos, arg in enumerate(args))
188
+ keyword = {name: self.cast_kwarg(name, value) for name, value in kwargs.items()}
189
+ return positional, keyword
190
+
191
+
192
+ def _get_params_caster(func: Callable, *args: Any, **kwargs: Any) -> ParamsCaster:
193
+ duties_module = sys.modules[func.__module__]
194
+ exec_globals = dict(duties_module.__dict__)
195
+ eval_str = False
196
+ for name in list(exec_globals.keys()):
197
+ if exec_globals[name] is annotations:
198
+ eval_str = True
199
+ del exec_globals[name]
200
+ break
201
+ exec_globals["__context_above"] = {}
202
+
203
+ # Don't keep first parameter: context.
204
+ params = list(signature(func).parameters.values())[1:]
205
+ params_no_types = [Parameter(param.name, param.kind, default=param.default) for param in params]
206
+ code_sig = Signature(parameters=params_no_types)
207
+ if eval_str:
208
+ params_types = [
209
+ Parameter(
210
+ param.name,
211
+ param.kind,
212
+ default=param.default,
213
+ annotation=(
214
+ _eval_type(
215
+ ForwardRef(param.annotation) if isinstance(param.annotation, str) else param.annotation,
216
+ exec_globals,
217
+ {},
218
+ )
219
+ if param.annotation is not Parameter.empty
220
+ else type(param.default)
221
+ ),
222
+ )
223
+ for param in params
224
+ ]
225
+ else:
226
+ params_types = params
227
+ cast_sig = Signature(parameters=params_types)
228
+
229
+ code = f"""
230
+ import inspect
231
+ def {func.__name__}{code_sig}: ...
232
+ __context_above['func'] = {func.__name__}
233
+ """
234
+
235
+ exec(textwrap.dedent(code), exec_globals) # noqa: S102
236
+ func = exec_globals["__context_above"]["func"]
237
+
238
+ # Trigger TypeError early.
239
+ func(*args, **kwargs)
240
+
241
+ return ParamsCaster(cast_sig)
242
+
243
+
244
+ def validate(
245
+ func: Callable,
246
+ *args: Any,
247
+ **kwargs: Any,
248
+ ) -> tuple[Sequence, dict[str, Any]]:
249
+ """Validate positional and keyword arguments against a function.
250
+
251
+ First we clone the function, removing the first parameter (the context)
252
+ and the body, to fail early with a `TypeError` if the arguments
253
+ are incorrect: not enough, too much, in the wrong order, etc.
254
+
255
+ Then we cast all the arguments using the function's signature
256
+ and we return them.
257
+
258
+ Parameters:
259
+ func: The function to copy.
260
+ *args: The positional arguments.
261
+ **kwargs: The keyword arguments.
262
+
263
+ Returns:
264
+ The casted arguments.
265
+ """
266
+ return _get_params_caster(func, *args, **kwargs).cast(*args, **kwargs)
@@ -3,13 +3,15 @@
3
3
  These callables are **deprecated** in favor of our new [tools][duty.tools].
4
4
  """
5
5
 
6
+ # YORE: Bump 2: Remove file.
7
+
6
8
  from __future__ import annotations
7
9
 
8
10
  import warnings
9
11
 
10
- from failprint.lazy import lazy
12
+ from failprint import lazy # noqa: F401
11
13
 
12
- from duty.callables import (
14
+ from duty._internal.callables import (
13
15
  autoflake, # noqa: F401
14
16
  black, # noqa: F401
15
17
  blacken_docs, # noqa: F401
@@ -29,8 +31,6 @@ from duty.callables import (
29
31
  twine, # noqa: F401
30
32
  )
31
33
 
32
- __all__ = ["lazy"]
33
-
34
34
  warnings.warn(
35
35
  "Callables are deprecated in favor of our new `duty.tools`. "
36
36
  "They are easier to use and provide more functionality "
@@ -1,132 +1,17 @@
1
- """Callable for [autoflake](https://github.com/PyCQA/autoflake)."""
1
+ """Deprecated. Use [`duty.tools.autoflake`][] instead."""
2
2
 
3
- from __future__ import annotations
3
+ # YORE: Bump 2: Remove file.
4
4
 
5
- from failprint.lazy import lazy
5
+ import warnings
6
+ from typing import Any
6
7
 
7
- from duty.callables import _io
8
+ from duty._internal.callables import autoflake as _autoflake
8
9
 
9
10
 
10
- @lazy(name="autoflake")
11
- def run(
12
- *files: str,
13
- config: str | None = None,
14
- check: bool | None = None,
15
- check_diff: bool | None = None,
16
- imports: list[str] | None = None,
17
- remove_all_unused_imports: bool | None = None,
18
- recursive: bool | None = None,
19
- jobs: int | None = None,
20
- exclude: list[str] | None = None,
21
- expand_star_imports: bool | None = None,
22
- ignore_init_module_imports: bool | None = None,
23
- remove_duplicate_keys: bool | None = None,
24
- remove_unused_variables: bool | None = None,
25
- remove_rhs_for_unused_variables: bool | None = None,
26
- ignore_pass_statements: bool | None = None,
27
- ignore_pass_after_docstring: bool | None = None,
28
- quiet: bool | None = None,
29
- verbose: bool | None = None,
30
- stdin_display_name: str | None = None,
31
- in_place: bool | None = None,
32
- stdout: bool | None = None,
33
- ) -> int:
34
- r"""Run `autoflake`.
35
-
36
- Parameters:
37
- *files: Files to format.
38
- config: Explicitly set the config file instead of auto determining based on file location.
39
- check: Return error code if changes are needed.
40
- check_diff: Return error code if changes are needed, also display file diffs.
41
- imports: By default, only unused standard library imports are removed; specify a comma-separated list of additional modules/packages.
42
- remove_all_unused_imports: Remove all unused imports (not just those from the standard library).
43
- recursive: Drill down directories recursively.
44
- jobs: Number of parallel jobs; match CPU count if value is 0 (default: 0).
45
- exclude: Exclude file/directory names that match these comma-separated globs.
46
- expand_star_imports: Expand wildcard star imports with undefined names; this only triggers if there is only one star import in the file; this is skipped if there are any uses of `__all__` or `del` in the file.
47
- ignore_init_module_imports: Exclude `__init__.py` when removing unused imports.
48
- remove_duplicate_keys: Remove all duplicate keys in objects.
49
- remove_unused_variables: Remove unused variables.
50
- remove_rhs_for_unused_variables: Remove RHS of statements when removing unused variables (unsafe).
51
- ignore_pass_statements: Ignore all pass statements.
52
- ignore_pass_after_docstring: Ignore pass statements after a newline ending on `\"\"\"`.
53
- quiet: Suppress output if there are no issues.
54
- verbose: Print more verbose logs (you can repeat `-v` to make it more verbose).
55
- stdin_display_name: The name used when processing input from stdin.
56
- in_place: Make changes to files instead of printing diffs.
57
- stdout: Print changed text to stdout. defaults to true when formatting stdin, or to false otherwise.
58
- """
59
- from autoflake import _main as autoflake
60
-
61
- cli_args = list(files)
62
-
63
- if check:
64
- cli_args.append("--check")
65
-
66
- if check_diff:
67
- cli_args.append("--check-diff")
68
-
69
- if imports:
70
- cli_args.append("--imports")
71
- cli_args.append(",".join(imports))
72
-
73
- if remove_all_unused_imports:
74
- cli_args.append("--remove-all-unused-imports")
75
-
76
- if recursive:
77
- cli_args.append("--recursive")
78
-
79
- if jobs:
80
- cli_args.append("--jobs")
81
- cli_args.append(str(jobs))
82
-
83
- if exclude:
84
- cli_args.append("--exclude")
85
- cli_args.append(",".join(exclude))
86
-
87
- if expand_star_imports:
88
- cli_args.append("--expand-star-imports")
89
-
90
- if ignore_init_module_imports:
91
- cli_args.append("--ignore-init-module-imports")
92
-
93
- if remove_duplicate_keys:
94
- cli_args.append("--remove-duplicate-keys")
95
-
96
- if remove_unused_variables:
97
- cli_args.append("--remove-unused-variables")
98
-
99
- if remove_rhs_for_unused_variables:
100
- cli_args.append("remove-rhs-for-unused-variables")
101
-
102
- if ignore_pass_statements:
103
- cli_args.append("--ignore-pass-statements")
104
-
105
- if ignore_pass_after_docstring:
106
- cli_args.append("--ignore-pass-after-docstring")
107
-
108
- if quiet:
109
- cli_args.append("--quiet")
110
-
111
- if verbose:
112
- cli_args.append("--verbose")
113
-
114
- if stdin_display_name:
115
- cli_args.append("--stdin-display-name")
116
- cli_args.append(stdin_display_name)
117
-
118
- if config:
119
- cli_args.append("--config")
120
- cli_args.append(config)
121
-
122
- if in_place:
123
- cli_args.append("--in-place")
124
-
125
- if stdout:
126
- cli_args.append("--stdout")
127
-
128
- return autoflake(
129
- cli_args,
130
- standard_out=_io._LazyStdout(),
131
- standard_error=_io._LazyStderr(),
11
+ def __getattr__(name: str) -> Any:
12
+ warnings.warn(
13
+ "Callables are deprecated in favor of tools, use `duty.tools.autoflake` instead of `duty.callables.autoflake`.",
14
+ DeprecationWarning,
15
+ stacklevel=2,
132
16
  )
17
+ return getattr(_autoflake, name)