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.
- {ballpython-2.1.1 → ballpython-2.1.2}/PKG-INFO +1 -1
- {ballpython-2.1.1 → ballpython-2.1.2}/ballpython.egg-info/PKG-INFO +1 -1
- {ballpython-2.1.1 → ballpython-2.1.2}/pycleaner/cli.py +14 -1
- {ballpython-2.1.1 → ballpython-2.1.2}/pycleaner/dead_code_detector.py +38 -8
- {ballpython-2.1.1 → ballpython-2.1.2}/pycleaner/frameworks/__init__.py +8 -1
- {ballpython-2.1.1 → ballpython-2.1.2}/pycleaner/linter_formatter.py +23 -4
- {ballpython-2.1.1 → ballpython-2.1.2}/pyproject.toml +1 -1
- {ballpython-2.1.1 → ballpython-2.1.2}/tests/test_dead_code_detector.py +22 -0
- {ballpython-2.1.1 → ballpython-2.1.2}/README.md +0 -0
- {ballpython-2.1.1 → ballpython-2.1.2}/ballpython/__init__.py +0 -0
- {ballpython-2.1.1 → ballpython-2.1.2}/ballpython/__main__.py +0 -0
- {ballpython-2.1.1 → ballpython-2.1.2}/ballpython/cli.py +0 -0
- {ballpython-2.1.1 → ballpython-2.1.2}/ballpython/py.typed +0 -0
- {ballpython-2.1.1 → ballpython-2.1.2}/ballpython.egg-info/SOURCES.txt +0 -0
- {ballpython-2.1.1 → ballpython-2.1.2}/ballpython.egg-info/dependency_links.txt +0 -0
- {ballpython-2.1.1 → ballpython-2.1.2}/ballpython.egg-info/entry_points.txt +0 -0
- {ballpython-2.1.1 → ballpython-2.1.2}/ballpython.egg-info/requires.txt +0 -0
- {ballpython-2.1.1 → ballpython-2.1.2}/ballpython.egg-info/top_level.txt +0 -0
- {ballpython-2.1.1 → ballpython-2.1.2}/pycleaner/__init__.py +0 -0
- {ballpython-2.1.1 → ballpython-2.1.2}/pycleaner/__main__.py +0 -0
- {ballpython-2.1.1 → ballpython-2.1.2}/pycleaner/baseline.py +0 -0
- {ballpython-2.1.1 → ballpython-2.1.2}/pycleaner/cache.py +0 -0
- {ballpython-2.1.1 → ballpython-2.1.2}/pycleaner/complexity_analyzer.py +0 -0
- {ballpython-2.1.1 → ballpython-2.1.2}/pycleaner/config.py +0 -0
- {ballpython-2.1.1 → ballpython-2.1.2}/pycleaner/dependency_auditor.py +0 -0
- {ballpython-2.1.1 → ballpython-2.1.2}/pycleaner/discovery.py +0 -0
- {ballpython-2.1.1 → ballpython-2.1.2}/pycleaner/explanations.py +0 -0
- {ballpython-2.1.1 → ballpython-2.1.2}/pycleaner/frameworks/plugins.py +0 -0
- {ballpython-2.1.1 → ballpython-2.1.2}/pycleaner/import_resolver.py +0 -0
- {ballpython-2.1.1 → ballpython-2.1.2}/pycleaner/modernizer.py +0 -0
- {ballpython-2.1.1 → ballpython-2.1.2}/pycleaner/pipeline.py +0 -0
- {ballpython-2.1.1 → ballpython-2.1.2}/pycleaner/py.typed +0 -0
- {ballpython-2.1.1 → ballpython-2.1.2}/pycleaner/security_scanner.py +0 -0
- {ballpython-2.1.1 → ballpython-2.1.2}/pycleaner/syntax_healer.py +0 -0
- {ballpython-2.1.1 → ballpython-2.1.2}/pycleaner/taint_engine.py +0 -0
- {ballpython-2.1.1 → ballpython-2.1.2}/pycleaner/test_generator.py +0 -0
- {ballpython-2.1.1 → ballpython-2.1.2}/pycleaner/type_checker.py +0 -0
- {ballpython-2.1.1 → ballpython-2.1.2}/pycleaner/typeshed_resolver.py +0 -0
- {ballpython-2.1.1 → ballpython-2.1.2}/pycleaner/verifier.py +0 -0
- {ballpython-2.1.1 → ballpython-2.1.2}/setup.cfg +0 -0
- {ballpython-2.1.1 → ballpython-2.1.2}/tests/test_baseline_and_explain.py +0 -0
- {ballpython-2.1.1 → ballpython-2.1.2}/tests/test_c_to_a_overhaul.py +0 -0
- {ballpython-2.1.1 → ballpython-2.1.2}/tests/test_cache.py +0 -0
- {ballpython-2.1.1 → ballpython-2.1.2}/tests/test_cli.py +0 -0
- {ballpython-2.1.1 → ballpython-2.1.2}/tests/test_cli_prove.py +0 -0
- {ballpython-2.1.1 → ballpython-2.1.2}/tests/test_complexity_analyzer.py +0 -0
- {ballpython-2.1.1 → ballpython-2.1.2}/tests/test_config.py +0 -0
- {ballpython-2.1.1 → ballpython-2.1.2}/tests/test_dead_code_fixer.py +0 -0
- {ballpython-2.1.1 → ballpython-2.1.2}/tests/test_dependency_auditor.py +0 -0
- {ballpython-2.1.1 → ballpython-2.1.2}/tests/test_discovery.py +0 -0
- {ballpython-2.1.1 → ballpython-2.1.2}/tests/test_frameworks.py +0 -0
- {ballpython-2.1.1 → ballpython-2.1.2}/tests/test_import_resolver.py +0 -0
- {ballpython-2.1.1 → ballpython-2.1.2}/tests/test_linter_formatter.py +0 -0
- {ballpython-2.1.1 → ballpython-2.1.2}/tests/test_modernizer.py +0 -0
- {ballpython-2.1.1 → ballpython-2.1.2}/tests/test_mutation_test_generator.py +0 -0
- {ballpython-2.1.1 → ballpython-2.1.2}/tests/test_pipeline.py +0 -0
- {ballpython-2.1.1 → ballpython-2.1.2}/tests/test_security_scanner.py +0 -0
- {ballpython-2.1.1 → ballpython-2.1.2}/tests/test_syntax_healer.py +0 -0
- {ballpython-2.1.1 → ballpython-2.1.2}/tests/test_taint_engine.py +0 -0
- {ballpython-2.1.1 → ballpython-2.1.2}/tests/test_test_generator.py +0 -0
- {ballpython-2.1.1 → ballpython-2.1.2}/tests/test_type_checker.py +0 -0
- {ballpython-2.1.1 → ballpython-2.1.2}/tests/test_verifier.py +0 -0
- {ballpython-2.1.1 → ballpython-2.1.2}/tests/test_verifier_pipeline.py +0 -0
|
@@ -901,7 +901,9 @@ def _run_progress_fix(
|
|
|
901
901
|
backup=opts.backup,
|
|
902
902
|
in_progress=True,
|
|
903
903
|
)
|
|
904
|
-
|
|
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
|
|
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 (
|
|
188
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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=
|
|
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
|
-
|
|
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
|
|
76
|
-
self.autoflake_cmd: str | None =
|
|
77
|
-
self.black_cmd: str | None =
|
|
78
|
-
self.isort_cmd: str | None =
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|