Source code for wemake_python_styleguide.visitors.base

"""
Contains detailed technical documentation about how to write a :term:`visitor`.

See also:
    Visitor is a well-known software engineering pattern:
    https://en.wikipedia.org/wiki/Visitor_pattern

Each visitor might work with one or many :term:`violations <violation>`.
Multiple visitors might work with the same violation.

.. mermaid::
   :caption: Visitor relation with violations.

    graph TD
        V1[Visitor 1] --> EA[Violation A]
        V1[Visitor 1] --> EB[Violation B]

        V2[Visitor 2] --> EA[Violation A]
        V2[Visitor 2] --> EC[Violation C]

        V3[Visitor 3] --> EZ[Violation WPS]

.. _visitors:

Visitors API
------------

.. currentmodule:: wemake_python_styleguide.visitors.base

.. autoclasstree:: wemake_python_styleguide.visitors.base

.. autosummary::
   :nosignatures:

   BaseNodeVisitor
   BaseFilenameVisitor
   BaseTokenVisitor

The decision relies on what parameters you need for the task.
It is highly unlikely that you will need two parameters at the same time.
See :ref:`tutorial` for more information about choosing a correct base class.

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

Then you will have to write logic for your visitor.
We follow these conventions:

- Public visitor methods start with ``visit_``,
  then comes the name of a token or node to be visited
- All other methods and attributes should be protected
- We try to separate as much logic from ``visit_`` methods as possible,
  so they only route for callbacks that actually execute the checks
- We place repeating logic into ``logic/`` package to be able to reuse it

There are different examples of visitors in this project already.

Reference
~~~~~~~~~

"""

import abc
import ast
import tokenize
from typing import List, Sequence, Type

from typing_extensions import final

from wemake_python_styleguide import constants
from wemake_python_styleguide.compat.routing import route_visit
from wemake_python_styleguide.logic.filenames import get_stem
from wemake_python_styleguide.types import ConfigurationOptions
from wemake_python_styleguide.violations.base import BaseViolation


[docs] class BaseVisitor(metaclass=abc.ABCMeta): """ Abstract base class for different types of visitors. Attributes: options: contains the options objects passed and parsed by ``flake8``. filename: filename passed by ``flake8``, each visitor has a file name. violations: list of :term:`violations <violation>` for the specific visitor. """ def __init__( self, options: ConfigurationOptions, filename: str = constants.STDIN, ) -> None: """Creates base visitor instance.""" self.options = options self.filename = filename self.violations: List[BaseViolation] = []
[docs] @classmethod def from_checker( cls: Type['BaseVisitor'], checker, ) -> 'BaseVisitor': """ Constructs visitor instance from the checker. Each unique visitor class should know how to construct itself from the :term:`checker` instance. Generally speaking, each visitor class needs to eject required parameters from checker and then run its constructor with these parameters. """ return cls(options=checker.options, filename=checker.filename)
[docs] @final def add_violation(self, violation: BaseViolation) -> None: """Adds violation to the visitor.""" self.violations.append(violation)
[docs] @abc.abstractmethod def run(self) -> None: """ Runs a visitor. Each visitor should know what exactly it needs to do when it was told to ``run``. """
def _post_visit(self) -> None: # noqa: B027 """ Executed after all nodes have been visited. This method is useful for counting statistics, etc. By default does nothing. """
[docs] class BaseNodeVisitor(ast.NodeVisitor, BaseVisitor): """ Allows to store violations while traversing node tree. This class should be used as a base class for all ``ast`` based checkers. Method ``visit()`` is defined in ``NodeVisitor`` class. Attributes: tree: ``ast`` tree to be checked. """ def __init__( self, options: ConfigurationOptions, tree: ast.AST, **kwargs, ) -> None: """Creates new ``ast`` based instance.""" super().__init__(options, **kwargs) self.tree = tree
[docs] @final @classmethod def from_checker( cls: Type['BaseNodeVisitor'], checker, ) -> 'BaseNodeVisitor': """Constructs visitor instance from the checker.""" return cls( options=checker.options, filename=checker.filename, tree=checker.tree, )
[docs] def visit(self, tree: ast.AST) -> None: """ Visits a node. Modified version of :class:`ast.NodeVisitor.visit` method. We need this to modify how visitors route. Why? Because python3.8 now uses ``visit_Constant`` instead of old methods like ``visit_Num``, ``visit_Str``, ``visit_Bytes``, etc. Some classes do redefine this method to catch all nodes. This is valid. """ return route_visit(self, tree)
[docs] @final def run(self) -> None: """Recursively visits all ``ast`` nodes. Then executes post hook.""" self.visit(self.tree) self._post_visit()
[docs] class BaseFilenameVisitor(BaseVisitor, metaclass=abc.ABCMeta): """ Abstract base class that allows to visit and check module file names. Has ``visit_filename()`` method that should be defined in subclasses. Attributes: stem: the last part of the filename. Does not contain extension. """ stem: str
[docs] @abc.abstractmethod def visit_filename(self) -> None: """Checks module file names."""
[docs] @final def run(self) -> None: """ Checks module's filename. Skips modules that are checked as piped output. Since these modules are checked as a ``stdin`` input. And do not have names. """ if self.filename != constants.STDIN: self.stem = get_stem(self.filename) self.visit_filename() self._post_visit()
[docs] class BaseTokenVisitor(BaseVisitor): """ Allows to check ``tokenize`` sequences. Attributes: file_tokens: ``tokenize.TokenInfo`` sequence to be checked. """ def __init__( self, options: ConfigurationOptions, file_tokens: Sequence[tokenize.TokenInfo], **kwargs, ) -> None: """Creates new ``tokenize`` based visitor instance.""" super().__init__(options, **kwargs) self.file_tokens = file_tokens self.token_index = -1
[docs] @final @classmethod def from_checker( cls: Type['BaseTokenVisitor'], checker, ) -> 'BaseTokenVisitor': """Constructs ``tokenize`` based visitor instance from the checker.""" return cls( options=checker.options, filename=checker.filename, file_tokens=checker.file_tokens, )
[docs] def visit(self, token: tokenize.TokenInfo) -> None: """ Runs custom defined handlers in a visitor for each specific token type. Uses ``.exact_type`` property to fetch the token name. So, you have to be extra careful with tokens like ``->`` and other operators, since they might resolve in just ``OP`` name. Does nothing if handler for any token type is not defined. Inspired by ``NodeVisitor`` class. See also: https://docs.python.org/3/library/tokenize.html """ token_type = tokenize.tok_name[token.exact_type].lower() method = getattr(self, 'visit_{0}'.format(token_type), None) if method is not None: method(token)
[docs] @final def run(self) -> None: """Visits all token types that have a handler method.""" for token in self.file_tokens: self.visit(token) self._post_visit()