ballpython 2.1.0__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.0 → ballpython-2.1.2}/PKG-INFO +1 -3
- {ballpython-2.1.0 → ballpython-2.1.2}/ballpython.egg-info/PKG-INFO +1 -3
- {ballpython-2.1.0 → ballpython-2.1.2}/ballpython.egg-info/SOURCES.txt +1 -0
- {ballpython-2.1.0 → ballpython-2.1.2}/ballpython.egg-info/requires.txt +0 -4
- {ballpython-2.1.0 → ballpython-2.1.2}/pycleaner/__init__.py +1 -1
- {ballpython-2.1.0 → ballpython-2.1.2}/pycleaner/cli.py +202 -36
- {ballpython-2.1.0 → ballpython-2.1.2}/pycleaner/complexity_analyzer.py +11 -8
- {ballpython-2.1.0 → ballpython-2.1.2}/pycleaner/dead_code_detector.py +156 -43
- {ballpython-2.1.0 → ballpython-2.1.2}/pycleaner/dependency_auditor.py +26 -5
- {ballpython-2.1.0 → ballpython-2.1.2}/pycleaner/discovery.py +17 -0
- {ballpython-2.1.0 → ballpython-2.1.2}/pycleaner/frameworks/__init__.py +8 -1
- {ballpython-2.1.0 → ballpython-2.1.2}/pycleaner/linter_formatter.py +23 -4
- {ballpython-2.1.0 → ballpython-2.1.2}/pycleaner/pipeline.py +99 -11
- {ballpython-2.1.0 → ballpython-2.1.2}/pycleaner/security_scanner.py +149 -14
- {ballpython-2.1.0 → ballpython-2.1.2}/pycleaner/syntax_healer.py +208 -43
- {ballpython-2.1.0 → ballpython-2.1.2}/pycleaner/taint_engine.py +19 -15
- {ballpython-2.1.0 → ballpython-2.1.2}/pycleaner/type_checker.py +128 -25
- {ballpython-2.1.0 → ballpython-2.1.2}/pycleaner/verifier.py +12 -4
- {ballpython-2.1.0 → ballpython-2.1.2}/pyproject.toml +1 -3
- ballpython-2.1.2/tests/test_c_to_a_overhaul.py +411 -0
- {ballpython-2.1.0 → ballpython-2.1.2}/tests/test_dead_code_detector.py +22 -0
- {ballpython-2.1.0 → ballpython-2.1.2}/tests/test_dependency_auditor.py +49 -0
- {ballpython-2.1.0 → ballpython-2.1.2}/tests/test_syntax_healer.py +1 -0
- {ballpython-2.1.0 → ballpython-2.1.2}/tests/test_type_checker.py +106 -1
- {ballpython-2.1.0 → ballpython-2.1.2}/README.md +0 -0
- {ballpython-2.1.0 → ballpython-2.1.2}/ballpython/__init__.py +0 -0
- {ballpython-2.1.0 → ballpython-2.1.2}/ballpython/__main__.py +0 -0
- {ballpython-2.1.0 → ballpython-2.1.2}/ballpython/cli.py +0 -0
- {ballpython-2.1.0 → ballpython-2.1.2}/ballpython/py.typed +0 -0
- {ballpython-2.1.0 → ballpython-2.1.2}/ballpython.egg-info/dependency_links.txt +0 -0
- {ballpython-2.1.0 → ballpython-2.1.2}/ballpython.egg-info/entry_points.txt +0 -0
- {ballpython-2.1.0 → ballpython-2.1.2}/ballpython.egg-info/top_level.txt +0 -0
- {ballpython-2.1.0 → ballpython-2.1.2}/pycleaner/__main__.py +0 -0
- {ballpython-2.1.0 → ballpython-2.1.2}/pycleaner/baseline.py +0 -0
- {ballpython-2.1.0 → ballpython-2.1.2}/pycleaner/cache.py +0 -0
- {ballpython-2.1.0 → ballpython-2.1.2}/pycleaner/config.py +0 -0
- {ballpython-2.1.0 → ballpython-2.1.2}/pycleaner/explanations.py +0 -0
- {ballpython-2.1.0 → ballpython-2.1.2}/pycleaner/frameworks/plugins.py +0 -0
- {ballpython-2.1.0 → ballpython-2.1.2}/pycleaner/import_resolver.py +0 -0
- {ballpython-2.1.0 → ballpython-2.1.2}/pycleaner/modernizer.py +0 -0
- {ballpython-2.1.0 → ballpython-2.1.2}/pycleaner/py.typed +0 -0
- {ballpython-2.1.0 → ballpython-2.1.2}/pycleaner/test_generator.py +0 -0
- {ballpython-2.1.0 → ballpython-2.1.2}/pycleaner/typeshed_resolver.py +0 -0
- {ballpython-2.1.0 → ballpython-2.1.2}/setup.cfg +0 -0
- {ballpython-2.1.0 → ballpython-2.1.2}/tests/test_baseline_and_explain.py +0 -0
- {ballpython-2.1.0 → ballpython-2.1.2}/tests/test_cache.py +0 -0
- {ballpython-2.1.0 → ballpython-2.1.2}/tests/test_cli.py +0 -0
- {ballpython-2.1.0 → ballpython-2.1.2}/tests/test_cli_prove.py +0 -0
- {ballpython-2.1.0 → ballpython-2.1.2}/tests/test_complexity_analyzer.py +0 -0
- {ballpython-2.1.0 → ballpython-2.1.2}/tests/test_config.py +0 -0
- {ballpython-2.1.0 → ballpython-2.1.2}/tests/test_dead_code_fixer.py +0 -0
- {ballpython-2.1.0 → ballpython-2.1.2}/tests/test_discovery.py +0 -0
- {ballpython-2.1.0 → ballpython-2.1.2}/tests/test_frameworks.py +0 -0
- {ballpython-2.1.0 → ballpython-2.1.2}/tests/test_import_resolver.py +0 -0
- {ballpython-2.1.0 → ballpython-2.1.2}/tests/test_linter_formatter.py +0 -0
- {ballpython-2.1.0 → ballpython-2.1.2}/tests/test_modernizer.py +0 -0
- {ballpython-2.1.0 → ballpython-2.1.2}/tests/test_mutation_test_generator.py +0 -0
- {ballpython-2.1.0 → ballpython-2.1.2}/tests/test_pipeline.py +0 -0
- {ballpython-2.1.0 → ballpython-2.1.2}/tests/test_security_scanner.py +0 -0
- {ballpython-2.1.0 → ballpython-2.1.2}/tests/test_taint_engine.py +0 -0
- {ballpython-2.1.0 → ballpython-2.1.2}/tests/test_test_generator.py +0 -0
- {ballpython-2.1.0 → ballpython-2.1.2}/tests/test_verifier.py +0 -0
- {ballpython-2.1.0 → ballpython-2.1.2}/tests/test_verifier_pipeline.py +0 -0
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ballpython
|
|
3
|
-
Version: 2.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
|
|
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.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
|
|
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"
|
|
@@ -555,7 +555,12 @@ def _create_printer(console: Any) -> Any:
|
|
|
555
555
|
else:
|
|
556
556
|
import re
|
|
557
557
|
|
|
558
|
-
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("\\]", "]")
|
|
559
564
|
print(cleaned)
|
|
560
565
|
|
|
561
566
|
return print_msg
|
|
@@ -684,8 +689,13 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
684
689
|
@dataclass
|
|
685
690
|
class _FixBatchState:
|
|
686
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
|
|
687
696
|
error_count: int = 0
|
|
688
697
|
diagnostics: list[dict[str, str]] = field(default_factory=list)
|
|
698
|
+
security_findings: list[Any] = field(default_factory=list)
|
|
689
699
|
proven_count: int = 0
|
|
690
700
|
suggested_count: int = 0
|
|
691
701
|
refused_count: int = 0
|
|
@@ -729,6 +739,12 @@ def _report_file_modifications(
|
|
|
729
739
|
print_msg(" - Lint: fixed errors and pruned unused imports", style="blue")
|
|
730
740
|
if result.format_changed:
|
|
731
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
|
+
)
|
|
732
748
|
|
|
733
749
|
|
|
734
750
|
def _accumulate_result(
|
|
@@ -741,6 +757,8 @@ def _accumulate_result(
|
|
|
741
757
|
print_msg, console, is_json = io_ctx
|
|
742
758
|
if result.diagnostics:
|
|
743
759
|
state.diagnostics.extend(result.diagnostics)
|
|
760
|
+
if getattr(result, "security_findings", None):
|
|
761
|
+
state.security_findings.extend(result.security_findings)
|
|
744
762
|
|
|
745
763
|
if result.proof_receipts:
|
|
746
764
|
state.proof_receipts.extend(result.proof_receipts)
|
|
@@ -766,12 +784,63 @@ def _accumulate_result(
|
|
|
766
784
|
|
|
767
785
|
if result.error:
|
|
768
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
|
+
|
|
769
835
|
if not is_json:
|
|
770
836
|
print_msg(f"[red]ERROR in {py_file.name}:[/red] {result.error}")
|
|
771
837
|
return
|
|
772
838
|
|
|
773
839
|
if result.changed:
|
|
774
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
|
|
775
844
|
if not is_json and not opts.in_progress:
|
|
776
845
|
_report_file_modifications(result, py_file, opts.apply_changes, print_msg)
|
|
777
846
|
if opts.show_diff and result.diff:
|
|
@@ -832,7 +901,9 @@ def _run_progress_fix(
|
|
|
832
901
|
backup=opts.backup,
|
|
833
902
|
in_progress=True,
|
|
834
903
|
)
|
|
835
|
-
|
|
904
|
+
print_msg, console, is_json = io_ctx
|
|
905
|
+
modified_items: list[tuple[CleanResult, Path]] = []
|
|
906
|
+
|
|
836
907
|
with Progress(
|
|
837
908
|
SpinnerColumn(),
|
|
838
909
|
TextColumn("[progress.description]{task.description}"),
|
|
@@ -847,8 +918,18 @@ def _run_progress_fix(
|
|
|
847
918
|
py_file, apply_changes=opts.apply_changes, backup=opts.backup
|
|
848
919
|
)
|
|
849
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))
|
|
850
923
|
progress.advance(task)
|
|
851
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
|
+
|
|
852
933
|
|
|
853
934
|
def _execute_fix_batch(
|
|
854
935
|
pipeline: CleanPipeline,
|
|
@@ -952,8 +1033,14 @@ def _compute_fix_exit_code(
|
|
|
952
1033
|
return 1
|
|
953
1034
|
if is_prove_cmd:
|
|
954
1035
|
return 0 if state.error_count == 0 else 1
|
|
1036
|
+
has_unresolved_security = any(
|
|
1037
|
+
getattr(sf, "severity", None) in ("CRITICAL", "HIGH")
|
|
1038
|
+
for sf in state.security_findings
|
|
1039
|
+
)
|
|
1040
|
+
if has_unresolved_security:
|
|
1041
|
+
return 1
|
|
955
1042
|
if not apply_changes and (
|
|
956
|
-
state.
|
|
1043
|
+
state.would_change_count > 0
|
|
957
1044
|
or state.error_count > 0
|
|
958
1045
|
or bool(audit_report.missing_packages)
|
|
959
1046
|
):
|
|
@@ -1012,7 +1099,11 @@ def _cmd_fix(
|
|
|
1012
1099
|
|
|
1013
1100
|
if is_json:
|
|
1014
1101
|
print(json.dumps(state.diagnostics, indent=2))
|
|
1015
|
-
|
|
1102
|
+
has_sec_issues = any(
|
|
1103
|
+
getattr(sf, "severity", None) in ("CRITICAL", "HIGH")
|
|
1104
|
+
for sf in state.security_findings
|
|
1105
|
+
)
|
|
1106
|
+
return 1 if (state.error_count > 0 or has_sec_issues) else 0
|
|
1016
1107
|
|
|
1017
1108
|
audit_report = _run_fix_audit(
|
|
1018
1109
|
root_dir,
|
|
@@ -1027,10 +1118,64 @@ def _cmd_fix(
|
|
|
1027
1118
|
or getattr(audit_report, "fixed_pyproject", False)
|
|
1028
1119
|
):
|
|
1029
1120
|
state.changed_count += 1
|
|
1121
|
+
state.would_change_count += 1
|
|
1122
|
+
state.actually_written_count += 1
|
|
1123
|
+
|
|
1124
|
+
if not opts.apply_changes:
|
|
1125
|
+
if state.partially_would_change_count > 0:
|
|
1126
|
+
err_label = (
|
|
1127
|
+
"1 remaining error"
|
|
1128
|
+
if state.error_count == 1
|
|
1129
|
+
else f"{state.error_count} remaining errors"
|
|
1130
|
+
)
|
|
1131
|
+
if state.would_change_count > 0:
|
|
1132
|
+
print_msg(
|
|
1133
|
+
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]"
|
|
1134
|
+
)
|
|
1135
|
+
else:
|
|
1136
|
+
print_msg(
|
|
1137
|
+
f"\n[bold]Summary: {len(py_files)} inspected, {state.partially_would_change_count} would partially change, 0 written, {err_label}.[/bold]"
|
|
1138
|
+
)
|
|
1139
|
+
else:
|
|
1140
|
+
print_msg(
|
|
1141
|
+
f"\n[bold]Summary: {len(py_files)} inspected, {state.would_change_count} would change, 0 written, {state.error_count} errors.[/bold]"
|
|
1142
|
+
)
|
|
1143
|
+
else:
|
|
1144
|
+
if state.partially_updated_count > 0:
|
|
1145
|
+
err_label = (
|
|
1146
|
+
"1 remaining error"
|
|
1147
|
+
if state.error_count == 1
|
|
1148
|
+
else f"{state.error_count} remaining errors"
|
|
1149
|
+
)
|
|
1150
|
+
if state.actually_written_count > 0:
|
|
1151
|
+
print_msg(
|
|
1152
|
+
f"\n[bold]Summary: {len(py_files)} inspected, {state.actually_written_count} updated, {state.partially_updated_count} partially updated, {err_label}.[/bold]"
|
|
1153
|
+
)
|
|
1154
|
+
else:
|
|
1155
|
+
print_msg(
|
|
1156
|
+
f"\n[bold]Summary: {len(py_files)} inspected, {state.partially_updated_count} partially updated, {err_label}.[/bold]"
|
|
1157
|
+
)
|
|
1158
|
+
else:
|
|
1159
|
+
print_msg(
|
|
1160
|
+
f"\n[bold]Summary: {len(py_files)} inspected, {state.actually_written_count} updated, {state.error_count} errors.[/bold]"
|
|
1161
|
+
)
|
|
1030
1162
|
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1163
|
+
critical_or_high_findings = [
|
|
1164
|
+
sf
|
|
1165
|
+
for sf in state.security_findings
|
|
1166
|
+
if getattr(sf, "severity", None) in ("CRITICAL", "HIGH")
|
|
1167
|
+
]
|
|
1168
|
+
if critical_or_high_findings and not is_json:
|
|
1169
|
+
print_msg(
|
|
1170
|
+
f"\n[bold red]Unresolved Security Findings ({len(critical_or_high_findings)} issue(s)):[/bold red]"
|
|
1171
|
+
)
|
|
1172
|
+
for sf in critical_or_high_findings:
|
|
1173
|
+
f_path = getattr(sf, "filepath", getattr(sf, "file", "<unknown>"))
|
|
1174
|
+
print_msg(
|
|
1175
|
+
f" [bold red]- [{sf.severity}] {f_path}:{sf.lineno}: {sf.message}[/bold red]"
|
|
1176
|
+
)
|
|
1177
|
+
if getattr(sf, "suggestion", None):
|
|
1178
|
+
print_msg(f" [dim]Suggestion: {sf.suggestion}[/dim]")
|
|
1034
1179
|
|
|
1035
1180
|
if pipeline.verify_proofs:
|
|
1036
1181
|
total_proven_callables = sum(
|
|
@@ -1187,7 +1332,7 @@ def _cmd_baseline(
|
|
|
1187
1332
|
fingerprints: list[BaselineFingerprint] = []
|
|
1188
1333
|
|
|
1189
1334
|
detector = DeadCodeDetector()
|
|
1190
|
-
dead_code_report = detector.
|
|
1335
|
+
dead_code_report = detector.scan_files(py_files)
|
|
1191
1336
|
for item in dead_code_report.items:
|
|
1192
1337
|
if item.kind == "unused-import":
|
|
1193
1338
|
rule = "DC002"
|
|
@@ -1206,7 +1351,7 @@ def _cmd_baseline(
|
|
|
1206
1351
|
)
|
|
1207
1352
|
|
|
1208
1353
|
scanner = SecurityScanner()
|
|
1209
|
-
sec_report = scanner.
|
|
1354
|
+
sec_report = scanner.scan_files(py_files)
|
|
1210
1355
|
for finding in sec_report.findings:
|
|
1211
1356
|
fingerprints.append(
|
|
1212
1357
|
BaselineManager.create_fingerprint(
|
|
@@ -1219,7 +1364,7 @@ def _cmd_baseline(
|
|
|
1219
1364
|
)
|
|
1220
1365
|
|
|
1221
1366
|
analyzer = ComplexityAnalyzer()
|
|
1222
|
-
comp_report = analyzer.
|
|
1367
|
+
comp_report = analyzer.analyze_files(py_files)
|
|
1223
1368
|
thresholds = (
|
|
1224
1369
|
getattr(args, "max_cyclomatic", config.max_cyclomatic_complexity),
|
|
1225
1370
|
getattr(args, "max_cognitive", config.max_cognitive_complexity),
|
|
@@ -1373,9 +1518,10 @@ def _cmd_cache(
|
|
|
1373
1518
|
console: Any,
|
|
1374
1519
|
) -> int:
|
|
1375
1520
|
"""Manage and inspect the content-addressable verification and AST cache."""
|
|
1376
|
-
|
|
1521
|
+
raw_db_path = getattr(args, "db", None) or getattr(
|
|
1377
1522
|
config, "cache_db_path", ".pycleaner/cache.db"
|
|
1378
1523
|
)
|
|
1524
|
+
db_path: str = str(raw_db_path) if raw_db_path else ".pycleaner/cache.db"
|
|
1379
1525
|
cache = ContentAddressableCache(db_path=db_path)
|
|
1380
1526
|
|
|
1381
1527
|
if getattr(args, "clear", False):
|
|
@@ -1445,7 +1591,18 @@ def _render_audit_cli_output(
|
|
|
1445
1591
|
|
|
1446
1592
|
def _cmd_audit(args: argparse.Namespace, config: PyCleanerConfig, print_msg) -> int:
|
|
1447
1593
|
"""Dependency audit command."""
|
|
1448
|
-
|
|
1594
|
+
targets = getattr(args, "paths", ["."])
|
|
1595
|
+
target_path = Path(targets[0]).resolve() if targets else Path.cwd()
|
|
1596
|
+
root_dir = find_project_root(target_path)
|
|
1597
|
+
explicit_files = (
|
|
1598
|
+
[
|
|
1599
|
+
Path(p).resolve()
|
|
1600
|
+
for p in targets
|
|
1601
|
+
if Path(p).is_file() and Path(p).suffix == ".py"
|
|
1602
|
+
]
|
|
1603
|
+
if targets
|
|
1604
|
+
else None
|
|
1605
|
+
)
|
|
1449
1606
|
|
|
1450
1607
|
is_check = getattr(args, "check", False)
|
|
1451
1608
|
fix_flag = getattr(args, "fix", False) or getattr(args, "fix_deps", False)
|
|
@@ -1454,6 +1611,7 @@ def _cmd_audit(args: argparse.Namespace, config: PyCleanerConfig, print_msg) ->
|
|
|
1454
1611
|
audit_report = auditor.audit(
|
|
1455
1612
|
fix=fix_flag and not is_check,
|
|
1456
1613
|
prune_unused=prune_flag and not is_check,
|
|
1614
|
+
target_files=explicit_files or None,
|
|
1457
1615
|
)
|
|
1458
1616
|
|
|
1459
1617
|
if getattr(args, "json", False):
|
|
@@ -1475,7 +1633,7 @@ def _render_scan_json(report: Any) -> int:
|
|
|
1475
1633
|
for f in report.findings
|
|
1476
1634
|
]
|
|
1477
1635
|
print(json.dumps(findings, indent=2))
|
|
1478
|
-
return 1 if report.critical_count > 0 else 0
|
|
1636
|
+
return 1 if (report.critical_count > 0 or report.high_count > 0) else 0
|
|
1479
1637
|
|
|
1480
1638
|
|
|
1481
1639
|
def _render_scan_table(report: Any, console: Any, target_base: str) -> None:
|
|
@@ -1526,23 +1684,24 @@ def _render_scan_cli_summary(
|
|
|
1526
1684
|
f"\n Total: {report.count} finding(s) - "
|
|
1527
1685
|
f"[red]{report.critical_count} critical[/red], [red]{report.high_count} high[/red]"
|
|
1528
1686
|
)
|
|
1529
|
-
return 1 if report.critical_count > 0 else 0
|
|
1687
|
+
return 1 if (report.critical_count > 0 or report.high_count > 0) else 0
|
|
1530
1688
|
|
|
1531
1689
|
|
|
1532
1690
|
def _cmd_scan(
|
|
1533
1691
|
args: argparse.Namespace, config: PyCleanerConfig, print_msg, console
|
|
1534
1692
|
) -> int:
|
|
1535
1693
|
"""Security scan command."""
|
|
1536
|
-
|
|
1537
|
-
if
|
|
1538
|
-
|
|
1694
|
+
targets = getattr(args, "paths", ["."])
|
|
1695
|
+
target_path = Path(targets[0]).resolve() if targets else Path.cwd()
|
|
1696
|
+
root_dir = find_project_root(target_path)
|
|
1697
|
+
py_files = discover_python_files(args.paths, config=config, root=root_dir)
|
|
1539
1698
|
|
|
1540
1699
|
severity = getattr(args, "severity", config.security_severity_threshold)
|
|
1541
1700
|
scanner = SecurityScanner(
|
|
1542
1701
|
severity_threshold=severity,
|
|
1543
1702
|
ignore_rules=set(config.ignore_security_rules),
|
|
1544
1703
|
)
|
|
1545
|
-
report = scanner.
|
|
1704
|
+
report = scanner.scan_files(py_files)
|
|
1546
1705
|
|
|
1547
1706
|
target_base = args.paths[0] if args.paths else "."
|
|
1548
1707
|
if getattr(args, "json", False):
|
|
@@ -1636,12 +1795,13 @@ def _cmd_complexity(
|
|
|
1636
1795
|
args: argparse.Namespace, config: PyCleanerConfig, print_msg, console
|
|
1637
1796
|
) -> int:
|
|
1638
1797
|
"""Complexity analysis command."""
|
|
1639
|
-
|
|
1640
|
-
if
|
|
1641
|
-
|
|
1798
|
+
targets = getattr(args, "paths", ["."])
|
|
1799
|
+
target_path = Path(targets[0]).resolve() if targets else Path.cwd()
|
|
1800
|
+
root_dir = find_project_root(target_path)
|
|
1801
|
+
py_files = discover_python_files(args.paths, config=config, root=root_dir)
|
|
1642
1802
|
|
|
1643
1803
|
analyzer = ComplexityAnalyzer()
|
|
1644
|
-
report = analyzer.
|
|
1804
|
+
report = analyzer.analyze_files(py_files)
|
|
1645
1805
|
|
|
1646
1806
|
thresholds = (
|
|
1647
1807
|
getattr(args, "max_cyclomatic", config.max_cyclomatic_complexity),
|
|
@@ -1711,17 +1871,20 @@ def _cmd_dead_code(
|
|
|
1711
1871
|
args: argparse.Namespace, config: PyCleanerConfig, print_msg, console
|
|
1712
1872
|
) -> int:
|
|
1713
1873
|
"""Dead code detection command."""
|
|
1714
|
-
|
|
1715
|
-
if
|
|
1716
|
-
|
|
1874
|
+
targets = getattr(args, "paths", ["."])
|
|
1875
|
+
target_path = Path(targets[0]).resolve() if targets else Path.cwd()
|
|
1876
|
+
root_dir = find_project_root(target_path)
|
|
1877
|
+
py_files = discover_python_files(args.paths, config=config, root=root_dir)
|
|
1717
1878
|
|
|
1718
1879
|
detector = DeadCodeDetector(
|
|
1719
1880
|
ignore_decorators=set(config.ignore_decorators),
|
|
1720
1881
|
ignore_names=set(config.ignore_names),
|
|
1721
1882
|
)
|
|
1722
1883
|
if getattr(args, "fix", False):
|
|
1723
|
-
print_msg(
|
|
1724
|
-
|
|
1884
|
+
print_msg(
|
|
1885
|
+
f"[bold green]Pruning dead code across {len(py_files)} file(s)...[/bold green]"
|
|
1886
|
+
)
|
|
1887
|
+
fix_results = detector.fix_files(py_files)
|
|
1725
1888
|
total_pruned = sum(len(res.pruned_items) for res in fix_results.values())
|
|
1726
1889
|
print_msg(
|
|
1727
1890
|
f"[green]Successfully fixed {len(fix_results)} file(s), pruned {total_pruned} dead code item(s).[/green]"
|
|
@@ -1734,7 +1897,7 @@ def _cmd_dead_code(
|
|
|
1734
1897
|
print_msg(f" [cyan]{rel_p}[/cyan]: {len(res.pruned_items)} pruned")
|
|
1735
1898
|
return 0
|
|
1736
1899
|
|
|
1737
|
-
report = detector.
|
|
1900
|
+
report = detector.scan_files(py_files)
|
|
1738
1901
|
|
|
1739
1902
|
if getattr(args, "json", False):
|
|
1740
1903
|
return _render_dead_code_json(report.items)
|
|
@@ -1814,12 +1977,13 @@ def _cmd_types(
|
|
|
1814
1977
|
args: argparse.Namespace, config: PyCleanerConfig, print_msg, console
|
|
1815
1978
|
) -> int:
|
|
1816
1979
|
"""Bidirectional type checking and inference with Typeshed stubs."""
|
|
1817
|
-
|
|
1818
|
-
if
|
|
1819
|
-
|
|
1980
|
+
targets = getattr(args, "paths", ["."])
|
|
1981
|
+
target_path = Path(targets[0]).resolve() if targets else Path.cwd()
|
|
1982
|
+
root_dir = find_project_root(target_path)
|
|
1983
|
+
py_files = discover_python_files(args.paths, config=config, root=root_dir)
|
|
1820
1984
|
|
|
1821
1985
|
checker = TypeChecker(strict=config.strict_types)
|
|
1822
|
-
report = checker.
|
|
1986
|
+
report = checker.check_files(py_files)
|
|
1823
1987
|
|
|
1824
1988
|
target_base = args.paths[0] if args.paths else "."
|
|
1825
1989
|
if getattr(args, "json", False):
|
|
@@ -1908,12 +2072,13 @@ def _cmd_taint(
|
|
|
1908
2072
|
args: argparse.Namespace, config: PyCleanerConfig, print_msg, console
|
|
1909
2073
|
) -> int:
|
|
1910
2074
|
"""Interprocedural SAST dataflow and taint vulnerability analysis."""
|
|
1911
|
-
|
|
1912
|
-
if
|
|
1913
|
-
|
|
2075
|
+
targets = getattr(args, "paths", ["."])
|
|
2076
|
+
target_path = Path(targets[0]).resolve() if targets else Path.cwd()
|
|
2077
|
+
root_dir = find_project_root(target_path)
|
|
2078
|
+
py_files = discover_python_files(args.paths, config=config, root=root_dir)
|
|
1914
2079
|
|
|
1915
2080
|
engine = TaintEngine()
|
|
1916
|
-
report = engine.
|
|
2081
|
+
report = engine.scan_files(py_files)
|
|
1917
2082
|
|
|
1918
2083
|
target_base = args.paths[0] if args.paths else "."
|
|
1919
2084
|
if getattr(args, "json", False):
|
|
@@ -2118,6 +2283,7 @@ def _cmd_watch(
|
|
|
2118
2283
|
except KeyboardInterrupt:
|
|
2119
2284
|
print_msg("\n[bold]Watch mode stopped.[/bold]")
|
|
2120
2285
|
return 0
|
|
2286
|
+
return 0
|
|
2121
2287
|
|
|
2122
2288
|
|
|
2123
2289
|
def _cmd_hook(args: argparse.Namespace, print_msg) -> int:
|
|
@@ -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
|