ballpython 2.1.1__tar.gz → 2.1.2__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 (63) hide show
  1. {ballpython-2.1.1 → ballpython-2.1.2}/PKG-INFO +1 -1
  2. {ballpython-2.1.1 → ballpython-2.1.2}/ballpython.egg-info/PKG-INFO +1 -1
  3. {ballpython-2.1.1 → ballpython-2.1.2}/pycleaner/cli.py +14 -1
  4. {ballpython-2.1.1 → ballpython-2.1.2}/pycleaner/dead_code_detector.py +38 -8
  5. {ballpython-2.1.1 → ballpython-2.1.2}/pycleaner/frameworks/__init__.py +8 -1
  6. {ballpython-2.1.1 → ballpython-2.1.2}/pycleaner/linter_formatter.py +23 -4
  7. {ballpython-2.1.1 → ballpython-2.1.2}/pyproject.toml +1 -1
  8. {ballpython-2.1.1 → ballpython-2.1.2}/tests/test_dead_code_detector.py +22 -0
  9. {ballpython-2.1.1 → ballpython-2.1.2}/README.md +0 -0
  10. {ballpython-2.1.1 → ballpython-2.1.2}/ballpython/__init__.py +0 -0
  11. {ballpython-2.1.1 → ballpython-2.1.2}/ballpython/__main__.py +0 -0
  12. {ballpython-2.1.1 → ballpython-2.1.2}/ballpython/cli.py +0 -0
  13. {ballpython-2.1.1 → ballpython-2.1.2}/ballpython/py.typed +0 -0
  14. {ballpython-2.1.1 → ballpython-2.1.2}/ballpython.egg-info/SOURCES.txt +0 -0
  15. {ballpython-2.1.1 → ballpython-2.1.2}/ballpython.egg-info/dependency_links.txt +0 -0
  16. {ballpython-2.1.1 → ballpython-2.1.2}/ballpython.egg-info/entry_points.txt +0 -0
  17. {ballpython-2.1.1 → ballpython-2.1.2}/ballpython.egg-info/requires.txt +0 -0
  18. {ballpython-2.1.1 → ballpython-2.1.2}/ballpython.egg-info/top_level.txt +0 -0
  19. {ballpython-2.1.1 → ballpython-2.1.2}/pycleaner/__init__.py +0 -0
  20. {ballpython-2.1.1 → ballpython-2.1.2}/pycleaner/__main__.py +0 -0
  21. {ballpython-2.1.1 → ballpython-2.1.2}/pycleaner/baseline.py +0 -0
  22. {ballpython-2.1.1 → ballpython-2.1.2}/pycleaner/cache.py +0 -0
  23. {ballpython-2.1.1 → ballpython-2.1.2}/pycleaner/complexity_analyzer.py +0 -0
  24. {ballpython-2.1.1 → ballpython-2.1.2}/pycleaner/config.py +0 -0
  25. {ballpython-2.1.1 → ballpython-2.1.2}/pycleaner/dependency_auditor.py +0 -0
  26. {ballpython-2.1.1 → ballpython-2.1.2}/pycleaner/discovery.py +0 -0
  27. {ballpython-2.1.1 → ballpython-2.1.2}/pycleaner/explanations.py +0 -0
  28. {ballpython-2.1.1 → ballpython-2.1.2}/pycleaner/frameworks/plugins.py +0 -0
  29. {ballpython-2.1.1 → ballpython-2.1.2}/pycleaner/import_resolver.py +0 -0
  30. {ballpython-2.1.1 → ballpython-2.1.2}/pycleaner/modernizer.py +0 -0
  31. {ballpython-2.1.1 → ballpython-2.1.2}/pycleaner/pipeline.py +0 -0
  32. {ballpython-2.1.1 → ballpython-2.1.2}/pycleaner/py.typed +0 -0
  33. {ballpython-2.1.1 → ballpython-2.1.2}/pycleaner/security_scanner.py +0 -0
  34. {ballpython-2.1.1 → ballpython-2.1.2}/pycleaner/syntax_healer.py +0 -0
  35. {ballpython-2.1.1 → ballpython-2.1.2}/pycleaner/taint_engine.py +0 -0
  36. {ballpython-2.1.1 → ballpython-2.1.2}/pycleaner/test_generator.py +0 -0
  37. {ballpython-2.1.1 → ballpython-2.1.2}/pycleaner/type_checker.py +0 -0
  38. {ballpython-2.1.1 → ballpython-2.1.2}/pycleaner/typeshed_resolver.py +0 -0
  39. {ballpython-2.1.1 → ballpython-2.1.2}/pycleaner/verifier.py +0 -0
  40. {ballpython-2.1.1 → ballpython-2.1.2}/setup.cfg +0 -0
  41. {ballpython-2.1.1 → ballpython-2.1.2}/tests/test_baseline_and_explain.py +0 -0
  42. {ballpython-2.1.1 → ballpython-2.1.2}/tests/test_c_to_a_overhaul.py +0 -0
  43. {ballpython-2.1.1 → ballpython-2.1.2}/tests/test_cache.py +0 -0
  44. {ballpython-2.1.1 → ballpython-2.1.2}/tests/test_cli.py +0 -0
  45. {ballpython-2.1.1 → ballpython-2.1.2}/tests/test_cli_prove.py +0 -0
  46. {ballpython-2.1.1 → ballpython-2.1.2}/tests/test_complexity_analyzer.py +0 -0
  47. {ballpython-2.1.1 → ballpython-2.1.2}/tests/test_config.py +0 -0
  48. {ballpython-2.1.1 → ballpython-2.1.2}/tests/test_dead_code_fixer.py +0 -0
  49. {ballpython-2.1.1 → ballpython-2.1.2}/tests/test_dependency_auditor.py +0 -0
  50. {ballpython-2.1.1 → ballpython-2.1.2}/tests/test_discovery.py +0 -0
  51. {ballpython-2.1.1 → ballpython-2.1.2}/tests/test_frameworks.py +0 -0
  52. {ballpython-2.1.1 → ballpython-2.1.2}/tests/test_import_resolver.py +0 -0
  53. {ballpython-2.1.1 → ballpython-2.1.2}/tests/test_linter_formatter.py +0 -0
  54. {ballpython-2.1.1 → ballpython-2.1.2}/tests/test_modernizer.py +0 -0
  55. {ballpython-2.1.1 → ballpython-2.1.2}/tests/test_mutation_test_generator.py +0 -0
  56. {ballpython-2.1.1 → ballpython-2.1.2}/tests/test_pipeline.py +0 -0
  57. {ballpython-2.1.1 → ballpython-2.1.2}/tests/test_security_scanner.py +0 -0
  58. {ballpython-2.1.1 → ballpython-2.1.2}/tests/test_syntax_healer.py +0 -0
  59. {ballpython-2.1.1 → ballpython-2.1.2}/tests/test_taint_engine.py +0 -0
  60. {ballpython-2.1.1 → ballpython-2.1.2}/tests/test_test_generator.py +0 -0
  61. {ballpython-2.1.1 → ballpython-2.1.2}/tests/test_type_checker.py +0 -0
  62. {ballpython-2.1.1 → ballpython-2.1.2}/tests/test_verifier.py +0 -0
  63. {ballpython-2.1.1 → ballpython-2.1.2}/tests/test_verifier_pipeline.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ballpython
