ballpython 2.0.7__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.
@@ -1,14 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ballpython
3
- Version: 2.0.7
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"
@@ -0,0 +1,34 @@
1
+ ballpython/__init__.py,sha256=GSB-LHdou1Un6DS6Zmh8tv_zrwh6HQHO3Py2z8GO39o,304
2
+ ballpython/__main__.py,sha256=aCRr0vnZfKYHDrPHBsYSjX5xTxtFJpTTSPVUSZTBK-I,136
3
+ ballpython/cli.py,sha256=LllCLRAQQ-fRR_lIXwmd2e3Q7DLCIvf-5jHBXsAhJWs,125
4
+ ballpython/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ pycleaner/__init__.py,sha256=aDgSTYYMyPj4l6QFHsUm9QvDA0z7g4FTJCiaE2Vsi7Y,3042
6
+ pycleaner/__main__.py,sha256=8QBFrPyxId563F6cipPMNVEBTG8TIZUgxQfp8gk1VSI,135
7
+ pycleaner/baseline.py,sha256=ljED3x3akie0SB3fzhVno_1UHlI2id-9vta0NIGC0mw,3857
8
+ pycleaner/cache.py,sha256=T3JIxYg8ZezRbSpXYXPD-SyMUkjg4nfKsdO0IuNypOY,10774
9
+ pycleaner/cli.py,sha256=p96G9t2EhPThJYOy9UkCxarigLkQE5b-kPTfujPCdPw,80824
10
+ pycleaner/complexity_analyzer.py,sha256=jOLzOCA_rIHerqiN0w9VCdvO9CTqmncef4CUG4uL6kg,15214
11
+ pycleaner/config.py,sha256=XhloyaY_9pdwbQuc3QaAPCerLeXz-i1da4xT50h-pjY,8514
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
+ pycleaner/explanations.py,sha256=2OOU_l3YL5o_Br_6fP6MF8HlY75-1JRmw3eTYT2lu8M,14219
16
+ pycleaner/import_resolver.py,sha256=Wgrch335AQVsnDv5-7F0l4Zm2oQPkNebPEgTRa7ayAg,32855
17
+ pycleaner/linter_formatter.py,sha256=wdTjwguPWzkJ9KRMcQpFHEirb3ogc9qay69yiphz16Y,21356
18
+ pycleaner/modernizer.py,sha256=kd9_G5EzdW_O-9zm1hA6v33Sb__UbpMZzSvopF-UK5w,16820
19
+ pycleaner/pipeline.py,sha256=EZtlfD8vQOaIm57EWY5h9UHGqiYKcS6B_9Fk9XV6IUY,24751
20
+ pycleaner/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
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
+ pycleaner/test_generator.py,sha256=MM3t0Dx5lyJB5s144IKXq2tXrE8ohKcSvicPR3c9MlU,19680
25
+ pycleaner/type_checker.py,sha256=qNlGccIZzWuBq_iOt3C9AwZe96-KrXwuPDPZ-iIN3nY,41720
26
+ pycleaner/typeshed_resolver.py,sha256=N-kWU_CI5DG59lQMkt7wMW9sHYjNCBdfFS42b4y2Kr4,12742
27
+ pycleaner/verifier.py,sha256=OySQpcBGnHwYfRZHQnKU7qsGao8lCwJ_Uc16Rn0mYS0,19453
28
+ pycleaner/frameworks/__init__.py,sha256=SOuSRl39af86eoQx7Qm8rTJGD4jsnEvgyhhQ6cVmRVA,3462
29
+ pycleaner/frameworks/plugins.py,sha256=qunMzS3q7xp1GEGxqkuhrnm-raiSRhYQMzmDxsEhSUg,11452
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
@@ -18,7 +18,7 @@ from pycleaner.discovery import (
18
18
  is_protected_file,
19
19
  )
20
20
 
21
- __version__ = "2.0.5"
21
+ __version__ = "2.1.1"
22
22
  __all__ = [
23
23
  "DEFAULT_IGNORED_DIRS",
24
24
  "PROTECTED_FILE_PATTERNS",
pycleaner/cache.py CHANGED
@@ -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
- pass # Best-effort dependency extraction; syntax errors handled downstream
51
+ logging.getLogger(__name__).debug("Suppressed exception", exc_info=True)
50
52
  return sorted(set(deps))
51
53
 
52
54
 
pycleaner/cli.py CHANGED
@@ -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(r"\[/?[a-z ]*\]", "", msg)
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]requirements.txt synchronized.[/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 = getattr(args, "fix_deps", False) or getattr(args, "fix_all", False)
922
- prune_any = getattr(args, "prune_deps", False) or getattr(args, "fix_all", False)
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.changed_count > 0
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
- return 0 if state.error_count == 0 else 1
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
- print_msg(
1013
- f"\n[bold]Summary: {len(py_files)} inspected, {state.changed_count} updated, {state.error_count} errors.[/bold]"
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.scan_project(root_dir)
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.scan_project(root_dir)
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.analyze_project(root_dir)
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
- db_path = getattr(args, "db", None) or getattr(
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
- print_msg(" [green]requirements.txt synchronized.[/green]")
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
- root_dir = find_project_root(args.paths[0] if args.paths else None)
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=getattr(args, "fix_deps", False) and not is_check,
1433
- prune_unused=getattr(args, "prune_deps", False) and not is_check,
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
- root_dir = Path(args.paths[0]).resolve() if args.paths else Path.cwd()
1514
- if not root_dir.is_dir():
1515
- root_dir = root_dir.parent
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.scan_project(root_dir, exclude_patterns=config.exclude)
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
- root_dir = Path(args.paths[0]).resolve() if args.paths else Path.cwd()
1617
- if not root_dir.is_dir():
1618
- root_dir = root_dir.parent
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.analyze_project(root_dir, exclude_patterns=config.exclude)
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
- root_dir = Path(args.paths[0]).resolve() if args.paths else Path.cwd()
1692
- if not root_dir.is_dir():
1693
- root_dir = root_dir.parent
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(f"[bold green]Pruning dead code across {root_dir}...[/bold green]")
1701
- fix_results = detector.fix_project(root_dir, exclude_patterns=config.exclude)
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.scan_project(root_dir, exclude_patterns=config.exclude)
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
- root_dir = Path(args.paths[0]).resolve() if args.paths else Path.cwd()
1795
- if not root_dir.is_dir():
1796
- root_dir = root_dir.parent
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.check_project(root_dir, exclude_patterns=config.exclude)
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
- root_dir = Path(args.paths[0]).resolve() if args.paths else Path.cwd()
1889
- if not root_dir.is_dir():
1890
- root_dir = root_dir.parent
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.scan_path(root_dir, exclude_patterns=config.exclude)
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
- _cmd_dead_code(args, config, print_msg, console)
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 analyze_project(
358
- self, root_dir: Path | str, exclude_patterns: Sequence[str] = ()
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 collect_project_python_files(
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