Source code for wemake_python_styleguide.violations.base

"""
Contains detailed technical information about :term:`violation` internals.

.. _violations:

Violations API
--------------

.. currentmodule:: wemake_python_styleguide.violations.base

.. autoclasstree:: wemake_python_styleguide.violations.base

.. autosummary::
   :nosignatures:

   ASTViolation
   MaybeASTViolation
   TokenizeViolation
   SimpleViolation

Violation cannot have more than one base class.
See :ref:`tutorial` for more information about choosing a correct base class.

Conventions
~~~~~~~~~~~

- Each violation class name should end with "Violation"
- Each violation must have a long docstring with full description
- Each violation must have "Reasoning" and "Solution" sections
- Each violation must have "versionadded" policy
- Each violation should have an example with correct and wrong usages
- If violation error template should have a parameter
  it should be the last part of the text: ``: {0}``

Deprecating a violation
~~~~~~~~~~~~~~~~~~~~~~~

When you want to mark some violation as deprecated,
then assign ``deprecated`` boolean flag to it:

.. code:: python

  @final
  class SomeViolation(ASTViolation):
      deprecated = True

Reference
~~~~~~~~~

"""

import abc
import ast
import enum
import tokenize
from typing import Callable, ClassVar, Optional, Set, Tuple, Union

from typing_extensions import TypeAlias, final

#: General type for all possible nodes where error happens.
ErrorNode: TypeAlias = Union[
    ast.AST,
    tokenize.TokenInfo,
    None,
]

#: We use this type to define helper classes with callbacks to add violations.
ErrorCallback: TypeAlias = Callable[['BaseViolation'], None]


[docs] @enum.unique class ViolationPostfixes(enum.Enum): """String values of postfixes used for violation baselines.""" bigger_than = ' > {0}' less_than = ' < {0}'
# TODO: remove `noqa` after a new release (0.17.0):
[docs] class BaseViolation(metaclass=abc.ABCMeta): # noqa: WPS338 """ Abstract base class for all style violations. It basically just defines how to create any error and how to format this error later on. Each subclass must define ``error_template`` and ``code`` fields. Attributes: error_template: message that will be shown to user after formatting. code: unique violation number. Used to identify the violation. previous_codes: just a documentation thing to track changes in time. deprecated: indicates that this violation will be removed soon. postfix_template: indicates message that we show at the very end. """ error_template: ClassVar[str] code: ClassVar[int] previous_codes: ClassVar[Set[int]] deprecated: ClassVar[bool] = False # assigned in __init_subclass__ full_code: ClassVar[str] summary: ClassVar[str] # We use this code to show base metrics and thresholds mostly: postfix_template: ClassVar[ViolationPostfixes] = ( ViolationPostfixes.bigger_than ) def __init_subclass__(cls, **kwargs) -> None: """Sets additional values for subclasses.""" super().__init_subclass__(**kwargs) violation_code = getattr(cls, 'code', None) if violation_code is None: return if cls.__doc__ is None: raise TypeError( 'Please include a docstring documenting {0}'.format(cls), ) # this is mostly done for docs to display the full code, # allowing its indexing in search engines and better discoverability cls.full_code = cls._full_code() cls.summary = cls.__doc__.lstrip().split('\n', maxsplit=1)[0] # this hack adds full code to summary table in the docs cls.__doc__ = _prepend_skipping_whitespaces( '{0} — '.format(cls.full_code), cls.__doc__, ) def __init__( self, node: ErrorNode, text: Optional[str] = None, baseline: Optional[int] = None, ) -> None: """ Creates a new instance of an abstract violation. Arguments: node: violation was raised by this node. If applicable. text: extra text to format the final message. If applicable. baseline: some complexity violations show the logic threshold here. """ self._node = node self._text = text self._baseline = baseline
[docs] @final def message(self) -> str: """ Returns error's formatted message with code and reason. Conditionally formats the ``error_template`` if it is required. """ formatted = self.error_template.format(self._text) if self._text and formatted == self.error_template: # pragma: no cover raise ValueError('Error message was not formatted', self) return '{0} {1}{2}'.format( self.full_code, formatted, self._postfix_information(), )
[docs] @final def node_items(self) -> Tuple[int, int, str]: """Returns tuple to match ``flake8`` API format.""" return (*self._location(), self.message())
@final @classmethod def _full_code(cls) -> str: """ Returns fully formatted code. Adds violation letter to the numbers. Also ensures that codes like ``3`` will be represented as ``WPS003``. """ return 'WPS{0}'.format(str(cls.code).zfill(3)) @final def _postfix_information(self) -> str: """ Adds useful information to the end of the violation message. Useful for complexity baselines and other thresholds. """ if self._baseline is None: return '' return self.postfix_template.value.format(self._baseline) @abc.abstractmethod def _location(self) -> Tuple[int, int]: """Base method for showing error location."""
class _BaseASTViolation(BaseViolation): """Used as a based type for all ``ast`` violations.""" _node: Optional[ast.AST] @final def _location(self) -> Tuple[int, int]: line_number = getattr(self._node, 'lineno', 0) column_offset = getattr(self._node, 'col_offset', 0) return line_number, column_offset
[docs] class ASTViolation(_BaseASTViolation): """Violation for ``ast`` based style visitors.""" _node: ast.AST
[docs] class MaybeASTViolation(_BaseASTViolation): """ Violation for ``ast`` and modules visitors. Is used for violations that share the same rule for nodes and module names. Is wildly used for naming rules. """ def __init__( self, node: Optional[ast.AST] = None, text: Optional[str] = None, baseline: Optional[int] = None, ) -> None: """Creates new instance of module violation without explicit node.""" super().__init__(node, text=text, baseline=baseline)
[docs] class TokenizeViolation(BaseViolation): """Violation for ``tokenize`` based visitors.""" _node: tokenize.TokenInfo @final def _location(self) -> Tuple[int, int]: return self._node.start
[docs] class SimpleViolation(BaseViolation): """Violation for cases where there's no associated nodes.""" _node: None def __init__( self, node=None, text: Optional[str] = None, baseline: Optional[int] = None, ) -> None: """Creates new instance of simple style violation.""" super().__init__(node, text=text, baseline=baseline) @final def _location(self) -> Tuple[int, int]: """ Return violation location inside the file. Default location is in the so-called "file beginning". Cannot be ignored by inline ``noqa`` comments. """ return 0, 0
def _prepend_skipping_whitespaces(prefix: str, text: str) -> str: lstripped_text = text.lstrip() leading_whitespaces = text[:len(text) - len(lstripped_text)] return leading_whitespaces + prefix + lstripped_text