Source code for project_config.reporters.base
"""Base reporters."""
from __future__ import annotations
import abc
import os
from collections.abc import Callable
from typing import TYPE_CHECKING, Any
import colored
from project_config.exceptions import (
    ProjectConfigCheckFailed,
    ProjectConfigException,
)
if TYPE_CHECKING:
    from project_config.compat import TypeAlias
    from project_config.types_ import ErrorDict
    FilesErrors: TypeAlias = dict[str, list[ErrorDict]]
    FormatterDefinitionType: TypeAlias = Callable[[str], str]
[docs]class InvalidColors(ProjectConfigException):
    """Invalid not supported colors in colored formatter."""
    def __init__(self, errors: list[str]):  # noqa: D107
        message = (
            "Invalid colors or subjects in 'colors'"
            " configuration for reporters:\n"
        )
        for error in errors:
            message += f"  - {error}\n"
        super().__init__(message)
[docs]class BaseReporter(abc.ABC):
    """Base reporter from which all reporters inherit."""
    __slots__ = {
        "rootdir",
        "errors",
        "format",
        "only_hints",
        "data",
    }
    exception_class = ProjectConfigCheckFailed
    def __init__(  # noqa: D107
        self,
        rootdir: str,
        fmt: str | None = None,
        only_hints: bool = False,  # noqa: FBT001, FBT002
    ):
        self.rootdir = rootdir
        self.errors: FilesErrors = {}
        self.format = fmt
        self.only_hints = only_hints
        # configuration, styles...
        self.data: dict[str, Any] = {}
[docs]    @abc.abstractmethod
    def generate_errors_report(self) -> str:
        """Generate check errors report.
        This method must be implemented by inherited reporters.
        """
[docs]    def generate_data_report(
        self,
        _data_key: str,
        _data: dict[str, Any],
    ) -> str:
        """Generate data report for configuration or styles.
        This method should be implemented by inherited reporters.
        Args:
            data_key (str): Configuration for which the data will
                be generated. Could be either ``"config"`` or
                ``"style"``.
            data (dict): Data to report.
        """
        raise NotImplementedError
    @property
    def success(self) -> bool:
        """Return if the reporter has not reported errors.
        Returns:
            bool: ``True`` if no errors reported, ``False`` otherwise.
        """
        return len(self.errors) == 0
[docs]    def raise_errors(self, errors_report: str | None = None) -> None:
        """Raise errors failure if no success.
        Raise the correspondent exception class for the reporter
        if the reporter has reported any error.
        """
        if not self.success:
            raise self.exception_class(
                (
                    self.generate_errors_report()
                    if errors_report is None
                    else errors_report
                ),
            )
[docs]    def report_error(self, error: ErrorDict) -> None:
        """Report an error.
        Args:
            error (dict): Error to report.
        """
        if "file" in error:
            file = error.pop("file")
            file = os.path.relpath(file, self.rootdir) + (
                "/" if file.endswith("/") else ""
            )
        else:
            file = "[CONFIGURATION]"  # pragma: no cover
        if file not in self.errors:
            self.errors[file] = []
        if "hint" in error and self.only_hints:
            error["message"] = error.pop("hint")
        self.errors[file].append(error)
[docs]class BaseFormattedReporter(BaseReporter, abc.ABC):
    """Reporter that requires formatted fields."""
[docs]    @abc.abstractmethod
    def format_fixed(self, output: str) -> str:
        """File name formatter."""
        raise NotImplementedError
[docs]    @abc.abstractmethod
    def format_file(self, fname: str) -> str:
        """File name formatter."""
        raise NotImplementedError
[docs]    @abc.abstractmethod
    def format_error_message(
        self,
        error_message: str,
    ) -> str:
        """Error message formatter."""
        raise NotImplementedError
[docs]    @abc.abstractmethod
    def format_definition(self, definition: str) -> str:
        """Definition formatter."""
        raise NotImplementedError
[docs]    @abc.abstractmethod
    def format_hint(self, hint: str) -> str:
        """Hint formatter."""
        raise NotImplementedError
