ballpython 2.1.0__py3-none-any.whl → 2.1.1__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.
- {ballpython-2.1.0.dist-info → ballpython-2.1.1.dist-info}/METADATA +1 -3
- {ballpython-2.1.0.dist-info → ballpython-2.1.1.dist-info}/RECORD +17 -17
- pycleaner/__init__.py +1 -1
- pycleaner/cli.py +188 -35
- pycleaner/complexity_analyzer.py +11 -8
- pycleaner/dead_code_detector.py +118 -35
- pycleaner/dependency_auditor.py +26 -5
- pycleaner/discovery.py +17 -0
- pycleaner/pipeline.py +99 -11
- pycleaner/security_scanner.py +149 -14
- pycleaner/syntax_healer.py +208 -43
- pycleaner/taint_engine.py +19 -15
- pycleaner/type_checker.py +128 -25
- pycleaner/verifier.py +12 -4
- {ballpython-2.1.0.dist-info → ballpython-2.1.1.dist-info}/WHEEL +0 -0
- {ballpython-2.1.0.dist-info → ballpython-2.1.1.dist-info}/entry_points.txt +0 -0
- {ballpython-2.1.0.dist-info → ballpython-2.1.1.dist-info}/top_level.txt +0 -0
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ballpython
|
|
3
|
-
Version: 2.1.
|
|
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"
|
|
@@ -2,33 +2,33 @@ ballpython/__init__.py,sha256=GSB-LHdou1Un6DS6Zmh8tv_zrwh6HQHO3Py2z8GO39o,304
|
|
|
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=
|
|
5
|
+
pycleaner/__init__.py,sha256=aDgSTYYMyPj4l6QFHsUm9QvDA0z7g4FTJCiaE2Vsi7Y,3042
|
|
6
6
|
pycleaner/__main__.py,sha256=8QBFrPyxId563F6cipPMNVEBTG8TIZUgxQfp8gk1VSI,135
|
|
7
7
|
pycleaner/baseline.py,sha256=ljED3x3akie0SB3fzhVno_1UHlI2id-9vta0NIGC0mw,3857
|
|
8
8
|
pycleaner/cache.py,sha256=T3JIxYg8ZezRbSpXYXPD-SyMUkjg4nfKsdO0IuNypOY,10774
|
|
9
|
-
pycleaner/cli.py,sha256=
|
|
10
|
-
pycleaner/complexity_analyzer.py,sha256=
|
|
9
|
+
pycleaner/cli.py,sha256=p96G9t2EhPThJYOy9UkCxarigLkQE5b-kPTfujPCdPw,80824
|
|
10
|
+
pycleaner/complexity_analyzer.py,sha256=jOLzOCA_rIHerqiN0w9VCdvO9CTqmncef4CUG4uL6kg,15214
|
|
11
11
|
pycleaner/config.py,sha256=XhloyaY_9pdwbQuc3QaAPCerLeXz-i1da4xT50h-pjY,8514
|
|
12
|
-
pycleaner/dead_code_detector.py,sha256=
|
|
13
|
-
pycleaner/dependency_auditor.py,sha256=
|
|
14
|
-
pycleaner/discovery.py,sha256=
|
|
12
|
+
pycleaner/dead_code_detector.py,sha256=4xu6h0lnsZSiUFykuj7DXUvf9aYpy0y1G_JFUwU0GZg,55452
|
|
13
|
+
pycleaner/dependency_auditor.py,sha256=PTIJhVyVxSDM-YjU1EAMhSx823vYz-7lYs8yPh5l6Tg,24135
|
|
14
|
+
pycleaner/discovery.py,sha256=2P9WU7c_GXROXLDXhh1QrLv8YINWmVQ39VOOMbvrP2g,6717
|
|
15
15
|
pycleaner/explanations.py,sha256=2OOU_l3YL5o_Br_6fP6MF8HlY75-1JRmw3eTYT2lu8M,14219
|
|
16
16
|
pycleaner/import_resolver.py,sha256=Wgrch335AQVsnDv5-7F0l4Zm2oQPkNebPEgTRa7ayAg,32855
|
|
17
17
|
pycleaner/linter_formatter.py,sha256=wdTjwguPWzkJ9KRMcQpFHEirb3ogc9qay69yiphz16Y,21356
|
|
18
18
|
pycleaner/modernizer.py,sha256=kd9_G5EzdW_O-9zm1hA6v33Sb__UbpMZzSvopF-UK5w,16820
|
|
19
|
-
pycleaner/pipeline.py,sha256=
|
|
19
|
+
pycleaner/pipeline.py,sha256=EZtlfD8vQOaIm57EWY5h9UHGqiYKcS6B_9Fk9XV6IUY,24751
|
|
20
20
|
pycleaner/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
21
|
-
pycleaner/security_scanner.py,sha256=
|
|
22
|
-
pycleaner/syntax_healer.py,sha256=
|
|
23
|
-
pycleaner/taint_engine.py,sha256
|
|
21
|
+
pycleaner/security_scanner.py,sha256=PAOwOfkVxMAYNVBjegmKbeWVJ1J6O8u7TO2JR99JQO4,28523
|
|
22
|
+
pycleaner/syntax_healer.py,sha256=9ycIfUxtX6IunlHpMh-_xW7so-FBwSmsIUTdRGin1sc,34905
|
|
23
|
+
pycleaner/taint_engine.py,sha256=Xp2vKjougug_55jEGrQ16nsG7nfeZdGLB7k07qeiWpk,27075
|
|
24
24
|
pycleaner/test_generator.py,sha256=MM3t0Dx5lyJB5s144IKXq2tXrE8ohKcSvicPR3c9MlU,19680
|
|
25
|
-
pycleaner/type_checker.py,sha256=
|
|
25
|
+
pycleaner/type_checker.py,sha256=qNlGccIZzWuBq_iOt3C9AwZe96-KrXwuPDPZ-iIN3nY,41720
|
|
26
26
|
pycleaner/typeshed_resolver.py,sha256=N-kWU_CI5DG59lQMkt7wMW9sHYjNCBdfFS42b4y2Kr4,12742
|
|
27
|
-
pycleaner/verifier.py,sha256=
|
|
27
|
+
pycleaner/verifier.py,sha256=OySQpcBGnHwYfRZHQnKU7qsGao8lCwJ_Uc16Rn0mYS0,19453
|
|
28
28
|
pycleaner/frameworks/__init__.py,sha256=SOuSRl39af86eoQx7Qm8rTJGD4jsnEvgyhhQ6cVmRVA,3462
|
|
29
29
|
pycleaner/frameworks/plugins.py,sha256=qunMzS3q7xp1GEGxqkuhrnm-raiSRhYQMzmDxsEhSUg,11452
|
|
30
|
-
ballpython-2.1.
|
|
31
|
-
ballpython-2.1.
|
|
32
|
-
ballpython-2.1.
|
|
33
|
-
ballpython-2.1.
|
|
34
|
-
ballpython-2.1.
|
|
30
|
+
ballpython-2.1.1.dist-info/METADATA,sha256=eAMzWv_heKS0P60R-uABCbTAJeSXFUuTZ2WuglqK7HY,3385
|
|
31
|
+
ballpython-2.1.1.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
32
|
+
ballpython-2.1.1.dist-info/entry_points.txt,sha256=6ux-5UxoVSmlK5p9M-GDVt24sCdwTQq9wgRDkpMIYU8,81
|
|
33
|
+
ballpython-2.1.1.dist-info/top_level.txt,sha256=u6lmy8RWZ_saxCZAQTL7EijB4sunegOiVyk5fHjpwLk,21
|
|
34
|
+
ballpython-2.1.1.dist-info/RECORD,,
|
pycleaner/__init__.py
CHANGED
pycleaner/cli.py
CHANGED
|
@@ -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:
|
|
@@ -952,8 +1021,14 @@ def _compute_fix_exit_code(
|
|
|
952
1021
|
return 1
|
|
953
1022
|
if is_prove_cmd:
|
|
954
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
|
|
955
1030
|
if not apply_changes and (
|
|
956
|
-
state.
|
|
1031
|
+
state.would_change_count > 0
|
|
957
1032
|
or state.error_count > 0
|
|
958
1033
|
or bool(audit_report.missing_packages)
|
|
959
1034
|
):
|
|
@@ -1012,7 +1087,11 @@ def _cmd_fix(
|
|
|
1012
1087
|
|
|
1013
1088
|
if is_json:
|
|
1014
1089
|
print(json.dumps(state.diagnostics, indent=2))
|
|
1015
|
-
|
|
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
|
|
1016
1095
|
|
|
1017
1096
|
audit_report = _run_fix_audit(
|
|
1018
1097
|
root_dir,
|
|
@@ -1027,10 +1106,64 @@ def _cmd_fix(
|
|
|
1027
1106
|
or getattr(audit_report, "fixed_pyproject", False)
|
|
1028
1107
|
):
|
|
1029
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
|
+
)
|
|
1030
1150
|
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
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]")
|
|
1034
1167
|
|
|
1035
1168
|
if pipeline.verify_proofs:
|
|
1036
1169
|
total_proven_callables = sum(
|
|
@@ -1187,7 +1320,7 @@ def _cmd_baseline(
|
|
|
1187
1320
|
fingerprints: list[BaselineFingerprint] = []
|
|
1188
1321
|
|
|
1189
1322
|
detector = DeadCodeDetector()
|
|
1190
|
-
dead_code_report = detector.
|
|
1323
|
+
dead_code_report = detector.scan_files(py_files)
|
|
1191
1324
|
for item in dead_code_report.items:
|
|
1192
1325
|
if item.kind == "unused-import":
|
|
1193
1326
|
rule = "DC002"
|
|
@@ -1206,7 +1339,7 @@ def _cmd_baseline(
|
|
|
1206
1339
|
)
|
|
1207
1340
|
|
|
1208
1341
|
scanner = SecurityScanner()
|
|
1209
|
-
sec_report = scanner.
|
|
1342
|
+
sec_report = scanner.scan_files(py_files)
|
|
1210
1343
|
for finding in sec_report.findings:
|
|
1211
1344
|
fingerprints.append(
|
|
1212
1345
|
BaselineManager.create_fingerprint(
|
|
@@ -1219,7 +1352,7 @@ def _cmd_baseline(
|
|
|
1219
1352
|
)
|
|
1220
1353
|
|
|
1221
1354
|
analyzer = ComplexityAnalyzer()
|
|
1222
|
-
comp_report = analyzer.
|
|
1355
|
+
comp_report = analyzer.analyze_files(py_files)
|
|
1223
1356
|
thresholds = (
|
|
1224
1357
|
getattr(args, "max_cyclomatic", config.max_cyclomatic_complexity),
|
|
1225
1358
|
getattr(args, "max_cognitive", config.max_cognitive_complexity),
|
|
@@ -1373,9 +1506,10 @@ def _cmd_cache(
|
|
|
1373
1506
|
console: Any,
|
|
1374
1507
|
) -> int:
|
|
1375
1508
|
"""Manage and inspect the content-addressable verification and AST cache."""
|
|
1376
|
-
|
|
1509
|
+
raw_db_path = getattr(args, "db", None) or getattr(
|
|
1377
1510
|
config, "cache_db_path", ".pycleaner/cache.db"
|
|
1378
1511
|
)
|
|
1512
|
+
db_path: str = str(raw_db_path) if raw_db_path else ".pycleaner/cache.db"
|
|
1379
1513
|
cache = ContentAddressableCache(db_path=db_path)
|
|
1380
1514
|
|
|
1381
1515
|
if getattr(args, "clear", False):
|
|
@@ -1445,7 +1579,18 @@ def _render_audit_cli_output(
|
|
|
1445
1579
|
|
|
1446
1580
|
def _cmd_audit(args: argparse.Namespace, config: PyCleanerConfig, print_msg) -> int:
|
|
1447
1581
|
"""Dependency audit command."""
|
|
1448
|
-
|
|
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
|
+
)
|
|
1449
1594
|
|
|
1450
1595
|
is_check = getattr(args, "check", False)
|
|
1451
1596
|
fix_flag = getattr(args, "fix", False) or getattr(args, "fix_deps", False)
|
|
@@ -1454,6 +1599,7 @@ def _cmd_audit(args: argparse.Namespace, config: PyCleanerConfig, print_msg) ->
|
|
|
1454
1599
|
audit_report = auditor.audit(
|
|
1455
1600
|
fix=fix_flag and not is_check,
|
|
1456
1601
|
prune_unused=prune_flag and not is_check,
|
|
1602
|
+
target_files=explicit_files or None,
|
|
1457
1603
|
)
|
|
1458
1604
|
|
|
1459
1605
|
if getattr(args, "json", False):
|
|
@@ -1475,7 +1621,7 @@ def _render_scan_json(report: Any) -> int:
|
|
|
1475
1621
|
for f in report.findings
|
|
1476
1622
|
]
|
|
1477
1623
|
print(json.dumps(findings, indent=2))
|
|
1478
|
-
return 1 if report.critical_count > 0 else 0
|
|
1624
|
+
return 1 if (report.critical_count > 0 or report.high_count > 0) else 0
|
|
1479
1625
|
|
|
1480
1626
|
|
|
1481
1627
|
def _render_scan_table(report: Any, console: Any, target_base: str) -> None:
|
|
@@ -1526,23 +1672,24 @@ def _render_scan_cli_summary(
|
|
|
1526
1672
|
f"\n Total: {report.count} finding(s) - "
|
|
1527
1673
|
f"[red]{report.critical_count} critical[/red], [red]{report.high_count} high[/red]"
|
|
1528
1674
|
)
|
|
1529
|
-
return 1 if report.critical_count > 0 else 0
|
|
1675
|
+
return 1 if (report.critical_count > 0 or report.high_count > 0) else 0
|
|
1530
1676
|
|
|
1531
1677
|
|
|
1532
1678
|
def _cmd_scan(
|
|
1533
1679
|
args: argparse.Namespace, config: PyCleanerConfig, print_msg, console
|
|
1534
1680
|
) -> int:
|
|
1535
1681
|
"""Security scan command."""
|
|
1536
|
-
|
|
1537
|
-
if
|
|
1538
|
-
|
|
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)
|
|
1539
1686
|
|
|
1540
1687
|
severity = getattr(args, "severity", config.security_severity_threshold)
|
|
1541
1688
|
scanner = SecurityScanner(
|
|
1542
1689
|
severity_threshold=severity,
|
|
1543
1690
|
ignore_rules=set(config.ignore_security_rules),
|
|
1544
1691
|
)
|
|
1545
|
-
report = scanner.
|
|
1692
|
+
report = scanner.scan_files(py_files)
|
|
1546
1693
|
|
|
1547
1694
|
target_base = args.paths[0] if args.paths else "."
|
|
1548
1695
|
if getattr(args, "json", False):
|
|
@@ -1636,12 +1783,13 @@ def _cmd_complexity(
|
|
|
1636
1783
|
args: argparse.Namespace, config: PyCleanerConfig, print_msg, console
|
|
1637
1784
|
) -> int:
|
|
1638
1785
|
"""Complexity analysis command."""
|
|
1639
|
-
|
|
1640
|
-
if
|
|
1641
|
-
|
|
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)
|
|
1642
1790
|
|
|
1643
1791
|
analyzer = ComplexityAnalyzer()
|
|
1644
|
-
report = analyzer.
|
|
1792
|
+
report = analyzer.analyze_files(py_files)
|
|
1645
1793
|
|
|
1646
1794
|
thresholds = (
|
|
1647
1795
|
getattr(args, "max_cyclomatic", config.max_cyclomatic_complexity),
|
|
@@ -1711,17 +1859,20 @@ def _cmd_dead_code(
|
|
|
1711
1859
|
args: argparse.Namespace, config: PyCleanerConfig, print_msg, console
|
|
1712
1860
|
) -> int:
|
|
1713
1861
|
"""Dead code detection command."""
|
|
1714
|
-
|
|
1715
|
-
if
|
|
1716
|
-
|
|
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)
|
|
1717
1866
|
|
|
1718
1867
|
detector = DeadCodeDetector(
|
|
1719
1868
|
ignore_decorators=set(config.ignore_decorators),
|
|
1720
1869
|
ignore_names=set(config.ignore_names),
|
|
1721
1870
|
)
|
|
1722
1871
|
if getattr(args, "fix", False):
|
|
1723
|
-
print_msg(
|
|
1724
|
-
|
|
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)
|
|
1725
1876
|
total_pruned = sum(len(res.pruned_items) for res in fix_results.values())
|
|
1726
1877
|
print_msg(
|
|
1727
1878
|
f"[green]Successfully fixed {len(fix_results)} file(s), pruned {total_pruned} dead code item(s).[/green]"
|
|
@@ -1734,7 +1885,7 @@ def _cmd_dead_code(
|
|
|
1734
1885
|
print_msg(f" [cyan]{rel_p}[/cyan]: {len(res.pruned_items)} pruned")
|
|
1735
1886
|
return 0
|
|
1736
1887
|
|
|
1737
|
-
report = detector.
|
|
1888
|
+
report = detector.scan_files(py_files)
|
|
1738
1889
|
|
|
1739
1890
|
if getattr(args, "json", False):
|
|
1740
1891
|
return _render_dead_code_json(report.items)
|
|
@@ -1814,12 +1965,13 @@ def _cmd_types(
|
|
|
1814
1965
|
args: argparse.Namespace, config: PyCleanerConfig, print_msg, console
|
|
1815
1966
|
) -> int:
|
|
1816
1967
|
"""Bidirectional type checking and inference with Typeshed stubs."""
|
|
1817
|
-
|
|
1818
|
-
if
|
|
1819
|
-
|
|
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)
|
|
1820
1972
|
|
|
1821
1973
|
checker = TypeChecker(strict=config.strict_types)
|
|
1822
|
-
report = checker.
|
|
1974
|
+
report = checker.check_files(py_files)
|
|
1823
1975
|
|
|
1824
1976
|
target_base = args.paths[0] if args.paths else "."
|
|
1825
1977
|
if getattr(args, "json", False):
|
|
@@ -1908,12 +2060,13 @@ def _cmd_taint(
|
|
|
1908
2060
|
args: argparse.Namespace, config: PyCleanerConfig, print_msg, console
|
|
1909
2061
|
) -> int:
|
|
1910
2062
|
"""Interprocedural SAST dataflow and taint vulnerability analysis."""
|
|
1911
|
-
|
|
1912
|
-
if
|
|
1913
|
-
|
|
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)
|
|
1914
2067
|
|
|
1915
2068
|
engine = TaintEngine()
|
|
1916
|
-
report = engine.
|
|
2069
|
+
report = engine.scan_files(py_files)
|
|
1917
2070
|
|
|
1918
2071
|
target_base = args.paths[0] if args.paths else "."
|
|
1919
2072
|
if getattr(args, "json", False):
|
pycleaner/complexity_analyzer.py
CHANGED
|
@@ -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
|