144 verbose: bool =
False,
145 exclude_dirs: Optional[set] =
None,
146 wildcard_exclude: Optional[list] =
None,
147 active_rules: Optional[dict] =
None,
148 analysis_cache: Optional[Dict[Path, Dict[str, Any]]] =
None,
151 Run linter checks on all Python files and aggregate pass/fail statistics.
153 Returns a dict with total_files, fully_compliant count, per-check pass
154 counts, and compliance percentage.
156 func =
"check_lint_health"
158 if exclude_dirs
is None:
159 exclude_dirs = set(LINTER_EXCLUDE_DIRS)
160 if wildcard_exclude
is None:
161 wildcard_exclude = list(WILDCARD_EXCLUDE_DIRS)
162 if active_rules
is None:
163 active_rules = dict(_DEFAULT_RULES)
165 python_files = list(find_python_files(project_root, exclude_dirs, wildcard_exclude))
172 "header",
"docstring",
"dependencies_section",
"diagnostics_profile",
173 "dbg_usage",
"dbg_import",
"func_pattern",
"syntax_warnings",
175 active_checks = [c
for c
in health_checks
if active_rules.get(c,
True)]
179 option_c_dir(project_root) /
"cache" /
"lint-cache.json",
181 linter_identity=current_linter_identity(),
184 total = len(python_files)
185 counters = {c: 0
for c
in active_checks}
189 not_run = {c: 0
for c
in active_checks}
195 applicable = {c: 0
for c
in active_checks}
202 "header":
lambda text, lines, py_file, tree, sw: validate_header_block(
203 lines[:4]
if len(lines) >= 4
else lines, py_file, ctx),
204 "docstring":
lambda text, lines, py_file, tree, sw: check_docstring(text, tree=tree),
205 "dependencies_section":
lambda text, lines, py_file, tree, sw: check_dependencies_section(text, tree=tree),
206 "diagnostics_profile":
lambda text, lines, py_file, tree, sw: check_diagnostics_profile(text, tree=tree),
207 "dbg_usage":
lambda text, lines, py_file, tree, sw: check_dbg_usage(text, tree=tree),
208 "dbg_import":
lambda text, lines, py_file, tree, sw: check_dbg_import(text, tree=tree),
209 "func_pattern":
lambda text, lines, py_file, tree, sw: check_func_pattern(text, tree=tree),
210 "syntax_warnings":
lambda text, lines, py_file, tree, sw: check_syntax_warnings(text, warnings_list=sw),
213 for py_file
in python_files:
215 text = py_file.read_text(encoding=
"utf-8")
219 lines = text.split(
'\n')
220 file_hash = content_hash(text)
226 file_rules, _file_rule_changes = effective_rules(
227 active_rules, to_project_relative(py_file, project_root),
230 file_checks = [c
for c
in health_checks
if file_rules.get(c,
True)]
231 cache_rules = dict(file_rules)
232 cache_rules[
"_health_dashboard"] =
True
233 file_rules_hash = rules_hash(cache_rules)
234 cached = cache.get(file_hash, file_rules_hash)
235 syntax_error_message: str |
None =
None
236 checks: Dict[str, Any]
238 if cached
is not None and all(
239 check_name
in cached.rule_results
for check_name
in file_checks
242 check_name: cached.rule_results[check_name]
243 for check_name
in file_checks
246 syntax_error_message = cached.json_detail.get(
"syntax_error_message")
250 if "header" in checks:
251 checks[
"header"], _ = validate_header_block(
252 lines[:4]
if len(lines) >= 4
else lines,
258 tree, syntax_warnings_list = safe_parse(text)
259 syntax_error_message = (
260 _describe_syntax_error(text)
if tree
is None else None
263 for check_name
in file_checks:
264 if check_name ==
"syntax_error":
267 ok = tree
is not None
268 elif tree
is None and check_name
in _AST_DEPENDENT_HEALTH_CHECKS:
271 checks[check_name] =
None
274 ok, _ = check_funcs[check_name](
275 text, lines, py_file, tree, syntax_warnings_list,
277 checks[check_name] = ok
282 content_hash=file_hash,
285 "syntax_error_message": syntax_error_message,
287 oct_version=__version__,
288 timestamp=datetime.now().isoformat(),
308 waived_checks = inline_disabled_rules(text) - _UNWAIVABLE_HEALTH_CHECKS
310 for check_name, ok
in checks.items():
311 if ok
is False and check_name
in waived_checks:
312 checks[check_name] =
True
314 if analysis_cache
is not None:
315 analysis_cache[py_file] = {
320 for check_name, ok
in checks.items():
324 applicable[check_name] = applicable.get(check_name, 0) + 1
326 not_run[check_name] = not_run.get(check_name, 0) + 1
329 counters[check_name] = counters.get(check_name, 0) + 1
331 counters.setdefault(check_name, 0)
335 file_compliant = all(v
for v
in checks.values()
if v
is not None)
340 rel_path = to_project_relative(py_file, project_root)
341 detail: Dict[str, Any] = {
344 "compliant": file_compliant,
346 if syntax_error_message
is not None:
349 detail[
"syntax_error_message"] = syntax_error_message
350 file_details.append(detail)
358 compliance_pct = round((fully_compliant / total * 100), 1)
if total > 0
else 0.0
361 "total_files": total,
362 "fully_compliant": fully_compliant,
363 "compliance_pct": compliance_pct,
365 "cache_hits": cache_hits,
368 for check_name, count
in counters.items():
374 skipped = not_run.get(check_name, 0)
375 ran = applicable.get(check_name, total) - skipped
376 pct = round((count / ran * 100), 1)
if ran > 0
else 0.0
377 result[
"checks"][check_name] = {
385 result[
"files"] = file_details
393 exclude_dirs: Optional[set] =
None,
394 wildcard_exclude: Optional[list] =
None,
395 active_rules: Optional[dict] =
None,
396 verbose: bool =
False,
397 analysis_cache: Optional[Dict[Path, Dict[str, Any]]] =
None,
400 Count violations that oct format could fix automatically.
402 Runs formatter checks in dry-run mode (no modifications).
403 Only counts fixes for rules that are active.
405 Plan 269 - "active" is a PER-FILE question wherever the project uses
406 ``linter.path_rules``. *active_rules* is the run's set; each file's
407 own set is resolved from it with
408 :func:`~oct.linter.oct_lint.effective_rules` over ``ctx.path_rules``,
409 which ``run_health`` has already parsed for :func:`check_lint_health`,
410 so the panel counts a fix only where the fixer's rule applies to that
411 file. Without it a module a ``tests/`` entry excused from
412 ``dbg_import`` was still offered the fix - the panel disagreeing with
413 ``oct lint`` on exactly the files the operator asked to treat
414 differently. The counter set is widened the same way: a rule a path
415 entry switches ON somewhere gets a counter even where the run
416 disables it, or the files it applies to could never be counted at
417 all. A project with no path rules produces byte-identical output.
419 Plan 277 - the file's own inline ``# OCT-LINT: disable[-file]=<rule>``
420 waivers are the third thing that can excuse a fixer, and the panel
421 now reads them too, through
422 :func:`~oct.core.waivers.inline_disabled_rules`: the linter's set,
423 written once and shared with the gate's format pass. Before, the
424 panel offered a ``dbg_import`` fix on every one of the nineteen
425 modules the operator had ruled exempt - a fix the gate would have
426 refused, and which on ``oct/core/diagnostics.py`` writes the
427 module-level self-import that file's own waiver reason says raises
430 When *verbose* is True the result includes a ``"files"`` key mapping
431 to a list of ``{"path": <rel_path>, "fixes": [<fix_name>, ...]}``
432 entries so the caller can show which files have fixable violations.
434 func =
"check_fixable_health"
444 if active_rules
is None:
445 active_rules = dict(_DEFAULT_RULES)
447 fmt_ctx = FormatterContext(
448 project_root=project_root,
449 project_name=project_root.name,
450 diagnostics_dir=ctx.diagnostics_dir,
451 tests_dir=ctx.tests_dir,
452 docs_dir=ctx.docs_dir,
459 if analysis_cache
is None:
460 if exclude_dirs
is None:
461 exclude_dirs = set(LINTER_EXCLUDE_DIRS)
462 if wildcard_exclude
is None:
463 wildcard_exclude = list(WILDCARD_EXCLUDE_DIRS)
465 find_python_files(project_root, exclude_dirs, wildcard_exclude),
468 python_files = list(analysis_cache)
473 "docstring":
"docstring",
474 "dbg_import":
"dbg_import",
475 "func_pattern":
"func_pattern",
481 path_rules = list(getattr(ctx,
"path_rules",
None)
or ())
484 for _prefix, rules
in path_rules
485 for rule, value
in rules.items()
489 k: 0
for k
in fix_rule_map
490 if active_rules.get(fix_rule_map[k],
True)
491 or fix_rule_map[k]
in path_enabled
495 "header": fix_header_block,
496 "docstring": fix_docstring,
497 "dbg_import": fix_dbg_import,
498 "func_pattern": fix_func_pattern,
501 file_details: list[dict] = []
503 for py_file
in python_files:
504 lint_checks: Optional[Dict[str, Any]] =
None
505 if analysis_cache
is not None:
506 analysis = analysis_cache[py_file]
507 text = analysis[
"text"]
508 lint_checks = analysis[
"checks"]
511 text = py_file.read_text(encoding=
"utf-8")
515 rel_path = to_project_relative(py_file, project_root)
520 file_rules, _changed = effective_rules(
521 active_rules, rel_path, path_rules,
524 file_rules = active_rules
533 waived_rules = inline_disabled_rules(text)
535 file_fixes: list[str] = []
536 for fix_name
in fixable:
537 rule_name = fix_rule_map[fix_name]
538 if not file_rules.get(rule_name,
True):
540 if rule_name
in waived_rules:
542 if lint_checks
is not None and lint_checks.get(rule_name)
is True:
544 if fix_name ==
"header" and lint_checks
is not None:
545 changed = lint_checks.get(rule_name)
is False
547 changed, _, _ = fix_funcs[fix_name](py_file, text, fmt_ctx)
549 fixable[fix_name] += 1
550 file_fixes.append(fix_name)
552 if verbose
and file_fixes:
553 file_details.append({
"path": rel_path,
"fixes": file_fixes})
555 fixable[
"total"] = sum(fixable.values())
557 fixable[
"files"] = file_details
1386 json_mode: bool =
False,
1387 verbose: bool =
False,
1388 test_timeout: int |
None =
None,
1389 run_tests: bool |
None =
None,
1390 log_output: bool =
False,
1391 timing: bool =
False,
1392 timing_last: int = 20,
1393 write_baseline: bool =
False,
1394 force_host: bool =
False,
1395 session: Path |
None =
None,
1398 Run the project health dashboard.
1403 The detected Option C project root directory.
1405 Output machine-readable JSON instead of terminal report.
1407 Show per-file details in addition to summary.
1408 test_timeout : int | None
1409 Maximum seconds to wait for pytest. Overrides ``health.test_timeout``
1410 in ``.octrc.json``. Defaults to 300 when neither is set (OI-413).
1411 run_tests : bool | None
1412 Run the project test suite. The CLI always passes a boolean and skips
1413 tests by default; ``None`` preserves legacy direct-call behaviour.
1416 _dbg(
"OCT.HEALTH", func, f
"health run starting at {project_root}", 3)
1417 if timing
or write_baseline
or session
is not None or force_host:
1420 timing_report, write_baseline
as save_baseline, session_report, render_report,
1423 data = timing_report(project_root, timing_last)
1425 save_baseline(project_root, data, force_host=force_host)
1426 data = timing_report(project_root, timing_last)
1427 if session
is not None:
1428 data[
"session"] = session_report(session)
1429 except (OSError, ValueError)
as exc:
1430 raise click.ClickException(str(exc))
from exc
1434 print(render_report(data))
1441 record_check_timings = any(project_root.iterdir())
1443 def check_timing(stage: str) -> Any:
1445 timed_stage(project_root, stage)
1446 if record_check_timings
1447 else nullcontext(
None)
1451 diagnostics_dir = project_root /
"oc_diagnostics"
1452 if not diagnostics_dir.is_dir():
1453 diagnostics_dir = project_root /
"diagnostics"
1454 if diagnostics_dir.is_dir():
1456 "Warning: 'diagnostics/' is deprecated and will be removed in v1.0.0. "
1457 "Rename to 'oc_diagnostics/'.",
1461 ctx = LinterContext(
1462 project_root=project_root,
1463 project_name=project_root.name,
1464 diagnostics_dir=diagnostics_dir,
1465 tests_dir=project_root /
"tests",
1466 docs_dir=project_root /
"docs",
1470 exclude_dirs = set(LINTER_EXCLUDE_DIRS)
1471 wildcard_exclude = list(WILDCARD_EXCLUDE_DIRS)
1472 excluded_by_config: List[str] = []
1473 active_rules = dict(_DEFAULT_RULES)
1474 disabled_rules: List[str] = []
1479 effective_test_timeout: int = 300
1480 from oct.core.octrc
import load_octrc_safe
1481 octrc = load_octrc_safe(project_root)
1482 linter_cfg = octrc.get(
"linter", {})
1483 if isinstance(linter_cfg, dict):
1484 for d
in linter_cfg.get(
"exclude_dirs_add", []):
1486 excluded_by_config.append(d)
1487 for d
in linter_cfg.get(
"exclude_dirs_remove", []):
1488 exclude_dirs.discard(d)
1489 for d
in linter_cfg.get(
"wildcard_exclude_add", []):
1490 if d
not in wildcard_exclude:
1491 wildcard_exclude.append(d)
1493 profile_name = linter_cfg.get(
"profile",
"default")
1494 active_rules = dict(_PROFILES.get(profile_name, _DEFAULT_RULES))
1495 rule_overrides = linter_cfg.get(
"rules", {})
1496 if isinstance(rule_overrides, dict):
1497 active_rules.update(rule_overrides)
1498 disabled_rules = [r
for r, v
in active_rules.items()
if not v]
1502 ctx.path_rules = parse_path_rules(linter_cfg)
1503 health_cfg = octrc.get(
"health", {})
1504 if isinstance(health_cfg, dict):
1505 cfg_timeout = health_cfg.get(
"test_timeout")
1506 if isinstance(cfg_timeout, int)
and cfg_timeout > 0:
1507 effective_test_timeout = cfg_timeout
1510 if test_timeout
is not None:
1511 effective_test_timeout = test_timeout
1514 active_checks = [c
for c
in _HEALTH_CHECKS
if active_rules.get(c,
True)]
1516 "header":
"header",
"docstring":
"docstring",
1517 "dbg_import":
"dbg_import",
"func_pattern":
"func_pattern",
1519 fixable_names = [k
for k
in _fix_rule_map
if active_rules.get(_fix_rule_map[k],
True)]
1521 from oct.core.terminal
import supports_ansi_on
1522 use_color = supports_ansi_on(sys.stdout)
and not json_mode
1523 G =
"\033[92m" if use_color
else ""
1524 R =
"\033[91m" if use_color
else ""
1525 Y =
"\033[93m" if use_color
else ""
1526 X =
"\033[0m" if use_color
else ""
1529 project_name=project_root.name,
1530 excluded_dirs=excluded_by_config,
1531 disabled_rules=disabled_rules,
1532 active_checks=active_checks,
1533 fixable_names=fixable_names,
1534 use_color=use_color,
1537 dashboard = Dashboard(quiet=json_mode)
1538 dashboard.render(template_lines, footer_line=lm.footer, tasks_total=7)
1539 lint_analysis: Dict[Path, Dict[str, Any]] = {}
1542 dashboard.set_task(
"Lint compliance")
1543 dashboard.update_line(lm.lint, f
"{Y}[>]{X} Lint Compliance: running...")
1544 with check_timing(
"health.lint")
as timing_sample:
1546 project_root, ctx, verbose=verbose,
1547 exclude_dirs=exclude_dirs, wildcard_exclude=wildcard_exclude,
1548 active_rules=active_rules,
1549 analysis_cache=lint_analysis,
1551 if timing_sample
is not None:
1552 timing_sample.files = lint_result[
"total_files"]
1553 pct = lint_result[
"compliance_pct"]
1554 pc = G
if pct == 100.0
else R
1555 dashboard.update_line(
1557 f
"{G}[x]{X} Lint Compliance: "
1558 f
"{pc}{pct}%{X} ({lint_result['fully_compliant']}/{lint_result['total_files']}"
1559 f
" files fully compliant; {lint_result.get('cache_hits', 0)} cache hits)",
1561 for ck, idx
in lm.lint_checks.items():
1562 cd = lint_result[
"checks"].get(ck)
1563 label = ck.replace(
"_",
" ").title()
1565 c = G
if cd[
"pass"] == cd[
"total"]
else R
1566 dashboard.update_line(
1567 idx, f
" {G}[x]{X} {label + ':':<20}{c}{_pct_str(cd['pass'], cd['total'])}{X}",
1570 dashboard.update_line(idx, f
" {Y}[-]{X} {label + ':':<20}disabled")
1571 dashboard.complete_task()
1574 dashboard.set_task(
"Auto-fixable checks")
1575 dashboard.update_line(lm.fixable, f
"{Y}[>]{X} Fixable Violations: running...")
1576 with check_timing(
"health.fixable")
as timing_sample:
1579 exclude_dirs=exclude_dirs, wildcard_exclude=wildcard_exclude,
1580 active_rules=active_rules,
1582 analysis_cache=lint_analysis,
1584 if timing_sample
is not None:
1585 timing_sample.files = lint_result[
"total_files"]
1586 fix_total = fixable_result.get(
"total", 0)
1587 fc = G
if fix_total == 0
else Y
1588 dashboard.update_line(lm.fixable, f
"{G}[x]{X} Fixable Violations: {fc}{fix_total} total{X}")
1589 for fn, idx
in lm.fixable_items.items():
1590 val = fixable_result.get(fn, 0)
1591 vc = G
if val == 0
else Y
1592 dashboard.update_line(
1593 idx, f
" {G}[x]{X} {_FIX_LABELS.get(fn, fn) + ':':<20}{vc}{val}{X}",
1595 dashboard.complete_task()
1598 dashboard.set_task(
"Documentation")
1599 dashboard.update_line(lm.docs, f
"{Y}[>]{X} Documentation: running...")
1600 _octrc_cfg = load_octrc_safe(project_root)
1601 _production_mode = bool(
1602 (_octrc_cfg.get(
"health")
or {}).get(
"production_mode",
False)
1604 with check_timing(
"health.docs")
as timing_sample:
1606 if timing_sample
is not None:
1607 timing_sample.files = len(docs_result[
"files"])
1608 dc = G
if docs_result[
"total_present"] == docs_result[
"total_required"]
else R
1609 dashboard.update_line(
1611 f
"{G}[x]{X} Documentation: "
1612 f
"{dc}{docs_result['total_present']}/{docs_result['total_required']} present{X}",
1614 for fi
in docs_result[
"files"]:
1615 idx = lm.docs_files.get(fi[
"name"])
1619 dashboard.update_line(idx, f
" {G}[x]{X} {fi['name']:<20}{G}[OK]{X}")
1620 elif fi.get(
"required",
True):
1621 dashboard.update_line(idx, f
" {R}[!]{X} {fi['name']:<20}{R}[MISSING]{X}")
1623 dashboard.update_line(idx, f
" {Y}[-]{X} {fi['name']:<20}{Y}[OPTIONAL]{X}")
1624 dashboard.complete_task()
1627 dashboard.set_task(
"Test suite")
1628 dashboard.update_line(lm.tests, f
"{Y}[>]{X} Tests: running...")
1630 _health_cfg = _octrc_cfg.get(
"health")
or {}
1631 _coverage_threshold_raw = _health_cfg.get(
"coverage_threshold")
1632 _coverage_threshold: Optional[float] =
None
1633 if isinstance(_coverage_threshold_raw, (int, float)):
1634 _coverage_threshold = float(_coverage_threshold_raw)
1635 with check_timing(
"health.tests"):
1636 if run_tests
is not False:
1639 timeout=effective_test_timeout,
1640 coverage_threshold=_coverage_threshold,
1641 production_mode=_production_mode,
1645 "status":
"skipped",
1646 "reason":
"run with --tests",
1648 ts = test_result[
"status"]
1649 cov_pct = test_result.get(
"coverage_pct")
1650 cov_thr = test_result.get(
"coverage_threshold")
1651 cov_msg = test_result.get(
"coverage_message")
1653 if cov_pct
is not None and cov_thr
is not None:
1654 cov_state =
"OK" if cov_pct >= cov_thr
else "below"
1655 cov_suffix = f
" | cov {cov_pct:.1f}%/{cov_thr:.0f}% [{cov_state}]"
1657 cov_suffix = f
" | {cov_msg}"
1658 test_summary = test_result.get(
"summary")
or test_result.get(
"reason",
"")
1659 tsm = f
" ({test_summary}{cov_suffix})" if test_summary
else cov_suffix
1661 dashboard.update_line(lm.tests, f
"{G}[x]{X} Tests: {G}PASS{X}{tsm}")
1662 elif ts ==
"skipped":
1663 dashboard.update_line(lm.tests, f
"{Y}[-]{X} Tests: {Y}SKIPPED{X}{tsm}")
1664 elif ts ==
"not_run":
1665 dashboard.update_line(lm.tests, f
"{Y}[-]{X} Tests: {Y}NOT RUN{X}{tsm}")
1667 dashboard.update_line(lm.tests, f
"{R}[!]{X} Tests: {R}{ts.upper()}{X}{tsm}")
1668 dashboard.complete_task()
1671 dashboard.set_task(
"Compatibility check")
1672 dashboard.update_line(lm.compat, f
"{Y}[>]{X} oc_diagnostics: checking...")
1673 with check_timing(
"health.compat"):
1675 if compat_result[
"installed"]:
1676 if compat_result[
"compatible"]:
1677 dashboard.update_line(
1679 f
"{G}[x]{X} oc_diagnostics: v{compat_result['version']} -- {G}compatible{X}",
1682 dashboard.update_line(
1684 f
"{R}[!]{X} oc_diagnostics: v{compat_result['version']} -- {R}INCOMPATIBLE{X}",
1687 dashboard.update_line(lm.compat, f
"{R}[!]{X} oc_diagnostics: {R}NOT INSTALLED{X}")
1688 dashboard.complete_task()
1691 dashboard.set_task(
"Virtual environment")
1692 dashboard.update_line(lm.venv, f
"{Y}[>]{X} Virtual Env: checking...")
1693 with check_timing(
"health.venv"):
1695 if venv_result[
"status"] ==
"OK":
1696 dashboard.update_line(
1697 lm.venv, f
"{G}[x]{X} Virtual Env: {G}[OK]{X} {venv_result['message']}",
1700 dashboard.update_line(
1701 lm.venv, f
"{R}[!]{X} Virtual Env: {Y}[WARN]{X} {venv_result['message']}",
1703 dashboard.complete_task()
1706 dashboard.set_task(
"Git integration")
1707 dashboard.update_line(lm.git, f
"{Y}[>]{X} Git Integration: checking...")
1708 with check_timing(
"health.git"):
1710 _GC = {
"OK": G,
"WARN": Y,
"FAIL": R,
"SKIP": Y}
1713 for gk, gl
in _GIT_LABELS.items():
1714 idx = lm.git_checks.get(gk)
1715 check = git_result.get(gk)
1716 if idx
is None or check
is None:
1719 status = check[
"status"]
1720 color = _GC.get(status, X)
1724 elif status
in (
"WARN",
"SKIP"):
1728 dashboard.update_line(idx, f
" {mk} {gl + ':':<20}{color}[{status}]{X} {check['detail']}")
1729 gc = G
if git_ok == git_total
else Y
1730 dashboard.update_line(lm.git, f
"{G}[x]{X} Git Integration: {gc}{git_ok}/{git_total} checks OK{X}")
1731 dashboard.complete_task()
1737 "project": project_root.name,
1738 "oct_version": __version__,
1739 "timestamp": datetime.now().isoformat(),
1740 "excluded_dirs": excluded_by_config,
1741 "disabled_rules": disabled_rules,
1742 "lint": lint_result,
1743 "fixable": fixable_result,
1744 "docs": docs_result,
1745 "tests": test_result,
1746 "compat": compat_result,
1747 "venv": venv_result,
1755 from oct.core.log_writer
import write_command_log
1756 log_path = write_command_log(project_root,
"health", report)
1757 print(f
"Log written to: {log_path}", file=sys.stderr)
1759 if verbose
and not json_mode
and "files" in lint_result:
1760 print(f
"\n{Y}{'-' * 70}{X}")
1761 print(f
"{Y}Per-file breakdown:{X}")
1762 print(f
"{Y}{'-' * 70}{X}\n")
1763 for file_info
in lint_result[
"files"]:
1764 if not file_info[
"compliant"]:
1765 failed = [k
for k, v
in file_info[
"checks"].items()
if not v]
1766 print(f
" {file_info['path']}")
1767 print(f
" Failed: {R}{', '.join(failed)}{X}")
1769 if verbose
and not json_mode
and fixable_result.get(
"files"):
1770 print(f
"\n{Y}{'-' * 70}{X}")
1771 print(f
"{Y}Fixable violations per file:{X}")
1772 print(f
"{Y}{'-' * 70}{X}\n")
1773 for file_info
in fixable_result[
"files"]:
1774 print(f
" {file_info['path']}")
1775 print(f
" Fixable: {Y}{', '.join(file_info['fixes'])}{X}")