Commit 505d835a authored by Pierre Smeyers's avatar Pierre Smeyers
Browse files

style: add pydoc, comments and rename variables

parent 4d893389
Loading
Loading
Loading
Loading
+82 −45
Original line number Diff line number Diff line
@@ -4,7 +4,7 @@ import re
from enum import Enum
from logging import Logger
from pathlib import Path
from typing import Optional, Union
from typing import Any, Optional, Union

import yaml
from pydantic import BaseModel
@@ -62,22 +62,37 @@ class TbcVarType(str, Enum):


class GlInput(BaseModel):
    # name: str
    """GitLab input from the 'specs' section."""

    description: Optional[str] = None
    """Input description."""
    type: GlInputType = GlInputType.string
    """Input type."""
    options: Optional[list[str]] = None
    """Input options (allowed values)."""
    default: Optional[Union[str, bool, int]] = None
    """Input default."""


class TbcVar(BaseModel):
    """to-be-continuous variable from the Kicker declaration."""

    name: str
    """Variable name."""
    description: Optional[str] = None
    """Variable name."""
    type: TbcVarType = TbcVarType.text
    """Variable type."""
    values: Optional[list[str]] = None
    """Variable allowed values (for an enumerated type)."""
    default: Optional[str] = None
    """Variable default value."""
    advanced: bool = False
    """Whether the variable is advanced."""
    secret: bool = False
    """Whether the variable is a secret."""
    mandatory: bool = False
    """Whether the variable is mandatory."""

    def input_name(self, var_prefix: str) -> str:
        return (
@@ -111,13 +126,21 @@ class TbcVar(BaseModel):


class DocVar(BaseModel):
    """Variable delcaration from the documentation (README)."""

    lock: bool
    """Whether the variable show a lock icon (expected for secrets)."""
    var_name: str
    """The variable name."""
    input_name: Optional[str] = None
    """The associated input name."""
    description: str
    """The textual variable description."""
    default_cell: str
    """The textual content of the default value column."""

    def default(self, type: GlInputType) -> Optional[str]:
        """Extracts and converts the variable default value from the cell content."""
        code_expr_match = re.match(r"`([^`]*)`", self.default_cell)
        explicit_default = code_expr_match.group(1) if code_expr_match else None

@@ -130,12 +153,12 @@ class DocVar(BaseModel):
        return explicit_default or ""


def _get_var(tpl_desc: dict[str, any], var_name: str) -> Optional[TbcVar]:
def _get_var(kicker: dict[str, any], var_name: str) -> Optional[TbcVar]:
    var = next(
        iter(
            [
                TbcVar.parse_obj(var)
                for var in tpl_desc.get("variables", [])
                for var in kicker.get("variables", [])
                if var["name"] == var_name
            ]
        ),
@@ -145,7 +168,7 @@ def _get_var(tpl_desc: dict[str, any], var_name: str) -> Optional[TbcVar]:
        return var

    # look into feature variables
    for feat in tpl_desc.get("features", []):
    for feat in kicker.get("features", []):
        if feat.get("enable_with") == var_name:
            return TbcVar(
                name=feat.get("enable_with"),
@@ -178,21 +201,26 @@ def _check_var(
    tbc_var: TbcVar,
    var_prefix: str,
    tpl_spec: dict[str, any],
    tpl_impl: dict[str, any],
    main_tpl_desc: Optional[dict[str, any]],
    tpl_body: dict[str, any],
    root_kicker: Optional[dict[str, any]],
    doc_vars: list[DocVar],
) -> int:
    """Check variable rules."""
    has_no_input = (
        tbc_var.secret
        or main_tpl_desc
        and _get_var(main_tpl_desc, tbc_var.name)
        or root_kicker
        and _get_var(root_kicker, tbc_var.name)
        or tbc_var.name.startswith("TBC_")
    )
    expected_input_name = tbc_var.input_name(var_prefix)
    expected_gl_input = tbc_var.to_gl()
    # check variable is declared in doc

    err_count = 0

    # check variable declaration from doc (warn only)
    # -----------------------------------
    doc_var = next(filter(lambda dv: dv.var_name == tbc_var.name, doc_vars), None)
    if doc_var == None:
    if doc_var is None:
        print(
            f"  {AnsiColors.YELLOW}⚠ <{tbc_var.name}>: not documented in README{AnsiColors.RESET}"
        )
@@ -219,52 +247,51 @@ def _check_var(
    # retrieve declared input from template specs
    declared_input = tpl_spec["spec"]["inputs"].get(expected_input_name)

    # check cases where variable must not be declared as an input
    # -----------------------------------------------------------
    if tbc_var.secret:
        # secrets should not be inputs
        if declared_input:
            print(
                f"  {AnsiColors.RED}✕ <{tbc_var.name}> is a secret: must not be declared{AnsiColors.RESET}"
            )
            return 1
        else:
            return err_count + 1
        print(
            f"  {AnsiColors.HGRAY}✕ <{tbc_var.name}> is a secret: skip{AnsiColors.RESET}"
        )
            return 0
    if main_tpl_desc and _get_var(main_tpl_desc, tbc_var.name):
        return err_count
    if root_kicker and _get_var(root_kicker, tbc_var.name):
        # a variant is overriding a variable from the main template: skip
        if declared_input:
            print(
                f"  {AnsiColors.RED}✕ <{tbc_var.name}> is an override: must not be declared{AnsiColors.RESET}"
            )
            return 1
        else:
            return err_count + 1
        print(
            f"  {AnsiColors.HGRAY}✕ <{tbc_var.name}> is an override: skip{AnsiColors.RESET}"
        )
            return 0
        return err_count
    if tbc_var.name.startswith("TBC_"):
        # global TBC variable: skip
        if declared_input:
            print(
                f"  {AnsiColors.RED}✕ <{tbc_var.name}> is global TBC: must not be declared{AnsiColors.RESET}"
            )
            return 1
        else:
            return err_count + 1
        print(
            f"  {AnsiColors.HGRAY}✕ <{tbc_var.name}> is global TBC: skip{AnsiColors.RESET}"
        )
            return 0
        return err_count

    # check if mapped GitLab input is declared
    if not declared_input:
        print(
            f"  {AnsiColors.RED}✕ <{tbc_var.name}/{expected_input_name}>: input not found{AnsiColors.RESET}"
        )
        return 1
        return err_count + 1

    err_count = 0
    # check actual input is as expected
    # check GitLab input is as expected
    # ---------------------------------
    actual_gl_input = GlInput.parse_obj(declared_input)

    if actual_gl_input.type != expected_gl_input.type:
@@ -291,7 +318,8 @@ def _check_var(
        err_count += 1

    # check variable is initialized from input
    actual_variable_value = tpl_impl["variables"].get(tbc_var.name)
    # ----------------------------------------
    actual_variable_value = tpl_body["variables"].get(tbc_var.name)
    expected_variable_value = f"$[[ inputs.{expected_input_name} ]]"
    if not actual_variable_value:
        print(
@@ -304,6 +332,7 @@ def _check_var(
        )
        err_count += 1

    # print an OK message if no error was found
    if err_count == 0:
        print(
            f"  {AnsiColors.GREEN}{AnsiColors.RESET} <{tbc_var.name}/{expected_input_name}>: OK"
@@ -313,33 +342,41 @@ def _check_var(


def _check_tpl(
    tpl_desc, main_tpl_desc, project_dir: Path, var_prefix: str, doc_vars: list[DocVar]
    kicker: dict[str, any],
    root_kicker: Optional[dict[str, any]],
    project_dir: Path,
    var_prefix: str,
    doc_vars: list[DocVar],
) -> int:
    tpl_path = project_dir / tpl_desc["template_path"]
    """Check a template (either main or variant)."""
    tpl_path = project_dir / kicker["template_path"]
    if not tpl_path.exists():
        print(f"{AnsiColors.RED}ERROR: Template file ({tpl_path}) not found: abort")
        exit(1)

    # load template
    with open(tpl_path, "r") as reader:
        all_tpl = list(yaml.load_all(reader, Loader=yaml.BaseLoader))
        tpl_spec = all_tpl[0]
        tpl_impl = all_tpl[-1]
        tpl_parts: list[dict[str, Any]] = list(
            yaml.load_all(reader, Loader=yaml.BaseLoader)
        )
        tpl_spec: dict[str, Any] = tpl_parts[0]
        tpl_body: dict[str, Any] = tpl_parts[-1]

    inputs: dict[str, dict[str, any]] = dict(tpl_spec["spec"]["inputs"])
    err_count = 0

    # check main template variables
    for var in tpl_desc.get("variables", []):
    for var in kicker.get("variables", []):
        tbc_var = TbcVar.parse_obj(var)
        err_count += _check_var(
            tbc_var, var_prefix, tpl_spec, tpl_impl, main_tpl_desc, doc_vars
            tbc_var, var_prefix, tpl_spec, tpl_body, root_kicker, doc_vars
        )
        input_name = tbc_var.input_name(var_prefix)
        if input_name in inputs:
            del inputs[input_name]

    # check feature variables
    for feat in tpl_desc.get("features", []):
    for feat in kicker.get("features", []):
        if feat.get("enable_with"):
            tbc_var = TbcVar(
                name=feat.get("enable_with"),
@@ -347,7 +384,7 @@ def _check_tpl(
                type=TbcVarType.boolean,
            )
            err_count += _check_var(
                tbc_var, var_prefix, tpl_spec, tpl_impl, main_tpl_desc, doc_vars
                tbc_var, var_prefix, tpl_spec, tpl_body, root_kicker, doc_vars
            )
            input_name = tbc_var.input_name(var_prefix)
            if input_name in inputs:
@@ -359,7 +396,7 @@ def _check_tpl(
                type=TbcVarType.boolean,
            )
            err_count += _check_var(
                tbc_var, var_prefix, tpl_spec, tpl_impl, main_tpl_desc, doc_vars
                tbc_var, var_prefix, tpl_spec, tpl_body, root_kicker, doc_vars
            )
            input_name = tbc_var.input_name(var_prefix)
            if input_name in inputs:
@@ -367,7 +404,7 @@ def _check_tpl(
        for var in feat.get("variables", []):
            tbc_var = TbcVar.parse_obj(var)
            err_count += _check_var(
                tbc_var, var_prefix, tpl_spec, tpl_impl, main_tpl_desc, doc_vars
                tbc_var, var_prefix, tpl_spec, tpl_body, root_kicker, doc_vars
            )
            input_name = tbc_var.input_name(var_prefix)
            if input_name in inputs: