ballpython 2.0.7__tar.gz → 2.1.1__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.0.7 → ballpython-2.1.1}/PKG-INFO +1 -3
- {ballpython-2.0.7 → ballpython-2.1.1}/ballpython.egg-info/PKG-INFO +1 -3
- {ballpython-2.0.7 → ballpython-2.1.1}/ballpython.egg-info/SOURCES.txt +1 -0
- {ballpython-2.0.7 → ballpython-2.1.1}/ballpython.egg-info/requires.txt +0 -4
- {ballpython-2.0.7 → ballpython-2.1.1}/pycleaner/__init__.py +1 -1
- {ballpython-2.0.7 → ballpython-2.1.1}/pycleaner/cache.py +3 -1
- {ballpython-2.0.7 → ballpython-2.1.1}/pycleaner/cli.py +228 -49
- {ballpython-2.0.7 → ballpython-2.1.1}/pycleaner/complexity_analyzer.py +11 -8
- ballpython-2.1.1/pycleaner/dead_code_detector.py +1380 -0
- {ballpython-2.0.7 → ballpython-2.1.1}/pycleaner/dependency_auditor.py +153 -17
- {ballpython-2.0.7 → ballpython-2.1.1}/pycleaner/discovery.py +17 -0
- {ballpython-2.0.7 → ballpython-2.1.1}/pycleaner/linter_formatter.py +6 -5
- {ballpython-2.0.7 → ballpython-2.1.1}/pycleaner/modernizer.py +2 -1
- {ballpython-2.0.7 → ballpython-2.1.1}/pycleaner/pipeline.py +99 -11
- {ballpython-2.0.7 → ballpython-2.1.1}/pycleaner/security_scanner.py +151 -15
- {ballpython-2.0.7 → ballpython-2.1.1}/pycleaner/syntax_healer.py +210 -44
- {ballpython-2.0.7 → ballpython-2.1.1}/pycleaner/taint_engine.py +19 -15
- {ballpython-2.0.7 → ballpython-2.1.1}/pycleaner/type_checker.py +128 -25
- {ballpython-2.0.7 → ballpython-2.1.1}/pycleaner/verifier.py +15 -6
- {ballpython-2.0.7 → ballpython-2.1.1}/pyproject.toml +1 -3
- ballpython-2.1.1/tests/test_c_to_a_overhaul.py +411 -0
- {ballpython-2.0.7 → ballpython-2.1.1}/tests/test_cli.py +51 -0
- ballpython-2.1.1/tests/test_dead_code_fixer.py +173 -0
- {ballpython-2.0.7 → ballpython-2.1.1}/tests/test_dependency_auditor.py +77 -0
- {ballpython-2.0.7 → ballpython-2.1.1}/tests/test_syntax_healer.py +1 -0
- {ballpython-2.0.7 → ballpython-2.1.1}/tests/test_type_checker.py +106 -1
- ballpython-2.0.7/pycleaner/dead_code_detector.py +0 -760
- ballpython-2.0.7/tests/test_dead_code_fixer.py +0 -90
- {ballpython-2.0.7 → ballpython-2.1.1}/README.md +0 -0
- {ballpython-2.0.7 → ballpython-2.1.1}/ballpython/__init__.py +0 -0
- {ballpython-2.0.7 → ballpython-2.1.1}/ballpython/__main__.py +0 -0
- {ballpython-2.0.7 → ballpython-2.1.1}/ballpython/cli.py +0 -0
- {ballpython-2.0.7 → ballpython-2.1.1}/ballpython/py.typed +0 -0
- {ballpython-2.0.7 → ballpython-2.1.1}/ballpython.egg-info/dependency_links.txt +0 -0
- {ballpython-2.0.7 → ballpython-2.1.1}/ballpython.egg-info/entry_points.txt +0 -0
- {ballpython-2.0.7 → ballpython-2.1.1}/ballpython.egg-info/top_level.txt +0 -0
- {ballpython-2.0.7 → ballpython-2.1.1}/pycleaner/__main__.py +0 -0
- {ballpython-2.0.7 → ballpython-2.1.1}/pycleaner/baseline.py +0 -0
- {ballpython-2.0.7 → ballpython-2.1.1}/pycleaner/config.py +0 -0
- {ballpython-2.0.7 → ballpython-2.1.1}/pycleaner/explanations.py +0 -0
- {ballpython-2.0.7 → ballpython-2.1.1}/pycleaner/frameworks/__init__.py +0 -0
- {ballpython-2.0.7 → ballpython-2.1.1}/pycleaner/frameworks/plugins.py +0 -0
- {ballpython-2.0.7 → ballpython-2.1.1}/pycleaner/import_resolver.py +0 -0
- {ballpython-2.0.7 → ballpython-2.1.1}/pycleaner/py.typed +0 -0
- {ballpython-2.0.7 → ballpython-2.1.1}/pycleaner/test_generator.py +0 -0
- {ballpython-2.0.7 → ballpython-2.1.1}/pycleaner/typeshed_resolver.py +0 -0
- {ballpython-2.0.7 → ballpython-2.1.1}/setup.cfg +0 -0
- {ballpython-2.0.7 → ballpython-2.1.1}/tests/test_baseline_and_explain.py +0 -0
- {ballpython-2.0.7 → ballpython-2.1.1}/tests/test_cache.py +0 -0
- {ballpython-2.0.7 → ballpython-2.1.1}/tests/test_cli_prove.py +0 -0
- {ballpython-2.0.7 → ballpython-2.1.1}/tests/test_complexity_analyzer.py +0 -0
- {ballpython-2.0.7 → ballpython-2.1.1}/tests/test_config.py +0 -0
- {ballpython-2.0.7 → ballpython-2.1.1}/tests/test_dead_code_detector.py +0 -0
- {ballpython-2.0.7 → ballpython-2.1.1}/tests/test_discovery.py +0 -0
- {ballpython-2.0.7 → ballpython-2.1.1}/tests/test_frameworks.py +0 -0
- {ballpython-2.0.7 → ballpython-2.1.1}/tests/test_import_resolver.py +0 -0
- {ballpython-2.0.7 → ballpython-2.1.1}/tests/test_linter_formatter.py +0 -0
- {ballpython-2.0.7 → ballpython-2.1.1}/tests/test_modernizer.py +0 -0
- {ballpython-2.0.7 → ballpython-2.1.1}/tests/test_mutation_test_generator.py +0 -0
- {ballpython-2.0.7 → ballpython-2.1.1}/tests/test_pipeline.py +0 -0
- {ballpython-2.0.7 → ballpython-2.1.1}/tests/test_security_scanner.py +0 -0
- {ballpython-2.0.7 → ballpython-2.1.1}/tests/test_taint_engine.py +0 -0
- {ballpython-2.0.7 → ballpython-2.1.1}/tests/test_test_generator.py +0 -0
- {ballpython-2.0.7 → ballpython-2.1.1}/tests/test_verifier.py +0 -0
- {ballpython-2.0.7 → ballpython-2.1.1}/tests/test_verifier_pipeline.py +0 -0
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ballpython
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1.1
|
|
4
4
|
Summary: The Ultimate Static Python Intelligence, Healing, Type Verification, and Security Suite
|
|
5
5
|
Author: Developer
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Requires-Python: >=3.10
|
|
8
8
|
Description-Content-Type: text/markdown
|
|
9
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
10
|
Provides-Extra: dev
|
|
13
11
|
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
14
12
|
Requires-Dist: black>=24.0.0; extra == "dev"
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ballpython
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1.1
|
|
4
4
|
Summary: The Ultimate Static Python Intelligence, Healing, Type Verification, and Security Suite
|
|
5
5
|
Author: Developer
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Requires-Python: >=3.10
|
|
8
8
|
Description-Content-Type: text/markdown
|
|
9
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
10
|
Provides-Extra: dev
|
|
13
11
|
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
14
12
|
Requires-Dist: black>=24.0.0; extra == "dev"
|
|
@@ -15,6 +15,7 @@ from __future__ import annotations
|
|
|
15
15
|
import ast
|
|
16
16
|
import hashlib
|
|
17
17
|
import json
|
|
18
|
+
import logging
|
|
18
19
|
import sqlite3
|
|
19
20
|
import time
|
|
20
21
|
from pathlib import Path
|
|
@@ -22,6 +23,7 @@ from typing import TYPE_CHECKING, Any
|
|
|
22
23
|
|
|
23
24
|
if TYPE_CHECKING:
|
|
24
25
|
from pycleaner.pipeline import CleanResult
|
|
26
|
+
|
|
25
27
|
from pycleaner.verifier import VerificationTier
|
|
26
28
|
|
|
27
29
|
|
|
@@ -46,7 +48,7 @@ def extract_file_dependencies(source: str) -> list[str]:
|
|
|
46
48
|
elif isinstance(node, ast.ImportFrom) and node.module:
|
|
47
49
|
deps.append(node.module.split(".")[0])
|
|
48
50
|
except SyntaxError:
|
|
49
|
-
|
|
51
|
+
logging.getLogger(__name__).debug("Suppressed exception", exc_info=True)
|
|
50
52
|
return sorted(set(deps))
|
|
51
53
|
|
|
52
54
|
|
|
@@ -157,6 +157,11 @@ def _register_fix_subparsers(subparsers: Any) -> None:
|
|
|
157
157
|
def _register_analysis_subparsers(subparsers: Any) -> None:
|
|
158
158
|
audit_p = subparsers.add_parser("audit", help="Audit project dependencies")
|
|
159
159
|
_add_common_args(audit_p)
|
|
160
|
+
audit_p.add_argument(
|
|
161
|
+
"--fix",
|
|
162
|
+
action="store_true",
|
|
163
|
+
help="Auto-fix requirements.txt and pyproject.toml and prune unused packages",
|
|
164
|
+
)
|
|
160
165
|
audit_p.add_argument(
|
|
161
166
|
"--fix-deps", action="store_true", help="Auto-fix requirements.txt"
|
|
162
167
|
)
|
|
@@ -550,7 +555,12 @@ def _create_printer(console: Any) -> Any:
|
|
|
550
555
|
else:
|
|
551
556
|
import re
|
|
552
557
|
|
|
553
|
-
cleaned = re.sub(
|
|
558
|
+
cleaned = re.sub(
|
|
559
|
+
r"(?<!\\)\[/?(?:bold|dim|italic|underline|blink|reverse|strike|black|red|green|yellow|blue|magenta|cyan|white)(?: [a-z]+)*\]",
|
|
560
|
+
"",
|
|
561
|
+
msg,
|
|
562
|
+
)
|
|
563
|
+
cleaned = cleaned.replace("\\[", "[").replace("\\]", "]")
|
|
554
564
|
print(cleaned)
|
|
555
565
|
|
|
556
566
|
return print_msg
|
|
@@ -679,8 +689,13 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
679
689
|
@dataclass
|
|
680
690
|
class _FixBatchState:
|
|
681
691
|
changed_count: int = 0
|
|
692
|
+
would_change_count: int = 0
|
|
693
|
+
actually_written_count: int = 0
|
|
694
|
+
partially_updated_count: int = 0
|
|
695
|
+
partially_would_change_count: int = 0
|
|
682
696
|
error_count: int = 0
|
|
683
697
|
diagnostics: list[dict[str, str]] = field(default_factory=list)
|
|
698
|
+
security_findings: list[Any] = field(default_factory=list)
|
|
684
699
|
proven_count: int = 0
|
|
685
700
|
suggested_count: int = 0
|
|
686
701
|
refused_count: int = 0
|
|
@@ -724,6 +739,12 @@ def _report_file_modifications(
|
|
|
724
739
|
print_msg(" - Lint: fixed errors and pruned unused imports", style="blue")
|
|
725
740
|
if result.format_changed:
|
|
726
741
|
print_msg(" - Format: applied PEP 8 formatting", style="blue")
|
|
742
|
+
for sf in getattr(result, "security_findings", []):
|
|
743
|
+
if getattr(sf, "severity", None) in ("CRITICAL", "HIGH"):
|
|
744
|
+
print_msg(
|
|
745
|
+
f" - Unresolved Security [{sf.severity}]: {sf.message} (line {sf.lineno})",
|
|
746
|
+
style="bold red",
|
|
747
|
+
)
|
|
727
748
|
|
|
728
749
|
|
|
729
750
|
def _accumulate_result(
|
|
@@ -736,6 +757,8 @@ def _accumulate_result(
|
|
|
736
757
|
print_msg, console, is_json = io_ctx
|
|
737
758
|
if result.diagnostics:
|
|
738
759
|
state.diagnostics.extend(result.diagnostics)
|
|
760
|
+
if getattr(result, "security_findings", None):
|
|
761
|
+
state.security_findings.extend(result.security_findings)
|
|
739
762
|
|
|
740
763
|
if result.proof_receipts:
|
|
741
764
|
state.proof_receipts.extend(result.proof_receipts)
|
|
@@ -761,12 +784,63 @@ def _accumulate_result(
|
|
|
761
784
|
|
|
762
785
|
if result.error:
|
|
763
786
|
state.error_count += 1
|
|
787
|
+
is_partial = (result.changed or bool(result.syntax_repairs)) and (
|
|
788
|
+
result.verification_tier != VerificationTier.TIER_C_REFUSED
|
|
789
|
+
)
|
|
790
|
+
if is_partial:
|
|
791
|
+
state.changed_count += 1
|
|
792
|
+
if opts.apply_changes:
|
|
793
|
+
state.partially_updated_count += 1
|
|
794
|
+
else:
|
|
795
|
+
state.partially_would_change_count += 1
|
|
796
|
+
|
|
797
|
+
if not is_json and not opts.in_progress:
|
|
798
|
+
action = (
|
|
799
|
+
"Partially cleaned"
|
|
800
|
+
if opts.apply_changes
|
|
801
|
+
else "Would partially modify"
|
|
802
|
+
)
|
|
803
|
+
print_msg(f"[yellow]{action}:[/yellow] {py_file.name}")
|
|
804
|
+
for repair in result.syntax_repairs:
|
|
805
|
+
print_msg(f" - Syntax: {repair}", style="cyan")
|
|
806
|
+
for mod in result.modernize_transforms:
|
|
807
|
+
print_msg(f" - Modernize: {mod}", style="green")
|
|
808
|
+
for dc in result.dead_code_pruned:
|
|
809
|
+
print_msg(f" - Dead-code: {dc}", style="yellow")
|
|
810
|
+
for imp in result.resolved_imports:
|
|
811
|
+
print_msg(f" - Import: {imp}", style="magenta")
|
|
812
|
+
if result.lint_changed:
|
|
813
|
+
print_msg(
|
|
814
|
+
" - Lint: fixed errors and pruned unused imports", style="blue"
|
|
815
|
+
)
|
|
816
|
+
if result.format_changed:
|
|
817
|
+
print_msg(" - Format: applied PEP 8 formatting", style="blue")
|
|
818
|
+
for sf in getattr(result, "security_findings", []):
|
|
819
|
+
if getattr(sf, "severity", None) in ("CRITICAL", "HIGH"):
|
|
820
|
+
print_msg(
|
|
821
|
+
f" - Unresolved Security [{sf.severity}]: {sf.message} (line {sf.lineno})",
|
|
822
|
+
style="bold red",
|
|
823
|
+
)
|
|
824
|
+
if opts.show_diff and result.diff:
|
|
825
|
+
_render_fix_diff(result.diff, console)
|
|
826
|
+
else:
|
|
827
|
+
if not is_json and not opts.in_progress:
|
|
828
|
+
for sf in getattr(result, "security_findings", []):
|
|
829
|
+
if getattr(sf, "severity", None) in ("CRITICAL", "HIGH"):
|
|
830
|
+
print_msg(
|
|
831
|
+
f" - Unresolved Security [{sf.severity}]: {sf.message} (line {sf.lineno})",
|
|
832
|
+
style="bold red",
|
|
833
|
+
)
|
|
834
|
+
|
|
764
835
|
if not is_json:
|
|
765
836
|
print_msg(f"[red]ERROR in {py_file.name}:[/red] {result.error}")
|
|
766
837
|
return
|
|
767
838
|
|
|
768
839
|
if result.changed:
|
|
769
840
|
state.changed_count += 1
|
|
841
|
+
state.would_change_count += 1
|
|
842
|
+
if opts.apply_changes and not result.error:
|
|
843
|
+
state.actually_written_count += 1
|
|
770
844
|
if not is_json and not opts.in_progress:
|
|
771
845
|
_report_file_modifications(result, py_file, opts.apply_changes, print_msg)
|
|
772
846
|
if opts.show_diff and result.diff:
|
|
@@ -897,15 +971,13 @@ def _render_audit_terminal_output(
|
|
|
897
971
|
f" [red]Missing:[/red] {', '.join(sorted(audit_report.missing_packages))}"
|
|
898
972
|
)
|
|
899
973
|
if not should_fix:
|
|
900
|
-
print_msg(
|
|
901
|
-
" [dim]Tip: Run with -a/--all or --fix-deps to append them[/dim]"
|
|
902
|
-
)
|
|
974
|
+
print_msg(" [dim]Tip: Run with -a/--all or --fix to append them[/dim]")
|
|
903
975
|
if audit_report.unused_packages:
|
|
904
976
|
print_msg(
|
|
905
977
|
f" [yellow]Unused:[/yellow] {', '.join(sorted(audit_report.unused_packages))}"
|
|
906
978
|
)
|
|
907
979
|
if audit_report.fixed_requirements:
|
|
908
|
-
print_msg(" [green]
|
|
980
|
+
print_msg(" [green]Project dependencies synchronized on disk.[/green]")
|
|
909
981
|
|
|
910
982
|
|
|
911
983
|
def _run_fix_audit(
|
|
@@ -918,8 +990,18 @@ def _run_fix_audit(
|
|
|
918
990
|
) -> DependencyAuditReport:
|
|
919
991
|
exclude_patterns = config.exclude if config else ()
|
|
920
992
|
auditor = DependencyAuditor(root_dir, exclude_patterns=exclude_patterns)
|
|
921
|
-
fix_any =
|
|
922
|
-
|
|
993
|
+
fix_any = (
|
|
994
|
+
getattr(args, "fix_deps", False)
|
|
995
|
+
or getattr(args, "fix_all", False)
|
|
996
|
+
or getattr(args, "fix", False)
|
|
997
|
+
or apply_changes
|
|
998
|
+
)
|
|
999
|
+
prune_any = (
|
|
1000
|
+
getattr(args, "prune_deps", False)
|
|
1001
|
+
or getattr(args, "fix_all", False)
|
|
1002
|
+
or getattr(args, "fix", False)
|
|
1003
|
+
or apply_changes
|
|
1004
|
+
)
|
|
923
1005
|
should_fix = apply_changes and fix_any
|
|
924
1006
|
should_prune = apply_changes and prune_any
|
|
925
1007
|
audit_report = auditor.audit(
|
|
@@ -939,8 +1021,14 @@ def _compute_fix_exit_code(
|
|
|
939
1021
|
return 1
|
|
940
1022
|
if is_prove_cmd:
|
|
941
1023
|
return 0 if state.error_count == 0 else 1
|
|
1024
|
+
has_unresolved_security = any(
|
|
1025
|
+
getattr(sf, "severity", None) in ("CRITICAL", "HIGH")
|
|
1026
|
+
for sf in state.security_findings
|
|
1027
|
+
)
|
|
1028
|
+
if has_unresolved_security:
|
|
1029
|
+
return 1
|
|
942
1030
|
if not apply_changes and (
|
|
943
|
-
state.
|
|
1031
|
+
state.would_change_count > 0
|
|
944
1032
|
or state.error_count > 0
|
|
945
1033
|
or bool(audit_report.missing_packages)
|
|
946
1034
|
):
|
|
@@ -999,7 +1087,11 @@ def _cmd_fix(
|
|
|
999
1087
|
|
|
1000
1088
|
if is_json:
|
|
1001
1089
|
print(json.dumps(state.diagnostics, indent=2))
|
|
1002
|
-
|
|
1090
|
+
has_sec_issues = any(
|
|
1091
|
+
getattr(sf, "severity", None) in ("CRITICAL", "HIGH")
|
|
1092
|
+
for sf in state.security_findings
|
|
1093
|
+
)
|
|
1094
|
+
return 1 if (state.error_count > 0 or has_sec_issues) else 0
|
|
1003
1095
|
|
|
1004
1096
|
audit_report = _run_fix_audit(
|
|
1005
1097
|
root_dir,
|
|
@@ -1009,9 +1101,69 @@ def _cmd_fix(
|
|
|
1009
1101
|
config=config,
|
|
1010
1102
|
target_files=py_files,
|
|
1011
1103
|
)
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1104
|
+
if opts.apply_changes and (
|
|
1105
|
+
audit_report.fixed_requirements
|
|
1106
|
+
or getattr(audit_report, "fixed_pyproject", False)
|
|
1107
|
+
):
|
|
1108
|
+
state.changed_count += 1
|
|
1109
|
+
state.would_change_count += 1
|
|
1110
|
+
state.actually_written_count += 1
|
|
1111
|
+
|
|
1112
|
+
if not opts.apply_changes:
|
|
1113
|
+
if state.partially_would_change_count > 0:
|
|
1114
|
+
err_label = (
|
|
1115
|
+
"1 remaining error"
|
|
1116
|
+
if state.error_count == 1
|
|
1117
|
+
else f"{state.error_count} remaining errors"
|
|
1118
|
+
)
|
|
1119
|
+
if state.would_change_count > 0:
|
|
1120
|
+
print_msg(
|
|
1121
|
+
f"\n[bold]Summary: {len(py_files)} inspected, {state.would_change_count} would change, {state.partially_would_change_count} would partially change, 0 written, {err_label}.[/bold]"
|
|
1122
|
+
)
|
|
1123
|
+
else:
|
|
1124
|
+
print_msg(
|
|
1125
|
+
f"\n[bold]Summary: {len(py_files)} inspected, {state.partially_would_change_count} would partially change, 0 written, {err_label}.[/bold]"
|
|
1126
|
+
)
|
|
1127
|
+
else:
|
|
1128
|
+
print_msg(
|
|
1129
|
+
f"\n[bold]Summary: {len(py_files)} inspected, {state.would_change_count} would change, 0 written, {state.error_count} errors.[/bold]"
|
|
1130
|
+
)
|
|
1131
|
+
else:
|
|
1132
|
+
if state.partially_updated_count > 0:
|
|
1133
|
+
err_label = (
|
|
1134
|
+
"1 remaining error"
|
|
1135
|
+
if state.error_count == 1
|
|
1136
|
+
else f"{state.error_count} remaining errors"
|
|
1137
|
+
)
|
|
1138
|
+
if state.actually_written_count > 0:
|
|
1139
|
+
print_msg(
|
|
1140
|
+
f"\n[bold]Summary: {len(py_files)} inspected, {state.actually_written_count} updated, {state.partially_updated_count} partially updated, {err_label}.[/bold]"
|
|
1141
|
+
)
|
|
1142
|
+
else:
|
|
1143
|
+
print_msg(
|
|
1144
|
+
f"\n[bold]Summary: {len(py_files)} inspected, {state.partially_updated_count} partially updated, {err_label}.[/bold]"
|
|
1145
|
+
)
|
|
1146
|
+
else:
|
|
1147
|
+
print_msg(
|
|
1148
|
+
f"\n[bold]Summary: {len(py_files)} inspected, {state.actually_written_count} updated, {state.error_count} errors.[/bold]"
|
|
1149
|
+
)
|
|
1150
|
+
|
|
1151
|
+
critical_or_high_findings = [
|
|
1152
|
+
sf
|
|
1153
|
+
for sf in state.security_findings
|
|
1154
|
+
if getattr(sf, "severity", None) in ("CRITICAL", "HIGH")
|
|
1155
|
+
]
|
|
1156
|
+
if critical_or_high_findings and not is_json:
|
|
1157
|
+
print_msg(
|
|
1158
|
+
f"\n[bold red]Unresolved Security Findings ({len(critical_or_high_findings)} issue(s)):[/bold red]"
|
|
1159
|
+
)
|
|
1160
|
+
for sf in critical_or_high_findings:
|
|
1161
|
+
f_path = getattr(sf, "filepath", getattr(sf, "file", "<unknown>"))
|
|
1162
|
+
print_msg(
|
|
1163
|
+
f" [bold red]- [{sf.severity}] {f_path}:{sf.lineno}: {sf.message}[/bold red]"
|
|
1164
|
+
)
|
|
1165
|
+
if getattr(sf, "suggestion", None):
|
|
1166
|
+
print_msg(f" [dim]Suggestion: {sf.suggestion}[/dim]")
|
|
1015
1167
|
|
|
1016
1168
|
if pipeline.verify_proofs:
|
|
1017
1169
|
total_proven_callables = sum(
|
|
@@ -1168,7 +1320,7 @@ def _cmd_baseline(
|
|
|
1168
1320
|
fingerprints: list[BaselineFingerprint] = []
|
|
1169
1321
|
|
|
1170
1322
|
detector = DeadCodeDetector()
|
|
1171
|
-
dead_code_report = detector.
|
|
1323
|
+
dead_code_report = detector.scan_files(py_files)
|
|
1172
1324
|
for item in dead_code_report.items:
|
|
1173
1325
|
if item.kind == "unused-import":
|
|
1174
1326
|
rule = "DC002"
|
|
@@ -1187,7 +1339,7 @@ def _cmd_baseline(
|
|
|
1187
1339
|
)
|
|
1188
1340
|
|
|
1189
1341
|
scanner = SecurityScanner()
|
|
1190
|
-
sec_report = scanner.
|
|
1342
|
+
sec_report = scanner.scan_files(py_files)
|
|
1191
1343
|
for finding in sec_report.findings:
|
|
1192
1344
|
fingerprints.append(
|
|
1193
1345
|
BaselineManager.create_fingerprint(
|
|
@@ -1200,7 +1352,7 @@ def _cmd_baseline(
|
|
|
1200
1352
|
)
|
|
1201
1353
|
|
|
1202
1354
|
analyzer = ComplexityAnalyzer()
|
|
1203
|
-
comp_report = analyzer.
|
|
1355
|
+
comp_report = analyzer.analyze_files(py_files)
|
|
1204
1356
|
thresholds = (
|
|
1205
1357
|
getattr(args, "max_cyclomatic", config.max_cyclomatic_complexity),
|
|
1206
1358
|
getattr(args, "max_cognitive", config.max_cognitive_complexity),
|
|
@@ -1354,9 +1506,10 @@ def _cmd_cache(
|
|
|
1354
1506
|
console: Any,
|
|
1355
1507
|
) -> int:
|
|
1356
1508
|
"""Manage and inspect the content-addressable verification and AST cache."""
|
|
1357
|
-
|
|
1509
|
+
raw_db_path = getattr(args, "db", None) or getattr(
|
|
1358
1510
|
config, "cache_db_path", ".pycleaner/cache.db"
|
|
1359
1511
|
)
|
|
1512
|
+
db_path: str = str(raw_db_path) if raw_db_path else ".pycleaner/cache.db"
|
|
1360
1513
|
cache = ContentAddressableCache(db_path=db_path)
|
|
1361
1514
|
|
|
1362
1515
|
if getattr(args, "clear", False):
|
|
@@ -1412,8 +1565,10 @@ def _render_audit_cli_output(
|
|
|
1412
1565
|
)
|
|
1413
1566
|
if not audit_report.missing_packages and not audit_report.unused_packages:
|
|
1414
1567
|
print_msg(" [green]All dependencies clean.[/green]")
|
|
1415
|
-
if audit_report.fixed_requirements
|
|
1416
|
-
|
|
1568
|
+
if audit_report.fixed_requirements or getattr(
|
|
1569
|
+
audit_report, "fixed_pyproject", False
|
|
1570
|
+
):
|
|
1571
|
+
print_msg(" [green]Project dependencies synchronized on disk.[/green]")
|
|
1417
1572
|
|
|
1418
1573
|
return (
|
|
1419
1574
|
1
|
|
@@ -1424,13 +1579,27 @@ def _render_audit_cli_output(
|
|
|
1424
1579
|
|
|
1425
1580
|
def _cmd_audit(args: argparse.Namespace, config: PyCleanerConfig, print_msg) -> int:
|
|
1426
1581
|
"""Dependency audit command."""
|
|
1427
|
-
|
|
1582
|
+
targets = getattr(args, "paths", ["."])
|
|
1583
|
+
target_path = Path(targets[0]).resolve() if targets else Path.cwd()
|
|
1584
|
+
root_dir = find_project_root(target_path)
|
|
1585
|
+
explicit_files = (
|
|
1586
|
+
[
|
|
1587
|
+
Path(p).resolve()
|
|
1588
|
+
for p in targets
|
|
1589
|
+
if Path(p).is_file() and Path(p).suffix == ".py"
|
|
1590
|
+
]
|
|
1591
|
+
if targets
|
|
1592
|
+
else None
|
|
1593
|
+
)
|
|
1428
1594
|
|
|
1429
1595
|
is_check = getattr(args, "check", False)
|
|
1596
|
+
fix_flag = getattr(args, "fix", False) or getattr(args, "fix_deps", False)
|
|
1597
|
+
prune_flag = getattr(args, "fix", False) or getattr(args, "prune_deps", False)
|
|
1430
1598
|
auditor = DependencyAuditor(root_dir, exclude_patterns=config.exclude)
|
|
1431
1599
|
audit_report = auditor.audit(
|
|
1432
|
-
fix=
|
|
1433
|
-
prune_unused=
|
|
1600
|
+
fix=fix_flag and not is_check,
|
|
1601
|
+
prune_unused=prune_flag and not is_check,
|
|
1602
|
+
target_files=explicit_files or None,
|
|
1434
1603
|
)
|
|
1435
1604
|
|
|
1436
1605
|
if getattr(args, "json", False):
|
|
@@ -1452,7 +1621,7 @@ def _render_scan_json(report: Any) -> int:
|
|
|
1452
1621
|
for f in report.findings
|
|
1453
1622
|
]
|
|
1454
1623
|
print(json.dumps(findings, indent=2))
|
|
1455
|
-
return 1 if report.critical_count > 0 else 0
|
|
1624
|
+
return 1 if (report.critical_count > 0 or report.high_count > 0) else 0
|
|
1456
1625
|
|
|
1457
1626
|
|
|
1458
1627
|
def _render_scan_table(report: Any, console: Any, target_base: str) -> None:
|
|
@@ -1503,23 +1672,24 @@ def _render_scan_cli_summary(
|
|
|
1503
1672
|
f"\n Total: {report.count} finding(s) - "
|
|
1504
1673
|
f"[red]{report.critical_count} critical[/red], [red]{report.high_count} high[/red]"
|
|
1505
1674
|
)
|
|
1506
|
-
return 1 if report.critical_count > 0 else 0
|
|
1675
|
+
return 1 if (report.critical_count > 0 or report.high_count > 0) else 0
|
|
1507
1676
|
|
|
1508
1677
|
|
|
1509
1678
|
def _cmd_scan(
|
|
1510
1679
|
args: argparse.Namespace, config: PyCleanerConfig, print_msg, console
|
|
1511
1680
|
) -> int:
|
|
1512
1681
|
"""Security scan command."""
|
|
1513
|
-
|
|
1514
|
-
if
|
|
1515
|
-
|
|
1682
|
+
targets = getattr(args, "paths", ["."])
|
|
1683
|
+
target_path = Path(targets[0]).resolve() if targets else Path.cwd()
|
|
1684
|
+
root_dir = find_project_root(target_path)
|
|
1685
|
+
py_files = discover_python_files(args.paths, config=config, root=root_dir)
|
|
1516
1686
|
|
|
1517
1687
|
severity = getattr(args, "severity", config.security_severity_threshold)
|
|
1518
1688
|
scanner = SecurityScanner(
|
|
1519
1689
|
severity_threshold=severity,
|
|
1520
1690
|
ignore_rules=set(config.ignore_security_rules),
|
|
1521
1691
|
)
|
|
1522
|
-
report = scanner.
|
|
1692
|
+
report = scanner.scan_files(py_files)
|
|
1523
1693
|
|
|
1524
1694
|
target_base = args.paths[0] if args.paths else "."
|
|
1525
1695
|
if getattr(args, "json", False):
|
|
@@ -1613,12 +1783,13 @@ def _cmd_complexity(
|
|
|
1613
1783
|
args: argparse.Namespace, config: PyCleanerConfig, print_msg, console
|
|
1614
1784
|
) -> int:
|
|
1615
1785
|
"""Complexity analysis command."""
|
|
1616
|
-
|
|
1617
|
-
if
|
|
1618
|
-
|
|
1786
|
+
targets = getattr(args, "paths", ["."])
|
|
1787
|
+
target_path = Path(targets[0]).resolve() if targets else Path.cwd()
|
|
1788
|
+
root_dir = find_project_root(target_path)
|
|
1789
|
+
py_files = discover_python_files(args.paths, config=config, root=root_dir)
|
|
1619
1790
|
|
|
1620
1791
|
analyzer = ComplexityAnalyzer()
|
|
1621
|
-
report = analyzer.
|
|
1792
|
+
report = analyzer.analyze_files(py_files)
|
|
1622
1793
|
|
|
1623
1794
|
thresholds = (
|
|
1624
1795
|
getattr(args, "max_cyclomatic", config.max_cyclomatic_complexity),
|
|
@@ -1688,17 +1859,20 @@ def _cmd_dead_code(
|
|
|
1688
1859
|
args: argparse.Namespace, config: PyCleanerConfig, print_msg, console
|
|
1689
1860
|
) -> int:
|
|
1690
1861
|
"""Dead code detection command."""
|
|
1691
|
-
|
|
1692
|
-
if
|
|
1693
|
-
|
|
1862
|
+
targets = getattr(args, "paths", ["."])
|
|
1863
|
+
target_path = Path(targets[0]).resolve() if targets else Path.cwd()
|
|
1864
|
+
root_dir = find_project_root(target_path)
|
|
1865
|
+
py_files = discover_python_files(args.paths, config=config, root=root_dir)
|
|
1694
1866
|
|
|
1695
1867
|
detector = DeadCodeDetector(
|
|
1696
1868
|
ignore_decorators=set(config.ignore_decorators),
|
|
1697
1869
|
ignore_names=set(config.ignore_names),
|
|
1698
1870
|
)
|
|
1699
1871
|
if getattr(args, "fix", False):
|
|
1700
|
-
print_msg(
|
|
1701
|
-
|
|
1872
|
+
print_msg(
|
|
1873
|
+
f"[bold green]Pruning dead code across {len(py_files)} file(s)...[/bold green]"
|
|
1874
|
+
)
|
|
1875
|
+
fix_results = detector.fix_files(py_files)
|
|
1702
1876
|
total_pruned = sum(len(res.pruned_items) for res in fix_results.values())
|
|
1703
1877
|
print_msg(
|
|
1704
1878
|
f"[green]Successfully fixed {len(fix_results)} file(s), pruned {total_pruned} dead code item(s).[/green]"
|
|
@@ -1711,7 +1885,7 @@ def _cmd_dead_code(
|
|
|
1711
1885
|
print_msg(f" [cyan]{rel_p}[/cyan]: {len(res.pruned_items)} pruned")
|
|
1712
1886
|
return 0
|
|
1713
1887
|
|
|
1714
|
-
report = detector.
|
|
1888
|
+
report = detector.scan_files(py_files)
|
|
1715
1889
|
|
|
1716
1890
|
if getattr(args, "json", False):
|
|
1717
1891
|
return _render_dead_code_json(report.items)
|
|
@@ -1791,12 +1965,13 @@ def _cmd_types(
|
|
|
1791
1965
|
args: argparse.Namespace, config: PyCleanerConfig, print_msg, console
|
|
1792
1966
|
) -> int:
|
|
1793
1967
|
"""Bidirectional type checking and inference with Typeshed stubs."""
|
|
1794
|
-
|
|
1795
|
-
if
|
|
1796
|
-
|
|
1968
|
+
targets = getattr(args, "paths", ["."])
|
|
1969
|
+
target_path = Path(targets[0]).resolve() if targets else Path.cwd()
|
|
1970
|
+
root_dir = find_project_root(target_path)
|
|
1971
|
+
py_files = discover_python_files(args.paths, config=config, root=root_dir)
|
|
1797
1972
|
|
|
1798
1973
|
checker = TypeChecker(strict=config.strict_types)
|
|
1799
|
-
report = checker.
|
|
1974
|
+
report = checker.check_files(py_files)
|
|
1800
1975
|
|
|
1801
1976
|
target_base = args.paths[0] if args.paths else "."
|
|
1802
1977
|
if getattr(args, "json", False):
|
|
@@ -1885,12 +2060,13 @@ def _cmd_taint(
|
|
|
1885
2060
|
args: argparse.Namespace, config: PyCleanerConfig, print_msg, console
|
|
1886
2061
|
) -> int:
|
|
1887
2062
|
"""Interprocedural SAST dataflow and taint vulnerability analysis."""
|
|
1888
|
-
|
|
1889
|
-
if
|
|
1890
|
-
|
|
2063
|
+
targets = getattr(args, "paths", ["."])
|
|
2064
|
+
target_path = Path(targets[0]).resolve() if targets else Path.cwd()
|
|
2065
|
+
root_dir = find_project_root(target_path)
|
|
2066
|
+
py_files = discover_python_files(args.paths, config=config, root=root_dir)
|
|
1891
2067
|
|
|
1892
2068
|
engine = TaintEngine()
|
|
1893
|
-
report = engine.
|
|
2069
|
+
report = engine.scan_files(py_files)
|
|
1894
2070
|
|
|
1895
2071
|
target_base = args.paths[0] if args.paths else "."
|
|
1896
2072
|
if getattr(args, "json", False):
|
|
@@ -1964,6 +2140,8 @@ def _run_ultimate_phases(
|
|
|
1964
2140
|
print_msg(
|
|
1965
2141
|
"\n[bold blue]Phase 2: Project Dependency Audit & Reconciliation[/bold blue]"
|
|
1966
2142
|
)
|
|
2143
|
+
args.fix = True
|
|
2144
|
+
args.prune_deps = True
|
|
1967
2145
|
audit_rc = _cmd_audit(args, config, print_msg)
|
|
1968
2146
|
print_msg(
|
|
1969
2147
|
"\n[bold blue]Phase 3: Bidirectional Type Verification & Typeshed Resolution[/bold blue]"
|
|
@@ -1981,7 +2159,7 @@ def _run_ultimate_phases(
|
|
|
1981
2159
|
def _cmd_ultimate(
|
|
1982
2160
|
args: argparse.Namespace, config: PyCleanerConfig, print_msg: Any, console: Any
|
|
1983
2161
|
) -> int:
|
|
1984
|
-
"""The Ultimate Python Tool flagship runner: executes full multi-layer analysis."""
|
|
2162
|
+
"""The Ultimate Python Tool flagship runner: executes full multi-layer analysis and automated healing."""
|
|
1985
2163
|
print_msg(
|
|
1986
2164
|
"[bold magenta]=== PyCleaner Ultimate: Full Spectrum Analysis & Healing ===[/bold magenta]\n"
|
|
1987
2165
|
)
|
|
@@ -1989,14 +2167,15 @@ def _cmd_ultimate(
|
|
|
1989
2167
|
args, config, print_msg, console
|
|
1990
2168
|
)
|
|
1991
2169
|
print_msg(
|
|
1992
|
-
"\n[bold blue]Phase 6: Structural Complexity & Dead Code Discovery[/bold blue]"
|
|
2170
|
+
"\n[bold blue]Phase 6: Structural Complexity & Dead Code Discovery & Healing[/bold blue]"
|
|
1993
2171
|
)
|
|
1994
2172
|
_cmd_complexity(args, config, print_msg, console)
|
|
1995
|
-
|
|
2173
|
+
args.fix = True
|
|
2174
|
+
dead_rc = _cmd_dead_code(args, config, print_msg, console)
|
|
1996
2175
|
print_msg(
|
|
1997
2176
|
"\n[bold magenta]=== Ultimate Python Tool: Analysis Complete ===[/bold magenta]"
|
|
1998
2177
|
)
|
|
1999
|
-
return max(fix_rc, audit_rc, type_rc, taint_rc, scan_rc)
|
|
2178
|
+
return max(fix_rc, audit_rc, type_rc, taint_rc, scan_rc, dead_rc)
|
|
2000
2179
|
|
|
2001
2180
|
|
|
2002
2181
|
def _collect_file_mtimes(files: Sequence[Path]) -> dict[Path, float]:
|
|
@@ -354,17 +354,12 @@ class ComplexityAnalyzer:
|
|
|
354
354
|
except OSError:
|
|
355
355
|
return None
|
|
356
356
|
|
|
357
|
-
def
|
|
358
|
-
|
|
359
|
-
) -> ComplexityReport:
|
|
360
|
-
"""Analyze complexity across all Python files in a project."""
|
|
361
|
-
root = Path(root_dir).resolve()
|
|
357
|
+
def analyze_files(self, target_files: Sequence[Path]) -> ComplexityReport:
|
|
358
|
+
"""Analyze complexity across a specific sequence of Python files without walking directories."""
|
|
362
359
|
all_functions: list[ComplexityMetrics] = []
|
|
363
360
|
files_scanned = 0
|
|
364
361
|
|
|
365
|
-
for fpath in
|
|
366
|
-
root, exclude_patterns=exclude_patterns
|
|
367
|
-
):
|
|
362
|
+
for fpath in target_files:
|
|
368
363
|
try:
|
|
369
364
|
content = fpath.read_text(encoding="utf-8", errors="replace")
|
|
370
365
|
metrics = self.analyze_source(content, filename=str(fpath)).functions
|
|
@@ -376,6 +371,14 @@ class ComplexityAnalyzer:
|
|
|
376
371
|
all_functions.sort(key=lambda f: f.cyclomatic, reverse=True)
|
|
377
372
|
return ComplexityReport(functions=all_functions, files_scanned=files_scanned)
|
|
378
373
|
|
|
374
|
+
def analyze_project(
|
|
375
|
+
self, root_dir: Path | str, exclude_patterns: Sequence[str] = ()
|
|
376
|
+
) -> ComplexityReport:
|
|
377
|
+
"""Analyze complexity across all Python files in a project."""
|
|
378
|
+
root = Path(root_dir).resolve()
|
|
379
|
+
py_files = collect_project_python_files(root, exclude_patterns=exclude_patterns)
|
|
380
|
+
return self.analyze_files(py_files)
|
|
381
|
+
|
|
379
382
|
@staticmethod
|
|
380
383
|
def _count_function_args(node: ast.FunctionDef | ast.AsyncFunctionDef) -> int:
|
|
381
384
|
all_args = node.args.posonlyargs + node.args.args + node.args.kwonlyargs
|