[docs]    @abc.abstractmethod
    def format_key(self, key: str) -> str:
        """Serialized key formatter."""
        raise NotImplementedError
[docs]    @abc.abstractmethod
    def format_metachar(self, metachar: str) -> str:
        """Meta characters string formatter."""
        raise NotImplementedError
[docs]    @abc.abstractmethod
    def format_config_key(self, config_key: str) -> str:
        """Configuration data key formatter, for example 'style'."""
        raise NotImplementedError
[docs]    @abc.abstractmethod
    def format_config_value(self, config_value: str) -> str:
        """Configuration data value formatter, for example 'style' urls."""
        raise NotImplementedError
[docs]def self_format_noop(_self: type, v: str) -> str:
    """Formatter that returns the value without formatting."""
    return v
[docs]class BaseNoopFormattedReporter(BaseFormattedReporter):
    """Reporter that requires formatted fields without format."""
[docs]def bold_color(value: str, color: str) -> str:
    """Colorize a string with bold formatting using `colored`_ library.
    .. _colored: https://gitlab.com/dslackw/colored
    Args:
        value (str): Value to colorize.
        color (str): Color to use for the formatting.
    Returns:
        str: Colorized string.
    """
    return colored.stylize(  # type: ignore
        value,
        colored.fg(color) + colored.attr("bold"),
    )
[docs]def colored_color_exists(color: str) -> bool:
    """Check if a color exists in the `colored`_ library.
    .. _colored: https://gitlab.com/dslackw/colored
    Args:
        color (str): Color to check.
    Returns:
        bool: ``True`` if the color exists, ``False`` otherwise.
    """
    try:
        colored.fg(color)
    except Exception:
        return False
    else:
        return True
[docs]class BaseColorReporter(BaseFormattedReporter):
    """Base reporter with colorized output."""
    def __init__(  # noqa: D107
        self,
        *args: Any,
        colors: dict[str, str] | None = None,
        **kwargs: Any,
    ) -> None:
        self.colors = self._normalize_colors(colors or {})
        super().__init__(*args, **kwargs)
[docs]    def _normalize_colors(self, colors: dict[str, str]) -> dict[str, str]:
        normalized_colors: dict[str, str] = {}
        errors: list[str] = []
        for subject, color in colors.items():
            normalized_subject = (
                subject.lower().replace("-", "_").replace(" ", "_")
            )
            if not hasattr(self, f"format_{normalized_subject}"):
                errors.append(
                    f"Invalid subject '{normalized_subject}' to colorize",
                )
            if not colored_color_exists(color):  # pragma: no cover
                errors.append(f"Color '{color}' not supported")
            normalized_colors[normalized_subject] = color
        if errors:
            raise InvalidColors(errors)
        return normalized_colors
[docs]    def format_fixed(self, output: str) -> str:  # noqa: D102
        return bold_color(output, self.colors.get("fixed", "green"))
[docs]    def format_file(self, fname: str) -> str:  # noqa: D102
        return bold_color(fname, self.colors.get("file", "light_red"))
[docs]    def format_error_message(self, error_message: str) -> str:  # noqa: D102
        return bold_color(
            error_message,
            self.colors.get("error_message", "yellow"),
        )
[docs]    def format_definition(self, definition: str) -> str:  # noqa: D102
        return bold_color(definition, self.colors.get("definition", "blue"))
[docs]    def format_hint(self, hint: str) -> str:  # noqa: D102
        return bold_color(hint, self.colors.get("hint", "green"))
[docs]    def format_key(self, key: str) -> str:  # noqa: D102
        return bold_color(key, self.colors.get("key", "cyan"))
[docs]    def format_metachar(self, metachar: str) -> str:  # noqa: D102
        return bold_color(metachar, self.colors.get("metachar", "grey_37"))
[docs]    def format_config_key(self, config_key: str) -> str:  # noqa: D102
        return bold_color(config_key, self.colors.get("config_key", "blue"))
[docs]    def format_config_value(self, config_value: str) -> str:  # noqa: D102
        return bold_color(
            config_value,
            self.colors.get("config_value", "yellow"),
        )