ballpython 2.0.3__tar.gz → 2.0.5__tar.gz

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 (62) hide show
  1. {ballpython-2.0.3 → ballpython-2.0.5}/PKG-INFO +2 -2
  2. {ballpython-2.0.3 → ballpython-2.0.5}/README.md +1 -1
  3. {ballpython-2.0.3 → ballpython-2.0.5}/ballpython.egg-info/PKG-INFO +2 -2
  4. {ballpython-2.0.3 → ballpython-2.0.5}/pycleaner/__init__.py +1 -1
  5. {ballpython-2.0.3 → ballpython-2.0.5}/pycleaner/cli.py +1 -1
  6. {ballpython-2.0.3 → ballpython-2.0.5}/pycleaner/frameworks/__init__.py +2 -0
  7. {ballpython-2.0.3 → ballpython-2.0.5}/pycleaner/frameworks/plugins.py +78 -4
  8. {ballpython-2.0.3 → ballpython-2.0.5}/pycleaner/security_scanner.py +37 -2
  9. {ballpython-2.0.3 → ballpython-2.0.5}/pyproject.toml +2 -2
  10. {ballpython-2.0.3 → ballpython-2.0.5}/tests/test_cli.py +18 -0
  11. {ballpython-2.0.3 → ballpython-2.0.5}/tests/test_frameworks.py +51 -0
  12. {ballpython-2.0.3 → ballpython-2.0.5}/tests/test_security_scanner.py +26 -0
  13. {ballpython-2.0.3 → ballpython-2.0.5}/ballpython/__init__.py +0 -0
  14. {ballpython-2.0.3 → ballpython-2.0.5}/ballpython/__main__.py +0 -0
  15. {ballpython-2.0.3 → ballpython-2.0.5}/ballpython/cli.py +0 -0
  16. {ballpython-2.0.3 → ballpython-2.0.5}/ballpython/py.typed +0 -0
  17. {ballpython-2.0.3 → ballpython-2.0.5}/ballpython.egg-info/SOURCES.txt +0 -0
  18. {ballpython-2.0.3 → ballpython-2.0.5}/ballpython.egg-info/dependency_links.txt +0 -0
  19. {ballpython-2.0.3 → ballpython-2.0.5}/ballpython.egg-info/entry_points.txt +0 -0
  20. {ballpython-2.0.3 → ballpython-2.0.5}/ballpython.egg-info/requires.txt +0 -0
  21. {ballpython-2.0.3 → ballpython-2.0.5}/ballpython.egg-info/top_level.txt +0 -0
  22. {ballpython-2.0.3 → ballpython-2.0.5}/pycleaner/__main__.py +0 -0
  23. {ballpython-2.0.3 → ballpython-2.0.5}/pycleaner/baseline.py +0 -0
  24. {ballpython-2.0.3 → ballpython-2.0.5}/pycleaner/cache.py +0 -0
  25. {ballpython-2.0.3 → ballpython-2.0.5}/pycleaner/complexity_analyzer.py +0 -0
  26. {ballpython-2.0.3 → ballpython-2.0.5}/pycleaner/config.py +0 -0
  27. {ballpython-2.0.3 → ballpython-2.0.5}/pycleaner/dead_code_detector.py +0 -0
  28. {ballpython-2.0.3 → ballpython-2.0.5}/pycleaner/dependency_auditor.py +0 -0
  29. {ballpython-2.0.3 → ballpython-2.0.5}/pycleaner/discovery.py +0 -0
  30. {ballpython-2.0.3 → ballpython-2.0.5}/pycleaner/explanations.py +0 -0
  31. {ballpython-2.0.3 → ballpython-2.0.5}/pycleaner/import_resolver.py +0 -0
  32. {ballpython-2.0.3 → ballpython-2.0.5}/pycleaner/linter_formatter.py +0 -0
  33. {ballpython-2.0.3 → ballpython-2.0.5}/pycleaner/modernizer.py +0 -0
  34. {ballpython-2.0.3 → ballpython-2.0.5}/pycleaner/pipeline.py +0 -0
  35. {ballpython-2.0.3 → ballpython-2.0.5}/pycleaner/py.typed +0 -0
  36. {ballpython-2.0.3 → ballpython-2.0.5}/pycleaner/syntax_healer.py +0 -0
  37. {ballpython-2.0.3 → ballpython-2.0.5}/pycleaner/taint_engine.py +0 -0
  38. {ballpython-2.0.3 → ballpython-2.0.5}/pycleaner/test_generator.py +0 -0
  39. {ballpython-2.0.3 → ballpython-2.0.5}/pycleaner/type_checker.py +0 -0
  40. {ballpython-2.0.3 → ballpython-2.0.5}/pycleaner/typeshed_resolver.py +0 -0
  41. {ballpython-2.0.3 → ballpython-2.0.5}/pycleaner/verifier.py +0 -0
  42. {ballpython-2.0.3 → ballpython-2.0.5}/setup.cfg +0 -0
  43. {ballpython-2.0.3 → ballpython-2.0.5}/tests/test_baseline_and_explain.py +0 -0
  44. {ballpython-2.0.3 → ballpython-2.0.5}/tests/test_cache.py +0 -0
  45. {ballpython-2.0.3 → ballpython-2.0.5}/tests/test_cli_prove.py +0 -0
  46. {ballpython-2.0.3 → ballpython-2.0.5}/tests/test_complexity_analyzer.py +0 -0
  47. {ballpython-2.0.3 → ballpython-2.0.5}/tests/test_config.py +0 -0
  48. {ballpython-2.0.3 → ballpython-2.0.5}/tests/test_dead_code_detector.py +0 -0
  49. {ballpython-2.0.3 → ballpython-2.0.5}/tests/test_dead_code_fixer.py +0 -0
  50. {ballpython-2.0.3 → ballpython-2.0.5}/tests/test_dependency_auditor.py +0 -0
  51. {ballpython-2.0.3 → ballpython-2.0.5}/tests/test_discovery.py +0 -0
  52. {ballpython-2.0.3 → ballpython-2.0.5}/tests/test_import_resolver.py +0 -0
  53. {ballpython-2.0.3 → ballpython-2.0.5}/tests/test_linter_formatter.py +0 -0
  54. {ballpython-2.0.3 → ballpython-2.0.5}/tests/test_modernizer.py +0 -0
  55. {ballpython-2.0.3 → ballpython-2.0.5}/tests/test_mutation_test_generator.py +0 -0
  56. {ballpython-2.0.3 → ballpython-2.0.5}/tests/test_pipeline.py +0 -0
  57. {ballpython-2.0.3 → ballpython-2.0.5}/tests/test_syntax_healer.py +0 -0
  58. {ballpython-2.0.3 → ballpython-2.0.5}/tests/test_taint_engine.py +0 -0
  59. {ballpython-2.0.3 → ballpython-2.0.5}/tests/test_test_generator.py +0 -0
  60. {ballpython-2.0.3 → ballpython-2.0.5}/tests/test_type_checker.py +0 -0
  61. {ballpython-2.0.3 → ballpython-2.0.5}/tests/test_verifier.py +0 -0
  62. {ballpython-2.0.3 → ballpython-2.0.5}/tests/test_verifier_pipeline.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ballpython
