|  | 
|  | 1 | +# License: MIT | 
|  | 2 | +# Copyright © 2023 Frequenz Energy-as-a-Service GmbH | 
|  | 3 | + | 
|  | 4 | +"""Cookiecutter pre-generation hooks. | 
|  | 5 | +
 | 
|  | 6 | +This module contains the pre-generation hooks for the cookiecutter template. It | 
|  | 7 | +validates the cookiecutter variables and prints an error message and exits with a | 
|  | 8 | +non-zero exit code if any of them are invalid. | 
|  | 9 | +""" | 
|  | 10 | + | 
|  | 11 | +import collections | 
|  | 12 | +import json | 
|  | 13 | +import re | 
|  | 14 | +import sys | 
|  | 15 | +from typing import Any | 
|  | 16 | + | 
|  | 17 | +NAME_REGEX = re.compile(r"^[a-zA-Z][_a-zA-Z0-9]+(-[_a-zA-Z][_a-zA-Z0-9]+)*$") | 
|  | 18 | +PYTHON_PACKAGE_REGEX = re.compile(r"^[a-zA-Z][_a-zA-Z0-9]+(\.[_a-zA-Z][_a-zA-Z0-9]+)*$") | 
|  | 19 | +PYPI_PACKAGE_REGEX = NAME_REGEX | 
|  | 20 | + | 
|  | 21 | + | 
|  | 22 | +def to_named_tuple(dictionary: dict[Any, Any], /) -> Any: | 
|  | 23 | +    """Convert a dictionary to a named tuple. | 
|  | 24 | +
 | 
|  | 25 | +    Args: | 
|  | 26 | +        dictionary: The dictionary to convert. | 
|  | 27 | +
 | 
|  | 28 | +    Returns: | 
|  | 29 | +        The named tuple with the same keys and values as the dictionary. | 
|  | 30 | +    """ | 
|  | 31 | +    filtered = {k: v for k, v in dictionary.items() if not k.startswith("_")} | 
|  | 32 | +    return collections.namedtuple("Cookiecutter", filtered.keys())(*filtered.values()) | 
|  | 33 | + | 
|  | 34 | + | 
|  | 35 | +cookiecutter = to_named_tuple(json.loads(r"""{{cookiecutter | tojson}}""")) | 
|  | 36 | + | 
|  | 37 | + | 
|  | 38 | +def main() -> None: | 
|  | 39 | +    """Validate the cookiecutter variables. | 
|  | 40 | +
 | 
|  | 41 | +    This function validates the cookiecutter variables and prints an error message and | 
|  | 42 | +    exits with a non-zero exit code if any of them are invalid. | 
|  | 43 | +    """ | 
|  | 44 | +    errors: dict[str, list[str]] = {} | 
|  | 45 | + | 
|  | 46 | +    def add_error(key: str, message: str) -> None: | 
|  | 47 | +        """Add an error to the error dictionary. | 
|  | 48 | +
 | 
|  | 49 | +        Args: | 
|  | 50 | +            key: The key of the error. | 
|  | 51 | +            message: The error message. | 
|  | 52 | +        """ | 
|  | 53 | +        errors.setdefault(key, []).append(message) | 
|  | 54 | + | 
|  | 55 | +    if not NAME_REGEX.match(cookiecutter.name): | 
|  | 56 | +        add_error("name", f"Invalid project name (must match {NAME_REGEX.pattern})") | 
|  | 57 | + | 
|  | 58 | +    if not PYTHON_PACKAGE_REGEX.match(cookiecutter.python_package): | 
|  | 59 | +        add_error( | 
|  | 60 | +            "python_package", | 
|  | 61 | +            f"Invalid package name (must match {PYTHON_PACKAGE_REGEX.pattern})", | 
|  | 62 | +        ) | 
|  | 63 | + | 
|  | 64 | +    if not PYPI_PACKAGE_REGEX.match(cookiecutter.pypi_package_name): | 
|  | 65 | +        add_error( | 
|  | 66 | +            "pypi_package_name", | 
|  | 67 | +            f"Invalid package name (must match {PYPI_PACKAGE_REGEX.pattern})", | 
|  | 68 | +        ) | 
|  | 69 | + | 
|  | 70 | +    if errors: | 
|  | 71 | +        print("The following errors were found:", file=sys.stderr) | 
|  | 72 | +        for key, messages in errors.items(): | 
|  | 73 | +            print(f"  {key}:", file=sys.stderr) | 
|  | 74 | +            for message in messages: | 
|  | 75 | +                print(f"    - {message}", file=sys.stderr) | 
|  | 76 | +        sys.exit(1) | 
|  | 77 | + | 
|  | 78 | + | 
|  | 79 | +if __name__ == "__main__": | 
|  | 80 | +    main() | 
0 commit comments