Source code for pyglint

"""Concise checker definition for Pylint."""

import collections
import hashlib
import typing as t

import astroid
import attr
import pylint.checkers.utils
import pylint.interfaces
from public import public


@attr.s(auto_attribs=True, frozen=True, order=False)
class ProblemID:
    prefix: str
    base: int
    number: int

    def __str__(self):
        return f"{self.prefix}{self.base:02d}{self.number:02d}"


class FormattingString(collections.UserString):
    def __mod__(self, other):

        try:
            return str(self) % other
        except TypeError:
            return NotImplemented


class FormattableDict:
    def __init__(self, *args, **kwargs):
        self.data = dict(*args, **kwargs)

    def __rmod__(self, string: str):
        return string.format_map(self.data)


[docs]@attr.s(auto_attribs=True, frozen=True, order=False) class Problem: """A problem found by a checker. Args: name: The name of the problem. Usually 2-4 words, hyphenated. text: The message text for display to the user. :func:`str.format` syntax is supported. Usually one short sentence. explanation: Prose description of the problem. Usually a few sentences. """ name: str text: FormattingString = attr.ib(converter=FormattingString) explanation: str id: ProblemID
ProblemT = t.TypeVar("ProblemT", bound=Problem) @attr.s(auto_attribs=True, frozen=True, order=False) class Message(t.Generic[ProblemT]): problem: ProblemT node: astroid.node_classes.NodeNG line: t.Optional[int] = None col_offset: t.Optional[int] = None confidence: pylint.interfaces.Confidence = pylint.interfaces.UNDEFINED data: t.Dict[str, t.Any] = attr.ib(factory=dict) def to_pylint(self): return str(self.problem.id) + " " + self.node CheckerFunction = t.Callable[ [pylint.checkers.BaseChecker, astroid.node_classes.NodeNG], t.Iterable[Message[ProblemT]], ] @attr.s(auto_attribs=True, frozen=True, order=False) class Checker: node_type: t.Type[astroid.node_classes.NodeNG] function: CheckerFunction problems: t.Iterable[Problem] def make_checker(node_type: t.Type[astroid.node_classes.NodeNG],) -> t.Callable: def wrapper(function: CheckerFunction) -> Checker: return Checker(node_type, function, ()) return wrapper def short_hash(name: str, length: int = 2) -> int: integer = int.from_bytes(hashlib.md5(name.encode()).digest(), "big") return int(str(integer)[:length])
[docs]@public @attr.s(auto_attribs=True, order=False) class CheckerGroup: """The main object for defining linters with Pyglint.""" name: str checkers: t.List[Checker] = attr.ib(factory=list) problems: t.Dict[str, Problem] = attr.ib(factory=dict) id_prefix: str = "E"
[docs] def problem(self, name: str, text: str, explanation: str): """Define a reusable :class:`Problem`.""" problem_id = ProblemID(self.id_prefix, short_hash(self.name), short_hash(name)) problem = Problem(name, text, explanation, problem_id) # pylint: disable=unsupported-assignment-operation self.problems[problem.name] = problem return problem
[docs] def check(self, node_type: t.Type[astroid.node_classes.NodeNG]): """Check for one or more pre-defined :class:`Problem` s. Args: node_type: The checker will be invoked with each instance of the given node type that pylint finds. """ def wrapper(function): checker = make_checker(node_type)(function) self.checkers.append(checker) return checker.function return wrapper
def _make_multicaller(checkers: t.Iterable[Checker]): def _call_each( self: pylint.checkers.BaseChecker, node: astroid.node_classes.NodeNG ) -> None: # pylint: disable=fixme # XXX Ideally multicaller wouldn't be necessary, each checker could separately # register its own message types. for checker in checkers: for msg in checker.function(self, node): self.add_message( msg.problem.name, node=node, line=msg.line, col_offset=msg.col_offset, confidence=msg.confidence, args=FormattableDict(msg.data), ) return _call_each def _make_visitors(group): node_type_checkers = {} for checker in group.checkers: node_type_checkers.setdefault(checker.node_type, set()).add(checker) visitors = {} for node_type, checkers in node_type_checkers.items(): # pylint: disable=fixme # XXX This is a hack. Visiting should use a better dispatch system than just type # name. visitor_method_name = "visit_" + node_type.__name__.split(".")[0].lower() visitors[visitor_method_name] = _make_multicaller(checkers) return visitors
[docs]@public def make_pylint_checker(group: CheckerGroup) -> pylint.checkers.BaseChecker: data: t.Dict[str, t.Any] = {} data["__implements__"] = (pylint.interfaces.IAstroidChecker,) data["name"] = group.name data["msgs"] = {} for problem in group.problems.values(): data["msgs"][str(problem.id)] = ( problem.text, problem.name, problem.explanation, ) data.update(_make_visitors(group)) return type(group.name, (pylint.checkers.BaseChecker,), data)
[docs]@public def message( node: astroid.node_classes.NodeNG, problem: Problem = None, line: t.Optional[int] = None, col_offset: t.Optional[int] = None, confidence: pylint.interfaces.Confidence = pylint.interfaces.UNDEFINED, **data, ) -> Message: return Message( problem, node, line=line, col_offset=col_offset, confidence=confidence, data=data, )
__version__ = "0.1.3"