From 5e69f771860de879c71c2c50fec92d29febf63cc Mon Sep 17 00:00:00 2001 From: Jimisola Laursen Date: Sun, 22 Mar 2026 19:52:22 +0100 Subject: [PATCH 1/6] refactor(status): replace colorama/tabulate with Rich in status.py and semantic_validator.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace manual ANSI escape codes and tabulate table rendering with Rich library (Panel, Table, Text, Console) in status.py (~323 → 255 lines) - Replace colorama/tabulate in semantic_validator._log_all_errors() with Rich - Remove colorama and tabulate from project dependencies; add rich>=13.0 - Update test_status_presentation.py: remove colorama dependency, add _render() helper, update assertions to check Text.plain/.style and rendered ANSI codes - Keep _ORANGE and _DIM as named constants (now Rich color name strings) Signed-off-by: Jimisola Laursen --- pyproject.toml | 3 +- src/reqstool/commands/status/status.py | 313 ++++++++---------- .../common/validators/semantic_validator.py | 29 +- .../status/test_status_presentation.py | 93 ++---- 4 files changed, 190 insertions(+), 248 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 230614de..bc1f5a41 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,6 @@ classifiers = [ ] requires-python = ">=3.13" dependencies = [ - "colorama==0.4.6", "Jinja2==3.1.6", "jsonschema[format-nongpl]==4.26.0", "lark==1.3.1", @@ -45,8 +44,8 @@ dependencies = [ "pygit2==1.19.1", "referencing==0.37.0", "requests-file==3.0.1", + "rich>=13.0", "ruamel.yaml==0.19.1", - "tabulate==0.10.0", "reqstool-python-decorators==0.1.0", "packaging==26.0", "requests==2.32.5", diff --git a/src/reqstool/commands/status/status.py b/src/reqstool/commands/status/status.py index ac220f53..b4659805 100644 --- a/src/reqstool/commands/status/status.py +++ b/src/reqstool/commands/status/status.py @@ -4,9 +4,11 @@ import json -from colorama import Fore, Style +from rich.console import Console +from rich.panel import Panel +from rich.table import Table, box +from rich.text import Text from reqstool_python_decorators.decorators.decorators import Requirements -from tabulate import tabulate from reqstool.common.validator_error_holder import ValidationErrorHolder from reqstool.common.validators.semantic_validator import SemanticValidator @@ -17,6 +19,10 @@ from reqstool.storage.requirements_repository import RequirementsRepository +_ORANGE = "dark_orange" +_DIM = "dim" + + @Requirements("REQ_027") class StatusCommand: def __init__(self, location: LocationInterface, format: str = "console"): @@ -44,6 +50,32 @@ def __status_result(self) -> tuple[str, int]: ) +def _format_test_cell(stats: TestStats) -> Text: + """Format a TestStats into a Rich Text object with colored counts.""" + if stats.not_applicable: + return Text() + + def _slot(value: int, style: str) -> Text: + t = Text() + if value == 0: + t.append(" -", style=_DIM) + else: + t.append(f"{value:>2}", style=style) + return t + + cell = Text() + cell.append_text(_slot(stats.total, "default")) + cell.append(" ") + cell.append_text(_slot(stats.passed, "green")) + cell.append(" ") + cell.append_text(_slot(stats.failed, "red")) + cell.append(" ") + cell.append_text(_slot(stats.skipped, "yellow")) + cell.append(" ") + cell.append_text(_slot(stats.missing, _ORANGE)) + return cell + + def _build_table( req_id: str, urn: str, @@ -52,24 +84,23 @@ def _build_table( mvrs: TestStats, completed: bool, implementation: IMPLEMENTATION, -) -> list[str]: - row = [urn] - # add color to requirement if it's completed or not - req_id_color = f"{Fore.GREEN}" if completed else f"{Fore.RED}" - row.append(f"{req_id_color}{req_id}{Style.RESET_ALL}") - - # Perform check for implementations +) -> list: + id_style = "green" if completed else "red" + row = [ + Text(urn), + Text(req_id, style=id_style), + ] if implementation == IMPLEMENTATION.NOT_APPLICABLE: - row.extend(["N/A"]) + row.append(Text("N/A", style="dim")) else: - color = Fore.GREEN if impls > 0 else Fore.RED - row.extend([f"{color}{impls}{Style.RESET_ALL}"]) - _extend_row(tests, row, kind="automated") - _extend_row(mvrs, row, kind="manual") + impl_style = "green" if impls > 0 else "red" + row.append(Text(str(impls), style=impl_style)) + row.append(_format_test_cell(tests)) + row.append(_format_test_cell(mvrs)) return row -def _get_row_with_totals(stats_service: StatisticsService) -> list[str]: +def _get_row_with_totals(stats_service: StatisticsService) -> list: ts = stats_service.total_statistics total_automatic = ts.passed_automatic_tests + ts.failed_automatic_tests total_manual = ts.passed_manual_tests + ts.failed_manual_tests @@ -93,19 +124,16 @@ def _get_row_with_totals(stats_service: StatisticsService) -> list[str]: missing=ts.missing_manual_tests, ) return [ - "Total", - "", - str(total_implementations), + Text("Total"), + Text(""), + Text(str(total_implementations)), _format_test_cell(auto_stats), _format_test_cell(manual_stats), ] -# builds the status table def _status_table(stats_service: StatisticsService) -> str: table_data = [] - headers = ["URN", "ID", "Implementation", "Automated Tests", "Manual Tests"] - for req, stats in stats_service.requirement_statistics.items(): table_data.append( _build_table( @@ -118,42 +146,34 @@ def _status_table(stats_service: StatisticsService) -> str: implementation=stats.implementation_type, ) ) - table_data.append(_get_row_with_totals(stats_service)) - col_align = ["center"] * len(headers) if table_data else [] - table = tabulate(tablefmt="fancy_grid", tabular_data=table_data, headers=headers, colalign=col_align) - - lines = table.split("\n") - - # Find a line without ANSI codes to measure visible width - visible_table_width = 75 - for line in lines: - if "╞" in line or "╘" in line: - visible_table_width = len(line) - break + table = Table(box=box.HEAVY_HEAD, show_header=True, header_style="bold") + for col in ["URN", "ID", "Implementation", "Automated Tests", "Manual Tests"]: + table.add_column(col, justify="center") + for row in table_data: + table.add_row(*row) ts = stats_service.total_statistics - header_req_data = ("\b" * len(str(ts.total_requirements))) + f"REQUIREMENTS: {str(ts.total_requirements)}" - inner_width = visible_table_width - 2 # subtract ╒ and ╕ - title = ( - "╒" + "═" * inner_width + "╕" + f"\n│{header_req_data.center(inner_width)}│" + "\n╘" + "═" * inner_width + "╛" - ) + panel = Panel(f"REQUIREMENTS: {ts.total_requirements}", expand=True) - table_with_title = f"{title}\n{table}\n" - - legend_line = ( - f"T = Total, {Fore.GREEN}P = Passed{Style.RESET_ALL}, " - f"{Fore.RED}F = Failed{Style.RESET_ALL}, " - f"{Fore.YELLOW}S = Skipped{Style.RESET_ALL}, " - f"{_ORANGE}M = Missing{Style.RESET_ALL}" - ) + legend = Text("T = Total, ") + legend.append("P = Passed", style="green") + legend.append(", ") + legend.append("F = Failed", style="red") + legend.append(", ") + legend.append("S = Skipped", style="yellow") + legend.append(", ") + legend.append("M = Missing", style=_ORANGE) statistics = _summarize_statistics(ts) - status = table_with_title + legend_line + statistics - - return status + console = Console(highlight=False, force_terminal=True, color_system="standard") + with console.capture() as cap: + console.print(panel) + console.print(table) + console.print(legend) + return cap.get() + statistics def _summarize_statistics(ts: TotalStats) -> str: @@ -162,8 +182,6 @@ def _summarize_statistics(ts: TotalStats) -> str: code_reqs = ts.total_requirements - nr_of_reqs_without_implementation code_completed = ts.completed_requirements - nr_of_completed_reqs_without_implementation - header_test_data = ("\b" * len(str(ts.total_tests))) + f"Total Tests: {str(ts.total_tests)}" - header_svcs_data = ("\b" * len(str(ts.total_svcs))) + f"Total SVCs: {str(ts.total_svcs)}" CODE, NA, IMPLEMENTATIONS = __colorize_headers( total=ts.total_requirements, total_completed=ts.completed_requirements, @@ -172,94 +190,75 @@ def _summarize_statistics(ts: TotalStats) -> str: ) implementation_data = [ - [ - str(code_reqs) + __numbers_as_percentage(numerator=code_reqs, denominator=code_reqs), - str(ts.with_implementation) - + __numbers_as_percentage(numerator=ts.with_implementation, denominator=code_reqs), - str(code_completed) + __numbers_as_percentage(numerator=code_completed, denominator=code_reqs), - str(ts.total_requirements - (nr_of_reqs_without_implementation + code_completed)) - + __numbers_as_percentage( - numerator=(ts.total_requirements - (nr_of_reqs_without_implementation + code_completed)), - denominator=code_reqs, - ), - str(nr_of_reqs_without_implementation) - + __numbers_as_percentage( - numerator=nr_of_reqs_without_implementation, - denominator=nr_of_reqs_without_implementation, - ), - str(nr_of_completed_reqs_without_implementation) - + __numbers_as_percentage( - numerator=nr_of_completed_reqs_without_implementation, - denominator=nr_of_reqs_without_implementation, - ), - str(nr_of_reqs_without_implementation - nr_of_completed_reqs_without_implementation) - + __numbers_as_percentage( - numerator=(nr_of_reqs_without_implementation - nr_of_completed_reqs_without_implementation), - denominator=nr_of_reqs_without_implementation, - ), - ] + str(code_reqs) + __numbers_as_percentage(numerator=code_reqs, denominator=code_reqs), + str(ts.with_implementation) + __numbers_as_percentage(numerator=ts.with_implementation, denominator=code_reqs), + str(code_completed) + __numbers_as_percentage(numerator=code_completed, denominator=code_reqs), + str(ts.total_requirements - (nr_of_reqs_without_implementation + code_completed)) + + __numbers_as_percentage( + numerator=(ts.total_requirements - (nr_of_reqs_without_implementation + code_completed)), + denominator=code_reqs, + ), + str(nr_of_reqs_without_implementation) + + __numbers_as_percentage( + numerator=nr_of_reqs_without_implementation, + denominator=nr_of_reqs_without_implementation, + ), + str(nr_of_completed_reqs_without_implementation) + + __numbers_as_percentage( + numerator=nr_of_completed_reqs_without_implementation, + denominator=nr_of_reqs_without_implementation, + ), + str(nr_of_reqs_without_implementation - nr_of_completed_reqs_without_implementation) + + __numbers_as_percentage( + numerator=(nr_of_reqs_without_implementation - nr_of_completed_reqs_without_implementation), + denominator=nr_of_reqs_without_implementation, + ), ] - table_svc_data = [ - [ - str(ts.passed_tests) + __numbers_as_percentage(numerator=ts.passed_tests, denominator=ts.total_tests), - str(ts.failed_tests) + __numbers_as_percentage(numerator=ts.failed_tests, denominator=ts.total_tests), - str(ts.skipped_tests) + __numbers_as_percentage(numerator=ts.skipped_tests, denominator=ts.total_tests), - str(ts.missing_automated_tests) - + __numbers_as_percentage(numerator=ts.missing_automated_tests, denominator=ts.total_svcs), - str(ts.missing_manual_tests) - + __numbers_as_percentage(numerator=ts.missing_manual_tests, denominator=ts.total_svcs), - ] + svc_data = [ + str(ts.passed_tests) + __numbers_as_percentage(numerator=ts.passed_tests, denominator=ts.total_tests), + str(ts.failed_tests) + __numbers_as_percentage(numerator=ts.failed_tests, denominator=ts.total_tests), + str(ts.skipped_tests) + __numbers_as_percentage(numerator=ts.skipped_tests, denominator=ts.total_tests), + str(ts.missing_automated_tests) + + __numbers_as_percentage(numerator=ts.missing_automated_tests, denominator=ts.total_svcs), + str(ts.missing_manual_tests) + + __numbers_as_percentage(numerator=ts.missing_manual_tests, denominator=ts.total_svcs), ] - implementation_headers = ["Total", "Implemented", "Verified", "Not Verified", "Total", "Verified", "Not Verified"] - - svc_headers = [ - "Passed tests", - "Failed tests", - "Skipped tests", - "SVCs missing tests", - "SVCs missing MVRs", - ] - - svc_table = tabulate( - tablefmt="fancy_grid", - tabular_data=table_svc_data, - headers=svc_headers, - colalign=["center"] * len(table_svc_data[0]), + code_na_header = Text() + code_na_header.append_text(CODE) + code_na_header.append(" | ") + code_na_header.append_text(NA) + + impl_table = Table(box=box.HEAVY_HEAD, show_header=True) + impl_table.add_column("Total", justify="center") + impl_table.add_column("Implemented", justify="center") + impl_table.add_column("Verified", justify="center") + impl_table.add_column("Not Verified", justify="center") + impl_table.add_column("Total", justify="center") + impl_table.add_column("Verified", justify="center") + impl_table.add_column("Not Verified", justify="center") + impl_table.add_row(*implementation_data) + + svc_table = Table( + box=box.HEAVY_HEAD, + show_header=True, + title=f"Total Tests: {ts.total_tests} | Total SVCs: {ts.total_svcs}", ) - - implementation_table = tabulate( - tablefmt="fancy_grid", - tabular_data=implementation_data, - headers=implementation_headers, - colalign=["center"] * len(implementation_data[0]), - ) - - total_tests_svcs_header = ( - "╒═══════════════════════════════════════════════════╤════════════════════════════════════════════╕" - f"\n│ {header_test_data} │" - f" {header_svcs_data} │" - "\n╘═══════════════════════════════════════════════════╧════════════════════════════════════════════╛" - ) - - test_header = ( - "╒═══════════════════════════════════════════════════════════╤═══════════════════════════════════════════╕" - f"\n| {CODE} │ {NA} │" - "\n╘═══════════════════════════════════════════════════════════╧═══════════════════════════════════════════╛" - ) - - impl_header = ( - "╒═══════════════════════════════════════════════════════════════════════════════════════════════════════╕" - f"\n| {IMPLEMENTATIONS} │" - "\n╘═══════════════════════════════════════════════════════════════════════════════════════════════════════╛" - ) - - table_with_title = ( - f"\n{impl_header}\n{test_header}\n" f"{implementation_table}\n{total_tests_svcs_header}\n{svc_table}" - ) - - return table_with_title + svc_table.add_column("Passed tests", justify="center") + svc_table.add_column("Failed tests", justify="center") + svc_table.add_column("Skipped tests", justify="center") + svc_table.add_column("SVCs missing tests", justify="center") + svc_table.add_column("SVCs missing MVRs", justify="center") + svc_table.add_row(*svc_data) + + console = Console(highlight=False, force_terminal=True, color_system="standard") + with console.capture() as cap: + console.print(Panel(IMPLEMENTATIONS, expand=True)) + console.print(code_na_header) + console.print(impl_table) + console.print(svc_table) + return cap.get() def __numbers_as_percentage(numerator: int, denominator: int) -> str: @@ -272,51 +271,13 @@ def __numbers_as_percentage(numerator: int, denominator: int) -> str: def __colorize_headers( total: int, total_completed: int, total_reqs_no_impl: int, completed_reqs_no_impl: int -) -> tuple[str, str, str]: +) -> tuple[Text, Text, Text]: total_code = total - total_reqs_no_impl total_code_completed = total_code == (total_completed - completed_reqs_no_impl) total_no_impl_completed = total_reqs_no_impl - completed_reqs_no_impl == 0 - CODE = f"{Fore.GREEN}{'Code'}{Style.RESET_ALL}" if total_code_completed else f"{Fore.RED}{'Code'}{Style.RESET_ALL}" - NA = f"{Fore.GREEN}{'N/A'}{Style.RESET_ALL}" if total_no_impl_completed else f"{Fore.RED}{'N/A'}{Style.RESET_ALL}" - IMPLEMENTATIONS = ( - f"{Fore.GREEN}{'IMPLEMENTATIONS'}{Style.RESET_ALL}" - if total == total_completed - else f"{Fore.RED}{'IMPLEMENTATIONS'}{Style.RESET_ALL}" - ) + CODE = Text("Code", style="green" if total_code_completed else "red") + NA = Text("N/A", style="green" if total_no_impl_completed else "red") + IMPLEMENTATIONS = Text("IMPLEMENTATIONS", style="green" if total == total_completed else "red") return CODE, NA, IMPLEMENTATIONS - - -_ORANGE = "\033[38;5;208m" -_DIM = Style.DIM - - -def _format_test_cell(stats: TestStats) -> str: - """Format a TestStats into a single fixed-width string with colored counts.""" - if stats.not_applicable: - return "" - - slots = [ - (stats.total, ""), - (stats.passed, Fore.GREEN), - (stats.failed, Fore.RED), - (stats.skipped, Fore.YELLOW), - (stats.missing, _ORANGE), - ] - - parts = [] - for value, color in slots: - if value == 0: - parts.append(f"{_DIM} -{Style.RESET_ALL}") - else: - text = f"{value:>2}" - if color: - text = f"{color}{text}{Style.RESET_ALL}" - parts.append(text) - - return " ".join(parts) - - -def _extend_row(result: TestStats, row: list[str], kind: str) -> None: - row.append(_format_test_cell(result)) diff --git a/src/reqstool/common/validators/semantic_validator.py b/src/reqstool/common/validators/semantic_validator.py index 54eb63f3..ef2df6e3 100644 --- a/src/reqstool/common/validators/semantic_validator.py +++ b/src/reqstool/common/validators/semantic_validator.py @@ -4,9 +4,10 @@ import re from typing import List -from colorama import Fore, Style +from rich.console import Console +from rich.table import Table, box +from rich.text import Text from reqstool_python_decorators.decorators.decorators import Requirements -from tabulate import tabulate from reqstool.common.models.urn_id import UrnId from reqstool.common.utils import Utils @@ -55,21 +56,27 @@ def validate_post_parsing(self, combined_raw_dataset: CombinedRawDataset) -> Lis def _log_all_errors(self): errors = self._validation_error_holder.get_errors() - validation_result = "" - table_data = [] if len(errors) > 0: - validation_result = f"{Fore.RED}FAIL{Style.RESET_ALL}" + result_text = Text("FAIL", style="red") + error_table = Table(box=box.HEAVY_HEAD, show_header=False) + error_table.add_column("Message") for error in errors: - table_data.append([re.sub(r"\s+", " ", error.msg.strip("\n"))]) + error_table.add_row(re.sub(r"\s+", " ", error.msg.strip("\n"))) else: - validation_result = f"{Fore.GREEN}PASS{Style.RESET_ALL}" + result_text = Text("PASS", style="green") + error_table = None - title = f"\n\nVALIDATION: {validation_result}" - table = tabulate(tablefmt="fancy_grid", tabular_data=table_data) - table_with_title = f"{title}\n{table}\n" + title = Text("\n\nVALIDATION: ") + title.append_text(result_text) - logging.info(table_with_title) + console = Console(highlight=False, force_terminal=True, color_system="standard") + with console.capture() as cap: + console.print(title) + if error_table is not None: + console.print(error_table) + + logging.info(cap.get()) @Requirements("REQ_022") def _validate_no_duplicate_requirement_ids(self, data: RequirementData) -> bool: diff --git a/tests/unit/reqstool/commands/status/test_status_presentation.py b/tests/unit/reqstool/commands/status/test_status_presentation.py index 2ee3444f..29a2cbc6 100644 --- a/tests/unit/reqstool/commands/status/test_status_presentation.py +++ b/tests/unit/reqstool/commands/status/test_status_presentation.py @@ -1,33 +1,43 @@ # Copyright © LFV -from colorama import Fore, Style +from rich.console import Console +from rich.text import Text -from reqstool.commands.status.status import _build_table, _extend_row, _format_test_cell, _ORANGE, _summarize_statistics +from reqstool.commands.status.status import _build_table, _format_test_cell, _ORANGE, _summarize_statistics from reqstool.models.requirements import IMPLEMENTATION from reqstool.services.statistics_service import TestStats, TotalStats +def _render(renderable) -> str: + """Render a Rich renderable to a string with ANSI codes.""" + console = Console(highlight=False, force_terminal=True, color_system="standard") + with console.capture() as cap: + console.print(renderable, end="") + return cap.get() + + # --------------------------------------------------------------------------- # _format_test_cell # --------------------------------------------------------------------------- def test_format_test_cell_not_applicable(): - assert _format_test_cell(TestStats(not_applicable=True)) == "" + result = _format_test_cell(TestStats(not_applicable=True)) + assert result.plain == "" def test_format_test_cell_all_zeros(): result = _format_test_cell(TestStats(total=0, passed=0, failed=0, skipped=0, missing=0)) # All slots are dim dashes - assert Style.DIM in result - plain = result.replace(Style.DIM, "").replace(Style.RESET_ALL, "").replace(" ", "").replace("-", "") + rendered = _render(result) + assert "\033[2m" in rendered # dim ANSI code + plain = result.plain.replace(" ", "").replace("-", "") assert plain == "" def test_format_test_cell_mixed_values(): result = _format_test_cell(TestStats(total=3, passed=2, failed=1, skipped=0, missing=0)) - # Strip ANSI to check content - plain = result.replace(Fore.GREEN, "").replace(Fore.RED, "").replace(Fore.YELLOW, "").replace(Style.RESET_ALL, "") + plain = result.plain assert " 3" in plain assert " 2" in plain assert " 1" in plain @@ -35,53 +45,18 @@ def test_format_test_cell_mixed_values(): def test_format_test_cell_colors(): result = _format_test_cell(TestStats(total=1, passed=1, failed=2, skipped=3, missing=4)) - assert Fore.GREEN in result # passed - assert Fore.RED in result # failed - assert Fore.YELLOW in result # skipped - assert _ORANGE in result # missing + rendered = _render(result) + assert "\033[32m" in rendered # passed is green + assert "\033[31m" in rendered # failed is red + assert "\033[33m" in rendered # skipped is yellow + assert " 4" in result.plain # missing value is present def test_format_test_cell_zero_slots_are_blank(): result = _format_test_cell(TestStats(total=5, passed=0, failed=0, skipped=0, missing=3)) - plain = ( - result.replace(Fore.GREEN, "") - .replace(Fore.RED, "") - .replace(Fore.YELLOW, "") - .replace(_ORANGE, "") - .replace(Style.RESET_ALL, "") - ) - assert " 5" in plain - assert " 3" in plain - - -# --------------------------------------------------------------------------- -# _extend_row -# --------------------------------------------------------------------------- - - -def test_extend_row_not_applicable(): - """not_applicable=True appends single empty cell.""" - row = [] - _extend_row(TestStats(not_applicable=True), row, kind="automated") - assert len(row) == 1 - assert row[0] == "" - - -def test_extend_row_appends_single_cell(): - """_extend_row appends exactly one cell.""" - row = [] - _extend_row(TestStats(total=5, passed=3, failed=2), row, kind="automated") - assert len(row) == 1 - - -def test_extend_row_cell_contains_values(): - """The single cell contains the test counts.""" - row = [] - _extend_row(TestStats(total=5, passed=3, failed=2), row, kind="automated") - plain = row[0].replace(Fore.GREEN, "").replace(Fore.RED, "").replace(Fore.YELLOW, "").replace(Style.RESET_ALL, "") + plain = result.plain assert " 5" in plain assert " 3" in plain - assert " 2" in plain # --------------------------------------------------------------------------- @@ -100,8 +75,8 @@ def test_build_table_completed_req_is_green(): completed=True, implementation=IMPLEMENTATION.IN_CODE, ) - assert Fore.GREEN in row[1] - assert "REQ_001" in row[1] + assert row[1].style == "green" + assert "REQ_001" in row[1].plain def test_build_table_incomplete_req_is_red(): @@ -115,7 +90,7 @@ def test_build_table_incomplete_req_is_red(): completed=False, implementation=IMPLEMENTATION.IN_CODE, ) - assert Fore.RED in row[1] + assert row[1].style == "red" def test_build_table_not_applicable_shows_na(): @@ -129,7 +104,7 @@ def test_build_table_not_applicable_shows_na(): completed=True, implementation=IMPLEMENTATION.NOT_APPLICABLE, ) - assert row[2] == "N/A" + assert row[2].plain == "N/A" def test_build_table_in_code_with_impls_shows_count(): @@ -143,8 +118,8 @@ def test_build_table_in_code_with_impls_shows_count(): completed=True, implementation=IMPLEMENTATION.IN_CODE, ) - assert "2" in row[2] - assert Fore.GREEN in row[2] + assert "2" in row[2].plain + assert row[2].style == "green" def test_build_table_in_code_no_impls_shows_zero(): @@ -158,8 +133,8 @@ def test_build_table_in_code_no_impls_shows_zero(): completed=False, implementation=IMPLEMENTATION.IN_CODE, ) - assert "0" in row[2] - assert Fore.RED in row[2] + assert "0" in row[2].plain + assert row[2].style == "red" def test_build_table_urn_is_first_column(): @@ -173,7 +148,7 @@ def test_build_table_urn_is_first_column(): completed=True, implementation=IMPLEMENTATION.IN_CODE, ) - assert row[0] == "ms-001" + assert row[0].plain == "ms-001" def test_build_table_returns_5_columns(): @@ -214,7 +189,7 @@ def test_summarize_statistics_all_complete_has_green_header(): total_svcs=2, ) ) - assert Fore.GREEN in result + assert "\033[32m" in result # green ANSI code def test_summarize_statistics_incomplete_has_red_header(): @@ -231,7 +206,7 @@ def test_summarize_statistics_incomplete_has_red_header(): total_svcs=3, ) ) - assert Fore.RED in result + assert "\033[31m" in result # red ANSI code def test_summarize_statistics_contains_percentage_string(): From 003ee2d129bdfa0b4f85760e4fc445c50ed478cf Mon Sep 17 00:00:00 2001 From: Jimisola Laursen Date: Sun, 22 Mar 2026 19:54:14 +0100 Subject: [PATCH 2/6] docs: mark status.py Rich refactor as fixed in PR #335 Signed-off-by: Jimisola Laursen --- PLAN_CODE_SMELLS.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/PLAN_CODE_SMELLS.md b/PLAN_CODE_SMELLS.md index 1c17b667..4ef8c97d 100644 --- a/PLAN_CODE_SMELLS.md +++ b/PLAN_CODE_SMELLS.md @@ -20,10 +20,10 @@ Identified during the Pydantic v2 migration (PR #306). These are **not** related ### ~~Duplicated filter parsing logic~~ — FIXED in PR #332 - Extracted `parse_filters()` into `common/filter_parser.py`; both generators reduced to 3-line calls; `# NOSONAR` removed -### Monolithic `status.py` with manual table rendering (322 lines) -- **Files:** `commands/status/status.py` -- **Problem:** Still manually constructs Unicode box-drawing characters and ANSI color codes (`colorama`, `\033[38;5;208m`). -- **Fix:** Replace manual table rendering with **Rich** library. Eliminates ~120 lines of string manipulation. +### ~~Monolithic `status.py` with manual table rendering (322 lines)~~ — FIXED in PR #335 +- Replaced `colorama` + `tabulate` with Rich (`Panel`, `Table`, `Text`, `Console`) in `status.py` and `semantic_validator.py` +- Removed `colorama` and `tabulate` dependencies; added `rich>=13.0` +- Reduced from 323 → 255 lines ### ~~No unit tests for CLI entry point~~ — FIXED in PR #331 - Added `tests/unit/reqstool/test_command.py` with 12 tests covering routing, deprecation warnings, error handling, argument parsing From 2eee3c4a0b11ae391ae734bdf7d108feb8ea13d2 Mon Sep 17 00:00:00 2001 From: Jimisola Laursen Date: Sun, 22 Mar 2026 20:06:46 +0100 Subject: [PATCH 3/6] =?UTF-8?q?fix(status):=20align=20layout=20=E2=80=94?= =?UTF-8?q?=20title=20centered=20over=20table,=20ID=20left-aligned,=20sepa?= =?UTF-8?q?rator=20before=20totals,=20Code/NA=20inside=20impl=20table?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jimisola Laursen --- src/reqstool/commands/status/status.py | 47 ++++++++++++++------------ 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/src/reqstool/commands/status/status.py b/src/reqstool/commands/status/status.py index b4659805..3acae124 100644 --- a/src/reqstool/commands/status/status.py +++ b/src/reqstool/commands/status/status.py @@ -5,7 +5,6 @@ import json from rich.console import Console -from rich.panel import Panel from rich.table import Table, box from rich.text import Text from reqstool_python_decorators.decorators.decorators import Requirements @@ -133,10 +132,25 @@ def _get_row_with_totals(stats_service: StatisticsService) -> list: def _status_table(stats_service: StatisticsService) -> str: - table_data = [] + ts = stats_service.total_statistics + + table = Table( + box=box.HEAVY_HEAD, + show_header=True, + header_style="bold", + title=f"REQUIREMENTS: {ts.total_requirements}", + title_style="bold", + title_justify="center", + ) + table.add_column("URN", justify="center") + table.add_column("ID", justify="left") + table.add_column("Implementation", justify="center") + table.add_column("Automated Tests", justify="center") + table.add_column("Manual Tests", justify="center") + for req, stats in stats_service.requirement_statistics.items(): - table_data.append( - _build_table( + table.add_row( + *_build_table( req_id=req.id, urn=req.urn, impls=stats.implementations, @@ -146,16 +160,9 @@ def _status_table(stats_service: StatisticsService) -> str: implementation=stats.implementation_type, ) ) - table_data.append(_get_row_with_totals(stats_service)) - - table = Table(box=box.HEAVY_HEAD, show_header=True, header_style="bold") - for col in ["URN", "ID", "Implementation", "Automated Tests", "Manual Tests"]: - table.add_column(col, justify="center") - for row in table_data: - table.add_row(*row) - ts = stats_service.total_statistics - panel = Panel(f"REQUIREMENTS: {ts.total_requirements}", expand=True) + table.add_section() + table.add_row(*_get_row_with_totals(stats_service)) legend = Text("T = Total, ") legend.append("P = Passed", style="green") @@ -170,7 +177,6 @@ def _status_table(stats_service: StatisticsService) -> str: console = Console(highlight=False, force_terminal=True, color_system="standard") with console.capture() as cap: - console.print(panel) console.print(table) console.print(legend) return cap.get() + statistics @@ -225,12 +231,7 @@ def _summarize_statistics(ts: TotalStats) -> str: + __numbers_as_percentage(numerator=ts.missing_manual_tests, denominator=ts.total_svcs), ] - code_na_header = Text() - code_na_header.append_text(CODE) - code_na_header.append(" | ") - code_na_header.append_text(NA) - - impl_table = Table(box=box.HEAVY_HEAD, show_header=True) + impl_table = Table(box=box.HEAVY_HEAD, show_header=True, title=IMPLEMENTATIONS, title_justify="center") impl_table.add_column("Total", justify="center") impl_table.add_column("Implemented", justify="center") impl_table.add_column("Verified", justify="center") @@ -238,12 +239,16 @@ def _summarize_statistics(ts: TotalStats) -> str: impl_table.add_column("Total", justify="center") impl_table.add_column("Verified", justify="center") impl_table.add_column("Not Verified", justify="center") + # Code / N/A subgroup row inside the table, separated from data by a section line + impl_table.add_row(CODE, Text(""), Text(""), Text(""), NA, Text(""), Text("")) + impl_table.add_section() impl_table.add_row(*implementation_data) svc_table = Table( box=box.HEAVY_HEAD, show_header=True, title=f"Total Tests: {ts.total_tests} | Total SVCs: {ts.total_svcs}", + title_justify="center", ) svc_table.add_column("Passed tests", justify="center") svc_table.add_column("Failed tests", justify="center") @@ -254,8 +259,6 @@ def _summarize_statistics(ts: TotalStats) -> str: console = Console(highlight=False, force_terminal=True, color_system="standard") with console.capture() as cap: - console.print(Panel(IMPLEMENTATIONS, expand=True)) - console.print(code_na_header) console.print(impl_table) console.print(svc_table) return cap.get() From a5f40b9e3c74a8499c925c8d0dee3930ebaa0dad Mon Sep 17 00:00:00 2001 From: Jimisola Laursen Date: Sun, 22 Mar 2026 20:17:58 +0100 Subject: [PATCH 4/6] =?UTF-8?q?fix(status):=20restore=20original=20layout?= =?UTF-8?q?=20=E2=80=94=20bordered=20header=20boxes,=20row=20separators,?= =?UTF-8?q?=20Code/NA=20and=20Tests/SVCs=20as=202-cell=20boxes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jimisola Laursen --- src/reqstool/commands/status/status.py | 102 ++++++++++++++++++------- 1 file changed, 74 insertions(+), 28 deletions(-) diff --git a/src/reqstool/commands/status/status.py b/src/reqstool/commands/status/status.py index 3acae124..90487e7b 100644 --- a/src/reqstool/commands/status/status.py +++ b/src/reqstool/commands/status/status.py @@ -3,6 +3,7 @@ from __future__ import annotations import json +import re from rich.console import Console from rich.table import Table, box @@ -20,6 +21,55 @@ _ORANGE = "dark_orange" _DIM = "dim" +_ANSI_ESCAPE = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])") + + +def _make_console() -> Console: + return Console(highlight=False, force_terminal=True, color_system="standard", width=200) + + +def _render(*renderables) -> str: + console = _make_console() + with console.capture() as cap: + for r in renderables: + console.print(r) + return cap.get() + + +def _visual_width(rendered: str) -> int: + """Visual width of rendered output after stripping ANSI codes.""" + return max((len(_ANSI_ESCAPE.sub("", line)) for line in rendered.split("\n") if line.strip()), default=0) + + +def _single_header_box(content, outer_width: int) -> Table: + """Single-cell bordered box sized to match outer_width. + + For box.DOUBLE_EDGE with default padding (0,1): + outer_width = 1(border) + 1(pad) + content_width + 1(pad) + 1(border) + => content_min_width = outer_width - 4 + """ + t = Table(box=box.DOUBLE_EDGE, show_header=False) + t.add_column("", justify="center", min_width=max(1, outer_width - 4)) + t.add_row(content) + return t + + +def _two_cell_header_box(left, right, outer_width: int, split: tuple[int, int] = (1, 1)) -> Table: + """Two-cell bordered box sized to match outer_width with given column split ratio. + + For box.DOUBLE_EDGE with default padding (0,1) and 2 columns: + outer_width = 1(left border) + 1(pad) + col1 + 1(pad) + 1(sep) + 1(pad) + col2 + 1(pad) + 1(right border) + => col1 + col2 = outer_width - 7 + """ + inner = max(2, outer_width - 7) + total_parts = split[0] + split[1] + col1_w = max(1, inner * split[0] // total_parts) + col2_w = max(1, inner - col1_w) + t = Table(box=box.DOUBLE_EDGE, show_header=False) + t.add_column("", justify="center", min_width=col1_w) + t.add_column("", justify="center", min_width=col2_w) + t.add_row(left, right) + return t @Requirements("REQ_027") @@ -134,14 +184,7 @@ def _get_row_with_totals(stats_service: StatisticsService) -> list: def _status_table(stats_service: StatisticsService) -> str: ts = stats_service.total_statistics - table = Table( - box=box.HEAVY_HEAD, - show_header=True, - header_style="bold", - title=f"REQUIREMENTS: {ts.total_requirements}", - title_style="bold", - title_justify="center", - ) + table = Table(box=box.DOUBLE_EDGE, show_header=True, header_style="bold", show_lines=True) table.add_column("URN", justify="center") table.add_column("ID", justify="left") table.add_column("Implementation", justify="center") @@ -164,6 +207,11 @@ def _status_table(stats_service: StatisticsService) -> str: table.add_section() table.add_row(*_get_row_with_totals(stats_service)) + rendered_table = _render(table) + table_w = _visual_width(rendered_table) + + req_box = _single_header_box(f"REQUIREMENTS: {ts.total_requirements}", table_w) + legend = Text("T = Total, ") legend.append("P = Passed", style="green") legend.append(", ") @@ -175,11 +223,7 @@ def _status_table(stats_service: StatisticsService) -> str: statistics = _summarize_statistics(ts) - console = Console(highlight=False, force_terminal=True, color_system="standard") - with console.capture() as cap: - console.print(table) - console.print(legend) - return cap.get() + statistics + return _render(req_box) + rendered_table + _render(legend) + statistics def _summarize_statistics(ts: TotalStats) -> str: @@ -231,7 +275,7 @@ def _summarize_statistics(ts: TotalStats) -> str: + __numbers_as_percentage(numerator=ts.missing_manual_tests, denominator=ts.total_svcs), ] - impl_table = Table(box=box.HEAVY_HEAD, show_header=True, title=IMPLEMENTATIONS, title_justify="center") + impl_table = Table(box=box.DOUBLE_EDGE, show_header=True) impl_table.add_column("Total", justify="center") impl_table.add_column("Implemented", justify="center") impl_table.add_column("Verified", justify="center") @@ -239,17 +283,9 @@ def _summarize_statistics(ts: TotalStats) -> str: impl_table.add_column("Total", justify="center") impl_table.add_column("Verified", justify="center") impl_table.add_column("Not Verified", justify="center") - # Code / N/A subgroup row inside the table, separated from data by a section line - impl_table.add_row(CODE, Text(""), Text(""), Text(""), NA, Text(""), Text("")) - impl_table.add_section() impl_table.add_row(*implementation_data) - svc_table = Table( - box=box.HEAVY_HEAD, - show_header=True, - title=f"Total Tests: {ts.total_tests} | Total SVCs: {ts.total_svcs}", - title_justify="center", - ) + svc_table = Table(box=box.DOUBLE_EDGE, show_header=True) svc_table.add_column("Passed tests", justify="center") svc_table.add_column("Failed tests", justify="center") svc_table.add_column("Skipped tests", justify="center") @@ -257,11 +293,21 @@ def _summarize_statistics(ts: TotalStats) -> str: svc_table.add_column("SVCs missing MVRs", justify="center") svc_table.add_row(*svc_data) - console = Console(highlight=False, force_terminal=True, color_system="standard") - with console.capture() as cap: - console.print(impl_table) - console.print(svc_table) - return cap.get() + rendered_impl = _render(impl_table) + impl_w = _visual_width(rendered_impl) + + rendered_svc = _render(svc_table) + svc_w = _visual_width(rendered_svc) + + impl_box = _single_header_box(IMPLEMENTATIONS, impl_w) + code_na_box = _two_cell_header_box(CODE, NA, impl_w, split=(4, 3)) + totals_box = _two_cell_header_box( + f"Total Tests: {ts.total_tests}", + f"Total SVCs: {ts.total_svcs}", + svc_w, + ) + + return _render(impl_box) + _render(code_na_box) + rendered_impl + _render(totals_box) + rendered_svc def __numbers_as_percentage(numerator: int, denominator: int) -> str: From 40449df19fca740a55a2ce8a3fd26494c4fa85a5 Mon Sep 17 00:00:00 2001 From: Jimisola Laursen Date: Sun, 22 Mar 2026 23:19:21 +0100 Subject: [PATCH 5/6] refactor(status): redesign summary layout with Rich tables - Replace colorama/tabulate svc table with two side-by-side Rich tables: Total Tests (Passed/Failed/Skipped) and Total SVCs (missing tests/MVRs) - Center IMPLEMENTATIONS header above In Code / Not in Code tables using two-pass render+measure approach - Make IMPLEMENTATIONS, In Code, Not in Code headers plain white (not colored green/red based on completion) - Use legend as table caption (centered automatically by Rich) - Add empty line before IMPLEMENTATIONS section Signed-off-by: Jimisola Laursen --- src/reqstool/commands/status/status.py | 172 +++++++----------- .../status/test_status_presentation.py | 14 +- 2 files changed, 73 insertions(+), 113 deletions(-) diff --git a/src/reqstool/commands/status/status.py b/src/reqstool/commands/status/status.py index 90487e7b..1ce9b44b 100644 --- a/src/reqstool/commands/status/status.py +++ b/src/reqstool/commands/status/status.py @@ -4,7 +4,9 @@ import json import re +import shutil +from rich.columns import Columns from rich.console import Console from rich.table import Table, box from rich.text import Text @@ -25,7 +27,8 @@ def _make_console() -> Console: - return Console(highlight=False, force_terminal=True, color_system="standard", width=200) + width = max(80, shutil.get_terminal_size((120, 24)).columns) + return Console(highlight=False, force_terminal=True, color_system="standard", width=width) def _render(*renderables) -> str: @@ -36,42 +39,6 @@ def _render(*renderables) -> str: return cap.get() -def _visual_width(rendered: str) -> int: - """Visual width of rendered output after stripping ANSI codes.""" - return max((len(_ANSI_ESCAPE.sub("", line)) for line in rendered.split("\n") if line.strip()), default=0) - - -def _single_header_box(content, outer_width: int) -> Table: - """Single-cell bordered box sized to match outer_width. - - For box.DOUBLE_EDGE with default padding (0,1): - outer_width = 1(border) + 1(pad) + content_width + 1(pad) + 1(border) - => content_min_width = outer_width - 4 - """ - t = Table(box=box.DOUBLE_EDGE, show_header=False) - t.add_column("", justify="center", min_width=max(1, outer_width - 4)) - t.add_row(content) - return t - - -def _two_cell_header_box(left, right, outer_width: int, split: tuple[int, int] = (1, 1)) -> Table: - """Two-cell bordered box sized to match outer_width with given column split ratio. - - For box.DOUBLE_EDGE with default padding (0,1) and 2 columns: - outer_width = 1(left border) + 1(pad) + col1 + 1(pad) + 1(sep) + 1(pad) + col2 + 1(pad) + 1(right border) - => col1 + col2 = outer_width - 7 - """ - inner = max(2, outer_width - 7) - total_parts = split[0] + split[1] - col1_w = max(1, inner * split[0] // total_parts) - col2_w = max(1, inner - col1_w) - t = Table(box=box.DOUBLE_EDGE, show_header=False) - t.add_column("", justify="center", min_width=col1_w) - t.add_column("", justify="center", min_width=col2_w) - t.add_row(left, right) - return t - - @Requirements("REQ_027") class StatusCommand: def __init__(self, location: LocationInterface, format: str = "console"): @@ -184,7 +151,24 @@ def _get_row_with_totals(stats_service: StatisticsService) -> list: def _status_table(stats_service: StatisticsService) -> str: ts = stats_service.total_statistics - table = Table(box=box.DOUBLE_EDGE, show_header=True, header_style="bold", show_lines=True) + legend = Text("T = Total, ") + legend.append("P = Passed", style="green") + legend.append(", ") + legend.append("F = Failed", style="red") + legend.append(", ") + legend.append("S = Skipped", style="yellow") + legend.append(", ") + legend.append("M = Missing", style=_ORANGE) + + table = Table( + box=box.DOUBLE_EDGE, + show_header=True, + header_style="bold", + show_lines=True, + title=f"REQUIREMENTS: {ts.total_requirements}", + title_style="bold", + caption=legend, + ) table.add_column("URN", justify="center") table.add_column("ID", justify="left") table.add_column("Implementation", justify="center") @@ -207,23 +191,9 @@ def _status_table(stats_service: StatisticsService) -> str: table.add_section() table.add_row(*_get_row_with_totals(stats_service)) - rendered_table = _render(table) - table_w = _visual_width(rendered_table) - - req_box = _single_header_box(f"REQUIREMENTS: {ts.total_requirements}", table_w) - - legend = Text("T = Total, ") - legend.append("P = Passed", style="green") - legend.append(", ") - legend.append("F = Failed", style="red") - legend.append(", ") - legend.append("S = Skipped", style="yellow") - legend.append(", ") - legend.append("M = Missing", style=_ORANGE) - statistics = _summarize_statistics(ts) - return _render(req_box) + rendered_table + _render(legend) + statistics + return _render(table) + statistics def _summarize_statistics(ts: TotalStats) -> str: @@ -232,22 +202,32 @@ def _summarize_statistics(ts: TotalStats) -> str: code_reqs = ts.total_requirements - nr_of_reqs_without_implementation code_completed = ts.completed_requirements - nr_of_completed_reqs_without_implementation - CODE, NA, IMPLEMENTATIONS = __colorize_headers( - total=ts.total_requirements, - total_completed=ts.completed_requirements, - total_reqs_no_impl=nr_of_reqs_without_implementation, - completed_reqs_no_impl=nr_of_completed_reqs_without_implementation, - ) + CODE, NA, IMPLEMENTATIONS = __colorize_headers() - implementation_data = [ + # Code group: 4 stats (total, implemented, verified, not verified) + code_table = Table(box=box.DOUBLE_EDGE, show_header=True, title=CODE, title_justify="center") + code_table.add_column("Total", justify="center") + code_table.add_column("Implemented", justify="center") + code_table.add_column("Verified", justify="center") + code_table.add_column("Not Verified", justify="center") + code_table.add_row( str(code_reqs) + __numbers_as_percentage(numerator=code_reqs, denominator=code_reqs), - str(ts.with_implementation) + __numbers_as_percentage(numerator=ts.with_implementation, denominator=code_reqs), + str(ts.with_implementation) + + __numbers_as_percentage(numerator=ts.with_implementation, denominator=code_reqs), str(code_completed) + __numbers_as_percentage(numerator=code_completed, denominator=code_reqs), str(ts.total_requirements - (nr_of_reqs_without_implementation + code_completed)) + __numbers_as_percentage( numerator=(ts.total_requirements - (nr_of_reqs_without_implementation + code_completed)), denominator=code_reqs, ), + ) + + # N/A group: 3 stats (total, verified, not verified) + na_table = Table(box=box.DOUBLE_EDGE, show_header=True, title=NA, title_justify="center") + na_table.add_column("Total", justify="center") + na_table.add_column("Verified", justify="center") + na_table.add_column("Not Verified", justify="center") + na_table.add_row( str(nr_of_reqs_without_implementation) + __numbers_as_percentage( numerator=nr_of_reqs_without_implementation, @@ -263,51 +243,39 @@ def _summarize_statistics(ts: TotalStats) -> str: numerator=(nr_of_reqs_without_implementation - nr_of_completed_reqs_without_implementation), denominator=nr_of_reqs_without_implementation, ), - ] + ) - svc_data = [ + tests_table = Table(box=box.DOUBLE_EDGE, show_header=True, title=f"Total Tests: {ts.total_tests}", title_style="white") + tests_table.add_column("Passed tests", justify="center") + tests_table.add_column("Failed tests", justify="center") + tests_table.add_column("Skipped tests", justify="center") + tests_table.add_row( str(ts.passed_tests) + __numbers_as_percentage(numerator=ts.passed_tests, denominator=ts.total_tests), str(ts.failed_tests) + __numbers_as_percentage(numerator=ts.failed_tests, denominator=ts.total_tests), str(ts.skipped_tests) + __numbers_as_percentage(numerator=ts.skipped_tests, denominator=ts.total_tests), + ) + + svcs_table = Table(box=box.DOUBLE_EDGE, show_header=True, title=f"Total SVCs: {ts.total_svcs}", title_style="white") + svcs_table.add_column("SVCs missing tests", justify="center") + svcs_table.add_column("SVCs missing MVRs", justify="center") + svcs_table.add_row( str(ts.missing_automated_tests) + __numbers_as_percentage(numerator=ts.missing_automated_tests, denominator=ts.total_svcs), str(ts.missing_manual_tests) + __numbers_as_percentage(numerator=ts.missing_manual_tests, denominator=ts.total_svcs), - ] + ) - impl_table = Table(box=box.DOUBLE_EDGE, show_header=True) - impl_table.add_column("Total", justify="center") - impl_table.add_column("Implemented", justify="center") - impl_table.add_column("Verified", justify="center") - impl_table.add_column("Not Verified", justify="center") - impl_table.add_column("Total", justify="center") - impl_table.add_column("Verified", justify="center") - impl_table.add_column("Not Verified", justify="center") - impl_table.add_row(*implementation_data) - - svc_table = Table(box=box.DOUBLE_EDGE, show_header=True) - svc_table.add_column("Passed tests", justify="center") - svc_table.add_column("Failed tests", justify="center") - svc_table.add_column("Skipped tests", justify="center") - svc_table.add_column("SVCs missing tests", justify="center") - svc_table.add_column("SVCs missing MVRs", justify="center") - svc_table.add_row(*svc_data) - - rendered_impl = _render(impl_table) - impl_w = _visual_width(rendered_impl) - - rendered_svc = _render(svc_table) - svc_w = _visual_width(rendered_svc) - - impl_box = _single_header_box(IMPLEMENTATIONS, impl_w) - code_na_box = _two_cell_header_box(CODE, NA, impl_w, split=(4, 3)) - totals_box = _two_cell_header_box( - f"Total Tests: {ts.total_tests}", - f"Total SVCs: {ts.total_svcs}", - svc_w, + cols_rendered = _render(Columns([code_table, na_table])) + cols_width = max( + (len(_ANSI_ESCAPE.sub("", line)) for line in cols_rendered.split("\n") if line.strip()), + default=80, ) + impl_console = Console(highlight=False, force_terminal=True, color_system="standard", width=cols_width) + with impl_console.capture() as cap: + impl_console.print(IMPLEMENTATIONS, justify="center") + impl_header = cap.get() - return _render(impl_box) + _render(code_na_box) + rendered_impl + _render(totals_box) + rendered_svc + return "\n" + impl_header + cols_rendered + _render(Columns([tests_table, svcs_table])) def __numbers_as_percentage(numerator: int, denominator: int) -> str: @@ -318,15 +286,5 @@ def __numbers_as_percentage(numerator: int, denominator: int) -> str: return percentage_as_string -def __colorize_headers( - total: int, total_completed: int, total_reqs_no_impl: int, completed_reqs_no_impl: int -) -> tuple[Text, Text, Text]: - total_code = total - total_reqs_no_impl - total_code_completed = total_code == (total_completed - completed_reqs_no_impl) - total_no_impl_completed = total_reqs_no_impl - completed_reqs_no_impl == 0 - - CODE = Text("Code", style="green" if total_code_completed else "red") - NA = Text("N/A", style="green" if total_no_impl_completed else "red") - IMPLEMENTATIONS = Text("IMPLEMENTATIONS", style="green" if total == total_completed else "red") - - return CODE, NA, IMPLEMENTATIONS +def __colorize_headers() -> tuple[Text, Text, Text]: + return Text("In Code", style="white"), Text("Not in Code", style="white"), Text("IMPLEMENTATIONS", style="bold white") diff --git a/tests/unit/reqstool/commands/status/test_status_presentation.py b/tests/unit/reqstool/commands/status/test_status_presentation.py index 29a2cbc6..0ce860a6 100644 --- a/tests/unit/reqstool/commands/status/test_status_presentation.py +++ b/tests/unit/reqstool/commands/status/test_status_presentation.py @@ -177,8 +177,8 @@ def test_summarize_statistics_zero_counts_no_crash(): assert "IMPLEMENTATIONS" in result -def test_summarize_statistics_all_complete_has_green_header(): - """All requirements complete: IMPLEMENTATIONS header is green.""" +def test_summarize_statistics_all_complete_has_white_header(): + """IMPLEMENTATIONS header is always white regardless of completion.""" result = _summarize_statistics( TotalStats( total_requirements=2, @@ -189,11 +189,12 @@ def test_summarize_statistics_all_complete_has_green_header(): total_svcs=2, ) ) - assert "\033[32m" in result # green ANSI code + assert "\033[37m" in result # white ANSI code + assert "IMPLEMENTATIONS" in result -def test_summarize_statistics_incomplete_has_red_header(): - """Incomplete requirements: at least one header is red.""" +def test_summarize_statistics_incomplete_has_white_header(): + """IMPLEMENTATIONS header is always white regardless of completion.""" result = _summarize_statistics( TotalStats( total_requirements=3, @@ -206,7 +207,8 @@ def test_summarize_statistics_incomplete_has_red_header(): total_svcs=3, ) ) - assert "\033[31m" in result # red ANSI code + assert "\033[37m" in result # white ANSI code + assert "IMPLEMENTATIONS" in result def test_summarize_statistics_contains_percentage_string(): From c009afd354888f50456183de884e7ac33cccb1c0 Mon Sep 17 00:00:00 2001 From: Jimisola Laursen Date: Sun, 22 Mar 2026 23:27:30 +0100 Subject: [PATCH 6/6] style(status): run black/flake8; remove PLAN_CODE_SMELLS.md - black reformatted status.py - removed unused Text and _ORANGE imports from test file - deleted PLAN_CODE_SMELLS.md (all items resolved) Signed-off-by: Jimisola Laursen --- PLAN_CODE_SMELLS.md | 47 ------------------- src/reqstool/commands/status/status.py | 13 +++-- .../status/test_status_presentation.py | 3 +- 3 files changed, 10 insertions(+), 53 deletions(-) delete mode 100644 PLAN_CODE_SMELLS.md diff --git a/PLAN_CODE_SMELLS.md b/PLAN_CODE_SMELLS.md deleted file mode 100644 index 4ef8c97d..00000000 --- a/PLAN_CODE_SMELLS.md +++ /dev/null @@ -1,47 +0,0 @@ -# Code Smells — Future Issues - -Identified during the Pydantic v2 migration (PR #306). These are **not** related to the migration itself and should be addressed in separate issues/PRs. - ---- - -## High Severity - -### ~~Scattered `sys.exit()` in location classes~~ — FIXED in PR #329 -- `LocationError` / `ArtifactDownloadError` / `ArtifactExtractionError` added to exception hierarchy -- Location classes now raise exceptions; only `command.py` calls `sys.exit()` - -### ~~Mutable singleton `TempDirectoryUtil`~~ — FIXED in PR #330 -- Replaced with instance-based `TempDirectoryManager` (context manager, DI, guaranteed cleanup) - ---- - -## Medium Severity - -### ~~Duplicated filter parsing logic~~ — FIXED in PR #332 -- Extracted `parse_filters()` into `common/filter_parser.py`; both generators reduced to 3-line calls; `# NOSONAR` removed - -### ~~Monolithic `status.py` with manual table rendering (322 lines)~~ — FIXED in PR #335 -- Replaced `colorama` + `tabulate` with Rich (`Panel`, `Table`, `Text`, `Console`) in `status.py` and `semantic_validator.py` -- Removed `colorama` and `tabulate` dependencies; added `rich>=13.0` -- Reduced from 323 → 255 lines - -### ~~No unit tests for CLI entry point~~ — FIXED in PR #331 -- Added `tests/unit/reqstool/test_command.py` with 12 tests covering routing, deprecation warnings, error handling, argument parsing - ---- - -## Low Severity - -### ~~Repetitive CLI parser setup~~ — FIXED in PR #333 -- Replaced 52-line repetitive body of `_add_subparsers_source` with `_LOCATION_DEFS` config dict + 16-line loop - ---- - -## Resolved (no longer valid) - -### `@dataclass` in generators/validators — RESOLVED -- `combined_indexed_dataset_generator.py` and `indexed_dataset_filter_processor.py` no longer exist -- Remaining `@dataclass` use in `syntax_validator.py` is legitimate (schema registry item) - -### Expression language — empty subclasses — RESOLVED -- `requirements_el.py` and `svcs_el.py` no longer exist; generic transformer used directly diff --git a/src/reqstool/commands/status/status.py b/src/reqstool/commands/status/status.py index 1ce9b44b..49105849 100644 --- a/src/reqstool/commands/status/status.py +++ b/src/reqstool/commands/status/status.py @@ -212,8 +212,7 @@ def _summarize_statistics(ts: TotalStats) -> str: code_table.add_column("Not Verified", justify="center") code_table.add_row( str(code_reqs) + __numbers_as_percentage(numerator=code_reqs, denominator=code_reqs), - str(ts.with_implementation) - + __numbers_as_percentage(numerator=ts.with_implementation, denominator=code_reqs), + str(ts.with_implementation) + __numbers_as_percentage(numerator=ts.with_implementation, denominator=code_reqs), str(code_completed) + __numbers_as_percentage(numerator=code_completed, denominator=code_reqs), str(ts.total_requirements - (nr_of_reqs_without_implementation + code_completed)) + __numbers_as_percentage( @@ -245,7 +244,9 @@ def _summarize_statistics(ts: TotalStats) -> str: ), ) - tests_table = Table(box=box.DOUBLE_EDGE, show_header=True, title=f"Total Tests: {ts.total_tests}", title_style="white") + tests_table = Table( + box=box.DOUBLE_EDGE, show_header=True, title=f"Total Tests: {ts.total_tests}", title_style="white" + ) tests_table.add_column("Passed tests", justify="center") tests_table.add_column("Failed tests", justify="center") tests_table.add_column("Skipped tests", justify="center") @@ -287,4 +288,8 @@ def __numbers_as_percentage(numerator: int, denominator: int) -> str: def __colorize_headers() -> tuple[Text, Text, Text]: - return Text("In Code", style="white"), Text("Not in Code", style="white"), Text("IMPLEMENTATIONS", style="bold white") + return ( + Text("In Code", style="white"), + Text("Not in Code", style="white"), + Text("IMPLEMENTATIONS", style="bold white"), + ) diff --git a/tests/unit/reqstool/commands/status/test_status_presentation.py b/tests/unit/reqstool/commands/status/test_status_presentation.py index 0ce860a6..29d9ba6d 100644 --- a/tests/unit/reqstool/commands/status/test_status_presentation.py +++ b/tests/unit/reqstool/commands/status/test_status_presentation.py @@ -1,9 +1,8 @@ # Copyright © LFV from rich.console import Console -from rich.text import Text -from reqstool.commands.status.status import _build_table, _format_test_cell, _ORANGE, _summarize_statistics +from reqstool.commands.status.status import _build_table, _format_test_cell, _summarize_statistics from reqstool.models.requirements import IMPLEMENTATION from reqstool.services.statistics_service import TestStats, TotalStats