Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove formatting options for listing rules #4432

Merged
merged 1 commit into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 5 additions & 12 deletions src/ansiblelint/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@

from ansible_compat.prerun import get_cache_dir
from filelock import BaseFileLock, FileLock, Timeout
from rich.markdown import Markdown
from rich.markup import escape

from ansiblelint.constants import RC, SKIP_SCHEMA_UPDATE
Expand Down Expand Up @@ -72,8 +73,6 @@
if TYPE_CHECKING:
# RulesCollection must be imported lazily or ansible gets imported too early.

from collections.abc import Callable

from ansiblelint.rules import RulesCollection
from ansiblelint.runner import LintResult

Expand Down Expand Up @@ -166,17 +165,11 @@ def initialize_options(arguments: list[str] | None = None) -> BaseFileLock | Non
def _do_list(rules: RulesCollection) -> int:
# On purpose lazy-imports to avoid pre-loading Ansible
# pylint: disable=import-outside-toplevel
from ansiblelint.generate_docs import rules_as_md, rules_as_rich, rules_as_str
from ansiblelint.generate_docs import rules_as_str

if options.list_rules:
_rule_format_map: dict[str, Callable[..., Any]] = {
"brief": rules_as_str,
"full": rules_as_rich,
"md": rules_as_md,
}

console.print(
_rule_format_map.get(options.format, rules_as_str)(rules),
rules_as_str(rules),
highlight=False,
)
return 0
Expand Down Expand Up @@ -339,9 +332,9 @@ def main(argv: list[str] | None = None) -> int:
from ansiblelint.rules import RulesCollection

if options.list_profiles:
from ansiblelint.generate_docs import profiles_as_rich
from ansiblelint.generate_docs import profiles_as_md

console.print(profiles_as_rich())
console.print(Markdown(profiles_as_md()))
return 0

app = get_app(
Expand Down
8 changes: 3 additions & 5 deletions src/ansiblelint/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,24 +255,22 @@ def get_cli_parser() -> argparse.ArgumentParser:
dest="list_profiles",
default=False,
action="store_true",
help="List all profiles, no formatting options available.",
help="List all profiles.",
)
listing_group.add_argument(
"-L",
"--list-rules",
dest="list_rules",
default=False,
action="store_true",
help="List all the rules. For listing rules only the following formats "
"for argument -f are supported: {brief, full, md} with 'brief' as default.",
help="List all the rules.",
)
listing_group.add_argument(
"-T",
"--list-tags",
dest="list_tags",
action="store_true",
help="List all the tags and the rules they cover. Increase the verbosity level "
"with `-v` to include 'opt-in' tag and its rules.",
help="List all the tags and the rules they cover.",
)
parser.add_argument(
"-f",
Expand Down
88 changes: 7 additions & 81 deletions src/ansiblelint/generate_docs.py
Original file line number Diff line number Diff line change
@@ -1,95 +1,26 @@
"""Utils to generate rules documentation."""

import logging
from collections.abc import Iterable

from rich import box
from rich.console import RenderableType, group
from rich.markdown import Markdown
from rich.table import Table

from ansiblelint.config import PROFILES
from ansiblelint.constants import RULE_DOC_URL
from ansiblelint.rules import RulesCollection, TransformMixin

DOC_HEADER = """
# Default Rules

(lint_default_rules)=

Below you can see the list of default rules Ansible Lint use to evaluate playbooks and roles:

"""

_logger = logging.getLogger(__name__)


def rules_as_str(rules: RulesCollection) -> RenderableType:
def rules_as_str(rules: RulesCollection) -> str:
"""Return rules as string."""
table = Table(show_header=False, header_style="title", box=box.SIMPLE)
result = ""
for rule in rules.alphabetical():
if issubclass(rule.__class__, TransformMixin):
rule.tags.insert(0, "autofix")
tag = f"[dim] ({', '.join(rule.tags)})[/dim]" if rule.tags else ""
table.add_row(
f"[link={RULE_DOC_URL}{rule.id}/]{rule.id}[/link]",
rule.shortdesc + tag,
)
return table
tag = f"{','.join(rule.tags)}" if rule.tags else ""
result += f"- [repr.url][link={RULE_DOC_URL}{rule.id}/]{rule.id}[/link][/] {rule.shortdesc}\n[dim] tags:{tag}[/dim]"

if rule.version_changed and rule.version_changed != "historic":
result += f"[dim] modified:{rule.version_changed}[/]"

def rules_as_md(rules: RulesCollection) -> str:
"""Return md documentation for a list of rules."""
result = DOC_HEADER

for rule in rules.alphabetical():
# because title == rule.id we get the desired labels for free
# and we do not have to insert `(target_header)=`
title = f"{rule.id}"

if rule.help:
if not rule.help.startswith(f"# {rule.id}"): # pragma: no cover
msg = f"Rule {rule.__class__} markdown help does not start with `# {rule.id}` header.\n{rule.help}"
raise RuntimeError(msg)
result += f"\n\n{rule.help}"
else:
description = rule.description
if rule.link:
description += f" [more]({rule.link})"

result += f"\n\n## {title}\n\n**{rule.shortdesc}**\n\n{description}"

# Safety net for preventing us from adding autofix to rules and
# forgetting to mention it inside their documentation.
if "autofix" in rule.tags and "autofix" not in rule.description:
msg = f"Rule {rule.id} is invalid because it has 'autofix' tag but this ability is not documented in its description."
raise RuntimeError(msg)

result += " \n"
return result


@group()
def rules_as_rich(rules: RulesCollection) -> Iterable[Table]:
"""Print documentation for a list of rules, returns empty string."""
width = max(16, *[len(rule.id) for rule in rules])
for rule in rules.alphabetical():
table = Table(show_header=True, header_style="title", box=box.MINIMAL)
table.add_column(rule.id, style="dim", width=width)
table.add_column(Markdown(rule.shortdesc))

description = rule.help or rule.description
if rule.link:
description += f" [(more)]({rule.link})"
table.add_row("description", Markdown(description))
if rule.version_changed:
table.add_row("version_changed", rule.version_changed)
if rule.tags:
table.add_row("tags", ", ".join(rule.tags))
if rule.severity:
table.add_row("severity", rule.severity)
yield table


def profiles_as_md(*, header: bool = False, docs_url: str = RULE_DOC_URL) -> str:
"""Return markdown representation of supported profiles."""
result = ""
Expand Down Expand Up @@ -127,8 +58,3 @@ def profiles_as_md(*, header: bool = False, docs_url: str = RULE_DOC_URL) -> str

result += "\n"
return result


def profiles_as_rich() -> Markdown:
"""Return rich representation of supported profiles."""
return Markdown(profiles_as_md())
6 changes: 2 additions & 4 deletions test/test_rules_collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,21 +140,19 @@ def test_no_duplicate_rule_ids() -> None:
assert not any(y > 1 for y in collections.Counter(rule_ids).values())


def test_rich_rule_listing() -> None:
def test_rule_listing() -> None:
"""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 = Path("./test/rules/fixtures").resolve()
result = run_ansible_lint("-r", str(rules_path), "-f", "full", "-L")
result = run_ansible_lint("-r", str(rules_path), "-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


def test_rules_id_format(config_options: Options) -> None:
Expand Down
2 changes: 1 addition & 1 deletion tools/generate_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

if __name__ == "__main__":
subprocess.run( # noqa: S603
["ansible-lint", "-L", "--format", "md"], # noqa: S607
["ansible-lint", "--list-rules"], # noqa: S607
check=True,
stdout=subprocess.DEVNULL,
)
Expand Down
Loading