Skip to content
Draft
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
1 change: 1 addition & 0 deletions doc/changelog.d/501.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add project metadata checks
116 changes: 100 additions & 16 deletions src/ansys/pre_commit_hooks/pyansys_quality_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,24 @@

import click

from ansys.pre_commit_hooks.quality_rules import README, repo_review_checks
try:
import tomllib
except ModuleNotFoundError: # pragma: no cover - Python 3.10 relies on ``toml``.
import toml as tomllib

from ansys.pre_commit_hooks.quality_rules import (
DEFAULT_RULE_FAMILIES,
RULE_FAMILIES,
repo_review_checks,
)
from ansys.pre_commit_hooks.quality_rules.common import (
ERROR,
PASSED,
WARNING,
RuleStatus,
_first_doc_line,
_QualityRule,
file_content,
normalize_check_result,
readme_path,
)
Expand Down Expand Up @@ -217,17 +228,35 @@ def _normalize_rule_ids(argument_values: list[str]) -> set[str]:
}


def _families_to_run(selected_families: set[str] | None) -> tuple[str, ...]:
"""Return the rule families to evaluate.

Parameters
----------
selected_families : set[str] or None
Explicitly requested families.

Returns
-------
tuple[str, ...]
The requested families, or every family when none is requested.
"""
if selected_families:
return tuple(sorted(selected_families))
return DEFAULT_RULE_FAMILIES


def _execute_check(
rule: README,
rule: _QualityRule,
rule_id: str,
repository_root: Path,
detected_readme_file: str | None,
) -> _RuleResult:
"""Execute and serialize one README rule.
"""Execute and serialize one rule.

Parameters
----------
rule : README
rule : _QualityRule
Rule instance to execute.
rule_id : str
Stable identifier for the rule.
Expand Down Expand Up @@ -262,24 +291,28 @@ def _run_checks(
*,
selected_rule_ids: set[str] | None = None,
ignored_rule_ids: set[str] | None = None,
selected_families: set[str] | None = None,
) -> _QualityReport:
"""Evaluate registered README rules for a repository.
"""Evaluate registered rules for a repository.

Parameters
----------
repository_root : pathlib.Path
Repository root directory.
selected_rule_ids : set[str] or None, optional
Rule IDs to evaluate. All rules are evaluated when omitted or empty.
Rule IDs to evaluate. All rules of the evaluated families are used when
omitted or empty.
ignored_rule_ids : set[str] or None, optional
Rule IDs to omit from the report.
selected_families : set[str] or None, optional
Rule families to evaluate. Defaults to every family.

