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 format_fixed(self, output: str) -> str: # noqa: D102 return output
[docs] def format_file(self, fname: str) -> str: # noqa: D102 return fname
[docs] def format_error_message(self, error_message: str) -> str: # noqa: D102 return error_message
[docs] def format_definition(self, definition: str) -> str: # noqa: D102 return definition
[docs] def format_hint(self, hint: str) -> str: # noqa: D102 return hint
[docs] def format_key(self, key: str) -> str: # noqa: D102 return key
[docs] def format_metachar(self, metachar: str) -> str: # noqa: D102 return metachar
[docs] def format_config_key(self, config_key: str) -> str: # noqa: D102 return config_key
[docs] def format_config_value(self, config_value: str) -> str: # noqa: D102 return config_value
[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"), )