3
- Version: 2.0.3
3
+ Version: 2.0.5
4
4
  Summary: The Ultimate Static Python Intelligence, Healing, Type Verification, and Security Suite
5
5
  Author: Developer
6
6
  License-Expression: MIT
@@ -78,7 +78,7 @@ Add `ballpython` to your `.pre-commit-config.yaml`:
78
78
  ```yaml
79
79
  repos:
80
80
  - repo: https://github.com/moonrox420/ball-python
81
- rev: v2.0.3
81
+ rev: v2.0.5
82
82
  hooks:
83
83
  - id: pycleaner
84
84
  args: ["check"]
@@ -59,7 +59,7 @@ Add `ballpython` to your `.pre-commit-config.yaml`:
59
59
  ```yaml
60
60
  repos:
61
61
  - repo: https://github.com/moonrox420/ball-python
62
- rev: v2.0.3
62
+ rev: v2.0.5
63
63
  hooks:
64
64
  - id: pycleaner
65
65
  args: ["check"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ballpython
3
- Version: 2.0.3
3
+ Version: 2.0.5
4
4
  Summary: The Ultimate Static Python Intelligence, Healing, Type Verification, and Security Suite
5
5
  Author: Developer
6
6
  License-Expression: MIT
@@ -78,7 +78,7 @@ Add `ballpython` to your `.pre-commit-config.yaml`:
78
78
  ```yaml
79
79
  repos:
80
80
  - repo: https://github.com/moonrox420/ball-python
81
- rev: v2.0.3
81
+ rev: v2.0.5
82
82
  hooks:
83
83
  - id: pycleaner
84
84
  args: ["check"]
@@ -18,7 +18,7 @@ from pycleaner.discovery import (
18
18
  is_protected_file,
19
19
  )
20
20
 
21
- __version__ = "2.0.3"
21
+ __version__ = "2.0.5"
22
22
  __all__ = [
23
23
  "BaselineFingerprint",
24
24
  "BaselineManager",
@@ -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
+
@@ -190,6 +190,24 @@ class _DangerousCallDetector(ast.NodeVisitor):
190
190
  self.filepath = filepath
191
191
  self.source_lines = source_lines
192
192
  self.findings: list[SecurityFinding] = []
193
+ self._is_test_module: bool = False
194
+
195
+ def check_tree(self, tree: ast.AST) -> None:
196
+ """Inspect imports to determine if module is a test file."""
197
+ for node in ast.walk(tree):
198
+ if isinstance(node, ast.Import):
199
+ for alias in node.names:
200
+ if alias.name.split(".")[0] in ("pytest", "unittest", "mock"):
201
+ self._is_test_module = True
202
+ return
203
+ elif isinstance(node, ast.ImportFrom):
204
+ if node.module and node.module.split(".")[0] in (
205
+ "pytest",
206
+ "unittest",
207
+ "mock",
208
+ ):
209
+ self._is_test_module = True
210
+ return
193
211
 
194
212
  def _get_snippet(self, lineno: int) -> str:
195
213
  if 1 <= lineno <= len(self.source_lines):
@@ -245,8 +263,11 @@ class _DangerousCallDetector(ast.NodeVisitor):
245
263
  parts = [p.lower() for p in Path(filepath).parts[:-1]]
246
264
  return any(p in ("tests", "test", "testing") for p in parts)
247
265
 
266
+ def _is_test(self) -> bool:
267
+ return self._is_test_module or self._is_test_path(self.filepath)
268
+
248
269
  def visit_Assert(self, node: ast.Assert) -> None:
249
- if self._is_test_path(self.filepath):
270
+ if self._is_test():
250
271
  return
251
272
  self._add_finding(
252
273
  SecurityFinding(
@@ -429,7 +450,20 @@ class _DangerousCallDetector(ast.NodeVisitor):
429
450
  if isinstance(arg, ast.BinOp) and isinstance(arg.op, ast.Add):
430
451
  return True
431
452
  if isinstance(arg, ast.Call):
432
- return isinstance(arg.func, ast.Attribute) and arg.func.attr == "format"
453
+ if isinstance(arg.func, ast.Attribute) and arg.func.attr == "format":
454
+ # Check for psycopg safe sql.SQL(...).format(...) query composition
455
+ val = arg.func.value
456
+ if isinstance(val, ast.Call):
457
+ func = val.func
458
+ func_id = ""
459
+ if isinstance(func, ast.Name):
460
+ func_id = func.id
461
+ elif isinstance(func, ast.Attribute):
462
+ func_id = func.attr
463
+ if func_id == "SQL":
464
+ return False
465
+ return True
466
+ return False
433
467
  if isinstance(arg, ast.BinOp) and isinstance(arg.op, ast.Mod):
434
468
  return isinstance(arg.left, ast.Constant) and isinstance(
435
469
  arg.left.value, str
@@ -483,6 +517,7 @@ class SecurityScanner:
483
517
  tree = ast.parse(source, filename=filename)
484
518
  source_lines = source.splitlines()
485
519
  detector = _DangerousCallDetector(filename, source_lines)
520
+ detector.check_tree(tree)
486
521
  detector.visit(tree)
487
522
  findings.extend(detector.findings)
488
523
  except SyntaxError:
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ballpython"
7
- version = "2.0.3"
7
+ version = "2.0.5"
8
8
  description = "The Ultimate Static Python Intelligence, Healing, Type Verification, and Security Suite"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -44,7 +44,7 @@ ballpython = ["py.typed"]
44
44
  [tool.pytest.ini_options]
45
45
  testpaths = ["tests"]
46
46
  python_files = ["test_*.py"]
47
- addopts = "-p no:pytestqt --basetemp=.pytest_temp"
47
+ addopts = "-p no:pytestqt"
48
48
 
49
49
  [tool.pycleaner]
50
50
  exclude = [
@@ -357,6 +357,24 @@ class TestCLI(unittest.TestCase):
357
357
  self.assertIn("types: [python]", output)
358
358
  self.assertIn("https://github.com/moonrox420/ball-python", output)
359
359
 
360
+ def test_default_invocation_runs_fix_not_ultimate(self) -> None:
361
+ with tempfile.TemporaryDirectory() as tmp_dir:
362
+ folder = Path(tmp_dir)
363
+ target = folder / "app.py"
364
+ target.write_text("def run(x)\n return x\n", encoding="utf-8")
365
+
366
+ stdout_buf = io.StringIO()
367
+ with redirect_stdout(stdout_buf):
368
+ exit_code = main([str(folder)])
369
+ output = stdout_buf.getvalue()
370
+
371
+ self.assertEqual(exit_code, 0)
372
+ self.assertIn("def run(x):", target.read_text(encoding="utf-8"))
373
+ # Must NOT invoke 6-phase analytical ultimate runner
374
+ self.assertNotIn("Full Spectrum Analysis & Healing", output)
375
+ self.assertNotIn("Phase 2: Project Dependency Audit", output)
376
+
360
377
 
361
378
  if __name__ == "__main__":
362
379
  unittest.main()
380
+
@@ -131,3 +131,54 @@ class Config:
131
131
  assert "timeout" not in names_flagged
132
132
  assert "retries" not in names_flagged
133
133
  assert "__post_init__" not in names_flagged
134
+
135
+
136
+ def test_pytorch_model_methods_not_flagged(tmp_path: Path) -> None:
137
+ code = """
138
+ import torch
139
+ import torch.nn as nn
140
+
141
+ class TransformerModel(nn.Module):
142
+ def __init__(self):
143
+ super().__init__()
144
+ self.linear = nn.Linear(10, 10)
145
+
146
+ def forward(self, x):
147
+ return self.linear(x)
148
+
149
+ def compute_loss(self, pred, target):
150
+ return (pred - target).sum()
151
+ """
152
+ f = tmp_path / "model.py"
153
+ f.write_text(code, encoding="utf-8")
154
+
155
+ detector = DeadCodeDetector()
156
+ report = detector.scan_project(tmp_path)
157
+
158
+ names_flagged = {item.name for item in report.items}
159
+ assert "forward" not in names_flagged
160
+ assert "compute_loss" not in names_flagged
161
+
162
+
163
+ def test_pydantic_settings_and_model_post_init_not_flagged(tmp_path: Path) -> None:
164
+ code = """
165
+ from pydantic_settings import BaseSettings, SettingsConfigDict
166
+
167
+ class AppConfig(BaseSettings):
168
+ api_key: str = "secret"
169
+ model_config = SettingsConfigDict(env_file=".env")
170
+
171
+ def model_post_init(self, context) -> None:
172
+ pass
173
+ """
174
+ f = tmp_path / "settings.py"
175
+ f.write_text(code, encoding="utf-8")
176
+
177
+ detector = DeadCodeDetector()
178
+ report = detector.scan_project(tmp_path)
179
+
180
+ names_flagged = {item.name for item in report.items}
181
+ assert "api_key" not in names_flagged
182
+ assert "model_config" not in names_flagged
183
+ assert "model_post_init" not in names_flagged
184
+
@@ -210,3 +210,29 @@ class TestSecurityScanner:
210
210
  def test_security_report_by_category(self) -> None:
211
211
  report = SecurityReport(findings=[])
212
212
  assert len(report.by_category("dangerous-eval")) == 0
213
+
214
+ def test_assert_suppressed_in_module_importing_pytest(self) -> None:
215
+ source = textwrap.dedent("""
216
+ import pytest
217
+
218
+ def test_feature():
219
+ assert 1 + 1 == 2
220
+ """)
221
+ scanner = SecurityScanner(severity_threshold="LOW")
222
+ report = scanner.scan_source(source, filename="test_root_file.py")
223
+ categories = {f.category for f in report.findings}
224
+ assert "assert-in-production" not in categories
225
+
226
+ def test_safe_sql_composition_not_flagged(self) -> None:
227
+ source = textwrap.dedent("""
228
+ from psycopg import sql
229
+
230
+ def get_data(conn, tbl):
231
+ with conn.cursor() as cur:
232
+ cur.execute(sql.SQL("SELECT count(*) FROM {}").format(sql.Identifier(tbl)))
233
+ """)
234
+ scanner = SecurityScanner()
235
+ report = scanner.scan_source(source, filename="query.py")
236
+ categories = {f.category for f in report.findings}
237
+ assert "sql-injection" not in categories
238
+
File without changes
File without changes