Returns
-------
_QualityReport
Rule results, status counts, and the resulting score.
"""
registered_rules = repo_review_checks()
registered_rules = repo_review_checks(_families_to_run(selected_families))
normalized_selected_ids = {rule_id.upper() for rule_id in selected_rule_ids or set()}
normalized_ignored_ids = {rule_id.upper() for rule_id in ignored_rule_ids or set()}
if normalized_selected_ids:
Expand Down Expand Up @@ -312,20 +345,23 @@ def _run_checks(

def _metadata_report(
selected_rule_ids: set[str] | None = None,
selected_families: set[str] | None = None,
) -> list[_RuleMetadata]:
"""Return metadata for available README rules.
"""Return metadata for available rules.

Parameters
----------
selected_rule_ids : set[str] or None, optional
Rule IDs to include. All rules are included when omitted or empty.
selected_families : set[str] or None, optional
Rule families to include. Every family is listed when omitted or empty.

Returns
-------
list[_RuleMetadata]
Metadata records ordered by rule ID.
"""
registered_rules = repo_review_checks()
registered_rules = repo_review_checks(selected_families or tuple(RULE_FAMILIES))
normalized_selected_ids = {rule_id.upper() for rule_id in selected_rule_ids or set()}
return [
{
Expand Down Expand Up @@ -440,7 +476,7 @@ def _validate_rule_ids(
rule_ids : set[str]
Normalized rule IDs to validate.
"""
available_rule_ids = sorted(repo_review_checks())
available_rule_ids = sorted(repo_review_checks(tuple(RULE_FAMILIES)))
unknown_rule_ids = sorted(rule_ids.difference(available_rule_ids))
if unknown_rule_ids:
raise click.UsageError(
Expand All @@ -449,12 +485,50 @@ def _validate_rule_ids(
)


def _pyproject_ignored_rule_ids(repository_root: Path) -> set[str]:
"""Read ignored rule IDs from ``[tool.pyansys-quality-report]``.

Parameters
----------
repository_root : pathlib.Path
Repository root directory.

Returns
-------
set[str]
Normalized rule IDs listed under the ``ignore`` key. The set is empty when
``pyproject.toml`` is missing or has no such key.

Raises
------
click.UsageError
If ``pyproject.toml`` is invalid TOML or ``ignore`` is not a list of strings.
"""
pyproject_content = file_content(repository_root, "pyproject.toml")
if pyproject_content is None:
return set()
try:
pyproject_data = tomllib.loads(pyproject_content)
except (TypeError, ValueError) as error:
raise click.UsageError(f"pyproject.toml is not valid TOML: {error}") from error

tool_settings = pyproject_data.get("tool", {}).get("pyansys-quality-report", {})
ignored_values = tool_settings.get("ignore", []) if isinstance(tool_settings, dict) else []
if not isinstance(ignored_values, list) or not all(
isinstance(value, str) for value in ignored_values
):
raise click.UsageError(
"[tool.pyansys-quality-report] ignore in pyproject.toml must be a list of strings"
)
return _normalize_rule_ids(ignored_values)


@click.command(help="Run PyAnsys README quality checks.")
@click.option("--json", "json_output", is_flag=True, help="Emit a JSON report.")
@click.option("--metadata", is_flag=True, help="List available rule metadata.")
@click.option("--check", "check", multiple=True, help="Select rule IDs.")
@click.option("--family", multiple=True, help="Select rule family.")
@click.option("--ignore", multiple=True, help="Ignore rule IDs.")
@click.option("--family", multiple=True, help="Select rule family (readme, project_metadata).")
@click.option("--ignore", multiple=True, help="Ignore rule IDs (also read from pyproject.toml).")
@click.option("--show-all", is_flag=True, help="Include passing checks.")
@click.option(
"--fails-only",
Expand Down Expand Up @@ -489,19 +563,29 @@ def _cli(
rule_family.strip().lower()
for argument_value in family
for rule_family in argument_value.split(",")
if rule_family.strip()
}
if selected_rule_families and selected_rule_families - {"readme"}:
raise click.UsageError("This release supports only the readme rule family")
unknown_families = selected_rule_families - set(RULE_FAMILIES)
if unknown_families:
raise click.UsageError(
f"unknown rule family(ies): {', '.join(sorted(unknown_families))}. "
f"Available families: {', '.join(RULE_FAMILIES)}"
)

if metadata:
autoflush_print(json.dumps(_metadata_report(selected_rule_ids), indent=2))
autoflush_print(
json.dumps(_metadata_report(selected_rule_ids, selected_rule_families), indent=2)
)
return 0

repository_root = _find_project_root(Path.cwd())
configured_ignored_rule_ids = _pyproject_ignored_rule_ids(repository_root)
_validate_rule_ids("[tool.pyansys-quality-report] ignore", configured_ignored_rule_ids)
quality_report = _run_checks(
repository_root,
selected_rule_ids=selected_rule_ids,
ignored_rule_ids=ignored_rule_ids,
ignored_rule_ids=ignored_rule_ids | configured_ignored_rule_ids,
selected_families=selected_rule_families,
)
if json_output:
autoflush_print(json.dumps(quality_report, indent=2))
Expand Down
54 changes: 47 additions & 7 deletions src/ansys/pre_commit_hooks/quality_rules/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,25 +22,65 @@

"""Quality-check registry subpackage."""

from collections.abc import Iterable

from ansys.pre_commit_hooks.quality_rules.common import _QualityRule
from ansys.pre_commit_hooks.quality_rules.project_metadata import ProjectMetadata
from ansys.pre_commit_hooks.quality_rules.readme import README

README_QUALITY_RULES: tuple[type[README], ...] = tuple(
sorted(README.__subclasses__(), key=lambda rule_class: rule_class.__name__)
)
PROJECT_METADATA_QUALITY_RULES: tuple[type[ProjectMetadata], ...] = tuple(
sorted(ProjectMetadata.__subclasses__(), key=lambda rule_class: rule_class.__name__)
)

# Maps each rule family name to its rule classes.
RULE_FAMILIES: dict[str, tuple[type[_QualityRule], ...]] = {
README.family: README_QUALITY_RULES,
ProjectMetadata.family: PROJECT_METADATA_QUALITY_RULES,
}
# Families evaluated when no ``--family`` is requested.
DEFAULT_RULE_FAMILIES: tuple[str, ...] = tuple(RULE_FAMILIES)

# Re-export each rule class so the names in ``__all__`` resolve.
globals().update({rule_class.__name__: rule_class for rule_class in README_QUALITY_RULES})
globals().update(
{
rule_class.__name__: rule_class
for rule_classes in RULE_FAMILIES.values()
for rule_class in rule_classes
}
)

__all__ = [
"DEFAULT_RULE_FAMILIES",
"PROJECT_METADATA_QUALITY_RULES",
"README",
"README_QUALITY_RULES",
"RULE_FAMILIES",
"ProjectMetadata",
"repo_review_checks",
]
__all__.extend(
rule_class.__name__ for rule_classes in RULE_FAMILIES.values() for rule_class in rule_classes
)

__all__ = ["README", "README_QUALITY_RULES", "repo_review_checks"]
__all__.extend(rule_class.__name__ for rule_class in README_QUALITY_RULES)

def repo_review_checks(families: Iterable[str] = DEFAULT_RULE_FAMILIES) -> dict[str, _QualityRule]:
"""Return the registered rule instances for the requested families.

def repo_review_checks() -> dict[str, README]:
"""Return the registered README rule instances.
Parameters
----------
families : collections.abc.Iterable[str], default: every family
Rule family names to include.

Returns
-------
dict[str, README]
dict[str, _QualityRule]
Rule instances keyed by their rule IDs.
"""
return {rule_class.__name__: rule_class() for rule_class in README_QUALITY_RULES}
return {
rule_class.__name__: rule_class()
for family in families
for rule_class in RULE_FAMILIES[family]
}
Loading
Loading