3
- Version: 2.1.1
3
+ Version: 2.1.2
4
4
  Summary: The Ultimate Static Python Intelligence, Healing, Type Verification, and Security Suite
5
5
  Author: Developer
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ballpython
3
- Version: 2.1.1
3
+ Version: 2.1.2
4
4
  Summary: The Ultimate Static Python Intelligence, Healing, Type Verification, and Security Suite
5
5
  Author: Developer
6
6
  License-Expression: MIT
@@ -901,7 +901,9 @@ def _run_progress_fix(
901
901
  backup=opts.backup,
902
902
  in_progress=True,
903
903
  )
904
- _print_msg, console, _is_json = io_ctx
904
+ print_msg, console, is_json = io_ctx
905
+ modified_items: list[tuple[CleanResult, Path]] = []
906
+
905
907
  with Progress(
906
908
  SpinnerColumn(),
907
909
  TextColumn("[progress.description]{task.description}"),
@@ -916,8 +918,18 @@ def _run_progress_fix(
916
918
  py_file, apply_changes=opts.apply_changes, backup=opts.backup
917
919
  )
918
920
  _accumulate_result(result, py_file, state, prog_opts, io_ctx)
921
+ if result.changed or result.error or result.syntax_repairs:
922
+ modified_items.append((result, py_file))
919
923
  progress.advance(task)
920
924
 
925
+ if not is_json and modified_items:
926
+ print_msg("")
927
+ for res, py_file in modified_items:
928
+ if not res.error and res.changed:
929
+ _report_file_modifications(res, py_file, opts.apply_changes, print_msg)
930
+ if opts.show_diff and res.diff:
931
+ _render_fix_diff(res.diff, console)
932
+
921
933
 
922
934
  def _execute_fix_batch(
923
935
  pipeline: CleanPipeline,
@@ -2271,6 +2283,7 @@ def _cmd_watch(
2271
2283
  except KeyboardInterrupt:
2272
2284
  print_msg("\n[bold]Watch mode stopped.[/bold]")
2273
2285
  return 0
2286
+ return 0
2274
2287
 
2275
2288
 
2276
2289
  def _cmd_hook(args: argparse.Namespace, print_msg) -> int:
@@ -155,7 +155,9 @@ class _DefinitionCollector(ast.NodeVisitor):
155
155
  return
156
156
 
157
157
  # Module-level constants or class attributes (not local function variables)
158
- if not self._scope_stack or len(self._scope_stack) == 1:
158
+ if not self._scope_stack or (
159
+ self._class_stack and len(self._scope_stack) == len(self._class_stack)
160
+ ):
159
161
  for target in node.targets:
160
162
  if isinstance(target, ast.Name):
161
163
  name = target.id
@@ -184,9 +186,12 @@ class _DefinitionCollector(ast.NodeVisitor):
184
186
  self.generic_visit(node)
185
187
  return
186
188
 
187
- if (not self._scope_stack or len(self._scope_stack) == 1) and isinstance(
188
- node.target, ast.Name
189
- ):
189
+ if (
190
+ not self._scope_stack
191
+ or (
192
+ self._class_stack and len(self._scope_stack) == len(self._class_stack)
193
+ )
194
+ ) and isinstance(node.target, ast.Name):
190
195
  name = node.target.id
191
196
  if not name.startswith("__"):
192
197
  context = (
@@ -573,9 +578,23 @@ class _ProjectScanState:
573
578
  def process_file(self, py_file: Path) -> None:
574
579
  try:
575
580
  content = py_file.read_text(encoding="utf-8", errors="replace")
581
+ except OSError:
582
+ return
583
+
584
+ try:
576
585
  tree = ast.parse(content, filename=str(py_file))
577
586
  except SyntaxError:
578
- return
587
+ from pycleaner.syntax_healer import SyntaxHealer
588
+
589
+ heal_res = SyntaxHealer().heal(content, filename=str(py_file))
590
+ if heal_res.is_valid:
591
+ try:
592
+ tree = ast.parse(heal_res.code, filename=str(py_file))
593
+ content = heal_res.code
594
+ except SyntaxError:
595
+ return
596
+ else:
597
+ return
579
598
 
580
599
  filepath_str = str(py_file)
581
600
  def_collector = _DefinitionCollector(
@@ -762,17 +781,28 @@ class DeadCodeDetector:
762
781
  return self.scan_files(py_files)
763
782
 
764
783
  def scan_source(self, source: str, filename: str = "<unknown>") -> DeadCodeReport:
765
- """Scan a single source string for dead code patterns (unreachable/empty only)."""
784
+ """Scan a single source string for dead code patterns (unreachable, empty branches, and local dead variables)."""
766
785
  try:
767
786
  tree = ast.parse(source, filename=filename)
768
787
  except SyntaxError:
769
- return DeadCodeReport()
788
+ from pycleaner.syntax_healer import SyntaxHealer
789
+
790
+ heal_res = SyntaxHealer().heal(source, filename=filename)
791
+ if heal_res.is_valid:
792
+ try:
793
+ tree = ast.parse(heal_res.code, filename=filename)
794
+ except SyntaxError:
795
+ return DeadCodeReport()
796
+ else:
797
+ return DeadCodeReport()
770
798
 
771
799
  unreachable = _UnreachableCodeDetector(filename, source.splitlines())
772
800
  unreachable.visit(tree)
773
801
 
802
+ items = list(unreachable.items)
803
+ items.sort(key=lambda x: (x.filepath, x.lineno))
774
804
  return DeadCodeReport(
775
- items=unreachable.items,
805
+ items=items,
776
806
  files_scanned=1,
777
807
  total_definitions=0,
778
808
  )
@@ -55,9 +55,11 @@ class FrameworkRegistry:
55
55
 
56
56
  def __init__(self) -> None:
57
57
  self._plugins: list[FrameworkPlugin] = []
58
+ self._applicable_cache: dict[tuple[int, str], list[FrameworkPlugin]] = {}
58
59
 
59
60
  def register(self, plugin: FrameworkPlugin) -> None:
60
61
  self._plugins.append(plugin)
62
+ self._applicable_cache.clear()
61
63
 
62
64
  @property
63
65
  def plugins(self) -> list[FrameworkPlugin]:
@@ -66,7 +68,12 @@ class FrameworkRegistry:
66
68
  def get_applicable_plugins(
67
69
  self, tree: ast.AST, filepath: Path | str
68
70
  ) -> list[FrameworkPlugin]:
69
- return [p for p in self._plugins if p.is_applicable(tree, filepath)]
71
+ cache_key = (id(tree), str(filepath))
72
+ if cache_key in self._applicable_cache:
73
+ return self._applicable_cache[cache_key]
74
+ applicable = [p for p in self._plugins if p.is_applicable(tree, filepath)]
75
+ self._applicable_cache[cache_key] = applicable
76
+ return applicable
70
77
 
71
78
  def is_protected(
72
79
  self,
@@ -57,6 +57,25 @@ class _UsageCollector(ast.NodeVisitor):
57
57
  self.generic_visit(node)
58
58
 
59
59
 
60
+ def _find_executable(name: str) -> str | None:
61
+ """Find CLI tool executable in PATH, virtual environment Scripts/bin, or python directory."""
62
+ found = shutil.which(name)
63
+ if found:
64
+ return found
65
+ from pathlib import Path
66
+ scripts_dir = Path(sys.prefix) / ("Scripts" if sys.platform == "win32" else "bin")
67
+ for ext in (".exe", ""):
68
+ candidate = scripts_dir / f"{name}{ext}"
69
+ if candidate.is_file():
70
+ return str(candidate)
71
+ parent_dir = Path(sys.executable).parent
72
+ for ext in (".exe", ""):
73
+ candidate = parent_dir / f"{name}{ext}"
74
+ if candidate.is_file():
75
+ return str(candidate)
76
+ return None
77
+
78
+
60
79
  class LinterFormatter:
61
80
  """Orchestrates static lint autofixes and canonical formatting."""
62
81
 
@@ -72,10 +91,10 @@ class LinterFormatter:
72
91
  DEFAULT_SELECT_RULES = "F401,F841,I,UP,E,W,B,SIM,RUF"
73
92
 
74
93
  def __init__(self, ruff_path: str | None = None) -> None:
75
- self.ruff_cmd: str | None = ruff_path or shutil.which("ruff")
76
- self.autoflake_cmd: str | None = shutil.which("autoflake")
77
- self.black_cmd: str | None = shutil.which("black")
78
- self.isort_cmd: str | None = shutil.which("isort")
94
+ self.ruff_cmd: str | None = ruff_path or _find_executable("ruff")
95
+ self.autoflake_cmd: str | None = _find_executable("autoflake")
96
+ self.black_cmd: str | None = _find_executable("black")
97
+ self.isort_cmd: str | None = _find_executable("isort")
79
98
 
80
99
  def fix_and_format(
81
100
  self,
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ballpython"
7
- version = "2.1.1"
7
+ version = "2.1.2"
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"
@@ -135,6 +135,28 @@ class TestDeadCodeDetector(unittest.TestCase):
135
135
  dead_names = [item.name for item in report.items if item.kind == "function"]
136
136
  self.assertNotIn("shared_tool", dead_names)
137
137
 
138
+ def test_unused_local_variable_detection(self) -> None:
139
+ code = (
140
+ "def compute(a, b):\n"
141
+ " unused_res = a * b\n"
142
+ " return a + b\n"
143
+ )
144
+ report = self.detector.scan_source(code, filename="compute.py")
145
+ vars_found = [item.name for item in report.items if item.kind == "variable"]
146
+ self.assertIn("unused_res", vars_found)
147
+
148
+ def test_scan_source_with_syntax_healing(self) -> None:
149
+ # Code with missing colon that is healable
150
+ code = (
151
+ "def broken(x)\n"
152
+ " dead_val = 123\n"
153
+ " return x\n"
154
+ )
155
+ report = self.detector.scan_source(code, filename="broken.py")
156
+ vars_found = [item.name for item in report.items if item.kind == "variable"]
157
+ self.assertIn("dead_val", vars_found)
158
+
138
159
 
139
160
  if __name__ == "__main__":
140
161
  unittest.main()
162
+
File without changes
File without changes
File without changes
File without changes