From b9c345f070914cd8957b9bd880e64ae69b63aa7e Mon Sep 17 00:00:00 2001 From: Sorin Sbarnea Date: Thu, 27 Aug 2020 11:12:52 +0100 Subject: [PATCH] Made console rule listing rich (#978) Adds 'rich' output format for listing rules (-L) and makes it default. This format is more verbose and includes all known rule metadata in a way that makes it easier to read. --- lib/ansiblelint/__main__.py | 16 +++++++++++----- lib/ansiblelint/cli.py | 4 ++-- lib/ansiblelint/color.py | 13 ++++++++++++- lib/ansiblelint/generate_docs.py | 23 +++++++++++++++++++++++ lib/ansiblelint/rules/__init__.py | 2 ++ test/TestRulesCollection.py | 19 +++++++++++++++++++ 6 files changed, 69 insertions(+), 8 deletions(-) diff --git a/lib/ansiblelint/__main__.py b/lib/ansiblelint/__main__.py index b2243d992e..30b688749a 100755 --- a/lib/ansiblelint/__main__.py +++ b/lib/ansiblelint/__main__.py @@ -27,11 +27,11 @@ import sys from typing import TYPE_CHECKING, List, Set, Type -from rich.console import Console from rich.markdown import Markdown from ansiblelint import cli, formatters -from ansiblelint.generate_docs import rules_as_rst +from ansiblelint.color import console +from ansiblelint.generate_docs import rules_as_rich, rules_as_rst from ansiblelint.rules import RulesCollection from ansiblelint.runner import Runner from ansiblelint.utils import get_playbooks_and_roles, get_rules_dirs @@ -42,7 +42,12 @@ from ansiblelint.errors import MatchError _logger = logging.getLogger(__name__) -console = Console() + +_rule_format_map = { + 'plain': str, + 'rich': rules_as_rich, + 'rst': rules_as_rst +} def initialize_logger(level: int = 0) -> None: @@ -123,8 +128,9 @@ def main() -> int: rules = RulesCollection(rulesdirs) if options.listrules: - formatted_rules = rules if options.format == 'plain' else rules_as_rst(rules) - print(formatted_rules) + console.print( + _rule_format_map[options.format](rules), + highlight=False) return 0 if options.listtags: diff --git a/lib/ansiblelint/cli.py b/lib/ansiblelint/cli.py index aa75dc0d73..4b69b47edb 100644 --- a/lib/ansiblelint/cli.py +++ b/lib/ansiblelint/cli.py @@ -96,8 +96,8 @@ def get_cli_parser() -> argparse.ArgumentParser: parser.add_argument('-L', dest='listrules', default=False, action='store_true', help="list all the rules") - parser.add_argument('-f', dest='format', default='plain', - choices=['plain', 'rst'], + parser.add_argument('-f', dest='format', default='rich', + choices=['rich', 'plain', 'rst'], help="Format used rules output, (default: %(default)s)") parser.add_argument('-q', dest='quiet', default=False, diff --git a/lib/ansiblelint/color.py b/lib/ansiblelint/color.py index 3fe5e77bd1..286a096839 100644 --- a/lib/ansiblelint/color.py +++ b/lib/ansiblelint/color.py @@ -1,6 +1,17 @@ -"""Console coloring support.""" +"""Console coloring and terminal support.""" from enum import Enum +from rich.console import Console +from rich.theme import Theme + +_theme = Theme({ + "info": "cyan", + "warning": "dim yellow", + "danger": "bold red", + "title": "yellow" +}) +console = Console(theme=_theme) + class Color(Enum): """Color styles.""" diff --git a/lib/ansiblelint/generate_docs.py b/lib/ansiblelint/generate_docs.py index 21a4204d79..b735b1ff50 100644 --- a/lib/ansiblelint/generate_docs.py +++ b/lib/ansiblelint/generate_docs.py @@ -1,5 +1,11 @@ """Utils to generate rule table .rst documentation.""" import logging +from typing import Iterable + +from rich import box +from rich.console import render_group +from rich.markdown import Markdown +from rich.table import Table from ansiblelint.rules import RulesCollection @@ -41,3 +47,20 @@ def rules_as_rst(rules: RulesCollection) -> str: r += f"\n\n.. _{d.id}:\n\n{title}\n{'*' * len(title)}\n\n{d.description}" return r + + +@render_group() +def rules_as_rich(rules: RulesCollection) -> Iterable[Table]: + """Print documentation for a list of rules, returns empty string.""" + for rule in rules: + table = Table(show_header=True, header_style="title", box=box.MINIMAL) + table.add_column(rule.id, style="dim", width=16) + table.add_column(Markdown(rule.shortdesc)) + table.add_row("description", Markdown(rule.description)) + if rule.version_added: + table.add_row("version_added", rule.version_added) + if rule.tags: + table.add_row("tags", ", ".join(rule.tags)) + if rule.severity: + table.add_row("severity", rule.severity) + yield table diff --git a/lib/ansiblelint/rules/__init__.py b/lib/ansiblelint/rules/__init__.py index 741dae5113..4da17ff330 100644 --- a/lib/ansiblelint/rules/__init__.py +++ b/lib/ansiblelint/rules/__init__.py @@ -28,6 +28,8 @@ def verbose(self) -> str: tags: List[str] = [] shortdesc: str = "" description: str = "" + version_added: str = "" + severity: str = "" match = None matchtask = None matchplay = None diff --git a/test/TestRulesCollection.py b/test/TestRulesCollection.py index 0f59b07975..3461682651 100644 --- a/test/TestRulesCollection.py +++ b/test/TestRulesCollection.py @@ -25,6 +25,8 @@ from ansiblelint.rules import RulesCollection +from . import run_ansible_lint + @pytest.fixture def test_rules_collection(): @@ -92,3 +94,20 @@ def test_no_duplicate_rule_ids(test_rules_collection): real_rules = RulesCollection([os.path.abspath('./lib/ansiblelint/rules')]) rule_ids = [rule.id for rule in real_rules] assert not any(y > 1 for y in collections.Counter(rule_ids).values()) + + +def test_rich_rule_listing(): + """Test that rich list format output is rendered as a table. + + This check also offers the contract of having rule id, short and long + descriptions in the console output. + """ + rules_path = os.path.abspath('./test/rules') + result = run_ansible_lint("-r", rules_path, "-f", "rich", "-L") + assert result.returncode == 0 + + for rule in RulesCollection([rules_path]): + assert rule.id in result.stdout + assert rule.shortdesc in result.stdout + # description could wrap inside table, so we do not check full length + assert rule.description[:30] in result.stdout