Source code for nitpick.violations

"""Violation codes.

Name inspired by `flake8's violations <https://flake8.pycqa.org/en/latest/user/error-codes.html>`_.
"""

from __future__ import annotations

from dataclasses import dataclass
from enum import Enum
from typing import TYPE_CHECKING

import click

from nitpick.constants import CONFIG_RUN_NITPICK_INIT_OR_CONFIGURE_STYLE_MANUALLY, FLAKE8_PREFIX, EmojiEnum

if TYPE_CHECKING:
    from nitpick.plugins.info import FileInfo


[docs]@dataclass(frozen=True) class Fuss: """Nitpick makes a fuss when configuration doesn't match. Fields inspired on :py:class:`SyntaxError` and `pyflakes.messages.Message <https://github.com/PyCQA/pyflakes/blob/master/pyflakes/messages.py#L6>`_. """ fixed: bool filename: str code: int message: str suggestion: str = "" lineno: int = 1 @property def colored_suggestion(self) -> str: """Suggestion with color.""" return click.style(f"\n{self.suggestion.rstrip()}", fg="green") if self.suggestion else "" @property def pretty(self) -> str: """Message to be used on the CLI.""" filename_plus_line = f"{self.filename}:{self.lineno}: " if self.filename.strip() else "" return f"{filename_plus_line}{FLAKE8_PREFIX}{self.code:03} {self.message.rstrip()}{self.colored_suggestion}" def __lt__(self, other: Fuss) -> bool: """Sort Fuss instances.""" return (self.filename, self.code, self.message, self.suggestion, self.lineno) < ( other.filename, other.code, other.message, other.suggestion, other.lineno, )
[docs]class ViolationEnum(Enum): """Base enum with violation codes and messages.""" def __init__(self, code: int, message: str = "", add_to_base=False) -> None: self.code = code self.message = message self.add_code = add_to_base
[docs]class StyleViolations(ViolationEnum): """Style violations.""" INVALID_DATA_TOOL_NITPICK = (1, " has an incorrect style. Invalid data in [{section}]:") INVALID_TOML = (1, " has an incorrect style. Invalid TOML{exception}") INVALID_CONFIG = (1, " has an incorrect style. Invalid config:") NO_STYLE_CONFIGURED = (4, f"No style file configured.{CONFIG_RUN_NITPICK_INIT_OR_CONFIGURE_STYLE_MANUALLY}")
[docs]class ProjectViolations(ViolationEnum): """Project initialization violations.""" NO_ROOT_DIR = ( 101, f"No root directory detected.{CONFIG_RUN_NITPICK_INIT_OR_CONFIGURE_STYLE_MANUALLY}", ) NO_PYTHON_FILE = (102, "No Python file was found on the root dir and subdir of {root!r}") MISSING_FILE = (103, " should exist{extra}") FILE_SHOULD_BE_DELETED = (104, " should be deleted{extra}") MINIMUM_VERSION = ( 203, "The style file you're using requires {project}>={expected} (you have {actual}). Please upgrade", )
[docs]class SharedViolations(ViolationEnum): """Shared violations used by all plugins.""" CREATE_FILE = (1, " was not found", True) CREATE_FILE_WITH_SUGGESTION = (1, " was not found. Create it with this content:", True) DELETE_FILE = (2, " should be deleted", True) MISSING_VALUES = (8, "{prefix} has missing values:", True) DIFFERENT_VALUES = (9, "{prefix} has different values. Use this:", True)
# TODO: refactor: the Reporter class should have a metaclass to track a global list of codes on __new__(), # to be used by the `nitpick violations` CLI command
[docs]class Reporter: # pylint: disable=too-few-public-methods """Error reporter.""" manual: int = 0 fixed: int = 0 def __init__(self, info: FileInfo | None = None, violation_base_code: int = 0) -> None: self.info: FileInfo | None = info self.violation_base_code = violation_base_code
[docs] def make_fuss(self, violation: ViolationEnum, suggestion: str = "", fixed=False, **kwargs) -> Fuss: """Make a fuss.""" formatted = violation.message.format(**kwargs) if kwargs else violation.message base = self.violation_base_code if violation.add_code else 0 Reporter.increment(fixed) # Remove right whitespace from suggestion (new lines, spaces, etc.) return Fuss( fixed, self.info.path_from_root if self.info else "", base + violation.code, formatted, suggestion.rstrip() )
[docs] @classmethod def reset(cls): """Reset the counters.""" cls.manual = cls.fixed = 0
[docs] @classmethod def increment(cls, fixed=False): """Increment the fixed ou manual count.""" if fixed: cls.fixed += 1 else: cls.manual += 1
[docs] @classmethod def get_counts(cls) -> str: """String representation with error counts and emojis.""" parts = [] if cls.fixed: parts.append(f"{EmojiEnum.GREEN_CHECK.value} {cls.fixed} fixed") if cls.manual: parts.append(f"{EmojiEnum.X_RED_CROSS.value} {cls.manual} to fix manually") if not parts: return f"No violations found. {EmojiEnum.STAR_CAKE.value}" return f"Violations: {', '.join(parts)}."