ballpython 2.0.2__py3-none-any.whl → 2.0.4__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.
@@ -0,0 +1,103 @@
1
+ Metadata-Version: 2.4
2
+ Name: ballpython
3
+ Version: 2.0.4
4
+ Summary: The Ultimate Static Python Intelligence, Healing, Type Verification, and Security Suite
5
+ Author: Developer
6
+ License-Expression: MIT
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: ruff>=0.1.0
10
+ Requires-Dist: rich>=13.0.0
11
+ Requires-Dist: tomli>=1.1.0; python_version < "3.11"
12
+ Provides-Extra: dev
13
+ Requires-Dist: pytest>=8.0.0; extra == "dev"
14
+ Requires-Dist: black>=24.0.0; extra == "dev"
15
+ Requires-Dist: isort>=5.13.0; extra == "dev"
16
+ Provides-Extra: security
17
+ Requires-Dist: cryptography>=41.0.0; extra == "security"
18
+ Requires-Dist: defusedxml>=0.7.1; extra == "security"
19
+
20
+ # ballpython
21
+
22
+ [![PyPI version](https://img.shields.io/pypi/v/ballpython.svg)](https://pypi.org/project/ballpython/)
23
+ [![Python Versions](https://img.shields.io/pypi/pyversions/ballpython.svg)](https://pypi.org/project/ballpython/)
24
+ [![CI](https://github.com/moonrox420/ball-python/actions/workflows/ci.yml/badge.svg)](https://github.com/moonrox420/ball-python/actions/workflows/ci.yml)
25
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
26
+
27
+ **The zero-config Python code cleaner, syntax healer, and quality suite.**
28
+
29
+ `ballpython` automatically repairs broken syntax, resolves missing imports, prunes dead code, scans for security vulnerabilities, and formats your code in a single command. Zero LLM dependencies, 100% offline, and fast.
30
+
31
+ Both `ballpython` and `pycleaner` CLI commands are identical.
32
+
33
+ ---
34
+
35
+ ## Installation
36
+
37
+ ```bash
38
+ pip install ballpython
39
+ ```
40
+
41
+ ---
42
+
43
+ ## Quickstart
44
+
45
+ ### 1. Fix and format your code
46
+ Run `ballpython` on any file or directory. It automatically heals syntax errors, resolves missing imports, removes unused variables, and applies canonical formatting:
47
+
48
+ ```bash
49
+ ballpython .
50
+ ```
51
+
52
+ ### 2. Check code in CI (Dry Run)
53
+ Check for syntax errors, security vulnerabilities, dead code, and linting violations without modifying any files on disk:
54
+
55
+ ```bash
56
+ ballpython check .
57
+ ```
58
+
59
+ ---
60
+
61
+ ## What It Does Automatically
62
+
63
+ When you run `ballpython`, all engines run under the hood in one pass:
64
+
65
+ * **Syntax Healing:** Repairs missing statement colons (`def`, `class`, `if`), accidental `=` assignments in conditionals, unclosed parentheses/brackets, tab/space mixups, and legacy Python 2 except/print statements.
66
+ * **Smart Import Resolution:** Resolves undefined symbols to standard library modules and common aliases, inserts `TYPE_CHECKING` guards, and prunes unused imports.
67
+ * **Formatting & Modernization:** Formats code to PEP 8 standards (powered by `ruff`), modernizes legacy type annotations (PEP 585/604), and removes redundant `object` inheritance.
68
+ * **Dead Code Pruning:** Discovers and eliminates unreachable code, empty pass blocks, and unused private definitions.
69
+ * **Security & Taint Audit:** Detects hardcoded API secrets, SQL/command injection patterns, and unsafe `eval`/`exec`/`pickle` calls.
70
+ * **Type Verification:** Verifies function call signatures against bundled Typeshed stubs.
71
+
72
+ ---
73
+
74
+ ## Pre-Commit Hook
75
+
76
+ Add `ballpython` to your `.pre-commit-config.yaml`:
77
+
78
+ ```yaml
79
+ repos:
80
+ - repo: https://github.com/moonrox420/ball-python
81
+ rev: v2.0.4
82
+ hooks:
83
+ - id: pycleaner
84
+ args: ["check"]
85
+ ```
86
+
87
+ ---
88
+
89
+ ## Configuration
90
+
91
+ Optional configuration can be placed in `pyproject.toml` under `[tool.pycleaner]`:
92
+
93
+ ```toml
94
+ [tool.pycleaner]
95
+ exclude = ["migrations/", "generated/"]
96
+ line-length = 88
97
+ ```
98
+
99
+ ---
100
+
101
+ ## License
102
+
103
+ MIT License.
@@ -2,11 +2,11 @@ ballpython/__init__.py,sha256=d8ONxgaz9jml_fHPVvg867Vt5jGRhGTqzWPtux3WJOc,206
2
2
  ballpython/__main__.py,sha256=aCRr0vnZfKYHDrPHBsYSjX5xTxtFJpTTSPVUSZTBK-I,136
3
3
  ballpython/cli.py,sha256=LllCLRAQQ-fRR_lIXwmd2e3Q7DLCIvf-5jHBXsAhJWs,125
4
4
  ballpython/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
- pycleaner/__init__.py,sha256=GMc3bYj3wg2AM_SZn_MRDq4i0f0PTaVq_ITR5spiOgI,2894
5
+ pycleaner/__init__.py,sha256=AbIZyqCYVTi-fjqPCgWZmTeSth_8ma-8sYbl9SghNnQ,2894
6
6
  pycleaner/__main__.py,sha256=8QBFrPyxId563F6cipPMNVEBTG8TIZUgxQfp8gk1VSI,135
7
7
  pycleaner/baseline.py,sha256=DS32C8_G8khYYNJo42FVv6Cc58lJj7NUmPOzWNfQwlU,3856
8
8
  pycleaner/cache.py,sha256=WhCiMkuL6ryWVr3l3XrYLLoI7A3bRF8jNxpOyedNW8M,10761
9
- pycleaner/cli.py,sha256=NhMMmLcf7RMuKjc0rjCmSGYn5ns_yib2rUbXRJDVaYY,73034
9
+ pycleaner/cli.py,sha256=MMK0U57pam-FuvB1UL7jwVk7m9Z_Cosuwuu5FIfrPp0,73029
10
10
  pycleaner/complexity_analyzer.py,sha256=uGC40cHCcr1tshzfIUw56sodEDySMFR3kGmjo7nq_1c,14977
11
11
  pycleaner/config.py,sha256=XhloyaY_9pdwbQuc3QaAPCerLeXz-i1da4xT50h-pjY,8514
12
12
  pycleaner/dead_code_detector.py,sha256=1DwtBSZL1ug1OI9-6jcJyxkhqHmJ0F8ZR19POI2zO3c,26549
@@ -18,17 +18,17 @@ pycleaner/linter_formatter.py,sha256=LB5j66OCASU8WG8QgRFpSX7C2c7_UNMBgkJh2hSWxAY
18
18
  pycleaner/modernizer.py,sha256=L9hxXXfdyTwf8itDO58y6bW5LobGo6dg8q537QdiYbM,12946
19
19
  pycleaner/pipeline.py,sha256=ZG7_9PNz0jBadFrZoiI7juLtUJXglGQhLgQhrLJqEac,20612
20
20
  pycleaner/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
- pycleaner/security_scanner.py,sha256=9mAWc8JxcWavwpJP9aYeiDSfzYrH1eZLmeZ54xRouOY,21841
21
+ pycleaner/security_scanner.py,sha256=h7WSPYf40X-PBlJVOsJL-KaknDaOcWCMhcPwXkUvlbA,21986
22
22
  pycleaner/syntax_healer.py,sha256=TlIMKIJhaX9GED3e2JNzNMJ5aXmL9eLJMCu7xKSjdv8,26711
23
23
  pycleaner/taint_engine.py,sha256=96cZdDpSPbo5b13zIZY18f_4GJm_tw0clqCZMBtjXMQ,26481
24
24
  pycleaner/test_generator.py,sha256=TJsNJSojc6eIsaKZkwMlpnb-MKHuDpNLZCPnMlO-a8M,19663
25
25
  pycleaner/type_checker.py,sha256=h09QI_8MRHv83s23myjSYFdMTsWoQ900ffBJHtRuhmE,34087
26
26
  pycleaner/typeshed_resolver.py,sha256=N-kWU_CI5DG59lQMkt7wMW9sHYjNCBdfFS42b4y2Kr4,12742
27
27
  pycleaner/verifier.py,sha256=pOfSLRNHe4qDUYJG8mxDxjY6Ph8QKBdJZmqBtoJX87w,17937
28
- pycleaner/frameworks/__init__.py,sha256=7MLgg469hCNtQR6dsE5gZcAZkCywJHz9zOPrbH8NmrE,3415
29
- pycleaner/frameworks/plugins.py,sha256=K3J4mVJw2MR_pa6Mhmo5oZOX8pUesL8lx9LUo4RLrNo,9390
30
- ballpython-2.0.2.dist-info/METADATA,sha256=tpostFTk54cwmBz56dghrw0fOAZVcsJPKA1N9NMUtZ4,12459
31
- ballpython-2.0.2.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
32
- ballpython-2.0.2.dist-info/entry_points.txt,sha256=6ux-5UxoVSmlK5p9M-GDVt24sCdwTQq9wgRDkpMIYU8,81
33
- ballpython-2.0.2.dist-info/top_level.txt,sha256=u6lmy8RWZ_saxCZAQTL7EijB4sunegOiVyk5fHjpwLk,21
34
- ballpython-2.0.2.dist-info/RECORD,,
28
+ pycleaner/frameworks/__init__.py,sha256=-W24zqvug8eUkkwqla5oh7rFOqQR-WI4TohQhbXGYoo,3472
29
+ pycleaner/frameworks/plugins.py,sha256=sMhP9mZ82sx10OI3opgXIEPXNAWFZRe2I__6yh2Uq_E,11453
30
+ ballpython-2.0.4.dist-info/METADATA,sha256=vIeFxyxog9s1kF_UlbDp4yJg-UCiSho2HQe3qhC4odE,3466
31
+ ballpython-2.0.4.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
32
+ ballpython-2.0.4.dist-info/entry_points.txt,sha256=6ux-5UxoVSmlK5p9M-GDVt24sCdwTQq9wgRDkpMIYU8,81
33
+ ballpython-2.0.4.dist-info/top_level.txt,sha256=u6lmy8RWZ_saxCZAQTL7EijB4sunegOiVyk5fHjpwLk,21
34
+ ballpython-2.0.4.dist-info/RECORD,,
pycleaner/__init__.py CHANGED
@@ -18,7 +18,7 @@ from pycleaner.discovery import (
18
18
  is_protected_file,
19
19
  )
20
20
 
21
- __version__ = "2.0.2"
21
+ __version__ = "2.0.4"
22
22
  __all__ = [
23
23
  "BaselineFingerprint",
24
24
  "BaselineManager",
pycleaner/cli.py CHANGED
@@ -567,7 +567,7 @@ def _resolve_cli_command(args: argparse.Namespace) -> str:
567
567
  return "all"
568
568
  if getattr(args, "check", False):
569
569
  return "check"
570
- return "ultimate"
570
+ return "fix"
571
571
 
572
572
 
573
573
  def _run_all_command(
@@ -103,6 +103,7 @@ def get_default_registry() -> FrameworkRegistry:
103
103
  FastAPIPlugin,
104
104
  PydanticPlugin,
105
105
  PytestPlugin,
106
+ PyTorchPlugin,
106
107
  SQLAlchemyPlugin,
107
108
  )
108
109
 
@@ -112,4 +113,5 @@ def get_default_registry() -> FrameworkRegistry:
112
113
  reg.register(FastAPIPlugin())
113
114
  reg.register(SQLAlchemyPlugin())
114
115
  reg.register(DataclassPlugin())
116
+ reg.register(PyTorchPlugin())
115
117
  return reg
@@ -2,7 +2,7 @@
2
2
  pycleaner.frameworks.plugins
3
3
  ============================
4
4
 
5
- Concrete Framework Plugins for Pydantic, Pytest, FastAPI, SQLAlchemy, and Dataclasses.
5
+ Concrete Framework Plugins for Pydantic, Pytest, FastAPI, SQLAlchemy, Dataclasses, and PyTorch.
6
6
  """
7
7
 
8
8
  from __future__ import annotations
@@ -50,15 +50,18 @@ class PydanticPlugin:
50
50
  for node in ast.walk(tree):
51
51
  if isinstance(node, ast.Import):
52
52
  for alias in node.names:
53
- if alias.name.split(".")[0] == "pydantic":
53
+ if alias.name.split(".")[0] in ("pydantic", "pydantic_settings"):
54
54
  return True
55
55
  elif isinstance(node, ast.ImportFrom):
56
- if node.module and node.module.split(".")[0] == "pydantic":
56
+ if node.module and node.module.split(".")[0] in (
57
+ "pydantic",
58
+ "pydantic_settings",
59
+ ):
57
60
  return True
58
61
  return False
59
62
 
60
63
  def get_protected_names(self, tree: ast.AST) -> set[str]:
61
- return {"model_config", "Config", "ConfigDict"}
64
+ return {"model_config", "Config", "ConfigDict", "model_post_init"}
62
65
 
63
66
  def get_protected_decorators(self) -> set[str]:
64
67
  return self._VALIDATOR_DECORATORS
@@ -288,3 +291,74 @@ class DataclassPlugin:
288
291
  tree: ast.AST,
289
292
  ) -> bool:
290
293
  return name in self.get_protected_names(tree)
294
+
295
+
296
+ class PyTorchPlugin:
297
+ """Understands PyTorch nn.Module, LightningModule, HuggingFace models, and datasets."""
298
+
299
+ name = "pytorch"
300
+ _MODULE_BASES = {
301
+ "Module",
302
+ "LightningModule",
303
+ "PreTrainedModel",
304
+ "Dataset",
305
+ "IterableDataset",
306
+ "Sampler",
307
+ }
308
+ _PROTECTED_METHODS = {
309
+ "forward",
310
+ "backward",
311
+ "training_step",
312
+ "validation_step",
313
+ "test_step",
314
+ "predict_step",
315
+ "configure_optimizers",
316
+ "optimizer_step",
317
+ "compute_loss",
318
+ "reset_parameters",
319
+ "extra_repr",
320
+ "state_dict",
321
+ "load_state_dict",
322
+ "collate_fn",
323
+ }
324
+
325
+ def is_applicable(self, tree: ast.AST, filepath: Path | str) -> bool:
326
+ torch_pkgs = {
327
+ "torch",
328
+ "torchvision",
329
+ "torchaudio",
330
+ "pytorch_lightning",
331
+ "lightning",
332
+ "transformers",
333
+ "accelerate",
334
+ "timm",
335
+ }
336
+ for node in ast.walk(tree):
337
+ if isinstance(node, ast.Import):
338
+ for alias in node.names:
339
+ if alias.name.split(".")[0] in torch_pkgs:
340
+ return True
341
+ elif isinstance(node, ast.ImportFrom):
342
+ if node.module and node.module.split(".")[0] in torch_pkgs:
343
+ return True
344
+ return False
345
+
346
+ def get_protected_names(self, tree: ast.AST) -> set[str]:
347
+ return self._PROTECTED_METHODS
348
+
349
+ def get_protected_decorators(self) -> set[str]:
350
+ return set()
351
+
352
+ def is_protected_field(self, node: ast.AST, class_node: ast.ClassDef) -> bool:
353
+ return _has_base_named(class_node, self._MODULE_BASES)
354
+
355
+ def should_ignore_definition(
356
+ self,
357
+ name: str,
358
+ kind: str,
359
+ node: ast.AST,
360
+ context: str,
361
+ tree: ast.AST,
362
+ ) -> bool:
363
+ return name in self._PROTECTED_METHODS
364
+
@@ -196,9 +196,14 @@ class _DangerousCallDetector(ast.NodeVisitor):
196
196
  return self.source_lines[lineno - 1].strip()
197
197
  return ""
198
198
 
199
- def _is_suppressed(self, lineno: int, category: str) -> bool:
200
- if 1 <= lineno <= len(self.source_lines):
201
- line = self.source_lines[lineno - 1]
199
+ def _is_suppressed(
200
+ self, lineno: int, category: str, end_lineno: int | None = None
201
+ ) -> bool:
202
+ start = max(1, lineno)
203
+ end = max(start, end_lineno or lineno)
204
+ max_line = len(self.source_lines)
205
+ for ln in range(start, min(end + 1, max_line + 1)):
206
+ line = self.source_lines[ln - 1]
202
207
  if "#" in line:
203
208
  comment = line.split("#", 1)[1].strip().lower()
204
209
  if (
@@ -208,21 +213,21 @@ class _DangerousCallDetector(ast.NodeVisitor):
208
213
  or f"ignore[{category.lower()}]" in comment
209
214
  ):
210
215
  return True
211
- if lineno > 1:
212
- prev_line = self.source_lines[lineno - 2]
213
- if prev_line.strip().startswith("#"):
214
- prev_comment = prev_line.strip().lstrip("#").strip().lower()
215
- if (
216
- "nosec" in prev_comment
217
- or "noqa" in prev_comment
218
- or "pycleaner: ignore" in prev_comment
219
- or f"ignore[{category.lower()}]" in prev_comment
220
- ):
221
- return True
216
+ if start > 1:
217
+ prev_line = self.source_lines[start - 2]
218
+ if prev_line.strip().startswith("#"):
219
+ prev_comment = prev_line.strip().lstrip("#").strip().lower()
220
+ if (
221
+ "nosec" in prev_comment
222
+ or "noqa" in prev_comment
223
+ or "pycleaner: ignore" in prev_comment
224
+ or f"ignore[{category.lower()}]" in prev_comment
225
+ ):
226
+ return True
222
227
  return False
223
228
 
224
229
  def _add_finding(self, finding: SecurityFinding) -> None:
225
- if not self._is_suppressed(finding.lineno, finding.category):
230
+ if not self._is_suppressed(finding.lineno, finding.category, finding.end_lineno):
226
231
  self.findings.append(finding)
227
232
 
228
233
  def visit_Call(self, node: ast.Call) -> None:
@@ -1,328 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: ballpython
3
- Version: 2.0.2
4
- Summary: The Ultimate Static Python Intelligence, Healing, Type Verification, and Security Suite
5
- Author: Developer
6
- License-Expression: MIT
7
- Requires-Python: >=3.10
8
- Description-Content-Type: text/markdown
9
- Requires-Dist: ruff>=0.1.0
10
- Requires-Dist: rich>=13.0.0
11
- Requires-Dist: tomli>=1.1.0; python_version < "3.11"
12
- Provides-Extra: dev
13
- Requires-Dist: pytest>=8.0.0; extra == "dev"
14
- Requires-Dist: black>=24.0.0; extra == "dev"
15
- Requires-Dist: isort>=5.13.0; extra == "dev"
16
- Provides-Extra: security
17
- Requires-Dist: cryptography>=41.0.0; extra == "security"
18
- Requires-Dist: defusedxml>=0.7.1; extra == "security"
19
-
20
- # pycleaner — Ultimate Static Python Code Quality Suite
21
-
22
- A production-grade, offline Python static analysis, syntax healing, linting, formatting, security scanning, complexity evaluation, dead code detection, and dependency auditing suite.
23
-
24
- Zero external LLM dependencies, zero mock modes, and built for deterministic developer workflows and CI/CD pipelines.
25
-
26
- ---
27
-
28
- ## Key Capabilities
29
-
30
- 1. **Syntax Healer (`SyntaxHealer`)**:
31
- - Statically repairs missing colons on compound statement headers (`def`, `class`, `if`, `elif`, `else`, `for`, `while`, `try`, `except`, `finally`, `with`, `async def`, `match`, `case`).
32
- - Corrects accidental single `=` assignments in conditionals (`if x = 1:` $\to$ `if x == 1:`) without corrupting keyword arguments (`func(x=1)`), augmented assignments, or walrus expressions (`x := 1`).
33
- - Modernizes legacy Python 2 statements (`print "..."` $\to$ `print(...)`, `except Error, e:` $\to$ `except Error as e:`, and multi-exception tuples `except (E1, E2), e:` on Python 3.12+).
34
- - Automatically closes unclosed parentheses, brackets, and braces.
35
- - Normalizes mixed tab characters into 4 spaces.
36
-
37
- 2. **Missing Import Resolver (`ImportResolver`)**:
38
- - Walks Python AST to extract undefined loaded symbols across local, function, class, and comprehension scopes.
39
- - Resolves undefined identifiers to standard library modules (`os`, `sys`, `json`, `re`, `subprocess`, `pathlib`, etc.), collections, typing constructs, dataclasses, concurrent primitives (`ThreadPoolExecutor`, `ProcessPoolExecutor`, `Lock`, `Queue`), and popular aliases (`np`, `pd`, `plt`, `sns`, `tf`, `nn`).
40
- - **`TYPE_CHECKING` Awareness**: Injects typing-only imports into `if TYPE_CHECKING:` guards and adds `from __future__ import annotations` when symbols are used strictly in type annotations.
41
- - **Custom Import Mapping**: Configurable mapping from symbol names to exact import statements.
42
-
43
- 3. **Linter & Formatter (`LinterFormatter`)**:
44
- - Integrates Rust-based `ruff` via in-memory stream processing (`--stdin-filename`).
45
- - Automatically removes unused imports (`F401`) and unused variables (`F841`).
46
- - Sorts and groups imports via `isort` / `ruff` or an internal pure-Python 3-tier sorter (stdlib $\to$ third-party $\to$ local).
47
- - Modernizes deprecated syntax via `pyupgrade` (`UP`).
48
- - Applies deterministic PEP 8 formatting (`ruff format` / `black` / pure-Python formatter).
49
- - Wraps long `from ... import (...)` statements exceeding configured `line-length`.
50
- - Modernizes legacy typing to PEP 585/604 syntax (`List[T]` -> `list[T]`, `Optional[T]` -> `T | None`), singleton comparisons, and `class X(object)` inheritance via `Modernizer` (disable with `--no-modernize`).
51
-
52
- 4. **Dead Code Detector (`DeadCodeDetector`)**:
53
- - Builds a cross-file symbol definition and reference graph across a project.
54
- - Identifies unused functions, methods, classes, module-level constants, and class attributes.
55
- - Respects public exports in `__all__` and framework decorators (`@app.route`, `@pytest.fixture`, `@abstractmethod`, etc.).
56
- - Detects unreachable code following unconditional `return`, `raise`, `break`, `continue`, or `sys.exit()`.
57
- - Identifies empty pass blocks with no comments.
58
- - Auto-prunes unreachable code, redundant `pass` statements, and `if False:` branches via `DeadCodeFixer` (enable with `dead-code --fix`, disable in the pipeline with `--no-dead-code`).
59
-
60
- 5. **Static Security Scanner (`SecurityScanner`)**:
61
- - Detects dangerous function calls: `eval()`, `exec()`, `compile()`, `pickle.loads()`, `os.system()`, and unsafe `yaml.load()` lacking `SafeLoader`.
62
- - Flags SQL injection patterns (string concatenation, f-strings, `%`-formatting, or `.format()` inside `cursor.execute()`).
63
- - Flags subprocess command injection (`shell=True`).
64
- - Flags insecure transport (`verify=False` in `requests` or `httpx`).
65
- - Scans for hardcoded secrets: AWS access/secret keys, GitHub tokens, Slack tokens, JWT tokens, private key headers, and generic passwords/API keys.
66
- - Flags production `assert` statements used for input validation (which get stripped under `python -O`).
67
-
68
- 6. **Complexity Analyzer (`ComplexityAnalyzer`)**:
69
- - McCabe Cyclomatic Complexity per function ($E - N + 2$).
70
- - Sonar-style Cognitive Complexity scoring (penalizing nested control flow, compound boolean conditions, and break in linear flow).
71
- - Metrics tracked: line count, argument count, return statement count, and maximum nesting depth.
72
- - Configurable thresholds with tabular and JSON reporting.
73
-
74
- 7. **Dependency Auditor (`DependencyAuditor`)**:
75
- - Statically scans all `.py` files across a repository to discover external third-party imports.
76
- - Maps module import names to PyPI distribution package names (e.g., `yaml` $\to$ `PyYAML`, `PIL` $\to$ `pillow`, `cv2` $\to$ `opencv-python`).
77
- - Compares imported dependencies against `requirements.txt` and `pyproject.toml`.
78
- - Synchronizes `requirements.txt` with `--fix-deps` and prunes unused packages with `--prune-deps`.
79
-
80
- 8. **Safety & Concurrency**:
81
- - Backup creation (`.pycleaner.bak`) before overwriting files (enabled by default).
82
- - Parallel multi-core file processing via `ProcessPoolExecutor`.
83
- - Continuous file watching (`watch` mode) with automatic re-healing on save.
84
- - Pre-commit configuration generator (`hook` subcommand).
85
-
86
- ---
87
-
88
- ## Installation
89
-
90
- ```bash
91
- # Editable install
92
- pip install -e .
93
-
94
- # Core dependencies
95
- pip install ruff rich
96
-
97
- # Optional security and development packages
98
- pip install -e ".[dev,security]"
99
- ```
100
-
101
- ---
102
-
103
- ## CLI Usage
104
-
105
- ### Subcommands
106
-
107
- #### `fix` (Default Action)
108
- Heals syntax, modernizes legacy typing (PEP 585/604), prunes dead code, resolves imports, fixes lint violations, and formats code:
109
- ```bash
110
- py -m pycleaner fix src/
111
- py -m pycleaner fix --diff path/to/script.py
112
- py -m pycleaner fix --no-backup src/
113
- py -m pycleaner fix --parallel --workers 4 src/
114
- py -m pycleaner fix --no-modernize --no-dead-code src/
115
- ```
116
-
117
- #### `check` (Dry-Run CI Verification)
118
- Inspects files without writing modifications to disk. Exits with code `1` if changes or errors are detected:
119
- ```bash
120
- py -m pycleaner check src/
121
- py -m pycleaner check --diff src/
122
- ```
123
-
124
- #### `scan` (Security Vulnerability Audit)
125
- Scans Python files for hardcoded secrets, dangerous calls, and injection vulnerabilities:
126
- ```bash
127
- py -m pycleaner scan .
128
- py -m pycleaner scan --severity HIGH .
129
- py -m pycleaner scan --json .
130
- ```
131
-
132
- #### `complexity` (Function Complexity Metrics)
133
- Calculates Cyclomatic Complexity, Cognitive Complexity, and architectural metrics:
134
- ```bash
135
- py -m pycleaner complexity .
136
- py -m pycleaner complexity --max-cyclomatic 10 --max-cognitive 15 .
137
- py -m pycleaner complexity --json .
138
- ```
139
-
140
- #### `dead-code` (Unused Symbols & Unreachable Branches)
141
- Finds unused functions, unused classes, empty pass branches, and dead code:
142
- ```bash
143
- py -m pycleaner dead-code .
144
- py -m pycleaner dead-code --json .
145
- py -m pycleaner dead-code --fix .
146
- ```
147
-
148
- #### `audit` (Project Dependency Verification)
149
- Audits imported third-party libraries against `requirements.txt`:
150
- ```bash
151
- # Audit only
152
- py -m pycleaner audit .
153
-
154
- # Automatically append missing dependencies
155
- py -m pycleaner audit --fix-deps .
156
-
157
- # Append missing and remove unimported dependencies
158
- py -m pycleaner audit --fix-deps --prune-deps .
159
- ```
160
-
161
- #### `all` (Complete Quality Sweep)
162
- Executes all engines in one command (fix + dependency audit + security scan + complexity + dead code):
163
- ```bash
164
- py -m pycleaner all .
165
- ```
166
-
167
- #### `watch` (Continuous File Watcher)
168
- Watches files for filesystem modifications and auto-cleans on save:
169
- ```bash
170
- py -m pycleaner watch src/ --interval 1.0
171
- ```
172
-
173
- #### `hook` (Pre-Commit Integration)
174
- Outputs a ready-to-use `.pre-commit-hooks.yaml` configuration block:
175
- ```bash
176
- py -m pycleaner hook
177
- ```
178
-
179
- ---
180
-
181
- ### Backward-Compatible Flat Invocations
182
-
183
- Legacy invocations continue to work seamlessly:
184
- ```bash
185
- pycleaner -a . # Equivalent to: pycleaner all .
186
- pycleaner --check --diff src/ # Equivalent to: pycleaner check --diff src/
187
- pycleaner --deps-only . # Equivalent to: pycleaner audit .
188
- pycleaner path/to/file.py # Equivalent to: pycleaner fix path/to/file.py
189
- ```
190
-
191
- ---
192
-
193
- ## Configuration
194
-
195
- `pycleaner` automatically reads configuration from `pyproject.toml` under `[tool.pycleaner]` or from `.pycleaner.toml`. An explicit file can be forced with `pycleaner --config path/to/pyproject.toml <command> ...`, which overrides target-path auto-discovery.
196
-
197
- ### `pyproject.toml` Example
198
-
199
- ```toml
200
- [tool.pycleaner]
201
- # File targeting
202
- exclude = ["migrations/", "generated/", "*_pb2.py", "*_pb2_grpc.py"]
203
- include = ["src/", "tests/"]
204
-
205
- # Syntax healing
206
- fix-py2-syntax = true
207
- fix-conditional-assignments = true
208
-
209
- # Import resolution
210
- custom-import-map = { "logger" = "from myapp.core.logging import logger" }
211
- auto-add-future-annotations = false
212
-
213
- # Linting & formatting
214
- line-length = 88
215
- select-rules = "F401,F841,I,UP,E,W,B,SIM,RUF"
216
-
217
- # Dead code
218
- ignore-decorators = ["@app.route", "@pytest.fixture", "@override"]
219
- ignore-names = ["_*", "test_*"]
220
-
221
- # Security
222
- security-severity-threshold = "LOW"
223
- ignore-security-rules = []
224
-
225
- # Complexity thresholds
226
- max-cyclomatic-complexity = 10
227
- max-cognitive-complexity = 15
228
- max-function-length = 50
229
- max-arguments = 5
230
-
231
- # Behavior
232
- backup = true
233
- parallel = false
234
- max-workers = 4
235
- ```
236
-
237
- ---
238
-
239
- ## Programmatic API
240
-
241
- ### 1. Cleaning Source Code or Files
242
- ```python
243
- from pathlib import Path
244
- from pycleaner import CleanPipeline, load_config
245
-
246
- # Load config with project overrides
247
- config = load_config(project_root=".")
248
- pipeline = CleanPipeline(config=config)
249
-
250
- # In-memory code processing
251
- dirty_code = """
252
- import math
253
-
254
- def calculate(x)
255
- if x = 0
256
- return Path('.')
257
- return Path(str(x))
258
- """
259
- result = pipeline.process_source(dirty_code, filename="example.py")
260
- print("Cleaned code:\n", result.cleaned_code)
261
- print("Repairs applied:", result.syntax_repairs)
262
- print("Imports added:", result.resolved_imports)
263
-
264
- # File processing with automatic backup
265
- file_result = pipeline.process_file("example.py", apply_changes=True, backup=True)
266
- ```
267
-
268
- ### 2. Security Vulnerability Scanning
269
- ```python
270
- from pycleaner import SecurityScanner
271
-
272
- scanner = SecurityScanner(severity_threshold="MEDIUM")
273
- report = scanner.scan_project("src/")
274
-
275
- for finding in report.findings:
276
- print(f"[{finding.severity}] {finding.category} at {finding.filepath}:{finding.lineno}")
277
- print(f" Message: {finding.message}")
278
- print(f" Fix: {finding.suggestion}")
279
- ```
280
-
281
- ### 3. Complexity Analysis
282
- ```python
283
- from pycleaner import ComplexityAnalyzer
284
-
285
- analyzer = ComplexityAnalyzer()
286
- report = analyzer.analyze_project("src/")
287
-
288
- print(f"Scanned {report.files_scanned} files, {report.count} functions.")
289
- print(f"Average Cyclomatic: {report.average_cyclomatic:.2f}")
290
-
291
- violations = report.above_threshold(max_cyclomatic=10, max_cognitive=15)
292
- for func in violations:
293
- print(f"{func.qualified_name} (CC={func.cyclomatic}, Cog={func.cognitive})")
294
- ```
295
-
296
- ### 4. Dead Code Detection
297
- ```python
298
- from pycleaner import DeadCodeDetector
299
-
300
- detector = DeadCodeDetector()
301
- report = detector.scan_project("src/")
302
-
303
- print(f"{report.count} dead code item(s) across {report.files_scanned} file(s).")
304
-
305
- for item in report.items:
306
- print(f"Dead {item.kind} '{item.name}' at {item.filepath}:{item.lineno}")
307
-
308
- for item in report.by_kind("unreachable"):
309
- print(f"Unreachable code at {item.filepath}:{item.lineno} ({item.reason})")
310
- ```
311
-
312
- ---
313
-
314
- ## Exit Codes
315
-
316
- | Exit Code | Meaning |
317
- |:---:|:---|
318
- | `0` | Clean run: all files valid, no errors, or changes cleanly written |
319
- | `1` | Issues found: check mode detected modifications, errors occurred, or critical security findings flagged |
320
- | `2` | Configuration error: invalid TOML syntax, invalid configuration keys, or mismatched types |
321
-
322
- ---
323
-
324
- ## License
325
-
326
- MIT License.
327
- # ball-python
328
- # ball-python