Source code for project_config.reporters.toml

"""TOML reporters."""

from __future__ import annotations

import json
from typing import TYPE_CHECKING, Any

import tomli_w

from project_config.reporters.base import (
    BaseColorReporter,
    BaseNoopFormattedReporter,
)


if TYPE_CHECKING:
    from project_config.reporters.base import (
        FilesErrors,
        FormatterDefinitionType,
    )


[docs]def _replace_nulls_by_repr_strings_in_dict( data: dict[str, Any], ) -> dict[str, Any]: """Iterates through a dictionary and replaces nulls by empty strings.""" if isinstance(data, dict): return { key: _replace_nulls_by_repr_strings_in_dict(value) for key, value in data.items() } if isinstance(data, list): return [_replace_nulls_by_repr_strings_in_dict(value) for value in data] if data is None: return "!!null" return data
[docs]def _normalize_indentation_to_2_spaces(string: str) -> str: """Normalizes indentation of the beginning of lines to 2 spaces.""" new_lines: list[str] = [] for line in string.splitlines(): # count number of spaces at the beginning of the line spaces_at_start = len(line) - len(line.lstrip()) if spaces_at_start < 3: # noqa: PLR2004 new_lines.append(line) else: new_lines.append( f'{" " * int(spaces_at_start / 2)}{line.lstrip()}', ) return "\n".join(new_lines)
[docs]def _common_generate_errors_report( # noqa: PLR0913 files_errors: FilesErrors, format_metachar: FormatterDefinitionType, format_file: FormatterDefinitionType, format_key: FormatterDefinitionType, format_error_message: FormatterDefinitionType, format_definition: FormatterDefinitionType, format_hint: FormatterDefinitionType, format_fixed: FormatterDefinitionType, ) -> str: report = "" for file, errors in files_errors.items(): report += ( f"{format_metachar('[[')}{format_file(json.dumps(file))}" f"{format_metachar(']]')}\n" ) for error in errors: report += ( f"{format_key('message')} =" f" {format_error_message(json.dumps(error['message']))}\n" f"{format_key('definition')} =" f" {format_definition(json.dumps(error['definition']))}\n" ) if "hint" in error: report += ( f"{format_key('hint')} =" f" {format_hint(json.dumps(error['hint']))}\n" ) if error.get("fixable"): if error.get("fixed"): report += ( f"{format_key('fixed')} = {format_fixed('true')}\n" ) else: report += ( f"{format_key('fixable')} = {format_fixed('true')}\n" ) report += "\n" return report.rstrip("\n")
[docs]class TomlReporter(BaseNoopFormattedReporter): """Black/white reporter in TOML format."""
[docs] def generate_errors_report(self) -> str: """Generate an errors report in black/white TOML format.""" return _common_generate_errors_report( self.errors, self.format_metachar, self.format_file, self.format_key, self.format_error_message, self.format_definition, self.format_hint, self.format_fixed, )
[docs] def generate_data_report( self, data_key: str, # noqa: ARG002 data: dict[str, Any], ) -> str: """Generate a data report in black/white TOML format.""" report = "" if "plugins" in data: if not data["plugins"]: data.pop("plugins") elif len(data["plugins"]) == 1: plugin = data.pop("plugins")[0] report += f"plugins = [{json.dumps(plugin)}]\n\n" report += tomli_w.dumps( _replace_nulls_by_repr_strings_in_dict(data), ) return _normalize_indentation_to_2_spaces(report)
[docs]class TomlColorReporter(BaseColorReporter): """Color reporter in TOML format."""
[docs] def generate_errors_report(self) -> str: """Generate an errors report in TOML format with colors.""" return _common_generate_errors_report( self.errors, self.format_metachar, self.format_file, self.format_key, self.format_error_message, self.format_definition, self.format_hint, self.format_fixed, )
[docs] def generate_data_report( # noqa: PLR0912, PLR0915 self, data_key: str, data: dict[str, Any], ) -> str: """Generate data report in TOML format with colors.""" report = "" if data_key == "style": plugins = data.pop("plugins", []) if plugins: report += ( f'{self.format_config_key("plugins")}' f' {self.format_metachar("=")} ' ) if len(plugins) == 1: report += ( f'{self.format_metachar("[")}' f"{self.format_config_value(json.dumps(plugins[0]))}" f'{self.format_metachar("]")}' ) else: report += f'{self.format_metachar("[")}\n' for plugin in plugins: report += ( f" {self.format_config_value(json.dumps(plugin))}" f'{self.format_metachar(",")}\n' ) report += self.format_metachar("]") report += "\n\n" context = None for line in tomli_w.dumps( _replace_nulls_by_repr_strings_in_dict(data), ).splitlines(): if not line: report += "\n" elif line.startswith("[[") and line.endswith("]]"): report += ( f"{self.format_metachar('[[')}" f"{self.format_config_key(line[2:-2])}" f"{self.format_metachar(']]')}\n" ) elif line.startswith("[") and line.endswith("]"): points_after_rules = self.format_metachar(".").join( [ self.format_key(action) for action in line[1:-1].split(".")[1:] ], ) report += ( f"{self.format_metachar('[')}" f"{self.format_config_key('rules')}" f"{self.format_metachar('.')}" f"{points_after_rules}" f"{self.format_metachar(']')}\n" ) actions_end = line.rstrip("]") if actions_end.endswith(("files", "files.not")): context = "files" else: context = "action" elif line.startswith("files ="): report += ( f"{self.format_key('files')}" f" {self.format_metachar('=')}" f" {self.format_metachar(line[8:])}\n" ) context = "files" elif line[0].isalpha(): key, value = line.split("=", maxsplit=1) report += ( f"{self.format_key(key.strip())}" f" {self.format_metachar('=')}" f" {self.format_config_value(value.strip())}\n" ) context = "action" elif not line.startswith(" "): if len(line) == 1: # end of action like ']' formatter = ( self.format_metachar if context == "files" else self.format_config_value ) report += f"{formatter(line)}\n" elif context == "files": # files: not # file, reason = line.split("=", maxsplit=1) report += ( f"{self.format_file(file.strip())}" f" {self.format_metachar('=')}" f" {self.format_config_value(reason.strip())}\n" ) else: report += f"{self.format_config_value(line)}\n" elif context == "files": indent = len(line) - len(line.lstrip()) report += ( f"{' ' * indent}" f"{self.format_file(line[indent:-1])}" f"{self.format_metachar(line[-1])}\n" ) else: # context == "action" indent = len(line) - len(line.lstrip()) report += ( f"{' ' * indent}" f"{self.format_config_value(line[indent:])}\n" ) else: for key, value in data.items(): report += ( f"{self.format_config_key(key)}" f' {self.format_metachar("=")}' ) if isinstance(value, list): report += f' {self.format_metachar("[")}\n' for item in value: report += ( f" {self.format_config_value(json.dumps(item))}" f'{self.format_metachar(",")}\n' ) report += f'{self.format_metachar("]")}\n' else: report += ( f" {self.format_config_value(json.dumps(value))}\n" ) return _normalize_indentation_to_2_spaces(report)