duty 1.3.0__py3-none-any.whl → 1.4.0__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/tools/_ruff.py ADDED
@@ -0,0 +1,451 @@
1
+ """Callable for [Ruff](https://github.com/charliermarsh/ruff)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import subprocess
7
+ import sys
8
+ from functools import lru_cache
9
+
10
+ from duty.tools._base import Tool
11
+
12
+
13
+ @lru_cache(maxsize=None)
14
+ def _find_ruff() -> str:
15
+ from ruff.__main__ import find_ruff_bin
16
+
17
+ try:
18
+ return find_ruff_bin()
19
+ except FileNotFoundError:
20
+ paths = os.environ["PATH"]
21
+ for path in paths.split(os.pathsep):
22
+ ruff = os.path.join(path, "ruff")
23
+ if os.path.exists(ruff):
24
+ return ruff
25
+ py_version = f"{sys.version_info[0]}.{sys.version_info[1]}"
26
+ pypackages_bin = os.path.join("__pypackages__", py_version, "bin")
27
+ ruff = os.path.join(pypackages_bin, "ruff")
28
+ if os.path.exists(ruff):
29
+ return ruff
30
+ return "ruff"
31
+
32
+
33
+ class ruff(Tool): # noqa: N801
34
+ """Call [Ruff](https://github.com/charliermarsh/ruff)."""
35
+
36
+ cli_name = "ruff"
37
+
38
+ @classmethod
39
+ def check(
40
+ cls,
41
+ *files: str,
42
+ config: str | None = None,
43
+ fix: bool | None = None,
44
+ show_source: bool | None = None,
45
+ show_fixes: bool | None = None,
46
+ diff: bool | None = None,
47
+ watch: bool | None = None,
48
+ fix_only: bool | None = None,
49
+ output_format: str | None = None,
50
+ statistics: bool | None = None,
51
+ add_noqa: bool | None = None,
52
+ show_files: bool | None = None,
53
+ show_settings: bool | None = None,
54
+ select: list[str] | None = None,
55
+ ignore: list[str] | None = None,
56
+ extend_select: list[str] | None = None,
57
+ per_file_ignores: dict[str, list[str]] | None = None,
58
+ fixable: list[str] | None = None,
59
+ unfixable: list[str] | None = None,
60
+ exclude: list[str] | None = None,
61
+ extend_exclude: list[str] | None = None,
62
+ respect_gitignore: bool | None = None,
63
+ force_exclude: bool | None = None,
64
+ no_cache: bool | None = None,
65
+ isolated: bool | None = None,
66
+ cache_dir: str | None = None,
67
+ stdin_filename: str | None = None,
68
+ exit_zero: bool | None = None,
69
+ exit_non_zero_on_fix: bool | None = None,
70
+ verbose: bool = False,
71
+ quiet: bool = False,
72
+ silent: bool = False,
73
+ ) -> ruff:
74
+ """Run Ruff on the given files or directories.
75
+
76
+ Parameters:
77
+ fix: Attempt to automatically fix lint violations
78
+ config: Path to the `pyproject.toml` or `ruff.toml` file to use for configuration
79
+ show_source: Show violations with source code
80
+ show_fixes: Show an enumeration of all autofixed lint violations
81
+ diff: Avoid writing any fixed files back; instead, output a diff for each changed file to stdout
82
+ watch: Run in watch mode by re-running whenever files change
83
+ fix_only: Fix any fixable lint violations, but don't report on leftover violations. Implies `--fix`
84
+ output_format: Output serialization format for violations [env: RUFF_FORMAT=] [possible values: text, json, junit, grouped, github, gitlab, pylint]
85
+ statistics: Show counts for every rule with at least one violation
86
+ add_noqa: Enable automatic additions of `noqa` directives to failing lines
87
+ show_files: See the files Ruff will be run against with the current settings
88
+ show_settings: See the settings Ruff will use to lint a given Python file
89
+ select: Comma-separated list of rule codes to enable (or ALL, to enable all rules)
90
+ ignore: Comma-separated list of rule codes to disable
91
+ extend_select: Like --select, but adds additional rule codes on top of the selected ones
92
+ per_file_ignores: List of mappings from file pattern to code to exclude
93
+ fixable: List of rule codes to treat as eligible for autofix. Only applicable when autofix itself is enabled (e.g., via `--fix`)
94
+ unfixable: List of rule codes to treat as ineligible for autofix. Only applicable when autofix itself is enabled (e.g., via `--fix`)
95
+ exclude: List of paths, used to omit files and/or directories from analysis
96
+ extend_exclude: Like --exclude, but adds additional files and directories on top of those already excluded
97
+ respect_gitignore: Respect file exclusions via `.gitignore` and other standard ignore files
98
+ force_exclude: Enforce exclusions, even for paths passed to Ruff directly on the command-line
99
+ no_cache: Disable cache reads
100
+ isolated: Ignore all configuration files
101
+ cache_dir: Path to the cache directory [env: RUFF_CACHE_DIR=]
102
+ stdin_filename: The name of the file when passing it through stdin
103
+ exit_zero: Exit with status code "0", even upon detecting lint violations
104
+ exit_non_zero_on_fix: Exit with a non-zero status code if any files were modified via autofix, even if no lint violations remain
105
+ verbose: Enable verbose logging.
106
+ quiet: Print lint violations, but nothing else.
107
+ silent: Disable all logging (but still exit with status code "1" upon detecting lint violations).
108
+ """
109
+ cli_args = ["check", *files]
110
+
111
+ if fix:
112
+ cli_args.append("--fix")
113
+
114
+ if show_source:
115
+ cli_args.append("--show-source")
116
+
117
+ if show_fixes:
118
+ cli_args.append("--show-fixes")
119
+
120
+ if diff:
121
+ cli_args.append("--diff")
122
+
123
+ if watch:
124
+ cli_args.append("--watch")
125
+
126
+ if fix_only:
127
+ cli_args.append("--fix-only")
128
+
129
+ if output_format:
130
+ cli_args.append("--format")
131
+ cli_args.append(output_format)
132
+
133
+ if config:
134
+ cli_args.append("--config")
135
+ cli_args.append(config)
136
+
137
+ if statistics:
138
+ cli_args.append("--statistics")
139
+
140
+ if add_noqa:
141
+ cli_args.append("--add-noqa")
142
+
143
+ if show_files:
144
+ cli_args.append("--show-files")
145
+
146
+ if show_settings:
147
+ cli_args.append("--show-settings")
148
+
149
+ if select:
150
+ cli_args.append("--select")
151
+ cli_args.append(",".join(select))
152
+
153
+ if ignore:
154
+ cli_args.append("--ignore")
155
+ cli_args.append(",".join(ignore))
156
+
157
+ if extend_select:
158
+ cli_args.append("--extend-select")
159
+ cli_args.append(",".join(extend_select))
160
+
161
+ if per_file_ignores:
162
+ cli_args.append("--per-file-ignores")
163
+ cli_args.append(
164
+ " ".join(f"{path}:{','.join(codes)}" for path, codes in per_file_ignores.items()),
165
+ )
166
+
167
+ if fixable:
168
+ cli_args.append("--fixable")
169
+ cli_args.append(",".join(fixable))
170
+
171
+ if unfixable:
172
+ cli_args.append("--unfixable")
173
+ cli_args.append(",".join(unfixable))
174
+
175
+ if exclude:
176
+ cli_args.append("--exclude")
177
+ cli_args.append(",".join(exclude))
178
+
179
+ if extend_exclude:
180
+ cli_args.append("--extend-exclude")
181
+ cli_args.append(",".join(extend_exclude))
182
+
183
+ if respect_gitignore:
184
+ cli_args.append("--respect-gitignore")
185
+
186
+ if force_exclude:
187
+ cli_args.append("--force-exclude")
188
+
189
+ if no_cache:
190
+ cli_args.append("--no-cache")
191
+
192
+ if isolated:
193
+ cli_args.append("--isolated")
194
+
195
+ if cache_dir:
196
+ cli_args.append("--cache-dir")
197
+ cli_args.append(cache_dir)
198
+
199
+ if stdin_filename:
200
+ cli_args.append("--stdin-filename")
201
+ cli_args.append(stdin_filename)
202
+
203
+ if exit_zero:
204
+ cli_args.append("--exit-zero")
205
+
206
+ if exit_non_zero_on_fix:
207
+ cli_args.append("--exit-non-zero-on-fix")
208
+
209
+ if verbose:
210
+ cli_args.append("--verbose")
211
+
212
+ if quiet:
213
+ cli_args.append("--quiet")
214
+
215
+ if silent:
216
+ cli_args.append("--silent")
217
+
218
+ return cls(cli_args)
219
+
220
+ @classmethod
221
+ def format(
222
+ cls,
223
+ *files: str,
224
+ config: str | None = None,
225
+ check: bool | None = None,
226
+ diff: bool | None = None,
227
+ target_version: str | None = None,
228
+ preview: bool | None = None,
229
+ exclude: list[str] | None = None,
230
+ extend_exclude: list[str] | None = None,
231
+ respect_gitignore: bool | None = None,
232
+ force_exclude: bool | None = None,
233
+ no_cache: bool | None = None,
234
+ isolated: bool | None = None,
235
+ cache_dir: str | None = None,
236
+ stdin_filename: str | None = None,
237
+ verbose: bool = False,
238
+ quiet: bool = False,
239
+ silent: bool = False,
240
+ ) -> ruff:
241
+ """Run Ruff formatter on the given files or directories.
242
+
243
+ Parameters:
244
+ check: Avoid writing any formatted files back; instead, exit with a non-zero status code if any files would have been modified, and zero otherwise
245
+ config: Path to the `pyproject.toml` or `ruff.toml` file to use for configuration
246
+ diff: Avoid writing any fixed files back; instead, output a diff for each changed file to stdout
247
+ target_version: The minimum Python version that should be supported [possible values: py37, py38, py39, py310, py311, py312]
248
+ preview: Enable preview mode; enables unstable formatting
249
+ exclude: List of paths, used to omit files and/or directories from analysis
250
+ extend_exclude: Like --exclude, but adds additional files and directories on top of those already excluded
251
+ respect_gitignore: Respect file exclusions via `.gitignore` and other standard ignore files
252
+ force_exclude: Enforce exclusions, even for paths passed to Ruff directly on the command-line
253
+ no_cache: Disable cache reads
254
+ isolated: Ignore all configuration files
255
+ cache_dir: Path to the cache directory [env: RUFF_CACHE_DIR=]
256
+ stdin_filename: The name of the file when passing it through stdin
257
+ verbose: Enable verbose logging.
258
+ quiet: Print lint violations, but nothing else.
259
+ silent: Disable all logging (but still exit with status code "1" upon detecting lint violations).
260
+ """
261
+ cli_args = ["format", *files]
262
+
263
+ if check:
264
+ cli_args.append("--check")
265
+
266
+ if diff:
267
+ cli_args.append("--diff")
268
+
269
+ if config:
270
+ cli_args.append("--config")
271
+ cli_args.append(config)
272
+
273
+ if target_version:
274
+ cli_args.append("--target-version")
275
+ cli_args.append(target_version)
276
+
277
+ if preview:
278
+ cli_args.append("--preview")
279
+
280
+ if exclude:
281
+ cli_args.append("--exclude")
282
+ cli_args.append(",".join(exclude))
283
+
284
+ if extend_exclude:
285
+ cli_args.append("--extend-exclude")
286
+ cli_args.append(",".join(extend_exclude))
287
+
288
+ if respect_gitignore:
289
+ cli_args.append("--respect-gitignore")
290
+
291
+ if force_exclude:
292
+ cli_args.append("--force-exclude")
293
+
294
+ if no_cache:
295
+ cli_args.append("--no-cache")
296
+
297
+ if isolated:
298
+ cli_args.append("--isolated")
299
+
300
+ if cache_dir:
301
+ cli_args.append("--cache-dir")
302
+ cli_args.append(cache_dir)
303
+
304
+ if stdin_filename:
305
+ cli_args.append("--stdin-filename")
306
+ cli_args.append(stdin_filename)
307
+
308
+ if verbose:
309
+ cli_args.append("--verbose")
310
+
311
+ if quiet:
312
+ cli_args.append("--quiet")
313
+
314
+ if silent:
315
+ cli_args.append("--silent")
316
+
317
+ return cls(cli_args)
318
+
319
+ @classmethod
320
+ def rule(
321
+ cls,
322
+ *,
323
+ output_format: str | None = None,
324
+ verbose: bool = False,
325
+ quiet: bool = False,
326
+ silent: bool = False,
327
+ ) -> ruff:
328
+ """Explain a rule.
329
+
330
+ Parameters:
331
+ output_format: Output format (default: pretty, possible values: text, json, pretty).
332
+ verbose: Enable verbose logging.
333
+ quiet: Print lint violations, but nothing else.
334
+ silent: Disable all logging (but still exit with status code "1" upon detecting lint violations).
335
+ """
336
+ cli_args = ["rule"]
337
+
338
+ if output_format:
339
+ cli_args.append("--format")
340
+ cli_args.append(output_format)
341
+
342
+ if verbose:
343
+ cli_args.append("--verbose")
344
+
345
+ if quiet:
346
+ cli_args.append("--quiet")
347
+
348
+ if silent:
349
+ cli_args.append("--silent")
350
+
351
+ return cls(cli_args)
352
+
353
+ @classmethod
354
+ def config(
355
+ cls,
356
+ *,
357
+ verbose: bool = False,
358
+ quiet: bool = False,
359
+ silent: bool = False,
360
+ ) -> ruff:
361
+ """List or describe the available configuration options.
362
+
363
+ Parameters:
364
+ verbose: Enable verbose logging.
365
+ quiet: Print lint violations, but nothing else.
366
+ silent: Disable all logging (but still exit with status code "1" upon detecting lint violations).
367
+ """
368
+ cli_args = ["config"]
369
+
370
+ if verbose:
371
+ cli_args.append("--verbose")
372
+
373
+ if quiet:
374
+ cli_args.append("--quiet")
375
+
376
+ if silent:
377
+ cli_args.append("--silent")
378
+
379
+ return cls(cli_args)
380
+
381
+ @classmethod
382
+ def linter(
383
+ cls,
384
+ *,
385
+ output_format: str | None = None,
386
+ verbose: bool = False,
387
+ quiet: bool = False,
388
+ silent: bool = False,
389
+ ) -> ruff:
390
+ """List all supported upstream linters.
391
+
392
+ Parameters:
393
+ output_format: Output format [default: pretty] [possible values: text, json, pretty].
394
+ verbose: Enable verbose logging.
395
+ quiet: Print lint violations, but nothing else.
396
+ silent: Disable all logging (but still exit with status code "1" upon detecting lint violations).
397
+ """
398
+ cli_args = ["linter"]
399
+
400
+ if output_format:
401
+ cli_args.append("--format")
402
+ cli_args.append(output_format)
403
+
404
+ if verbose:
405
+ cli_args.append("--verbose")
406
+
407
+ if quiet:
408
+ cli_args.append("--quiet")
409
+
410
+ if silent:
411
+ cli_args.append("--silent")
412
+
413
+ return cls(cli_args)
414
+
415
+ @classmethod
416
+ def clean(
417
+ cls,
418
+ *,
419
+ verbose: bool = False,
420
+ quiet: bool = False,
421
+ silent: bool = False,
422
+ ) -> ruff:
423
+ """Clear any caches in the current directory and any subdirectories.
424
+
425
+ Parameters:
426
+ verbose: Enable verbose logging.
427
+ quiet: Print lint violations, but nothing else.
428
+ silent: Disable all logging (but still exit with status code "1" upon detecting lint violations).
429
+ """
430
+ cli_args = ["clean"]
431
+
432
+ if verbose:
433
+ cli_args.append("--verbose")
434
+
435
+ if quiet:
436
+ cli_args.append("--quiet")
437
+
438
+ if silent:
439
+ cli_args.append("--silent")
440
+
441
+ return cls(cli_args)
442
+
443
+ def __call__(self) -> int:
444
+ process = subprocess.run(
445
+ [_find_ruff(), *self.cli_args], # noqa: S603
446
+ capture_output=True,
447
+ text=True,
448
+ check=False,
449
+ )
450
+ print(process.stdout) # noqa: T201
451
+ return process.returncode
duty/tools/_safety.py ADDED
@@ -0,0 +1,97 @@
1
+ """Callable for [Safety](https://github.com/pyupio/safety)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib
6
+ import sys
7
+ from io import StringIO
8
+ from typing import Literal, Sequence, cast
9
+
10
+ from duty.tools._base import Tool
11
+
12
+
13
+ class safety(Tool): # noqa: N801
14
+ """Call [Safety](https://github.com/pyupio/safety)."""
15
+
16
+ cli_name = "safety"
17
+
18
+ @classmethod
19
+ def check(
20
+ cls,
21
+ requirements: str | Sequence[str],
22
+ *,
23
+ ignore_vulns: dict[str, str] | None = None,
24
+ formatter: Literal["json", "bare", "text"] = "text",
25
+ full_report: bool = True,
26
+ ) -> safety:
27
+ """Run the safety check command.
28
+
29
+ This function makes sure we load the original, unpatched version of safety.
30
+
31
+ Parameters:
32
+ requirements: Python "requirements" (list of pinned dependencies).
33
+ ignore_vulns: Vulnerabilities to ignore.
34
+ formatter: Report format.
35
+ full_report: Whether to output a full report.
36
+
37
+ Returns:
38
+ Success/failure.
39
+ """
40
+ return cls(py_args=dict(locals()))
41
+
42
+ @property
43
+ def cli_command(self) -> str:
44
+ raise ValueError("This command cannot be translated to a CLI command.")
45
+
46
+ def __call__(self) -> bool:
47
+ requirements = self.py_args["requirements"]
48
+ ignore_vulns = self.py_args["ignore_vulns"]
49
+ formatter = self.py_args["formatter"]
50
+ full_report = self.py_args["full_report"]
51
+
52
+ # set default parameter values
53
+ ignore_vulns = ignore_vulns or {}
54
+
55
+ # undo possible patching
56
+ # see https://github.com/pyupio/safety/issues/348
57
+ for module in sys.modules:
58
+ if module.startswith("safety.") or module == "safety":
59
+ del sys.modules[module]
60
+
61
+ importlib.invalidate_caches()
62
+
63
+ # reload original, unpatched safety
64
+ from safety.formatter import SafetyFormatter
65
+ from safety.safety import calculate_remediations, check
66
+ from safety.util import read_requirements
67
+
68
+ # check using safety as a library
69
+ if isinstance(requirements, (list, tuple, set)):
70
+ requirements = "\n".join(requirements)
71
+ packages = list(read_requirements(StringIO(cast(str, requirements))))
72
+
73
+ # TODO: Safety 3 support, merge once support for v2 is dropped.
74
+ check_kwargs = {"packages": packages, "ignore_vulns": ignore_vulns}
75
+ try:
76
+ from safety.auth.cli_utils import build_client_session
77
+
78
+ client_session, _ = build_client_session()
79
+ check_kwargs["session"] = client_session
80
+ except ImportError:
81
+ pass
82
+
83
+ vulns, db_full = check(**check_kwargs)
84
+ remediations = calculate_remediations(vulns, db_full)
85
+ output_report = SafetyFormatter(formatter).render_vulnerabilities(
86
+ announcements=[],
87
+ vulnerabilities=vulns,
88
+ remediations=remediations,
89
+ full=full_report,
90
+ packages=packages,
91
+ )
92
+
93
+ # print report, return status
94
+ if vulns:
95
+ print(output_report) # noqa: T201
96
+ return False
97
+ return True
duty/tools/_ssort.py ADDED
@@ -0,0 +1,44 @@
1
+ """Callable for [ssort](https://github.com/bwhmather/ssort)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+
7
+ from duty.tools._base import Tool
8
+
9
+
10
+ class ssort(Tool): # noqa: N801
11
+ """Call [ssort](https://github.com/bwhmather/ssort)."""
12
+
13
+ cli_name = "ssort"
14
+
15
+ def __init__(
16
+ self,
17
+ *files: str,
18
+ diff: bool | None = None,
19
+ check: bool | None = None,
20
+ ) -> None:
21
+ """Run `ssort`.
22
+
23
+ Parameters:
24
+ *files: Files to format.
25
+ diff: Prints a diff of all changes ssort would make to a file.
26
+ check: Check the file for unsorted statements. Returns 0 if nothing needs to be changed. Otherwise returns 1.
27
+ """
28
+ cli_args = list(files)
29
+
30
+ if diff:
31
+ cli_args.append("--diff")
32
+
33
+ if check:
34
+ cli_args.append("--check")
35
+
36
+ def __call__(self) -> int:
37
+ from ssort._main import main as run_ssort
38
+
39
+ old_sys_argv = sys.argv
40
+ sys.argv = ["ssort", *self.cli_args]
41
+ try:
42
+ return run_ssort()
43
+ finally:
44
+ sys.argv = old_sys_argv