ballpython 2.0.3__py3-none-any.whl → 2.0.5__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.
@@ -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"]
@@ -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=dK4jaytHQuXqaK3pqum6o8z8JrRLU-XHUfv_NBOtdxs,2894
5
+ pycleaner/__init__.py,sha256=G8pQ2M9iH32OK4bKf5Koao554EdGiiHue5rdZ5AS7sM,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=h7WSPYf40X-PBlJVOsJL-KaknDaOcWCMhcPwXkUvlbA,21986
21
+ pycleaner/security_scanner.py,sha256=cWf4cAGQrgwsGslSQDfZv0XUN3sWvlcOIQaogpmv6C0,23422
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.3.dist-info/METADATA,sha256=K1nMS6fIKnHpTsJ0mRLaKNv_o1TFuzb7SI_KgywGCaw,3466
31
- ballpython-2.0.3.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
32
- ballpython-2.0.3.dist-info/entry_points.txt,sha256=6ux-5UxoVSmlK5p9M-GDVt24sCdwTQq9wgRDkpMIYU8,81
33
- ballpython-2.0.3.dist-info/top_level.txt,sha256=u6lmy8RWZ_saxCZAQTL7EijB4sunegOiVyk5fHjpwLk,21
34
- ballpython-2.0.3.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.5.dist-info/METADATA,sha256=LqjBIVBjanDL5rdXJ7gES5vF4ezSqbG2Y1SKMG6rXx4,3466
31
+ ballpython-2.0.5.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
32
+ ballpython-2.0.5.dist-info/entry_points.txt,sha256=6ux-5UxoVSmlK5p9M-GDVt24sCdwTQq9wgRDkpMIYU8,81
33
+ ballpython-2.0.5.dist-info/top_level.txt,sha256=u6lmy8RWZ_saxCZAQTL7EijB4sunegOiVyk5fHjpwLk,21
34
+ ballpython-2.0.5.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.3"
21
+ __version__ = "2.0.5"
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
+
@@ -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: