initial
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
# Copyright (c) 2008, 2012 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
|
||||
# Copyright (c) 2014, 2016-2020 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2014 Arun Persaud <arun@nubati.net>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
|
||||
# Copyright (c) 2018 Nick Drozd <nicholasdrozd@gmail.com>
|
||||
# Copyright (c) 2020 Pierre Sassoulas <pierre.sassoulas@gmail.com>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
import sys
|
||||
|
||||
from pylint.__pkginfo__ import version as __version__
|
||||
|
||||
# pylint: disable=import-outside-toplevel
|
||||
|
||||
|
||||
def run_pylint():
|
||||
from pylint.lint import Run as PylintRun
|
||||
|
||||
try:
|
||||
PylintRun(sys.argv[1:])
|
||||
except KeyboardInterrupt:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def run_epylint():
|
||||
from pylint.epylint import Run as EpylintRun
|
||||
|
||||
EpylintRun()
|
||||
|
||||
|
||||
def run_pyreverse():
|
||||
"""run pyreverse"""
|
||||
from pylint.pyreverse.main import Run as PyreverseRun
|
||||
|
||||
PyreverseRun(sys.argv[1:])
|
||||
|
||||
|
||||
def run_symilar():
|
||||
"""run symilar"""
|
||||
from pylint.checkers.similar import Run as SimilarRun
|
||||
|
||||
SimilarRun(sys.argv[1:])
|
||||
@@ -0,0 +1,18 @@
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
#!/usr/bin/env python
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pylint
|
||||
|
||||
# Strip out the current working directory from sys.path.
|
||||
# Having the working directory in `sys.path` means that `pylint` might
|
||||
# inadvertently import user code from modules having the same name as
|
||||
# stdlib or pylint's own modules.
|
||||
# CPython issue: https://bugs.python.org/issue33053
|
||||
if sys.path[0] == "" or sys.path[0] == os.getcwd():
|
||||
sys.path.pop(0)
|
||||
|
||||
pylint.run_pylint()
|
||||
@@ -0,0 +1,97 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2006-2015 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
|
||||
# Copyright (c) 2010 Julien Jehannet <julien.jehannet@logilab.fr>
|
||||
# Copyright (c) 2013-2014 Google, Inc.
|
||||
# Copyright (c) 2014-2020 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2014 Brett Cannon <brett@python.org>
|
||||
# Copyright (c) 2014 Ricardo Gemignani <ricardo.gemignani@gmail.com>
|
||||
# Copyright (c) 2014 Arun Persaud <arun@nubati.net>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
|
||||
# Copyright (c) 2016 Moises Lopez <moylop260@vauxoo.com>
|
||||
# Copyright (c) 2016 Florian Bruhin <git@the-compiler.org>
|
||||
# Copyright (c) 2016 Jakub Wilk <jwilk@jwilk.net>
|
||||
# Copyright (c) 2017-2018 Hugo <hugovk@users.noreply.github.com>
|
||||
# Copyright (c) 2018-2019 Ashley Whetter <ashley@awhetter.co.uk>
|
||||
# Copyright (c) 2018 ssolanki <sushobhitsolanki@gmail.com>
|
||||
# Copyright (c) 2018 Sushobhit <31987769+sushobhit27@users.noreply.github.com>
|
||||
# Copyright (c) 2019 Ville Skyttä <ville.skytta@iki.fi>
|
||||
# Copyright (c) 2019 Hugo van Kemenade <hugovk@users.noreply.github.com>
|
||||
# Copyright (c) 2019 Dan Hemberger <846186+hemberger@users.noreply.github.com>
|
||||
# Copyright (c) 2019 jab <jab@users.noreply.github.com>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
# pylint: disable=redefined-builtin,invalid-name
|
||||
"""pylint packaging information"""
|
||||
|
||||
from os.path import join
|
||||
|
||||
# For an official release, use dev_version = None
|
||||
numversion = (2, 5, 3)
|
||||
dev_version = None
|
||||
|
||||
version = ".".join(str(num) for num in numversion)
|
||||
if dev_version is not None:
|
||||
version += "-dev" + str(dev_version)
|
||||
|
||||
install_requires = [
|
||||
"astroid>=2.4.0,<=2.5",
|
||||
"isort>=4.2.5,<5",
|
||||
"mccabe>=0.6,<0.7",
|
||||
"toml>=0.7.1",
|
||||
]
|
||||
|
||||
dependency_links = [] # type: ignore
|
||||
|
||||
extras_require = {}
|
||||
extras_require[':sys_platform=="win32"'] = ["colorama"]
|
||||
|
||||
license = "GPL"
|
||||
description = "python code static checker"
|
||||
web = "https://github.com/PyCQA/pylint"
|
||||
mailinglist = "mailto:code-quality@python.org"
|
||||
author = "Python Code Quality Authority"
|
||||
author_email = "code-quality@python.org"
|
||||
|
||||
classifiers = [
|
||||
"Development Status :: 6 - Mature",
|
||||
"Environment :: Console",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: GNU General Public License (GPL)",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.5",
|
||||
"Programming Language :: Python :: 3.6",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3 :: Only",
|
||||
"Programming Language :: Python :: Implementation :: CPython",
|
||||
"Programming Language :: Python :: Implementation :: PyPy",
|
||||
"Topic :: Software Development :: Debuggers",
|
||||
"Topic :: Software Development :: Quality Assurance",
|
||||
"Topic :: Software Development :: Testing",
|
||||
]
|
||||
|
||||
|
||||
long_desc = """\
|
||||
Pylint is a Python source code analyzer which looks for programming
|
||||
errors, helps enforcing a coding standard and sniffs for some code
|
||||
smells (as defined in Martin Fowler's Refactoring book)
|
||||
.
|
||||
Pylint can be seen as another PyChecker since nearly all tests you
|
||||
can do with PyChecker can also be done with Pylint. However, Pylint
|
||||
offers some more features, like checking length of lines of code,
|
||||
checking if variable names are well-formed according to your coding
|
||||
standard, or checking if declared interfaces are truly implemented,
|
||||
and much more.
|
||||
.
|
||||
Additionally, it is possible to write plugins to add your own checks.
|
||||
.
|
||||
Pylint is shipped with "pyreverse" (UML diagram generator)
|
||||
and "symilar" (an independent similarities checker)."""
|
||||
|
||||
scripts = [
|
||||
join("bin", filename) for filename in ("pylint", "symilar", "epylint", "pyreverse")
|
||||
]
|
||||
@@ -0,0 +1,67 @@
|
||||
# Copyright (c) 2006-2014 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
|
||||
# Copyright (c) 2013-2014 Google, Inc.
|
||||
# Copyright (c) 2013 buck@yelp.com <buck@yelp.com>
|
||||
# Copyright (c) 2014-2018 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2014 Brett Cannon <brett@python.org>
|
||||
# Copyright (c) 2014 Arun Persaud <arun@nubati.net>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
|
||||
# Copyright (c) 2016 Moises Lopez <moylop260@vauxoo.com>
|
||||
# Copyright (c) 2017-2018 Bryce Guinta <bryce.paul.guinta@gmail.com>
|
||||
# Copyright (c) 2018-2019 Pierre Sassoulas <pierre.sassoulas@gmail.com>
|
||||
# Copyright (c) 2018 ssolanki <sushobhitsolanki@gmail.com>
|
||||
# Copyright (c) 2019 Bruno P. Kinoshita <kinow@users.noreply.github.com>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""utilities methods and classes for checkers
|
||||
|
||||
Base id of standard checkers (used in msg and report ids):
|
||||
01: base
|
||||
02: classes
|
||||
03: format
|
||||
04: import
|
||||
05: misc
|
||||
06: variables
|
||||
07: exceptions
|
||||
08: similar
|
||||
09: design_analysis
|
||||
10: newstyle
|
||||
11: typecheck
|
||||
12: logging
|
||||
13: string_format
|
||||
14: string_constant
|
||||
15: stdlib
|
||||
16: python3
|
||||
17: refactoring
|
||||
18-50: not yet used: reserved for future internal checkers.
|
||||
51-99: perhaps used: reserved for external checkers
|
||||
|
||||
The raw_metrics checker has no number associated since it doesn't emit any
|
||||
messages nor reports. XXX not true, emit a 07 report !
|
||||
|
||||
"""
|
||||
|
||||
from pylint.checkers.base_checker import BaseChecker, BaseTokenChecker
|
||||
from pylint.utils import register_plugins
|
||||
|
||||
|
||||
def table_lines_from_stats(stats, _, columns):
|
||||
"""get values listed in <columns> from <stats> and <old_stats>,
|
||||
and return a formated list of values, designed to be given to a
|
||||
ureport.Table object
|
||||
"""
|
||||
lines = []
|
||||
for m_type in columns:
|
||||
new = stats[m_type]
|
||||
new = "%.3f" % new if isinstance(new, float) else str(new)
|
||||
lines += (m_type.replace("_", " "), new, "NC", "NC")
|
||||
return lines
|
||||
|
||||
|
||||
def initialize(linter):
|
||||
"""initialize linter with checkers in this package """
|
||||
register_plugins(linter, __path__[0])
|
||||
|
||||
|
||||
__all__ = ("BaseChecker", "BaseTokenChecker", "initialize")
|
||||
@@ -0,0 +1,90 @@
|
||||
# Copyright (c) 2015-2018 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2017 Derek Gustafson <degustaf@gmail.com>
|
||||
# Copyright (c) 2019 Pierre Sassoulas <pierre.sassoulas@gmail.com>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""Checker for anything related to the async protocol (PEP 492)."""
|
||||
|
||||
import sys
|
||||
|
||||
import astroid
|
||||
from astroid import bases, exceptions
|
||||
|
||||
from pylint import checkers, interfaces, utils
|
||||
from pylint.checkers import utils as checker_utils
|
||||
from pylint.checkers.utils import decorated_with
|
||||
|
||||
|
||||
class AsyncChecker(checkers.BaseChecker):
|
||||
__implements__ = interfaces.IAstroidChecker
|
||||
name = "async"
|
||||
msgs = {
|
||||
"E1700": (
|
||||
"Yield inside async function",
|
||||
"yield-inside-async-function",
|
||||
"Used when an `yield` or `yield from` statement is "
|
||||
"found inside an async function.",
|
||||
{"minversion": (3, 5)},
|
||||
),
|
||||
"E1701": (
|
||||
"Async context manager '%s' doesn't implement __aenter__ and __aexit__.",
|
||||
"not-async-context-manager",
|
||||
"Used when an async context manager is used with an object "
|
||||
"that does not implement the async context management protocol.",
|
||||
{"minversion": (3, 5)},
|
||||
),
|
||||
}
|
||||
|
||||
def open(self):
|
||||
self._ignore_mixin_members = utils.get_global_option(
|
||||
self, "ignore-mixin-members"
|
||||
)
|
||||
self._async_generators = ["contextlib.asynccontextmanager"]
|
||||
|
||||
@checker_utils.check_messages("yield-inside-async-function")
|
||||
def visit_asyncfunctiondef(self, node):
|
||||
for child in node.nodes_of_class(astroid.Yield):
|
||||
if child.scope() is node and (
|
||||
sys.version_info[:2] == (3, 5) or isinstance(child, astroid.YieldFrom)
|
||||
):
|
||||
self.add_message("yield-inside-async-function", node=child)
|
||||
|
||||
@checker_utils.check_messages("not-async-context-manager")
|
||||
def visit_asyncwith(self, node):
|
||||
for ctx_mgr, _ in node.items:
|
||||
inferred = checker_utils.safe_infer(ctx_mgr)
|
||||
if inferred is None or inferred is astroid.Uninferable:
|
||||
continue
|
||||
|
||||
if isinstance(inferred, bases.AsyncGenerator):
|
||||
# Check if we are dealing with a function decorated
|
||||
# with contextlib.asynccontextmanager.
|
||||
if decorated_with(inferred.parent, self._async_generators):
|
||||
continue
|
||||
else:
|
||||
try:
|
||||
inferred.getattr("__aenter__")
|
||||
inferred.getattr("__aexit__")
|
||||
except exceptions.NotFoundError:
|
||||
if isinstance(inferred, astroid.Instance):
|
||||
# If we do not know the bases of this class,
|
||||
# just skip it.
|
||||
if not checker_utils.has_known_bases(inferred):
|
||||
continue
|
||||
# Just ignore mixin classes.
|
||||
if self._ignore_mixin_members:
|
||||
if inferred.name[-5:].lower() == "mixin":
|
||||
continue
|
||||
else:
|
||||
continue
|
||||
|
||||
self.add_message(
|
||||
"not-async-context-manager", node=node, args=(inferred.name,)
|
||||
)
|
||||
|
||||
|
||||
def register(linter):
|
||||
"""required method to auto register this checker"""
|
||||
linter.register_checker(AsyncChecker(linter))
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,190 @@
|
||||
# Copyright (c) 2006-2014 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
|
||||
# Copyright (c) 2013-2014 Google, Inc.
|
||||
# Copyright (c) 2013 buck@yelp.com <buck@yelp.com>
|
||||
# Copyright (c) 2014-2019 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2014 Brett Cannon <brett@python.org>
|
||||
# Copyright (c) 2014 Arun Persaud <arun@nubati.net>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
|
||||
# Copyright (c) 2016 Moises Lopez <moylop260@vauxoo.com>
|
||||
# Copyright (c) 2017-2018 Bryce Guinta <bryce.paul.guinta@gmail.com>
|
||||
# Copyright (c) 2018-2019 Pierre Sassoulas <pierre.sassoulas@gmail.com>
|
||||
# Copyright (c) 2018 ssolanki <sushobhitsolanki@gmail.com>
|
||||
# Copyright (c) 2019 Bruno P. Kinoshita <kinow@users.noreply.github.com>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
from inspect import cleandoc
|
||||
from typing import Any
|
||||
|
||||
from pylint.config import OptionsProviderMixIn
|
||||
from pylint.constants import _MSG_ORDER, WarningScope
|
||||
from pylint.exceptions import InvalidMessageError
|
||||
from pylint.interfaces import UNDEFINED, IRawChecker, ITokenChecker, implements
|
||||
from pylint.message.message_definition import MessageDefinition
|
||||
from pylint.utils import get_rst_section, get_rst_title
|
||||
|
||||
|
||||
class BaseChecker(OptionsProviderMixIn):
|
||||
|
||||
# checker name (you may reuse an existing one)
|
||||
name = None # type: str
|
||||
# options level (0 will be displaying in --help, 1 in --long-help)
|
||||
level = 1
|
||||
# ordered list of options to control the checker behaviour
|
||||
options = () # type: Any
|
||||
# messages issued by this checker
|
||||
msgs = {} # type: Any
|
||||
# reports issued by this checker
|
||||
reports = () # type: Any
|
||||
# mark this checker as enabled or not.
|
||||
enabled = True
|
||||
|
||||
def __init__(self, linter=None):
|
||||
"""checker instances should have the linter as argument
|
||||
|
||||
:param ILinter linter: is an object implementing ILinter."""
|
||||
if self.name is not None:
|
||||
self.name = self.name.lower()
|
||||
OptionsProviderMixIn.__init__(self)
|
||||
self.linter = linter
|
||||
|
||||
def __gt__(self, other):
|
||||
"""Permit to sort a list of Checker by name."""
|
||||
return "{}{}".format(self.name, self.msgs).__gt__(
|
||||
"{}{}".format(other.name, other.msgs)
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
status = "Checker" if self.enabled else "Disabled checker"
|
||||
return "{} '{}' (responsible for '{}')".format(
|
||||
status, self.name, "', '".join(self.msgs.keys())
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
"""This might be incomplete because multiple class inheriting BaseChecker
|
||||
can have the same name. Cf MessageHandlerMixIn.get_full_documentation()"""
|
||||
return self.get_full_documentation(
|
||||
msgs=self.msgs, options=self.options_and_values(), reports=self.reports
|
||||
)
|
||||
|
||||
def get_full_documentation(self, msgs, options, reports, doc=None, module=None):
|
||||
result = ""
|
||||
checker_title = "%s checker" % (self.name.replace("_", " ").title())
|
||||
if module:
|
||||
# Provide anchor to link against
|
||||
result += ".. _%s:\n\n" % module
|
||||
result += "%s\n" % get_rst_title(checker_title, "~")
|
||||
if module:
|
||||
result += "This checker is provided by ``%s``.\n" % module
|
||||
result += "Verbatim name of the checker is ``%s``.\n\n" % self.name
|
||||
if doc:
|
||||
# Provide anchor to link against
|
||||
result += get_rst_title("{} Documentation".format(checker_title), "^")
|
||||
result += "%s\n\n" % cleandoc(doc)
|
||||
# options might be an empty generator and not be False when casted to boolean
|
||||
options = list(options)
|
||||
if options:
|
||||
result += get_rst_title("{} Options".format(checker_title), "^")
|
||||
result += "%s\n" % get_rst_section(None, options)
|
||||
if msgs:
|
||||
result += get_rst_title("{} Messages".format(checker_title), "^")
|
||||
for msgid, msg in sorted(
|
||||
msgs.items(), key=lambda kv: (_MSG_ORDER.index(kv[0][0]), kv[1])
|
||||
):
|
||||
msg = self.create_message_definition_from_tuple(msgid, msg)
|
||||
result += "%s\n" % msg.format_help(checkerref=False)
|
||||
result += "\n"
|
||||
if reports:
|
||||
result += get_rst_title("{} Reports".format(checker_title), "^")
|
||||
for report in reports:
|
||||
result += ":%s: %s\n" % report[:2]
|
||||
result += "\n"
|
||||
result += "\n"
|
||||
return result
|
||||
|
||||
def add_message(
|
||||
self, msgid, line=None, node=None, args=None, confidence=None, col_offset=None
|
||||
):
|
||||
if not confidence:
|
||||
confidence = UNDEFINED
|
||||
self.linter.add_message(msgid, line, node, args, confidence, col_offset)
|
||||
|
||||
def check_consistency(self):
|
||||
"""Check the consistency of msgid.
|
||||
|
||||
msg ids for a checker should be a string of len 4, where the two first
|
||||
characters are the checker id and the two last the msg id in this
|
||||
checker.
|
||||
|
||||
:raises InvalidMessageError: If the checker id in the messages are not
|
||||
always the same. """
|
||||
checker_id = None
|
||||
existing_ids = []
|
||||
for message in self.messages:
|
||||
if checker_id is not None and checker_id != message.msgid[1:3]:
|
||||
error_msg = "Inconsistent checker part in message id "
|
||||
error_msg += "'{}' (expected 'x{checker_id}xx' ".format(
|
||||
message.msgid, checker_id=checker_id
|
||||
)
|
||||
error_msg += "because we already had {existing_ids}).".format(
|
||||
existing_ids=existing_ids
|
||||
)
|
||||
raise InvalidMessageError(error_msg)
|
||||
checker_id = message.msgid[1:3]
|
||||
existing_ids.append(message.msgid)
|
||||
|
||||
def create_message_definition_from_tuple(self, msgid, msg_tuple):
|
||||
if implements(self, (IRawChecker, ITokenChecker)):
|
||||
default_scope = WarningScope.LINE
|
||||
else:
|
||||
default_scope = WarningScope.NODE
|
||||
options = {}
|
||||
if len(msg_tuple) > 3:
|
||||
(msg, symbol, descr, options) = msg_tuple
|
||||
elif len(msg_tuple) > 2:
|
||||
(msg, symbol, descr) = msg_tuple
|
||||
else:
|
||||
error_msg = """Messages should have a msgid and a symbol. Something like this :
|
||||
|
||||
"W1234": (
|
||||
"message",
|
||||
"message-symbol",
|
||||
"Message description with detail.",
|
||||
...
|
||||
),
|
||||
"""
|
||||
raise InvalidMessageError(error_msg)
|
||||
options.setdefault("scope", default_scope)
|
||||
return MessageDefinition(self, msgid, msg, descr, symbol, **options)
|
||||
|
||||
@property
|
||||
def messages(self) -> list:
|
||||
return [
|
||||
self.create_message_definition_from_tuple(msgid, msg_tuple)
|
||||
for msgid, msg_tuple in sorted(self.msgs.items())
|
||||
]
|
||||
|
||||
# dummy methods implementing the IChecker interface
|
||||
|
||||
def get_message_definition(self, msgid):
|
||||
for message_definition in self.messages:
|
||||
if message_definition.msgid == msgid:
|
||||
return message_definition
|
||||
error_msg = "MessageDefinition for '{}' does not exists. ".format(msgid)
|
||||
error_msg += "Choose from {}.".format([m.msgid for m in self.messages])
|
||||
raise InvalidMessageError(error_msg)
|
||||
|
||||
def open(self):
|
||||
"""called before visiting project (i.e set of modules)"""
|
||||
|
||||
def close(self):
|
||||
"""called after visiting project (i.e set of modules)"""
|
||||
|
||||
|
||||
class BaseTokenChecker(BaseChecker):
|
||||
"""Base class for checkers that want to have access to the token stream."""
|
||||
|
||||
def process_tokens(self, tokens):
|
||||
"""Should be overridden by subclasses."""
|
||||
raise NotImplementedError()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,500 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2006, 2009-2010, 2012-2015 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
|
||||
# Copyright (c) 2012, 2014 Google, Inc.
|
||||
# Copyright (c) 2014-2019 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2014 Arun Persaud <arun@nubati.net>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
|
||||
# Copyright (c) 2016 Łukasz Rogalski <rogalski.91@gmail.com>
|
||||
# Copyright (c) 2017 ahirnish <ahirnish@gmail.com>
|
||||
# Copyright (c) 2018 Lucas Cimon <lucas.cimon@gmail.com>
|
||||
# Copyright (c) 2018 Mike Frysinger <vapier@gmail.com>
|
||||
# Copyright (c) 2018 Mark Miller <725mrm@gmail.com>
|
||||
# Copyright (c) 2018 Ashley Whetter <ashley@awhetter.co.uk>
|
||||
# Copyright (c) 2018 Ville Skyttä <ville.skytta@iki.fi>
|
||||
# Copyright (c) 2018 Jakub Wilk <jwilk@jwilk.net>
|
||||
# Copyright (c) 2019 Pierre Sassoulas <pierre.sassoulas@gmail.com>
|
||||
# Copyright (c) 2019 Michael Scott Cuthbert <cuthbert@mit.edu>
|
||||
# Copyright (c) 2020 Anthony Sottile <asottile@umich.edu>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""check for signs of poor design"""
|
||||
|
||||
import re
|
||||
from collections import defaultdict
|
||||
|
||||
import astroid
|
||||
from astroid import BoolOp, If, decorators
|
||||
|
||||
from pylint import utils
|
||||
from pylint.checkers import BaseChecker
|
||||
from pylint.checkers.utils import check_messages
|
||||
from pylint.interfaces import IAstroidChecker
|
||||
|
||||
MSGS = {
|
||||
"R0901": (
|
||||
"Too many ancestors (%s/%s)",
|
||||
"too-many-ancestors",
|
||||
"Used when class has too many parent classes, try to reduce "
|
||||
"this to get a simpler (and so easier to use) class.",
|
||||
),
|
||||
"R0902": (
|
||||
"Too many instance attributes (%s/%s)",
|
||||
"too-many-instance-attributes",
|
||||
"Used when class has too many instance attributes, try to reduce "
|
||||
"this to get a simpler (and so easier to use) class.",
|
||||
),
|
||||
"R0903": (
|
||||
"Too few public methods (%s/%s)",
|
||||
"too-few-public-methods",
|
||||
"Used when class has too few public methods, so be sure it's "
|
||||
"really worth it.",
|
||||
),
|
||||
"R0904": (
|
||||
"Too many public methods (%s/%s)",
|
||||
"too-many-public-methods",
|
||||
"Used when class has too many public methods, try to reduce "
|
||||
"this to get a simpler (and so easier to use) class.",
|
||||
),
|
||||
"R0911": (
|
||||
"Too many return statements (%s/%s)",
|
||||
"too-many-return-statements",
|
||||
"Used when a function or method has too many return statement, "
|
||||
"making it hard to follow.",
|
||||
),
|
||||
"R0912": (
|
||||
"Too many branches (%s/%s)",
|
||||
"too-many-branches",
|
||||
"Used when a function or method has too many branches, "
|
||||
"making it hard to follow.",
|
||||
),
|
||||
"R0913": (
|
||||
"Too many arguments (%s/%s)",
|
||||
"too-many-arguments",
|
||||
"Used when a function or method takes too many arguments.",
|
||||
),
|
||||
"R0914": (
|
||||
"Too many local variables (%s/%s)",
|
||||
"too-many-locals",
|
||||
"Used when a function or method has too many local variables.",
|
||||
),
|
||||
"R0915": (
|
||||
"Too many statements (%s/%s)",
|
||||
"too-many-statements",
|
||||
"Used when a function or method has too many statements. You "
|
||||
"should then split it in smaller functions / methods.",
|
||||
),
|
||||
"R0916": (
|
||||
"Too many boolean expressions in if statement (%s/%s)",
|
||||
"too-many-boolean-expressions",
|
||||
"Used when an if statement contains too many boolean expressions.",
|
||||
),
|
||||
}
|
||||
SPECIAL_OBJ = re.compile("^_{2}[a-z]+_{2}$")
|
||||
DATACLASSES_DECORATORS = frozenset({"dataclass", "attrs"})
|
||||
DATACLASS_IMPORT = "dataclasses"
|
||||
TYPING_NAMEDTUPLE = "typing.NamedTuple"
|
||||
|
||||
|
||||
def _is_exempt_from_public_methods(node: astroid.ClassDef) -> bool:
|
||||
"""Check if a class is exempt from too-few-public-methods"""
|
||||
|
||||
# If it's a typing.Namedtuple or an Enum
|
||||
for ancestor in node.ancestors():
|
||||
if ancestor.name == "Enum" and ancestor.root().name == "enum":
|
||||
return True
|
||||
if ancestor.qname() == TYPING_NAMEDTUPLE:
|
||||
return True
|
||||
|
||||
# Or if it's a dataclass
|
||||
if not node.decorators:
|
||||
return False
|
||||
|
||||
root_locals = set(node.root().locals)
|
||||
for decorator in node.decorators.nodes:
|
||||
if isinstance(decorator, astroid.Call):
|
||||
decorator = decorator.func
|
||||
if not isinstance(decorator, (astroid.Name, astroid.Attribute)):
|
||||
continue
|
||||
if isinstance(decorator, astroid.Name):
|
||||
name = decorator.name
|
||||
else:
|
||||
name = decorator.attrname
|
||||
if name in DATACLASSES_DECORATORS and (
|
||||
root_locals.intersection(DATACLASSES_DECORATORS)
|
||||
or DATACLASS_IMPORT in root_locals
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _count_boolean_expressions(bool_op):
|
||||
"""Counts the number of boolean expressions in BoolOp `bool_op` (recursive)
|
||||
|
||||
example: a and (b or c or (d and e)) ==> 5 boolean expressions
|
||||
"""
|
||||
nb_bool_expr = 0
|
||||
for bool_expr in bool_op.get_children():
|
||||
if isinstance(bool_expr, BoolOp):
|
||||
nb_bool_expr += _count_boolean_expressions(bool_expr)
|
||||
else:
|
||||
nb_bool_expr += 1
|
||||
return nb_bool_expr
|
||||
|
||||
|
||||
def _count_methods_in_class(node):
|
||||
all_methods = sum(1 for method in node.methods() if not method.name.startswith("_"))
|
||||
# Special methods count towards the number of public methods,
|
||||
# but don't count towards there being too many methods.
|
||||
for method in node.mymethods():
|
||||
if SPECIAL_OBJ.search(method.name) and method.name != "__init__":
|
||||
all_methods += 1
|
||||
return all_methods
|
||||
|
||||
|
||||
class MisdesignChecker(BaseChecker):
|
||||
"""checks for sign of poor/misdesign:
|
||||
* number of methods, attributes, local variables...
|
||||
* size, complexity of functions, methods
|
||||
"""
|
||||
|
||||
__implements__ = (IAstroidChecker,)
|
||||
|
||||
# configuration section name
|
||||
name = "design"
|
||||
# messages
|
||||
msgs = MSGS
|
||||
priority = -2
|
||||
# configuration options
|
||||
options = (
|
||||
(
|
||||
"max-args",
|
||||
{
|
||||
"default": 5,
|
||||
"type": "int",
|
||||
"metavar": "<int>",
|
||||
"help": "Maximum number of arguments for function / method.",
|
||||
},
|
||||
),
|
||||
(
|
||||
"max-locals",
|
||||
{
|
||||
"default": 15,
|
||||
"type": "int",
|
||||
"metavar": "<int>",
|
||||
"help": "Maximum number of locals for function / method body.",
|
||||
},
|
||||
),
|
||||
(
|
||||
"max-returns",
|
||||
{
|
||||
"default": 6,
|
||||
"type": "int",
|
||||
"metavar": "<int>",
|
||||
"help": "Maximum number of return / yield for function / "
|
||||
"method body.",
|
||||
},
|
||||
),
|
||||
(
|
||||
"max-branches",
|
||||
{
|
||||
"default": 12,
|
||||
"type": "int",
|
||||
"metavar": "<int>",
|
||||
"help": "Maximum number of branch for function / method body.",
|
||||
},
|
||||
),
|
||||
(
|
||||
"max-statements",
|
||||
{
|
||||
"default": 50,
|
||||
"type": "int",
|
||||
"metavar": "<int>",
|
||||
"help": "Maximum number of statements in function / method " "body.",
|
||||
},
|
||||
),
|
||||
(
|
||||
"max-parents",
|
||||
{
|
||||
"default": 7,
|
||||
"type": "int",
|
||||
"metavar": "<num>",
|
||||
"help": "Maximum number of parents for a class (see R0901).",
|
||||
},
|
||||
),
|
||||
(
|
||||
"max-attributes",
|
||||
{
|
||||
"default": 7,
|
||||
"type": "int",
|
||||
"metavar": "<num>",
|
||||
"help": "Maximum number of attributes for a class \
|
||||
(see R0902).",
|
||||
},
|
||||
),
|
||||
(
|
||||
"min-public-methods",
|
||||
{
|
||||
"default": 2,
|
||||
"type": "int",
|
||||
"metavar": "<num>",
|
||||
"help": "Minimum number of public methods for a class \
|
||||
(see R0903).",
|
||||
},
|
||||
),
|
||||
(
|
||||
"max-public-methods",
|
||||
{
|
||||
"default": 20,
|
||||
"type": "int",
|
||||
"metavar": "<num>",
|
||||
"help": "Maximum number of public methods for a class \
|
||||
(see R0904).",
|
||||
},
|
||||
),
|
||||
(
|
||||
"max-bool-expr",
|
||||
{
|
||||
"default": 5,
|
||||
"type": "int",
|
||||
"metavar": "<num>",
|
||||
"help": "Maximum number of boolean expressions in an if "
|
||||
"statement (see R0916).",
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
def __init__(self, linter=None):
|
||||
BaseChecker.__init__(self, linter)
|
||||
self.stats = None
|
||||
self._returns = None
|
||||
self._branches = None
|
||||
self._stmts = None
|
||||
|
||||
def open(self):
|
||||
"""initialize visit variables"""
|
||||
self.stats = self.linter.add_stats()
|
||||
self._returns = []
|
||||
self._branches = defaultdict(int)
|
||||
self._stmts = []
|
||||
|
||||
def _inc_all_stmts(self, amount):
|
||||
for i in range(len(self._stmts)):
|
||||
self._stmts[i] += amount
|
||||
|
||||
@decorators.cachedproperty
|
||||
def _ignored_argument_names(self):
|
||||
return utils.get_global_option(self, "ignored-argument-names", default=None)
|
||||
|
||||
@check_messages(
|
||||
"too-many-ancestors",
|
||||
"too-many-instance-attributes",
|
||||
"too-few-public-methods",
|
||||
"too-many-public-methods",
|
||||
)
|
||||
def visit_classdef(self, node):
|
||||
"""check size of inheritance hierarchy and number of instance attributes
|
||||
"""
|
||||
nb_parents = len(list(node.ancestors()))
|
||||
if nb_parents > self.config.max_parents:
|
||||
self.add_message(
|
||||
"too-many-ancestors",
|
||||
node=node,
|
||||
args=(nb_parents, self.config.max_parents),
|
||||
)
|
||||
|
||||
if len(node.instance_attrs) > self.config.max_attributes:
|
||||
self.add_message(
|
||||
"too-many-instance-attributes",
|
||||
node=node,
|
||||
args=(len(node.instance_attrs), self.config.max_attributes),
|
||||
)
|
||||
|
||||
@check_messages("too-few-public-methods", "too-many-public-methods")
|
||||
def leave_classdef(self, node):
|
||||
"""check number of public methods"""
|
||||
my_methods = sum(
|
||||
1 for method in node.mymethods() if not method.name.startswith("_")
|
||||
)
|
||||
|
||||
# Does the class contain less than n public methods ?
|
||||
# This checks only the methods defined in the current class,
|
||||
# since the user might not have control over the classes
|
||||
# from the ancestors. It avoids some false positives
|
||||
# for classes such as unittest.TestCase, which provides
|
||||
# a lot of assert methods. It doesn't make sense to warn
|
||||
# when the user subclasses TestCase to add his own tests.
|
||||
if my_methods > self.config.max_public_methods:
|
||||
self.add_message(
|
||||
"too-many-public-methods",
|
||||
node=node,
|
||||
args=(my_methods, self.config.max_public_methods),
|
||||
)
|
||||
|
||||
# Stop here for exception, metaclass, interface classes and other
|
||||
# classes for which we don't need to count the methods.
|
||||
if node.type != "class" or _is_exempt_from_public_methods(node):
|
||||
return
|
||||
|
||||
# Does the class contain more than n public methods ?
|
||||
# This checks all the methods defined by ancestors and
|
||||
# by the current class.
|
||||
all_methods = _count_methods_in_class(node)
|
||||
if all_methods < self.config.min_public_methods:
|
||||
self.add_message(
|
||||
"too-few-public-methods",
|
||||
node=node,
|
||||
args=(all_methods, self.config.min_public_methods),
|
||||
)
|
||||
|
||||
@check_messages(
|
||||
"too-many-return-statements",
|
||||
"too-many-branches",
|
||||
"too-many-arguments",
|
||||
"too-many-locals",
|
||||
"too-many-statements",
|
||||
"keyword-arg-before-vararg",
|
||||
)
|
||||
def visit_functiondef(self, node):
|
||||
"""check function name, docstring, arguments, redefinition,
|
||||
variable names, max locals
|
||||
"""
|
||||
# init branch and returns counters
|
||||
self._returns.append(0)
|
||||
# check number of arguments
|
||||
args = node.args.args
|
||||
ignored_argument_names = self._ignored_argument_names
|
||||
if args is not None:
|
||||
ignored_args_num = 0
|
||||
if ignored_argument_names:
|
||||
ignored_args_num = sum(
|
||||
1 for arg in args if ignored_argument_names.match(arg.name)
|
||||
)
|
||||
|
||||
argnum = len(args) - ignored_args_num
|
||||
if argnum > self.config.max_args:
|
||||
self.add_message(
|
||||
"too-many-arguments",
|
||||
node=node,
|
||||
args=(len(args), self.config.max_args),
|
||||
)
|
||||
else:
|
||||
ignored_args_num = 0
|
||||
# check number of local variables
|
||||
locnum = len(node.locals) - ignored_args_num
|
||||
if locnum > self.config.max_locals:
|
||||
self.add_message(
|
||||
"too-many-locals", node=node, args=(locnum, self.config.max_locals)
|
||||
)
|
||||
# init new statements counter
|
||||
self._stmts.append(1)
|
||||
|
||||
visit_asyncfunctiondef = visit_functiondef
|
||||
|
||||
@check_messages(
|
||||
"too-many-return-statements",
|
||||
"too-many-branches",
|
||||
"too-many-arguments",
|
||||
"too-many-locals",
|
||||
"too-many-statements",
|
||||
)
|
||||
def leave_functiondef(self, node):
|
||||
"""most of the work is done here on close:
|
||||
checks for max returns, branch, return in __init__
|
||||
"""
|
||||
returns = self._returns.pop()
|
||||
if returns > self.config.max_returns:
|
||||
self.add_message(
|
||||
"too-many-return-statements",
|
||||
node=node,
|
||||
args=(returns, self.config.max_returns),
|
||||
)
|
||||
branches = self._branches[node]
|
||||
if branches > self.config.max_branches:
|
||||
self.add_message(
|
||||
"too-many-branches",
|
||||
node=node,
|
||||
args=(branches, self.config.max_branches),
|
||||
)
|
||||
# check number of statements
|
||||
stmts = self._stmts.pop()
|
||||
if stmts > self.config.max_statements:
|
||||
self.add_message(
|
||||
"too-many-statements",
|
||||
node=node,
|
||||
args=(stmts, self.config.max_statements),
|
||||
)
|
||||
|
||||
leave_asyncfunctiondef = leave_functiondef
|
||||
|
||||
def visit_return(self, _):
|
||||
"""count number of returns"""
|
||||
if not self._returns:
|
||||
return # return outside function, reported by the base checker
|
||||
self._returns[-1] += 1
|
||||
|
||||
def visit_default(self, node):
|
||||
"""default visit method -> increments the statements counter if
|
||||
necessary
|
||||
"""
|
||||
if node.is_statement:
|
||||
self._inc_all_stmts(1)
|
||||
|
||||
def visit_tryexcept(self, node):
|
||||
"""increments the branches counter"""
|
||||
branches = len(node.handlers)
|
||||
if node.orelse:
|
||||
branches += 1
|
||||
self._inc_branch(node, branches)
|
||||
self._inc_all_stmts(branches)
|
||||
|
||||
def visit_tryfinally(self, node):
|
||||
"""increments the branches counter"""
|
||||
self._inc_branch(node, 2)
|
||||
self._inc_all_stmts(2)
|
||||
|
||||
@check_messages("too-many-boolean-expressions")
|
||||
def visit_if(self, node):
|
||||
"""increments the branches counter and checks boolean expressions"""
|
||||
self._check_boolean_expressions(node)
|
||||
branches = 1
|
||||
# don't double count If nodes coming from some 'elif'
|
||||
if node.orelse and (len(node.orelse) > 1 or not isinstance(node.orelse[0], If)):
|
||||
branches += 1
|
||||
self._inc_branch(node, branches)
|
||||
self._inc_all_stmts(branches)
|
||||
|
||||
def _check_boolean_expressions(self, node):
|
||||
"""Go through "if" node `node` and counts its boolean expressions
|
||||
|
||||
if the "if" node test is a BoolOp node
|
||||
"""
|
||||
condition = node.test
|
||||
if not isinstance(condition, BoolOp):
|
||||
return
|
||||
nb_bool_expr = _count_boolean_expressions(condition)
|
||||
if nb_bool_expr > self.config.max_bool_expr:
|
||||
self.add_message(
|
||||
"too-many-boolean-expressions",
|
||||
node=condition,
|
||||
args=(nb_bool_expr, self.config.max_bool_expr),
|
||||
)
|
||||
|
||||
def visit_while(self, node):
|
||||
"""increments the branches counter"""
|
||||
branches = 1
|
||||
if node.orelse:
|
||||
branches += 1
|
||||
self._inc_branch(node, branches)
|
||||
|
||||
visit_for = visit_while
|
||||
|
||||
def _inc_branch(self, node, branchesnum=1):
|
||||
"""increments the branches counter"""
|
||||
self._branches[node.scope()] += branchesnum
|
||||
|
||||
|
||||
def register(linter):
|
||||
"""required method to auto register this checker """
|
||||
linter.register_checker(MisdesignChecker(linter))
|
||||
@@ -0,0 +1,554 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2006-2011, 2013-2014 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
|
||||
# Copyright (c) 2011-2014 Google, Inc.
|
||||
# Copyright (c) 2012 Tim Hatch <tim@timhatch.com>
|
||||
# Copyright (c) 2013-2019 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2014 Brett Cannon <brett@python.org>
|
||||
# Copyright (c) 2014 Arun Persaud <arun@nubati.net>
|
||||
# Copyright (c) 2015 Rene Zhang <rz99@cornell.edu>
|
||||
# Copyright (c) 2015 Florian Bruhin <me@the-compiler.org>
|
||||
# Copyright (c) 2015 Steven Myint <hg@stevenmyint.com>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
|
||||
# Copyright (c) 2016 Erik <erik.eriksson@yahoo.com>
|
||||
# Copyright (c) 2016 Jakub Wilk <jwilk@jwilk.net>
|
||||
# Copyright (c) 2017 Łukasz Rogalski <rogalski.91@gmail.com>
|
||||
# Copyright (c) 2017 Martin von Gagern <gagern@google.com>
|
||||
# Copyright (c) 2018 Lucas Cimon <lucas.cimon@gmail.com>
|
||||
# Copyright (c) 2018 ssolanki <sushobhitsolanki@gmail.com>
|
||||
# Copyright (c) 2018 Natalie Serebryakova <natalie.serebryakova@Natalies-MacBook-Pro.local>
|
||||
# Copyright (c) 2018 Sushobhit <31987769+sushobhit27@users.noreply.github.com>
|
||||
# Copyright (c) 2018 Carey Metcalfe <carey@cmetcalfe.ca>
|
||||
# Copyright (c) 2018 Mike Frysinger <vapier@gmail.com>
|
||||
# Copyright (c) 2018 Alexander Todorov <atodorov@otb.bg>
|
||||
# Copyright (c) 2018 Ville Skyttä <ville.skytta@iki.fi>
|
||||
# Copyright (c) 2019 Djailla <bastien.vallet@gmail.com>
|
||||
# Copyright (c) 2019 Hugo van Kemenade <hugovk@users.noreply.github.com>
|
||||
# Copyright (c) 2019 Pierre Sassoulas <pierre.sassoulas@gmail.com>
|
||||
# Copyright (c) 2020 Anthony Sottile <asottile@umich.edu>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""Checks for various exception related errors."""
|
||||
import builtins
|
||||
import inspect
|
||||
import typing
|
||||
|
||||
import astroid
|
||||
from astroid.node_classes import NodeNG
|
||||
|
||||
from pylint import checkers, interfaces
|
||||
from pylint.checkers import utils
|
||||
|
||||
|
||||
def _builtin_exceptions():
|
||||
def predicate(obj):
|
||||
return isinstance(obj, type) and issubclass(obj, BaseException)
|
||||
|
||||
members = inspect.getmembers(builtins, predicate)
|
||||
return {exc.__name__ for (_, exc) in members}
|
||||
|
||||
|
||||
def _annotated_unpack_infer(stmt, context=None):
|
||||
"""
|
||||
Recursively generate nodes inferred by the given statement.
|
||||
If the inferred value is a list or a tuple, recurse on the elements.
|
||||
Returns an iterator which yields tuples in the format
|
||||
('original node', 'inferred node').
|
||||
"""
|
||||
if isinstance(stmt, (astroid.List, astroid.Tuple)):
|
||||
for elt in stmt.elts:
|
||||
inferred = utils.safe_infer(elt)
|
||||
if inferred and inferred is not astroid.Uninferable:
|
||||
yield elt, inferred
|
||||
return
|
||||
for inferred in stmt.infer(context):
|
||||
if inferred is astroid.Uninferable:
|
||||
continue
|
||||
yield stmt, inferred
|
||||
|
||||
|
||||
def _is_raising(body: typing.List) -> bool:
|
||||
"""Return true if the given statement node raise an exception"""
|
||||
for node in body:
|
||||
if isinstance(node, astroid.Raise):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
OVERGENERAL_EXCEPTIONS = ("BaseException", "Exception")
|
||||
BUILTINS_NAME = builtins.__name__
|
||||
|
||||
MSGS = {
|
||||
"E0701": (
|
||||
"Bad except clauses order (%s)",
|
||||
"bad-except-order",
|
||||
"Used when except clauses are not in the correct order (from the "
|
||||
"more specific to the more generic). If you don't fix the order, "
|
||||
"some exceptions may not be caught by the most specific handler.",
|
||||
),
|
||||
"E0702": (
|
||||
"Raising %s while only classes or instances are allowed",
|
||||
"raising-bad-type",
|
||||
"Used when something which is neither a class, an instance or a "
|
||||
"string is raised (i.e. a `TypeError` will be raised).",
|
||||
),
|
||||
"E0703": (
|
||||
"Exception context set to something which is not an exception, nor None",
|
||||
"bad-exception-context",
|
||||
'Used when using the syntax "raise ... from ...", '
|
||||
"where the exception context is not an exception, "
|
||||
"nor None.",
|
||||
),
|
||||
"E0704": (
|
||||
"The raise statement is not inside an except clause",
|
||||
"misplaced-bare-raise",
|
||||
"Used when a bare raise is not used inside an except clause. "
|
||||
"This generates an error, since there are no active exceptions "
|
||||
"to be reraised. An exception to this rule is represented by "
|
||||
"a bare raise inside a finally clause, which might work, as long "
|
||||
"as an exception is raised inside the try block, but it is "
|
||||
"nevertheless a code smell that must not be relied upon.",
|
||||
),
|
||||
"E0710": (
|
||||
"Raising a new style class which doesn't inherit from BaseException",
|
||||
"raising-non-exception",
|
||||
"Used when a new style class which doesn't inherit from "
|
||||
"BaseException is raised.",
|
||||
),
|
||||
"E0711": (
|
||||
"NotImplemented raised - should raise NotImplementedError",
|
||||
"notimplemented-raised",
|
||||
"Used when NotImplemented is raised instead of NotImplementedError",
|
||||
),
|
||||
"E0712": (
|
||||
"Catching an exception which doesn't inherit from Exception: %s",
|
||||
"catching-non-exception",
|
||||
"Used when a class which doesn't inherit from "
|
||||
"Exception is used as an exception in an except clause.",
|
||||
),
|
||||
"W0702": (
|
||||
"No exception type(s) specified",
|
||||
"bare-except",
|
||||
"Used when an except clause doesn't specify exceptions type to catch.",
|
||||
),
|
||||
"W0703": (
|
||||
"Catching too general exception %s",
|
||||
"broad-except",
|
||||
"Used when an except catches a too general exception, "
|
||||
"possibly burying unrelated errors.",
|
||||
),
|
||||
"W0705": (
|
||||
"Catching previously caught exception type %s",
|
||||
"duplicate-except",
|
||||
"Used when an except catches a type that was already caught by "
|
||||
"a previous handler.",
|
||||
),
|
||||
"W0706": (
|
||||
"The except handler raises immediately",
|
||||
"try-except-raise",
|
||||
"Used when an except handler uses raise as its first or only "
|
||||
"operator. This is useless because it raises back the exception "
|
||||
"immediately. Remove the raise operator or the entire "
|
||||
"try-except-raise block!",
|
||||
),
|
||||
"W0711": (
|
||||
'Exception to catch is the result of a binary "%s" operation',
|
||||
"binary-op-exception",
|
||||
"Used when the exception to catch is of the form "
|
||||
'"except A or B:". If intending to catch multiple, '
|
||||
'rewrite as "except (A, B):"',
|
||||
),
|
||||
"W0715": (
|
||||
"Exception arguments suggest string formatting might be intended",
|
||||
"raising-format-tuple",
|
||||
"Used when passing multiple arguments to an exception "
|
||||
"constructor, the first of them a string literal containing what "
|
||||
"appears to be placeholders intended for formatting",
|
||||
),
|
||||
"W0716": (
|
||||
"Invalid exception operation. %s",
|
||||
"wrong-exception-operation",
|
||||
"Used when an operation is done against an exception, but the operation "
|
||||
"is not valid for the exception in question. Usually emitted when having "
|
||||
"binary operations between exceptions in except handlers.",
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class BaseVisitor:
|
||||
"""Base class for visitors defined in this module."""
|
||||
|
||||
def __init__(self, checker, node):
|
||||
self._checker = checker
|
||||
self._node = node
|
||||
|
||||
def visit(self, node):
|
||||
name = node.__class__.__name__.lower()
|
||||
dispatch_meth = getattr(self, "visit_" + name, None)
|
||||
if dispatch_meth:
|
||||
dispatch_meth(node)
|
||||
else:
|
||||
self.visit_default(node)
|
||||
|
||||
def visit_default(self, node): # pylint: disable=unused-argument
|
||||
"""Default implementation for all the nodes."""
|
||||
|
||||
|
||||
class ExceptionRaiseRefVisitor(BaseVisitor):
|
||||
"""Visit references (anything that is not an AST leaf)."""
|
||||
|
||||
def visit_name(self, name):
|
||||
if name.name == "NotImplemented":
|
||||
self._checker.add_message("notimplemented-raised", node=self._node)
|
||||
|
||||
def visit_call(self, call):
|
||||
if isinstance(call.func, astroid.Name):
|
||||
self.visit_name(call.func)
|
||||
if (
|
||||
len(call.args) > 1
|
||||
and isinstance(call.args[0], astroid.Const)
|
||||
and isinstance(call.args[0].value, str)
|
||||
):
|
||||
msg = call.args[0].value
|
||||
if "%" in msg or ("{" in msg and "}" in msg):
|
||||
self._checker.add_message("raising-format-tuple", node=self._node)
|
||||
|
||||
|
||||
class ExceptionRaiseLeafVisitor(BaseVisitor):
|
||||
"""Visitor for handling leaf kinds of a raise value."""
|
||||
|
||||
def visit_const(self, const):
|
||||
if not isinstance(const.value, str):
|
||||
# raising-string will be emitted from python3 porting checker.
|
||||
self._checker.add_message(
|
||||
"raising-bad-type", node=self._node, args=const.value.__class__.__name__
|
||||
)
|
||||
|
||||
def visit_instance(self, instance):
|
||||
# pylint: disable=protected-access
|
||||
cls = instance._proxied
|
||||
self.visit_classdef(cls)
|
||||
|
||||
# Exception instances have a particular class type
|
||||
visit_exceptioninstance = visit_instance
|
||||
|
||||
def visit_classdef(self, cls):
|
||||
if not utils.inherit_from_std_ex(cls) and utils.has_known_bases(cls):
|
||||
if cls.newstyle:
|
||||
self._checker.add_message("raising-non-exception", node=self._node)
|
||||
|
||||
def visit_tuple(self, _):
|
||||
self._checker.add_message("raising-bad-type", node=self._node, args="tuple")
|
||||
|
||||
def visit_default(self, node):
|
||||
name = getattr(node, "name", node.__class__.__name__)
|
||||
self._checker.add_message("raising-bad-type", node=self._node, args=name)
|
||||
|
||||
|
||||
class ExceptionsChecker(checkers.BaseChecker):
|
||||
"""Exception related checks."""
|
||||
|
||||
__implements__ = interfaces.IAstroidChecker
|
||||
|
||||
name = "exceptions"
|
||||
msgs = MSGS
|
||||
priority = -4
|
||||
options = (
|
||||
(
|
||||
"overgeneral-exceptions",
|
||||
{
|
||||
"default": OVERGENERAL_EXCEPTIONS,
|
||||
"type": "csv",
|
||||
"metavar": "<comma-separated class names>",
|
||||
"help": "Exceptions that will emit a warning "
|
||||
'when being caught. Defaults to "%s".'
|
||||
% (", ".join(OVERGENERAL_EXCEPTIONS),),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
def open(self):
|
||||
self._builtin_exceptions = _builtin_exceptions()
|
||||
super().open()
|
||||
|
||||
@utils.check_messages(
|
||||
"misplaced-bare-raise",
|
||||
"raising-bad-type",
|
||||
"raising-non-exception",
|
||||
"notimplemented-raised",
|
||||
"bad-exception-context",
|
||||
"raising-format-tuple",
|
||||
)
|
||||
def visit_raise(self, node):
|
||||
if node.exc is None:
|
||||
self._check_misplaced_bare_raise(node)
|
||||
return
|
||||
|
||||
if node.cause:
|
||||
self._check_bad_exception_context(node)
|
||||
|
||||
expr = node.exc
|
||||
ExceptionRaiseRefVisitor(self, node).visit(expr)
|
||||
|
||||
try:
|
||||
inferred_value = expr.inferred()[-1]
|
||||
except astroid.InferenceError:
|
||||
pass
|
||||
else:
|
||||
if inferred_value:
|
||||
ExceptionRaiseLeafVisitor(self, node).visit(inferred_value)
|
||||
|
||||
def _check_misplaced_bare_raise(self, node):
|
||||
# Filter out if it's present in __exit__.
|
||||
scope = node.scope()
|
||||
if (
|
||||
isinstance(scope, astroid.FunctionDef)
|
||||
and scope.is_method()
|
||||
and scope.name == "__exit__"
|
||||
):
|
||||
return
|
||||
|
||||
current = node
|
||||
# Stop when a new scope is generated or when the raise
|
||||
# statement is found inside a TryFinally.
|
||||
ignores = (astroid.ExceptHandler, astroid.FunctionDef)
|
||||
while current and not isinstance(current.parent, ignores):
|
||||
current = current.parent
|
||||
|
||||
expected = (astroid.ExceptHandler,)
|
||||
if not current or not isinstance(current.parent, expected):
|
||||
self.add_message("misplaced-bare-raise", node=node)
|
||||
|
||||
def _check_bad_exception_context(self, node):
|
||||
"""Verify that the exception context is properly set.
|
||||
|
||||
An exception context can be only `None` or an exception.
|
||||
"""
|
||||
cause = utils.safe_infer(node.cause)
|
||||
if cause in (astroid.Uninferable, None):
|
||||
return
|
||||
|
||||
if isinstance(cause, astroid.Const):
|
||||
if cause.value is not None:
|
||||
self.add_message("bad-exception-context", node=node)
|
||||
elif not isinstance(cause, astroid.ClassDef) and not utils.inherit_from_std_ex(
|
||||
cause
|
||||
):
|
||||
self.add_message("bad-exception-context", node=node)
|
||||
|
||||
def _check_catching_non_exception(self, handler, exc, part):
|
||||
if isinstance(exc, astroid.Tuple):
|
||||
# Check if it is a tuple of exceptions.
|
||||
inferred = [utils.safe_infer(elt) for elt in exc.elts]
|
||||
if any(node is astroid.Uninferable for node in inferred):
|
||||
# Don't emit if we don't know every component.
|
||||
return
|
||||
if all(
|
||||
node
|
||||
and (utils.inherit_from_std_ex(node) or not utils.has_known_bases(node))
|
||||
for node in inferred
|
||||
):
|
||||
return
|
||||
|
||||
if not isinstance(exc, astroid.ClassDef):
|
||||
# Don't emit the warning if the inferred stmt
|
||||
# is None, but the exception handler is something else,
|
||||
# maybe it was redefined.
|
||||
if isinstance(exc, astroid.Const) and exc.value is None:
|
||||
if (
|
||||
isinstance(handler.type, astroid.Const)
|
||||
and handler.type.value is None
|
||||
) or handler.type.parent_of(exc):
|
||||
# If the exception handler catches None or
|
||||
# the exception component, which is None, is
|
||||
# defined by the entire exception handler, then
|
||||
# emit a warning.
|
||||
self.add_message(
|
||||
"catching-non-exception",
|
||||
node=handler.type,
|
||||
args=(part.as_string(),),
|
||||
)
|
||||
else:
|
||||
self.add_message(
|
||||
"catching-non-exception",
|
||||
node=handler.type,
|
||||
args=(part.as_string(),),
|
||||
)
|
||||
return
|
||||
|
||||
if (
|
||||
not utils.inherit_from_std_ex(exc)
|
||||
and exc.name not in self._builtin_exceptions
|
||||
):
|
||||
if utils.has_known_bases(exc):
|
||||
self.add_message(
|
||||
"catching-non-exception", node=handler.type, args=(exc.name,)
|
||||
)
|
||||
|
||||
def _check_try_except_raise(self, node):
|
||||
def gather_exceptions_from_handler(
|
||||
handler,
|
||||
) -> typing.Optional[typing.List[NodeNG]]:
|
||||
exceptions = [] # type: typing.List[NodeNG]
|
||||
if handler.type:
|
||||
exceptions_in_handler = utils.safe_infer(handler.type)
|
||||
if isinstance(exceptions_in_handler, astroid.Tuple):
|
||||
exceptions = list(
|
||||
{
|
||||
exception
|
||||
for exception in exceptions_in_handler.elts
|
||||
if isinstance(exception, astroid.Name)
|
||||
}
|
||||
)
|
||||
elif exceptions_in_handler:
|
||||
exceptions = [exceptions_in_handler]
|
||||
else:
|
||||
# Break when we cannot infer anything reliably.
|
||||
return None
|
||||
return exceptions
|
||||
|
||||
bare_raise = False
|
||||
handler_having_bare_raise = None
|
||||
excs_in_bare_handler = []
|
||||
for handler in node.handlers:
|
||||
if bare_raise:
|
||||
# check that subsequent handler is not parent of handler which had bare raise.
|
||||
# since utils.safe_infer can fail for bare except, check it before.
|
||||
# also break early if bare except is followed by bare except.
|
||||
|
||||
excs_in_current_handler = gather_exceptions_from_handler(handler)
|
||||
|
||||
if not excs_in_current_handler:
|
||||
bare_raise = False
|
||||
break
|
||||
if excs_in_bare_handler is None:
|
||||
# It can be `None` when the inference failed
|
||||
break
|
||||
|
||||
for exc_in_current_handler in excs_in_current_handler:
|
||||
inferred_current = utils.safe_infer(exc_in_current_handler)
|
||||
if any(
|
||||
utils.is_subclass_of(
|
||||
utils.safe_infer(exc_in_bare_handler), inferred_current
|
||||
)
|
||||
for exc_in_bare_handler in excs_in_bare_handler
|
||||
):
|
||||
bare_raise = False
|
||||
break
|
||||
|
||||
# `raise` as the first operator inside the except handler
|
||||
if _is_raising([handler.body[0]]):
|
||||
# flags when there is a bare raise
|
||||
if handler.body[0].exc is None:
|
||||
bare_raise = True
|
||||
handler_having_bare_raise = handler
|
||||
excs_in_bare_handler = gather_exceptions_from_handler(handler)
|
||||
else:
|
||||
if bare_raise:
|
||||
self.add_message("try-except-raise", node=handler_having_bare_raise)
|
||||
|
||||
@utils.check_messages("wrong-exception-operation")
|
||||
def visit_binop(self, node):
|
||||
if isinstance(node.parent, astroid.ExceptHandler):
|
||||
# except (V | A)
|
||||
suggestion = "Did you mean '(%s, %s)' instead?" % (
|
||||
node.left.as_string(),
|
||||
node.right.as_string(),
|
||||
)
|
||||
self.add_message("wrong-exception-operation", node=node, args=(suggestion,))
|
||||
|
||||
@utils.check_messages("wrong-exception-operation")
|
||||
def visit_compare(self, node):
|
||||
if isinstance(node.parent, astroid.ExceptHandler):
|
||||
# except (V < A)
|
||||
suggestion = "Did you mean '(%s, %s)' instead?" % (
|
||||
node.left.as_string(),
|
||||
", ".join(operand.as_string() for _, operand in node.ops),
|
||||
)
|
||||
self.add_message("wrong-exception-operation", node=node, args=(suggestion,))
|
||||
|
||||
@utils.check_messages(
|
||||
"bare-except",
|
||||
"broad-except",
|
||||
"try-except-raise",
|
||||
"binary-op-exception",
|
||||
"bad-except-order",
|
||||
"catching-non-exception",
|
||||
"duplicate-except",
|
||||
)
|
||||
def visit_tryexcept(self, node):
|
||||
"""check for empty except"""
|
||||
self._check_try_except_raise(node)
|
||||
exceptions_classes = []
|
||||
nb_handlers = len(node.handlers)
|
||||
for index, handler in enumerate(node.handlers):
|
||||
if handler.type is None:
|
||||
if not _is_raising(handler.body):
|
||||
self.add_message("bare-except", node=handler)
|
||||
|
||||
# check if an "except:" is followed by some other
|
||||
# except
|
||||
if index < (nb_handlers - 1):
|
||||
msg = "empty except clause should always appear last"
|
||||
self.add_message("bad-except-order", node=node, args=msg)
|
||||
|
||||
elif isinstance(handler.type, astroid.BoolOp):
|
||||
self.add_message(
|
||||
"binary-op-exception", node=handler, args=handler.type.op
|
||||
)
|
||||
else:
|
||||
try:
|
||||
excs = list(_annotated_unpack_infer(handler.type))
|
||||
except astroid.InferenceError:
|
||||
continue
|
||||
|
||||
for part, exc in excs:
|
||||
if exc is astroid.Uninferable:
|
||||
continue
|
||||
if isinstance(exc, astroid.Instance) and utils.inherit_from_std_ex(
|
||||
exc
|
||||
):
|
||||
# pylint: disable=protected-access
|
||||
exc = exc._proxied
|
||||
|
||||
self._check_catching_non_exception(handler, exc, part)
|
||||
|
||||
if not isinstance(exc, astroid.ClassDef):
|
||||
continue
|
||||
|
||||
exc_ancestors = [
|
||||
anc
|
||||
for anc in exc.ancestors()
|
||||
if isinstance(anc, astroid.ClassDef)
|
||||
]
|
||||
|
||||
for previous_exc in exceptions_classes:
|
||||
if previous_exc in exc_ancestors:
|
||||
msg = "%s is an ancestor class of %s" % (
|
||||
previous_exc.name,
|
||||
exc.name,
|
||||
)
|
||||
self.add_message(
|
||||
"bad-except-order", node=handler.type, args=msg
|
||||
)
|
||||
if (
|
||||
exc.name in self.config.overgeneral_exceptions
|
||||
and exc.root().name == utils.EXCEPTIONS_MODULE
|
||||
and not _is_raising(handler.body)
|
||||
):
|
||||
self.add_message(
|
||||
"broad-except", args=exc.name, node=handler.type
|
||||
)
|
||||
|
||||
if exc in exceptions_classes:
|
||||
self.add_message(
|
||||
"duplicate-except", args=exc.name, node=handler.type
|
||||
)
|
||||
|
||||
exceptions_classes += [exc for _, exc in excs]
|
||||
|
||||
|
||||
def register(linter):
|
||||
"""required method to auto register this checker"""
|
||||
linter.register_checker(ExceptionsChecker(linter))
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,991 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2006-2015 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
|
||||
# Copyright (c) 2012-2014 Google, Inc.
|
||||
# Copyright (c) 2013 buck@yelp.com <buck@yelp.com>
|
||||
# Copyright (c) 2014-2020 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2014 Brett Cannon <brett@python.org>
|
||||
# Copyright (c) 2014 Arun Persaud <arun@nubati.net>
|
||||
# Copyright (c) 2015-2016 Moises Lopez <moylop260@vauxoo.com>
|
||||
# Copyright (c) 2015 Dmitry Pribysh <dmand@yandex.ru>
|
||||
# Copyright (c) 2015 Cezar <celnazli@bitdefender.com>
|
||||
# Copyright (c) 2015 Florian Bruhin <me@the-compiler.org>
|
||||
# Copyright (c) 2015 Noam Yorav-Raphael <noamraph@gmail.com>
|
||||
# Copyright (c) 2015 James Morgensen <james.morgensen@gmail.com>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
|
||||
# Copyright (c) 2016 Jared Garst <cultofjared@gmail.com>
|
||||
# Copyright (c) 2016 Maik Röder <maikroeder@gmail.com>
|
||||
# Copyright (c) 2016 Glenn Matthews <glenn@e-dad.net>
|
||||
# Copyright (c) 2016 Ashley Whetter <ashley@awhetter.co.uk>
|
||||
# Copyright (c) 2017 hippo91 <guillaume.peillex@gmail.com>
|
||||
# Copyright (c) 2017 Michka Popoff <michkapopoff@gmail.com>
|
||||
# Copyright (c) 2017 Łukasz Rogalski <rogalski.91@gmail.com>
|
||||
# Copyright (c) 2017 Erik Wright <erik.wright@shopify.com>
|
||||
# Copyright (c) 2018 Lucas Cimon <lucas.cimon@gmail.com>
|
||||
# Copyright (c) 2018 Hornwitser <github@hornwitser.no>
|
||||
# Copyright (c) 2018 ssolanki <sushobhitsolanki@gmail.com>
|
||||
# Copyright (c) 2018 Natalie Serebryakova <natalie.serebryakova@Natalies-MacBook-Pro.local>
|
||||
# Copyright (c) 2018 Mike Frysinger <vapier@gmail.com>
|
||||
# Copyright (c) 2018 Sushobhit <31987769+sushobhit27@users.noreply.github.com>
|
||||
# Copyright (c) 2018 Marianna Polatoglou <mpolatoglou@bloomberg.net>
|
||||
# Copyright (c) 2019 Nick Drozd <nicholasdrozd@gmail.com>
|
||||
# Copyright (c) 2019 Hugo van Kemenade <hugovk@users.noreply.github.com>
|
||||
# Copyright (c) 2019 Pierre Sassoulas <pierre.sassoulas@gmail.com>
|
||||
# Copyright (c) 2019 Nick Smith <clickthisnick@users.noreply.github.com>
|
||||
# Copyright (c) 2019 Paul Renvoisé <renvoisepaul@gmail.com>
|
||||
# Copyright (c) 2020 Anthony Sottile <asottile@umich.edu>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""imports checkers for Python code"""
|
||||
|
||||
import collections
|
||||
import copy
|
||||
import os
|
||||
import sys
|
||||
from distutils import sysconfig
|
||||
|
||||
import astroid
|
||||
import isort
|
||||
from astroid import modutils
|
||||
from astroid.decorators import cached
|
||||
|
||||
from pylint.checkers import BaseChecker
|
||||
from pylint.checkers.utils import (
|
||||
check_messages,
|
||||
is_from_fallback_block,
|
||||
node_ignores_exception,
|
||||
)
|
||||
from pylint.exceptions import EmptyReportError
|
||||
from pylint.graph import DotBackend, get_cycles
|
||||
from pylint.interfaces import IAstroidChecker
|
||||
from pylint.reporters.ureports.nodes import Paragraph, VerbatimText
|
||||
from pylint.utils import get_global_option
|
||||
|
||||
|
||||
def _qualified_names(modname):
|
||||
"""Split the names of the given module into subparts
|
||||
|
||||
For example,
|
||||
_qualified_names('pylint.checkers.ImportsChecker')
|
||||
returns
|
||||
['pylint', 'pylint.checkers', 'pylint.checkers.ImportsChecker']
|
||||
"""
|
||||
names = modname.split(".")
|
||||
return [".".join(names[0 : i + 1]) for i in range(len(names))]
|
||||
|
||||
|
||||
def _get_import_name(importnode, modname):
|
||||
"""Get a prepared module name from the given import node
|
||||
|
||||
In the case of relative imports, this will return the
|
||||
absolute qualified module name, which might be useful
|
||||
for debugging. Otherwise, the initial module name
|
||||
is returned unchanged.
|
||||
"""
|
||||
if isinstance(importnode, astroid.ImportFrom):
|
||||
if importnode.level:
|
||||
root = importnode.root()
|
||||
if isinstance(root, astroid.Module):
|
||||
modname = root.relative_to_absolute_name(
|
||||
modname, level=importnode.level
|
||||
)
|
||||
return modname
|
||||
|
||||
|
||||
def _get_first_import(node, context, name, base, level, alias):
|
||||
"""return the node where [base.]<name> is imported or None if not found
|
||||
"""
|
||||
fullname = "%s.%s" % (base, name) if base else name
|
||||
|
||||
first = None
|
||||
found = False
|
||||
for first in context.body:
|
||||
if first is node:
|
||||
continue
|
||||
if first.scope() is node.scope() and first.fromlineno > node.fromlineno:
|
||||
continue
|
||||
if isinstance(first, astroid.Import):
|
||||
if any(fullname == iname[0] for iname in first.names):
|
||||
found = True
|
||||
break
|
||||
elif isinstance(first, astroid.ImportFrom):
|
||||
if level == first.level:
|
||||
for imported_name, imported_alias in first.names:
|
||||
if fullname == "%s.%s" % (first.modname, imported_name):
|
||||
found = True
|
||||
break
|
||||
if (
|
||||
name != "*"
|
||||
and name == imported_name
|
||||
and not (alias or imported_alias)
|
||||
):
|
||||
found = True
|
||||
break
|
||||
if found:
|
||||
break
|
||||
if found and not astroid.are_exclusive(first, node):
|
||||
return first
|
||||
return None
|
||||
|
||||
|
||||
def _ignore_import_failure(node, modname, ignored_modules):
|
||||
for submodule in _qualified_names(modname):
|
||||
if submodule in ignored_modules:
|
||||
return True
|
||||
|
||||
return node_ignores_exception(node, ImportError)
|
||||
|
||||
|
||||
# utilities to represents import dependencies as tree and dot graph ###########
|
||||
|
||||
|
||||
def _make_tree_defs(mod_files_list):
|
||||
"""get a list of 2-uple (module, list_of_files_which_import_this_module),
|
||||
it will return a dictionary to represent this as a tree
|
||||
"""
|
||||
tree_defs = {}
|
||||
for mod, files in mod_files_list:
|
||||
node = (tree_defs, ())
|
||||
for prefix in mod.split("."):
|
||||
node = node[0].setdefault(prefix, [{}, []])
|
||||
node[1] += files
|
||||
return tree_defs
|
||||
|
||||
|
||||
def _repr_tree_defs(data, indent_str=None):
|
||||
"""return a string which represents imports as a tree"""
|
||||
lines = []
|
||||
nodes = data.items()
|
||||
for i, (mod, (sub, files)) in enumerate(sorted(nodes, key=lambda x: x[0])):
|
||||
if not files:
|
||||
files = ""
|
||||
else:
|
||||
files = "(%s)" % ",".join(sorted(files))
|
||||
if indent_str is None:
|
||||
lines.append("%s %s" % (mod, files))
|
||||
sub_indent_str = " "
|
||||
else:
|
||||
lines.append(r"%s\-%s %s" % (indent_str, mod, files))
|
||||
if i == len(nodes) - 1:
|
||||
sub_indent_str = "%s " % indent_str
|
||||
else:
|
||||
sub_indent_str = "%s| " % indent_str
|
||||
if sub:
|
||||
lines.append(_repr_tree_defs(sub, sub_indent_str))
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _dependencies_graph(filename, dep_info):
|
||||
"""write dependencies as a dot (graphviz) file
|
||||
"""
|
||||
done = {}
|
||||
printer = DotBackend(filename[:-4], rankdir="LR")
|
||||
printer.emit('URL="." node[shape="box"]')
|
||||
for modname, dependencies in sorted(dep_info.items()):
|
||||
done[modname] = 1
|
||||
printer.emit_node(modname)
|
||||
for depmodname in dependencies:
|
||||
if depmodname not in done:
|
||||
done[depmodname] = 1
|
||||
printer.emit_node(depmodname)
|
||||
for depmodname, dependencies in sorted(dep_info.items()):
|
||||
for modname in dependencies:
|
||||
printer.emit_edge(modname, depmodname)
|
||||
printer.generate(filename)
|
||||
|
||||
|
||||
def _make_graph(filename, dep_info, sect, gtype):
|
||||
"""generate a dependencies graph and add some information about it in the
|
||||
report's section
|
||||
"""
|
||||
_dependencies_graph(filename, dep_info)
|
||||
sect.append(Paragraph("%simports graph has been written to %s" % (gtype, filename)))
|
||||
|
||||
|
||||
# the import checker itself ###################################################
|
||||
|
||||
MSGS = {
|
||||
"E0401": (
|
||||
"Unable to import %s",
|
||||
"import-error",
|
||||
"Used when pylint has been unable to import a module.",
|
||||
{"old_names": [("F0401", "old-import-error")]},
|
||||
),
|
||||
"E0402": (
|
||||
"Attempted relative import beyond top-level package",
|
||||
"relative-beyond-top-level",
|
||||
"Used when a relative import tries to access too many levels "
|
||||
"in the current package.",
|
||||
),
|
||||
"R0401": (
|
||||
"Cyclic import (%s)",
|
||||
"cyclic-import",
|
||||
"Used when a cyclic import between two or more modules is detected.",
|
||||
),
|
||||
"W0401": (
|
||||
"Wildcard import %s",
|
||||
"wildcard-import",
|
||||
"Used when `from module import *` is detected.",
|
||||
),
|
||||
"W0402": (
|
||||
"Uses of a deprecated module %r",
|
||||
"deprecated-module",
|
||||
"Used a module marked as deprecated is imported.",
|
||||
),
|
||||
"W0404": (
|
||||
"Reimport %r (imported line %s)",
|
||||
"reimported",
|
||||
"Used when a module is reimported multiple times.",
|
||||
),
|
||||
"W0406": (
|
||||
"Module import itself",
|
||||
"import-self",
|
||||
"Used when a module is importing itself.",
|
||||
),
|
||||
"W0407": (
|
||||
"Prefer importing %r instead of %r",
|
||||
"preferred-module",
|
||||
"Used when a module imported has a preferred replacement module.",
|
||||
),
|
||||
"W0410": (
|
||||
"__future__ import is not the first non docstring statement",
|
||||
"misplaced-future",
|
||||
"Python 2.5 and greater require __future__ import to be the "
|
||||
"first non docstring statement in the module.",
|
||||
),
|
||||
"C0410": (
|
||||
"Multiple imports on one line (%s)",
|
||||
"multiple-imports",
|
||||
"Used when import statement importing multiple modules is detected.",
|
||||
),
|
||||
"C0411": (
|
||||
"%s should be placed before %s",
|
||||
"wrong-import-order",
|
||||
"Used when PEP8 import order is not respected (standard imports "
|
||||
"first, then third-party libraries, then local imports)",
|
||||
),
|
||||
"C0412": (
|
||||
"Imports from package %s are not grouped",
|
||||
"ungrouped-imports",
|
||||
"Used when imports are not grouped by packages",
|
||||
),
|
||||
"C0413": (
|
||||
'Import "%s" should be placed at the top of the module',
|
||||
"wrong-import-position",
|
||||
"Used when code and imports are mixed",
|
||||
),
|
||||
"C0414": (
|
||||
"Import alias does not rename original package",
|
||||
"useless-import-alias",
|
||||
"Used when an import alias is same as original package."
|
||||
"e.g using import numpy as numpy instead of import numpy as np",
|
||||
),
|
||||
"C0415": (
|
||||
"Import outside toplevel (%s)",
|
||||
"import-outside-toplevel",
|
||||
"Used when an import statement is used anywhere other than the module "
|
||||
"toplevel. Move this import to the top of the file.",
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
DEFAULT_STANDARD_LIBRARY = ()
|
||||
DEFAULT_KNOWN_THIRD_PARTY = ("enchant",)
|
||||
DEFAULT_PREFERRED_MODULES = ()
|
||||
|
||||
|
||||
class ImportsChecker(BaseChecker):
|
||||
"""checks for
|
||||
* external modules dependencies
|
||||
* relative / wildcard imports
|
||||
* cyclic imports
|
||||
* uses of deprecated modules
|
||||
* uses of modules instead of preferred modules
|
||||
"""
|
||||
|
||||
__implements__ = IAstroidChecker
|
||||
|
||||
name = "imports"
|
||||
msgs = MSGS
|
||||
priority = -2
|
||||
deprecated_modules = ("optparse", "tkinter.tix")
|
||||
|
||||
options = (
|
||||
(
|
||||
"deprecated-modules",
|
||||
{
|
||||
"default": deprecated_modules,
|
||||
"type": "csv",
|
||||
"metavar": "<modules>",
|
||||
"help": "Deprecated modules which should not be used,"
|
||||
" separated by a comma.",
|
||||
},
|
||||
),
|
||||
(
|
||||
"preferred-modules",
|
||||
{
|
||||
"default": DEFAULT_PREFERRED_MODULES,
|
||||
"type": "csv",
|
||||
"metavar": "<module:preferred-module>",
|
||||
"help": "Couples of modules and preferred modules,"
|
||||
" separated by a comma.",
|
||||
},
|
||||
),
|
||||
(
|
||||
"import-graph",
|
||||
{
|
||||
"default": "",
|
||||
"type": "string",
|
||||
"metavar": "<file.dot>",
|
||||
"help": "Create a graph of every (i.e. internal and"
|
||||
" external) dependencies in the given file"
|
||||
" (report RP0402 must not be disabled).",
|
||||
},
|
||||
),
|
||||
(
|
||||
"ext-import-graph",
|
||||
{
|
||||
"default": "",
|
||||
"type": "string",
|
||||
"metavar": "<file.dot>",
|
||||
"help": "Create a graph of external dependencies in the"
|
||||
" given file (report RP0402 must not be disabled).",
|
||||
},
|
||||
),
|
||||
(
|
||||
"int-import-graph",
|
||||
{
|
||||
"default": "",
|
||||
"type": "string",
|
||||
"metavar": "<file.dot>",
|
||||
"help": "Create a graph of internal dependencies in the"
|
||||
" given file (report RP0402 must not be disabled).",
|
||||
},
|
||||
),
|
||||
(
|
||||
"known-standard-library",
|
||||
{
|
||||
"default": DEFAULT_STANDARD_LIBRARY,
|
||||
"type": "csv",
|
||||
"metavar": "<modules>",
|
||||
"help": "Force import order to recognize a module as part of "
|
||||
"the standard compatibility libraries.",
|
||||
},
|
||||
),
|
||||
(
|
||||
"known-third-party",
|
||||
{
|
||||
"default": DEFAULT_KNOWN_THIRD_PARTY,
|
||||
"type": "csv",
|
||||
"metavar": "<modules>",
|
||||
"help": "Force import order to recognize a module as part of "
|
||||
"a third party library.",
|
||||
},
|
||||
),
|
||||
(
|
||||
"allow-any-import-level",
|
||||
{
|
||||
"default": (),
|
||||
"type": "csv",
|
||||
"metavar": "<modules>",
|
||||
"help": (
|
||||
"List of modules that can be imported at any level, not just "
|
||||
"the top level one."
|
||||
),
|
||||
},
|
||||
),
|
||||
(
|
||||
"analyse-fallback-blocks",
|
||||
{
|
||||
"default": False,
|
||||
"type": "yn",
|
||||
"metavar": "<y_or_n>",
|
||||
"help": "Analyse import fallback blocks. This can be used to "
|
||||
"support both Python 2 and 3 compatible code, which "
|
||||
"means that the block might have code that exists "
|
||||
"only in one or another interpreter, leading to false "
|
||||
"positives when analysed.",
|
||||
},
|
||||
),
|
||||
(
|
||||
"allow-wildcard-with-all",
|
||||
{
|
||||
"default": False,
|
||||
"type": "yn",
|
||||
"metavar": "<y_or_n>",
|
||||
"help": "Allow wildcard imports from modules that define __all__.",
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
def __init__(self, linter=None):
|
||||
BaseChecker.__init__(self, linter)
|
||||
self.stats = None
|
||||
self.import_graph = None
|
||||
self._imports_stack = []
|
||||
self._first_non_import_node = None
|
||||
self._module_pkg = {} # mapping of modules to the pkg they belong in
|
||||
self._allow_any_import_level = set()
|
||||
self.reports = (
|
||||
("RP0401", "External dependencies", self._report_external_dependencies),
|
||||
("RP0402", "Modules dependencies graph", self._report_dependencies_graph),
|
||||
)
|
||||
|
||||
self._site_packages = self._compute_site_packages()
|
||||
|
||||
@staticmethod
|
||||
def _compute_site_packages():
|
||||
def _normalized_path(path):
|
||||
return os.path.normcase(os.path.abspath(path))
|
||||
|
||||
paths = set()
|
||||
real_prefix = getattr(sys, "real_prefix", None)
|
||||
for prefix in filter(None, (real_prefix, sys.prefix)):
|
||||
path = sysconfig.get_python_lib(prefix=prefix)
|
||||
path = _normalized_path(path)
|
||||
paths.add(path)
|
||||
|
||||
# Handle Debian's derivatives /usr/local.
|
||||
if os.path.isfile("/etc/debian_version"):
|
||||
for prefix in filter(None, (real_prefix, sys.prefix)):
|
||||
libpython = os.path.join(
|
||||
prefix,
|
||||
"local",
|
||||
"lib",
|
||||
"python" + sysconfig.get_python_version(),
|
||||
"dist-packages",
|
||||
)
|
||||
paths.add(libpython)
|
||||
return paths
|
||||
|
||||
def open(self):
|
||||
"""called before visiting project (i.e set of modules)"""
|
||||
self.linter.add_stats(dependencies={})
|
||||
self.linter.add_stats(cycles=[])
|
||||
self.stats = self.linter.stats
|
||||
self.import_graph = collections.defaultdict(set)
|
||||
self._module_pkg = {} # mapping of modules to the pkg they belong in
|
||||
self._excluded_edges = collections.defaultdict(set)
|
||||
self._ignored_modules = get_global_option(self, "ignored-modules", default=[])
|
||||
# Build a mapping {'module': 'preferred-module'}
|
||||
self.preferred_modules = dict(
|
||||
module.split(":")
|
||||
for module in self.config.preferred_modules
|
||||
if ":" in module
|
||||
)
|
||||
self._allow_any_import_level = set(self.config.allow_any_import_level)
|
||||
|
||||
def _import_graph_without_ignored_edges(self):
|
||||
filtered_graph = copy.deepcopy(self.import_graph)
|
||||
for node in filtered_graph:
|
||||
filtered_graph[node].difference_update(self._excluded_edges[node])
|
||||
return filtered_graph
|
||||
|
||||
def close(self):
|
||||
"""called before visiting project (i.e set of modules)"""
|
||||
if self.linter.is_message_enabled("cyclic-import"):
|
||||
graph = self._import_graph_without_ignored_edges()
|
||||
vertices = list(graph)
|
||||
for cycle in get_cycles(graph, vertices=vertices):
|
||||
self.add_message("cyclic-import", args=" -> ".join(cycle))
|
||||
|
||||
@check_messages(*MSGS)
|
||||
def visit_import(self, node):
|
||||
"""triggered when an import statement is seen"""
|
||||
self._check_reimport(node)
|
||||
self._check_import_as_rename(node)
|
||||
self._check_toplevel(node)
|
||||
|
||||
names = [name for name, _ in node.names]
|
||||
if len(names) >= 2:
|
||||
self.add_message("multiple-imports", args=", ".join(names), node=node)
|
||||
|
||||
for name in names:
|
||||
self._check_deprecated_module(node, name)
|
||||
self._check_preferred_module(node, name)
|
||||
imported_module = self._get_imported_module(node, name)
|
||||
if isinstance(node.parent, astroid.Module):
|
||||
# Allow imports nested
|
||||
self._check_position(node)
|
||||
if isinstance(node.scope(), astroid.Module):
|
||||
self._record_import(node, imported_module)
|
||||
|
||||
if imported_module is None:
|
||||
continue
|
||||
|
||||
self._add_imported_module(node, imported_module.name)
|
||||
|
||||
@check_messages(*MSGS)
|
||||
def visit_importfrom(self, node):
|
||||
"""triggered when a from statement is seen"""
|
||||
basename = node.modname
|
||||
imported_module = self._get_imported_module(node, basename)
|
||||
|
||||
self._check_import_as_rename(node)
|
||||
self._check_misplaced_future(node)
|
||||
self._check_deprecated_module(node, basename)
|
||||
self._check_preferred_module(node, basename)
|
||||
self._check_wildcard_imports(node, imported_module)
|
||||
self._check_same_line_imports(node)
|
||||
self._check_reimport(node, basename=basename, level=node.level)
|
||||
self._check_toplevel(node)
|
||||
|
||||
if isinstance(node.parent, astroid.Module):
|
||||
# Allow imports nested
|
||||
self._check_position(node)
|
||||
if isinstance(node.scope(), astroid.Module):
|
||||
self._record_import(node, imported_module)
|
||||
if imported_module is None:
|
||||
return
|
||||
for name, _ in node.names:
|
||||
if name != "*":
|
||||
self._add_imported_module(node, "%s.%s" % (imported_module.name, name))
|
||||
else:
|
||||
self._add_imported_module(node, imported_module.name)
|
||||
|
||||
@check_messages(*MSGS)
|
||||
def leave_module(self, node):
|
||||
# Check imports are grouped by category (standard, 3rd party, local)
|
||||
std_imports, ext_imports, loc_imports = self._check_imports_order(node)
|
||||
|
||||
# Check that imports are grouped by package within a given category
|
||||
met_import = set() # set for 'import x' style
|
||||
met_from = set() # set for 'from x import y' style
|
||||
current_package = None
|
||||
for import_node, import_name in std_imports + ext_imports + loc_imports:
|
||||
if not self.linter.is_message_enabled(
|
||||
"ungrouped-imports", import_node.fromlineno
|
||||
):
|
||||
continue
|
||||
if isinstance(import_node, astroid.node_classes.ImportFrom):
|
||||
met = met_from
|
||||
else:
|
||||
met = met_import
|
||||
package, _, _ = import_name.partition(".")
|
||||
if current_package and current_package != package and package in met:
|
||||
self.add_message("ungrouped-imports", node=import_node, args=package)
|
||||
current_package = package
|
||||
met.add(package)
|
||||
|
||||
self._imports_stack = []
|
||||
self._first_non_import_node = None
|
||||
|
||||
def compute_first_non_import_node(self, node):
|
||||
if not self.linter.is_message_enabled("wrong-import-position", node.fromlineno):
|
||||
return
|
||||
# if the node does not contain an import instruction, and if it is the
|
||||
# first node of the module, keep a track of it (all the import positions
|
||||
# of the module will be compared to the position of this first
|
||||
# instruction)
|
||||
if self._first_non_import_node:
|
||||
return
|
||||
if not isinstance(node.parent, astroid.Module):
|
||||
return
|
||||
nested_allowed = [astroid.TryExcept, astroid.TryFinally]
|
||||
is_nested_allowed = [
|
||||
allowed for allowed in nested_allowed if isinstance(node, allowed)
|
||||
]
|
||||
if is_nested_allowed and any(
|
||||
node.nodes_of_class((astroid.Import, astroid.ImportFrom))
|
||||
):
|
||||
return
|
||||
if isinstance(node, astroid.Assign):
|
||||
# Add compatibility for module level dunder names
|
||||
# https://www.python.org/dev/peps/pep-0008/#module-level-dunder-names
|
||||
valid_targets = [
|
||||
isinstance(target, astroid.AssignName)
|
||||
and target.name.startswith("__")
|
||||
and target.name.endswith("__")
|
||||
for target in node.targets
|
||||
]
|
||||
if all(valid_targets):
|
||||
return
|
||||
self._first_non_import_node = node
|
||||
|
||||
visit_tryfinally = (
|
||||
visit_tryexcept
|
||||
) = (
|
||||
visit_assignattr
|
||||
) = (
|
||||
visit_assign
|
||||
) = (
|
||||
visit_ifexp
|
||||
) = visit_comprehension = visit_expr = visit_if = compute_first_non_import_node
|
||||
|
||||
def visit_functiondef(self, node):
|
||||
if not self.linter.is_message_enabled("wrong-import-position", node.fromlineno):
|
||||
return
|
||||
# If it is the first non import instruction of the module, record it.
|
||||
if self._first_non_import_node:
|
||||
return
|
||||
|
||||
# Check if the node belongs to an `If` or a `Try` block. If they
|
||||
# contain imports, skip recording this node.
|
||||
if not isinstance(node.parent.scope(), astroid.Module):
|
||||
return
|
||||
|
||||
root = node
|
||||
while not isinstance(root.parent, astroid.Module):
|
||||
root = root.parent
|
||||
|
||||
if isinstance(root, (astroid.If, astroid.TryFinally, astroid.TryExcept)):
|
||||
if any(root.nodes_of_class((astroid.Import, astroid.ImportFrom))):
|
||||
return
|
||||
|
||||
self._first_non_import_node = node
|
||||
|
||||
visit_classdef = visit_for = visit_while = visit_functiondef
|
||||
|
||||
def _check_misplaced_future(self, node):
|
||||
basename = node.modname
|
||||
if basename == "__future__":
|
||||
# check if this is the first non-docstring statement in the module
|
||||
prev = node.previous_sibling()
|
||||
if prev:
|
||||
# consecutive future statements are possible
|
||||
if not (
|
||||
isinstance(prev, astroid.ImportFrom)
|
||||
and prev.modname == "__future__"
|
||||
):
|
||||
self.add_message("misplaced-future", node=node)
|
||||
return
|
||||
|
||||
def _check_same_line_imports(self, node):
|
||||
# Detect duplicate imports on the same line.
|
||||
names = (name for name, _ in node.names)
|
||||
counter = collections.Counter(names)
|
||||
for name, count in counter.items():
|
||||
if count > 1:
|
||||
self.add_message("reimported", node=node, args=(name, node.fromlineno))
|
||||
|
||||
def _check_position(self, node):
|
||||
"""Check `node` import or importfrom node position is correct
|
||||
|
||||
Send a message if `node` comes before another instruction
|
||||
"""
|
||||
# if a first non-import instruction has already been encountered,
|
||||
# it means the import comes after it and therefore is not well placed
|
||||
if self._first_non_import_node:
|
||||
self.add_message("wrong-import-position", node=node, args=node.as_string())
|
||||
|
||||
def _record_import(self, node, importedmodnode):
|
||||
"""Record the package `node` imports from"""
|
||||
if isinstance(node, astroid.ImportFrom):
|
||||
importedname = node.modname
|
||||
else:
|
||||
importedname = importedmodnode.name if importedmodnode else None
|
||||
if not importedname:
|
||||
importedname = node.names[0][0].split(".")[0]
|
||||
|
||||
if isinstance(node, astroid.ImportFrom) and (node.level or 0) >= 1:
|
||||
# We need the importedname with first point to detect local package
|
||||
# Example of node:
|
||||
# 'from .my_package1 import MyClass1'
|
||||
# the output should be '.my_package1' instead of 'my_package1'
|
||||
# Example of node:
|
||||
# 'from . import my_package2'
|
||||
# the output should be '.my_package2' instead of '{pyfile}'
|
||||
importedname = "." + importedname
|
||||
|
||||
self._imports_stack.append((node, importedname))
|
||||
|
||||
@staticmethod
|
||||
def _is_fallback_import(node, imports):
|
||||
imports = [import_node for (import_node, _) in imports]
|
||||
return any(astroid.are_exclusive(import_node, node) for import_node in imports)
|
||||
|
||||
def _check_imports_order(self, _module_node):
|
||||
"""Checks imports of module `node` are grouped by category
|
||||
|
||||
Imports must follow this order: standard, 3rd party, local
|
||||
"""
|
||||
std_imports = []
|
||||
third_party_imports = []
|
||||
first_party_imports = []
|
||||
# need of a list that holds third or first party ordered import
|
||||
external_imports = []
|
||||
local_imports = []
|
||||
third_party_not_ignored = []
|
||||
first_party_not_ignored = []
|
||||
local_not_ignored = []
|
||||
isort_obj = isort.SortImports(
|
||||
file_contents="",
|
||||
known_third_party=self.config.known_third_party,
|
||||
known_standard_library=self.config.known_standard_library,
|
||||
)
|
||||
for node, modname in self._imports_stack:
|
||||
if modname.startswith("."):
|
||||
package = "." + modname.split(".")[1]
|
||||
else:
|
||||
package = modname.split(".")[0]
|
||||
nested = not isinstance(node.parent, astroid.Module)
|
||||
ignore_for_import_order = not self.linter.is_message_enabled(
|
||||
"wrong-import-order", node.fromlineno
|
||||
)
|
||||
import_category = isort_obj.place_module(package)
|
||||
node_and_package_import = (node, package)
|
||||
if import_category in ("FUTURE", "STDLIB"):
|
||||
std_imports.append(node_and_package_import)
|
||||
wrong_import = (
|
||||
third_party_not_ignored
|
||||
or first_party_not_ignored
|
||||
or local_not_ignored
|
||||
)
|
||||
if self._is_fallback_import(node, wrong_import):
|
||||
continue
|
||||
if wrong_import and not nested:
|
||||
self.add_message(
|
||||
"wrong-import-order",
|
||||
node=node,
|
||||
args=(
|
||||
'standard import "%s"' % node.as_string(),
|
||||
'"%s"' % wrong_import[0][0].as_string(),
|
||||
),
|
||||
)
|
||||
elif import_category == "THIRDPARTY":
|
||||
third_party_imports.append(node_and_package_import)
|
||||
external_imports.append(node_and_package_import)
|
||||
if not nested and not ignore_for_import_order:
|
||||
third_party_not_ignored.append(node_and_package_import)
|
||||
wrong_import = first_party_not_ignored or local_not_ignored
|
||||
if wrong_import and not nested:
|
||||
self.add_message(
|
||||
"wrong-import-order",
|
||||
node=node,
|
||||
args=(
|
||||
'third party import "%s"' % node.as_string(),
|
||||
'"%s"' % wrong_import[0][0].as_string(),
|
||||
),
|
||||
)
|
||||
elif import_category == "FIRSTPARTY":
|
||||
first_party_imports.append(node_and_package_import)
|
||||
external_imports.append(node_and_package_import)
|
||||
if not nested and not ignore_for_import_order:
|
||||
first_party_not_ignored.append(node_and_package_import)
|
||||
wrong_import = local_not_ignored
|
||||
if wrong_import and not nested:
|
||||
self.add_message(
|
||||
"wrong-import-order",
|
||||
node=node,
|
||||
args=(
|
||||
'first party import "%s"' % node.as_string(),
|
||||
'"%s"' % wrong_import[0][0].as_string(),
|
||||
),
|
||||
)
|
||||
elif import_category == "LOCALFOLDER":
|
||||
local_imports.append((node, package))
|
||||
if not nested and not ignore_for_import_order:
|
||||
local_not_ignored.append((node, package))
|
||||
return std_imports, external_imports, local_imports
|
||||
|
||||
def _get_imported_module(self, importnode, modname):
|
||||
try:
|
||||
return importnode.do_import_module(modname)
|
||||
except astroid.TooManyLevelsError:
|
||||
if _ignore_import_failure(importnode, modname, self._ignored_modules):
|
||||
return None
|
||||
self.add_message("relative-beyond-top-level", node=importnode)
|
||||
except astroid.AstroidSyntaxError as exc:
|
||||
message = "Cannot import {!r} due to syntax error {!r}".format(
|
||||
modname, str(exc.error) # pylint: disable=no-member; false positive
|
||||
)
|
||||
self.add_message("syntax-error", line=importnode.lineno, args=message)
|
||||
|
||||
except astroid.AstroidBuildingException:
|
||||
if not self.linter.is_message_enabled("import-error"):
|
||||
return None
|
||||
if _ignore_import_failure(importnode, modname, self._ignored_modules):
|
||||
return None
|
||||
if not self.config.analyse_fallback_blocks and is_from_fallback_block(
|
||||
importnode
|
||||
):
|
||||
return None
|
||||
|
||||
dotted_modname = _get_import_name(importnode, modname)
|
||||
self.add_message("import-error", args=repr(dotted_modname), node=importnode)
|
||||
|
||||
def _add_imported_module(self, node, importedmodname):
|
||||
"""notify an imported module, used to analyze dependencies"""
|
||||
module_file = node.root().file
|
||||
context_name = node.root().name
|
||||
base = os.path.splitext(os.path.basename(module_file))[0]
|
||||
|
||||
try:
|
||||
importedmodname = modutils.get_module_part(importedmodname, module_file)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
if context_name == importedmodname:
|
||||
self.add_message("import-self", node=node)
|
||||
|
||||
elif not modutils.is_standard_module(importedmodname):
|
||||
# if this is not a package __init__ module
|
||||
if base != "__init__" and context_name not in self._module_pkg:
|
||||
# record the module's parent, or the module itself if this is
|
||||
# a top level module, as the package it belongs to
|
||||
self._module_pkg[context_name] = context_name.rsplit(".", 1)[0]
|
||||
|
||||
# handle dependencies
|
||||
importedmodnames = self.stats["dependencies"].setdefault(
|
||||
importedmodname, set()
|
||||
)
|
||||
if context_name not in importedmodnames:
|
||||
importedmodnames.add(context_name)
|
||||
|
||||
# update import graph
|
||||
self.import_graph[context_name].add(importedmodname)
|
||||
if not self.linter.is_message_enabled("cyclic-import", line=node.lineno):
|
||||
self._excluded_edges[context_name].add(importedmodname)
|
||||
|
||||
def _check_deprecated_module(self, node, mod_path):
|
||||
"""check if the module is deprecated"""
|
||||
for mod_name in self.config.deprecated_modules:
|
||||
if mod_path == mod_name or mod_path.startswith(mod_name + "."):
|
||||
self.add_message("deprecated-module", node=node, args=mod_path)
|
||||
|
||||
def _check_preferred_module(self, node, mod_path):
|
||||
"""check if the module has a preferred replacement"""
|
||||
if mod_path in self.preferred_modules:
|
||||
self.add_message(
|
||||
"preferred-module",
|
||||
node=node,
|
||||
args=(self.preferred_modules[mod_path], mod_path),
|
||||
)
|
||||
|
||||
def _check_import_as_rename(self, node):
|
||||
names = node.names
|
||||
for name in names:
|
||||
if not all(name):
|
||||
return
|
||||
|
||||
real_name = name[0]
|
||||
splitted_packages = real_name.rsplit(".")
|
||||
real_name = splitted_packages[-1]
|
||||
imported_name = name[1]
|
||||
# consider only following cases
|
||||
# import x as x
|
||||
# and ignore following
|
||||
# import x.y.z as z
|
||||
if real_name == imported_name and len(splitted_packages) == 1:
|
||||
self.add_message("useless-import-alias", node=node)
|
||||
|
||||
def _check_reimport(self, node, basename=None, level=None):
|
||||
"""check if the import is necessary (i.e. not already done)"""
|
||||
if not self.linter.is_message_enabled("reimported"):
|
||||
return
|
||||
|
||||
frame = node.frame()
|
||||
root = node.root()
|
||||
contexts = [(frame, level)]
|
||||
if root is not frame:
|
||||
contexts.append((root, None))
|
||||
|
||||
for known_context, known_level in contexts:
|
||||
for name, alias in node.names:
|
||||
first = _get_first_import(
|
||||
node, known_context, name, basename, known_level, alias
|
||||
)
|
||||
if first is not None:
|
||||
self.add_message(
|
||||
"reimported", node=node, args=(name, first.fromlineno)
|
||||
)
|
||||
|
||||
def _report_external_dependencies(self, sect, _, _dummy):
|
||||
"""return a verbatim layout for displaying dependencies"""
|
||||
dep_info = _make_tree_defs(self._external_dependencies_info().items())
|
||||
if not dep_info:
|
||||
raise EmptyReportError()
|
||||
tree_str = _repr_tree_defs(dep_info)
|
||||
sect.append(VerbatimText(tree_str))
|
||||
|
||||
def _report_dependencies_graph(self, sect, _, _dummy):
|
||||
"""write dependencies as a dot (graphviz) file"""
|
||||
dep_info = self.stats["dependencies"]
|
||||
if not dep_info or not (
|
||||
self.config.import_graph
|
||||
or self.config.ext_import_graph
|
||||
or self.config.int_import_graph
|
||||
):
|
||||
raise EmptyReportError()
|
||||
filename = self.config.import_graph
|
||||
if filename:
|
||||
_make_graph(filename, dep_info, sect, "")
|
||||
filename = self.config.ext_import_graph
|
||||
if filename:
|
||||
_make_graph(filename, self._external_dependencies_info(), sect, "external ")
|
||||
filename = self.config.int_import_graph
|
||||
if filename:
|
||||
_make_graph(filename, self._internal_dependencies_info(), sect, "internal ")
|
||||
|
||||
def _filter_dependencies_graph(self, internal):
|
||||
"""build the internal or the external dependency graph"""
|
||||
graph = collections.defaultdict(set)
|
||||
for importee, importers in self.stats["dependencies"].items():
|
||||
for importer in importers:
|
||||
package = self._module_pkg.get(importer, importer)
|
||||
is_inside = importee.startswith(package)
|
||||
if is_inside and internal or not is_inside and not internal:
|
||||
graph[importee].add(importer)
|
||||
return graph
|
||||
|
||||
@cached
|
||||
def _external_dependencies_info(self):
|
||||
"""return cached external dependencies information or build and
|
||||
cache them
|
||||
"""
|
||||
return self._filter_dependencies_graph(internal=False)
|
||||
|
||||
@cached
|
||||
def _internal_dependencies_info(self):
|
||||
"""return cached internal dependencies information or build and
|
||||
cache them
|
||||
"""
|
||||
return self._filter_dependencies_graph(internal=True)
|
||||
|
||||
def _check_wildcard_imports(self, node, imported_module):
|
||||
if node.root().package:
|
||||
# Skip the check if in __init__.py issue #2026
|
||||
return
|
||||
|
||||
wildcard_import_is_allowed = self._wildcard_import_is_allowed(imported_module)
|
||||
for name, _ in node.names:
|
||||
if name == "*" and not wildcard_import_is_allowed:
|
||||
self.add_message("wildcard-import", args=node.modname, node=node)
|
||||
|
||||
def _wildcard_import_is_allowed(self, imported_module):
|
||||
return (
|
||||
self.config.allow_wildcard_with_all
|
||||
and imported_module is not None
|
||||
and "__all__" in imported_module.locals
|
||||
)
|
||||
|
||||
def _check_toplevel(self, node):
|
||||
"""Check whether the import is made outside the module toplevel.
|
||||
"""
|
||||
# If the scope of the import is a module, then obviously it is
|
||||
# not outside the module toplevel.
|
||||
if isinstance(node.scope(), astroid.Module):
|
||||
return
|
||||
|
||||
module_names = [
|
||||
"{}.{}".format(node.modname, name[0])
|
||||
if isinstance(node, astroid.ImportFrom)
|
||||
else name[0]
|
||||
for name in node.names
|
||||
]
|
||||
|
||||
# Get the full names of all the imports that are not whitelisted.
|
||||
scoped_imports = [
|
||||
name for name in module_names if name not in self._allow_any_import_level
|
||||
]
|
||||
|
||||
if scoped_imports:
|
||||
self.add_message(
|
||||
"import-outside-toplevel", args=", ".join(scoped_imports), node=node
|
||||
)
|
||||
|
||||
|
||||
def register(linter):
|
||||
"""required method to auto register this checker """
|
||||
linter.register_checker(ImportsChecker(linter))
|
||||
@@ -0,0 +1,415 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2009-2011, 2013-2014 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
|
||||
# Copyright (c) 2009, 2012, 2014 Google, Inc.
|
||||
# Copyright (c) 2012 Mike Bryant <leachim@leachim.info>
|
||||
# Copyright (c) 2014 Brett Cannon <brett@python.org>
|
||||
# Copyright (c) 2014 Arun Persaud <arun@nubati.net>
|
||||
# Copyright (c) 2015-2020 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
|
||||
# Copyright (c) 2016, 2019-2020 Ashley Whetter <ashley@awhetter.co.uk>
|
||||
# Copyright (c) 2016 Chris Murray <chris@chrismurray.scot>
|
||||
# Copyright (c) 2017 guillaume2 <guillaume.peillex@gmail.col>
|
||||
# Copyright (c) 2017 Łukasz Rogalski <rogalski.91@gmail.com>
|
||||
# Copyright (c) 2018 Alan Chan <achan961117@gmail.com>
|
||||
# Copyright (c) 2018 Yury Gribov <tetra2005@gmail.com>
|
||||
# Copyright (c) 2018 Mike Frysinger <vapier@gmail.com>
|
||||
# Copyright (c) 2018 Mariatta Wijaya <mariatta@python.org>
|
||||
# Copyright (c) 2019 Djailla <bastien.vallet@gmail.com>
|
||||
# Copyright (c) 2019 Pierre Sassoulas <pierre.sassoulas@gmail.com>
|
||||
# Copyright (c) 2019 Svet <svet@hyperscience.com>
|
||||
# Copyright (c) 2020 Anthony Sottile <asottile@umich.edu>
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""checker for use of Python logging
|
||||
"""
|
||||
import string
|
||||
|
||||
import astroid
|
||||
|
||||
from pylint import checkers, interfaces
|
||||
from pylint.checkers import utils
|
||||
from pylint.checkers.utils import check_messages
|
||||
|
||||
MSGS = {
|
||||
"W1201": (
|
||||
"Use %s formatting in logging functions",
|
||||
"logging-not-lazy",
|
||||
"Used when a logging statement has a call form of "
|
||||
'"logging.<logging method>(format_string % (format_args...))". '
|
||||
"Use another type of string formatting instead. "
|
||||
"You can use % formatting but leave interpolation to "
|
||||
"the logging function by passing the parameters as arguments. "
|
||||
"If logging-fstring-interpolation is disabled then "
|
||||
"you can use fstring formatting. "
|
||||
"If logging-format-interpolation is disabled then "
|
||||
"you can use str.format.",
|
||||
),
|
||||
"W1202": (
|
||||
"Use %s formatting in logging functions",
|
||||
"logging-format-interpolation",
|
||||
"Used when a logging statement has a call form of "
|
||||
'"logging.<logging method>(format_string.format(format_args...))". '
|
||||
"Use another type of string formatting instead. "
|
||||
"You can use % formatting but leave interpolation to "
|
||||
"the logging function by passing the parameters as arguments. "
|
||||
"If logging-fstring-interpolation is disabled then "
|
||||
"you can use fstring formatting. "
|
||||
"If logging-not-lazy is disabled then "
|
||||
"you can use % formatting as normal.",
|
||||
),
|
||||
"W1203": (
|
||||
"Use %s formatting in logging functions",
|
||||
"logging-fstring-interpolation",
|
||||
"Used when a logging statement has a call form of "
|
||||
'"logging.<logging method>(f"...")".'
|
||||
"Use another type of string formatting instead. "
|
||||
"You can use % formatting but leave interpolation to "
|
||||
"the logging function by passing the parameters as arguments. "
|
||||
"If logging-format-interpolation is disabled then "
|
||||
"you can use str.format. "
|
||||
"If logging-not-lazy is disabled then "
|
||||
"you can use % formatting as normal.",
|
||||
),
|
||||
"E1200": (
|
||||
"Unsupported logging format character %r (%#02x) at index %d",
|
||||
"logging-unsupported-format",
|
||||
"Used when an unsupported format character is used in a logging "
|
||||
"statement format string.",
|
||||
),
|
||||
"E1201": (
|
||||
"Logging format string ends in middle of conversion specifier",
|
||||
"logging-format-truncated",
|
||||
"Used when a logging statement format string terminates before "
|
||||
"the end of a conversion specifier.",
|
||||
),
|
||||
"E1205": (
|
||||
"Too many arguments for logging format string",
|
||||
"logging-too-many-args",
|
||||
"Used when a logging format string is given too many arguments.",
|
||||
),
|
||||
"E1206": (
|
||||
"Not enough arguments for logging format string",
|
||||
"logging-too-few-args",
|
||||
"Used when a logging format string is given too few arguments.",
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
CHECKED_CONVENIENCE_FUNCTIONS = {
|
||||
"critical",
|
||||
"debug",
|
||||
"error",
|
||||
"exception",
|
||||
"fatal",
|
||||
"info",
|
||||
"warn",
|
||||
"warning",
|
||||
}
|
||||
|
||||
|
||||
def is_method_call(func, types=(), methods=()):
|
||||
"""Determines if a BoundMethod node represents a method call.
|
||||
|
||||
Args:
|
||||
func (astroid.BoundMethod): The BoundMethod AST node to check.
|
||||
types (Optional[String]): Optional sequence of caller type names to restrict check.
|
||||
methods (Optional[String]): Optional sequence of method names to restrict check.
|
||||
|
||||
Returns:
|
||||
bool: true if the node represents a method call for the given type and
|
||||
method names, False otherwise.
|
||||
"""
|
||||
return (
|
||||
isinstance(func, astroid.BoundMethod)
|
||||
and isinstance(func.bound, astroid.Instance)
|
||||
and (func.bound.name in types if types else True)
|
||||
and (func.name in methods if methods else True)
|
||||
)
|
||||
|
||||
|
||||
class LoggingChecker(checkers.BaseChecker):
|
||||
"""Checks use of the logging module."""
|
||||
|
||||
__implements__ = interfaces.IAstroidChecker
|
||||
name = "logging"
|
||||
msgs = MSGS
|
||||
|
||||
options = (
|
||||
(
|
||||
"logging-modules",
|
||||
{
|
||||
"default": ("logging",),
|
||||
"type": "csv",
|
||||
"metavar": "<comma separated list>",
|
||||
"help": "Logging modules to check that the string format "
|
||||
"arguments are in logging function parameter format.",
|
||||
},
|
||||
),
|
||||
(
|
||||
"logging-format-style",
|
||||
{
|
||||
"default": "old",
|
||||
"type": "choice",
|
||||
"metavar": "<old (%) or new ({)>",
|
||||
"choices": ["old", "new"],
|
||||
"help": "The type of string formatting that logging methods do. "
|
||||
"`old` means using % formatting, `new` is for `{}` formatting.",
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
def visit_module(self, node): # pylint: disable=unused-argument
|
||||
"""Clears any state left in this checker from last module checked."""
|
||||
# The code being checked can just as easily "import logging as foo",
|
||||
# so it is necessary to process the imports and store in this field
|
||||
# what name the logging module is actually given.
|
||||
self._logging_names = set()
|
||||
logging_mods = self.config.logging_modules
|
||||
|
||||
self._format_style = self.config.logging_format_style
|
||||
|
||||
self._logging_modules = set(logging_mods)
|
||||
self._from_imports = {}
|
||||
for logging_mod in logging_mods:
|
||||
parts = logging_mod.rsplit(".", 1)
|
||||
if len(parts) > 1:
|
||||
self._from_imports[parts[0]] = parts[1]
|
||||
|
||||
def visit_importfrom(self, node):
|
||||
"""Checks to see if a module uses a non-Python logging module."""
|
||||
try:
|
||||
logging_name = self._from_imports[node.modname]
|
||||
for module, as_name in node.names:
|
||||
if module == logging_name:
|
||||
self._logging_names.add(as_name or module)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def visit_import(self, node):
|
||||
"""Checks to see if this module uses Python's built-in logging."""
|
||||
for module, as_name in node.names:
|
||||
if module in self._logging_modules:
|
||||
self._logging_names.add(as_name or module)
|
||||
|
||||
@check_messages(*MSGS)
|
||||
def visit_call(self, node):
|
||||
"""Checks calls to logging methods."""
|
||||
|
||||
def is_logging_name():
|
||||
return (
|
||||
isinstance(node.func, astroid.Attribute)
|
||||
and isinstance(node.func.expr, astroid.Name)
|
||||
and node.func.expr.name in self._logging_names
|
||||
)
|
||||
|
||||
def is_logger_class():
|
||||
try:
|
||||
for inferred in node.func.infer():
|
||||
if isinstance(inferred, astroid.BoundMethod):
|
||||
parent = inferred._proxied.parent
|
||||
if isinstance(parent, astroid.ClassDef) and (
|
||||
parent.qname() == "logging.Logger"
|
||||
or any(
|
||||
ancestor.qname() == "logging.Logger"
|
||||
for ancestor in parent.ancestors()
|
||||
)
|
||||
):
|
||||
return True, inferred._proxied.name
|
||||
except astroid.exceptions.InferenceError:
|
||||
pass
|
||||
return False, None
|
||||
|
||||
if is_logging_name():
|
||||
name = node.func.attrname
|
||||
else:
|
||||
result, name = is_logger_class()
|
||||
if not result:
|
||||
return
|
||||
self._check_log_method(node, name)
|
||||
|
||||
def _check_log_method(self, node, name):
|
||||
"""Checks calls to logging.log(level, format, *format_args)."""
|
||||
if name == "log":
|
||||
if node.starargs or node.kwargs or len(node.args) < 2:
|
||||
# Either a malformed call, star args, or double-star args. Beyond
|
||||
# the scope of this checker.
|
||||
return
|
||||
format_pos = 1
|
||||
elif name in CHECKED_CONVENIENCE_FUNCTIONS:
|
||||
if node.starargs or node.kwargs or not node.args:
|
||||
# Either no args, star args, or double-star args. Beyond the
|
||||
# scope of this checker.
|
||||
return
|
||||
format_pos = 0
|
||||
else:
|
||||
return
|
||||
|
||||
if isinstance(node.args[format_pos], astroid.BinOp):
|
||||
binop = node.args[format_pos]
|
||||
emit = binop.op == "%"
|
||||
if binop.op == "+":
|
||||
total_number_of_strings = sum(
|
||||
1
|
||||
for operand in (binop.left, binop.right)
|
||||
if self._is_operand_literal_str(utils.safe_infer(operand))
|
||||
)
|
||||
emit = total_number_of_strings > 0
|
||||
if emit:
|
||||
self.add_message(
|
||||
"logging-not-lazy", node=node, args=(self._helper_string(node),),
|
||||
)
|
||||
elif isinstance(node.args[format_pos], astroid.Call):
|
||||
self._check_call_func(node.args[format_pos])
|
||||
elif isinstance(node.args[format_pos], astroid.Const):
|
||||
self._check_format_string(node, format_pos)
|
||||
elif isinstance(node.args[format_pos], astroid.JoinedStr):
|
||||
self.add_message(
|
||||
"logging-fstring-interpolation",
|
||||
node=node,
|
||||
args=(self._helper_string(node),),
|
||||
)
|
||||
|
||||
def _helper_string(self, node):
|
||||
"""Create a string that lists the valid types of formatting for this node."""
|
||||
valid_types = ["lazy %"]
|
||||
|
||||
if not self.linter.is_message_enabled(
|
||||
"logging-fstring-formatting", node.fromlineno
|
||||
):
|
||||
valid_types.append("fstring")
|
||||
if not self.linter.is_message_enabled(
|
||||
"logging-format-interpolation", node.fromlineno
|
||||
):
|
||||
valid_types.append(".format()")
|
||||
if not self.linter.is_message_enabled("logging-not-lazy", node.fromlineno):
|
||||
valid_types.append("%")
|
||||
|
||||
return " or ".join(valid_types)
|
||||
|
||||
@staticmethod
|
||||
def _is_operand_literal_str(operand):
|
||||
"""
|
||||
Return True if the operand in argument is a literal string
|
||||
"""
|
||||
return isinstance(operand, astroid.Const) and operand.name == "str"
|
||||
|
||||
def _check_call_func(self, node):
|
||||
"""Checks that function call is not format_string.format().
|
||||
|
||||
Args:
|
||||
node (astroid.node_classes.Call):
|
||||
Call AST node to be checked.
|
||||
"""
|
||||
func = utils.safe_infer(node.func)
|
||||
types = ("str", "unicode")
|
||||
methods = ("format",)
|
||||
if is_method_call(func, types, methods) and not is_complex_format_str(
|
||||
func.bound
|
||||
):
|
||||
self.add_message(
|
||||
"logging-format-interpolation",
|
||||
node=node,
|
||||
args=(self._helper_string(node),),
|
||||
)
|
||||
|
||||
def _check_format_string(self, node, format_arg):
|
||||
"""Checks that format string tokens match the supplied arguments.
|
||||
|
||||
Args:
|
||||
node (astroid.node_classes.NodeNG): AST node to be checked.
|
||||
format_arg (int): Index of the format string in the node arguments.
|
||||
"""
|
||||
num_args = _count_supplied_tokens(node.args[format_arg + 1 :])
|
||||
if not num_args:
|
||||
# If no args were supplied the string is not interpolated and can contain
|
||||
# formatting characters - it's used verbatim. Don't check any further.
|
||||
return
|
||||
|
||||
format_string = node.args[format_arg].value
|
||||
required_num_args = 0
|
||||
if isinstance(format_string, bytes):
|
||||
format_string = format_string.decode()
|
||||
if isinstance(format_string, str):
|
||||
try:
|
||||
if self._format_style == "old":
|
||||
keyword_args, required_num_args, _, _ = utils.parse_format_string(
|
||||
format_string
|
||||
)
|
||||
if keyword_args:
|
||||
# Keyword checking on logging strings is complicated by
|
||||
# special keywords - out of scope.
|
||||
return
|
||||
elif self._format_style == "new":
|
||||
(
|
||||
keyword_arguments,
|
||||
implicit_pos_args,
|
||||
explicit_pos_args,
|
||||
) = utils.parse_format_method_string(format_string)
|
||||
|
||||
keyword_args_cnt = len(
|
||||
{k for k, l in keyword_arguments if not isinstance(k, int)}
|
||||
)
|
||||
required_num_args = (
|
||||
keyword_args_cnt + implicit_pos_args + explicit_pos_args
|
||||
)
|
||||
except utils.UnsupportedFormatCharacter as ex:
|
||||
char = format_string[ex.index]
|
||||
self.add_message(
|
||||
"logging-unsupported-format",
|
||||
node=node,
|
||||
args=(char, ord(char), ex.index),
|
||||
)
|
||||
return
|
||||
except utils.IncompleteFormatString:
|
||||
self.add_message("logging-format-truncated", node=node)
|
||||
return
|
||||
if num_args > required_num_args:
|
||||
self.add_message("logging-too-many-args", node=node)
|
||||
elif num_args < required_num_args:
|
||||
self.add_message("logging-too-few-args", node=node)
|
||||
|
||||
|
||||
def is_complex_format_str(node):
|
||||
"""Checks if node represents a string with complex formatting specs.
|
||||
|
||||
Args:
|
||||
node (astroid.node_classes.NodeNG): AST node to check
|
||||
Returns:
|
||||
bool: True if inferred string uses complex formatting, False otherwise
|
||||
"""
|
||||
inferred = utils.safe_infer(node)
|
||||
if inferred is None or not (
|
||||
isinstance(inferred, astroid.Const) and isinstance(inferred.value, str)
|
||||
):
|
||||
return True
|
||||
try:
|
||||
parsed = list(string.Formatter().parse(inferred.value))
|
||||
except ValueError:
|
||||
# This format string is invalid
|
||||
return False
|
||||
for _, _, format_spec, _ in parsed:
|
||||
if format_spec:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _count_supplied_tokens(args):
|
||||
"""Counts the number of tokens in an args list.
|
||||
|
||||
The Python log functions allow for special keyword arguments: func,
|
||||
exc_info and extra. To handle these cases correctly, we only count
|
||||
arguments that aren't keywords.
|
||||
|
||||
Args:
|
||||
args (list): AST nodes that are arguments for a log format string.
|
||||
|
||||
Returns:
|
||||
int: Number of AST nodes that aren't keywords.
|
||||
"""
|
||||
return sum(1 for arg in args if not isinstance(arg, astroid.Keyword))
|
||||
|
||||
|
||||
def register(linter):
|
||||
"""Required method to auto-register this checker."""
|
||||
linter.register_checker(LoggingChecker(linter))
|
||||
@@ -0,0 +1,199 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2006, 2009-2013 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
|
||||
# Copyright (c) 2012-2014 Google, Inc.
|
||||
# Copyright (c) 2014-2019 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2014 Brett Cannon <brett@python.org>
|
||||
# Copyright (c) 2014 Alexandru Coman <fcoman@bitdefender.com>
|
||||
# Copyright (c) 2014 Arun Persaud <arun@nubati.net>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
|
||||
# Copyright (c) 2016 Łukasz Rogalski <rogalski.91@gmail.com>
|
||||
# Copyright (c) 2016 glegoux <gilles.legoux@gmail.com>
|
||||
# Copyright (c) 2017-2019 hippo91 <guillaume.peillex@gmail.com>
|
||||
# Copyright (c) 2017 Mikhail Fesenko <proggga@gmail.com>
|
||||
# Copyright (c) 2018 Rogalski, Lukasz <lukasz.rogalski@intel.com>
|
||||
# Copyright (c) 2018 Lucas Cimon <lucas.cimon@gmail.com>
|
||||
# Copyright (c) 2018 Ville Skyttä <ville.skytta@iki.fi>
|
||||
# Copyright (c) 2019-2020 Pierre Sassoulas <pierre.sassoulas@gmail.com>
|
||||
# Copyright (c) 2020 Anthony Sottile <asottile@umich.edu>
|
||||
# Copyright (c) 2020 Benny <benny.mueller91@gmail.com>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
|
||||
"""Check source code is ascii only or has an encoding declaration (PEP 263)"""
|
||||
|
||||
import re
|
||||
import tokenize
|
||||
|
||||
from pylint.checkers import BaseChecker
|
||||
from pylint.interfaces import IRawChecker, ITokenChecker
|
||||
from pylint.message import MessagesHandlerMixIn
|
||||
from pylint.utils.pragma_parser import OPTION_PO, PragmaParserError, parse_pragma
|
||||
|
||||
|
||||
class ByIdManagedMessagesChecker(BaseChecker):
|
||||
|
||||
"""checks for messages that are enabled or disabled by id instead of symbol."""
|
||||
|
||||
__implements__ = IRawChecker
|
||||
|
||||
# configuration section name
|
||||
name = "miscellaneous"
|
||||
msgs = {
|
||||
"I0023": (
|
||||
"%s",
|
||||
"use-symbolic-message-instead",
|
||||
"Used when a message is enabled or disabled by id.",
|
||||
)
|
||||
}
|
||||
|
||||
options = ()
|
||||
|
||||
def process_module(self, module):
|
||||
"""inspect the source file to find messages activated or deactivated by id."""
|
||||
managed_msgs = MessagesHandlerMixIn.get_by_id_managed_msgs()
|
||||
for (mod_name, msg_id, msg_symbol, lineno, is_disabled) in managed_msgs:
|
||||
if mod_name == module.name:
|
||||
if is_disabled:
|
||||
txt = "Id '{ident}' is used to disable '{symbol}' message emission".format(
|
||||
ident=msg_id, symbol=msg_symbol
|
||||
)
|
||||
else:
|
||||
txt = "Id '{ident}' is used to enable '{symbol}' message emission".format(
|
||||
ident=msg_id, symbol=msg_symbol
|
||||
)
|
||||
self.add_message("use-symbolic-message-instead", line=lineno, args=txt)
|
||||
MessagesHandlerMixIn.clear_by_id_managed_msgs()
|
||||
|
||||
|
||||
class EncodingChecker(BaseChecker):
|
||||
|
||||
"""checks for:
|
||||
* warning notes in the code like FIXME, XXX
|
||||
* encoding issues.
|
||||
"""
|
||||
|
||||
__implements__ = (IRawChecker, ITokenChecker)
|
||||
|
||||
# configuration section name
|
||||
name = "miscellaneous"
|
||||
msgs = {
|
||||
"W0511": (
|
||||
"%s",
|
||||
"fixme",
|
||||
"Used when a warning note as FIXME or XXX is detected.",
|
||||
)
|
||||
}
|
||||
|
||||
options = (
|
||||
(
|
||||
"notes",
|
||||
{
|
||||
"type": "csv",
|
||||
"metavar": "<comma separated values>",
|
||||
"default": ("FIXME", "XXX", "TODO"),
|
||||
"help": (
|
||||
"List of note tags to take in consideration, "
|
||||
"separated by a comma."
|
||||
),
|
||||
},
|
||||
),
|
||||
(
|
||||
"notes-rgx",
|
||||
{
|
||||
"type": "string",
|
||||
"metavar": "<regexp>",
|
||||
"help": "Regular expression of note tags to take in consideration.",
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
def open(self):
|
||||
super().open()
|
||||
|
||||
notes = "|".join(map(re.escape, self.config.notes))
|
||||
if self.config.notes_rgx:
|
||||
regex_string = r"#\s*(%s|%s)\b" % (notes, self.config.notes_rgx)
|
||||
else:
|
||||
regex_string = r"#\s*(%s)\b" % (notes)
|
||||
|
||||
self._fixme_pattern = re.compile(regex_string, re.I)
|
||||
|
||||
def _check_encoding(self, lineno, line, file_encoding):
|
||||
try:
|
||||
return line.decode(file_encoding)
|
||||
except UnicodeDecodeError:
|
||||
pass
|
||||
except LookupError:
|
||||
if line.startswith("#") and "coding" in line and file_encoding in line:
|
||||
self.add_message(
|
||||
"syntax-error",
|
||||
line=lineno,
|
||||
args='Cannot decode using encoding "{}",'
|
||||
" bad encoding".format(file_encoding),
|
||||
)
|
||||
|
||||
def process_module(self, module):
|
||||
"""inspect the source file to find encoding problem"""
|
||||
if module.file_encoding:
|
||||
encoding = module.file_encoding
|
||||
else:
|
||||
encoding = "ascii"
|
||||
|
||||
with module.stream() as stream:
|
||||
for lineno, line in enumerate(stream):
|
||||
self._check_encoding(lineno + 1, line, encoding)
|
||||
|
||||
def process_tokens(self, tokens):
|
||||
"""inspect the source to find fixme problems"""
|
||||
if not self.config.notes:
|
||||
return
|
||||
comments = (
|
||||
token_info for token_info in tokens if token_info.type == tokenize.COMMENT
|
||||
)
|
||||
for comment in comments:
|
||||
comment_text = comment.string[1:].lstrip() # trim '#' and whitespaces
|
||||
|
||||
# handle pylint disable clauses
|
||||
disable_option_match = OPTION_PO.search(comment_text)
|
||||
if disable_option_match:
|
||||
try:
|
||||
values = []
|
||||
try:
|
||||
for pragma_repr in (
|
||||
p_rep
|
||||
for p_rep in parse_pragma(disable_option_match.group(2))
|
||||
if p_rep.action == "disable"
|
||||
):
|
||||
values.extend(pragma_repr.messages)
|
||||
except PragmaParserError:
|
||||
# Printing useful information dealing with this error is done in the lint package
|
||||
pass
|
||||
values = [_val.upper() for _val in values]
|
||||
if set(values) & set(self.config.notes):
|
||||
continue
|
||||
except ValueError:
|
||||
self.add_message(
|
||||
"bad-inline-option",
|
||||
args=disable_option_match.group(1).strip(),
|
||||
line=comment.start[0],
|
||||
)
|
||||
continue
|
||||
|
||||
# emit warnings if necessary
|
||||
match = self._fixme_pattern.search("#" + comment_text.lower())
|
||||
if match:
|
||||
note = match.group(1)
|
||||
self.add_message(
|
||||
"fixme",
|
||||
col_offset=comment.string.lower().index(note.lower()),
|
||||
args=comment_text,
|
||||
line=comment.start[0],
|
||||
)
|
||||
|
||||
|
||||
def register(linter):
|
||||
"""required method to auto register this checker"""
|
||||
linter.register_checker(EncodingChecker(linter))
|
||||
linter.register_checker(ByIdManagedMessagesChecker(linter))
|
||||
@@ -0,0 +1,133 @@
|
||||
# Copyright (c) 2006, 2008-2011, 2013-2014 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
|
||||
# Copyright (c) 2012-2014 Google, Inc.
|
||||
# Copyright (c) 2013-2019 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2014 Michal Nowikowski <godfryd@gmail.com>
|
||||
# Copyright (c) 2014 Brett Cannon <brett@python.org>
|
||||
# Copyright (c) 2014 Arun Persaud <arun@nubati.net>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
|
||||
# Copyright (c) 2016 Alexander Todorov <atodorov@otb.bg>
|
||||
# Copyright (c) 2016 Jakub Wilk <jwilk@jwilk.net>
|
||||
# Copyright (c) 2018 Lucas Cimon <lucas.cimon@gmail.com>
|
||||
# Copyright (c) 2018 Natalie Serebryakova <natalie.serebryakova@Natalies-MacBook-Pro.local>
|
||||
# Copyright (c) 2018 ssolanki <sushobhitsolanki@gmail.com>
|
||||
# Copyright (c) 2019 Hugo van Kemenade <hugovk@users.noreply.github.com>
|
||||
# Copyright (c) 2019 Robert Schweizer <robert_schweizer@gmx.de>
|
||||
# Copyright (c) 2019 Pierre Sassoulas <pierre.sassoulas@gmail.com>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""check for new / old style related problems
|
||||
"""
|
||||
import astroid
|
||||
|
||||
from pylint.checkers import BaseChecker
|
||||
from pylint.checkers.utils import check_messages, has_known_bases, node_frame_class
|
||||
from pylint.interfaces import IAstroidChecker
|
||||
|
||||
MSGS = {
|
||||
"E1003": (
|
||||
"Bad first argument %r given to super()",
|
||||
"bad-super-call",
|
||||
"Used when another argument than the current class is given as "
|
||||
"first argument of the super builtin.",
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
class NewStyleConflictChecker(BaseChecker):
|
||||
"""checks for usage of new style capabilities on old style classes and
|
||||
other new/old styles conflicts problems
|
||||
* use of property, __slots__, super
|
||||
* "super" usage
|
||||
"""
|
||||
|
||||
__implements__ = (IAstroidChecker,)
|
||||
|
||||
# configuration section name
|
||||
name = "newstyle"
|
||||
# messages
|
||||
msgs = MSGS
|
||||
priority = -2
|
||||
# configuration options
|
||||
options = ()
|
||||
|
||||
@check_messages("bad-super-call")
|
||||
def visit_functiondef(self, node):
|
||||
"""check use of super"""
|
||||
# ignore actual functions or method within a new style class
|
||||
if not node.is_method():
|
||||
return
|
||||
klass = node.parent.frame()
|
||||
for stmt in node.nodes_of_class(astroid.Call):
|
||||
if node_frame_class(stmt) != node_frame_class(node):
|
||||
# Don't look down in other scopes.
|
||||
continue
|
||||
|
||||
expr = stmt.func
|
||||
if not isinstance(expr, astroid.Attribute):
|
||||
continue
|
||||
|
||||
call = expr.expr
|
||||
# skip the test if using super
|
||||
if not (
|
||||
isinstance(call, astroid.Call)
|
||||
and isinstance(call.func, astroid.Name)
|
||||
and call.func.name == "super"
|
||||
):
|
||||
continue
|
||||
|
||||
# super should not be used on an old style class
|
||||
if klass.newstyle or not has_known_bases(klass):
|
||||
# super first arg should not be the class
|
||||
if not call.args:
|
||||
continue
|
||||
|
||||
# calling super(type(self), self) can lead to recursion loop
|
||||
# in derived classes
|
||||
arg0 = call.args[0]
|
||||
if (
|
||||
isinstance(arg0, astroid.Call)
|
||||
and isinstance(arg0.func, astroid.Name)
|
||||
and arg0.func.name == "type"
|
||||
):
|
||||
self.add_message("bad-super-call", node=call, args=("type",))
|
||||
continue
|
||||
|
||||
# calling super(self.__class__, self) can lead to recursion loop
|
||||
# in derived classes
|
||||
if (
|
||||
len(call.args) >= 2
|
||||
and isinstance(call.args[1], astroid.Name)
|
||||
and call.args[1].name == "self"
|
||||
and isinstance(arg0, astroid.Attribute)
|
||||
and arg0.attrname == "__class__"
|
||||
):
|
||||
self.add_message(
|
||||
"bad-super-call", node=call, args=("self.__class__",)
|
||||
)
|
||||
continue
|
||||
|
||||
try:
|
||||
supcls = call.args and next(call.args[0].infer(), None)
|
||||
except astroid.InferenceError:
|
||||
continue
|
||||
|
||||
if klass is not supcls:
|
||||
name = None
|
||||
# if supcls is not Uninferable, then supcls was inferred
|
||||
# and use its name. Otherwise, try to look
|
||||
# for call.args[0].name
|
||||
if supcls:
|
||||
name = supcls.name
|
||||
elif call.args and hasattr(call.args[0], "name"):
|
||||
name = call.args[0].name
|
||||
if name:
|
||||
self.add_message("bad-super-call", node=call, args=(name,))
|
||||
|
||||
visit_asyncfunctiondef = visit_functiondef
|
||||
|
||||
|
||||
def register(linter):
|
||||
"""required method to auto register this checker """
|
||||
linter.register_checker(NewStyleConflictChecker(linter))
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,121 @@
|
||||
# Copyright (c) 2007, 2010, 2013, 2015 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
|
||||
# Copyright (c) 2013 Google, Inc.
|
||||
# Copyright (c) 2014 Arun Persaud <arun@nubati.net>
|
||||
# Copyright (c) 2015-2018 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2015 Mike Frysinger <vapier@gentoo.org>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
|
||||
# Copyright (c) 2016 Glenn Matthews <glenn@e-dad.net>
|
||||
# Copyright (c) 2018 ssolanki <sushobhitsolanki@gmail.com>
|
||||
# Copyright (c) 2019 Pierre Sassoulas <pierre.sassoulas@gmail.com>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
""" Copyright (c) 2003-2010 LOGILAB S.A. (Paris, FRANCE).
|
||||
http://www.logilab.fr/ -- mailto:contact@logilab.fr
|
||||
|
||||
Raw metrics checker
|
||||
"""
|
||||
|
||||
import tokenize
|
||||
from typing import Any
|
||||
|
||||
from pylint.checkers import BaseTokenChecker
|
||||
from pylint.exceptions import EmptyReportError
|
||||
from pylint.interfaces import ITokenChecker
|
||||
from pylint.reporters.ureports.nodes import Table
|
||||
|
||||
|
||||
def report_raw_stats(sect, stats, _):
|
||||
"""calculate percentage of code / doc / comment / empty
|
||||
"""
|
||||
total_lines = stats["total_lines"]
|
||||
if not total_lines:
|
||||
raise EmptyReportError()
|
||||
sect.description = "%s lines have been analyzed" % total_lines
|
||||
lines = ("type", "number", "%", "previous", "difference")
|
||||
for node_type in ("code", "docstring", "comment", "empty"):
|
||||
key = node_type + "_lines"
|
||||
total = stats[key]
|
||||
percent = float(total * 100) / total_lines
|
||||
lines += (node_type, str(total), "%.2f" % percent, "NC", "NC")
|
||||
sect.append(Table(children=lines, cols=5, rheaders=1))
|
||||
|
||||
|
||||
class RawMetricsChecker(BaseTokenChecker):
|
||||
"""does not check anything but gives some raw metrics :
|
||||
* total number of lines
|
||||
* total number of code lines
|
||||
* total number of docstring lines
|
||||
* total number of comments lines
|
||||
* total number of empty lines
|
||||
"""
|
||||
|
||||
__implements__ = (ITokenChecker,)
|
||||
|
||||
# configuration section name
|
||||
name = "metrics"
|
||||
# configuration options
|
||||
options = ()
|
||||
# messages
|
||||
msgs = {} # type: Any
|
||||
# reports
|
||||
reports = (("RP0701", "Raw metrics", report_raw_stats),)
|
||||
|
||||
def __init__(self, linter):
|
||||
BaseTokenChecker.__init__(self, linter)
|
||||
self.stats = None
|
||||
|
||||
def open(self):
|
||||
"""init statistics"""
|
||||
self.stats = self.linter.add_stats(
|
||||
total_lines=0,
|
||||
code_lines=0,
|
||||
empty_lines=0,
|
||||
docstring_lines=0,
|
||||
comment_lines=0,
|
||||
)
|
||||
|
||||
def process_tokens(self, tokens):
|
||||
"""update stats"""
|
||||
i = 0
|
||||
tokens = list(tokens)
|
||||
while i < len(tokens):
|
||||
i, lines_number, line_type = get_type(tokens, i)
|
||||
self.stats["total_lines"] += lines_number
|
||||
self.stats[line_type] += lines_number
|
||||
|
||||
|
||||
JUNK = (tokenize.NL, tokenize.INDENT, tokenize.NEWLINE, tokenize.ENDMARKER)
|
||||
|
||||
|
||||
def get_type(tokens, start_index):
|
||||
"""return the line type : docstring, comment, code, empty"""
|
||||
i = start_index
|
||||
tok_type = tokens[i][0]
|
||||
start = tokens[i][2]
|
||||
pos = start
|
||||
line_type = None
|
||||
while i < len(tokens) and tokens[i][2][0] == start[0]:
|
||||
tok_type = tokens[i][0]
|
||||
pos = tokens[i][3]
|
||||
if line_type is None:
|
||||
if tok_type == tokenize.STRING:
|
||||
line_type = "docstring_lines"
|
||||
elif tok_type == tokenize.COMMENT:
|
||||
line_type = "comment_lines"
|
||||
elif tok_type in JUNK:
|
||||
pass
|
||||
else:
|
||||
line_type = "code_lines"
|
||||
i += 1
|
||||
if line_type is None:
|
||||
line_type = "empty_lines"
|
||||
elif i < len(tokens) and tokens[i][0] == tokenize.NEWLINE:
|
||||
i += 1
|
||||
return i, pos[0] - start[0] + 1, line_type
|
||||
|
||||
|
||||
def register(linter):
|
||||
""" required method to auto register this checker """
|
||||
linter.register_checker(RawMetricsChecker(linter))
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,455 @@
|
||||
# Copyright (c) 2006, 2008-2014 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
|
||||
# Copyright (c) 2012 Ry4an Brase <ry4an-hg@ry4an.org>
|
||||
# Copyright (c) 2012 Google, Inc.
|
||||
# Copyright (c) 2012 Anthony VEREZ <anthony.verez.external@cassidian.com>
|
||||
# Copyright (c) 2014-2019 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2014 Brett Cannon <brett@python.org>
|
||||
# Copyright (c) 2014 Arun Persaud <arun@nubati.net>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
|
||||
# Copyright (c) 2017, 2020 Anthony Sottile <asottile@umich.edu>
|
||||
# Copyright (c) 2017 Mikhail Fesenko <proggga@gmail.com>
|
||||
# Copyright (c) 2018 Scott Worley <scottworley@scottworley.com>
|
||||
# Copyright (c) 2018 ssolanki <sushobhitsolanki@gmail.com>
|
||||
# Copyright (c) 2019 Hugo van Kemenade <hugovk@users.noreply.github.com>
|
||||
# Copyright (c) 2019 Taewon D. Kim <kimt33@mcmaster.ca>
|
||||
# Copyright (c) 2019 Pierre Sassoulas <pierre.sassoulas@gmail.com>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
# pylint: disable=redefined-builtin
|
||||
"""a similarities / code duplication command line tool and pylint checker
|
||||
"""
|
||||
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from getopt import getopt
|
||||
from itertools import groupby
|
||||
|
||||
import astroid
|
||||
|
||||
from pylint.checkers import BaseChecker, table_lines_from_stats
|
||||
from pylint.interfaces import IRawChecker
|
||||
from pylint.reporters.ureports.nodes import Table
|
||||
from pylint.utils import decoding_stream
|
||||
|
||||
|
||||
class Similar:
|
||||
"""finds copy-pasted lines of code in a project"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
min_lines=4,
|
||||
ignore_comments=False,
|
||||
ignore_docstrings=False,
|
||||
ignore_imports=False,
|
||||
):
|
||||
self.min_lines = min_lines
|
||||
self.ignore_comments = ignore_comments
|
||||
self.ignore_docstrings = ignore_docstrings
|
||||
self.ignore_imports = ignore_imports
|
||||
self.linesets = []
|
||||
|
||||
def append_stream(self, streamid, stream, encoding=None):
|
||||
"""append a file to search for similarities"""
|
||||
if encoding is None:
|
||||
readlines = stream.readlines
|
||||
else:
|
||||
readlines = decoding_stream(stream, encoding).readlines
|
||||
try:
|
||||
self.linesets.append(
|
||||
LineSet(
|
||||
streamid,
|
||||
readlines(),
|
||||
self.ignore_comments,
|
||||
self.ignore_docstrings,
|
||||
self.ignore_imports,
|
||||
)
|
||||
)
|
||||
except UnicodeDecodeError:
|
||||
pass
|
||||
|
||||
def run(self):
|
||||
"""start looking for similarities and display results on stdout"""
|
||||
self._display_sims(self._compute_sims())
|
||||
|
||||
def _compute_sims(self):
|
||||
"""compute similarities in appended files"""
|
||||
no_duplicates = defaultdict(list)
|
||||
for num, lineset1, idx1, lineset2, idx2 in self._iter_sims():
|
||||
duplicate = no_duplicates[num]
|
||||
for couples in duplicate:
|
||||
if (lineset1, idx1) in couples or (lineset2, idx2) in couples:
|
||||
couples.add((lineset1, idx1))
|
||||
couples.add((lineset2, idx2))
|
||||
break
|
||||
else:
|
||||
duplicate.append({(lineset1, idx1), (lineset2, idx2)})
|
||||
sims = []
|
||||
for num, ensembles in no_duplicates.items():
|
||||
for couples in ensembles:
|
||||
sims.append((num, couples))
|
||||
sims.sort()
|
||||
sims.reverse()
|
||||
return sims
|
||||
|
||||
def _display_sims(self, sims):
|
||||
"""display computed similarities on stdout"""
|
||||
nb_lignes_dupliquees = 0
|
||||
for num, couples in sims:
|
||||
print()
|
||||
print(num, "similar lines in", len(couples), "files")
|
||||
couples = sorted(couples)
|
||||
lineset = idx = None
|
||||
for lineset, idx in couples:
|
||||
print("==%s:%s" % (lineset.name, idx))
|
||||
if lineset:
|
||||
for line in lineset._real_lines[idx : idx + num]:
|
||||
print(" ", line.rstrip())
|
||||
nb_lignes_dupliquees += num * (len(couples) - 1)
|
||||
nb_total_lignes = sum([len(lineset) for lineset in self.linesets])
|
||||
print(
|
||||
"TOTAL lines=%s duplicates=%s percent=%.2f"
|
||||
% (
|
||||
nb_total_lignes,
|
||||
nb_lignes_dupliquees,
|
||||
nb_lignes_dupliquees * 100.0 / nb_total_lignes,
|
||||
)
|
||||
)
|
||||
|
||||
def _find_common(self, lineset1, lineset2):
|
||||
"""find similarities in the two given linesets"""
|
||||
lines1 = lineset1.enumerate_stripped
|
||||
lines2 = lineset2.enumerate_stripped
|
||||
find = lineset2.find
|
||||
index1 = 0
|
||||
min_lines = self.min_lines
|
||||
while index1 < len(lineset1):
|
||||
skip = 1
|
||||
num = 0
|
||||
for index2 in find(lineset1[index1]):
|
||||
non_blank = 0
|
||||
for num, ((_, line1), (_, line2)) in enumerate(
|
||||
zip(lines1(index1), lines2(index2))
|
||||
):
|
||||
if line1 != line2:
|
||||
if non_blank > min_lines:
|
||||
yield num, lineset1, index1, lineset2, index2
|
||||
skip = max(skip, num)
|
||||
break
|
||||
if line1:
|
||||
non_blank += 1
|
||||
else:
|
||||
# we may have reach the end
|
||||
num += 1
|
||||
if non_blank > min_lines:
|
||||
yield num, lineset1, index1, lineset2, index2
|
||||
skip = max(skip, num)
|
||||
index1 += skip
|
||||
|
||||
def _iter_sims(self):
|
||||
"""iterate on similarities among all files, by making a cartesian
|
||||
product
|
||||
"""
|
||||
for idx, lineset in enumerate(self.linesets[:-1]):
|
||||
for lineset2 in self.linesets[idx + 1 :]:
|
||||
yield from self._find_common(lineset, lineset2)
|
||||
|
||||
|
||||
def stripped_lines(lines, ignore_comments, ignore_docstrings, ignore_imports):
|
||||
"""return lines with leading/trailing whitespace and any ignored code
|
||||
features removed
|
||||
"""
|
||||
if ignore_imports:
|
||||
tree = astroid.parse("".join(lines))
|
||||
node_is_import_by_lineno = (
|
||||
(node.lineno, isinstance(node, (astroid.Import, astroid.ImportFrom)))
|
||||
for node in tree.body
|
||||
)
|
||||
line_begins_import = {
|
||||
lineno: all(is_import for _, is_import in node_is_import_group)
|
||||
for lineno, node_is_import_group in groupby(
|
||||
node_is_import_by_lineno, key=lambda x: x[0]
|
||||
)
|
||||
}
|
||||
current_line_is_import = False
|
||||
|
||||
strippedlines = []
|
||||
docstring = None
|
||||
for lineno, line in enumerate(lines, start=1):
|
||||
line = line.strip()
|
||||
if ignore_docstrings:
|
||||
if not docstring and any(
|
||||
line.startswith(i) for i in ['"""', "'''", 'r"""', "r'''"]
|
||||
):
|
||||
docstring = line[:3]
|
||||
line = line[3:]
|
||||
if docstring:
|
||||
if line.endswith(docstring):
|
||||
docstring = None
|
||||
line = ""
|
||||
if ignore_imports:
|
||||
current_line_is_import = line_begins_import.get(
|
||||
lineno, current_line_is_import
|
||||
)
|
||||
if current_line_is_import:
|
||||
line = ""
|
||||
if ignore_comments:
|
||||
line = line.split("#", 1)[0].strip()
|
||||
strippedlines.append(line)
|
||||
return strippedlines
|
||||
|
||||
|
||||
class LineSet:
|
||||
"""Holds and indexes all the lines of a single source file"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name,
|
||||
lines,
|
||||
ignore_comments=False,
|
||||
ignore_docstrings=False,
|
||||
ignore_imports=False,
|
||||
):
|
||||
self.name = name
|
||||
self._real_lines = lines
|
||||
self._stripped_lines = stripped_lines(
|
||||
lines, ignore_comments, ignore_docstrings, ignore_imports
|
||||
)
|
||||
self._index = self._mk_index()
|
||||
|
||||
def __str__(self):
|
||||
return "<Lineset for %s>" % self.name
|
||||
|
||||
def __len__(self):
|
||||
return len(self._real_lines)
|
||||
|
||||
def __getitem__(self, index):
|
||||
return self._stripped_lines[index]
|
||||
|
||||
def __lt__(self, other):
|
||||
return self.name < other.name
|
||||
|
||||
def __hash__(self):
|
||||
return id(self)
|
||||
|
||||
def enumerate_stripped(self, start_at=0):
|
||||
"""return an iterator on stripped lines, starting from a given index
|
||||
if specified, else 0
|
||||
"""
|
||||
idx = start_at
|
||||
if start_at:
|
||||
lines = self._stripped_lines[start_at:]
|
||||
else:
|
||||
lines = self._stripped_lines
|
||||
for line in lines:
|
||||
# if line:
|
||||
yield idx, line
|
||||
idx += 1
|
||||
|
||||
def find(self, stripped_line):
|
||||
"""return positions of the given stripped line in this set"""
|
||||
return self._index.get(stripped_line, ())
|
||||
|
||||
def _mk_index(self):
|
||||
"""create the index for this set"""
|
||||
index = defaultdict(list)
|
||||
for line_no, line in enumerate(self._stripped_lines):
|
||||
if line:
|
||||
index[line].append(line_no)
|
||||
return index
|
||||
|
||||
|
||||
MSGS = {
|
||||
"R0801": (
|
||||
"Similar lines in %s files\n%s",
|
||||
"duplicate-code",
|
||||
"Indicates that a set of similar lines has been detected "
|
||||
"among multiple file. This usually means that the code should "
|
||||
"be refactored to avoid this duplication.",
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
def report_similarities(sect, stats, old_stats):
|
||||
"""make a layout with some stats about duplication"""
|
||||
lines = ["", "now", "previous", "difference"]
|
||||
lines += table_lines_from_stats(
|
||||
stats, old_stats, ("nb_duplicated_lines", "percent_duplicated_lines")
|
||||
)
|
||||
sect.append(Table(children=lines, cols=4, rheaders=1, cheaders=1))
|
||||
|
||||
|
||||
# wrapper to get a pylint checker from the similar class
|
||||
class SimilarChecker(BaseChecker, Similar):
|
||||
"""checks for similarities and duplicated code. This computation may be
|
||||
memory / CPU intensive, so you should disable it if you experiment some
|
||||
problems.
|
||||
"""
|
||||
|
||||
__implements__ = (IRawChecker,)
|
||||
# configuration section name
|
||||
name = "similarities"
|
||||
# messages
|
||||
msgs = MSGS
|
||||
# configuration options
|
||||
# for available dict keys/values see the optik parser 'add_option' method
|
||||
options = (
|
||||
(
|
||||
"min-similarity-lines", # type: ignore
|
||||
{
|
||||
"default": 4,
|
||||
"type": "int",
|
||||
"metavar": "<int>",
|
||||
"help": "Minimum lines number of a similarity.",
|
||||
},
|
||||
),
|
||||
(
|
||||
"ignore-comments",
|
||||
{
|
||||
"default": True,
|
||||
"type": "yn",
|
||||
"metavar": "<y or n>",
|
||||
"help": "Ignore comments when computing similarities.",
|
||||
},
|
||||
),
|
||||
(
|
||||
"ignore-docstrings",
|
||||
{
|
||||
"default": True,
|
||||
"type": "yn",
|
||||
"metavar": "<y or n>",
|
||||
"help": "Ignore docstrings when computing similarities.",
|
||||
},
|
||||
),
|
||||
(
|
||||
"ignore-imports",
|
||||
{
|
||||
"default": False,
|
||||
"type": "yn",
|
||||
"metavar": "<y or n>",
|
||||
"help": "Ignore imports when computing similarities.",
|
||||
},
|
||||
),
|
||||
)
|
||||
# reports
|
||||
reports = (("RP0801", "Duplication", report_similarities),) # type: ignore
|
||||
|
||||
def __init__(self, linter=None):
|
||||
BaseChecker.__init__(self, linter)
|
||||
Similar.__init__(
|
||||
self, min_lines=4, ignore_comments=True, ignore_docstrings=True
|
||||
)
|
||||
self.stats = None
|
||||
|
||||
def set_option(self, optname, value, action=None, optdict=None):
|
||||
"""method called to set an option (registered in the options list)
|
||||
|
||||
overridden to report options setting to Similar
|
||||
"""
|
||||
BaseChecker.set_option(self, optname, value, action, optdict)
|
||||
if optname == "min-similarity-lines":
|
||||
self.min_lines = self.config.min_similarity_lines
|
||||
elif optname == "ignore-comments":
|
||||
self.ignore_comments = self.config.ignore_comments
|
||||
elif optname == "ignore-docstrings":
|
||||
self.ignore_docstrings = self.config.ignore_docstrings
|
||||
elif optname == "ignore-imports":
|
||||
self.ignore_imports = self.config.ignore_imports
|
||||
|
||||
def open(self):
|
||||
"""init the checkers: reset linesets and statistics information"""
|
||||
self.linesets = []
|
||||
self.stats = self.linter.add_stats(
|
||||
nb_duplicated_lines=0, percent_duplicated_lines=0
|
||||
)
|
||||
|
||||
def process_module(self, node):
|
||||
"""process a module
|
||||
|
||||
the module's content is accessible via the stream object
|
||||
|
||||
stream must implement the readlines method
|
||||
"""
|
||||
with node.stream() as stream:
|
||||
self.append_stream(self.linter.current_name, stream, node.file_encoding)
|
||||
|
||||
def close(self):
|
||||
"""compute and display similarities on closing (i.e. end of parsing)"""
|
||||
total = sum(len(lineset) for lineset in self.linesets)
|
||||
duplicated = 0
|
||||
stats = self.stats
|
||||
for num, couples in self._compute_sims():
|
||||
msg = []
|
||||
lineset = idx = None
|
||||
for lineset, idx in couples:
|
||||
msg.append("==%s:%s" % (lineset.name, idx))
|
||||
msg.sort()
|
||||
|
||||
if lineset:
|
||||
for line in lineset._real_lines[idx : idx + num]:
|
||||
msg.append(line.rstrip())
|
||||
|
||||
self.add_message("R0801", args=(len(couples), "\n".join(msg)))
|
||||
duplicated += num * (len(couples) - 1)
|
||||
stats["nb_duplicated_lines"] = duplicated
|
||||
stats["percent_duplicated_lines"] = total and duplicated * 100.0 / total
|
||||
|
||||
|
||||
def register(linter):
|
||||
"""required method to auto register this checker """
|
||||
linter.register_checker(SimilarChecker(linter))
|
||||
|
||||
|
||||
def usage(status=0):
|
||||
"""display command line usage information"""
|
||||
print("finds copy pasted blocks in a set of files")
|
||||
print()
|
||||
print(
|
||||
"Usage: symilar [-d|--duplicates min_duplicated_lines] \
|
||||
[-i|--ignore-comments] [--ignore-docstrings] [--ignore-imports] file1..."
|
||||
)
|
||||
sys.exit(status)
|
||||
|
||||
|
||||
def Run(argv=None):
|
||||
"""standalone command line access point"""
|
||||
if argv is None:
|
||||
argv = sys.argv[1:]
|
||||
|
||||
s_opts = "hdi"
|
||||
l_opts = (
|
||||
"help",
|
||||
"duplicates=",
|
||||
"ignore-comments",
|
||||
"ignore-imports",
|
||||
"ignore-docstrings",
|
||||
)
|
||||
min_lines = 4
|
||||
ignore_comments = False
|
||||
ignore_docstrings = False
|
||||
ignore_imports = False
|
||||
opts, args = getopt(argv, s_opts, l_opts)
|
||||
for opt, val in opts:
|
||||
if opt in ("-d", "--duplicates"):
|
||||
min_lines = int(val)
|
||||
elif opt in ("-h", "--help"):
|
||||
usage()
|
||||
elif opt in ("-i", "--ignore-comments"):
|
||||
ignore_comments = True
|
||||
elif opt in ("--ignore-docstrings",):
|
||||
ignore_docstrings = True
|
||||
elif opt in ("--ignore-imports",):
|
||||
ignore_imports = True
|
||||
if not args:
|
||||
usage(1)
|
||||
sim = Similar(min_lines, ignore_comments, ignore_docstrings, ignore_imports)
|
||||
for filename in args:
|
||||
with open(filename) as stream:
|
||||
sim.append_stream(filename, stream)
|
||||
sim.run()
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
Run()
|
||||
@@ -0,0 +1,415 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2014-2019 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2014 Michal Nowikowski <godfryd@gmail.com>
|
||||
# Copyright (c) 2014 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
|
||||
# Copyright (c) 2015 Pavel Roskin <proski@gnu.org>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
|
||||
# Copyright (c) 2016-2017 Pedro Algarvio <pedro@algarvio.me>
|
||||
# Copyright (c) 2016 Alexander Todorov <atodorov@otb.bg>
|
||||
# Copyright (c) 2017 Łukasz Rogalski <rogalski.91@gmail.com>
|
||||
# Copyright (c) 2017 Mikhail Fesenko <proggga@gmail.com>
|
||||
# Copyright (c) 2018, 2020 Anthony Sottile <asottile@umich.edu>
|
||||
# Copyright (c) 2018 ssolanki <sushobhitsolanki@gmail.com>
|
||||
# Copyright (c) 2018 Mike Frysinger <vapier@gmail.com>
|
||||
# Copyright (c) 2018 Sushobhit <31987769+sushobhit27@users.noreply.github.com>
|
||||
# Copyright (c) 2019 Peter Kolbus <peter.kolbus@gmail.com>
|
||||
# Copyright (c) 2019 agutole <toldo_carp@hotmail.com>
|
||||
# Copyright (c) 2019 Pierre Sassoulas <pierre.sassoulas@gmail.com>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""Checker for spelling errors in comments and docstrings.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import tokenize
|
||||
|
||||
from pylint.checkers import BaseTokenChecker
|
||||
from pylint.checkers.utils import check_messages
|
||||
from pylint.interfaces import IAstroidChecker, ITokenChecker
|
||||
|
||||
try:
|
||||
import enchant
|
||||
from enchant.tokenize import ( # type: ignore
|
||||
get_tokenizer,
|
||||
Chunker,
|
||||
Filter,
|
||||
EmailFilter,
|
||||
URLFilter,
|
||||
WikiWordFilter,
|
||||
)
|
||||
except ImportError:
|
||||
enchant = None
|
||||
# pylint: disable=no-init
|
||||
class Filter: # type: ignore
|
||||
def _skip(self, word):
|
||||
raise NotImplementedError
|
||||
|
||||
class Chunker: # type: ignore
|
||||
pass
|
||||
|
||||
|
||||
if enchant is not None:
|
||||
br = enchant.Broker()
|
||||
dicts = br.list_dicts()
|
||||
dict_choices = [""] + [d[0] for d in dicts]
|
||||
dicts = ["%s (%s)" % (d[0], d[1].name) for d in dicts]
|
||||
dicts = ", ".join(dicts)
|
||||
instr = ""
|
||||
else:
|
||||
dicts = "none"
|
||||
dict_choices = [""]
|
||||
instr = " To make it work, install the python-enchant package."
|
||||
|
||||
|
||||
class WordsWithDigigtsFilter(Filter):
|
||||
"""Skips words with digits.
|
||||
"""
|
||||
|
||||
def _skip(self, word):
|
||||
for char in word:
|
||||
if char.isdigit():
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class WordsWithUnderscores(Filter):
|
||||
"""Skips words with underscores.
|
||||
|
||||
They are probably function parameter names.
|
||||
"""
|
||||
|
||||
def _skip(self, word):
|
||||
return "_" in word
|
||||
|
||||
|
||||
class CamelCasedWord(Filter):
|
||||
r"""Filter skipping over camelCasedWords.
|
||||
This filter skips any words matching the following regular expression:
|
||||
|
||||
^([a-z]\w+[A-Z]+\w+)
|
||||
|
||||
That is, any words that are camelCasedWords.
|
||||
"""
|
||||
_pattern = re.compile(r"^([a-z]+([\d]|[A-Z])(?:\w+)?)")
|
||||
|
||||
def _skip(self, word):
|
||||
return bool(self._pattern.match(word))
|
||||
|
||||
|
||||
class SphinxDirectives(Filter):
|
||||
r"""Filter skipping over Sphinx Directives.
|
||||
This filter skips any words matching the following regular expression:
|
||||
|
||||
^:([a-z]+):`([^`]+)(`)?
|
||||
|
||||
That is, for example, :class:`BaseQuery`
|
||||
"""
|
||||
# The final ` in the pattern is optional because enchant strips it out
|
||||
_pattern = re.compile(r"^:([a-z]+):`([^`]+)(`)?")
|
||||
|
||||
def _skip(self, word):
|
||||
return bool(self._pattern.match(word))
|
||||
|
||||
|
||||
class ForwardSlashChunkder(Chunker):
|
||||
"""
|
||||
This chunker allows splitting words like 'before/after' into 'before' and 'after'
|
||||
"""
|
||||
|
||||
def next(self):
|
||||
while True:
|
||||
if not self._text:
|
||||
raise StopIteration()
|
||||
if "/" not in self._text:
|
||||
text = self._text
|
||||
self._offset = 0
|
||||
self._text = ""
|
||||
return (text, 0)
|
||||
pre_text, post_text = self._text.split("/", 1)
|
||||
self._text = post_text
|
||||
self._offset = 0
|
||||
if (
|
||||
not pre_text
|
||||
or not post_text
|
||||
or not pre_text[-1].isalpha()
|
||||
or not post_text[0].isalpha()
|
||||
):
|
||||
self._text = ""
|
||||
self._offset = 0
|
||||
return (pre_text + "/" + post_text, 0)
|
||||
return (pre_text, 0)
|
||||
|
||||
def _next(self):
|
||||
while True:
|
||||
if "/" not in self._text:
|
||||
return (self._text, 0)
|
||||
pre_text, post_text = self._text.split("/", 1)
|
||||
if not pre_text or not post_text:
|
||||
break
|
||||
if not pre_text[-1].isalpha() or not post_text[0].isalpha():
|
||||
raise StopIteration()
|
||||
self._text = pre_text + " " + post_text
|
||||
raise StopIteration()
|
||||
|
||||
|
||||
class SpellingChecker(BaseTokenChecker):
|
||||
"""Check spelling in comments and docstrings"""
|
||||
|
||||
__implements__ = (ITokenChecker, IAstroidChecker)
|
||||
name = "spelling"
|
||||
msgs = {
|
||||
"C0401": (
|
||||
"Wrong spelling of a word '%s' in a comment:\n%s\n"
|
||||
"%s\nDid you mean: '%s'?",
|
||||
"wrong-spelling-in-comment",
|
||||
"Used when a word in comment is not spelled correctly.",
|
||||
),
|
||||
"C0402": (
|
||||
"Wrong spelling of a word '%s' in a docstring:\n%s\n"
|
||||
"%s\nDid you mean: '%s'?",
|
||||
"wrong-spelling-in-docstring",
|
||||
"Used when a word in docstring is not spelled correctly.",
|
||||
),
|
||||
"C0403": (
|
||||
"Invalid characters %r in a docstring",
|
||||
"invalid-characters-in-docstring",
|
||||
"Used when a word in docstring cannot be checked by enchant.",
|
||||
),
|
||||
}
|
||||
options = (
|
||||
(
|
||||
"spelling-dict",
|
||||
{
|
||||
"default": "",
|
||||
"type": "choice",
|
||||
"metavar": "<dict name>",
|
||||
"choices": dict_choices,
|
||||
"help": "Spelling dictionary name. "
|
||||
"Available dictionaries: %s.%s" % (dicts, instr),
|
||||
},
|
||||
),
|
||||
(
|
||||
"spelling-ignore-words",
|
||||
{
|
||||
"default": "",
|
||||
"type": "string",
|
||||
"metavar": "<comma separated words>",
|
||||
"help": "List of comma separated words that " "should not be checked.",
|
||||
},
|
||||
),
|
||||
(
|
||||
"spelling-private-dict-file",
|
||||
{
|
||||
"default": "",
|
||||
"type": "string",
|
||||
"metavar": "<path to file>",
|
||||
"help": "A path to a file that contains the private "
|
||||
"dictionary; one word per line.",
|
||||
},
|
||||
),
|
||||
(
|
||||
"spelling-store-unknown-words",
|
||||
{
|
||||
"default": "n",
|
||||
"type": "yn",
|
||||
"metavar": "<y_or_n>",
|
||||
"help": "Tells whether to store unknown words to the "
|
||||
"private dictionary (see the "
|
||||
"--spelling-private-dict-file option) instead of "
|
||||
"raising a message.",
|
||||
},
|
||||
),
|
||||
(
|
||||
"max-spelling-suggestions",
|
||||
{
|
||||
"default": 4,
|
||||
"type": "int",
|
||||
"metavar": "N",
|
||||
"help": "Limits count of emitted suggestions for " "spelling mistakes.",
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
def open(self):
|
||||
self.initialized = False
|
||||
self.private_dict_file = None
|
||||
|
||||
if enchant is None:
|
||||
return
|
||||
dict_name = self.config.spelling_dict
|
||||
if not dict_name:
|
||||
return
|
||||
|
||||
self.ignore_list = [
|
||||
w.strip() for w in self.config.spelling_ignore_words.split(",")
|
||||
]
|
||||
# "param" appears in docstring in param description and
|
||||
# "pylint" appears in comments in pylint pragmas.
|
||||
self.ignore_list.extend(["param", "pylint"])
|
||||
|
||||
# Expand tilde to allow e.g. spelling-private-dict-file = ~/.pylintdict
|
||||
if self.config.spelling_private_dict_file:
|
||||
self.config.spelling_private_dict_file = os.path.expanduser(
|
||||
self.config.spelling_private_dict_file
|
||||
)
|
||||
|
||||
if self.config.spelling_private_dict_file:
|
||||
self.spelling_dict = enchant.DictWithPWL(
|
||||
dict_name, self.config.spelling_private_dict_file
|
||||
)
|
||||
self.private_dict_file = open(self.config.spelling_private_dict_file, "a")
|
||||
else:
|
||||
self.spelling_dict = enchant.Dict(dict_name)
|
||||
|
||||
if self.config.spelling_store_unknown_words:
|
||||
self.unknown_words = set()
|
||||
|
||||
self.tokenizer = get_tokenizer(
|
||||
dict_name,
|
||||
chunkers=[ForwardSlashChunkder],
|
||||
filters=[
|
||||
EmailFilter,
|
||||
URLFilter,
|
||||
WikiWordFilter,
|
||||
WordsWithDigigtsFilter,
|
||||
WordsWithUnderscores,
|
||||
CamelCasedWord,
|
||||
SphinxDirectives,
|
||||
],
|
||||
)
|
||||
self.initialized = True
|
||||
|
||||
def close(self):
|
||||
if self.private_dict_file:
|
||||
self.private_dict_file.close()
|
||||
|
||||
def _check_spelling(self, msgid, line, line_num):
|
||||
original_line = line
|
||||
try:
|
||||
initial_space = re.search(r"^[^\S]\s*", line).regs[0][1]
|
||||
except (IndexError, AttributeError):
|
||||
initial_space = 0
|
||||
if line.strip().startswith("#"):
|
||||
line = line.strip()[1:]
|
||||
starts_with_comment = True
|
||||
else:
|
||||
starts_with_comment = False
|
||||
for word, word_start_at in self.tokenizer(line.strip()):
|
||||
word_start_at += initial_space
|
||||
lower_cased_word = word.casefold()
|
||||
|
||||
# Skip words from ignore list.
|
||||
if word in self.ignore_list or lower_cased_word in self.ignore_list:
|
||||
continue
|
||||
|
||||
# Strip starting u' from unicode literals and r' from raw strings.
|
||||
if word.startswith(("u'", 'u"', "r'", 'r"')) and len(word) > 2:
|
||||
word = word[2:]
|
||||
lower_cased_word = lower_cased_word[2:]
|
||||
|
||||
# If it is a known word, then continue.
|
||||
try:
|
||||
if self.spelling_dict.check(lower_cased_word):
|
||||
# The lower cased version of word passed spell checking
|
||||
continue
|
||||
|
||||
# If we reached this far, it means there was a spelling mistake.
|
||||
# Let's retry with the original work because 'unicode' is a
|
||||
# spelling mistake but 'Unicode' is not
|
||||
if self.spelling_dict.check(word):
|
||||
continue
|
||||
except enchant.errors.Error:
|
||||
self.add_message(
|
||||
"invalid-characters-in-docstring", line=line_num, args=(word,)
|
||||
)
|
||||
continue
|
||||
|
||||
# Store word to private dict or raise a message.
|
||||
if self.config.spelling_store_unknown_words:
|
||||
if lower_cased_word not in self.unknown_words:
|
||||
self.private_dict_file.write("%s\n" % lower_cased_word)
|
||||
self.unknown_words.add(lower_cased_word)
|
||||
else:
|
||||
# Present up to N suggestions.
|
||||
suggestions = self.spelling_dict.suggest(word)
|
||||
del suggestions[self.config.max_spelling_suggestions :]
|
||||
|
||||
line_segment = line[word_start_at:]
|
||||
match = re.search(r"(\W|^)(%s)(\W|$)" % word, line_segment)
|
||||
if match:
|
||||
# Start position of second group in regex.
|
||||
col = match.regs[2][0]
|
||||
else:
|
||||
col = line_segment.index(word)
|
||||
|
||||
col += word_start_at
|
||||
|
||||
if starts_with_comment:
|
||||
col += 1
|
||||
indicator = (" " * col) + ("^" * len(word))
|
||||
|
||||
self.add_message(
|
||||
msgid,
|
||||
line=line_num,
|
||||
args=(
|
||||
word,
|
||||
original_line,
|
||||
indicator,
|
||||
"'{}'".format("' or '".join(suggestions)),
|
||||
),
|
||||
)
|
||||
|
||||
def process_tokens(self, tokens):
|
||||
if not self.initialized:
|
||||
return
|
||||
|
||||
# Process tokens and look for comments.
|
||||
for (tok_type, token, (start_row, _), _, _) in tokens:
|
||||
if tok_type == tokenize.COMMENT:
|
||||
if start_row == 1 and token.startswith("#!/"):
|
||||
# Skip shebang lines
|
||||
continue
|
||||
if token.startswith("# pylint:"):
|
||||
# Skip pylint enable/disable comments
|
||||
continue
|
||||
self._check_spelling("wrong-spelling-in-comment", token, start_row)
|
||||
|
||||
@check_messages("wrong-spelling-in-docstring")
|
||||
def visit_module(self, node):
|
||||
if not self.initialized:
|
||||
return
|
||||
self._check_docstring(node)
|
||||
|
||||
@check_messages("wrong-spelling-in-docstring")
|
||||
def visit_classdef(self, node):
|
||||
if not self.initialized:
|
||||
return
|
||||
self._check_docstring(node)
|
||||
|
||||
@check_messages("wrong-spelling-in-docstring")
|
||||
def visit_functiondef(self, node):
|
||||
if not self.initialized:
|
||||
return
|
||||
self._check_docstring(node)
|
||||
|
||||
visit_asyncfunctiondef = visit_functiondef
|
||||
|
||||
def _check_docstring(self, node):
|
||||
"""check the node has any spelling errors"""
|
||||
docstring = node.doc
|
||||
if not docstring:
|
||||
return
|
||||
|
||||
start_line = node.lineno + 1
|
||||
|
||||
# Go through lines of docstring
|
||||
for idx, line in enumerate(docstring.splitlines()):
|
||||
self._check_spelling("wrong-spelling-in-docstring", line, start_line + idx)
|
||||
|
||||
|
||||
def register(linter):
|
||||
"""required method to auto register this checker """
|
||||
linter.register_checker(SpellingChecker(linter))
|
||||
@@ -0,0 +1,458 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2013-2014 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
|
||||
# Copyright (c) 2013-2014 Google, Inc.
|
||||
# Copyright (c) 2014-2019 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2014 Cosmin Poieana <cmin@ropython.org>
|
||||
# Copyright (c) 2014 Vlad Temian <vladtemian@gmail.com>
|
||||
# Copyright (c) 2014 Arun Persaud <arun@nubati.net>
|
||||
# Copyright (c) 2015 Cezar <celnazli@bitdefender.com>
|
||||
# Copyright (c) 2015 Chris Rebert <code@rebertia.com>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
|
||||
# Copyright (c) 2016 Jared Garst <cultofjared@gmail.com>
|
||||
# Copyright (c) 2017 Renat Galimov <renat2017@gmail.com>
|
||||
# Copyright (c) 2017 Martin <MartinBasti@users.noreply.github.com>
|
||||
# Copyright (c) 2017 Christopher Zurcher <zurcher@users.noreply.github.com>
|
||||
# Copyright (c) 2017 Łukasz Rogalski <rogalski.91@gmail.com>
|
||||
# Copyright (c) 2018 Lucas Cimon <lucas.cimon@gmail.com>
|
||||
# Copyright (c) 2018 Banjamin Freeman <befreeman@users.noreply.github.com>
|
||||
# Copyright (c) 2018 Ioana Tagirta <ioana.tagirta@gmail.com>
|
||||
# Copyright (c) 2019 Julien Palard <julien@palard.fr>
|
||||
# Copyright (c) 2019 laike9m <laike9m@users.noreply.github.com>
|
||||
# Copyright (c) 2019 Hugo van Kemenade <hugovk@users.noreply.github.com>
|
||||
# Copyright (c) 2019 Robert Schweizer <robert_schweizer@gmx.de>
|
||||
# Copyright (c) 2019 fadedDexofan <fadedDexofan@gmail.com>
|
||||
# Copyright (c) 2019 Pierre Sassoulas <pierre.sassoulas@gmail.com>
|
||||
# Copyright (c) 2020 Anthony Sottile <asottile@umich.edu>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""Checkers for various standard library functions."""
|
||||
|
||||
import sys
|
||||
|
||||
import astroid
|
||||
from astroid.bases import Instance
|
||||
from astroid.node_classes import Const
|
||||
|
||||
from pylint.checkers import BaseChecker, utils
|
||||
from pylint.interfaces import IAstroidChecker
|
||||
|
||||
OPEN_FILES = {"open", "file"}
|
||||
UNITTEST_CASE = "unittest.case"
|
||||
THREADING_THREAD = "threading.Thread"
|
||||
COPY_COPY = "copy.copy"
|
||||
OS_ENVIRON = "os._Environ"
|
||||
ENV_GETTERS = {"os.getenv"}
|
||||
SUBPROCESS_POPEN = "subprocess.Popen"
|
||||
SUBPROCESS_RUN = "subprocess.run"
|
||||
OPEN_MODULE = "_io"
|
||||
|
||||
|
||||
def _check_mode_str(mode):
|
||||
# check type
|
||||
if not isinstance(mode, str):
|
||||
return False
|
||||
# check syntax
|
||||
modes = set(mode)
|
||||
_mode = "rwatb+Ux"
|
||||
creating = "x" in modes
|
||||
if modes - set(_mode) or len(mode) > len(modes):
|
||||
return False
|
||||
# check logic
|
||||
reading = "r" in modes
|
||||
writing = "w" in modes
|
||||
appending = "a" in modes
|
||||
text = "t" in modes
|
||||
binary = "b" in modes
|
||||
if "U" in modes:
|
||||
if writing or appending or creating:
|
||||
return False
|
||||
reading = True
|
||||
if text and binary:
|
||||
return False
|
||||
total = reading + writing + appending + creating
|
||||
if total > 1:
|
||||
return False
|
||||
if not (reading or writing or appending or creating):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class StdlibChecker(BaseChecker):
|
||||
__implements__ = (IAstroidChecker,)
|
||||
name = "stdlib"
|
||||
|
||||
msgs = {
|
||||
"W1501": (
|
||||
'"%s" is not a valid mode for open.',
|
||||
"bad-open-mode",
|
||||
"Python supports: r, w, a[, x] modes with b, +, "
|
||||
"and U (only with r) options. "
|
||||
"See http://docs.python.org/2/library/functions.html#open",
|
||||
),
|
||||
"W1502": (
|
||||
"Using datetime.time in a boolean context.",
|
||||
"boolean-datetime",
|
||||
"Using datetime.time in a boolean context can hide "
|
||||
"subtle bugs when the time they represent matches "
|
||||
"midnight UTC. This behaviour was fixed in Python 3.5. "
|
||||
"See http://bugs.python.org/issue13936 for reference.",
|
||||
{"maxversion": (3, 5)},
|
||||
),
|
||||
"W1503": (
|
||||
"Redundant use of %s with constant value %r",
|
||||
"redundant-unittest-assert",
|
||||
"The first argument of assertTrue and assertFalse is "
|
||||
"a condition. If a constant is passed as parameter, that "
|
||||
"condition will be always true. In this case a warning "
|
||||
"should be emitted.",
|
||||
),
|
||||
"W1505": (
|
||||
"Using deprecated method %s()",
|
||||
"deprecated-method",
|
||||
"The method is marked as deprecated and will be removed in "
|
||||
"a future version of Python. Consider looking for an "
|
||||
"alternative in the documentation.",
|
||||
),
|
||||
"W1506": (
|
||||
"threading.Thread needs the target function",
|
||||
"bad-thread-instantiation",
|
||||
"The warning is emitted when a threading.Thread class "
|
||||
"is instantiated without the target function being passed. "
|
||||
"By default, the first parameter is the group param, not the target param. ",
|
||||
),
|
||||
"W1507": (
|
||||
"Using copy.copy(os.environ). Use os.environ.copy() instead. ",
|
||||
"shallow-copy-environ",
|
||||
"os.environ is not a dict object but proxy object, so "
|
||||
"shallow copy has still effects on original object. "
|
||||
"See https://bugs.python.org/issue15373 for reference. ",
|
||||
),
|
||||
"E1507": (
|
||||
"%s does not support %s type argument",
|
||||
"invalid-envvar-value",
|
||||
"Env manipulation functions support only string type arguments. "
|
||||
"See https://docs.python.org/3/library/os.html#os.getenv. ",
|
||||
),
|
||||
"W1508": (
|
||||
"%s default type is %s. Expected str or None.",
|
||||
"invalid-envvar-default",
|
||||
"Env manipulation functions return None or str values. "
|
||||
"Supplying anything different as a default may cause bugs. "
|
||||
"See https://docs.python.org/3/library/os.html#os.getenv. ",
|
||||
),
|
||||
"W1509": (
|
||||
"Using preexec_fn keyword which may be unsafe in the presence "
|
||||
"of threads",
|
||||
"subprocess-popen-preexec-fn",
|
||||
"The preexec_fn parameter is not safe to use in the presence "
|
||||
"of threads in your application. The child process could "
|
||||
"deadlock before exec is called. If you must use it, keep it "
|
||||
"trivial! Minimize the number of libraries you call into."
|
||||
"https://docs.python.org/3/library/subprocess.html#popen-constructor",
|
||||
),
|
||||
"W1510": (
|
||||
"Using subprocess.run without explicitly set `check` is not recommended.",
|
||||
"subprocess-run-check",
|
||||
"The check parameter should always be used with explicitly set "
|
||||
"`check` keyword to make clear what the error-handling behavior is."
|
||||
"https://docs.python.org/3/library/subprocess.html#subprocess.run",
|
||||
),
|
||||
}
|
||||
|
||||
deprecated = {
|
||||
0: {
|
||||
"cgi.parse_qs",
|
||||
"cgi.parse_qsl",
|
||||
"ctypes.c_buffer",
|
||||
"distutils.command.register.register.check_metadata",
|
||||
"distutils.command.sdist.sdist.check_metadata",
|
||||
"tkinter.Misc.tk_menuBar",
|
||||
"tkinter.Menu.tk_bindForTraversal",
|
||||
},
|
||||
2: {
|
||||
(2, 6, 0): {
|
||||
"commands.getstatus",
|
||||
"os.popen2",
|
||||
"os.popen3",
|
||||
"os.popen4",
|
||||
"macostools.touched",
|
||||
},
|
||||
(2, 7, 0): {
|
||||
"unittest.case.TestCase.assertEquals",
|
||||
"unittest.case.TestCase.assertNotEquals",
|
||||
"unittest.case.TestCase.assertAlmostEquals",
|
||||
"unittest.case.TestCase.assertNotAlmostEquals",
|
||||
"unittest.case.TestCase.assert_",
|
||||
"xml.etree.ElementTree.Element.getchildren",
|
||||
"xml.etree.ElementTree.Element.getiterator",
|
||||
"xml.etree.ElementTree.XMLParser.getiterator",
|
||||
"xml.etree.ElementTree.XMLParser.doctype",
|
||||
},
|
||||
},
|
||||
3: {
|
||||
(3, 0, 0): {
|
||||
"inspect.getargspec",
|
||||
"failUnlessEqual",
|
||||
"assertEquals",
|
||||
"failIfEqual",
|
||||
"assertNotEquals",
|
||||
"failUnlessAlmostEqual",
|
||||
"assertAlmostEquals",
|
||||
"failIfAlmostEqual",
|
||||
"assertNotAlmostEquals",
|
||||
"failUnless",
|
||||
"assert_",
|
||||
"failUnlessRaises",
|
||||
"failIf",
|
||||
"assertRaisesRegexp",
|
||||
"assertRegexpMatches",
|
||||
"assertNotRegexpMatches",
|
||||
},
|
||||
(3, 1, 0): {
|
||||
"base64.encodestring",
|
||||
"base64.decodestring",
|
||||
"ntpath.splitunc",
|
||||
},
|
||||
(3, 2, 0): {
|
||||
"cgi.escape",
|
||||
"configparser.RawConfigParser.readfp",
|
||||
"xml.etree.ElementTree.Element.getchildren",
|
||||
"xml.etree.ElementTree.Element.getiterator",
|
||||
"xml.etree.ElementTree.XMLParser.getiterator",
|
||||
"xml.etree.ElementTree.XMLParser.doctype",
|
||||
},
|
||||
(3, 3, 0): {
|
||||
"inspect.getmoduleinfo",
|
||||
"logging.warn",
|
||||
"logging.Logger.warn",
|
||||
"logging.LoggerAdapter.warn",
|
||||
"nntplib._NNTPBase.xpath",
|
||||
"platform.popen",
|
||||
},
|
||||
(3, 4, 0): {
|
||||
"importlib.find_loader",
|
||||
"plistlib.readPlist",
|
||||
"plistlib.writePlist",
|
||||
"plistlib.readPlistFromBytes",
|
||||
"plistlib.writePlistToBytes",
|
||||
},
|
||||
(3, 4, 4): {"asyncio.tasks.async"},
|
||||
(3, 5, 0): {
|
||||
"fractions.gcd",
|
||||
"inspect.formatargspec",
|
||||
"inspect.getcallargs",
|
||||
"platform.linux_distribution",
|
||||
"platform.dist",
|
||||
},
|
||||
(3, 6, 0): {"importlib._bootstrap_external.FileLoader.load_module"},
|
||||
},
|
||||
}
|
||||
|
||||
def _check_bad_thread_instantiation(self, node):
|
||||
if not node.kwargs and not node.keywords and len(node.args) <= 1:
|
||||
self.add_message("bad-thread-instantiation", node=node)
|
||||
|
||||
def _check_for_preexec_fn_in_popen(self, node):
|
||||
if node.keywords:
|
||||
for keyword in node.keywords:
|
||||
if keyword.arg == "preexec_fn":
|
||||
self.add_message("subprocess-popen-preexec-fn", node=node)
|
||||
|
||||
def _check_for_check_kw_in_run(self, node):
|
||||
kwargs = {keyword.arg for keyword in (node.keywords or ())}
|
||||
if "check" not in kwargs:
|
||||
self.add_message("subprocess-run-check", node=node)
|
||||
|
||||
def _check_shallow_copy_environ(self, node):
|
||||
arg = utils.get_argument_from_call(node, position=0)
|
||||
for inferred in arg.inferred():
|
||||
if inferred.qname() == OS_ENVIRON:
|
||||
self.add_message("shallow-copy-environ", node=node)
|
||||
break
|
||||
|
||||
@utils.check_messages(
|
||||
"bad-open-mode",
|
||||
"redundant-unittest-assert",
|
||||
"deprecated-method",
|
||||
"bad-thread-instantiation",
|
||||
"shallow-copy-environ",
|
||||
"invalid-envvar-value",
|
||||
"invalid-envvar-default",
|
||||
"subprocess-popen-preexec-fn",
|
||||
"subprocess-run-check",
|
||||
)
|
||||
def visit_call(self, node):
|
||||
"""Visit a Call node."""
|
||||
try:
|
||||
for inferred in node.func.infer():
|
||||
if inferred is astroid.Uninferable:
|
||||
continue
|
||||
if inferred.root().name == OPEN_MODULE:
|
||||
if getattr(node.func, "name", None) in OPEN_FILES:
|
||||
self._check_open_mode(node)
|
||||
elif inferred.root().name == UNITTEST_CASE:
|
||||
self._check_redundant_assert(node, inferred)
|
||||
elif isinstance(inferred, astroid.ClassDef):
|
||||
if inferred.qname() == THREADING_THREAD:
|
||||
self._check_bad_thread_instantiation(node)
|
||||
elif inferred.qname() == SUBPROCESS_POPEN:
|
||||
self._check_for_preexec_fn_in_popen(node)
|
||||
elif isinstance(inferred, astroid.FunctionDef):
|
||||
name = inferred.qname()
|
||||
if name == COPY_COPY:
|
||||
self._check_shallow_copy_environ(node)
|
||||
elif name in ENV_GETTERS:
|
||||
self._check_env_function(node, inferred)
|
||||
elif name == SUBPROCESS_RUN:
|
||||
self._check_for_check_kw_in_run(node)
|
||||
self._check_deprecated_method(node, inferred)
|
||||
except astroid.InferenceError:
|
||||
return
|
||||
|
||||
@utils.check_messages("boolean-datetime")
|
||||
def visit_unaryop(self, node):
|
||||
if node.op == "not":
|
||||
self._check_datetime(node.operand)
|
||||
|
||||
@utils.check_messages("boolean-datetime")
|
||||
def visit_if(self, node):
|
||||
self._check_datetime(node.test)
|
||||
|
||||
@utils.check_messages("boolean-datetime")
|
||||
def visit_ifexp(self, node):
|
||||
self._check_datetime(node.test)
|
||||
|
||||
@utils.check_messages("boolean-datetime")
|
||||
def visit_boolop(self, node):
|
||||
for value in node.values:
|
||||
self._check_datetime(value)
|
||||
|
||||
def _check_deprecated_method(self, node, inferred):
|
||||
py_vers = sys.version_info[0]
|
||||
|
||||
if isinstance(node.func, astroid.Attribute):
|
||||
func_name = node.func.attrname
|
||||
elif isinstance(node.func, astroid.Name):
|
||||
func_name = node.func.name
|
||||
else:
|
||||
# Not interested in other nodes.
|
||||
return
|
||||
|
||||
# Reject nodes which aren't of interest to us.
|
||||
acceptable_nodes = (
|
||||
astroid.BoundMethod,
|
||||
astroid.UnboundMethod,
|
||||
astroid.FunctionDef,
|
||||
)
|
||||
if not isinstance(inferred, acceptable_nodes):
|
||||
return
|
||||
|
||||
qname = inferred.qname()
|
||||
if any(name in self.deprecated[0] for name in (qname, func_name)):
|
||||
self.add_message("deprecated-method", node=node, args=(func_name,))
|
||||
else:
|
||||
for since_vers, func_list in self.deprecated[py_vers].items():
|
||||
if since_vers <= sys.version_info and any(
|
||||
name in func_list for name in (qname, func_name)
|
||||
):
|
||||
self.add_message("deprecated-method", node=node, args=(func_name,))
|
||||
break
|
||||
|
||||
def _check_redundant_assert(self, node, infer):
|
||||
if (
|
||||
isinstance(infer, astroid.BoundMethod)
|
||||
and node.args
|
||||
and isinstance(node.args[0], astroid.Const)
|
||||
and infer.name in ["assertTrue", "assertFalse"]
|
||||
):
|
||||
self.add_message(
|
||||
"redundant-unittest-assert",
|
||||
args=(infer.name, node.args[0].value),
|
||||
node=node,
|
||||
)
|
||||
|
||||
def _check_datetime(self, node):
|
||||
""" Check that a datetime was inferred.
|
||||
If so, emit boolean-datetime warning.
|
||||
"""
|
||||
try:
|
||||
inferred = next(node.infer())
|
||||
except astroid.InferenceError:
|
||||
return
|
||||
if isinstance(inferred, Instance) and inferred.qname() == "datetime.time":
|
||||
self.add_message("boolean-datetime", node=node)
|
||||
|
||||
def _check_open_mode(self, node):
|
||||
"""Check that the mode argument of an open or file call is valid."""
|
||||
try:
|
||||
mode_arg = utils.get_argument_from_call(node, position=1, keyword="mode")
|
||||
except utils.NoSuchArgumentError:
|
||||
return
|
||||
if mode_arg:
|
||||
mode_arg = utils.safe_infer(mode_arg)
|
||||
if isinstance(mode_arg, astroid.Const) and not _check_mode_str(
|
||||
mode_arg.value
|
||||
):
|
||||
self.add_message("bad-open-mode", node=node, args=mode_arg.value)
|
||||
|
||||
def _check_env_function(self, node, infer):
|
||||
env_name_kwarg = "key"
|
||||
env_value_kwarg = "default"
|
||||
if node.keywords:
|
||||
kwargs = {keyword.arg: keyword.value for keyword in node.keywords}
|
||||
else:
|
||||
kwargs = None
|
||||
if node.args:
|
||||
env_name_arg = node.args[0]
|
||||
elif kwargs and env_name_kwarg in kwargs:
|
||||
env_name_arg = kwargs[env_name_kwarg]
|
||||
else:
|
||||
env_name_arg = None
|
||||
|
||||
if env_name_arg:
|
||||
self._check_invalid_envvar_value(
|
||||
node=node,
|
||||
message="invalid-envvar-value",
|
||||
call_arg=utils.safe_infer(env_name_arg),
|
||||
infer=infer,
|
||||
allow_none=False,
|
||||
)
|
||||
|
||||
if len(node.args) == 2:
|
||||
env_value_arg = node.args[1]
|
||||
elif kwargs and env_value_kwarg in kwargs:
|
||||
env_value_arg = kwargs[env_value_kwarg]
|
||||
else:
|
||||
env_value_arg = None
|
||||
|
||||
if env_value_arg:
|
||||
self._check_invalid_envvar_value(
|
||||
node=node,
|
||||
infer=infer,
|
||||
message="invalid-envvar-default",
|
||||
call_arg=utils.safe_infer(env_value_arg),
|
||||
allow_none=True,
|
||||
)
|
||||
|
||||
def _check_invalid_envvar_value(self, node, infer, message, call_arg, allow_none):
|
||||
if call_arg in (astroid.Uninferable, None):
|
||||
return
|
||||
|
||||
name = infer.qname()
|
||||
if isinstance(call_arg, Const):
|
||||
emit = False
|
||||
if call_arg.value is None:
|
||||
emit = not allow_none
|
||||
elif not isinstance(call_arg.value, str):
|
||||
emit = True
|
||||
if emit:
|
||||
self.add_message(message, node=node, args=(name, call_arg.pytype()))
|
||||
else:
|
||||
self.add_message(message, node=node, args=(name, call_arg.pytype()))
|
||||
|
||||
|
||||
def register(linter):
|
||||
"""required method to auto register this checker """
|
||||
linter.register_checker(StdlibChecker(linter))
|
||||
@@ -0,0 +1,952 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2009 Charles Hebert <charles.hebert@logilab.fr>
|
||||
# Copyright (c) 2010-2014 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
|
||||
# Copyright (c) 2010 Daniel Harding <dharding@gmail.com>
|
||||
# Copyright (c) 2012-2014 Google, Inc.
|
||||
# Copyright (c) 2013-2020 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2014 Brett Cannon <brett@python.org>
|
||||
# Copyright (c) 2014 Arun Persaud <arun@nubati.net>
|
||||
# Copyright (c) 2015 Rene Zhang <rz99@cornell.edu>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
|
||||
# Copyright (c) 2016, 2018 Jakub Wilk <jwilk@jwilk.net>
|
||||
# Copyright (c) 2016 Peter Dawyndt <Peter.Dawyndt@UGent.be>
|
||||
# Copyright (c) 2017 Łukasz Rogalski <rogalski.91@gmail.com>
|
||||
# Copyright (c) 2017 Ville Skyttä <ville.skytta@iki.fi>
|
||||
# Copyright (c) 2018, 2020 Anthony Sottile <asottile@umich.edu>
|
||||
# Copyright (c) 2018-2019 Lucas Cimon <lucas.cimon@gmail.com>
|
||||
# Copyright (c) 2018 Alan Chan <achan961117@gmail.com>
|
||||
# Copyright (c) 2018 Yury Gribov <tetra2005@gmail.com>
|
||||
# Copyright (c) 2018 ssolanki <sushobhitsolanki@gmail.com>
|
||||
# Copyright (c) 2018 Nick Drozd <nicholasdrozd@gmail.com>
|
||||
# Copyright (c) 2019 Wes Turner <westurner@google.com>
|
||||
# Copyright (c) 2019 Djailla <bastien.vallet@gmail.com>
|
||||
# Copyright (c) 2019 Hugo van Kemenade <hugovk@users.noreply.github.com>
|
||||
# Copyright (c) 2019 Pierre Sassoulas <pierre.sassoulas@gmail.com>
|
||||
# Copyright (c) 2020 Anthony <tanant@users.noreply.github.com>
|
||||
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""Checker for string formatting operations.
|
||||
"""
|
||||
|
||||
import builtins
|
||||
import collections
|
||||
import numbers
|
||||
import re
|
||||
import tokenize
|
||||
import typing
|
||||
from typing import Iterable
|
||||
|
||||
import astroid
|
||||
from astroid.arguments import CallSite
|
||||
from astroid.node_classes import Const
|
||||
|
||||
from pylint.checkers import BaseChecker, BaseTokenChecker, utils
|
||||
from pylint.checkers.utils import check_messages
|
||||
from pylint.interfaces import IAstroidChecker, IRawChecker, ITokenChecker
|
||||
|
||||
_AST_NODE_STR_TYPES = ("__builtin__.unicode", "__builtin__.str", "builtins.str")
|
||||
# Prefixes for both strings and bytes literals per
|
||||
# https://docs.python.org/3/reference/lexical_analysis.html#string-and-bytes-literals
|
||||
_PREFIXES = {
|
||||
"r",
|
||||
"u",
|
||||
"R",
|
||||
"U",
|
||||
"f",
|
||||
"F",
|
||||
"fr",
|
||||
"Fr",
|
||||
"fR",
|
||||
"FR",
|
||||
"rf",
|
||||
"rF",
|
||||
"Rf",
|
||||
"RF",
|
||||
"b",
|
||||
"B",
|
||||
"br",
|
||||
"Br",
|
||||
"bR",
|
||||
"BR",
|
||||
"rb",
|
||||
"rB",
|
||||
"Rb",
|
||||
"RB",
|
||||
}
|
||||
SINGLE_QUOTED_REGEX = re.compile("(%s)?'''" % "|".join(_PREFIXES))
|
||||
DOUBLE_QUOTED_REGEX = re.compile('(%s)?"""' % "|".join(_PREFIXES))
|
||||
QUOTE_DELIMITER_REGEX = re.compile("(%s)?(\"|')" % "|".join(_PREFIXES), re.DOTALL)
|
||||
|
||||
MSGS = {
|
||||
"E1300": (
|
||||
"Unsupported format character %r (%#02x) at index %d",
|
||||
"bad-format-character",
|
||||
"Used when an unsupported format character is used in a format string.",
|
||||
),
|
||||
"E1301": (
|
||||
"Format string ends in middle of conversion specifier",
|
||||
"truncated-format-string",
|
||||
"Used when a format string terminates before the end of a "
|
||||
"conversion specifier.",
|
||||
),
|
||||
"E1302": (
|
||||
"Mixing named and unnamed conversion specifiers in format string",
|
||||
"mixed-format-string",
|
||||
"Used when a format string contains both named (e.g. '%(foo)d') "
|
||||
"and unnamed (e.g. '%d') conversion specifiers. This is also "
|
||||
"used when a named conversion specifier contains * for the "
|
||||
"minimum field width and/or precision.",
|
||||
),
|
||||
"E1303": (
|
||||
"Expected mapping for format string, not %s",
|
||||
"format-needs-mapping",
|
||||
"Used when a format string that uses named conversion specifiers "
|
||||
"is used with an argument that is not a mapping.",
|
||||
),
|
||||
"W1300": (
|
||||
"Format string dictionary key should be a string, not %s",
|
||||
"bad-format-string-key",
|
||||
"Used when a format string that uses named conversion specifiers "
|
||||
"is used with a dictionary whose keys are not all strings.",
|
||||
),
|
||||
"W1301": (
|
||||
"Unused key %r in format string dictionary",
|
||||
"unused-format-string-key",
|
||||
"Used when a format string that uses named conversion specifiers "
|
||||
"is used with a dictionary that contains keys not required by the "
|
||||
"format string.",
|
||||
),
|
||||
"E1304": (
|
||||
"Missing key %r in format string dictionary",
|
||||
"missing-format-string-key",
|
||||
"Used when a format string that uses named conversion specifiers "
|
||||
"is used with a dictionary that doesn't contain all the keys "
|
||||
"required by the format string.",
|
||||
),
|
||||
"E1305": (
|
||||
"Too many arguments for format string",
|
||||
"too-many-format-args",
|
||||
"Used when a format string that uses unnamed conversion "
|
||||
"specifiers is given too many arguments.",
|
||||
),
|
||||
"E1306": (
|
||||
"Not enough arguments for format string",
|
||||
"too-few-format-args",
|
||||
"Used when a format string that uses unnamed conversion "
|
||||
"specifiers is given too few arguments",
|
||||
),
|
||||
"E1307": (
|
||||
"Argument %r does not match format type %r",
|
||||
"bad-string-format-type",
|
||||
"Used when a type required by format string "
|
||||
"is not suitable for actual argument type",
|
||||
),
|
||||
"E1310": (
|
||||
"Suspicious argument in %s.%s call",
|
||||
"bad-str-strip-call",
|
||||
"The argument to a str.{l,r,}strip call contains a duplicate character, ",
|
||||
),
|
||||
"W1302": (
|
||||
"Invalid format string",
|
||||
"bad-format-string",
|
||||
"Used when a PEP 3101 format string is invalid.",
|
||||
),
|
||||
"W1303": (
|
||||
"Missing keyword argument %r for format string",
|
||||
"missing-format-argument-key",
|
||||
"Used when a PEP 3101 format string that uses named fields "
|
||||
"doesn't receive one or more required keywords.",
|
||||
),
|
||||
"W1304": (
|
||||
"Unused format argument %r",
|
||||
"unused-format-string-argument",
|
||||
"Used when a PEP 3101 format string that uses named "
|
||||
"fields is used with an argument that "
|
||||
"is not required by the format string.",
|
||||
),
|
||||
"W1305": (
|
||||
"Format string contains both automatic field numbering "
|
||||
"and manual field specification",
|
||||
"format-combined-specification",
|
||||
"Used when a PEP 3101 format string contains both automatic "
|
||||
"field numbering (e.g. '{}') and manual field "
|
||||
"specification (e.g. '{0}').",
|
||||
),
|
||||
"W1306": (
|
||||
"Missing format attribute %r in format specifier %r",
|
||||
"missing-format-attribute",
|
||||
"Used when a PEP 3101 format string uses an "
|
||||
"attribute specifier ({0.length}), but the argument "
|
||||
"passed for formatting doesn't have that attribute.",
|
||||
),
|
||||
"W1307": (
|
||||
"Using invalid lookup key %r in format specifier %r",
|
||||
"invalid-format-index",
|
||||
"Used when a PEP 3101 format string uses a lookup specifier "
|
||||
"({a[1]}), but the argument passed for formatting "
|
||||
"doesn't contain or doesn't have that key as an attribute.",
|
||||
),
|
||||
"W1308": (
|
||||
"Duplicate string formatting argument %r, consider passing as named argument",
|
||||
"duplicate-string-formatting-argument",
|
||||
"Used when we detect that a string formatting is "
|
||||
"repeating an argument instead of using named string arguments",
|
||||
),
|
||||
"W1309": (
|
||||
"Using an f-string that does not have any interpolated variables",
|
||||
"f-string-without-interpolation",
|
||||
"Used when we detect an f-string that does not use any interpolation variables, "
|
||||
"in which case it can be either a normal string or a bug in the code.",
|
||||
),
|
||||
}
|
||||
|
||||
OTHER_NODES = (
|
||||
astroid.Const,
|
||||
astroid.List,
|
||||
astroid.Lambda,
|
||||
astroid.FunctionDef,
|
||||
astroid.ListComp,
|
||||
astroid.SetComp,
|
||||
astroid.GeneratorExp,
|
||||
)
|
||||
|
||||
BUILTINS_STR = builtins.__name__ + ".str"
|
||||
BUILTINS_FLOAT = builtins.__name__ + ".float"
|
||||
BUILTINS_INT = builtins.__name__ + ".int"
|
||||
|
||||
|
||||
def get_access_path(key, parts):
|
||||
""" Given a list of format specifiers, returns
|
||||
the final access path (e.g. a.b.c[0][1]).
|
||||
"""
|
||||
path = []
|
||||
for is_attribute, specifier in parts:
|
||||
if is_attribute:
|
||||
path.append(".{}".format(specifier))
|
||||
else:
|
||||
path.append("[{!r}]".format(specifier))
|
||||
return str(key) + "".join(path)
|
||||
|
||||
|
||||
def arg_matches_format_type(arg_type, format_type):
|
||||
if format_type in "sr":
|
||||
# All types can be printed with %s and %r
|
||||
return True
|
||||
if isinstance(arg_type, astroid.Instance):
|
||||
arg_type = arg_type.pytype()
|
||||
if arg_type == BUILTINS_STR:
|
||||
return format_type == "c"
|
||||
if arg_type == BUILTINS_FLOAT:
|
||||
return format_type in "deEfFgGn%"
|
||||
if arg_type == BUILTINS_INT:
|
||||
# Integers allow all types
|
||||
return True
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class StringFormatChecker(BaseChecker):
|
||||
"""Checks string formatting operations to ensure that the format string
|
||||
is valid and the arguments match the format string.
|
||||
"""
|
||||
|
||||
__implements__ = (IAstroidChecker,)
|
||||
name = "string"
|
||||
msgs = MSGS
|
||||
|
||||
# pylint: disable=too-many-branches
|
||||
@check_messages(
|
||||
"bad-format-character",
|
||||
"truncated-format-string",
|
||||
"mixed-format-string",
|
||||
"bad-format-string-key",
|
||||
"missing-format-string-key",
|
||||
"unused-format-string-key",
|
||||
"bad-string-format-type",
|
||||
"format-needs-mapping",
|
||||
"too-many-format-args",
|
||||
"too-few-format-args",
|
||||
"bad-string-format-type",
|
||||
)
|
||||
def visit_binop(self, node):
|
||||
if node.op != "%":
|
||||
return
|
||||
left = node.left
|
||||
args = node.right
|
||||
|
||||
if not (isinstance(left, astroid.Const) and isinstance(left.value, str)):
|
||||
return
|
||||
format_string = left.value
|
||||
try:
|
||||
(
|
||||
required_keys,
|
||||
required_num_args,
|
||||
required_key_types,
|
||||
required_arg_types,
|
||||
) = utils.parse_format_string(format_string)
|
||||
except utils.UnsupportedFormatCharacter as exc:
|
||||
formatted = format_string[exc.index]
|
||||
self.add_message(
|
||||
"bad-format-character",
|
||||
node=node,
|
||||
args=(formatted, ord(formatted), exc.index),
|
||||
)
|
||||
return
|
||||
except utils.IncompleteFormatString:
|
||||
self.add_message("truncated-format-string", node=node)
|
||||
return
|
||||
if required_keys and required_num_args:
|
||||
# The format string uses both named and unnamed format
|
||||
# specifiers.
|
||||
self.add_message("mixed-format-string", node=node)
|
||||
elif required_keys:
|
||||
# The format string uses only named format specifiers.
|
||||
# Check that the RHS of the % operator is a mapping object
|
||||
# that contains precisely the set of keys required by the
|
||||
# format string.
|
||||
if isinstance(args, astroid.Dict):
|
||||
keys = set()
|
||||
unknown_keys = False
|
||||
for k, _ in args.items:
|
||||
if isinstance(k, astroid.Const):
|
||||
key = k.value
|
||||
if isinstance(key, str):
|
||||
keys.add(key)
|
||||
else:
|
||||
self.add_message(
|
||||
"bad-format-string-key", node=node, args=key
|
||||
)
|
||||
else:
|
||||
# One of the keys was something other than a
|
||||
# constant. Since we can't tell what it is,
|
||||
# suppress checks for missing keys in the
|
||||
# dictionary.
|
||||
unknown_keys = True
|
||||
if not unknown_keys:
|
||||
for key in required_keys:
|
||||
if key not in keys:
|
||||
self.add_message(
|
||||
"missing-format-string-key", node=node, args=key
|
||||
)
|
||||
for key in keys:
|
||||
if key not in required_keys:
|
||||
self.add_message(
|
||||
"unused-format-string-key", node=node, args=key
|
||||
)
|
||||
for key, arg in args.items:
|
||||
if not isinstance(key, astroid.Const):
|
||||
continue
|
||||
format_type = required_key_types.get(key.value, None)
|
||||
arg_type = utils.safe_infer(arg)
|
||||
if (
|
||||
format_type is not None
|
||||
and arg_type not in (None, astroid.Uninferable)
|
||||
and not arg_matches_format_type(arg_type, format_type)
|
||||
):
|
||||
self.add_message(
|
||||
"bad-string-format-type",
|
||||
node=node,
|
||||
args=(arg_type.pytype(), format_type),
|
||||
)
|
||||
elif isinstance(args, (OTHER_NODES, astroid.Tuple)):
|
||||
type_name = type(args).__name__
|
||||
self.add_message("format-needs-mapping", node=node, args=type_name)
|
||||
# else:
|
||||
# The RHS of the format specifier is a name or
|
||||
# expression. It may be a mapping object, so
|
||||
# there's nothing we can check.
|
||||
else:
|
||||
# The format string uses only unnamed format specifiers.
|
||||
# Check that the number of arguments passed to the RHS of
|
||||
# the % operator matches the number required by the format
|
||||
# string.
|
||||
args_elts = ()
|
||||
if isinstance(args, astroid.Tuple):
|
||||
rhs_tuple = utils.safe_infer(args)
|
||||
num_args = None
|
||||
if hasattr(rhs_tuple, "elts"):
|
||||
args_elts = rhs_tuple.elts
|
||||
num_args = len(args_elts)
|
||||
elif isinstance(args, (OTHER_NODES, (astroid.Dict, astroid.DictComp))):
|
||||
args_elts = [args]
|
||||
num_args = 1
|
||||
else:
|
||||
# The RHS of the format specifier is a name or
|
||||
# expression. It could be a tuple of unknown size, so
|
||||
# there's nothing we can check.
|
||||
num_args = None
|
||||
if num_args is not None:
|
||||
if num_args > required_num_args:
|
||||
self.add_message("too-many-format-args", node=node)
|
||||
elif num_args < required_num_args:
|
||||
self.add_message("too-few-format-args", node=node)
|
||||
for arg, format_type in zip(args_elts, required_arg_types):
|
||||
if not arg:
|
||||
continue
|
||||
arg_type = utils.safe_infer(arg)
|
||||
if arg_type not in (
|
||||
None,
|
||||
astroid.Uninferable,
|
||||
) and not arg_matches_format_type(arg_type, format_type):
|
||||
self.add_message(
|
||||
"bad-string-format-type",
|
||||
node=node,
|
||||
args=(arg_type.pytype(), format_type),
|
||||
)
|
||||
|
||||
@check_messages("f-string-without-interpolation")
|
||||
def visit_joinedstr(self, node):
|
||||
if isinstance(node.parent, astroid.FormattedValue):
|
||||
return
|
||||
for value in node.values:
|
||||
if isinstance(value, astroid.FormattedValue):
|
||||
return
|
||||
self.add_message("f-string-without-interpolation", node=node)
|
||||
|
||||
@check_messages(*MSGS)
|
||||
def visit_call(self, node):
|
||||
func = utils.safe_infer(node.func)
|
||||
if (
|
||||
isinstance(func, astroid.BoundMethod)
|
||||
and isinstance(func.bound, astroid.Instance)
|
||||
and func.bound.name in ("str", "unicode", "bytes")
|
||||
):
|
||||
if func.name in ("strip", "lstrip", "rstrip") and node.args:
|
||||
arg = utils.safe_infer(node.args[0])
|
||||
if not isinstance(arg, astroid.Const) or not isinstance(arg.value, str):
|
||||
return
|
||||
if len(arg.value) != len(set(arg.value)):
|
||||
self.add_message(
|
||||
"bad-str-strip-call",
|
||||
node=node,
|
||||
args=(func.bound.name, func.name),
|
||||
)
|
||||
elif func.name == "format":
|
||||
self._check_new_format(node, func)
|
||||
|
||||
def _detect_vacuous_formatting(self, node, positional_arguments):
|
||||
counter = collections.Counter(
|
||||
arg.name for arg in positional_arguments if isinstance(arg, astroid.Name)
|
||||
)
|
||||
for name, count in counter.items():
|
||||
if count == 1:
|
||||
continue
|
||||
self.add_message(
|
||||
"duplicate-string-formatting-argument", node=node, args=(name,)
|
||||
)
|
||||
|
||||
def _check_new_format(self, node, func):
|
||||
"""Check the new string formatting. """
|
||||
# Skip ormat nodes which don't have an explicit string on the
|
||||
# left side of the format operation.
|
||||
# We do this because our inference engine can't properly handle
|
||||
# redefinitions of the original string.
|
||||
# Note that there may not be any left side at all, if the format method
|
||||
# has been assigned to another variable. See issue 351. For example:
|
||||
#
|
||||
# fmt = 'some string {}'.format
|
||||
# fmt('arg')
|
||||
if isinstance(node.func, astroid.Attribute) and not isinstance(
|
||||
node.func.expr, astroid.Const
|
||||
):
|
||||
return
|
||||
if node.starargs or node.kwargs:
|
||||
return
|
||||
try:
|
||||
strnode = next(func.bound.infer())
|
||||
except astroid.InferenceError:
|
||||
return
|
||||
if not (isinstance(strnode, astroid.Const) and isinstance(strnode.value, str)):
|
||||
return
|
||||
try:
|
||||
call_site = CallSite.from_call(node)
|
||||
except astroid.InferenceError:
|
||||
return
|
||||
|
||||
try:
|
||||
fields, num_args, manual_pos = utils.parse_format_method_string(
|
||||
strnode.value
|
||||
)
|
||||
except utils.IncompleteFormatString:
|
||||
self.add_message("bad-format-string", node=node)
|
||||
return
|
||||
|
||||
positional_arguments = call_site.positional_arguments
|
||||
named_arguments = call_site.keyword_arguments
|
||||
named_fields = {field[0] for field in fields if isinstance(field[0], str)}
|
||||
if num_args and manual_pos:
|
||||
self.add_message("format-combined-specification", node=node)
|
||||
return
|
||||
|
||||
check_args = False
|
||||
# Consider "{[0]} {[1]}" as num_args.
|
||||
num_args += sum(1 for field in named_fields if field == "")
|
||||
if named_fields:
|
||||
for field in named_fields:
|
||||
if field and field not in named_arguments:
|
||||
self.add_message(
|
||||
"missing-format-argument-key", node=node, args=(field,)
|
||||
)
|
||||
for field in named_arguments:
|
||||
if field not in named_fields:
|
||||
self.add_message(
|
||||
"unused-format-string-argument", node=node, args=(field,)
|
||||
)
|
||||
# num_args can be 0 if manual_pos is not.
|
||||
num_args = num_args or manual_pos
|
||||
if positional_arguments or num_args:
|
||||
empty = any(True for field in named_fields if field == "")
|
||||
if named_arguments or empty:
|
||||
# Verify the required number of positional arguments
|
||||
# only if the .format got at least one keyword argument.
|
||||
# This means that the format strings accepts both
|
||||
# positional and named fields and we should warn
|
||||
# when one of the them is missing or is extra.
|
||||
check_args = True
|
||||
else:
|
||||
check_args = True
|
||||
if check_args:
|
||||
# num_args can be 0 if manual_pos is not.
|
||||
num_args = num_args or manual_pos
|
||||
if len(positional_arguments) > num_args:
|
||||
self.add_message("too-many-format-args", node=node)
|
||||
elif len(positional_arguments) < num_args:
|
||||
self.add_message("too-few-format-args", node=node)
|
||||
|
||||
self._detect_vacuous_formatting(node, positional_arguments)
|
||||
self._check_new_format_specifiers(node, fields, named_arguments)
|
||||
|
||||
def _check_new_format_specifiers(self, node, fields, named):
|
||||
"""
|
||||
Check attribute and index access in the format
|
||||
string ("{0.a}" and "{0[a]}").
|
||||
"""
|
||||
for key, specifiers in fields:
|
||||
# Obtain the argument. If it can't be obtained
|
||||
# or inferred, skip this check.
|
||||
if key == "":
|
||||
# {[0]} will have an unnamed argument, defaulting
|
||||
# to 0. It will not be present in `named`, so use the value
|
||||
# 0 for it.
|
||||
key = 0
|
||||
if isinstance(key, numbers.Number):
|
||||
try:
|
||||
argname = utils.get_argument_from_call(node, key)
|
||||
except utils.NoSuchArgumentError:
|
||||
continue
|
||||
else:
|
||||
if key not in named:
|
||||
continue
|
||||
argname = named[key]
|
||||
if argname in (astroid.Uninferable, None):
|
||||
continue
|
||||
try:
|
||||
argument = utils.safe_infer(argname)
|
||||
except astroid.InferenceError:
|
||||
continue
|
||||
if not specifiers or not argument:
|
||||
# No need to check this key if it doesn't
|
||||
# use attribute / item access
|
||||
continue
|
||||
if argument.parent and isinstance(argument.parent, astroid.Arguments):
|
||||
# Ignore any object coming from an argument,
|
||||
# because we can't infer its value properly.
|
||||
continue
|
||||
previous = argument
|
||||
parsed = []
|
||||
for is_attribute, specifier in specifiers:
|
||||
if previous is astroid.Uninferable:
|
||||
break
|
||||
parsed.append((is_attribute, specifier))
|
||||
if is_attribute:
|
||||
try:
|
||||
previous = previous.getattr(specifier)[0]
|
||||
except astroid.NotFoundError:
|
||||
if (
|
||||
hasattr(previous, "has_dynamic_getattr")
|
||||
and previous.has_dynamic_getattr()
|
||||
):
|
||||
# Don't warn if the object has a custom __getattr__
|
||||
break
|
||||
path = get_access_path(key, parsed)
|
||||
self.add_message(
|
||||
"missing-format-attribute",
|
||||
args=(specifier, path),
|
||||
node=node,
|
||||
)
|
||||
break
|
||||
else:
|
||||
warn_error = False
|
||||
if hasattr(previous, "getitem"):
|
||||
try:
|
||||
previous = previous.getitem(astroid.Const(specifier))
|
||||
except (
|
||||
astroid.AstroidIndexError,
|
||||
astroid.AstroidTypeError,
|
||||
astroid.AttributeInferenceError,
|
||||
):
|
||||
warn_error = True
|
||||
except astroid.InferenceError:
|
||||
break
|
||||
if previous is astroid.Uninferable:
|
||||
break
|
||||
else:
|
||||
try:
|
||||
# Lookup __getitem__ in the current node,
|
||||
# but skip further checks, because we can't
|
||||
# retrieve the looked object
|
||||
previous.getattr("__getitem__")
|
||||
break
|
||||
except astroid.NotFoundError:
|
||||
warn_error = True
|
||||
if warn_error:
|
||||
path = get_access_path(key, parsed)
|
||||
self.add_message(
|
||||
"invalid-format-index", args=(specifier, path), node=node
|
||||
)
|
||||
break
|
||||
|
||||
try:
|
||||
previous = next(previous.infer())
|
||||
except astroid.InferenceError:
|
||||
# can't check further if we can't infer it
|
||||
break
|
||||
|
||||
|
||||
class StringConstantChecker(BaseTokenChecker):
|
||||
"""Check string literals"""
|
||||
|
||||
__implements__ = (IAstroidChecker, ITokenChecker, IRawChecker)
|
||||
name = "string"
|
||||
msgs = {
|
||||
"W1401": (
|
||||
"Anomalous backslash in string: '%s'. "
|
||||
"String constant might be missing an r prefix.",
|
||||
"anomalous-backslash-in-string",
|
||||
"Used when a backslash is in a literal string but not as an escape.",
|
||||
),
|
||||
"W1402": (
|
||||
"Anomalous Unicode escape in byte string: '%s'. "
|
||||
"String constant might be missing an r or u prefix.",
|
||||
"anomalous-unicode-escape-in-string",
|
||||
"Used when an escape like \\u is encountered in a byte "
|
||||
"string where it has no effect.",
|
||||
),
|
||||
"W1404": (
|
||||
"Implicit string concatenation found in %s",
|
||||
"implicit-str-concat",
|
||||
"String literals are implicitly concatenated in a "
|
||||
"literal iterable definition : "
|
||||
"maybe a comma is missing ?",
|
||||
{"old_names": [("W1403", "implicit-str-concat-in-sequence")]},
|
||||
),
|
||||
"W1405": (
|
||||
"Quote delimiter %s is inconsistent with the rest of the file",
|
||||
"inconsistent-quotes",
|
||||
"Quote delimiters are not used consistently throughout a module "
|
||||
"(with allowances made for avoiding unnecessary escaping).",
|
||||
),
|
||||
}
|
||||
options = (
|
||||
(
|
||||
"check-str-concat-over-line-jumps",
|
||||
{
|
||||
"default": False,
|
||||
"type": "yn",
|
||||
"metavar": "<y_or_n>",
|
||||
"help": "This flag controls whether the "
|
||||
"implicit-str-concat should generate a warning "
|
||||
"on implicit string concatenation in sequences defined over "
|
||||
"several lines.",
|
||||
},
|
||||
),
|
||||
(
|
||||
"check-quote-consistency",
|
||||
{
|
||||
"default": False,
|
||||
"type": "yn",
|
||||
"metavar": "<y_or_n>",
|
||||
"help": "This flag controls whether inconsistent-quotes generates a "
|
||||
"warning when the character used as a quote delimiter is used "
|
||||
"inconsistently within a module.",
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
# Characters that have a special meaning after a backslash in either
|
||||
# Unicode or byte strings.
|
||||
ESCAPE_CHARACTERS = "abfnrtvx\n\r\t\\'\"01234567"
|
||||
|
||||
# Characters that have a special meaning after a backslash but only in
|
||||
# Unicode strings.
|
||||
UNICODE_ESCAPE_CHARACTERS = "uUN"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.string_tokens = {} # token position -> (token value, next token)
|
||||
|
||||
def process_module(self, module):
|
||||
self._unicode_literals = "unicode_literals" in module.future_imports
|
||||
|
||||
def process_tokens(self, tokens):
|
||||
encoding = "ascii"
|
||||
for i, (tok_type, token, start, _, line) in enumerate(tokens):
|
||||
if tok_type == tokenize.ENCODING:
|
||||
# this is always the first token processed
|
||||
encoding = token
|
||||
elif tok_type == tokenize.STRING:
|
||||
# 'token' is the whole un-parsed token; we can look at the start
|
||||
# of it to see whether it's a raw or unicode string etc.
|
||||
self.process_string_token(token, start[0])
|
||||
# We figure the next token, ignoring comments & newlines:
|
||||
j = i + 1
|
||||
while j < len(tokens) and tokens[j].type in (
|
||||
tokenize.NEWLINE,
|
||||
tokenize.NL,
|
||||
tokenize.COMMENT,
|
||||
):
|
||||
j += 1
|
||||
next_token = tokens[j] if j < len(tokens) else None
|
||||
if encoding != "ascii":
|
||||
# We convert `tokenize` character count into a byte count,
|
||||
# to match with astroid `.col_offset`
|
||||
start = (start[0], len(line[: start[1]].encode(encoding)))
|
||||
self.string_tokens[start] = (str_eval(token), next_token)
|
||||
|
||||
if self.config.check_quote_consistency:
|
||||
self.check_for_consistent_string_delimiters(tokens)
|
||||
|
||||
@check_messages("implicit-str-concat")
|
||||
def visit_list(self, node):
|
||||
self.check_for_concatenated_strings(node.elts, "list")
|
||||
|
||||
@check_messages("implicit-str-concat")
|
||||
def visit_set(self, node):
|
||||
self.check_for_concatenated_strings(node.elts, "set")
|
||||
|
||||
@check_messages("implicit-str-concat")
|
||||
def visit_tuple(self, node):
|
||||
self.check_for_concatenated_strings(node.elts, "tuple")
|
||||
|
||||
def visit_assign(self, node):
|
||||
if isinstance(node.value, astroid.Const) and isinstance(node.value.value, str):
|
||||
self.check_for_concatenated_strings([node.value], "assignment")
|
||||
|
||||
def check_for_consistent_string_delimiters(
|
||||
self, tokens: Iterable[tokenize.TokenInfo]
|
||||
) -> None:
|
||||
"""Adds a message for each string using inconsistent quote delimiters.
|
||||
|
||||
Quote delimiters are used inconsistently if " and ' are mixed in a module's
|
||||
shortstrings without having done so to avoid escaping an internal quote
|
||||
character.
|
||||
|
||||
Args:
|
||||
tokens: The tokens to be checked against for consistent usage.
|
||||
"""
|
||||
string_delimiters = collections.Counter() # type: typing.Counter[str]
|
||||
|
||||
# First, figure out which quote character predominates in the module
|
||||
for tok_type, token, _, _, _ in tokens:
|
||||
if tok_type == tokenize.STRING and _is_quote_delimiter_chosen_freely(token):
|
||||
string_delimiters[_get_quote_delimiter(token)] += 1
|
||||
|
||||
if len(string_delimiters) > 1:
|
||||
# Ties are broken arbitrarily
|
||||
most_common_delimiter = string_delimiters.most_common(1)[0][0]
|
||||
for tok_type, token, start, _, _ in tokens:
|
||||
if tok_type != tokenize.STRING:
|
||||
continue
|
||||
quote_delimiter = _get_quote_delimiter(token)
|
||||
if (
|
||||
_is_quote_delimiter_chosen_freely(token)
|
||||
and quote_delimiter != most_common_delimiter
|
||||
):
|
||||
self.add_message(
|
||||
"inconsistent-quotes", line=start[0], args=(quote_delimiter,)
|
||||
)
|
||||
|
||||
def check_for_concatenated_strings(self, elements, iterable_type):
|
||||
for elt in elements:
|
||||
if not (isinstance(elt, Const) and elt.pytype() in _AST_NODE_STR_TYPES):
|
||||
continue
|
||||
if elt.col_offset < 0:
|
||||
# This can happen in case of escaped newlines
|
||||
continue
|
||||
if (elt.lineno, elt.col_offset) not in self.string_tokens:
|
||||
# This may happen with Latin1 encoding
|
||||
# cf. https://github.com/PyCQA/pylint/issues/2610
|
||||
continue
|
||||
matching_token, next_token = self.string_tokens[
|
||||
(elt.lineno, elt.col_offset)
|
||||
]
|
||||
# We detect string concatenation: the AST Const is the
|
||||
# combination of 2 string tokens
|
||||
if matching_token != elt.value and next_token is not None:
|
||||
if next_token.type == tokenize.STRING and (
|
||||
next_token.start[0] == elt.lineno
|
||||
or self.config.check_str_concat_over_line_jumps
|
||||
):
|
||||
self.add_message(
|
||||
"implicit-str-concat", line=elt.lineno, args=(iterable_type,)
|
||||
)
|
||||
|
||||
def process_string_token(self, token, start_row):
|
||||
quote_char = None
|
||||
index = None
|
||||
for index, char in enumerate(token):
|
||||
if char in "'\"":
|
||||
quote_char = char
|
||||
break
|
||||
if quote_char is None:
|
||||
return
|
||||
|
||||
prefix = token[:index].lower() # markers like u, b, r.
|
||||
after_prefix = token[index:]
|
||||
if after_prefix[:3] == after_prefix[-3:] == 3 * quote_char:
|
||||
string_body = after_prefix[3:-3]
|
||||
else:
|
||||
string_body = after_prefix[1:-1] # Chop off quotes
|
||||
# No special checks on raw strings at the moment.
|
||||
if "r" not in prefix:
|
||||
self.process_non_raw_string_token(prefix, string_body, start_row)
|
||||
|
||||
def process_non_raw_string_token(self, prefix, string_body, start_row):
|
||||
"""check for bad escapes in a non-raw string.
|
||||
|
||||
prefix: lowercase string of eg 'ur' string prefix markers.
|
||||
string_body: the un-parsed body of the string, not including the quote
|
||||
marks.
|
||||
start_row: integer line number in the source.
|
||||
"""
|
||||
# Walk through the string; if we see a backslash then escape the next
|
||||
# character, and skip over it. If we see a non-escaped character,
|
||||
# alert, and continue.
|
||||
#
|
||||
# Accept a backslash when it escapes a backslash, or a quote, or
|
||||
# end-of-line, or one of the letters that introduce a special escape
|
||||
# sequence <http://docs.python.org/reference/lexical_analysis.html>
|
||||
#
|
||||
index = 0
|
||||
while True:
|
||||
index = string_body.find("\\", index)
|
||||
if index == -1:
|
||||
break
|
||||
# There must be a next character; having a backslash at the end
|
||||
# of the string would be a SyntaxError.
|
||||
next_char = string_body[index + 1]
|
||||
match = string_body[index : index + 2]
|
||||
if next_char in self.UNICODE_ESCAPE_CHARACTERS:
|
||||
if "u" in prefix:
|
||||
pass
|
||||
elif "b" not in prefix:
|
||||
pass # unicode by default
|
||||
else:
|
||||
self.add_message(
|
||||
"anomalous-unicode-escape-in-string",
|
||||
line=start_row,
|
||||
args=(match,),
|
||||
col_offset=index,
|
||||
)
|
||||
elif next_char not in self.ESCAPE_CHARACTERS:
|
||||
self.add_message(
|
||||
"anomalous-backslash-in-string",
|
||||
line=start_row,
|
||||
args=(match,),
|
||||
col_offset=index,
|
||||
)
|
||||
# Whether it was a valid escape or not, backslash followed by
|
||||
# another character can always be consumed whole: the second
|
||||
# character can never be the start of a new backslash escape.
|
||||
index += 2
|
||||
|
||||
|
||||
def register(linter):
|
||||
"""required method to auto register this checker """
|
||||
linter.register_checker(StringFormatChecker(linter))
|
||||
linter.register_checker(StringConstantChecker(linter))
|
||||
|
||||
|
||||
def str_eval(token):
|
||||
"""
|
||||
Mostly replicate `ast.literal_eval(token)` manually to avoid any performance hit.
|
||||
This supports f-strings, contrary to `ast.literal_eval`.
|
||||
We have to support all string literal notations:
|
||||
https://docs.python.org/3/reference/lexical_analysis.html#string-and-bytes-literals
|
||||
"""
|
||||
if token[0:2].lower() in ("fr", "rf"):
|
||||
token = token[2:]
|
||||
elif token[0].lower() in ("r", "u", "f"):
|
||||
token = token[1:]
|
||||
if token[0:3] in ('"""', "'''"):
|
||||
return token[3:-3]
|
||||
return token[1:-1]
|
||||
|
||||
|
||||
def _is_long_string(string_token: str) -> bool:
|
||||
"""Is this string token a "longstring" (is it triple-quoted)?
|
||||
|
||||
Long strings are triple-quoted as defined in
|
||||
https://docs.python.org/3/reference/lexical_analysis.html#string-and-bytes-literals
|
||||
|
||||
This function only checks characters up through the open quotes. Because it's meant
|
||||
to be applied only to tokens that represent string literals, it doesn't bother to
|
||||
check for close-quotes (demonstrating that the literal is a well-formed string).
|
||||
|
||||
Args:
|
||||
string_token: The string token to be parsed.
|
||||
|
||||
Returns:
|
||||
A boolean representing whether or not this token matches a longstring
|
||||
regex.
|
||||
"""
|
||||
return bool(
|
||||
SINGLE_QUOTED_REGEX.match(string_token)
|
||||
or DOUBLE_QUOTED_REGEX.match(string_token)
|
||||
)
|
||||
|
||||
|
||||
def _get_quote_delimiter(string_token: str) -> str:
|
||||
"""Returns the quote character used to delimit this token string.
|
||||
|
||||
This function does little checking for whether the token is a well-formed
|
||||
string.
|
||||
|
||||
Args:
|
||||
string_token: The token to be parsed.
|
||||
|
||||
Returns:
|
||||
A string containing solely the first quote delimiter character in the passed
|
||||
string.
|
||||
|
||||
Raises:
|
||||
ValueError: No quote delimiter characters are present.
|
||||
"""
|
||||
match = QUOTE_DELIMITER_REGEX.match(string_token)
|
||||
if not match:
|
||||
raise ValueError("string token %s is not a well-formed string" % string_token)
|
||||
return match.group(2)
|
||||
|
||||
|
||||
def _is_quote_delimiter_chosen_freely(string_token: str) -> bool:
|
||||
"""Was there a non-awkward option for the quote delimiter?
|
||||
|
||||
Args:
|
||||
string_token: The quoted string whose delimiters are to be checked.
|
||||
|
||||
Returns:
|
||||
Whether there was a choice in this token's quote character that would
|
||||
not have involved backslash-escaping an interior quote character. Long
|
||||
strings are excepted from this analysis under the assumption that their
|
||||
quote characters are set by policy.
|
||||
"""
|
||||
quote_delimiter = _get_quote_delimiter(string_token)
|
||||
unchosen_delimiter = '"' if quote_delimiter == "'" else "'"
|
||||
return bool(
|
||||
quote_delimiter
|
||||
and not _is_long_string(string_token)
|
||||
and unchosen_delimiter not in str_eval(string_token)
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,977 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2006-2010, 2012-2014 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
|
||||
# Copyright (c) 2008 pyves@crater.logilab.fr <pyves@crater.logilab.fr>
|
||||
# Copyright (c) 2010 Julien Jehannet <julien.jehannet@logilab.fr>
|
||||
# Copyright (c) 2013 Google, Inc.
|
||||
# Copyright (c) 2013 John McGehee <jmcgehee@altera.com>
|
||||
# Copyright (c) 2014-2020 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2014 Brett Cannon <brett@python.org>
|
||||
# Copyright (c) 2014 Arun Persaud <arun@nubati.net>
|
||||
# Copyright (c) 2015 Aru Sahni <arusahni@gmail.com>
|
||||
# Copyright (c) 2015 John Kirkham <jakirkham@gmail.com>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
|
||||
# Copyright (c) 2016 Erik <erik.eriksson@yahoo.com>
|
||||
# Copyright (c) 2016 Alexander Todorov <atodorov@otb.bg>
|
||||
# Copyright (c) 2016 Moises Lopez <moylop260@vauxoo.com>
|
||||
# Copyright (c) 2017-2019 Ville Skyttä <ville.skytta@iki.fi>
|
||||
# Copyright (c) 2017 hippo91 <guillaume.peillex@gmail.com>
|
||||
# Copyright (c) 2017 ahirnish <ahirnish@gmail.com>
|
||||
# Copyright (c) 2017 Łukasz Rogalski <rogalski.91@gmail.com>
|
||||
# Copyright (c) 2018, 2020 Anthony Sottile <asottile@umich.edu>
|
||||
# Copyright (c) 2018 Jim Robertson <jrobertson98atx@gmail.com>
|
||||
# Copyright (c) 2018 ssolanki <sushobhitsolanki@gmail.com>
|
||||
# Copyright (c) 2018 Bryce Guinta <bryce.paul.guinta@gmail.com>
|
||||
# Copyright (c) 2018 Sushobhit <31987769+sushobhit27@users.noreply.github.com>
|
||||
# Copyright (c) 2018 Gary Tyler McLeod <mail@garytyler.com>
|
||||
# Copyright (c) 2018 Konstantin <Github@pheanex.de>
|
||||
# Copyright (c) 2018 Nick Drozd <nicholasdrozd@gmail.com>
|
||||
# Copyright (c) 2019 Janne Rönkkö <jannero@users.noreply.github.com>
|
||||
# Copyright (c) 2019 Ashley Whetter <ashley@awhetter.co.uk>
|
||||
# Copyright (c) 2019 Hugo van Kemenade <hugovk@users.noreply.github.com>
|
||||
# Copyright (c) 2019 Pierre Sassoulas <pierre.sassoulas@gmail.com>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""utilities for Pylint configuration :
|
||||
|
||||
* pylintrc
|
||||
* pylint.d (PYLINTHOME)
|
||||
"""
|
||||
import collections
|
||||
import configparser
|
||||
import contextlib
|
||||
import copy
|
||||
import functools
|
||||
import optparse
|
||||
import os
|
||||
import pickle
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
from typing import Any, Dict, Tuple
|
||||
|
||||
import toml
|
||||
|
||||
from pylint import utils
|
||||
|
||||
USER_HOME = os.path.expanduser("~")
|
||||
if "PYLINTHOME" in os.environ:
|
||||
PYLINT_HOME = os.environ["PYLINTHOME"]
|
||||
if USER_HOME == "~":
|
||||
USER_HOME = os.path.dirname(PYLINT_HOME)
|
||||
elif USER_HOME == "~":
|
||||
PYLINT_HOME = ".pylint.d"
|
||||
else:
|
||||
PYLINT_HOME = os.path.join(USER_HOME, ".pylint.d")
|
||||
|
||||
|
||||
def _get_pdata_path(base_name, recurs):
|
||||
base_name = base_name.replace(os.sep, "_")
|
||||
return os.path.join(PYLINT_HOME, "%s%s%s" % (base_name, recurs, ".stats"))
|
||||
|
||||
|
||||
def load_results(base):
|
||||
data_file = _get_pdata_path(base, 1)
|
||||
try:
|
||||
with open(data_file, "rb") as stream:
|
||||
return pickle.load(stream)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
return {}
|
||||
|
||||
|
||||
def save_results(results, base):
|
||||
if not os.path.exists(PYLINT_HOME):
|
||||
try:
|
||||
os.mkdir(PYLINT_HOME)
|
||||
except OSError:
|
||||
print("Unable to create directory %s" % PYLINT_HOME, file=sys.stderr)
|
||||
data_file = _get_pdata_path(base, 1)
|
||||
try:
|
||||
with open(data_file, "wb") as stream:
|
||||
pickle.dump(results, stream)
|
||||
except OSError as ex:
|
||||
print("Unable to create file %s: %s" % (data_file, ex), file=sys.stderr)
|
||||
|
||||
|
||||
def _toml_has_config(path):
|
||||
with open(path) as toml_handle:
|
||||
content = toml.load(toml_handle)
|
||||
try:
|
||||
content["tool"]["pylint"]
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _cfg_has_config(path):
|
||||
parser = configparser.ConfigParser()
|
||||
parser.read(path)
|
||||
return any(section.startswith("pylint.") for section in parser.sections())
|
||||
|
||||
|
||||
def find_default_config_files():
|
||||
"""Find all possible config files."""
|
||||
rc_names = ("pylintrc", ".pylintrc")
|
||||
config_names = rc_names + ("pyproject.toml", "setup.cfg")
|
||||
for config_name in config_names:
|
||||
if os.path.isfile(config_name):
|
||||
if config_name.endswith(".toml") and not _toml_has_config(config_name):
|
||||
continue
|
||||
if config_name.endswith(".cfg") and not _cfg_has_config(config_name):
|
||||
continue
|
||||
|
||||
yield os.path.abspath(config_name)
|
||||
|
||||
if os.path.isfile("__init__.py"):
|
||||
curdir = os.path.abspath(os.getcwd())
|
||||
while os.path.isfile(os.path.join(curdir, "__init__.py")):
|
||||
curdir = os.path.abspath(os.path.join(curdir, ".."))
|
||||
for rc_name in rc_names:
|
||||
rc_path = os.path.join(curdir, rc_name)
|
||||
if os.path.isfile(rc_path):
|
||||
yield rc_path
|
||||
|
||||
if "PYLINTRC" in os.environ and os.path.exists(os.environ["PYLINTRC"]):
|
||||
if os.path.isfile(os.environ["PYLINTRC"]):
|
||||
yield os.environ["PYLINTRC"]
|
||||
else:
|
||||
user_home = os.path.expanduser("~")
|
||||
if user_home not in ("~", "/root"):
|
||||
home_rc = os.path.join(user_home, ".pylintrc")
|
||||
if os.path.isfile(home_rc):
|
||||
yield home_rc
|
||||
home_rc = os.path.join(user_home, ".config", "pylintrc")
|
||||
if os.path.isfile(home_rc):
|
||||
yield home_rc
|
||||
|
||||
if os.path.isfile("/etc/pylintrc"):
|
||||
yield "/etc/pylintrc"
|
||||
|
||||
|
||||
def find_pylintrc():
|
||||
"""search the pylint rc file and return its path if it find it, else None
|
||||
"""
|
||||
for config_file in find_default_config_files():
|
||||
if config_file.endswith("pylintrc"):
|
||||
return config_file
|
||||
|
||||
return None
|
||||
|
||||
|
||||
PYLINTRC = find_pylintrc()
|
||||
|
||||
ENV_HELP = (
|
||||
"""
|
||||
The following environment variables are used:
|
||||
* PYLINTHOME
|
||||
Path to the directory where persistent data for the run will be stored. If
|
||||
not found, it defaults to ~/.pylint.d/ or .pylint.d (in the current working
|
||||
directory).
|
||||
* PYLINTRC
|
||||
Path to the configuration file. See the documentation for the method used
|
||||
to search for configuration file.
|
||||
"""
|
||||
% globals() # type: ignore
|
||||
)
|
||||
|
||||
|
||||
class UnsupportedAction(Exception):
|
||||
"""raised by set_option when it doesn't know what to do for an action"""
|
||||
|
||||
|
||||
def _multiple_choice_validator(choices, name, value):
|
||||
values = utils._check_csv(value)
|
||||
for csv_value in values:
|
||||
if csv_value not in choices:
|
||||
msg = "option %s: invalid value: %r, should be in %s"
|
||||
raise optparse.OptionValueError(msg % (name, csv_value, choices))
|
||||
return values
|
||||
|
||||
|
||||
def _choice_validator(choices, name, value):
|
||||
if value not in choices:
|
||||
msg = "option %s: invalid value: %r, should be in %s"
|
||||
raise optparse.OptionValueError(msg % (name, value, choices))
|
||||
return value
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def _csv_validator(_, name, value):
|
||||
return utils._check_csv(value)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def _regexp_validator(_, name, value):
|
||||
if hasattr(value, "pattern"):
|
||||
return value
|
||||
return re.compile(value)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def _regexp_csv_validator(_, name, value):
|
||||
return [_regexp_validator(_, name, val) for val in _csv_validator(_, name, value)]
|
||||
|
||||
|
||||
def _yn_validator(opt, _, value):
|
||||
if isinstance(value, int):
|
||||
return bool(value)
|
||||
if value in ("y", "yes"):
|
||||
return True
|
||||
if value in ("n", "no"):
|
||||
return False
|
||||
msg = "option %s: invalid yn value %r, should be in (y, yes, n, no)"
|
||||
raise optparse.OptionValueError(msg % (opt, value))
|
||||
|
||||
|
||||
def _non_empty_string_validator(opt, _, value):
|
||||
if not value:
|
||||
msg = "indent string can't be empty."
|
||||
raise optparse.OptionValueError(msg)
|
||||
return utils._unquote(value)
|
||||
|
||||
|
||||
VALIDATORS = {
|
||||
"string": utils._unquote,
|
||||
"int": int,
|
||||
"regexp": re.compile,
|
||||
"regexp_csv": _regexp_csv_validator,
|
||||
"csv": _csv_validator,
|
||||
"yn": _yn_validator,
|
||||
"choice": lambda opt, name, value: _choice_validator(opt["choices"], name, value),
|
||||
"multiple_choice": lambda opt, name, value: _multiple_choice_validator(
|
||||
opt["choices"], name, value
|
||||
),
|
||||
"non_empty_string": _non_empty_string_validator,
|
||||
}
|
||||
|
||||
|
||||
def _call_validator(opttype, optdict, option, value):
|
||||
if opttype not in VALIDATORS:
|
||||
raise Exception('Unsupported type "%s"' % opttype)
|
||||
try:
|
||||
return VALIDATORS[opttype](optdict, option, value)
|
||||
except TypeError:
|
||||
try:
|
||||
return VALIDATORS[opttype](value)
|
||||
except Exception:
|
||||
raise optparse.OptionValueError(
|
||||
"%s value (%r) should be of type %s" % (option, value, opttype)
|
||||
)
|
||||
|
||||
|
||||
def _validate(value, optdict, name=""):
|
||||
"""return a validated value for an option according to its type
|
||||
|
||||
optional argument name is only used for error message formatting
|
||||
"""
|
||||
try:
|
||||
_type = optdict["type"]
|
||||
except KeyError:
|
||||
return value
|
||||
return _call_validator(_type, optdict, name, value)
|
||||
|
||||
|
||||
def _level_options(group, outputlevel):
|
||||
return [
|
||||
option
|
||||
for option in group.option_list
|
||||
if (getattr(option, "level", 0) or 0) <= outputlevel
|
||||
and option.help is not optparse.SUPPRESS_HELP
|
||||
]
|
||||
|
||||
|
||||
def _expand_default(self, option):
|
||||
"""Patch OptionParser.expand_default with custom behaviour
|
||||
|
||||
This will handle defaults to avoid overriding values in the
|
||||
configuration file.
|
||||
"""
|
||||
if self.parser is None or not self.default_tag:
|
||||
return option.help
|
||||
optname = option._long_opts[0][2:]
|
||||
try:
|
||||
provider = self.parser.options_manager._all_options[optname]
|
||||
except KeyError:
|
||||
value = None
|
||||
else:
|
||||
optdict = provider.get_option_def(optname)
|
||||
optname = provider.option_attrname(optname, optdict)
|
||||
value = getattr(provider.config, optname, optdict)
|
||||
value = utils._format_option_value(optdict, value)
|
||||
if value is optparse.NO_DEFAULT or not value:
|
||||
value = self.NO_DEFAULT_VALUE
|
||||
return option.help.replace(self.default_tag, str(value))
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _patch_optparse():
|
||||
orig_default = optparse.HelpFormatter
|
||||
try:
|
||||
optparse.HelpFormatter.expand_default = _expand_default
|
||||
yield
|
||||
finally:
|
||||
optparse.HelpFormatter.expand_default = orig_default
|
||||
|
||||
|
||||
def _multiple_choices_validating_option(opt, name, value):
|
||||
return _multiple_choice_validator(opt.choices, name, value)
|
||||
|
||||
|
||||
# pylint: disable=no-member
|
||||
class Option(optparse.Option):
|
||||
TYPES = optparse.Option.TYPES + (
|
||||
"regexp",
|
||||
"regexp_csv",
|
||||
"csv",
|
||||
"yn",
|
||||
"multiple_choice",
|
||||
"non_empty_string",
|
||||
)
|
||||
ATTRS = optparse.Option.ATTRS + ["hide", "level"]
|
||||
TYPE_CHECKER = copy.copy(optparse.Option.TYPE_CHECKER)
|
||||
TYPE_CHECKER["regexp"] = _regexp_validator
|
||||
TYPE_CHECKER["regexp_csv"] = _regexp_csv_validator
|
||||
TYPE_CHECKER["csv"] = _csv_validator
|
||||
TYPE_CHECKER["yn"] = _yn_validator
|
||||
TYPE_CHECKER["multiple_choice"] = _multiple_choices_validating_option
|
||||
TYPE_CHECKER["non_empty_string"] = _non_empty_string_validator
|
||||
|
||||
def __init__(self, *opts, **attrs):
|
||||
optparse.Option.__init__(self, *opts, **attrs)
|
||||
if hasattr(self, "hide") and self.hide:
|
||||
self.help = optparse.SUPPRESS_HELP
|
||||
|
||||
def _check_choice(self):
|
||||
if self.type in ("choice", "multiple_choice"):
|
||||
if self.choices is None:
|
||||
raise optparse.OptionError(
|
||||
"must supply a list of choices for type 'choice'", self
|
||||
)
|
||||
if not isinstance(self.choices, (tuple, list)):
|
||||
raise optparse.OptionError(
|
||||
"choices must be a list of strings ('%s' supplied)"
|
||||
% str(type(self.choices)).split("'")[1],
|
||||
self,
|
||||
)
|
||||
elif self.choices is not None:
|
||||
raise optparse.OptionError(
|
||||
"must not supply choices for type %r" % self.type, self
|
||||
)
|
||||
|
||||
# pylint: disable=unsupported-assignment-operation
|
||||
optparse.Option.CHECK_METHODS[2] = _check_choice # type: ignore
|
||||
|
||||
def process(self, opt, value, values, parser):
|
||||
# First, convert the value(s) to the right type. Howl if any
|
||||
# value(s) are bogus.
|
||||
value = self.convert_value(opt, value)
|
||||
if self.type == "named":
|
||||
existent = getattr(values, self.dest)
|
||||
if existent:
|
||||
existent.update(value)
|
||||
value = existent
|
||||
# And then take whatever action is expected of us.
|
||||
# This is a separate method to make life easier for
|
||||
# subclasses to add new actions.
|
||||
return self.take_action(self.action, self.dest, opt, value, values, parser)
|
||||
|
||||
|
||||
class OptionParser(optparse.OptionParser):
|
||||
def __init__(self, option_class, *args, **kwargs):
|
||||
optparse.OptionParser.__init__(self, option_class=Option, *args, **kwargs)
|
||||
|
||||
def format_option_help(self, formatter=None):
|
||||
if formatter is None:
|
||||
formatter = self.formatter
|
||||
outputlevel = getattr(formatter, "output_level", 0)
|
||||
formatter.store_option_strings(self)
|
||||
result = []
|
||||
result.append(formatter.format_heading("Options"))
|
||||
formatter.indent()
|
||||
if self.option_list:
|
||||
result.append(optparse.OptionContainer.format_option_help(self, formatter))
|
||||
result.append("\n")
|
||||
for group in self.option_groups:
|
||||
if group.level <= outputlevel and (
|
||||
group.description or _level_options(group, outputlevel)
|
||||
):
|
||||
result.append(group.format_help(formatter))
|
||||
result.append("\n")
|
||||
formatter.dedent()
|
||||
# Drop the last "\n", or the header if no options or option groups:
|
||||
return "".join(result[:-1])
|
||||
|
||||
def _match_long_opt(self, opt):
|
||||
"""Disable abbreviations."""
|
||||
if opt not in self._long_opt:
|
||||
raise optparse.BadOptionError(opt)
|
||||
return opt
|
||||
|
||||
|
||||
# pylint: disable=abstract-method; by design?
|
||||
class _ManHelpFormatter(optparse.HelpFormatter):
|
||||
def __init__(
|
||||
self, indent_increment=0, max_help_position=24, width=79, short_first=0
|
||||
):
|
||||
optparse.HelpFormatter.__init__(
|
||||
self, indent_increment, max_help_position, width, short_first
|
||||
)
|
||||
|
||||
def format_heading(self, heading):
|
||||
return ".SH %s\n" % heading.upper()
|
||||
|
||||
def format_description(self, description):
|
||||
return description
|
||||
|
||||
def format_option(self, option):
|
||||
try:
|
||||
optstring = option.option_strings
|
||||
except AttributeError:
|
||||
optstring = self.format_option_strings(option)
|
||||
if option.help:
|
||||
help_text = self.expand_default(option)
|
||||
help_string = " ".join([l.strip() for l in help_text.splitlines()])
|
||||
help_string = help_string.replace("\\", "\\\\")
|
||||
help_string = help_string.replace("[current:", "[default:")
|
||||
else:
|
||||
help_string = ""
|
||||
return """.IP "%s"
|
||||
%s
|
||||
""" % (
|
||||
optstring,
|
||||
help_string,
|
||||
)
|
||||
|
||||
def format_head(self, optparser, pkginfo, section=1):
|
||||
long_desc = ""
|
||||
try:
|
||||
pgm = optparser._get_prog_name()
|
||||
except AttributeError:
|
||||
# py >= 2.4.X (dunno which X exactly, at least 2)
|
||||
pgm = optparser.get_prog_name()
|
||||
short_desc = self.format_short_description(pgm, pkginfo.description)
|
||||
if hasattr(pkginfo, "long_desc"):
|
||||
long_desc = self.format_long_description(pgm, pkginfo.long_desc)
|
||||
return "%s\n%s\n%s\n%s" % (
|
||||
self.format_title(pgm, section),
|
||||
short_desc,
|
||||
self.format_synopsis(pgm),
|
||||
long_desc,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def format_title(pgm, section):
|
||||
date = "%d-%02d-%02d" % time.localtime()[:3]
|
||||
return '.TH %s %s "%s" %s' % (pgm, section, date, pgm)
|
||||
|
||||
@staticmethod
|
||||
def format_short_description(pgm, short_desc):
|
||||
return """.SH NAME
|
||||
.B %s
|
||||
\\- %s
|
||||
""" % (
|
||||
pgm,
|
||||
short_desc.strip(),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def format_synopsis(pgm):
|
||||
return (
|
||||
""".SH SYNOPSIS
|
||||
.B %s
|
||||
[
|
||||
.I OPTIONS
|
||||
] [
|
||||
.I <arguments>
|
||||
]
|
||||
"""
|
||||
% pgm
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def format_long_description(pgm, long_desc):
|
||||
long_desc = "\n".join(line.lstrip() for line in long_desc.splitlines())
|
||||
long_desc = long_desc.replace("\n.\n", "\n\n")
|
||||
if long_desc.lower().startswith(pgm):
|
||||
long_desc = long_desc[len(pgm) :]
|
||||
return """.SH DESCRIPTION
|
||||
.B %s
|
||||
%s
|
||||
""" % (
|
||||
pgm,
|
||||
long_desc.strip(),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def format_tail(pkginfo):
|
||||
tail = """.SH SEE ALSO
|
||||
/usr/share/doc/pythonX.Y-%s/
|
||||
|
||||
.SH BUGS
|
||||
Please report bugs on the project\'s mailing list:
|
||||
%s
|
||||
|
||||
.SH AUTHOR
|
||||
%s <%s>
|
||||
""" % (
|
||||
getattr(pkginfo, "debian_name", "pylint"),
|
||||
pkginfo.mailinglist,
|
||||
pkginfo.author,
|
||||
pkginfo.author_email,
|
||||
)
|
||||
|
||||
if hasattr(pkginfo, "copyright"):
|
||||
tail += (
|
||||
"""
|
||||
.SH COPYRIGHT
|
||||
%s
|
||||
"""
|
||||
% pkginfo.copyright
|
||||
)
|
||||
|
||||
return tail
|
||||
|
||||
|
||||
class OptionsManagerMixIn:
|
||||
"""Handle configuration from both a configuration file and command line options"""
|
||||
|
||||
def __init__(self, usage, config_file=None, version=None):
|
||||
self.config_file = config_file
|
||||
self.reset_parsers(usage, version=version)
|
||||
# list of registered options providers
|
||||
self.options_providers = []
|
||||
# dictionary associating option name to checker
|
||||
self._all_options = collections.OrderedDict()
|
||||
self._short_options = {}
|
||||
self._nocallback_options = {}
|
||||
self._mygroups = {}
|
||||
# verbosity
|
||||
self._maxlevel = 0
|
||||
|
||||
def reset_parsers(self, usage="", version=None):
|
||||
# configuration file parser
|
||||
self.cfgfile_parser = configparser.ConfigParser(
|
||||
inline_comment_prefixes=("#", ";")
|
||||
)
|
||||
# command line parser
|
||||
self.cmdline_parser = OptionParser(Option, usage=usage, version=version)
|
||||
self.cmdline_parser.options_manager = self
|
||||
self._optik_option_attrs = set(self.cmdline_parser.option_class.ATTRS)
|
||||
|
||||
def register_options_provider(self, provider, own_group=True):
|
||||
"""register an options provider"""
|
||||
assert provider.priority <= 0, "provider's priority can't be >= 0"
|
||||
for i in range(len(self.options_providers)):
|
||||
if provider.priority > self.options_providers[i].priority:
|
||||
self.options_providers.insert(i, provider)
|
||||
break
|
||||
else:
|
||||
self.options_providers.append(provider)
|
||||
non_group_spec_options = [
|
||||
option for option in provider.options if "group" not in option[1]
|
||||
]
|
||||
groups = getattr(provider, "option_groups", ())
|
||||
if own_group and non_group_spec_options:
|
||||
self.add_option_group(
|
||||
provider.name.upper(),
|
||||
provider.__doc__,
|
||||
non_group_spec_options,
|
||||
provider,
|
||||
)
|
||||
else:
|
||||
for opt, optdict in non_group_spec_options:
|
||||
self.add_optik_option(provider, self.cmdline_parser, opt, optdict)
|
||||
for gname, gdoc in groups:
|
||||
gname = gname.upper()
|
||||
goptions = [
|
||||
option
|
||||
for option in provider.options
|
||||
if option[1].get("group", "").upper() == gname
|
||||
]
|
||||
self.add_option_group(gname, gdoc, goptions, provider)
|
||||
|
||||
def add_option_group(self, group_name, _, options, provider):
|
||||
# add option group to the command line parser
|
||||
if group_name in self._mygroups:
|
||||
group = self._mygroups[group_name]
|
||||
else:
|
||||
group = optparse.OptionGroup(
|
||||
self.cmdline_parser, title=group_name.capitalize()
|
||||
)
|
||||
self.cmdline_parser.add_option_group(group)
|
||||
group.level = provider.level
|
||||
self._mygroups[group_name] = group
|
||||
# add section to the config file
|
||||
if (
|
||||
group_name != "DEFAULT"
|
||||
and group_name not in self.cfgfile_parser._sections
|
||||
):
|
||||
self.cfgfile_parser.add_section(group_name)
|
||||
# add provider's specific options
|
||||
for opt, optdict in options:
|
||||
self.add_optik_option(provider, group, opt, optdict)
|
||||
|
||||
def add_optik_option(self, provider, optikcontainer, opt, optdict):
|
||||
args, optdict = self.optik_option(provider, opt, optdict)
|
||||
option = optikcontainer.add_option(*args, **optdict)
|
||||
self._all_options[opt] = provider
|
||||
self._maxlevel = max(self._maxlevel, option.level or 0)
|
||||
|
||||
def optik_option(self, provider, opt, optdict):
|
||||
"""get our personal option definition and return a suitable form for
|
||||
use with optik/optparse
|
||||
"""
|
||||
optdict = copy.copy(optdict)
|
||||
if "action" in optdict:
|
||||
self._nocallback_options[provider] = opt
|
||||
else:
|
||||
optdict["action"] = "callback"
|
||||
optdict["callback"] = self.cb_set_provider_option
|
||||
# default is handled here and *must not* be given to optik if you
|
||||
# want the whole machinery to work
|
||||
if "default" in optdict:
|
||||
if (
|
||||
"help" in optdict
|
||||
and optdict.get("default") is not None
|
||||
and optdict["action"] not in ("store_true", "store_false")
|
||||
):
|
||||
optdict["help"] += " [current: %default]"
|
||||
del optdict["default"]
|
||||
args = ["--" + str(opt)]
|
||||
if "short" in optdict:
|
||||
self._short_options[optdict["short"]] = opt
|
||||
args.append("-" + optdict["short"])
|
||||
del optdict["short"]
|
||||
# cleanup option definition dict before giving it to optik
|
||||
for key in list(optdict.keys()):
|
||||
if key not in self._optik_option_attrs:
|
||||
optdict.pop(key)
|
||||
return args, optdict
|
||||
|
||||
def cb_set_provider_option(self, option, opt, value, parser):
|
||||
"""optik callback for option setting"""
|
||||
if opt.startswith("--"):
|
||||
# remove -- on long option
|
||||
opt = opt[2:]
|
||||
else:
|
||||
# short option, get its long equivalent
|
||||
opt = self._short_options[opt[1:]]
|
||||
# trick since we can't set action='store_true' on options
|
||||
if value is None:
|
||||
value = 1
|
||||
self.global_set_option(opt, value)
|
||||
|
||||
def global_set_option(self, opt, value):
|
||||
"""set option on the correct option provider"""
|
||||
self._all_options[opt].set_option(opt, value)
|
||||
|
||||
def generate_config(self, stream=None, skipsections=(), encoding=None):
|
||||
"""write a configuration file according to the current configuration
|
||||
into the given stream or stdout
|
||||
"""
|
||||
options_by_section = {}
|
||||
sections = []
|
||||
for provider in self.options_providers:
|
||||
for section, options in provider.options_by_section():
|
||||
if section is None:
|
||||
section = provider.name
|
||||
if section in skipsections:
|
||||
continue
|
||||
options = [
|
||||
(n, d, v)
|
||||
for (n, d, v) in options
|
||||
if d.get("type") is not None and not d.get("deprecated")
|
||||
]
|
||||
if not options:
|
||||
continue
|
||||
if section not in sections:
|
||||
sections.append(section)
|
||||
alloptions = options_by_section.setdefault(section, [])
|
||||
alloptions += options
|
||||
stream = stream or sys.stdout
|
||||
printed = False
|
||||
for section in sections:
|
||||
if printed:
|
||||
print("\n", file=stream)
|
||||
utils.format_section(
|
||||
stream, section.upper(), sorted(options_by_section[section])
|
||||
)
|
||||
printed = True
|
||||
|
||||
def generate_manpage(self, pkginfo, section=1, stream=None):
|
||||
with _patch_optparse():
|
||||
_generate_manpage(
|
||||
self.cmdline_parser,
|
||||
pkginfo,
|
||||
section,
|
||||
stream=stream or sys.stdout,
|
||||
level=self._maxlevel,
|
||||
)
|
||||
|
||||
def load_provider_defaults(self):
|
||||
"""initialize configuration using default values"""
|
||||
for provider in self.options_providers:
|
||||
provider.load_defaults()
|
||||
|
||||
def read_config_file(self, config_file=None, verbose=None):
|
||||
"""read the configuration file but do not load it (i.e. dispatching
|
||||
values to each options provider)
|
||||
"""
|
||||
helplevel = 1
|
||||
while helplevel <= self._maxlevel:
|
||||
opt = "-".join(["long"] * helplevel) + "-help"
|
||||
if opt in self._all_options:
|
||||
break # already processed
|
||||
|
||||
helpfunc = functools.partial(self.helpfunc, level=helplevel)
|
||||
|
||||
helpmsg = "%s verbose help." % " ".join(["more"] * helplevel)
|
||||
optdict = {"action": "callback", "callback": helpfunc, "help": helpmsg}
|
||||
provider = self.options_providers[0]
|
||||
self.add_optik_option(provider, self.cmdline_parser, opt, optdict)
|
||||
provider.options += ((opt, optdict),)
|
||||
helplevel += 1
|
||||
if config_file is None:
|
||||
config_file = self.config_file
|
||||
if config_file is not None:
|
||||
config_file = os.path.expanduser(config_file)
|
||||
if not os.path.exists(config_file):
|
||||
raise OSError("The config file {:s} doesn't exist!".format(config_file))
|
||||
|
||||
use_config_file = config_file and os.path.exists(config_file)
|
||||
if use_config_file: # pylint: disable=too-many-nested-blocks
|
||||
parser = self.cfgfile_parser
|
||||
|
||||
if config_file.endswith(".toml"):
|
||||
with open(config_file) as fp:
|
||||
content = toml.load(fp)
|
||||
|
||||
try:
|
||||
sections_values = content["tool"]["pylint"]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
for section, values in sections_values.items():
|
||||
# TOML has rich types, convert values to
|
||||
# strings as ConfigParser expects.
|
||||
for option, value in values.items():
|
||||
if isinstance(value, bool):
|
||||
values[option] = "yes" if value else "no"
|
||||
elif isinstance(value, int):
|
||||
values[option] = str(value)
|
||||
elif isinstance(value, list):
|
||||
values[option] = ",".join(value)
|
||||
parser._sections[section.upper()] = values
|
||||
else:
|
||||
# Use this encoding in order to strip the BOM marker, if any.
|
||||
with open(config_file, encoding="utf_8_sig") as fp:
|
||||
parser.read_file(fp)
|
||||
|
||||
# normalize sections'title
|
||||
for sect, values in list(parser._sections.items()):
|
||||
if sect.startswith("pylint."):
|
||||
sect = sect[len("pylint.") :]
|
||||
if not sect.isupper() and values:
|
||||
parser._sections[sect.upper()] = values
|
||||
|
||||
if not verbose:
|
||||
return
|
||||
|
||||
if use_config_file:
|
||||
msg = "Using config file {}".format(os.path.abspath(config_file))
|
||||
else:
|
||||
msg = "No config file found, using default configuration"
|
||||
print(msg, file=sys.stderr)
|
||||
|
||||
def load_config_file(self):
|
||||
"""dispatch values previously read from a configuration file to each
|
||||
options provider)
|
||||
"""
|
||||
parser = self.cfgfile_parser
|
||||
for section in parser.sections():
|
||||
for option, value in parser.items(section):
|
||||
try:
|
||||
self.global_set_option(option, value)
|
||||
except (KeyError, optparse.OptionError):
|
||||
continue
|
||||
|
||||
def load_configuration(self, **kwargs):
|
||||
"""override configuration according to given parameters"""
|
||||
return self.load_configuration_from_config(kwargs)
|
||||
|
||||
def load_configuration_from_config(self, config):
|
||||
for opt, opt_value in config.items():
|
||||
opt = opt.replace("_", "-")
|
||||
provider = self._all_options[opt]
|
||||
provider.set_option(opt, opt_value)
|
||||
|
||||
def load_command_line_configuration(self, args=None):
|
||||
"""Override configuration according to command line parameters
|
||||
|
||||
return additional arguments
|
||||
"""
|
||||
with _patch_optparse():
|
||||
if args is None:
|
||||
args = sys.argv[1:]
|
||||
else:
|
||||
args = list(args)
|
||||
(options, args) = self.cmdline_parser.parse_args(args=args)
|
||||
for provider in self._nocallback_options:
|
||||
config = provider.config
|
||||
for attr in config.__dict__.keys():
|
||||
value = getattr(options, attr, None)
|
||||
if value is None:
|
||||
continue
|
||||
setattr(config, attr, value)
|
||||
return args
|
||||
|
||||
def add_help_section(self, title, description, level=0):
|
||||
"""add a dummy option section for help purpose """
|
||||
group = optparse.OptionGroup(
|
||||
self.cmdline_parser, title=title.capitalize(), description=description
|
||||
)
|
||||
group.level = level
|
||||
self._maxlevel = max(self._maxlevel, level)
|
||||
self.cmdline_parser.add_option_group(group)
|
||||
|
||||
def help(self, level=0):
|
||||
"""return the usage string for available options """
|
||||
self.cmdline_parser.formatter.output_level = level
|
||||
with _patch_optparse():
|
||||
return self.cmdline_parser.format_help()
|
||||
|
||||
def helpfunc(self, option, opt, val, p, level): # pylint: disable=unused-argument
|
||||
print(self.help(level))
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
class OptionsProviderMixIn:
|
||||
"""Mixin to provide options to an OptionsManager"""
|
||||
|
||||
# those attributes should be overridden
|
||||
priority = -1
|
||||
name = "default"
|
||||
options = () # type: Tuple[Tuple[str, Dict[str, Any]], ...]
|
||||
level = 0
|
||||
|
||||
def __init__(self):
|
||||
self.config = optparse.Values()
|
||||
self.load_defaults()
|
||||
|
||||
def load_defaults(self):
|
||||
"""initialize the provider using default values"""
|
||||
for opt, optdict in self.options:
|
||||
action = optdict.get("action")
|
||||
if action != "callback":
|
||||
# callback action have no default
|
||||
if optdict is None:
|
||||
optdict = self.get_option_def(opt)
|
||||
default = optdict.get("default")
|
||||
self.set_option(opt, default, action, optdict)
|
||||
|
||||
def option_attrname(self, opt, optdict=None):
|
||||
"""get the config attribute corresponding to opt"""
|
||||
if optdict is None:
|
||||
optdict = self.get_option_def(opt)
|
||||
return optdict.get("dest", opt.replace("-", "_"))
|
||||
|
||||
def option_value(self, opt):
|
||||
"""get the current value for the given option"""
|
||||
return getattr(self.config, self.option_attrname(opt), None)
|
||||
|
||||
def set_option(self, optname, value, action=None, optdict=None):
|
||||
"""method called to set an option (registered in the options list)"""
|
||||
if optdict is None:
|
||||
optdict = self.get_option_def(optname)
|
||||
if value is not None:
|
||||
value = _validate(value, optdict, optname)
|
||||
if action is None:
|
||||
action = optdict.get("action", "store")
|
||||
if action == "store":
|
||||
setattr(self.config, self.option_attrname(optname, optdict), value)
|
||||
elif action in ("store_true", "count"):
|
||||
setattr(self.config, self.option_attrname(optname, optdict), 0)
|
||||
elif action == "store_false":
|
||||
setattr(self.config, self.option_attrname(optname, optdict), 1)
|
||||
elif action == "append":
|
||||
optname = self.option_attrname(optname, optdict)
|
||||
_list = getattr(self.config, optname, None)
|
||||
if _list is None:
|
||||
if isinstance(value, (list, tuple)):
|
||||
_list = value
|
||||
elif value is not None:
|
||||
_list = []
|
||||
_list.append(value)
|
||||
setattr(self.config, optname, _list)
|
||||
elif isinstance(_list, tuple):
|
||||
setattr(self.config, optname, _list + (value,))
|
||||
else:
|
||||
_list.append(value)
|
||||
elif action == "callback":
|
||||
optdict["callback"](None, optname, value, None)
|
||||
else:
|
||||
raise UnsupportedAction(action)
|
||||
|
||||
def get_option_def(self, opt):
|
||||
"""return the dictionary defining an option given its name"""
|
||||
assert self.options
|
||||
for option in self.options:
|
||||
if option[0] == opt:
|
||||
return option[1]
|
||||
raise optparse.OptionError(
|
||||
"no such option %s in section %r" % (opt, self.name), opt
|
||||
)
|
||||
|
||||
def options_by_section(self):
|
||||
"""return an iterator on options grouped by section
|
||||
|
||||
(section, [list of (optname, optdict, optvalue)])
|
||||
"""
|
||||
sections = {}
|
||||
for optname, optdict in self.options:
|
||||
sections.setdefault(optdict.get("group"), []).append(
|
||||
(optname, optdict, self.option_value(optname))
|
||||
)
|
||||
if None in sections:
|
||||
yield None, sections.pop(None)
|
||||
for section, options in sorted(sections.items()):
|
||||
yield section.upper(), options
|
||||
|
||||
def options_and_values(self, options=None):
|
||||
if options is None:
|
||||
options = self.options
|
||||
for optname, optdict in options:
|
||||
yield (optname, optdict, self.option_value(optname))
|
||||
|
||||
|
||||
class ConfigurationMixIn(OptionsManagerMixIn, OptionsProviderMixIn):
|
||||
"""basic mixin for simple configurations which don't need the
|
||||
manager / providers model
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
if not args:
|
||||
kwargs.setdefault("usage", "")
|
||||
OptionsManagerMixIn.__init__(self, *args, **kwargs)
|
||||
OptionsProviderMixIn.__init__(self)
|
||||
if not getattr(self, "option_groups", None):
|
||||
self.option_groups = []
|
||||
for _, optdict in self.options:
|
||||
try:
|
||||
gdef = (optdict["group"].upper(), "")
|
||||
except KeyError:
|
||||
continue
|
||||
if gdef not in self.option_groups:
|
||||
self.option_groups.append(gdef)
|
||||
self.register_options_provider(self, own_group=False)
|
||||
|
||||
|
||||
def _generate_manpage(optparser, pkginfo, section=1, stream=sys.stdout, level=0):
|
||||
formatter = _ManHelpFormatter()
|
||||
formatter.output_level = level
|
||||
formatter.parser = optparser
|
||||
print(formatter.format_head(optparser, pkginfo, section), file=stream)
|
||||
print(optparser.format_option_help(formatter), file=stream)
|
||||
print(formatter.format_tail(pkginfo), file=stream)
|
||||
@@ -0,0 +1,41 @@
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
import re
|
||||
|
||||
# Allow stopping after the first semicolon/hash encountered,
|
||||
# so that an option can be continued with the reasons
|
||||
# why it is active or disabled.
|
||||
OPTION_RGX = re.compile(r"\s*#.*\bpylint:\s*([^;#]+)[;#]{0,1}")
|
||||
|
||||
PY_EXTS = (".py", ".pyc", ".pyo", ".pyw", ".so", ".dll")
|
||||
|
||||
MSG_STATE_CONFIDENCE = 2
|
||||
_MSG_ORDER = "EWRCIF"
|
||||
MSG_STATE_SCOPE_CONFIG = 0
|
||||
MSG_STATE_SCOPE_MODULE = 1
|
||||
|
||||
# The line/node distinction does not apply to fatal errors and reports.
|
||||
_SCOPE_EXEMPT = "FR"
|
||||
|
||||
MSG_TYPES = {
|
||||
"I": "info",
|
||||
"C": "convention",
|
||||
"R": "refactor",
|
||||
"W": "warning",
|
||||
"E": "error",
|
||||
"F": "fatal",
|
||||
}
|
||||
MSG_TYPES_LONG = {v: k for k, v in MSG_TYPES.items()}
|
||||
|
||||
MSG_TYPES_STATUS = {"I": 0, "C": 16, "R": 8, "W": 4, "E": 2, "F": 1}
|
||||
|
||||
# You probably don't want to change the MAIN_CHECKER_NAME
|
||||
# This would affect rcfile generation and retro-compatibility
|
||||
# on all project using [MASTER] in their rcfile.
|
||||
MAIN_CHECKER_NAME = "master"
|
||||
|
||||
|
||||
class WarningScope:
|
||||
LINE = "line-based-msg"
|
||||
NODE = "node-based-msg"
|
||||
@@ -0,0 +1,200 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# mode: python; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4
|
||||
# -*- vim:fenc=utf-8:ft=python:et:sw=4:ts=4:sts=4
|
||||
|
||||
# Copyright (c) 2008-2014 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
|
||||
# Copyright (c) 2014 Jakob Normark <jakobnormark@gmail.com>
|
||||
# Copyright (c) 2014 Brett Cannon <brett@python.org>
|
||||
# Copyright (c) 2014 Manuel Vázquez Acosta <mva.led@gmail.com>
|
||||
# Copyright (c) 2014 Derek Harland <derek.harland@finq.co.nz>
|
||||
# Copyright (c) 2014 Arun Persaud <arun@nubati.net>
|
||||
# Copyright (c) 2015-2019 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2015 Mihai Balint <balint.mihai@gmail.com>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
|
||||
# Copyright (c) 2017 hippo91 <guillaume.peillex@gmail.com>
|
||||
# Copyright (c) 2017 Daniela Plascencia <daplascen@gmail.com>
|
||||
# Copyright (c) 2018 Sushobhit <31987769+sushobhit27@users.noreply.github.com>
|
||||
# Copyright (c) 2018 Ryan McGuire <ryan@enigmacurry.com>
|
||||
# Copyright (c) 2018 thernstig <30827238+thernstig@users.noreply.github.com>
|
||||
# Copyright (c) 2018 Radostin Stoyanov <rst0git@users.noreply.github.com>
|
||||
# Copyright (c) 2019 Hugo van Kemenade <hugovk@users.noreply.github.com>
|
||||
# Copyright (c) 2019 Pierre Sassoulas <pierre.sassoulas@gmail.com>
|
||||
# Copyright (c) 2020 Anthony Sottile <asottile@umich.edu>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""Emacs and Flymake compatible Pylint.
|
||||
|
||||
This script is for integration with emacs and is compatible with flymake mode.
|
||||
|
||||
epylint walks out of python packages before invoking pylint. This avoids
|
||||
reporting import errors that occur when a module within a package uses the
|
||||
absolute import path to get another module within this package.
|
||||
|
||||
For example:
|
||||
- Suppose a package is structured as
|
||||
|
||||
a/__init__.py
|
||||
a/b/x.py
|
||||
a/c/y.py
|
||||
|
||||
- Then if y.py imports x as "from a.b import x" the following produces pylint
|
||||
errors
|
||||
|
||||
cd a/c; pylint y.py
|
||||
|
||||
- The following obviously doesn't
|
||||
|
||||
pylint a/c/y.py
|
||||
|
||||
- As this script will be invoked by emacs within the directory of the file
|
||||
we are checking we need to go out of it to avoid these false positives.
|
||||
|
||||
|
||||
You may also use py_run to run pylint with desired options and get back (or not)
|
||||
its output.
|
||||
"""
|
||||
import os
|
||||
import os.path as osp
|
||||
import shlex
|
||||
import sys
|
||||
from io import StringIO
|
||||
from subprocess import PIPE, Popen
|
||||
|
||||
|
||||
def _get_env():
|
||||
"""Extracts the environment PYTHONPATH and appends the current sys.path to
|
||||
those."""
|
||||
env = dict(os.environ)
|
||||
env["PYTHONPATH"] = os.pathsep.join(sys.path)
|
||||
return env
|
||||
|
||||
|
||||
def lint(filename, options=()):
|
||||
"""Pylint the given file.
|
||||
|
||||
When run from emacs we will be in the directory of a file, and passed its
|
||||
filename. If this file is part of a package and is trying to import other
|
||||
modules from within its own package or another package rooted in a directory
|
||||
below it, pylint will classify it as a failed import.
|
||||
|
||||
To get around this, we traverse down the directory tree to find the root of
|
||||
the package this module is in. We then invoke pylint from this directory.
|
||||
|
||||
Finally, we must correct the filenames in the output generated by pylint so
|
||||
Emacs doesn't become confused (it will expect just the original filename,
|
||||
while pylint may extend it with extra directories if we've traversed down
|
||||
the tree)
|
||||
"""
|
||||
# traverse downwards until we are out of a python package
|
||||
full_path = osp.abspath(filename)
|
||||
parent_path = osp.dirname(full_path)
|
||||
child_path = osp.basename(full_path)
|
||||
|
||||
while parent_path != "/" and osp.exists(osp.join(parent_path, "__init__.py")):
|
||||
child_path = osp.join(osp.basename(parent_path), child_path)
|
||||
parent_path = osp.dirname(parent_path)
|
||||
|
||||
# Start pylint
|
||||
# Ensure we use the python and pylint associated with the running epylint
|
||||
run_cmd = "import sys; from pylint.lint import Run; Run(sys.argv[1:])"
|
||||
cmd = (
|
||||
[sys.executable, "-c", run_cmd]
|
||||
+ [
|
||||
"--msg-template",
|
||||
"{path}:{line}: {category} ({msg_id}, {symbol}, {obj}) {msg}",
|
||||
"-r",
|
||||
"n",
|
||||
child_path,
|
||||
]
|
||||
+ list(options)
|
||||
)
|
||||
process = Popen(
|
||||
cmd, stdout=PIPE, cwd=parent_path, env=_get_env(), universal_newlines=True
|
||||
)
|
||||
|
||||
for line in process.stdout:
|
||||
# remove pylintrc warning
|
||||
if line.startswith("No config file found"):
|
||||
continue
|
||||
|
||||
# modify the file name thats output to reverse the path traversal we made
|
||||
parts = line.split(":")
|
||||
if parts and parts[0] == child_path:
|
||||
line = ":".join([filename] + parts[1:])
|
||||
print(line, end=" ")
|
||||
|
||||
process.wait()
|
||||
return process.returncode
|
||||
|
||||
|
||||
def py_run(command_options="", return_std=False, stdout=None, stderr=None):
|
||||
"""Run pylint from python
|
||||
|
||||
``command_options`` is a string containing ``pylint`` command line options;
|
||||
``return_std`` (boolean) indicates return of created standard output
|
||||
and error (see below);
|
||||
``stdout`` and ``stderr`` are 'file-like' objects in which standard output
|
||||
could be written.
|
||||
|
||||
Calling agent is responsible for stdout/err management (creation, close).
|
||||
Default standard output and error are those from sys,
|
||||
or standalone ones (``subprocess.PIPE``) are used
|
||||
if they are not set and ``return_std``.
|
||||
|
||||
If ``return_std`` is set to ``True``, this function returns a 2-uple
|
||||
containing standard output and error related to created process,
|
||||
as follows: ``(stdout, stderr)``.
|
||||
|
||||
To silently run Pylint on a module, and get its standard output and error:
|
||||
>>> (pylint_stdout, pylint_stderr) = py_run( 'module_name.py', True)
|
||||
"""
|
||||
# Detect if we use Python as executable or not, else default to `python`
|
||||
executable = sys.executable if "python" in sys.executable else "python"
|
||||
|
||||
# Create command line to call pylint
|
||||
epylint_part = [executable, "-c", "from pylint import epylint;epylint.Run()"]
|
||||
options = shlex.split(command_options, posix=not sys.platform.startswith("win"))
|
||||
cli = epylint_part + options
|
||||
|
||||
# Providing standard output and/or error if not set
|
||||
if stdout is None:
|
||||
if return_std:
|
||||
stdout = PIPE
|
||||
else:
|
||||
stdout = sys.stdout
|
||||
if stderr is None:
|
||||
if return_std:
|
||||
stderr = PIPE
|
||||
else:
|
||||
stderr = sys.stderr
|
||||
# Call pylint in a subprocess
|
||||
process = Popen(
|
||||
cli,
|
||||
shell=False,
|
||||
stdout=stdout,
|
||||
stderr=stderr,
|
||||
env=_get_env(),
|
||||
universal_newlines=True,
|
||||
)
|
||||
proc_stdout, proc_stderr = process.communicate()
|
||||
# Return standard output and error
|
||||
if return_std:
|
||||
return StringIO(proc_stdout), StringIO(proc_stderr)
|
||||
return None
|
||||
|
||||
|
||||
def Run():
|
||||
if len(sys.argv) == 1:
|
||||
print("Usage: %s <filename> [options]" % sys.argv[0])
|
||||
sys.exit(1)
|
||||
elif not osp.exists(sys.argv[1]):
|
||||
print("%s does not exist" % sys.argv[1])
|
||||
sys.exit(1)
|
||||
else:
|
||||
sys.exit(lint(sys.argv[1], sys.argv[2:]))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
Run()
|
||||
@@ -0,0 +1,31 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2016-2018 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2016 Glenn Matthews <glenn@e-dad.net>
|
||||
# Copyright (c) 2017 Łukasz Rogalski <rogalski.91@gmail.com>
|
||||
# Copyright (c) 2018 Ville Skyttä <ville.skytta@iki.fi>
|
||||
# Copyright (c) 2019 Thomas Hisch <t.hisch@gmail.com>
|
||||
# Copyright (c) 2020 Anthony Sottile <asottile@umich.edu>
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""Exception classes raised by various operations within pylint."""
|
||||
|
||||
|
||||
class InvalidMessageError(Exception):
|
||||
"""raised when a message creation, registration or addition is rejected"""
|
||||
|
||||
|
||||
class UnknownMessageError(Exception):
|
||||
"""raised when an unregistered message id is encountered"""
|
||||
|
||||
|
||||
class EmptyReportError(Exception):
|
||||
"""raised when a report is empty and so should not be displayed"""
|
||||
|
||||
|
||||
class InvalidReporterError(Exception):
|
||||
"""raised when selected reporter is invalid (e.g. not found)"""
|
||||
|
||||
|
||||
class InvalidArgsError(ValueError):
|
||||
"""raised when passed arguments are invalid, e.g., have the wrong length"""
|
||||
@@ -0,0 +1,808 @@
|
||||
# Copyright (c) 2016-2019 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2016-2019 Ashley Whetter <ashley@awhetter.co.uk>
|
||||
# Copyright (c) 2016 Yuri Bochkarev <baltazar.bz@gmail.com>
|
||||
# Copyright (c) 2016 Glenn Matthews <glenn@e-dad.net>
|
||||
# Copyright (c) 2016 Moises Lopez <moylop260@vauxoo.com>
|
||||
# Copyright (c) 2017 hippo91 <guillaume.peillex@gmail.com>
|
||||
# Copyright (c) 2017 Mitar <mitar.github@tnode.com>
|
||||
# Copyright (c) 2018, 2020 Anthony Sottile <asottile@umich.edu>
|
||||
# Copyright (c) 2018 Jim Robertson <jrobertson98atx@gmail.com>
|
||||
# Copyright (c) 2018 ssolanki <sushobhitsolanki@gmail.com>
|
||||
# Copyright (c) 2018 Mitchell T.H. Young <mitchelly@gmail.com>
|
||||
# Copyright (c) 2018 Adrian Chirieac <chirieacam@gmail.com>
|
||||
# Copyright (c) 2019 Hugo van Kemenade <hugovk@users.noreply.github.com>
|
||||
# Copyright (c) 2019 Danny Hermes <daniel.j.hermes@gmail.com>
|
||||
# Copyright (c) 2019 Zeb Nicholls <zebedee.nicholls@climate-energy-college.org>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""Utility methods for docstring checking."""
|
||||
|
||||
import re
|
||||
from typing import List
|
||||
|
||||
import astroid
|
||||
|
||||
from pylint.checkers import utils
|
||||
|
||||
|
||||
def space_indentation(s):
|
||||
"""The number of leading spaces in a string
|
||||
|
||||
:param str s: input string
|
||||
|
||||
:rtype: int
|
||||
:return: number of leading spaces
|
||||
"""
|
||||
return len(s) - len(s.lstrip(" "))
|
||||
|
||||
|
||||
def get_setters_property_name(node):
|
||||
"""Get the name of the property that the given node is a setter for.
|
||||
|
||||
:param node: The node to get the property name for.
|
||||
:type node: str
|
||||
|
||||
:rtype: str or None
|
||||
:returns: The name of the property that the node is a setter for,
|
||||
or None if one could not be found.
|
||||
"""
|
||||
decorators = node.decorators.nodes if node.decorators else []
|
||||
for decorator in decorators:
|
||||
if (
|
||||
isinstance(decorator, astroid.Attribute)
|
||||
and decorator.attrname == "setter"
|
||||
and isinstance(decorator.expr, astroid.Name)
|
||||
):
|
||||
return decorator.expr.name
|
||||
return None
|
||||
|
||||
|
||||
def get_setters_property(node):
|
||||
"""Get the property node for the given setter node.
|
||||
|
||||
:param node: The node to get the property for.
|
||||
:type node: astroid.FunctionDef
|
||||
|
||||
:rtype: astroid.FunctionDef or None
|
||||
:returns: The node relating to the property of the given setter node,
|
||||
or None if one could not be found.
|
||||
"""
|
||||
property_ = None
|
||||
|
||||
property_name = get_setters_property_name(node)
|
||||
class_node = utils.node_frame_class(node)
|
||||
if property_name and class_node:
|
||||
class_attrs = class_node.getattr(node.name)
|
||||
for attr in class_attrs:
|
||||
if utils.decorated_with_property(attr):
|
||||
property_ = attr
|
||||
break
|
||||
|
||||
return property_
|
||||
|
||||
|
||||
def returns_something(return_node):
|
||||
"""Check if a return node returns a value other than None.
|
||||
|
||||
:param return_node: The return node to check.
|
||||
:type return_node: astroid.Return
|
||||
|
||||
:rtype: bool
|
||||
:return: True if the return node returns a value other than None,
|
||||
False otherwise.
|
||||
"""
|
||||
returns = return_node.value
|
||||
|
||||
if returns is None:
|
||||
return False
|
||||
|
||||
return not (isinstance(returns, astroid.Const) and returns.value is None)
|
||||
|
||||
|
||||
def _get_raise_target(node):
|
||||
if isinstance(node.exc, astroid.Call):
|
||||
func = node.exc.func
|
||||
if isinstance(func, (astroid.Name, astroid.Attribute)):
|
||||
return utils.safe_infer(func)
|
||||
return None
|
||||
|
||||
|
||||
def _split_multiple_exc_types(target: str) -> List[str]:
|
||||
delimiters = r"(\s*,(?:\s*or\s)?\s*|\s+or\s+)"
|
||||
return re.split(delimiters, target)
|
||||
|
||||
|
||||
def possible_exc_types(node):
|
||||
"""
|
||||
Gets all of the possible raised exception types for the given raise node.
|
||||
|
||||
.. note::
|
||||
|
||||
Caught exception types are ignored.
|
||||
|
||||
|
||||
:param node: The raise node to find exception types for.
|
||||
:type node: astroid.node_classes.NodeNG
|
||||
|
||||
:returns: A list of exception types possibly raised by :param:`node`.
|
||||
:rtype: set(str)
|
||||
"""
|
||||
excs = []
|
||||
if isinstance(node.exc, astroid.Name):
|
||||
inferred = utils.safe_infer(node.exc)
|
||||
if inferred:
|
||||
excs = [inferred.name]
|
||||
elif node.exc is None:
|
||||
handler = node.parent
|
||||
while handler and not isinstance(handler, astroid.ExceptHandler):
|
||||
handler = handler.parent
|
||||
|
||||
if handler and handler.type:
|
||||
inferred_excs = astroid.unpack_infer(handler.type)
|
||||
excs = (exc.name for exc in inferred_excs if exc is not astroid.Uninferable)
|
||||
else:
|
||||
target = _get_raise_target(node)
|
||||
if isinstance(target, astroid.ClassDef):
|
||||
excs = [target.name]
|
||||
elif isinstance(target, astroid.FunctionDef):
|
||||
for ret in target.nodes_of_class(astroid.Return):
|
||||
if ret.frame() != target:
|
||||
# return from inner function - ignore it
|
||||
continue
|
||||
|
||||
val = utils.safe_infer(ret.value)
|
||||
if (
|
||||
val
|
||||
and isinstance(val, (astroid.Instance, astroid.ClassDef))
|
||||
and utils.inherit_from_std_ex(val)
|
||||
):
|
||||
excs.append(val.name)
|
||||
|
||||
try:
|
||||
return {exc for exc in excs if not utils.node_ignores_exception(node, exc)}
|
||||
except astroid.InferenceError:
|
||||
return set()
|
||||
|
||||
|
||||
def docstringify(docstring, default_type="default"):
|
||||
for docstring_type in [
|
||||
SphinxDocstring,
|
||||
EpytextDocstring,
|
||||
GoogleDocstring,
|
||||
NumpyDocstring,
|
||||
]:
|
||||
instance = docstring_type(docstring)
|
||||
if instance.is_valid():
|
||||
return instance
|
||||
|
||||
docstring_type = DOCSTRING_TYPES.get(default_type, Docstring)
|
||||
return docstring_type(docstring)
|
||||
|
||||
|
||||
class Docstring:
|
||||
re_for_parameters_see = re.compile(
|
||||
r"""
|
||||
For\s+the\s+(other)?\s*parameters\s*,\s+see
|
||||
""",
|
||||
re.X | re.S,
|
||||
)
|
||||
|
||||
supports_yields = None
|
||||
"""True if the docstring supports a "yield" section.
|
||||
|
||||
False if the docstring uses the returns section to document generators.
|
||||
"""
|
||||
|
||||
# These methods are designed to be overridden
|
||||
# pylint: disable=no-self-use
|
||||
def __init__(self, doc):
|
||||
doc = doc or ""
|
||||
self.doc = doc.expandtabs()
|
||||
|
||||
def is_valid(self):
|
||||
return False
|
||||
|
||||
def exceptions(self):
|
||||
return set()
|
||||
|
||||
def has_params(self):
|
||||
return False
|
||||
|
||||
def has_returns(self):
|
||||
return False
|
||||
|
||||
def has_rtype(self):
|
||||
return False
|
||||
|
||||
def has_property_returns(self):
|
||||
return False
|
||||
|
||||
def has_property_type(self):
|
||||
return False
|
||||
|
||||
def has_yields(self):
|
||||
return False
|
||||
|
||||
def has_yields_type(self):
|
||||
return False
|
||||
|
||||
def match_param_docs(self):
|
||||
return set(), set()
|
||||
|
||||
def params_documented_elsewhere(self):
|
||||
return self.re_for_parameters_see.search(self.doc) is not None
|
||||
|
||||
|
||||
class SphinxDocstring(Docstring):
|
||||
re_type = r"""
|
||||
[~!.]? # Optional link style prefix
|
||||
\w(?:\w|\.[^\.])* # Valid python name
|
||||
"""
|
||||
|
||||
re_simple_container_type = r"""
|
||||
{type} # a container type
|
||||
[\(\[] [^\n\s]+ [\)\]] # with the contents of the container
|
||||
""".format(
|
||||
type=re_type
|
||||
)
|
||||
|
||||
re_multiple_simple_type = r"""
|
||||
(?:{container_type}|{type})
|
||||
(?:(?:\s+(?:of|or)\s+|\s*,\s*)(?:{container_type}|{type}))*
|
||||
""".format(
|
||||
type=re_type, container_type=re_simple_container_type
|
||||
)
|
||||
|
||||
re_xref = r"""
|
||||
(?::\w+:)? # optional tag
|
||||
`{}` # what to reference
|
||||
""".format(
|
||||
re_type
|
||||
)
|
||||
|
||||
re_param_raw = r"""
|
||||
: # initial colon
|
||||
(?: # Sphinx keywords
|
||||
param|parameter|
|
||||
arg|argument|
|
||||
key|keyword
|
||||
)
|
||||
\s+ # whitespace
|
||||
|
||||
(?: # optional type declaration
|
||||
({type}|{container_type})
|
||||
\s+
|
||||
)?
|
||||
|
||||
(\w+) # Parameter name
|
||||
\s* # whitespace
|
||||
: # final colon
|
||||
""".format(
|
||||
type=re_type, container_type=re_simple_container_type
|
||||
)
|
||||
re_param_in_docstring = re.compile(re_param_raw, re.X | re.S)
|
||||
|
||||
re_type_raw = r"""
|
||||
:type # Sphinx keyword
|
||||
\s+ # whitespace
|
||||
({type}) # Parameter name
|
||||
\s* # whitespace
|
||||
: # final colon
|
||||
""".format(
|
||||
type=re_multiple_simple_type
|
||||
)
|
||||
re_type_in_docstring = re.compile(re_type_raw, re.X | re.S)
|
||||
|
||||
re_property_type_raw = r"""
|
||||
:type: # Sphinx keyword
|
||||
\s+ # whitespace
|
||||
{type} # type declaration
|
||||
""".format(
|
||||
type=re_multiple_simple_type
|
||||
)
|
||||
re_property_type_in_docstring = re.compile(re_property_type_raw, re.X | re.S)
|
||||
|
||||
re_raise_raw = r"""
|
||||
: # initial colon
|
||||
(?: # Sphinx keyword
|
||||
raises?|
|
||||
except|exception
|
||||
)
|
||||
\s+ # whitespace
|
||||
({type}) # exception type
|
||||
\s* # whitespace
|
||||
: # final colon
|
||||
""".format(
|
||||
type=re_multiple_simple_type
|
||||
)
|
||||
re_raise_in_docstring = re.compile(re_raise_raw, re.X | re.S)
|
||||
|
||||
re_rtype_in_docstring = re.compile(r":rtype:")
|
||||
|
||||
re_returns_in_docstring = re.compile(r":returns?:")
|
||||
|
||||
supports_yields = False
|
||||
|
||||
def is_valid(self):
|
||||
return bool(
|
||||
self.re_param_in_docstring.search(self.doc)
|
||||
or self.re_raise_in_docstring.search(self.doc)
|
||||
or self.re_rtype_in_docstring.search(self.doc)
|
||||
or self.re_returns_in_docstring.search(self.doc)
|
||||
or self.re_property_type_in_docstring.search(self.doc)
|
||||
)
|
||||
|
||||
def exceptions(self):
|
||||
types = set()
|
||||
|
||||
for match in re.finditer(self.re_raise_in_docstring, self.doc):
|
||||
raise_type = match.group(1)
|
||||
types.update(_split_multiple_exc_types(raise_type))
|
||||
|
||||
return types
|
||||
|
||||
def has_params(self):
|
||||
if not self.doc:
|
||||
return False
|
||||
|
||||
return self.re_param_in_docstring.search(self.doc) is not None
|
||||
|
||||
def has_returns(self):
|
||||
if not self.doc:
|
||||
return False
|
||||
|
||||
return bool(self.re_returns_in_docstring.search(self.doc))
|
||||
|
||||
def has_rtype(self):
|
||||
if not self.doc:
|
||||
return False
|
||||
|
||||
return bool(self.re_rtype_in_docstring.search(self.doc))
|
||||
|
||||
def has_property_returns(self):
|
||||
if not self.doc:
|
||||
return False
|
||||
|
||||
# The summary line is the return doc,
|
||||
# so the first line must not be a known directive.
|
||||
return not self.doc.lstrip().startswith(":")
|
||||
|
||||
def has_property_type(self):
|
||||
if not self.doc:
|
||||
return False
|
||||
|
||||
return bool(self.re_property_type_in_docstring.search(self.doc))
|
||||
|
||||
def match_param_docs(self):
|
||||
params_with_doc = set()
|
||||
params_with_type = set()
|
||||
|
||||
for match in re.finditer(self.re_param_in_docstring, self.doc):
|
||||
name = match.group(2)
|
||||
params_with_doc.add(name)
|
||||
param_type = match.group(1)
|
||||
if param_type is not None:
|
||||
params_with_type.add(name)
|
||||
|
||||
params_with_type.update(re.findall(self.re_type_in_docstring, self.doc))
|
||||
return params_with_doc, params_with_type
|
||||
|
||||
|
||||
class EpytextDocstring(SphinxDocstring):
|
||||
"""
|
||||
Epytext is similar to Sphinx. See the docs:
|
||||
http://epydoc.sourceforge.net/epytext.html
|
||||
http://epydoc.sourceforge.net/fields.html#fields
|
||||
|
||||
It's used in PyCharm:
|
||||
https://www.jetbrains.com/help/pycharm/2016.1/creating-documentation-comments.html#d848203e314
|
||||
https://www.jetbrains.com/help/pycharm/2016.1/using-docstrings-to-specify-types.html
|
||||
"""
|
||||
|
||||
re_param_in_docstring = re.compile(
|
||||
SphinxDocstring.re_param_raw.replace(":", "@", 1), re.X | re.S
|
||||
)
|
||||
|
||||
re_type_in_docstring = re.compile(
|
||||
SphinxDocstring.re_type_raw.replace(":", "@", 1), re.X | re.S
|
||||
)
|
||||
|
||||
re_property_type_in_docstring = re.compile(
|
||||
SphinxDocstring.re_property_type_raw.replace(":", "@", 1), re.X | re.S
|
||||
)
|
||||
|
||||
re_raise_in_docstring = re.compile(
|
||||
SphinxDocstring.re_raise_raw.replace(":", "@", 1), re.X | re.S
|
||||
)
|
||||
|
||||
re_rtype_in_docstring = re.compile(
|
||||
r"""
|
||||
@ # initial "at" symbol
|
||||
(?: # Epytext keyword
|
||||
rtype|returntype
|
||||
)
|
||||
: # final colon
|
||||
""",
|
||||
re.X | re.S,
|
||||
)
|
||||
|
||||
re_returns_in_docstring = re.compile(r"@returns?:")
|
||||
|
||||
def has_property_returns(self):
|
||||
if not self.doc:
|
||||
return False
|
||||
|
||||
# If this is a property docstring, the summary is the return doc.
|
||||
if self.has_property_type():
|
||||
# The summary line is the return doc,
|
||||
# so the first line must not be a known directive.
|
||||
return not self.doc.lstrip().startswith("@")
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class GoogleDocstring(Docstring):
|
||||
re_type = SphinxDocstring.re_type
|
||||
|
||||
re_xref = SphinxDocstring.re_xref
|
||||
|
||||
re_container_type = r"""
|
||||
(?:{type}|{xref}) # a container type
|
||||
[\(\[] [^\n]+ [\)\]] # with the contents of the container
|
||||
""".format(
|
||||
type=re_type, xref=re_xref
|
||||
)
|
||||
|
||||
re_multiple_type = r"""
|
||||
(?:{container_type}|{type}|{xref})
|
||||
(?:(?:\s+(?:of|or)\s+|\s*,\s*)(?:{container_type}|{type}|{xref}))*
|
||||
""".format(
|
||||
type=re_type, xref=re_xref, container_type=re_container_type
|
||||
)
|
||||
|
||||
_re_section_template = r"""
|
||||
^([ ]*) {0} \s*: \s*$ # Google parameter header
|
||||
( .* ) # section
|
||||
"""
|
||||
|
||||
re_param_section = re.compile(
|
||||
_re_section_template.format(r"(?:Args|Arguments|Parameters)"),
|
||||
re.X | re.S | re.M,
|
||||
)
|
||||
|
||||
re_keyword_param_section = re.compile(
|
||||
_re_section_template.format(r"Keyword\s(?:Args|Arguments|Parameters)"),
|
||||
re.X | re.S | re.M,
|
||||
)
|
||||
|
||||
re_param_line = re.compile(
|
||||
r"""
|
||||
\s* \*{{0,2}}(\w+) # identifier potentially with asterisks
|
||||
\s* ( [(]
|
||||
{type}
|
||||
(?:,\s+optional)?
|
||||
[)] )? \s* : # optional type declaration
|
||||
\s* (.*) # beginning of optional description
|
||||
""".format(
|
||||
type=re_multiple_type
|
||||
),
|
||||
re.X | re.S | re.M,
|
||||
)
|
||||
|
||||
re_raise_section = re.compile(
|
||||
_re_section_template.format(r"Raises"), re.X | re.S | re.M
|
||||
)
|
||||
|
||||
re_raise_line = re.compile(
|
||||
r"""
|
||||
\s* ({type}) \s* : # identifier
|
||||
\s* (.*) # beginning of optional description
|
||||
""".format(
|
||||
type=re_multiple_type
|
||||
),
|
||||
re.X | re.S | re.M,
|
||||
)
|
||||
|
||||
re_returns_section = re.compile(
|
||||
_re_section_template.format(r"Returns?"), re.X | re.S | re.M
|
||||
)
|
||||
|
||||
re_returns_line = re.compile(
|
||||
r"""
|
||||
\s* ({type}:)? # identifier
|
||||
\s* (.*) # beginning of description
|
||||
""".format(
|
||||
type=re_multiple_type
|
||||
),
|
||||
re.X | re.S | re.M,
|
||||
)
|
||||
|
||||
re_property_returns_line = re.compile(
|
||||
r"""
|
||||
^{type}: # indentifier
|
||||
\s* (.*) # Summary line / description
|
||||
""".format(
|
||||
type=re_multiple_type
|
||||
),
|
||||
re.X | re.S | re.M,
|
||||
)
|
||||
|
||||
re_yields_section = re.compile(
|
||||
_re_section_template.format(r"Yields?"), re.X | re.S | re.M
|
||||
)
|
||||
|
||||
re_yields_line = re_returns_line
|
||||
|
||||
supports_yields = True
|
||||
|
||||
def is_valid(self):
|
||||
return bool(
|
||||
self.re_param_section.search(self.doc)
|
||||
or self.re_raise_section.search(self.doc)
|
||||
or self.re_returns_section.search(self.doc)
|
||||
or self.re_yields_section.search(self.doc)
|
||||
or self.re_property_returns_line.search(self._first_line())
|
||||
)
|
||||
|
||||
def has_params(self):
|
||||
if not self.doc:
|
||||
return False
|
||||
|
||||
return self.re_param_section.search(self.doc) is not None
|
||||
|
||||
def has_returns(self):
|
||||
if not self.doc:
|
||||
return False
|
||||
|
||||
entries = self._parse_section(self.re_returns_section)
|
||||
for entry in entries:
|
||||
match = self.re_returns_line.match(entry)
|
||||
if not match:
|
||||
continue
|
||||
|
||||
return_desc = match.group(2)
|
||||
if return_desc:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def has_rtype(self):
|
||||
if not self.doc:
|
||||
return False
|
||||
|
||||
entries = self._parse_section(self.re_returns_section)
|
||||
for entry in entries:
|
||||
match = self.re_returns_line.match(entry)
|
||||
if not match:
|
||||
continue
|
||||
|
||||
return_type = match.group(1)
|
||||
if return_type:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def has_property_returns(self):
|
||||
# The summary line is the return doc,
|
||||
# so the first line must not be a known directive.
|
||||
first_line = self._first_line()
|
||||
return not bool(
|
||||
self.re_param_section.search(first_line)
|
||||
or self.re_raise_section.search(first_line)
|
||||
or self.re_returns_section.search(first_line)
|
||||
or self.re_yields_section.search(first_line)
|
||||
)
|
||||
|
||||
def has_property_type(self):
|
||||
if not self.doc:
|
||||
return False
|
||||
|
||||
return bool(self.re_property_returns_line.match(self._first_line()))
|
||||
|
||||
def has_yields(self):
|
||||
if not self.doc:
|
||||
return False
|
||||
|
||||
entries = self._parse_section(self.re_yields_section)
|
||||
for entry in entries:
|
||||
match = self.re_yields_line.match(entry)
|
||||
if not match:
|
||||
continue
|
||||
|
||||
yield_desc = match.group(2)
|
||||
if yield_desc:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def has_yields_type(self):
|
||||
if not self.doc:
|
||||
return False
|
||||
|
||||
entries = self._parse_section(self.re_yields_section)
|
||||
for entry in entries:
|
||||
match = self.re_yields_line.match(entry)
|
||||
if not match:
|
||||
continue
|
||||
|
||||
yield_type = match.group(1)
|
||||
if yield_type:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def exceptions(self):
|
||||
types = set()
|
||||
|
||||
entries = self._parse_section(self.re_raise_section)
|
||||
for entry in entries:
|
||||
match = self.re_raise_line.match(entry)
|
||||
if not match:
|
||||
continue
|
||||
|
||||
exc_type = match.group(1)
|
||||
exc_desc = match.group(2)
|
||||
if exc_desc:
|
||||
types.update(_split_multiple_exc_types(exc_type))
|
||||
|
||||
return types
|
||||
|
||||
def match_param_docs(self):
|
||||
params_with_doc = set()
|
||||
params_with_type = set()
|
||||
|
||||
entries = self._parse_section(self.re_param_section)
|
||||
entries.extend(self._parse_section(self.re_keyword_param_section))
|
||||
for entry in entries:
|
||||
match = self.re_param_line.match(entry)
|
||||
if not match:
|
||||
continue
|
||||
|
||||
param_name = match.group(1)
|
||||
param_type = match.group(2)
|
||||
param_desc = match.group(3)
|
||||
if param_type:
|
||||
params_with_type.add(param_name)
|
||||
|
||||
if param_desc:
|
||||
params_with_doc.add(param_name)
|
||||
|
||||
return params_with_doc, params_with_type
|
||||
|
||||
def _first_line(self):
|
||||
return self.doc.lstrip().split("\n", 1)[0]
|
||||
|
||||
@staticmethod
|
||||
def min_section_indent(section_match):
|
||||
return len(section_match.group(1)) + 1
|
||||
|
||||
@staticmethod
|
||||
def _is_section_header(_):
|
||||
# Google parsing does not need to detect section headers,
|
||||
# because it works off of indentation level only
|
||||
return False
|
||||
|
||||
def _parse_section(self, section_re):
|
||||
section_match = section_re.search(self.doc)
|
||||
if section_match is None:
|
||||
return []
|
||||
|
||||
min_indentation = self.min_section_indent(section_match)
|
||||
|
||||
entries = []
|
||||
entry = []
|
||||
is_first = True
|
||||
for line in section_match.group(2).splitlines():
|
||||
if not line.strip():
|
||||
continue
|
||||
indentation = space_indentation(line)
|
||||
if indentation < min_indentation:
|
||||
break
|
||||
|
||||
# The first line after the header defines the minimum
|
||||
# indentation.
|
||||
if is_first:
|
||||
min_indentation = indentation
|
||||
is_first = False
|
||||
|
||||
if indentation == min_indentation:
|
||||
if self._is_section_header(line):
|
||||
break
|
||||
# Lines with minimum indentation must contain the beginning
|
||||
# of a new parameter documentation.
|
||||
if entry:
|
||||
entries.append("\n".join(entry))
|
||||
entry = []
|
||||
|
||||
entry.append(line)
|
||||
|
||||
if entry:
|
||||
entries.append("\n".join(entry))
|
||||
|
||||
return entries
|
||||
|
||||
|
||||
class NumpyDocstring(GoogleDocstring):
|
||||
_re_section_template = r"""
|
||||
^([ ]*) {0} \s*?$ # Numpy parameters header
|
||||
\s* [-=]+ \s*?$ # underline
|
||||
( .* ) # section
|
||||
"""
|
||||
|
||||
re_param_section = re.compile(
|
||||
_re_section_template.format(r"(?:Args|Arguments|Parameters)"),
|
||||
re.X | re.S | re.M,
|
||||
)
|
||||
|
||||
re_param_line = re.compile(
|
||||
r"""
|
||||
\s* (\w+) # identifier
|
||||
\s* :
|
||||
\s* (?:({type})(?:,\s+optional)?)? # optional type declaration
|
||||
\n # description starts on a new line
|
||||
\s* (.*) # description
|
||||
""".format(
|
||||
type=GoogleDocstring.re_multiple_type
|
||||
),
|
||||
re.X | re.S,
|
||||
)
|
||||
|
||||
re_raise_section = re.compile(
|
||||
_re_section_template.format(r"Raises"), re.X | re.S | re.M
|
||||
)
|
||||
|
||||
re_raise_line = re.compile(
|
||||
r"""
|
||||
\s* ({type})$ # type declaration
|
||||
\s* (.*) # optional description
|
||||
""".format(
|
||||
type=GoogleDocstring.re_type
|
||||
),
|
||||
re.X | re.S | re.M,
|
||||
)
|
||||
|
||||
re_returns_section = re.compile(
|
||||
_re_section_template.format(r"Returns?"), re.X | re.S | re.M
|
||||
)
|
||||
|
||||
re_returns_line = re.compile(
|
||||
r"""
|
||||
\s* (?:\w+\s+:\s+)? # optional name
|
||||
({type})$ # type declaration
|
||||
\s* (.*) # optional description
|
||||
""".format(
|
||||
type=GoogleDocstring.re_multiple_type
|
||||
),
|
||||
re.X | re.S | re.M,
|
||||
)
|
||||
|
||||
re_yields_section = re.compile(
|
||||
_re_section_template.format(r"Yields?"), re.X | re.S | re.M
|
||||
)
|
||||
|
||||
re_yields_line = re_returns_line
|
||||
|
||||
supports_yields = True
|
||||
|
||||
@staticmethod
|
||||
def min_section_indent(section_match):
|
||||
return len(section_match.group(1))
|
||||
|
||||
@staticmethod
|
||||
def _is_section_header(line):
|
||||
return bool(re.match(r"\s*-+$", line))
|
||||
|
||||
|
||||
DOCSTRING_TYPES = {
|
||||
"sphinx": SphinxDocstring,
|
||||
"epytext": EpytextDocstring,
|
||||
"google": GoogleDocstring,
|
||||
"numpy": NumpyDocstring,
|
||||
"default": Docstring,
|
||||
}
|
||||
"""A map of the name of the docstring type to its class.
|
||||
|
||||
:type: dict(str, type)
|
||||
"""
|
||||
@@ -0,0 +1,71 @@
|
||||
# Copyright (c) 2016, 2018 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2019 Hugo van Kemenade <hugovk@users.noreply.github.com>
|
||||
# Copyright (c) 2019 Pierre Sassoulas <pierre.sassoulas@gmail.com>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""Checker for deprecated builtins."""
|
||||
import astroid
|
||||
|
||||
from pylint.checkers import BaseChecker
|
||||
from pylint.checkers.utils import check_messages
|
||||
from pylint.interfaces import IAstroidChecker
|
||||
|
||||
BAD_FUNCTIONS = ["map", "filter"]
|
||||
# Some hints regarding the use of bad builtins.
|
||||
BUILTIN_HINTS = {"map": "Using a list comprehension can be clearer."}
|
||||
BUILTIN_HINTS["filter"] = BUILTIN_HINTS["map"]
|
||||
|
||||
|
||||
class BadBuiltinChecker(BaseChecker):
|
||||
|
||||
__implements__ = (IAstroidChecker,)
|
||||
name = "deprecated_builtins"
|
||||
msgs = {
|
||||
"W0141": (
|
||||
"Used builtin function %s",
|
||||
"bad-builtin",
|
||||
"Used when a black listed builtin function is used (see the "
|
||||
"bad-function option). Usual black listed functions are the ones "
|
||||
"like map, or filter , where Python offers now some cleaner "
|
||||
"alternative like list comprehension.",
|
||||
)
|
||||
}
|
||||
|
||||
options = (
|
||||
(
|
||||
"bad-functions",
|
||||
{
|
||||
"default": BAD_FUNCTIONS,
|
||||
"type": "csv",
|
||||
"metavar": "<builtin function names>",
|
||||
"help": "List of builtins function names that should not be "
|
||||
"used, separated by a comma",
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
@check_messages("bad-builtin")
|
||||
def visit_call(self, node):
|
||||
if isinstance(node.func, astroid.Name):
|
||||
name = node.func.name
|
||||
# ignore the name if it's not a builtin (i.e. not defined in the
|
||||
# locals nor globals scope)
|
||||
if not (name in node.frame() or name in node.root()):
|
||||
if name in self.config.bad_functions:
|
||||
hint = BUILTIN_HINTS.get(name)
|
||||
if hint:
|
||||
args = "%r. %s" % (name, hint)
|
||||
else:
|
||||
args = repr(name)
|
||||
self.add_message("bad-builtin", node=node, args=args)
|
||||
|
||||
|
||||
def register(linter):
|
||||
"""Required method to auto register this checker.
|
||||
|
||||
:param linter: Main interface object for Pylint plugins
|
||||
:type linter: Pylint object
|
||||
"""
|
||||
linter.register_checker(BadBuiltinChecker(linter))
|
||||
@@ -0,0 +1,74 @@
|
||||
# Copyright (c) 2019-2020 Tyler Thieding <tyler@thieding.com>
|
||||
# Copyright (c) 2019 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2020 Anthony Sottile <asottile@umich.edu>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""Looks for try/except statements with too much code in the try clause."""
|
||||
|
||||
from astroid.node_classes import For, If, While, With
|
||||
|
||||
from pylint import checkers, interfaces
|
||||
|
||||
|
||||
class BroadTryClauseChecker(checkers.BaseChecker):
|
||||
"""Checks for try clauses with too many lines.
|
||||
|
||||
According to PEP 8, ``try`` clauses shall contain the absolute minimum
|
||||
amount of code. This checker enforces a maximum number of statements within
|
||||
``try`` clauses.
|
||||
|
||||
"""
|
||||
|
||||
__implements__ = interfaces.IAstroidChecker
|
||||
|
||||
# configuration section name
|
||||
name = "broad_try_clause"
|
||||
msgs = {
|
||||
"W0717": (
|
||||
"%s",
|
||||
"too-many-try-statements",
|
||||
"Try clause contains too many statements.",
|
||||
)
|
||||
}
|
||||
|
||||
priority = -2
|
||||
options = (
|
||||
(
|
||||
"max-try-statements",
|
||||
{
|
||||
"default": 1,
|
||||
"type": "int",
|
||||
"metavar": "<int>",
|
||||
"help": "Maximum number of statements allowed in a try clause",
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
def _count_statements(self, try_node):
|
||||
statement_count = len(try_node.body)
|
||||
|
||||
for body_node in try_node.body:
|
||||
if isinstance(body_node, (For, If, While, With)):
|
||||
statement_count += self._count_statements(body_node)
|
||||
|
||||
return statement_count
|
||||
|
||||
def visit_tryexcept(self, node):
|
||||
try_clause_statements = self._count_statements(node)
|
||||
if try_clause_statements > self.config.max_try_statements:
|
||||
msg = "try clause contains {} statements, expected at most {}".format(
|
||||
try_clause_statements, self.config.max_try_statements
|
||||
)
|
||||
self.add_message(
|
||||
"too-many-try-statements", node.lineno, node=node, args=msg
|
||||
)
|
||||
|
||||
def visit_tryfinally(self, node):
|
||||
self.visit_tryexcept(node)
|
||||
|
||||
|
||||
def register(linter):
|
||||
"""Required method to auto register this checker."""
|
||||
linter.register_checker(BroadTryClauseChecker(linter))
|
||||
@@ -0,0 +1,23 @@
|
||||
# Copyright (c) 2014-2015 Bruno Daniel <bruno.daniel@blue-yonder.com>
|
||||
# Copyright (c) 2015-2016, 2018 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2016 Ashley Whetter <ashley@awhetter.co.uk>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
import warnings
|
||||
|
||||
from pylint.extensions import docparams
|
||||
|
||||
|
||||
def register(linter):
|
||||
"""Required method to auto register this checker.
|
||||
|
||||
:param linter: Main interface object for Pylint plugins
|
||||
:type linter: Pylint object
|
||||
"""
|
||||
warnings.warn(
|
||||
"This plugin is deprecated, use pylint.extensions.docparams instead.",
|
||||
DeprecationWarning,
|
||||
)
|
||||
linter.register_checker(docparams.DocstringParameterChecker(linter))
|
||||
@@ -0,0 +1,79 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2015 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
|
||||
# Copyright (c) 2016-2019 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2016 Glenn Matthews <glmatthe@cisco.com>
|
||||
# Copyright (c) 2018 Ville Skyttä <ville.skytta@iki.fi>
|
||||
# Copyright (c) 2019 Pierre Sassoulas <pierre.sassoulas@gmail.com>
|
||||
# Copyright (c) 2020 Anthony Sottile <asottile@umich.edu>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
import astroid
|
||||
|
||||
from pylint.checkers import BaseTokenChecker
|
||||
from pylint.checkers.utils import check_messages
|
||||
from pylint.interfaces import IAstroidChecker, ITokenChecker
|
||||
|
||||
|
||||
class ElseifUsedChecker(BaseTokenChecker):
|
||||
"""Checks for use of "else if" when an "elif" could be used
|
||||
"""
|
||||
|
||||
__implements__ = (ITokenChecker, IAstroidChecker)
|
||||
name = "else_if_used"
|
||||
msgs = {
|
||||
"R5501": (
|
||||
'Consider using "elif" instead of "else if"',
|
||||
"else-if-used",
|
||||
"Used when an else statement is immediately followed by "
|
||||
"an if statement and does not contain statements that "
|
||||
"would be unrelated to it.",
|
||||
)
|
||||
}
|
||||
|
||||
def __init__(self, linter=None):
|
||||
BaseTokenChecker.__init__(self, linter)
|
||||
self._init()
|
||||
|
||||
def _init(self):
|
||||
self._elifs = []
|
||||
self._if_counter = 0
|
||||
|
||||
def process_tokens(self, tokens):
|
||||
# Process tokens and look for 'if' or 'elif'
|
||||
for _, token, _, _, _ in tokens:
|
||||
if token == "elif":
|
||||
self._elifs.append(True)
|
||||
elif token == "if":
|
||||
self._elifs.append(False)
|
||||
|
||||
def leave_module(self, _):
|
||||
self._init()
|
||||
|
||||
def visit_ifexp(self, node):
|
||||
if isinstance(node.parent, astroid.FormattedValue):
|
||||
return
|
||||
self._if_counter += 1
|
||||
|
||||
def visit_comprehension(self, node):
|
||||
self._if_counter += len(node.ifs)
|
||||
|
||||
@check_messages("else-if-used")
|
||||
def visit_if(self, node):
|
||||
if isinstance(node.parent, astroid.If):
|
||||
orelse = node.parent.orelse
|
||||
# current if node must directly follow an "else"
|
||||
if orelse and orelse == [node]:
|
||||
if not self._elifs[self._if_counter]:
|
||||
self.add_message("else-if-used", node=node)
|
||||
self._if_counter += 1
|
||||
|
||||
|
||||
def register(linter):
|
||||
"""Required method to auto register this checker.
|
||||
|
||||
:param linter: Main interface object for Pylint plugins
|
||||
:type linter: Pylint object
|
||||
"""
|
||||
linter.register_checker(ElseifUsedChecker(linter))
|
||||
@@ -0,0 +1,75 @@
|
||||
# Copyright (c) 2016 Alexander Todorov <atodorov@otb.bg>
|
||||
# Copyright (c) 2017-2018 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2019 Pierre Sassoulas <pierre.sassoulas@gmail.com>
|
||||
# Copyright (c) 2020 Anthony Sottile <asottile@umich.edu>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""Looks for comparisons to empty string."""
|
||||
|
||||
import itertools
|
||||
|
||||
import astroid
|
||||
|
||||
from pylint import checkers, interfaces
|
||||
from pylint.checkers import utils
|
||||
|
||||
|
||||
def _is_constant_zero(node):
|
||||
return isinstance(node, astroid.Const) and node.value == 0
|
||||
|
||||
|
||||
class CompareToZeroChecker(checkers.BaseChecker):
|
||||
"""Checks for comparisons to zero.
|
||||
Most of the times you should use the fact that integers with a value of 0 are false.
|
||||
An exception to this rule is when 0 is allowed in the program and has a
|
||||
different meaning than None!
|
||||
"""
|
||||
|
||||
__implements__ = (interfaces.IAstroidChecker,)
|
||||
|
||||
# configuration section name
|
||||
name = "compare-to-zero"
|
||||
msgs = {
|
||||
"C2001": (
|
||||
"Avoid comparisons to zero",
|
||||
"compare-to-zero",
|
||||
"Used when Pylint detects comparison to a 0 constant.",
|
||||
)
|
||||
}
|
||||
|
||||
priority = -2
|
||||
options = ()
|
||||
|
||||
@utils.check_messages("compare-to-zero")
|
||||
def visit_compare(self, node):
|
||||
_operators = ["!=", "==", "is not", "is"]
|
||||
# note: astroid.Compare has the left most operand in node.left
|
||||
# while the rest are a list of tuples in node.ops
|
||||
# the format of the tuple is ('compare operator sign', node)
|
||||
# here we squash everything into `ops` to make it easier for processing later
|
||||
ops = [("", node.left)]
|
||||
ops.extend(node.ops)
|
||||
ops = list(itertools.chain(*ops))
|
||||
|
||||
for ops_idx in range(len(ops) - 2):
|
||||
op_1 = ops[ops_idx]
|
||||
op_2 = ops[ops_idx + 1]
|
||||
op_3 = ops[ops_idx + 2]
|
||||
error_detected = False
|
||||
|
||||
# 0 ?? X
|
||||
if _is_constant_zero(op_1) and op_2 in _operators:
|
||||
error_detected = True
|
||||
# X ?? 0
|
||||
elif op_2 in _operators and _is_constant_zero(op_3):
|
||||
error_detected = True
|
||||
|
||||
if error_detected:
|
||||
self.add_message("compare-to-zero", node=node)
|
||||
|
||||
|
||||
def register(linter):
|
||||
"""Required method to auto register this checker."""
|
||||
linter.register_checker(CompareToZeroChecker(linter))
|
||||
@@ -0,0 +1,539 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2014-2015 Bruno Daniel <bruno.daniel@blue-yonder.com>
|
||||
# Copyright (c) 2015-2019 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2016-2019 Ashley Whetter <ashley@awhetter.co.uk>
|
||||
# Copyright (c) 2016 Glenn Matthews <glenn@e-dad.net>
|
||||
# Copyright (c) 2016 Glenn Matthews <glmatthe@cisco.com>
|
||||
# Copyright (c) 2016 Moises Lopez <moylop260@vauxoo.com>
|
||||
# Copyright (c) 2017 Ville Skyttä <ville.skytta@iki.fi>
|
||||
# Copyright (c) 2017 John Paraskevopoulos <io.paraskev@gmail.com>
|
||||
# Copyright (c) 2018, 2020 Anthony Sottile <asottile@umich.edu>
|
||||
# Copyright (c) 2018 Jim Robertson <jrobertson98atx@gmail.com>
|
||||
# Copyright (c) 2018 Sushobhit <31987769+sushobhit27@users.noreply.github.com>
|
||||
# Copyright (c) 2018 Adam Dangoor <adamdangoor@gmail.com>
|
||||
# Copyright (c) 2019 Hugo van Kemenade <hugovk@users.noreply.github.com>
|
||||
# Copyright (c) 2019 Pierre Sassoulas <pierre.sassoulas@gmail.com>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""Pylint plugin for checking in Sphinx, Google, or Numpy style docstrings
|
||||
"""
|
||||
import astroid
|
||||
|
||||
import pylint.extensions._check_docs_utils as utils
|
||||
from pylint.checkers import BaseChecker
|
||||
from pylint.checkers import utils as checker_utils
|
||||
from pylint.interfaces import IAstroidChecker
|
||||
|
||||
|
||||
class DocstringParameterChecker(BaseChecker):
|
||||
"""Checker for Sphinx, Google, or Numpy style docstrings
|
||||
|
||||
* Check that all function, method and constructor parameters are mentioned
|
||||
in the params and types part of the docstring. Constructor parameters
|
||||
can be documented in either the class docstring or ``__init__`` docstring,
|
||||
but not both.
|
||||
* Check that there are no naming inconsistencies between the signature and
|
||||
the documentation, i.e. also report documented parameters that are missing
|
||||
in the signature. This is important to find cases where parameters are
|
||||
renamed only in the code, not in the documentation.
|
||||
* Check that all explicitly raised exceptions in a function are documented
|
||||
in the function docstring. Caught exceptions are ignored.
|
||||
|
||||
Activate this checker by adding the line::
|
||||
|
||||
load-plugins=pylint.extensions.docparams
|
||||
|
||||
to the ``MASTER`` section of your ``.pylintrc``.
|
||||
|
||||
:param linter: linter object
|
||||
:type linter: :class:`pylint.lint.PyLinter`
|
||||
"""
|
||||
|
||||
__implements__ = IAstroidChecker
|
||||
|
||||
name = "parameter_documentation"
|
||||
msgs = {
|
||||
"W9005": (
|
||||
'"%s" has constructor parameters documented in class and __init__',
|
||||
"multiple-constructor-doc",
|
||||
"Please remove parameter declarations in the class or constructor.",
|
||||
),
|
||||
"W9006": (
|
||||
'"%s" not documented as being raised',
|
||||
"missing-raises-doc",
|
||||
"Please document exceptions for all raised exception types.",
|
||||
),
|
||||
"W9008": (
|
||||
"Redundant returns documentation",
|
||||
"redundant-returns-doc",
|
||||
"Please remove the return/rtype documentation from this method.",
|
||||
),
|
||||
"W9010": (
|
||||
"Redundant yields documentation",
|
||||
"redundant-yields-doc",
|
||||
"Please remove the yields documentation from this method.",
|
||||
),
|
||||
"W9011": (
|
||||
"Missing return documentation",
|
||||
"missing-return-doc",
|
||||
"Please add documentation about what this method returns.",
|
||||
{"old_names": [("W9007", "old-missing-returns-doc")]},
|
||||
),
|
||||
"W9012": (
|
||||
"Missing return type documentation",
|
||||
"missing-return-type-doc",
|
||||
"Please document the type returned by this method.",
|
||||
# we can't use the same old_name for two different warnings
|
||||
# {'old_names': [('W9007', 'missing-returns-doc')]},
|
||||
),
|
||||
"W9013": (
|
||||
"Missing yield documentation",
|
||||
"missing-yield-doc",
|
||||
"Please add documentation about what this generator yields.",
|
||||
{"old_names": [("W9009", "old-missing-yields-doc")]},
|
||||
),
|
||||
"W9014": (
|
||||
"Missing yield type documentation",
|
||||
"missing-yield-type-doc",
|
||||
"Please document the type yielded by this method.",
|
||||
# we can't use the same old_name for two different warnings
|
||||
# {'old_names': [('W9009', 'missing-yields-doc')]},
|
||||
),
|
||||
"W9015": (
|
||||
'"%s" missing in parameter documentation',
|
||||
"missing-param-doc",
|
||||
"Please add parameter declarations for all parameters.",
|
||||
{"old_names": [("W9003", "old-missing-param-doc")]},
|
||||
),
|
||||
"W9016": (
|
||||
'"%s" missing in parameter type documentation',
|
||||
"missing-type-doc",
|
||||
"Please add parameter type declarations for all parameters.",
|
||||
{"old_names": [("W9004", "old-missing-type-doc")]},
|
||||
),
|
||||
"W9017": (
|
||||
'"%s" differing in parameter documentation',
|
||||
"differing-param-doc",
|
||||
"Please check parameter names in declarations.",
|
||||
),
|
||||
"W9018": (
|
||||
'"%s" differing in parameter type documentation',
|
||||
"differing-type-doc",
|
||||
"Please check parameter names in type declarations.",
|
||||
),
|
||||
}
|
||||
|
||||
options = (
|
||||
(
|
||||
"accept-no-param-doc",
|
||||
{
|
||||
"default": True,
|
||||
"type": "yn",
|
||||
"metavar": "<y or n>",
|
||||
"help": "Whether to accept totally missing parameter "
|
||||
"documentation in the docstring of a function that has "
|
||||
"parameters.",
|
||||
},
|
||||
),
|
||||
(
|
||||
"accept-no-raise-doc",
|
||||
{
|
||||
"default": True,
|
||||
"type": "yn",
|
||||
"metavar": "<y or n>",
|
||||
"help": "Whether to accept totally missing raises "
|
||||
"documentation in the docstring of a function that "
|
||||
"raises an exception.",
|
||||
},
|
||||
),
|
||||
(
|
||||
"accept-no-return-doc",
|
||||
{
|
||||
"default": True,
|
||||
"type": "yn",
|
||||
"metavar": "<y or n>",
|
||||
"help": "Whether to accept totally missing return "
|
||||
"documentation in the docstring of a function that "
|
||||
"returns a statement.",
|
||||
},
|
||||
),
|
||||
(
|
||||
"accept-no-yields-doc",
|
||||
{
|
||||
"default": True,
|
||||
"type": "yn",
|
||||
"metavar": "<y or n>",
|
||||
"help": "Whether to accept totally missing yields "
|
||||
"documentation in the docstring of a generator.",
|
||||
},
|
||||
),
|
||||
(
|
||||
"default-docstring-type",
|
||||
{
|
||||
"type": "choice",
|
||||
"default": "default",
|
||||
"choices": list(utils.DOCSTRING_TYPES),
|
||||
"help": "If the docstring type cannot be guessed "
|
||||
"the specified docstring type will be used.",
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
priority = -2
|
||||
|
||||
constructor_names = {"__init__", "__new__"}
|
||||
not_needed_param_in_docstring = {"self", "cls"}
|
||||
|
||||
def visit_functiondef(self, node):
|
||||
"""Called for function and method definitions (def).
|
||||
|
||||
:param node: Node for a function or method definition in the AST
|
||||
:type node: :class:`astroid.scoped_nodes.Function`
|
||||
"""
|
||||
node_doc = utils.docstringify(node.doc, self.config.default_docstring_type)
|
||||
self.check_functiondef_params(node, node_doc)
|
||||
self.check_functiondef_returns(node, node_doc)
|
||||
self.check_functiondef_yields(node, node_doc)
|
||||
|
||||
def check_functiondef_params(self, node, node_doc):
|
||||
node_allow_no_param = None
|
||||
if node.name in self.constructor_names:
|
||||
class_node = checker_utils.node_frame_class(node)
|
||||
if class_node is not None:
|
||||
class_doc = utils.docstringify(
|
||||
class_node.doc, self.config.default_docstring_type
|
||||
)
|
||||
self.check_single_constructor_params(class_doc, node_doc, class_node)
|
||||
|
||||
# __init__ or class docstrings can have no parameters documented
|
||||
# as long as the other documents them.
|
||||
node_allow_no_param = (
|
||||
class_doc.has_params()
|
||||
or class_doc.params_documented_elsewhere()
|
||||
or None
|
||||
)
|
||||
class_allow_no_param = (
|
||||
node_doc.has_params()
|
||||
or node_doc.params_documented_elsewhere()
|
||||
or None
|
||||
)
|
||||
|
||||
self.check_arguments_in_docstring(
|
||||
class_doc, node.args, class_node, class_allow_no_param
|
||||
)
|
||||
|
||||
self.check_arguments_in_docstring(
|
||||
node_doc, node.args, node, node_allow_no_param
|
||||
)
|
||||
|
||||
def check_functiondef_returns(self, node, node_doc):
|
||||
if (not node_doc.supports_yields and node.is_generator()) or node.is_abstract():
|
||||
return
|
||||
|
||||
return_nodes = node.nodes_of_class(astroid.Return)
|
||||
if (node_doc.has_returns() or node_doc.has_rtype()) and not any(
|
||||
utils.returns_something(ret_node) for ret_node in return_nodes
|
||||
):
|
||||
self.add_message("redundant-returns-doc", node=node)
|
||||
|
||||
def check_functiondef_yields(self, node, node_doc):
|
||||
if not node_doc.supports_yields or node.is_abstract():
|
||||
return
|
||||
|
||||
if (
|
||||
node_doc.has_yields() or node_doc.has_yields_type()
|
||||
) and not node.is_generator():
|
||||
self.add_message("redundant-yields-doc", node=node)
|
||||
|
||||
def visit_raise(self, node):
|
||||
func_node = node.frame()
|
||||
if not isinstance(func_node, astroid.FunctionDef):
|
||||
return
|
||||
|
||||
expected_excs = utils.possible_exc_types(node)
|
||||
|
||||
if not expected_excs:
|
||||
return
|
||||
|
||||
if not func_node.doc:
|
||||
# If this is a property setter,
|
||||
# the property should have the docstring instead.
|
||||
property_ = utils.get_setters_property(func_node)
|
||||
if property_:
|
||||
func_node = property_
|
||||
|
||||
doc = utils.docstringify(func_node.doc, self.config.default_docstring_type)
|
||||
if not doc.is_valid():
|
||||
if doc.doc:
|
||||
self._handle_no_raise_doc(expected_excs, func_node)
|
||||
return
|
||||
|
||||
found_excs_full_names = doc.exceptions()
|
||||
|
||||
# Extract just the class name, e.g. "error" from "re.error"
|
||||
found_excs_class_names = {exc.split(".")[-1] for exc in found_excs_full_names}
|
||||
missing_excs = expected_excs - found_excs_class_names
|
||||
self._add_raise_message(missing_excs, func_node)
|
||||
|
||||
def visit_return(self, node):
|
||||
if not utils.returns_something(node):
|
||||
return
|
||||
|
||||
func_node = node.frame()
|
||||
if not isinstance(func_node, astroid.FunctionDef):
|
||||
return
|
||||
|
||||
doc = utils.docstringify(func_node.doc, self.config.default_docstring_type)
|
||||
if not doc.is_valid() and self.config.accept_no_return_doc:
|
||||
return
|
||||
|
||||
is_property = checker_utils.decorated_with_property(func_node)
|
||||
|
||||
if not (doc.has_returns() or (doc.has_property_returns() and is_property)):
|
||||
self.add_message("missing-return-doc", node=func_node)
|
||||
|
||||
if func_node.returns:
|
||||
return
|
||||
|
||||
if not (doc.has_rtype() or (doc.has_property_type() and is_property)):
|
||||
self.add_message("missing-return-type-doc", node=func_node)
|
||||
|
||||
def visit_yield(self, node):
|
||||
func_node = node.frame()
|
||||
if not isinstance(func_node, astroid.FunctionDef):
|
||||
return
|
||||
|
||||
doc = utils.docstringify(func_node.doc, self.config.default_docstring_type)
|
||||
if not doc.is_valid() and self.config.accept_no_yields_doc:
|
||||
return
|
||||
|
||||
if doc.supports_yields:
|
||||
doc_has_yields = doc.has_yields()
|
||||
doc_has_yields_type = doc.has_yields_type()
|
||||
else:
|
||||
doc_has_yields = doc.has_returns()
|
||||
doc_has_yields_type = doc.has_rtype()
|
||||
|
||||
if not doc_has_yields:
|
||||
self.add_message("missing-yield-doc", node=func_node)
|
||||
|
||||
if not (doc_has_yields_type or func_node.returns):
|
||||
self.add_message("missing-yield-type-doc", node=func_node)
|
||||
|
||||
def visit_yieldfrom(self, node):
|
||||
self.visit_yield(node)
|
||||
|
||||
def _compare_missing_args(
|
||||
self,
|
||||
found_argument_names,
|
||||
message_id,
|
||||
not_needed_names,
|
||||
expected_argument_names,
|
||||
warning_node,
|
||||
):
|
||||
"""Compare the found argument names with the expected ones and
|
||||
generate a message if there are arguments missing.
|
||||
|
||||
:param set found_argument_names: argument names found in the
|
||||
docstring
|
||||
|
||||
:param str message_id: pylint message id
|
||||
|
||||
:param not_needed_names: names that may be omitted
|
||||
:type not_needed_names: set of str
|
||||
|
||||
:param set expected_argument_names: Expected argument names
|
||||
:param NodeNG warning_node: The node to be analyzed
|
||||
"""
|
||||
missing_argument_names = (
|
||||
expected_argument_names - found_argument_names
|
||||
) - not_needed_names
|
||||
if missing_argument_names:
|
||||
self.add_message(
|
||||
message_id,
|
||||
args=(", ".join(sorted(missing_argument_names)),),
|
||||
node=warning_node,
|
||||
)
|
||||
|
||||
def _compare_different_args(
|
||||
self,
|
||||
found_argument_names,
|
||||
message_id,
|
||||
not_needed_names,
|
||||
expected_argument_names,
|
||||
warning_node,
|
||||
):
|
||||
"""Compare the found argument names with the expected ones and
|
||||
generate a message if there are extra arguments found.
|
||||
|
||||
:param set found_argument_names: argument names found in the
|
||||
docstring
|
||||
|
||||
:param str message_id: pylint message id
|
||||
|
||||
:param not_needed_names: names that may be omitted
|
||||
:type not_needed_names: set of str
|
||||
|
||||
:param set expected_argument_names: Expected argument names
|
||||
:param NodeNG warning_node: The node to be analyzed
|
||||
"""
|
||||
differing_argument_names = (
|
||||
(expected_argument_names ^ found_argument_names)
|
||||
- not_needed_names
|
||||
- expected_argument_names
|
||||
)
|
||||
|
||||
if differing_argument_names:
|
||||
self.add_message(
|
||||
message_id,
|
||||
args=(", ".join(sorted(differing_argument_names)),),
|
||||
node=warning_node,
|
||||
)
|
||||
|
||||
def check_arguments_in_docstring(
|
||||
self, doc, arguments_node, warning_node, accept_no_param_doc=None
|
||||
):
|
||||
"""Check that all parameters in a function, method or class constructor
|
||||
on the one hand and the parameters mentioned in the parameter
|
||||
documentation (e.g. the Sphinx tags 'param' and 'type') on the other
|
||||
hand are consistent with each other.
|
||||
|
||||
* Undocumented parameters except 'self' are noticed.
|
||||
* Undocumented parameter types except for 'self' and the ``*<args>``
|
||||
and ``**<kwargs>`` parameters are noticed.
|
||||
* Parameters mentioned in the parameter documentation that don't or no
|
||||
longer exist in the function parameter list are noticed.
|
||||
* If the text "For the parameters, see" or "For the other parameters,
|
||||
see" (ignoring additional whitespace) is mentioned in the docstring,
|
||||
missing parameter documentation is tolerated.
|
||||
* If there's no Sphinx style, Google style or NumPy style parameter
|
||||
documentation at all, i.e. ``:param`` is never mentioned etc., the
|
||||
checker assumes that the parameters are documented in another format
|
||||
and the absence is tolerated.
|
||||
|
||||
:param doc: Docstring for the function, method or class.
|
||||
:type doc: :class:`Docstring`
|
||||
|
||||
:param arguments_node: Arguments node for the function, method or
|
||||
class constructor.
|
||||
:type arguments_node: :class:`astroid.scoped_nodes.Arguments`
|
||||
|
||||
:param warning_node: The node to assign the warnings to
|
||||
:type warning_node: :class:`astroid.scoped_nodes.Node`
|
||||
|
||||
:param accept_no_param_doc: Whether or not to allow no parameters
|
||||
to be documented.
|
||||
If None then this value is read from the configuration.
|
||||
:type accept_no_param_doc: bool or None
|
||||
"""
|
||||
# Tolerate missing param or type declarations if there is a link to
|
||||
# another method carrying the same name.
|
||||
if not doc.doc:
|
||||
return
|
||||
|
||||
if accept_no_param_doc is None:
|
||||
accept_no_param_doc = self.config.accept_no_param_doc
|
||||
tolerate_missing_params = doc.params_documented_elsewhere()
|
||||
|
||||
# Collect the function arguments.
|
||||
expected_argument_names = {arg.name for arg in arguments_node.args}
|
||||
expected_argument_names.update(arg.name for arg in arguments_node.kwonlyargs)
|
||||
not_needed_type_in_docstring = self.not_needed_param_in_docstring.copy()
|
||||
|
||||
if arguments_node.vararg is not None:
|
||||
expected_argument_names.add(arguments_node.vararg)
|
||||
not_needed_type_in_docstring.add(arguments_node.vararg)
|
||||
if arguments_node.kwarg is not None:
|
||||
expected_argument_names.add(arguments_node.kwarg)
|
||||
not_needed_type_in_docstring.add(arguments_node.kwarg)
|
||||
params_with_doc, params_with_type = doc.match_param_docs()
|
||||
|
||||
# Tolerate no parameter documentation at all.
|
||||
if not params_with_doc and not params_with_type and accept_no_param_doc:
|
||||
tolerate_missing_params = True
|
||||
|
||||
if not tolerate_missing_params:
|
||||
self._compare_missing_args(
|
||||
params_with_doc,
|
||||
"missing-param-doc",
|
||||
self.not_needed_param_in_docstring,
|
||||
expected_argument_names,
|
||||
warning_node,
|
||||
)
|
||||
|
||||
for index, arg_name in enumerate(arguments_node.args):
|
||||
if arguments_node.annotations[index]:
|
||||
params_with_type.add(arg_name.name)
|
||||
for index, arg_name in enumerate(arguments_node.kwonlyargs):
|
||||
if arguments_node.kwonlyargs_annotations[index]:
|
||||
params_with_type.add(arg_name.name)
|
||||
|
||||
if not tolerate_missing_params:
|
||||
self._compare_missing_args(
|
||||
params_with_type,
|
||||
"missing-type-doc",
|
||||
not_needed_type_in_docstring,
|
||||
expected_argument_names,
|
||||
warning_node,
|
||||
)
|
||||
|
||||
self._compare_different_args(
|
||||
params_with_doc,
|
||||
"differing-param-doc",
|
||||
self.not_needed_param_in_docstring,
|
||||
expected_argument_names,
|
||||
warning_node,
|
||||
)
|
||||
self._compare_different_args(
|
||||
params_with_type,
|
||||
"differing-type-doc",
|
||||
not_needed_type_in_docstring,
|
||||
expected_argument_names,
|
||||
warning_node,
|
||||
)
|
||||
|
||||
def check_single_constructor_params(self, class_doc, init_doc, class_node):
|
||||
if class_doc.has_params() and init_doc.has_params():
|
||||
self.add_message(
|
||||
"multiple-constructor-doc", args=(class_node.name,), node=class_node
|
||||
)
|
||||
|
||||
def _handle_no_raise_doc(self, excs, node):
|
||||
if self.config.accept_no_raise_doc:
|
||||
return
|
||||
|
||||
self._add_raise_message(excs, node)
|
||||
|
||||
def _add_raise_message(self, missing_excs, node):
|
||||
"""
|
||||
Adds a message on :param:`node` for the missing exception type.
|
||||
|
||||
:param missing_excs: A list of missing exception types.
|
||||
:type missing_excs: set(str)
|
||||
|
||||
:param node: The node show the message on.
|
||||
:type node: astroid.node_classes.NodeNG
|
||||
"""
|
||||
if node.is_abstract():
|
||||
try:
|
||||
missing_excs.remove("NotImplementedError")
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
if not missing_excs:
|
||||
return
|
||||
|
||||
self.add_message(
|
||||
"missing-raises-doc", args=(", ".join(sorted(missing_excs)),), node=node
|
||||
)
|
||||
|
||||
|
||||
def register(linter):
|
||||
"""Required method to auto register this checker.
|
||||
|
||||
:param linter: Main interface object for Pylint plugins
|
||||
:type linter: Pylint object
|
||||
"""
|
||||
linter.register_checker(DocstringParameterChecker(linter))
|
||||
@@ -0,0 +1,91 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2016-2018 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2016 Łukasz Rogalski <rogalski.91@gmail.com>
|
||||
# Copyright (c) 2016 Luis Escobar <lescobar@vauxoo.com>
|
||||
# Copyright (c) 2019 Pierre Sassoulas <pierre.sassoulas@gmail.com>
|
||||
# Copyright (c) 2020 Anthony Sottile <asottile@umich.edu>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
import linecache
|
||||
|
||||
from pylint import checkers
|
||||
from pylint.checkers.utils import check_messages
|
||||
from pylint.interfaces import HIGH, IAstroidChecker
|
||||
|
||||
|
||||
class DocStringStyleChecker(checkers.BaseChecker):
|
||||
"""Checks format of docstrings based on PEP 0257"""
|
||||
|
||||
__implements__ = IAstroidChecker
|
||||
name = "docstyle"
|
||||
|
||||
msgs = {
|
||||
"C0198": (
|
||||
'Bad docstring quotes in %s, expected """, given %s',
|
||||
"bad-docstring-quotes",
|
||||
"Used when a docstring does not have triple double quotes.",
|
||||
),
|
||||
"C0199": (
|
||||
"First line empty in %s docstring",
|
||||
"docstring-first-line-empty",
|
||||
"Used when a blank line is found at the beginning of a docstring.",
|
||||
),
|
||||
}
|
||||
|
||||
@check_messages("docstring-first-line-empty", "bad-docstring-quotes")
|
||||
def visit_module(self, node):
|
||||
self._check_docstring("module", node)
|
||||
|
||||
def visit_classdef(self, node):
|
||||
self._check_docstring("class", node)
|
||||
|
||||
def visit_functiondef(self, node):
|
||||
ftype = "method" if node.is_method() else "function"
|
||||
self._check_docstring(ftype, node)
|
||||
|
||||
visit_asyncfunctiondef = visit_functiondef
|
||||
|
||||
def _check_docstring(self, node_type, node):
|
||||
docstring = node.doc
|
||||
if docstring and docstring[0] == "\n":
|
||||
self.add_message(
|
||||
"docstring-first-line-empty",
|
||||
node=node,
|
||||
args=(node_type,),
|
||||
confidence=HIGH,
|
||||
)
|
||||
|
||||
# Use "linecache", instead of node.as_string(), because the latter
|
||||
# looses the original form of the docstrings.
|
||||
|
||||
if docstring:
|
||||
lineno = node.fromlineno + 1
|
||||
line = linecache.getline(node.root().file, lineno).lstrip()
|
||||
if line and line.find('"""') == 0:
|
||||
return
|
||||
if line and "'''" in line:
|
||||
quotes = "'''"
|
||||
elif line and line[0] == '"':
|
||||
quotes = '"'
|
||||
elif line and line[0] == "'":
|
||||
quotes = "'"
|
||||
else:
|
||||
quotes = False
|
||||
if quotes:
|
||||
self.add_message(
|
||||
"bad-docstring-quotes",
|
||||
node=node,
|
||||
args=(node_type, quotes),
|
||||
confidence=HIGH,
|
||||
)
|
||||
|
||||
|
||||
def register(linter):
|
||||
"""Required method to auto register this checker.
|
||||
|
||||
:param linter: Main interface object for Pylint plugins
|
||||
:type linter: Pylint object
|
||||
"""
|
||||
linter.register_checker(DocStringStyleChecker(linter))
|
||||
@@ -0,0 +1,75 @@
|
||||
# Copyright (c) 2016 Alexander Todorov <atodorov@otb.bg>
|
||||
# Copyright (c) 2017-2018 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2019 Pierre Sassoulas <pierre.sassoulas@gmail.com>
|
||||
# Copyright (c) 2020 Anthony Sottile <asottile@umich.edu>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""Looks for comparisons to empty string."""
|
||||
|
||||
import itertools
|
||||
|
||||
import astroid
|
||||
|
||||
from pylint import checkers, interfaces
|
||||
from pylint.checkers import utils
|
||||
|
||||
|
||||
def _is_constant_empty_str(node):
|
||||
return isinstance(node, astroid.Const) and node.value == ""
|
||||
|
||||
|
||||
class CompareToEmptyStringChecker(checkers.BaseChecker):
|
||||
"""Checks for comparisons to empty string.
|
||||
Most of the times you should use the fact that empty strings are false.
|
||||
An exception to this rule is when an empty string value is allowed in the program
|
||||
and has a different meaning than None!
|
||||
"""
|
||||
|
||||
__implements__ = (interfaces.IAstroidChecker,)
|
||||
|
||||
# configuration section name
|
||||
name = "compare-to-empty-string"
|
||||
msgs = {
|
||||
"C1901": (
|
||||
"Avoid comparisons to empty string",
|
||||
"compare-to-empty-string",
|
||||
"Used when Pylint detects comparison to an empty string constant.",
|
||||
)
|
||||
}
|
||||
|
||||
priority = -2
|
||||
options = ()
|
||||
|
||||
@utils.check_messages("compare-to-empty-string")
|
||||
def visit_compare(self, node):
|
||||
_operators = ["!=", "==", "is not", "is"]
|
||||
# note: astroid.Compare has the left most operand in node.left
|
||||
# while the rest are a list of tuples in node.ops
|
||||
# the format of the tuple is ('compare operator sign', node)
|
||||
# here we squash everything into `ops` to make it easier for processing later
|
||||
ops = [("", node.left)]
|
||||
ops.extend(node.ops)
|
||||
ops = list(itertools.chain(*ops))
|
||||
|
||||
for ops_idx in range(len(ops) - 2):
|
||||
op_1 = ops[ops_idx]
|
||||
op_2 = ops[ops_idx + 1]
|
||||
op_3 = ops[ops_idx + 2]
|
||||
error_detected = False
|
||||
|
||||
# x ?? ""
|
||||
if _is_constant_empty_str(op_1) and op_2 in _operators:
|
||||
error_detected = True
|
||||
# '' ?? X
|
||||
elif op_2 in _operators and _is_constant_empty_str(op_3):
|
||||
error_detected = True
|
||||
|
||||
if error_detected:
|
||||
self.add_message("compare-to-empty-string", node=node)
|
||||
|
||||
|
||||
def register(linter):
|
||||
"""Required method to auto register this checker."""
|
||||
linter.register_checker(CompareToEmptyStringChecker(linter))
|
||||
@@ -0,0 +1,199 @@
|
||||
# Copyright (c) 2016-2019 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2016 Moises Lopez <moylop260@vauxoo.com>
|
||||
# Copyright (c) 2017 hippo91 <guillaume.peillex@gmail.com>
|
||||
# Copyright (c) 2019 Hugo van Kemenade <hugovk@users.noreply.github.com>
|
||||
# Copyright (c) 2019 Pierre Sassoulas <pierre.sassoulas@gmail.com>
|
||||
# Copyright (c) 2020 Anthony Sottile <asottile@umich.edu>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""Module to add McCabe checker class for pylint. """
|
||||
|
||||
from mccabe import PathGraph as Mccabe_PathGraph
|
||||
from mccabe import PathGraphingAstVisitor as Mccabe_PathGraphingAstVisitor
|
||||
|
||||
from pylint import checkers
|
||||
from pylint.checkers.utils import check_messages
|
||||
from pylint.interfaces import HIGH, IAstroidChecker
|
||||
|
||||
|
||||
class PathGraph(Mccabe_PathGraph):
|
||||
def __init__(self, node):
|
||||
super().__init__(name="", entity="", lineno=1)
|
||||
self.root = node
|
||||
|
||||
|
||||
class PathGraphingAstVisitor(Mccabe_PathGraphingAstVisitor):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._bottom_counter = 0
|
||||
|
||||
def default(self, node, *args):
|
||||
for child in node.get_children():
|
||||
self.dispatch(child, *args)
|
||||
|
||||
def dispatch(self, node, *args):
|
||||
self.node = node
|
||||
klass = node.__class__
|
||||
meth = self._cache.get(klass)
|
||||
if meth is None:
|
||||
class_name = klass.__name__
|
||||
meth = getattr(self.visitor, "visit" + class_name, self.default)
|
||||
self._cache[klass] = meth
|
||||
return meth(node, *args)
|
||||
|
||||
def visitFunctionDef(self, node):
|
||||
if self.graph is not None:
|
||||
# closure
|
||||
pathnode = self._append_node(node)
|
||||
self.tail = pathnode
|
||||
self.dispatch_list(node.body)
|
||||
bottom = "%s" % self._bottom_counter
|
||||
self._bottom_counter += 1
|
||||
self.graph.connect(self.tail, bottom)
|
||||
self.graph.connect(node, bottom)
|
||||
self.tail = bottom
|
||||
else:
|
||||
self.graph = PathGraph(node)
|
||||
self.tail = node
|
||||
self.dispatch_list(node.body)
|
||||
self.graphs["%s%s" % (self.classname, node.name)] = self.graph
|
||||
self.reset()
|
||||
|
||||
visitAsyncFunctionDef = visitFunctionDef
|
||||
|
||||
def visitSimpleStatement(self, node):
|
||||
self._append_node(node)
|
||||
|
||||
visitAssert = (
|
||||
visitAssign
|
||||
) = (
|
||||
visitAugAssign
|
||||
) = (
|
||||
visitDelete
|
||||
) = (
|
||||
visitPrint
|
||||
) = (
|
||||
visitRaise
|
||||
) = (
|
||||
visitYield
|
||||
) = (
|
||||
visitImport
|
||||
) = (
|
||||
visitCall
|
||||
) = (
|
||||
visitSubscript
|
||||
) = (
|
||||
visitPass
|
||||
) = (
|
||||
visitContinue
|
||||
) = (
|
||||
visitBreak
|
||||
) = visitGlobal = visitReturn = visitExpr = visitAwait = visitSimpleStatement
|
||||
|
||||
def visitWith(self, node):
|
||||
self._append_node(node)
|
||||
self.dispatch_list(node.body)
|
||||
|
||||
visitAsyncWith = visitWith
|
||||
|
||||
def _append_node(self, node):
|
||||
if not self.tail:
|
||||
return None
|
||||
self.graph.connect(self.tail, node)
|
||||
self.tail = node
|
||||
return node
|
||||
|
||||
def _subgraph(self, node, name, extra_blocks=()):
|
||||
"""create the subgraphs representing any `if` and `for` statements"""
|
||||
if self.graph is None:
|
||||
# global loop
|
||||
self.graph = PathGraph(node)
|
||||
self._subgraph_parse(node, node, extra_blocks)
|
||||
self.graphs["%s%s" % (self.classname, name)] = self.graph
|
||||
self.reset()
|
||||
else:
|
||||
self._append_node(node)
|
||||
self._subgraph_parse(node, node, extra_blocks)
|
||||
|
||||
def _subgraph_parse(self, node, pathnode, extra_blocks):
|
||||
"""parse the body and any `else` block of `if` and `for` statements"""
|
||||
loose_ends = []
|
||||
self.tail = node
|
||||
self.dispatch_list(node.body)
|
||||
loose_ends.append(self.tail)
|
||||
for extra in extra_blocks:
|
||||
self.tail = node
|
||||
self.dispatch_list(extra.body)
|
||||
loose_ends.append(self.tail)
|
||||
if node.orelse:
|
||||
self.tail = node
|
||||
self.dispatch_list(node.orelse)
|
||||
loose_ends.append(self.tail)
|
||||
else:
|
||||
loose_ends.append(node)
|
||||
if node:
|
||||
bottom = "%s" % self._bottom_counter
|
||||
self._bottom_counter += 1
|
||||
for end in loose_ends:
|
||||
self.graph.connect(end, bottom)
|
||||
self.tail = bottom
|
||||
|
||||
|
||||
class McCabeMethodChecker(checkers.BaseChecker):
|
||||
"""Checks McCabe complexity cyclomatic threshold in methods and functions
|
||||
to validate a too complex code.
|
||||
"""
|
||||
|
||||
__implements__ = IAstroidChecker
|
||||
name = "design"
|
||||
|
||||
msgs = {
|
||||
"R1260": (
|
||||
"%s is too complex. The McCabe rating is %d",
|
||||
"too-complex",
|
||||
"Used when a method or function is too complex based on "
|
||||
"McCabe Complexity Cyclomatic",
|
||||
)
|
||||
}
|
||||
options = (
|
||||
(
|
||||
"max-complexity",
|
||||
{
|
||||
"default": 10,
|
||||
"type": "int",
|
||||
"metavar": "<int>",
|
||||
"help": "McCabe complexity cyclomatic threshold",
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
@check_messages("too-complex")
|
||||
def visit_module(self, node):
|
||||
"""visit an astroid.Module node to check too complex rating and
|
||||
add message if is greather than max_complexity stored from options"""
|
||||
visitor = PathGraphingAstVisitor()
|
||||
for child in node.body:
|
||||
visitor.preorder(child, visitor)
|
||||
for graph in visitor.graphs.values():
|
||||
complexity = graph.complexity()
|
||||
node = graph.root
|
||||
if hasattr(node, "name"):
|
||||
node_name = "'%s'" % node.name
|
||||
else:
|
||||
node_name = "This '%s'" % node.__class__.__name__.lower()
|
||||
if complexity <= self.config.max_complexity:
|
||||
continue
|
||||
self.add_message(
|
||||
"too-complex", node=node, confidence=HIGH, args=(node_name, complexity)
|
||||
)
|
||||
|
||||
|
||||
def register(linter):
|
||||
"""Required method to auto register this checker.
|
||||
|
||||
:param linter: Main interface object for Pylint plugins
|
||||
:type linter: Pylint object
|
||||
"""
|
||||
linter.register_checker(McCabeMethodChecker(linter))
|
||||
@@ -0,0 +1,86 @@
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""Looks for overlapping exceptions."""
|
||||
|
||||
import astroid
|
||||
|
||||
from pylint import checkers, interfaces
|
||||
from pylint.checkers import utils
|
||||
from pylint.checkers.exceptions import _annotated_unpack_infer
|
||||
|
||||
|
||||
class OverlappingExceptionsChecker(checkers.BaseChecker):
|
||||
"""Checks for two or more exceptions in the same exception handler
|
||||
clause that are identical or parts of the same inheritance hierarchy
|
||||
(i.e. overlapping)."""
|
||||
|
||||
__implements__ = interfaces.IAstroidChecker
|
||||
|
||||
name = "overlap-except"
|
||||
msgs = {
|
||||
"W0714": (
|
||||
"Overlapping exceptions (%s)",
|
||||
"overlapping-except",
|
||||
"Used when exceptions in handler overlap or are identical",
|
||||
)
|
||||
}
|
||||
priority = -2
|
||||
options = ()
|
||||
|
||||
@utils.check_messages("overlapping-except")
|
||||
def visit_tryexcept(self, node):
|
||||
"""check for empty except"""
|
||||
for handler in node.handlers:
|
||||
if handler.type is None:
|
||||
continue
|
||||
if isinstance(handler.type, astroid.BoolOp):
|
||||
continue
|
||||
try:
|
||||
excs = list(_annotated_unpack_infer(handler.type))
|
||||
except astroid.InferenceError:
|
||||
continue
|
||||
|
||||
handled_in_clause = []
|
||||
for part, exc in excs:
|
||||
if exc is astroid.Uninferable:
|
||||
continue
|
||||
if isinstance(exc, astroid.Instance) and utils.inherit_from_std_ex(exc):
|
||||
# pylint: disable=protected-access
|
||||
exc = exc._proxied
|
||||
|
||||
if not isinstance(exc, astroid.ClassDef):
|
||||
continue
|
||||
|
||||
exc_ancestors = [
|
||||
anc for anc in exc.ancestors() if isinstance(anc, astroid.ClassDef)
|
||||
]
|
||||
|
||||
for prev_part, prev_exc in handled_in_clause:
|
||||
prev_exc_ancestors = [
|
||||
anc
|
||||
for anc in prev_exc.ancestors()
|
||||
if isinstance(anc, astroid.ClassDef)
|
||||
]
|
||||
if exc == prev_exc:
|
||||
self.add_message(
|
||||
"overlapping-except",
|
||||
node=handler.type,
|
||||
args="%s and %s are the same"
|
||||
% (prev_part.as_string(), part.as_string()),
|
||||
)
|
||||
elif prev_exc in exc_ancestors or exc in prev_exc_ancestors:
|
||||
ancestor = part if exc in prev_exc_ancestors else prev_part
|
||||
descendant = part if prev_exc in exc_ancestors else prev_part
|
||||
self.add_message(
|
||||
"overlapping-except",
|
||||
node=handler.type,
|
||||
args="%s is an ancestor class of %s"
|
||||
% (ancestor.as_string(), descendant.as_string()),
|
||||
)
|
||||
handled_in_clause += [(part, exc)]
|
||||
|
||||
|
||||
def register(linter):
|
||||
"""Required method to auto register this checker."""
|
||||
linter.register_checker(OverlappingExceptionsChecker(linter))
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2016-2018 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2016 Glenn Matthews <glmatthe@cisco.com>
|
||||
# Copyright (c) 2018 Sushobhit <31987769+sushobhit27@users.noreply.github.com>
|
||||
# Copyright (c) 2018 Ville Skyttä <ville.skytta@iki.fi>
|
||||
# Copyright (c) 2019 Pierre Sassoulas <pierre.sassoulas@gmail.com>
|
||||
# Copyright (c) 2020 Anthony Sottile <asottile@umich.edu>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
import astroid
|
||||
|
||||
from pylint.checkers import BaseChecker
|
||||
from pylint.checkers.utils import check_messages, is_none, node_type
|
||||
from pylint.interfaces import IAstroidChecker
|
||||
|
||||
BUILTINS = "builtins"
|
||||
|
||||
|
||||
class MultipleTypesChecker(BaseChecker):
|
||||
"""Checks for variable type redefinitions (NoneType excepted)
|
||||
|
||||
At a function, method, class or module scope
|
||||
|
||||
This rule could be improved:
|
||||
|
||||
- Currently, if an attribute is set to different types in 2 methods of a
|
||||
same class, it won't be detected (see functional test)
|
||||
- One could improve the support for inference on assignment with tuples,
|
||||
ifexpr, etc. Also it would be great to have support for inference on
|
||||
str.split()
|
||||
"""
|
||||
|
||||
__implements__ = IAstroidChecker
|
||||
|
||||
name = "multiple_types"
|
||||
msgs = {
|
||||
"R0204": (
|
||||
"Redefinition of %s type from %s to %s",
|
||||
"redefined-variable-type",
|
||||
"Used when the type of a variable changes inside a "
|
||||
"method or a function.",
|
||||
)
|
||||
}
|
||||
|
||||
def visit_classdef(self, _):
|
||||
self._assigns.append({})
|
||||
|
||||
@check_messages("redefined-variable-type")
|
||||
def leave_classdef(self, _):
|
||||
self._check_and_add_messages()
|
||||
|
||||
visit_functiondef = visit_classdef
|
||||
leave_functiondef = leave_module = leave_classdef
|
||||
|
||||
def visit_module(self, _):
|
||||
self._assigns = [{}]
|
||||
|
||||
def _check_and_add_messages(self):
|
||||
assigns = self._assigns.pop()
|
||||
for name, args in assigns.items():
|
||||
if len(args) <= 1:
|
||||
continue
|
||||
orig_node, orig_type = args[0]
|
||||
# Check if there is a type in the following nodes that would be
|
||||
# different from orig_type.
|
||||
for redef_node, redef_type in args[1:]:
|
||||
if redef_type == orig_type:
|
||||
continue
|
||||
# if a variable is defined to several types in an if node,
|
||||
# this is not actually redefining.
|
||||
orig_parent = orig_node.parent
|
||||
redef_parent = redef_node.parent
|
||||
if isinstance(orig_parent, astroid.If):
|
||||
if orig_parent == redef_parent:
|
||||
if (
|
||||
redef_node in orig_parent.orelse
|
||||
and orig_node not in orig_parent.orelse
|
||||
):
|
||||
orig_node, orig_type = redef_node, redef_type
|
||||
continue
|
||||
elif isinstance(
|
||||
redef_parent, astroid.If
|
||||
) and redef_parent in orig_parent.nodes_of_class(astroid.If):
|
||||
orig_node, orig_type = redef_node, redef_type
|
||||
continue
|
||||
orig_type = orig_type.replace(BUILTINS + ".", "")
|
||||
redef_type = redef_type.replace(BUILTINS + ".", "")
|
||||
self.add_message(
|
||||
"redefined-variable-type",
|
||||
node=redef_node,
|
||||
args=(name, orig_type, redef_type),
|
||||
)
|
||||
break
|
||||
|
||||
def visit_assign(self, node):
|
||||
# we don't handle multiple assignment nor slice assignment
|
||||
target = node.targets[0]
|
||||
if isinstance(target, (astroid.Tuple, astroid.Subscript)):
|
||||
return
|
||||
# ignore NoneType
|
||||
if is_none(node):
|
||||
return
|
||||
_type = node_type(node.value)
|
||||
if _type:
|
||||
self._assigns[-1].setdefault(target.as_string(), []).append(
|
||||
(node, _type.pytype())
|
||||
)
|
||||
|
||||
|
||||
def register(linter):
|
||||
"""Required method to auto register this checker.
|
||||
|
||||
:param linter: Main interface object for Pylint plugins
|
||||
:type linter: Pylint object
|
||||
"""
|
||||
linter.register_checker(MultipleTypesChecker(linter))
|
||||
@@ -0,0 +1,193 @@
|
||||
# Copyright (c) 2015-2018 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2015 Florian Bruhin <me@the-compiler.org>
|
||||
# Copyright (c) 2016 Ashley Whetter <ashley@awhetter.co.uk>
|
||||
# Copyright (c) 2018 ssolanki <sushobhitsolanki@gmail.com>
|
||||
# Copyright (c) 2019 Nick Drozd <nicholasdrozd@gmail.com>
|
||||
# Copyright (c) 2019 Pierre Sassoulas <pierre.sassoulas@gmail.com>
|
||||
# Copyright (c) 2020 Benjamin Graham <benwilliamgraham@gmail.com>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""Graph manipulation utilities.
|
||||
|
||||
(dot generation adapted from pypy/translator/tool/make_dot.py)
|
||||
"""
|
||||
|
||||
import codecs
|
||||
import os
|
||||
import os.path as osp
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
|
||||
def target_info_from_filename(filename):
|
||||
"""Transforms /some/path/foo.png into ('/some/path', 'foo.png', 'png')."""
|
||||
basename = osp.basename(filename)
|
||||
storedir = osp.dirname(osp.abspath(filename))
|
||||
target = filename.split(".")[-1]
|
||||
return storedir, basename, target
|
||||
|
||||
|
||||
class DotBackend:
|
||||
"""Dot File backend."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
graphname,
|
||||
rankdir=None,
|
||||
size=None,
|
||||
ratio=None,
|
||||
charset="utf-8",
|
||||
renderer="dot",
|
||||
additional_param=None,
|
||||
):
|
||||
if additional_param is None:
|
||||
additional_param = {}
|
||||
self.graphname = graphname
|
||||
self.renderer = renderer
|
||||
self.lines = []
|
||||
self._source = None
|
||||
self.emit("digraph %s {" % normalize_node_id(graphname))
|
||||
if rankdir:
|
||||
self.emit("rankdir=%s" % rankdir)
|
||||
if ratio:
|
||||
self.emit("ratio=%s" % ratio)
|
||||
if size:
|
||||
self.emit('size="%s"' % size)
|
||||
if charset:
|
||||
assert charset.lower() in ("utf-8", "iso-8859-1", "latin1"), (
|
||||
"unsupported charset %s" % charset
|
||||
)
|
||||
self.emit('charset="%s"' % charset)
|
||||
for param in additional_param.items():
|
||||
self.emit("=".join(param))
|
||||
|
||||
def get_source(self):
|
||||
"""returns self._source"""
|
||||
if self._source is None:
|
||||
self.emit("}\n")
|
||||
self._source = "\n".join(self.lines)
|
||||
del self.lines
|
||||
return self._source
|
||||
|
||||
source = property(get_source)
|
||||
|
||||
def generate(self, outputfile=None, mapfile=None):
|
||||
"""Generates a graph file.
|
||||
|
||||
:param str outputfile: filename and path [defaults to graphname.png]
|
||||
:param str mapfile: filename and path
|
||||
|
||||
:rtype: str
|
||||
:return: a path to the generated file
|
||||
"""
|
||||
name = self.graphname
|
||||
if outputfile is not None:
|
||||
_, _, target = target_info_from_filename(outputfile)
|
||||
if target != "dot":
|
||||
pdot, dot_sourcepath = tempfile.mkstemp(".dot", name)
|
||||
os.close(pdot)
|
||||
else:
|
||||
dot_sourcepath = outputfile
|
||||
else:
|
||||
target = "png"
|
||||
pdot, dot_sourcepath = tempfile.mkstemp(".dot", name)
|
||||
ppng, outputfile = tempfile.mkstemp(".png", name)
|
||||
os.close(pdot)
|
||||
os.close(ppng)
|
||||
pdot = codecs.open(dot_sourcepath, "w", encoding="utf8")
|
||||
pdot.write(self.source)
|
||||
pdot.close()
|
||||
if target != "dot":
|
||||
use_shell = sys.platform == "win32"
|
||||
if mapfile:
|
||||
subprocess.call(
|
||||
[
|
||||
self.renderer,
|
||||
"-Tcmapx",
|
||||
"-o",
|
||||
mapfile,
|
||||
"-T",
|
||||
target,
|
||||
dot_sourcepath,
|
||||
"-o",
|
||||
outputfile,
|
||||
],
|
||||
shell=use_shell,
|
||||
)
|
||||
else:
|
||||
subprocess.call(
|
||||
[self.renderer, "-T", target, dot_sourcepath, "-o", outputfile],
|
||||
shell=use_shell,
|
||||
)
|
||||
os.unlink(dot_sourcepath)
|
||||
return outputfile
|
||||
|
||||
def emit(self, line):
|
||||
"""Adds <line> to final output."""
|
||||
self.lines.append(line)
|
||||
|
||||
def emit_edge(self, name1, name2, **props):
|
||||
"""emit an edge from <name1> to <name2>.
|
||||
edge properties: see http://www.graphviz.org/doc/info/attrs.html
|
||||
"""
|
||||
attrs = ['%s="%s"' % (prop, value) for prop, value in props.items()]
|
||||
n_from, n_to = normalize_node_id(name1), normalize_node_id(name2)
|
||||
self.emit("%s -> %s [%s];" % (n_from, n_to, ", ".join(sorted(attrs))))
|
||||
|
||||
def emit_node(self, name, **props):
|
||||
"""emit a node with given properties.
|
||||
node properties: see http://www.graphviz.org/doc/info/attrs.html
|
||||
"""
|
||||
attrs = ['%s="%s"' % (prop, value) for prop, value in props.items()]
|
||||
self.emit("%s [%s];" % (normalize_node_id(name), ", ".join(sorted(attrs))))
|
||||
|
||||
|
||||
def normalize_node_id(nid):
|
||||
"""Returns a suitable DOT node id for `nid`."""
|
||||
return '"%s"' % nid
|
||||
|
||||
|
||||
def get_cycles(graph_dict, vertices=None):
|
||||
"""given a dictionary representing an ordered graph (i.e. key are vertices
|
||||
and values is a list of destination vertices representing edges), return a
|
||||
list of detected cycles
|
||||
"""
|
||||
if not graph_dict:
|
||||
return ()
|
||||
result = []
|
||||
if vertices is None:
|
||||
vertices = graph_dict.keys()
|
||||
for vertice in vertices:
|
||||
_get_cycles(graph_dict, [], set(), result, vertice)
|
||||
return result
|
||||
|
||||
|
||||
def _get_cycles(graph_dict, path, visited, result, vertice):
|
||||
"""recursive function doing the real work for get_cycles"""
|
||||
if vertice in path:
|
||||
cycle = [vertice]
|
||||
for node in path[::-1]:
|
||||
if node == vertice:
|
||||
break
|
||||
cycle.insert(0, node)
|
||||
# make a canonical representation
|
||||
start_from = min(cycle)
|
||||
index = cycle.index(start_from)
|
||||
cycle = cycle[index:] + cycle[0:index]
|
||||
# append it to result if not already in
|
||||
if cycle not in result:
|
||||
result.append(cycle)
|
||||
return
|
||||
path.append(vertice)
|
||||
try:
|
||||
for node in graph_dict[vertice]:
|
||||
# don't check already visited nodes again
|
||||
if node not in visited:
|
||||
_get_cycles(graph_dict, path, visited, result, node)
|
||||
visited.add(node)
|
||||
except KeyError:
|
||||
pass
|
||||
path.pop()
|
||||
@@ -0,0 +1,103 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2009-2010, 2012-2013 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
|
||||
# Copyright (c) 2013-2014 Google, Inc.
|
||||
# Copyright (c) 2014 Michal Nowikowski <godfryd@gmail.com>
|
||||
# Copyright (c) 2014 Arun Persaud <arun@nubati.net>
|
||||
# Copyright (c) 2015-2018 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2015 Florian Bruhin <me@the-compiler.org>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
|
||||
# Copyright (c) 2018 ssolanki <sushobhitsolanki@gmail.com>
|
||||
# Copyright (c) 2018 Ville Skyttä <ville.skytta@iki.fi>
|
||||
# Copyright (c) 2020 Anthony Sottile <asottile@umich.edu>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""Interfaces for Pylint objects"""
|
||||
from collections import namedtuple
|
||||
|
||||
Confidence = namedtuple("Confidence", ["name", "description"])
|
||||
# Warning Certainties
|
||||
HIGH = Confidence("HIGH", "No false positive possible.")
|
||||
INFERENCE = Confidence("INFERENCE", "Warning based on inference result.")
|
||||
INFERENCE_FAILURE = Confidence(
|
||||
"INFERENCE_FAILURE", "Warning based on inference with failures."
|
||||
)
|
||||
UNDEFINED = Confidence("UNDEFINED", "Warning without any associated confidence level.")
|
||||
|
||||
CONFIDENCE_LEVELS = [HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED]
|
||||
|
||||
|
||||
class Interface:
|
||||
"""Base class for interfaces."""
|
||||
|
||||
@classmethod
|
||||
def is_implemented_by(cls, instance):
|
||||
return implements(instance, cls)
|
||||
|
||||
|
||||
def implements(obj, interface):
|
||||
"""Return true if the give object (maybe an instance or class) implements
|
||||
the interface.
|
||||
"""
|
||||
kimplements = getattr(obj, "__implements__", ())
|
||||
if not isinstance(kimplements, (list, tuple)):
|
||||
kimplements = (kimplements,)
|
||||
for implementedinterface in kimplements:
|
||||
if issubclass(implementedinterface, interface):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class IChecker(Interface):
|
||||
"""This is a base interface, not designed to be used elsewhere than for
|
||||
sub interfaces definition.
|
||||
"""
|
||||
|
||||
def open(self):
|
||||
"""called before visiting project (i.e set of modules)"""
|
||||
|
||||
def close(self):
|
||||
"""called after visiting project (i.e set of modules)"""
|
||||
|
||||
|
||||
class IRawChecker(IChecker):
|
||||
"""interface for checker which need to parse the raw file
|
||||
"""
|
||||
|
||||
def process_module(self, astroid):
|
||||
""" process a module
|
||||
|
||||
the module's content is accessible via astroid.stream
|
||||
"""
|
||||
|
||||
|
||||
class ITokenChecker(IChecker):
|
||||
"""Interface for checkers that need access to the token list."""
|
||||
|
||||
def process_tokens(self, tokens):
|
||||
"""Process a module.
|
||||
|
||||
tokens is a list of all source code tokens in the file.
|
||||
"""
|
||||
|
||||
|
||||
class IAstroidChecker(IChecker):
|
||||
""" interface for checker which prefers receive events according to
|
||||
statement type
|
||||
"""
|
||||
|
||||
|
||||
class IReporter(Interface):
|
||||
""" reporter collect messages and display results encapsulated in a layout
|
||||
"""
|
||||
|
||||
def handle_message(self, msg):
|
||||
"""Handle the given message object."""
|
||||
|
||||
def display_reports(self, layout):
|
||||
"""display results encapsulated in the layout tree
|
||||
"""
|
||||
|
||||
|
||||
__all__ = ("IRawChecker", "IAstroidChecker", "ITokenChecker", "IReporter")
|
||||
@@ -0,0 +1,91 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2006-2015 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
|
||||
# Copyright (c) 2008 Fabrice Douchant <Fabrice.Douchant@logilab.fr>
|
||||
# Copyright (c) 2009 Vincent
|
||||
# Copyright (c) 2009 Mads Kiilerich <mads@kiilerich.com>
|
||||
# Copyright (c) 2011-2014 Google, Inc.
|
||||
# Copyright (c) 2012 David Pursehouse <david.pursehouse@sonymobile.com>
|
||||
# Copyright (c) 2012 Kevin Jing Qiu <kevin.jing.qiu@gmail.com>
|
||||
# Copyright (c) 2012 FELD Boris <lothiraldan@gmail.com>
|
||||
# Copyright (c) 2012 JT Olds <jtolds@xnet5.com>
|
||||
# Copyright (c) 2014-2020 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2014-2015 Michal Nowikowski <godfryd@gmail.com>
|
||||
# Copyright (c) 2014 Brett Cannon <brett@python.org>
|
||||
# Copyright (c) 2014 Alexandru Coman <fcoman@bitdefender.com>
|
||||
# Copyright (c) 2014 Daniel Harding <dharding@living180.net>
|
||||
# Copyright (c) 2014 Arun Persaud <arun@nubati.net>
|
||||
# Copyright (c) 2014 Dan Goldsmith <djgoldsmith@googlemail.com>
|
||||
# Copyright (c) 2015-2016 Florian Bruhin <me@the-compiler.org>
|
||||
# Copyright (c) 2015 Aru Sahni <arusahni@gmail.com>
|
||||
# Copyright (c) 2015 Steven Myint <hg@stevenmyint.com>
|
||||
# Copyright (c) 2015 Simu Toni <simutoni@gmail.com>
|
||||
# Copyright (c) 2015 Mihai Balint <balint.mihai@gmail.com>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
|
||||
# Copyright (c) 2016-2017 Łukasz Rogalski <rogalski.91@gmail.com>
|
||||
# Copyright (c) 2016 Glenn Matthews <glenn@e-dad.net>
|
||||
# Copyright (c) 2016 Alan Evangelista <alanoe@linux.vnet.ibm.com>
|
||||
# Copyright (c) 2017-2019 hippo91 <guillaume.peillex@gmail.com>
|
||||
# Copyright (c) 2017-2018 Ville Skyttä <ville.skytta@iki.fi>
|
||||
# Copyright (c) 2017 Daniel Miller <millerdev@gmail.com>
|
||||
# Copyright (c) 2017 Roman Ivanov <me@roivanov.com>
|
||||
# Copyright (c) 2017 Ned Batchelder <ned@nedbatchelder.com>
|
||||
# Copyright (c) 2018-2020 Pierre Sassoulas <pierre.sassoulas@gmail.com>
|
||||
# Copyright (c) 2018, 2020 Anthony Sottile <asottile@umich.edu>
|
||||
# Copyright (c) 2018-2019 Nick Drozd <nicholasdrozd@gmail.com>
|
||||
# Copyright (c) 2018 Matus Valo <matusvalo@users.noreply.github.com>
|
||||
# Copyright (c) 2018 Lucas Cimon <lucas.cimon@gmail.com>
|
||||
# Copyright (c) 2018 ssolanki <sushobhitsolanki@gmail.com>
|
||||
# Copyright (c) 2018 Randall Leeds <randall@bleeds.info>
|
||||
# Copyright (c) 2018 Mike Frysinger <vapier@gmail.com>
|
||||
# Copyright (c) 2018 Sushobhit <31987769+sushobhit27@users.noreply.github.com>
|
||||
# Copyright (c) 2018 Jason Owen <jason.a.owen@gmail.com>
|
||||
# Copyright (c) 2018 Gary Tyler McLeod <mail@garytyler.com>
|
||||
# Copyright (c) 2018 Yuval Langer <yuvallanger@mail.tau.ac.il>
|
||||
# Copyright (c) 2018 kapsh <kapsh@kap.sh>
|
||||
# Copyright (c) 2019 syutbai <syutbai@gmail.com>
|
||||
# Copyright (c) 2019 Thomas Hisch <t.hisch@gmail.com>
|
||||
# Copyright (c) 2019 Hugues <hugues.bruant@affirm.com>
|
||||
# Copyright (c) 2019 Janne Rönkkö <jannero@users.noreply.github.com>
|
||||
# Copyright (c) 2019 Ashley Whetter <ashley@awhetter.co.uk>
|
||||
# Copyright (c) 2019 Trevor Bekolay <tbekolay@gmail.com>
|
||||
# Copyright (c) 2019 Hugo van Kemenade <hugovk@users.noreply.github.com>
|
||||
# Copyright (c) 2019 Robert Schweizer <robert_schweizer@gmx.de>
|
||||
# Copyright (c) 2019 Andres Perez Hortal <andresperezcba@gmail.com>
|
||||
# Copyright (c) 2019 Peter Kolbus <peter.kolbus@gmail.com>
|
||||
# Copyright (c) 2019 Nicolas Dickreuter <dickreuter@gmail.com>
|
||||
# Copyright (c) 2020 anubh-v <anubhav@u.nus.edu>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
""" pylint [options] modules_or_packages
|
||||
|
||||
Check that module(s) satisfy a coding standard (and more !).
|
||||
|
||||
pylint --help
|
||||
|
||||
Display this help message and exit.
|
||||
|
||||
pylint --help-msg <msg-id>[,<msg-id>]
|
||||
|
||||
Display help messages about given message identifiers and exit.
|
||||
"""
|
||||
import sys
|
||||
|
||||
from pylint.lint.check_parallel import check_parallel
|
||||
from pylint.lint.pylinter import PyLinter
|
||||
from pylint.lint.report_functions import (
|
||||
report_messages_by_module_stats,
|
||||
report_messages_stats,
|
||||
report_total_messages_stats,
|
||||
)
|
||||
from pylint.lint.run import Run
|
||||
from pylint.lint.utils import (
|
||||
ArgumentPreprocessingError,
|
||||
_patch_sys_path,
|
||||
fix_import_path,
|
||||
preprocess_options,
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
Run(sys.argv[1:])
|
||||
@@ -0,0 +1,118 @@
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
import collections
|
||||
import functools
|
||||
|
||||
from pylint import reporters
|
||||
from pylint.lint.utils import _patch_sys_path
|
||||
from pylint.message import Message
|
||||
|
||||
try:
|
||||
import multiprocessing
|
||||
except ImportError:
|
||||
multiprocessing = None # type: ignore
|
||||
|
||||
# PyLinter object used by worker processes when checking files using multiprocessing
|
||||
# should only be used by the worker processes
|
||||
_worker_linter = None
|
||||
|
||||
|
||||
def _get_new_args(message):
|
||||
location = (
|
||||
message.abspath,
|
||||
message.path,
|
||||
message.module,
|
||||
message.obj,
|
||||
message.line,
|
||||
message.column,
|
||||
)
|
||||
return (message.msg_id, message.symbol, location, message.msg, message.confidence)
|
||||
|
||||
|
||||
def _merge_stats(stats):
|
||||
merged = {}
|
||||
by_msg = collections.Counter()
|
||||
for stat in stats:
|
||||
message_stats = stat.pop("by_msg", {})
|
||||
by_msg.update(message_stats)
|
||||
|
||||
for key, item in stat.items():
|
||||
if key not in merged:
|
||||
merged[key] = item
|
||||
elif isinstance(item, dict):
|
||||
merged[key].update(item)
|
||||
else:
|
||||
merged[key] = merged[key] + item
|
||||
|
||||
merged["by_msg"] = by_msg
|
||||
return merged
|
||||
|
||||
|
||||
def _worker_initialize(linter, arguments=None):
|
||||
global _worker_linter # pylint: disable=global-statement
|
||||
_worker_linter = linter
|
||||
|
||||
# On the worker process side the messages are just collected and passed back to
|
||||
# parent process as _worker_check_file function's return value
|
||||
_worker_linter.set_reporter(reporters.CollectingReporter())
|
||||
_worker_linter.open()
|
||||
|
||||
# Patch sys.path so that each argument is importable just like in single job mode
|
||||
_patch_sys_path(arguments or ())
|
||||
|
||||
|
||||
def _worker_check_single_file(file_item):
|
||||
name, filepath, modname = file_item
|
||||
|
||||
_worker_linter.open()
|
||||
_worker_linter.check_single_file(name, filepath, modname)
|
||||
|
||||
msgs = [_get_new_args(m) for m in _worker_linter.reporter.messages]
|
||||
return (
|
||||
_worker_linter.current_name,
|
||||
_worker_linter.file_state.base_name,
|
||||
msgs,
|
||||
_worker_linter.stats,
|
||||
_worker_linter.msg_status,
|
||||
)
|
||||
|
||||
|
||||
def check_parallel(linter, jobs, files, arguments=None):
|
||||
"""Use the given linter to lint the files with given amount of workers (jobs)"""
|
||||
# The reporter does not need to be passed to worker processess, i.e. the reporter does
|
||||
# not need to be pickleable
|
||||
original_reporter = linter.reporter
|
||||
linter.reporter = None
|
||||
|
||||
# The linter is inherited by all the pool's workers, i.e. the linter
|
||||
# is identical to the linter object here. This is requred so that
|
||||
# a custom PyLinter object can be used.
|
||||
initializer = functools.partial(_worker_initialize, arguments=arguments)
|
||||
with multiprocessing.Pool(jobs, initializer=initializer, initargs=[linter]) as pool:
|
||||
# ..and now when the workers have inherited the linter, the actual reporter
|
||||
# can be set back here on the parent process so that results get stored into
|
||||
# correct reporter
|
||||
linter.set_reporter(original_reporter)
|
||||
linter.open()
|
||||
|
||||
all_stats = []
|
||||
|
||||
for module, base_name, messages, stats, msg_status in pool.imap_unordered(
|
||||
_worker_check_single_file, files
|
||||
):
|
||||
linter.file_state.base_name = base_name
|
||||
linter.set_current_module(module)
|
||||
for msg in messages:
|
||||
msg = Message(*msg)
|
||||
linter.reporter.handle_message(msg)
|
||||
|
||||
all_stats.append(stats)
|
||||
linter.msg_status |= msg_status
|
||||
|
||||
linter.stats = _merge_stats(all_stats)
|
||||
|
||||
# Insert stats data to local checkers.
|
||||
for checker in linter.get_checkers():
|
||||
if checker is not linter:
|
||||
checker.stats = linter.stats
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,76 @@
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
import collections
|
||||
|
||||
from pylint import checkers, exceptions
|
||||
from pylint.reporters.ureports import nodes as report_nodes
|
||||
|
||||
|
||||
def report_total_messages_stats(sect, stats, previous_stats):
|
||||
"""make total errors / warnings report"""
|
||||
lines = ["type", "number", "previous", "difference"]
|
||||
lines += checkers.table_lines_from_stats(
|
||||
stats, previous_stats, ("convention", "refactor", "warning", "error")
|
||||
)
|
||||
sect.append(report_nodes.Table(children=lines, cols=4, rheaders=1))
|
||||
|
||||
|
||||
def report_messages_stats(sect, stats, _):
|
||||
"""make messages type report"""
|
||||
if not stats["by_msg"]:
|
||||
# don't print this report when we didn't detected any errors
|
||||
raise exceptions.EmptyReportError()
|
||||
in_order = sorted(
|
||||
[
|
||||
(value, msg_id)
|
||||
for msg_id, value in stats["by_msg"].items()
|
||||
if not msg_id.startswith("I")
|
||||
]
|
||||
)
|
||||
in_order.reverse()
|
||||
lines = ("message id", "occurrences")
|
||||
for value, msg_id in in_order:
|
||||
lines += (msg_id, str(value))
|
||||
sect.append(report_nodes.Table(children=lines, cols=2, rheaders=1))
|
||||
|
||||
|
||||
def report_messages_by_module_stats(sect, stats, _):
|
||||
"""make errors / warnings by modules report"""
|
||||
if len(stats["by_module"]) == 1:
|
||||
# don't print this report when we are analysing a single module
|
||||
raise exceptions.EmptyReportError()
|
||||
by_mod = collections.defaultdict(dict)
|
||||
for m_type in ("fatal", "error", "warning", "refactor", "convention"):
|
||||
total = stats[m_type]
|
||||
for module in stats["by_module"].keys():
|
||||
mod_total = stats["by_module"][module][m_type]
|
||||
if total == 0:
|
||||
percent = 0
|
||||
else:
|
||||
percent = float((mod_total) * 100) / total
|
||||
by_mod[module][m_type] = percent
|
||||
sorted_result = []
|
||||
for module, mod_info in by_mod.items():
|
||||
sorted_result.append(
|
||||
(
|
||||
mod_info["error"],
|
||||
mod_info["warning"],
|
||||
mod_info["refactor"],
|
||||
mod_info["convention"],
|
||||
module,
|
||||
)
|
||||
)
|
||||
sorted_result.sort()
|
||||
sorted_result.reverse()
|
||||
lines = ["module", "error", "warning", "refactor", "convention"]
|
||||
for line in sorted_result:
|
||||
# Don't report clean modules.
|
||||
if all(entry == 0 for entry in line[:-1]):
|
||||
continue
|
||||
lines.append(line[-1])
|
||||
for val in line[:-1]:
|
||||
lines.append("%.2f" % val)
|
||||
if len(lines) == 5:
|
||||
raise exceptions.EmptyReportError()
|
||||
sect.append(report_nodes.Table(children=lines, cols=5, rheaders=1))
|
||||
@@ -0,0 +1,425 @@
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
import os
|
||||
import sys
|
||||
import warnings
|
||||
|
||||
from pylint import __pkginfo__, config, extensions, interfaces
|
||||
from pylint.lint.pylinter import PyLinter
|
||||
from pylint.lint.utils import ArgumentPreprocessingError, preprocess_options
|
||||
from pylint.utils import utils
|
||||
|
||||
try:
|
||||
import multiprocessing
|
||||
except ImportError:
|
||||
multiprocessing = None # type: ignore
|
||||
|
||||
|
||||
def _cpu_count() -> int:
|
||||
"""Use sched_affinity if available for virtualized or containerized environments."""
|
||||
sched_getaffinity = getattr(os, "sched_getaffinity", None)
|
||||
# pylint: disable=not-callable,using-constant-test
|
||||
if sched_getaffinity:
|
||||
return len(sched_getaffinity(0))
|
||||
if multiprocessing:
|
||||
return multiprocessing.cpu_count()
|
||||
return 1
|
||||
|
||||
|
||||
def cb_list_extensions(option, optname, value, parser):
|
||||
"""List all the extensions under pylint.extensions"""
|
||||
|
||||
for filename in os.listdir(os.path.dirname(extensions.__file__)):
|
||||
if filename.endswith(".py") and not filename.startswith("_"):
|
||||
extension_name, _, _ = filename.partition(".")
|
||||
print("pylint.extensions.{}".format(extension_name))
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def cb_list_confidence_levels(option, optname, value, parser):
|
||||
for level in interfaces.CONFIDENCE_LEVELS:
|
||||
print("%-18s: %s" % level)
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def cb_init_hook(optname, value):
|
||||
"""exec arbitrary code to set sys.path for instance"""
|
||||
exec(value) # pylint: disable=exec-used
|
||||
|
||||
|
||||
UNUSED_PARAM_SENTINEL = object()
|
||||
|
||||
|
||||
class Run:
|
||||
"""helper class to use as main for pylint :
|
||||
|
||||
run(*sys.argv[1:])
|
||||
"""
|
||||
|
||||
LinterClass = PyLinter
|
||||
option_groups = (
|
||||
(
|
||||
"Commands",
|
||||
"Options which are actually commands. Options in this \
|
||||
group are mutually exclusive.",
|
||||
),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _return_one(*args): # pylint: disable=unused-argument
|
||||
return 1
|
||||
|
||||
def __init__(
|
||||
self, args, reporter=None, exit=True, do_exit=UNUSED_PARAM_SENTINEL,
|
||||
): # pylint: disable=redefined-builtin
|
||||
self._rcfile = None
|
||||
self._plugins = []
|
||||
self.verbose = None
|
||||
try:
|
||||
preprocess_options(
|
||||
args,
|
||||
{
|
||||
# option: (callback, takearg)
|
||||
"init-hook": (cb_init_hook, True),
|
||||
"rcfile": (self.cb_set_rcfile, True),
|
||||
"load-plugins": (self.cb_add_plugins, True),
|
||||
"verbose": (self.cb_verbose_mode, False),
|
||||
},
|
||||
)
|
||||
except ArgumentPreprocessingError as ex:
|
||||
print(ex, file=sys.stderr)
|
||||
sys.exit(32)
|
||||
|
||||
self.linter = linter = self.LinterClass(
|
||||
(
|
||||
(
|
||||
"rcfile",
|
||||
{
|
||||
"action": "callback",
|
||||
"callback": Run._return_one,
|
||||
"group": "Commands",
|
||||
"type": "string",
|
||||
"metavar": "<file>",
|
||||
"help": "Specify a configuration file to load.",
|
||||
},
|
||||
),
|
||||
(
|
||||
"init-hook",
|
||||
{
|
||||
"action": "callback",
|
||||
"callback": Run._return_one,
|
||||
"type": "string",
|
||||
"metavar": "<code>",
|
||||
"level": 1,
|
||||
"help": "Python code to execute, usually for sys.path "
|
||||
"manipulation such as pygtk.require().",
|
||||
},
|
||||
),
|
||||
(
|
||||
"help-msg",
|
||||
{
|
||||
"action": "callback",
|
||||
"type": "string",
|
||||
"metavar": "<msg-id>",
|
||||
"callback": self.cb_help_message,
|
||||
"group": "Commands",
|
||||
"help": "Display a help message for the given message id and "
|
||||
"exit. The value may be a comma separated list of message ids.",
|
||||
},
|
||||
),
|
||||
(
|
||||
"list-msgs",
|
||||
{
|
||||
"action": "callback",
|
||||
"metavar": "<msg-id>",
|
||||
"callback": self.cb_list_messages,
|
||||
"group": "Commands",
|
||||
"level": 1,
|
||||
"help": "Generate pylint's messages.",
|
||||
},
|
||||
),
|
||||
(
|
||||
"list-msgs-enabled",
|
||||
{
|
||||
"action": "callback",
|
||||
"metavar": "<msg-id>",
|
||||
"callback": self.cb_list_messages_enabled,
|
||||
"group": "Commands",
|
||||
"level": 1,
|
||||
"help": "Display a list of what messages are enabled "
|
||||
"and disabled with the given configuration.",
|
||||
},
|
||||
),
|
||||
(
|
||||
"list-groups",
|
||||
{
|
||||
"action": "callback",
|
||||
"metavar": "<msg-id>",
|
||||
"callback": self.cb_list_groups,
|
||||
"group": "Commands",
|
||||
"level": 1,
|
||||
"help": "List pylint's message groups.",
|
||||
},
|
||||
),
|
||||
(
|
||||
"list-conf-levels",
|
||||
{
|
||||
"action": "callback",
|
||||
"callback": cb_list_confidence_levels,
|
||||
"group": "Commands",
|
||||
"level": 1,
|
||||
"help": "Generate pylint's confidence levels.",
|
||||
},
|
||||
),
|
||||
(
|
||||
"list-extensions",
|
||||
{
|
||||
"action": "callback",
|
||||
"callback": cb_list_extensions,
|
||||
"group": "Commands",
|
||||
"level": 1,
|
||||
"help": "List available extensions.",
|
||||
},
|
||||
),
|
||||
(
|
||||
"full-documentation",
|
||||
{
|
||||
"action": "callback",
|
||||
"metavar": "<msg-id>",
|
||||
"callback": self.cb_full_documentation,
|
||||
"group": "Commands",
|
||||
"level": 1,
|
||||
"help": "Generate pylint's full documentation.",
|
||||
},
|
||||
),
|
||||
(
|
||||
"generate-rcfile",
|
||||
{
|
||||
"action": "callback",
|
||||
"callback": self.cb_generate_config,
|
||||
"group": "Commands",
|
||||
"help": "Generate a sample configuration file according to "
|
||||
"the current configuration. You can put other options "
|
||||
"before this one to get them in the generated "
|
||||
"configuration.",
|
||||
},
|
||||
),
|
||||
(
|
||||
"generate-man",
|
||||
{
|
||||
"action": "callback",
|
||||
"callback": self.cb_generate_manpage,
|
||||
"group": "Commands",
|
||||
"help": "Generate pylint's man page.",
|
||||
"hide": True,
|
||||
},
|
||||
),
|
||||
(
|
||||
"errors-only",
|
||||
{
|
||||
"action": "callback",
|
||||
"callback": self.cb_error_mode,
|
||||
"short": "E",
|
||||
"help": "In error mode, checkers without error messages are "
|
||||
"disabled and for others, only the ERROR messages are "
|
||||
"displayed, and no reports are done by default.",
|
||||
},
|
||||
),
|
||||
(
|
||||
"py3k",
|
||||
{
|
||||
"action": "callback",
|
||||
"callback": self.cb_python3_porting_mode,
|
||||
"help": "In Python 3 porting mode, all checkers will be "
|
||||
"disabled and only messages emitted by the porting "
|
||||
"checker will be displayed.",
|
||||
},
|
||||
),
|
||||
(
|
||||
"verbose",
|
||||
{
|
||||
"action": "callback",
|
||||
"callback": self.cb_verbose_mode,
|
||||
"short": "v",
|
||||
"help": "In verbose mode, extra non-checker-related info "
|
||||
"will be displayed.",
|
||||
},
|
||||
),
|
||||
),
|
||||
option_groups=self.option_groups,
|
||||
pylintrc=self._rcfile,
|
||||
)
|
||||
# register standard checkers
|
||||
linter.load_default_plugins()
|
||||
# load command line plugins
|
||||
linter.load_plugin_modules(self._plugins)
|
||||
# add some help section
|
||||
linter.add_help_section("Environment variables", config.ENV_HELP, level=1)
|
||||
# pylint: disable=bad-continuation
|
||||
linter.add_help_section(
|
||||
"Output",
|
||||
"Using the default text output, the message format is : \n"
|
||||
" \n"
|
||||
" MESSAGE_TYPE: LINE_NUM:[OBJECT:] MESSAGE \n"
|
||||
" \n"
|
||||
"There are 5 kind of message types : \n"
|
||||
" * (C) convention, for programming standard violation \n"
|
||||
" * (R) refactor, for bad code smell \n"
|
||||
" * (W) warning, for python specific problems \n"
|
||||
" * (E) error, for probable bugs in the code \n"
|
||||
" * (F) fatal, if an error occurred which prevented pylint from doing further\n"
|
||||
"processing.\n",
|
||||
level=1,
|
||||
)
|
||||
linter.add_help_section(
|
||||
"Output status code",
|
||||
"Pylint should leave with following status code: \n"
|
||||
" * 0 if everything went fine \n"
|
||||
" * 1 if a fatal message was issued \n"
|
||||
" * 2 if an error message was issued \n"
|
||||
" * 4 if a warning message was issued \n"
|
||||
" * 8 if a refactor message was issued \n"
|
||||
" * 16 if a convention message was issued \n"
|
||||
" * 32 on usage error \n"
|
||||
" \n"
|
||||
"status 1 to 16 will be bit-ORed so you can know which different categories has\n"
|
||||
"been issued by analysing pylint output status code\n",
|
||||
level=1,
|
||||
)
|
||||
# read configuration
|
||||
linter.disable("I")
|
||||
linter.enable("c-extension-no-member")
|
||||
linter.read_config_file(verbose=self.verbose)
|
||||
config_parser = linter.cfgfile_parser
|
||||
# run init hook, if present, before loading plugins
|
||||
if config_parser.has_option("MASTER", "init-hook"):
|
||||
cb_init_hook(
|
||||
"init-hook", utils._unquote(config_parser.get("MASTER", "init-hook"))
|
||||
)
|
||||
# is there some additional plugins in the file configuration, in
|
||||
if config_parser.has_option("MASTER", "load-plugins"):
|
||||
plugins = utils._splitstrip(config_parser.get("MASTER", "load-plugins"))
|
||||
linter.load_plugin_modules(plugins)
|
||||
# now we can load file config and command line, plugins (which can
|
||||
# provide options) have been registered
|
||||
linter.load_config_file()
|
||||
|
||||
if reporter:
|
||||
# if a custom reporter is provided as argument, it may be overridden
|
||||
# by file parameters, so re-set it here, but before command line
|
||||
# parsing so it's still overrideable by command line option
|
||||
linter.set_reporter(reporter)
|
||||
try:
|
||||
args = linter.load_command_line_configuration(args)
|
||||
except SystemExit as exc:
|
||||
if exc.code == 2: # bad options
|
||||
exc.code = 32
|
||||
raise
|
||||
if not args:
|
||||
print(linter.help())
|
||||
sys.exit(32)
|
||||
|
||||
if linter.config.jobs < 0:
|
||||
print(
|
||||
"Jobs number (%d) should be greater than or equal to 0"
|
||||
% linter.config.jobs,
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(32)
|
||||
if linter.config.jobs > 1 or linter.config.jobs == 0:
|
||||
if multiprocessing is None:
|
||||
print(
|
||||
"Multiprocessing library is missing, " "fallback to single process",
|
||||
file=sys.stderr,
|
||||
)
|
||||
linter.set_option("jobs", 1)
|
||||
elif linter.config.jobs == 0:
|
||||
linter.config.jobs = _cpu_count()
|
||||
|
||||
# We have loaded configuration from config file and command line. Now, we can
|
||||
# load plugin specific configuration.
|
||||
linter.load_plugin_configuration()
|
||||
|
||||
linter.check(args)
|
||||
score_value = linter.generate_reports()
|
||||
|
||||
if do_exit is not UNUSED_PARAM_SENTINEL:
|
||||
warnings.warn(
|
||||
"do_exit is deprecated and it is going to be removed in a future version.",
|
||||
DeprecationWarning,
|
||||
)
|
||||
exit = do_exit
|
||||
|
||||
if exit:
|
||||
if linter.config.exit_zero:
|
||||
sys.exit(0)
|
||||
else:
|
||||
if score_value and score_value > linter.config.fail_under:
|
||||
sys.exit(0)
|
||||
sys.exit(self.linter.msg_status)
|
||||
|
||||
def cb_set_rcfile(self, name, value):
|
||||
"""callback for option preprocessing (i.e. before option parsing)"""
|
||||
self._rcfile = value
|
||||
|
||||
def cb_add_plugins(self, name, value):
|
||||
"""callback for option preprocessing (i.e. before option parsing)"""
|
||||
self._plugins.extend(utils._splitstrip(value))
|
||||
|
||||
def cb_error_mode(self, *args, **kwargs):
|
||||
"""error mode:
|
||||
* disable all but error messages
|
||||
* disable the 'miscellaneous' checker which can be safely deactivated in
|
||||
debug
|
||||
* disable reports
|
||||
* do not save execution information
|
||||
"""
|
||||
self.linter.error_mode()
|
||||
|
||||
def cb_generate_config(self, *args, **kwargs):
|
||||
"""optik callback for sample config file generation"""
|
||||
self.linter.generate_config(skipsections=("COMMANDS",))
|
||||
sys.exit(0)
|
||||
|
||||
def cb_generate_manpage(self, *args, **kwargs):
|
||||
"""optik callback for sample config file generation"""
|
||||
self.linter.generate_manpage(__pkginfo__)
|
||||
sys.exit(0)
|
||||
|
||||
def cb_help_message(self, option, optname, value, parser):
|
||||
"""optik callback for printing some help about a particular message"""
|
||||
self.linter.msgs_store.help_message(utils._splitstrip(value))
|
||||
sys.exit(0)
|
||||
|
||||
def cb_full_documentation(self, option, optname, value, parser):
|
||||
"""optik callback for printing full documentation"""
|
||||
self.linter.print_full_documentation()
|
||||
sys.exit(0)
|
||||
|
||||
def cb_list_messages(self, option, optname, value, parser):
|
||||
"""optik callback for printing available messages"""
|
||||
self.linter.msgs_store.list_messages()
|
||||
sys.exit(0)
|
||||
|
||||
def cb_list_messages_enabled(self, option, optname, value, parser):
|
||||
"""optik callback for printing available messages"""
|
||||
self.linter.list_messages_enabled()
|
||||
sys.exit(0)
|
||||
|
||||
def cb_list_groups(self, *args, **kwargs):
|
||||
"""List all the check groups that pylint knows about
|
||||
|
||||
These should be useful to know what check groups someone can disable
|
||||
or enable.
|
||||
"""
|
||||
for check in self.linter.get_checker_names():
|
||||
print(check)
|
||||
sys.exit(0)
|
||||
|
||||
def cb_python3_porting_mode(self, *args, **kwargs):
|
||||
"""Activate only the python3 porting checker."""
|
||||
self.linter.python3_porting_mode()
|
||||
|
||||
def cb_verbose_mode(self, *args, **kwargs):
|
||||
self.verbose = True
|
||||
@@ -0,0 +1,76 @@
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
import contextlib
|
||||
import sys
|
||||
|
||||
from pylint.utils import utils
|
||||
|
||||
|
||||
class ArgumentPreprocessingError(Exception):
|
||||
"""Raised if an error occurs during argument preprocessing."""
|
||||
|
||||
|
||||
def preprocess_options(args, search_for):
|
||||
"""look for some options (keys of <search_for>) which have to be processed
|
||||
before others
|
||||
|
||||
values of <search_for> are callback functions to call when the option is
|
||||
found
|
||||
"""
|
||||
i = 0
|
||||
while i < len(args):
|
||||
arg = args[i]
|
||||
if arg.startswith("--"):
|
||||
try:
|
||||
option, val = arg[2:].split("=", 1)
|
||||
except ValueError:
|
||||
option, val = arg[2:], None
|
||||
try:
|
||||
cb, takearg = search_for[option]
|
||||
except KeyError:
|
||||
i += 1
|
||||
else:
|
||||
del args[i]
|
||||
if takearg and val is None:
|
||||
if i >= len(args) or args[i].startswith("-"):
|
||||
msg = "Option %s expects a value" % option
|
||||
raise ArgumentPreprocessingError(msg)
|
||||
val = args[i]
|
||||
del args[i]
|
||||
elif not takearg and val is not None:
|
||||
msg = "Option %s doesn't expects a value" % option
|
||||
raise ArgumentPreprocessingError(msg)
|
||||
cb(option, val)
|
||||
else:
|
||||
i += 1
|
||||
|
||||
|
||||
def _patch_sys_path(args):
|
||||
original = list(sys.path)
|
||||
changes = []
|
||||
seen = set()
|
||||
for arg in args:
|
||||
path = utils.get_python_path(arg)
|
||||
if path not in seen:
|
||||
changes.append(path)
|
||||
seen.add(path)
|
||||
|
||||
sys.path[:] = changes + sys.path
|
||||
return original
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def fix_import_path(args):
|
||||
"""Prepare sys.path for running the linter checks.
|
||||
|
||||
Within this context, each of the given arguments is importable.
|
||||
Paths are added to sys.path in corresponding order to the arguments.
|
||||
We avoid adding duplicate directories to sys.path.
|
||||
`sys.path` is reset to its original value upon exiting this context.
|
||||
"""
|
||||
original = _patch_sys_path(args)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
sys.path[:] = original
|
||||
@@ -0,0 +1,56 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2006-2014 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
|
||||
# Copyright (c) 2009 Vincent
|
||||
# Copyright (c) 2009 Mads Kiilerich <mads@kiilerich.com>
|
||||
# Copyright (c) 2012-2014 Google, Inc.
|
||||
# Copyright (c) 2014-2018 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2014-2015 Michal Nowikowski <godfryd@gmail.com>
|
||||
# Copyright (c) 2014 LCD 47 <lcd047@gmail.com>
|
||||
# Copyright (c) 2014 Brett Cannon <brett@python.org>
|
||||
# Copyright (c) 2014 Arun Persaud <arun@nubati.net>
|
||||
# Copyright (c) 2014 Damien Nozay <damien.nozay@gmail.com>
|
||||
# Copyright (c) 2015 Aru Sahni <arusahni@gmail.com>
|
||||
# Copyright (c) 2015 Florian Bruhin <me@the-compiler.org>
|
||||
# Copyright (c) 2015 Simu Toni <simutoni@gmail.com>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
|
||||
# Copyright (c) 2016 Łukasz Rogalski <rogalski.91@gmail.com>
|
||||
# Copyright (c) 2016 Moises Lopez <moylop260@vauxoo.com>
|
||||
# Copyright (c) 2016 Glenn Matthews <glenn@e-dad.net>
|
||||
# Copyright (c) 2016 Glenn Matthews <glmatthe@cisco.com>
|
||||
# Copyright (c) 2016 Ashley Whetter <ashley@awhetter.co.uk>
|
||||
# Copyright (c) 2016 xmo-odoo <xmo-odoo@users.noreply.github.com>
|
||||
# Copyright (c) 2017, 2020 Anthony Sottile <asottile@umich.edu>
|
||||
# Copyright (c) 2017 Pierre Sassoulas <pierre.sassoulas@cea.fr>
|
||||
# Copyright (c) 2017-2018 Bryce Guinta <bryce.paul.guinta@gmail.com>
|
||||
# Copyright (c) 2017-2018 hippo91 <guillaume.peillex@gmail.com>
|
||||
# Copyright (c) 2017 Chris Lamb <chris@chris-lamb.co.uk>
|
||||
# Copyright (c) 2017 Thomas Hisch <t.hisch@gmail.com>
|
||||
# Copyright (c) 2017 Mikhail Fesenko <proggga@gmail.com>
|
||||
# Copyright (c) 2017 Craig Citro <craigcitro@gmail.com>
|
||||
# Copyright (c) 2017 Ville Skyttä <ville.skytta@iki.fi>
|
||||
# Copyright (c) 2018-2019 Pierre Sassoulas <pierre.sassoulas@gmail.com>
|
||||
# Copyright (c) 2018 Pierre Sassoulas <pierre.sassoulas@wisebim.fr>
|
||||
# Copyright (c) 2018 ssolanki <sushobhitsolanki@gmail.com>
|
||||
# Copyright (c) 2018 Bryce Guinta <bryce.guinta@protonmail.com>
|
||||
# Copyright (c) 2018 Sushobhit <31987769+sushobhit27@users.noreply.github.com>
|
||||
# Copyright (c) 2018 Reverb C <reverbc@users.noreply.github.com>
|
||||
# Copyright (c) 2018 Nick Drozd <nicholasdrozd@gmail.com>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""All the classes related to Message handling."""
|
||||
|
||||
from pylint.message.message import Message
|
||||
from pylint.message.message_definition import MessageDefinition
|
||||
from pylint.message.message_definition_store import MessageDefinitionStore
|
||||
from pylint.message.message_handler_mix_in import MessagesHandlerMixIn
|
||||
from pylint.message.message_id_store import MessageIdStore
|
||||
|
||||
__all__ = [
|
||||
"Message",
|
||||
"MessageDefinition",
|
||||
"MessageDefinitionStore",
|
||||
"MessagesHandlerMixIn",
|
||||
"MessageIdStore",
|
||||
]
|
||||
@@ -0,0 +1,51 @@
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
|
||||
import collections
|
||||
|
||||
from pylint.constants import MSG_TYPES
|
||||
|
||||
_MsgBase = collections.namedtuple(
|
||||
"_MsgBase",
|
||||
[
|
||||
"msg_id",
|
||||
"symbol",
|
||||
"msg",
|
||||
"C",
|
||||
"category",
|
||||
"confidence",
|
||||
"abspath",
|
||||
"path",
|
||||
"module",
|
||||
"obj",
|
||||
"line",
|
||||
"column",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
class Message(_MsgBase):
|
||||
"""This class represent a message to be issued by the reporters"""
|
||||
|
||||
def __new__(cls, msg_id, symbol, location, msg, confidence):
|
||||
return _MsgBase.__new__(
|
||||
cls,
|
||||
msg_id,
|
||||
symbol,
|
||||
msg,
|
||||
msg_id[0],
|
||||
MSG_TYPES[msg_id[0]],
|
||||
confidence,
|
||||
*location
|
||||
)
|
||||
|
||||
def format(self, template):
|
||||
"""Format the message according to the given template.
|
||||
|
||||
The template format is the one of the format method :
|
||||
cf. http://docs.python.org/2/library/string.html#formatstrings
|
||||
"""
|
||||
# For some reason, _asdict on derived namedtuples does not work with
|
||||
# Python 3.4. Needs some investigation.
|
||||
return template.format(**dict(zip(self._fields, self)))
|
||||
@@ -0,0 +1,82 @@
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
import sys
|
||||
|
||||
from pylint.constants import MSG_TYPES
|
||||
from pylint.exceptions import InvalidMessageError
|
||||
from pylint.utils import normalize_text
|
||||
|
||||
|
||||
class MessageDefinition:
|
||||
def __init__(
|
||||
self,
|
||||
checker,
|
||||
msgid,
|
||||
msg,
|
||||
description,
|
||||
symbol,
|
||||
scope,
|
||||
minversion=None,
|
||||
maxversion=None,
|
||||
old_names=None,
|
||||
):
|
||||
self.checker_name = checker.name
|
||||
self.check_msgid(msgid)
|
||||
self.msgid = msgid
|
||||
self.symbol = symbol
|
||||
self.msg = msg
|
||||
self.description = description
|
||||
self.scope = scope
|
||||
self.minversion = minversion
|
||||
self.maxversion = maxversion
|
||||
self.old_names = []
|
||||
if old_names:
|
||||
for old_msgid, old_symbol in old_names:
|
||||
self.check_msgid(old_msgid)
|
||||
self.old_names.append([old_msgid, old_symbol])
|
||||
|
||||
@staticmethod
|
||||
def check_msgid(msgid: str) -> None:
|
||||
if len(msgid) != 5:
|
||||
raise InvalidMessageError("Invalid message id %r" % msgid)
|
||||
if msgid[0] not in MSG_TYPES:
|
||||
raise InvalidMessageError("Bad message type %s in %r" % (msgid[0], msgid))
|
||||
|
||||
def __repr__(self):
|
||||
return "MessageDefinition:%s (%s)" % (self.symbol, self.msgid)
|
||||
|
||||
def __str__(self):
|
||||
return "%s:\n%s %s" % (repr(self), self.msg, self.description)
|
||||
|
||||
def may_be_emitted(self):
|
||||
"""return True if message may be emitted using the current interpreter"""
|
||||
if self.minversion is not None and self.minversion > sys.version_info:
|
||||
return False
|
||||
if self.maxversion is not None and self.maxversion <= sys.version_info:
|
||||
return False
|
||||
return True
|
||||
|
||||
def format_help(self, checkerref=False):
|
||||
"""return the help string for the given message id"""
|
||||
desc = self.description
|
||||
if checkerref:
|
||||
desc += " This message belongs to the %s checker." % self.checker_name
|
||||
title = self.msg
|
||||
if self.minversion or self.maxversion:
|
||||
restr = []
|
||||
if self.minversion:
|
||||
restr.append("< %s" % ".".join([str(n) for n in self.minversion]))
|
||||
if self.maxversion:
|
||||
restr.append(">= %s" % ".".join([str(n) for n in self.maxversion]))
|
||||
restr = " or ".join(restr)
|
||||
if checkerref:
|
||||
desc += " It can't be emitted when using Python %s." % restr
|
||||
else:
|
||||
desc += " This message can't be emitted when using Python %s." % restr
|
||||
msg_help = normalize_text(" ".join(desc.split()), indent=" ")
|
||||
message_id = "%s (%s)" % (self.symbol, self.msgid)
|
||||
if title != "%s":
|
||||
title = title.splitlines()[0]
|
||||
return ":%s: *%s*\n%s" % (message_id, title.rstrip(" "), msg_help)
|
||||
return ":%s:\n%s" % (message_id, msg_help)
|
||||
@@ -0,0 +1,88 @@
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
import collections
|
||||
|
||||
from pylint.exceptions import UnknownMessageError
|
||||
from pylint.message.message_id_store import MessageIdStore
|
||||
|
||||
|
||||
class MessageDefinitionStore:
|
||||
|
||||
"""The messages store knows information about every possible message definition but has
|
||||
no particular state during analysis.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.message_id_store = MessageIdStore()
|
||||
# Primary registry for all active messages definitions.
|
||||
# It contains the 1:1 mapping from msgid to MessageDefinition.
|
||||
# Keys are msgid, values are MessageDefinition
|
||||
self._messages_definitions = {}
|
||||
# MessageDefinition kept by category
|
||||
self._msgs_by_category = collections.defaultdict(list)
|
||||
|
||||
@property
|
||||
def messages(self) -> list:
|
||||
"""The list of all active messages."""
|
||||
return self._messages_definitions.values()
|
||||
|
||||
def register_messages_from_checker(self, checker):
|
||||
"""Register all messages definitions from a checker.
|
||||
|
||||
:param BaseChecker checker:
|
||||
"""
|
||||
checker.check_consistency()
|
||||
for message in checker.messages:
|
||||
self.register_message(message)
|
||||
|
||||
def register_message(self, message):
|
||||
"""Register a MessageDefinition with consistency in mind.
|
||||
|
||||
:param MessageDefinition message: The message definition being added.
|
||||
"""
|
||||
self.message_id_store.register_message_definition(message)
|
||||
self._messages_definitions[message.msgid] = message
|
||||
self._msgs_by_category[message.msgid[0]].append(message.msgid)
|
||||
|
||||
def get_message_definitions(self, msgid_or_symbol: str) -> list:
|
||||
"""Returns the Message object for this message.
|
||||
:param str msgid_or_symbol: msgid_or_symbol may be either a numeric or symbolic id.
|
||||
:raises UnknownMessageError: if the message id is not defined.
|
||||
:rtype: List of MessageDefinition
|
||||
:return: A message definition corresponding to msgid_or_symbol
|
||||
"""
|
||||
return [
|
||||
self._messages_definitions[m]
|
||||
for m in self.message_id_store.get_active_msgids(msgid_or_symbol)
|
||||
]
|
||||
|
||||
def get_msg_display_string(self, msgid_or_symbol: str):
|
||||
"""Generates a user-consumable representation of a message. """
|
||||
message_definitions = self.get_message_definitions(msgid_or_symbol)
|
||||
if len(message_definitions) == 1:
|
||||
return repr(message_definitions[0].symbol)
|
||||
return repr([md.symbol for md in message_definitions])
|
||||
|
||||
def help_message(self, msgids_or_symbols: list):
|
||||
"""Display help messages for the given message identifiers"""
|
||||
for msgids_or_symbol in msgids_or_symbols:
|
||||
try:
|
||||
for message_definition in self.get_message_definitions(
|
||||
msgids_or_symbol
|
||||
):
|
||||
print(message_definition.format_help(checkerref=True))
|
||||
print("")
|
||||
except UnknownMessageError as ex:
|
||||
print(ex)
|
||||
print("")
|
||||
continue
|
||||
|
||||
def list_messages(self):
|
||||
"""Output full messages list documentation in ReST format. """
|
||||
messages = sorted(self._messages_definitions.values(), key=lambda m: m.msgid)
|
||||
for message in messages:
|
||||
if not message.may_be_emitted():
|
||||
continue
|
||||
print(message.format_help(checkerref=False))
|
||||
print("")
|
||||
@@ -0,0 +1,390 @@
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
import sys
|
||||
|
||||
from pylint.constants import (
|
||||
_SCOPE_EXEMPT,
|
||||
MAIN_CHECKER_NAME,
|
||||
MSG_STATE_CONFIDENCE,
|
||||
MSG_STATE_SCOPE_CONFIG,
|
||||
MSG_STATE_SCOPE_MODULE,
|
||||
MSG_TYPES,
|
||||
MSG_TYPES_LONG,
|
||||
MSG_TYPES_STATUS,
|
||||
WarningScope,
|
||||
)
|
||||
from pylint.exceptions import InvalidMessageError, UnknownMessageError
|
||||
from pylint.interfaces import UNDEFINED
|
||||
from pylint.message.message import Message
|
||||
from pylint.utils import get_module_and_frameid, get_rst_section, get_rst_title
|
||||
|
||||
|
||||
class MessagesHandlerMixIn:
|
||||
"""a mix-in class containing all the messages related methods for the main
|
||||
lint class
|
||||
"""
|
||||
|
||||
__by_id_managed_msgs = [] # type: ignore
|
||||
|
||||
def __init__(self):
|
||||
self._msgs_state = {}
|
||||
self.msg_status = 0
|
||||
|
||||
def _checker_messages(self, checker):
|
||||
for known_checker in self._checkers[checker.lower()]:
|
||||
yield from known_checker.msgs
|
||||
|
||||
@classmethod
|
||||
def clear_by_id_managed_msgs(cls):
|
||||
cls.__by_id_managed_msgs.clear()
|
||||
|
||||
@classmethod
|
||||
def get_by_id_managed_msgs(cls):
|
||||
return cls.__by_id_managed_msgs
|
||||
|
||||
def _register_by_id_managed_msg(self, msgid, line, is_disabled=True):
|
||||
"""If the msgid is a numeric one, then register it to inform the user
|
||||
it could furnish instead a symbolic msgid."""
|
||||
try:
|
||||
message_definitions = self.msgs_store.get_message_definitions(msgid)
|
||||
for message_definition in message_definitions:
|
||||
if msgid == message_definition.msgid:
|
||||
MessagesHandlerMixIn.__by_id_managed_msgs.append(
|
||||
(
|
||||
self.current_name,
|
||||
message_definition.msgid,
|
||||
message_definition.symbol,
|
||||
line,
|
||||
is_disabled,
|
||||
)
|
||||
)
|
||||
except UnknownMessageError:
|
||||
pass
|
||||
|
||||
def disable(self, msgid, scope="package", line=None, ignore_unknown=False):
|
||||
"""don't output message of the given id"""
|
||||
self._set_msg_status(
|
||||
msgid, enable=False, scope=scope, line=line, ignore_unknown=ignore_unknown
|
||||
)
|
||||
self._register_by_id_managed_msg(msgid, line)
|
||||
|
||||
def enable(self, msgid, scope="package", line=None, ignore_unknown=False):
|
||||
"""reenable message of the given id"""
|
||||
self._set_msg_status(
|
||||
msgid, enable=True, scope=scope, line=line, ignore_unknown=ignore_unknown
|
||||
)
|
||||
self._register_by_id_managed_msg(msgid, line, is_disabled=False)
|
||||
|
||||
def _set_msg_status(
|
||||
self, msgid, enable, scope="package", line=None, ignore_unknown=False
|
||||
):
|
||||
assert scope in ("package", "module")
|
||||
|
||||
if msgid == "all":
|
||||
for _msgid in MSG_TYPES:
|
||||
self._set_msg_status(_msgid, enable, scope, line, ignore_unknown)
|
||||
if enable and not self._python3_porting_mode:
|
||||
# Don't activate the python 3 porting checker if it wasn't activated explicitly.
|
||||
self.disable("python3")
|
||||
return
|
||||
|
||||
# msgid is a category?
|
||||
category_id = msgid.upper()
|
||||
if category_id not in MSG_TYPES:
|
||||
category_id = MSG_TYPES_LONG.get(category_id)
|
||||
if category_id is not None:
|
||||
for _msgid in self.msgs_store._msgs_by_category.get(category_id):
|
||||
self._set_msg_status(_msgid, enable, scope, line)
|
||||
return
|
||||
|
||||
# msgid is a checker name?
|
||||
if msgid.lower() in self._checkers:
|
||||
for checker in self._checkers[msgid.lower()]:
|
||||
for _msgid in checker.msgs:
|
||||
self._set_msg_status(_msgid, enable, scope, line)
|
||||
return
|
||||
|
||||
# msgid is report id?
|
||||
if msgid.lower().startswith("rp"):
|
||||
if enable:
|
||||
self.enable_report(msgid)
|
||||
else:
|
||||
self.disable_report(msgid)
|
||||
return
|
||||
|
||||
try:
|
||||
# msgid is a symbolic or numeric msgid.
|
||||
message_definitions = self.msgs_store.get_message_definitions(msgid)
|
||||
except UnknownMessageError:
|
||||
if ignore_unknown:
|
||||
return
|
||||
raise
|
||||
for message_definition in message_definitions:
|
||||
self._set_one_msg_status(scope, message_definition, line, enable)
|
||||
|
||||
def _set_one_msg_status(self, scope, msg, line, enable):
|
||||
if scope == "module":
|
||||
self.file_state.set_msg_status(msg, line, enable)
|
||||
if not enable and msg.symbol != "locally-disabled":
|
||||
self.add_message(
|
||||
"locally-disabled", line=line, args=(msg.symbol, msg.msgid)
|
||||
)
|
||||
else:
|
||||
msgs = self._msgs_state
|
||||
msgs[msg.msgid] = enable
|
||||
# sync configuration object
|
||||
self.config.enable = [
|
||||
self._message_symbol(mid) for mid, val in sorted(msgs.items()) if val
|
||||
]
|
||||
self.config.disable = [
|
||||
self._message_symbol(mid)
|
||||
for mid, val in sorted(msgs.items())
|
||||
if not val
|
||||
]
|
||||
|
||||
def _message_symbol(self, msgid):
|
||||
"""Get the message symbol of the given message id
|
||||
|
||||
Return the original message id if the message does not
|
||||
exist.
|
||||
"""
|
||||
try:
|
||||
return [md.symbol for md in self.msgs_store.get_message_definitions(msgid)]
|
||||
except UnknownMessageError:
|
||||
return msgid
|
||||
|
||||
def get_message_state_scope(self, msgid, line=None, confidence=UNDEFINED):
|
||||
"""Returns the scope at which a message was enabled/disabled."""
|
||||
if self.config.confidence and confidence.name not in self.config.confidence:
|
||||
return MSG_STATE_CONFIDENCE
|
||||
try:
|
||||
if line in self.file_state._module_msgs_state[msgid]:
|
||||
return MSG_STATE_SCOPE_MODULE
|
||||
except (KeyError, TypeError):
|
||||
return MSG_STATE_SCOPE_CONFIG
|
||||
return None
|
||||
|
||||
def is_message_enabled(self, msg_descr, line=None, confidence=None):
|
||||
"""return true if the message associated to the given message id is
|
||||
enabled
|
||||
|
||||
msgid may be either a numeric or symbolic message id.
|
||||
"""
|
||||
if self.config.confidence and confidence:
|
||||
if confidence.name not in self.config.confidence:
|
||||
return False
|
||||
try:
|
||||
message_definitions = self.msgs_store.get_message_definitions(msg_descr)
|
||||
msgids = [md.msgid for md in message_definitions]
|
||||
except UnknownMessageError:
|
||||
# The linter checks for messages that are not registered
|
||||
# due to version mismatch, just treat them as message IDs
|
||||
# for now.
|
||||
msgids = [msg_descr]
|
||||
for msgid in msgids:
|
||||
if self.is_one_message_enabled(msgid, line):
|
||||
return True
|
||||
return False
|
||||
|
||||
def is_one_message_enabled(self, msgid, line):
|
||||
if line is None:
|
||||
return self._msgs_state.get(msgid, True)
|
||||
try:
|
||||
return self.file_state._module_msgs_state[msgid][line]
|
||||
except KeyError:
|
||||
# Check if the message's line is after the maximum line existing in ast tree.
|
||||
# This line won't appear in the ast tree and won't be referred in
|
||||
# self.file_state._module_msgs_state
|
||||
# This happens for example with a commented line at the end of a module.
|
||||
max_line_number = self.file_state.get_effective_max_line_number()
|
||||
if max_line_number and line > max_line_number:
|
||||
fallback = True
|
||||
lines = self.file_state._raw_module_msgs_state.get(msgid, {})
|
||||
|
||||
# Doesn't consider scopes, as a disable can be in a different scope
|
||||
# than that of the current line.
|
||||
closest_lines = reversed(
|
||||
[
|
||||
(message_line, enable)
|
||||
for message_line, enable in lines.items()
|
||||
if message_line <= line
|
||||
]
|
||||
)
|
||||
last_line, is_enabled = next(closest_lines, (None, None))
|
||||
if last_line is not None:
|
||||
fallback = is_enabled
|
||||
|
||||
return self._msgs_state.get(msgid, fallback)
|
||||
return self._msgs_state.get(msgid, True)
|
||||
|
||||
def add_message(
|
||||
self, msgid, line=None, node=None, args=None, confidence=None, col_offset=None
|
||||
):
|
||||
"""Adds a message given by ID or name.
|
||||
|
||||
If provided, the message string is expanded using args.
|
||||
|
||||
AST checkers must provide the node argument (but may optionally
|
||||
provide line if the line number is different), raw and token checkers
|
||||
must provide the line argument.
|
||||
"""
|
||||
if confidence is None:
|
||||
confidence = UNDEFINED
|
||||
message_definitions = self.msgs_store.get_message_definitions(msgid)
|
||||
for message_definition in message_definitions:
|
||||
self.add_one_message(
|
||||
message_definition, line, node, args, confidence, col_offset
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def check_message_definition(message_definition, line, node):
|
||||
if message_definition.msgid[0] not in _SCOPE_EXEMPT:
|
||||
# Fatal messages and reports are special, the node/scope distinction
|
||||
# does not apply to them.
|
||||
if message_definition.scope == WarningScope.LINE:
|
||||
if line is None:
|
||||
raise InvalidMessageError(
|
||||
"Message %s must provide line, got None"
|
||||
% message_definition.msgid
|
||||
)
|
||||
if node is not None:
|
||||
raise InvalidMessageError(
|
||||
"Message %s must only provide line, "
|
||||
"got line=%s, node=%s" % (message_definition.msgid, line, node)
|
||||
)
|
||||
elif message_definition.scope == WarningScope.NODE:
|
||||
# Node-based warnings may provide an override line.
|
||||
if node is None:
|
||||
raise InvalidMessageError(
|
||||
"Message %s must provide Node, got None"
|
||||
% message_definition.msgid
|
||||
)
|
||||
|
||||
def add_one_message(
|
||||
self, message_definition, line, node, args, confidence, col_offset
|
||||
):
|
||||
self.check_message_definition(message_definition, line, node)
|
||||
if line is None and node is not None:
|
||||
line = node.fromlineno
|
||||
if col_offset is None and hasattr(node, "col_offset"):
|
||||
col_offset = node.col_offset
|
||||
|
||||
# should this message be displayed
|
||||
if not self.is_message_enabled(message_definition.msgid, line, confidence):
|
||||
self.file_state.handle_ignored_message(
|
||||
self.get_message_state_scope(
|
||||
message_definition.msgid, line, confidence
|
||||
),
|
||||
message_definition.msgid,
|
||||
line,
|
||||
node,
|
||||
args,
|
||||
confidence,
|
||||
)
|
||||
return
|
||||
# update stats
|
||||
msg_cat = MSG_TYPES[message_definition.msgid[0]]
|
||||
self.msg_status |= MSG_TYPES_STATUS[message_definition.msgid[0]]
|
||||
self.stats[msg_cat] += 1
|
||||
self.stats["by_module"][self.current_name][msg_cat] += 1
|
||||
try:
|
||||
self.stats["by_msg"][message_definition.symbol] += 1
|
||||
except KeyError:
|
||||
self.stats["by_msg"][message_definition.symbol] = 1
|
||||
# expand message ?
|
||||
msg = message_definition.msg
|
||||
if args:
|
||||
msg %= args
|
||||
# get module and object
|
||||
if node is None:
|
||||
module, obj = self.current_name, ""
|
||||
abspath = self.current_file
|
||||
else:
|
||||
module, obj = get_module_and_frameid(node)
|
||||
abspath = node.root().file
|
||||
path = abspath.replace(self.reporter.path_strip_prefix, "", 1)
|
||||
# add the message
|
||||
self.reporter.handle_message(
|
||||
Message(
|
||||
message_definition.msgid,
|
||||
message_definition.symbol,
|
||||
(abspath, path, module, obj, line or 1, col_offset or 0),
|
||||
msg,
|
||||
confidence,
|
||||
)
|
||||
)
|
||||
|
||||
def _get_checkers_infos(self):
|
||||
by_checker = {}
|
||||
for checker in self.get_checkers():
|
||||
name = checker.name
|
||||
if name != "master":
|
||||
try:
|
||||
by_checker[name]["checker"] = checker
|
||||
by_checker[name]["options"] += checker.options_and_values()
|
||||
by_checker[name]["msgs"].update(checker.msgs)
|
||||
by_checker[name]["reports"] += checker.reports
|
||||
except KeyError:
|
||||
by_checker[name] = {
|
||||
"checker": checker,
|
||||
"options": list(checker.options_and_values()),
|
||||
"msgs": dict(checker.msgs),
|
||||
"reports": list(checker.reports),
|
||||
}
|
||||
return by_checker
|
||||
|
||||
def get_checkers_documentation(self):
|
||||
result = get_rst_title("Pylint global options and switches", "-")
|
||||
result += """
|
||||
Pylint provides global options and switches.
|
||||
|
||||
"""
|
||||
for checker in self.get_checkers():
|
||||
name = checker.name
|
||||
if name == MAIN_CHECKER_NAME:
|
||||
if checker.options:
|
||||
for section, options in checker.options_by_section():
|
||||
if section is None:
|
||||
title = "General options"
|
||||
else:
|
||||
title = "%s options" % section.capitalize()
|
||||
result += get_rst_title(title, "~")
|
||||
result += "%s\n" % get_rst_section(None, options)
|
||||
result += get_rst_title("Pylint checkers' options and switches", "-")
|
||||
result += """\
|
||||
|
||||
Pylint checkers can provide three set of features:
|
||||
|
||||
* options that control their execution,
|
||||
* messages that they can raise,
|
||||
* reports that they can generate.
|
||||
|
||||
Below is a list of all checkers and their features.
|
||||
|
||||
"""
|
||||
by_checker = self._get_checkers_infos()
|
||||
for checker in sorted(by_checker):
|
||||
information = by_checker[checker]
|
||||
checker = information["checker"]
|
||||
del information["checker"]
|
||||
result += checker.get_full_documentation(**information)
|
||||
return result
|
||||
|
||||
def print_full_documentation(self, stream=None):
|
||||
"""output a full documentation in ReST format"""
|
||||
if not stream:
|
||||
stream = sys.stdout
|
||||
print(self.get_checkers_documentation()[:-1], file=stream)
|
||||
|
||||
@staticmethod
|
||||
def _print_checker_doc(information, stream=None):
|
||||
"""Helper method for print_full_documentation.
|
||||
|
||||
Also used by doc/exts/pylint_extensions.py.
|
||||
"""
|
||||
if not stream:
|
||||
stream = sys.stdout
|
||||
checker = information["checker"]
|
||||
del information["checker"]
|
||||
print(checker.get_full_documentation(**information)[:-1], file=stream)
|
||||
@@ -0,0 +1,126 @@
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
from typing import List
|
||||
|
||||
from pylint.exceptions import InvalidMessageError, UnknownMessageError
|
||||
|
||||
|
||||
class MessageIdStore:
|
||||
|
||||
"""The MessageIdStore store MessageId and make sure that there is a 1-1 relation between msgid and symbol."""
|
||||
|
||||
def __init__(self):
|
||||
self.__msgid_to_symbol = {}
|
||||
self.__symbol_to_msgid = {}
|
||||
self.__old_names = {}
|
||||
|
||||
def __len__(self):
|
||||
return len(self.__msgid_to_symbol)
|
||||
|
||||
def __repr__(self):
|
||||
result = "MessageIdStore: [\n"
|
||||
for msgid, symbol in self.__msgid_to_symbol.items():
|
||||
result += " - {msgid} ({symbol})\n".format(msgid=msgid, symbol=symbol)
|
||||
result += "]"
|
||||
return result
|
||||
|
||||
def get_symbol(self, msgid: str) -> str:
|
||||
return self.__msgid_to_symbol[msgid]
|
||||
|
||||
def get_msgid(self, symbol: str) -> str:
|
||||
return self.__symbol_to_msgid[symbol]
|
||||
|
||||
def register_message_definition(self, message_definition):
|
||||
self.check_msgid_and_symbol(message_definition.msgid, message_definition.symbol)
|
||||
self.add_msgid_and_symbol(message_definition.msgid, message_definition.symbol)
|
||||
for old_msgid, old_symbol in message_definition.old_names:
|
||||
self.check_msgid_and_symbol(old_msgid, old_symbol)
|
||||
self.add_legacy_msgid_and_symbol(
|
||||
old_msgid, old_symbol, message_definition.msgid
|
||||
)
|
||||
|
||||
def add_msgid_and_symbol(self, msgid: str, symbol: str) -> None:
|
||||
"""Add valid message id.
|
||||
|
||||
There is a little duplication with add_legacy_msgid_and_symbol to avoid a function call,
|
||||
this is called a lot at initialization."""
|
||||
self.__msgid_to_symbol[msgid] = symbol
|
||||
self.__symbol_to_msgid[symbol] = msgid
|
||||
|
||||
def add_legacy_msgid_and_symbol(self, msgid: str, symbol: str, new_msgid: str):
|
||||
"""Add valid legacy message id.
|
||||
|
||||
There is a little duplication with add_msgid_and_symbol to avoid a function call,
|
||||
this is called a lot at initialization."""
|
||||
self.__msgid_to_symbol[msgid] = symbol
|
||||
self.__symbol_to_msgid[symbol] = msgid
|
||||
existing_old_names = self.__old_names.get(msgid, [])
|
||||
existing_old_names.append(new_msgid)
|
||||
self.__old_names[msgid] = existing_old_names
|
||||
|
||||
def check_msgid_and_symbol(self, msgid: str, symbol: str) -> None:
|
||||
existing_msgid = self.__symbol_to_msgid.get(symbol)
|
||||
existing_symbol = self.__msgid_to_symbol.get(msgid)
|
||||
if existing_symbol is None and existing_msgid is None:
|
||||
return
|
||||
if existing_msgid is not None:
|
||||
if existing_msgid != msgid:
|
||||
self._raise_duplicate_msgid(symbol, msgid, existing_msgid)
|
||||
if existing_symbol != symbol:
|
||||
self._raise_duplicate_symbol(msgid, symbol, existing_symbol)
|
||||
|
||||
@staticmethod
|
||||
def _raise_duplicate_symbol(msgid, symbol, other_symbol):
|
||||
"""Raise an error when a symbol is duplicated.
|
||||
|
||||
:param str msgid: The msgid corresponding to the symbols
|
||||
:param str symbol: Offending symbol
|
||||
:param str other_symbol: Other offending symbol
|
||||
:raises InvalidMessageError:"""
|
||||
symbols = [symbol, other_symbol]
|
||||
symbols.sort()
|
||||
error_message = "Message id '{msgid}' cannot have both ".format(msgid=msgid)
|
||||
error_message += "'{other_symbol}' and '{symbol}' as symbolic name.".format(
|
||||
other_symbol=symbols[0], symbol=symbols[1]
|
||||
)
|
||||
raise InvalidMessageError(error_message)
|
||||
|
||||
@staticmethod
|
||||
def _raise_duplicate_msgid(symbol, msgid, other_msgid):
|
||||
"""Raise an error when a msgid is duplicated.
|
||||
|
||||
:param str symbol: The symbol corresponding to the msgids
|
||||
:param str msgid: Offending msgid
|
||||
:param str other_msgid: Other offending msgid
|
||||
:raises InvalidMessageError:"""
|
||||
msgids = [msgid, other_msgid]
|
||||
msgids.sort()
|
||||
error_message = (
|
||||
"Message symbol '{symbol}' cannot be used for "
|
||||
"'{other_msgid}' and '{msgid}' at the same time."
|
||||
" If you're creating an 'old_names' use 'old-{symbol}' as the old symbol."
|
||||
).format(symbol=symbol, other_msgid=msgids[0], msgid=msgids[1])
|
||||
raise InvalidMessageError(error_message)
|
||||
|
||||
def get_active_msgids(self, msgid_or_symbol: str) -> List[str]:
|
||||
"""Return msgids but the input can be a symbol."""
|
||||
# Only msgid can have a digit as second letter
|
||||
is_msgid = msgid_or_symbol[1:].isdigit()
|
||||
if is_msgid:
|
||||
msgid = msgid_or_symbol.upper()
|
||||
symbol = self.__msgid_to_symbol.get(msgid)
|
||||
else:
|
||||
msgid = self.__symbol_to_msgid.get(msgid_or_symbol)
|
||||
symbol = msgid_or_symbol
|
||||
if not msgid or not symbol:
|
||||
error_msg = "No such message id or symbol '{msgid_or_symbol}'.".format(
|
||||
msgid_or_symbol=msgid_or_symbol
|
||||
)
|
||||
raise UnknownMessageError(error_msg)
|
||||
# logging.debug(
|
||||
# "Return for {} and msgid {} is {}".format(
|
||||
# msgid_or_symbol, msgid, self.__old_names.get(msgid, [msgid])
|
||||
# )
|
||||
# )
|
||||
return self.__old_names.get(msgid, [msgid])
|
||||
@@ -0,0 +1,8 @@
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""
|
||||
pyreverse.extensions
|
||||
"""
|
||||
|
||||
__revision__ = "$Id $"
|
||||
@@ -0,0 +1,240 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2006, 2008-2010, 2013-2014 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
|
||||
# Copyright (c) 2014 Brett Cannon <brett@python.org>
|
||||
# Copyright (c) 2014 Arun Persaud <arun@nubati.net>
|
||||
# Copyright (c) 2015-2018 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2015 Florian Bruhin <me@the-compiler.org>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
|
||||
# Copyright (c) 2016 Ashley Whetter <ashley@awhetter.co.uk>
|
||||
# Copyright (c) 2017 Łukasz Rogalski <rogalski.91@gmail.com>
|
||||
# Copyright (c) 2018 ssolanki <sushobhitsolanki@gmail.com>
|
||||
# Copyright (c) 2018 Sushobhit <31987769+sushobhit27@users.noreply.github.com>
|
||||
# Copyright (c) 2018 Ville Skyttä <ville.skytta@iki.fi>
|
||||
# Copyright (c) 2019 Pierre Sassoulas <pierre.sassoulas@gmail.com>
|
||||
# Copyright (c) 2020 Anthony Sottile <asottile@umich.edu>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""handle diagram generation options for class diagram or default diagrams
|
||||
"""
|
||||
|
||||
import astroid
|
||||
|
||||
from pylint.pyreverse.diagrams import ClassDiagram, PackageDiagram
|
||||
from pylint.pyreverse.utils import LocalsVisitor
|
||||
|
||||
BUILTINS_NAME = "builtins"
|
||||
|
||||
# diagram generators ##########################################################
|
||||
|
||||
|
||||
class DiaDefGenerator:
|
||||
"""handle diagram generation options"""
|
||||
|
||||
def __init__(self, linker, handler):
|
||||
"""common Diagram Handler initialization"""
|
||||
self.config = handler.config
|
||||
self._set_default_options()
|
||||
self.linker = linker
|
||||
self.classdiagram = None # defined by subclasses
|
||||
|
||||
def get_title(self, node):
|
||||
"""get title for objects"""
|
||||
title = node.name
|
||||
if self.module_names:
|
||||
title = "%s.%s" % (node.root().name, title)
|
||||
return title
|
||||
|
||||
def _set_option(self, option):
|
||||
"""activate some options if not explicitly deactivated"""
|
||||
# if we have a class diagram, we want more information by default;
|
||||
# so if the option is None, we return True
|
||||
if option is None:
|
||||
return bool(self.config.classes)
|
||||
return option
|
||||
|
||||
def _set_default_options(self):
|
||||
"""set different default options with _default dictionary"""
|
||||
self.module_names = self._set_option(self.config.module_names)
|
||||
all_ancestors = self._set_option(self.config.all_ancestors)
|
||||
all_associated = self._set_option(self.config.all_associated)
|
||||
anc_level, association_level = (0, 0)
|
||||
if all_ancestors:
|
||||
anc_level = -1
|
||||
if all_associated:
|
||||
association_level = -1
|
||||
if self.config.show_ancestors is not None:
|
||||
anc_level = self.config.show_ancestors
|
||||
if self.config.show_associated is not None:
|
||||
association_level = self.config.show_associated
|
||||
self.anc_level, self.association_level = anc_level, association_level
|
||||
|
||||
def _get_levels(self):
|
||||
"""help function for search levels"""
|
||||
return self.anc_level, self.association_level
|
||||
|
||||
def show_node(self, node):
|
||||
"""true if builtins and not show_builtins"""
|
||||
if self.config.show_builtin:
|
||||
return True
|
||||
return node.root().name != BUILTINS_NAME
|
||||
|
||||
def add_class(self, node):
|
||||
"""visit one class and add it to diagram"""
|
||||
self.linker.visit(node)
|
||||
self.classdiagram.add_object(self.get_title(node), node)
|
||||
|
||||
def get_ancestors(self, node, level):
|
||||
"""return ancestor nodes of a class node"""
|
||||
if level == 0:
|
||||
return
|
||||
for ancestor in node.ancestors(recurs=False):
|
||||
if not self.show_node(ancestor):
|
||||
continue
|
||||
yield ancestor
|
||||
|
||||
def get_associated(self, klass_node, level):
|
||||
"""return associated nodes of a class node"""
|
||||
if level == 0:
|
||||
return
|
||||
for association_nodes in list(klass_node.instance_attrs_type.values()) + list(
|
||||
klass_node.locals_type.values()
|
||||
):
|
||||
for node in association_nodes:
|
||||
if isinstance(node, astroid.Instance):
|
||||
node = node._proxied
|
||||
if not (isinstance(node, astroid.ClassDef) and self.show_node(node)):
|
||||
continue
|
||||
yield node
|
||||
|
||||
def extract_classes(self, klass_node, anc_level, association_level):
|
||||
"""extract recursively classes related to klass_node"""
|
||||
if self.classdiagram.has_node(klass_node) or not self.show_node(klass_node):
|
||||
return
|
||||
self.add_class(klass_node)
|
||||
|
||||
for ancestor in self.get_ancestors(klass_node, anc_level):
|
||||
self.extract_classes(ancestor, anc_level - 1, association_level)
|
||||
|
||||
for node in self.get_associated(klass_node, association_level):
|
||||
self.extract_classes(node, anc_level, association_level - 1)
|
||||
|
||||
|
||||
class DefaultDiadefGenerator(LocalsVisitor, DiaDefGenerator):
|
||||
"""generate minimum diagram definition for the project :
|
||||
|
||||
* a package diagram including project's modules
|
||||
* a class diagram including project's classes
|
||||
"""
|
||||
|
||||
def __init__(self, linker, handler):
|
||||
DiaDefGenerator.__init__(self, linker, handler)
|
||||
LocalsVisitor.__init__(self)
|
||||
|
||||
def visit_project(self, node):
|
||||
"""visit a pyreverse.utils.Project node
|
||||
|
||||
create a diagram definition for packages
|
||||
"""
|
||||
mode = self.config.mode
|
||||
if len(node.modules) > 1:
|
||||
self.pkgdiagram = PackageDiagram("packages %s" % node.name, mode)
|
||||
else:
|
||||
self.pkgdiagram = None
|
||||
self.classdiagram = ClassDiagram("classes %s" % node.name, mode)
|
||||
|
||||
def leave_project(self, node): # pylint: disable=unused-argument
|
||||
"""leave the pyreverse.utils.Project node
|
||||
|
||||
return the generated diagram definition
|
||||
"""
|
||||
if self.pkgdiagram:
|
||||
return self.pkgdiagram, self.classdiagram
|
||||
return (self.classdiagram,)
|
||||
|
||||
def visit_module(self, node):
|
||||
"""visit an astroid.Module node
|
||||
|
||||
add this class to the package diagram definition
|
||||
"""
|
||||
if self.pkgdiagram:
|
||||
self.linker.visit(node)
|
||||
self.pkgdiagram.add_object(node.name, node)
|
||||
|
||||
def visit_classdef(self, node):
|
||||
"""visit an astroid.Class node
|
||||
|
||||
add this class to the class diagram definition
|
||||
"""
|
||||
anc_level, association_level = self._get_levels()
|
||||
self.extract_classes(node, anc_level, association_level)
|
||||
|
||||
def visit_importfrom(self, node):
|
||||
"""visit astroid.ImportFrom and catch modules for package diagram
|
||||
"""
|
||||
if self.pkgdiagram:
|
||||
self.pkgdiagram.add_from_depend(node, node.modname)
|
||||
|
||||
|
||||
class ClassDiadefGenerator(DiaDefGenerator):
|
||||
"""generate a class diagram definition including all classes related to a
|
||||
given class
|
||||
"""
|
||||
|
||||
def __init__(self, linker, handler):
|
||||
DiaDefGenerator.__init__(self, linker, handler)
|
||||
|
||||
def class_diagram(self, project, klass):
|
||||
"""return a class diagram definition for the given klass and its
|
||||
related klasses
|
||||
"""
|
||||
|
||||
self.classdiagram = ClassDiagram(klass, self.config.mode)
|
||||
if len(project.modules) > 1:
|
||||
module, klass = klass.rsplit(".", 1)
|
||||
module = project.get_module(module)
|
||||
else:
|
||||
module = project.modules[0]
|
||||
klass = klass.split(".")[-1]
|
||||
klass = next(module.ilookup(klass))
|
||||
|
||||
anc_level, association_level = self._get_levels()
|
||||
self.extract_classes(klass, anc_level, association_level)
|
||||
return self.classdiagram
|
||||
|
||||
|
||||
# diagram handler #############################################################
|
||||
|
||||
|
||||
class DiadefsHandler:
|
||||
"""handle diagram definitions :
|
||||
|
||||
get it from user (i.e. xml files) or generate them
|
||||
"""
|
||||
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
|
||||
def get_diadefs(self, project, linker):
|
||||
"""Get the diagrams configuration data
|
||||
|
||||
:param project:The pyreverse project
|
||||
:type project: pyreverse.utils.Project
|
||||
:param linker: The linker
|
||||
:type linker: pyreverse.inspector.Linker(IdGeneratorMixIn, LocalsVisitor)
|
||||
|
||||
:returns: The list of diagram definitions
|
||||
:rtype: list(:class:`pylint.pyreverse.diagrams.ClassDiagram`)
|
||||
"""
|
||||
|
||||
# read and interpret diagram definitions (Diadefs)
|
||||
diagrams = []
|
||||
generator = ClassDiadefGenerator(linker, self)
|
||||
for klass in self.config.classes:
|
||||
diagrams.append(generator.class_diagram(project, klass))
|
||||
if not diagrams:
|
||||
diagrams = DefaultDiadefGenerator(linker, self).visit(project)
|
||||
for diagram in diagrams:
|
||||
diagram.extract_relationships()
|
||||
return diagrams
|
||||
@@ -0,0 +1,269 @@
|
||||
# Copyright (c) 2006, 2008-2010, 2012-2014 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
|
||||
# Copyright (c) 2014-2018 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2014 Brett Cannon <brett@python.org>
|
||||
# Copyright (c) 2014 Arun Persaud <arun@nubati.net>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
|
||||
# Copyright (c) 2018 ssolanki <sushobhitsolanki@gmail.com>
|
||||
# Copyright (c) 2019 Pierre Sassoulas <pierre.sassoulas@gmail.com>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""diagram objects
|
||||
"""
|
||||
|
||||
import astroid
|
||||
|
||||
from pylint.checkers.utils import decorated_with_property
|
||||
from pylint.pyreverse.utils import FilterMixIn, is_interface
|
||||
|
||||
|
||||
class Figure:
|
||||
"""base class for counter handling"""
|
||||
|
||||
|
||||
class Relationship(Figure):
|
||||
"""a relation ship from an object in the diagram to another
|
||||
"""
|
||||
|
||||
def __init__(self, from_object, to_object, relation_type, name=None):
|
||||
Figure.__init__(self)
|
||||
self.from_object = from_object
|
||||
self.to_object = to_object
|
||||
self.type = relation_type
|
||||
self.name = name
|
||||
|
||||
|
||||
class DiagramEntity(Figure):
|
||||
"""a diagram object, i.e. a label associated to an astroid node
|
||||
"""
|
||||
|
||||
def __init__(self, title="No name", node=None):
|
||||
Figure.__init__(self)
|
||||
self.title = title
|
||||
self.node = node
|
||||
|
||||
|
||||
class ClassDiagram(Figure, FilterMixIn):
|
||||
"""main class diagram handling
|
||||
"""
|
||||
|
||||
TYPE = "class"
|
||||
|
||||
def __init__(self, title, mode):
|
||||
FilterMixIn.__init__(self, mode)
|
||||
Figure.__init__(self)
|
||||
self.title = title
|
||||
self.objects = []
|
||||
self.relationships = {}
|
||||
self._nodes = {}
|
||||
self.depends = []
|
||||
|
||||
def get_relationships(self, role):
|
||||
# sorted to get predictable (hence testable) results
|
||||
return sorted(
|
||||
self.relationships.get(role, ()),
|
||||
key=lambda x: (x.from_object.fig_id, x.to_object.fig_id),
|
||||
)
|
||||
|
||||
def add_relationship(self, from_object, to_object, relation_type, name=None):
|
||||
"""create a relation ship
|
||||
"""
|
||||
rel = Relationship(from_object, to_object, relation_type, name)
|
||||
self.relationships.setdefault(relation_type, []).append(rel)
|
||||
|
||||
def get_relationship(self, from_object, relation_type):
|
||||
"""return a relation ship or None
|
||||
"""
|
||||
for rel in self.relationships.get(relation_type, ()):
|
||||
if rel.from_object is from_object:
|
||||
return rel
|
||||
raise KeyError(relation_type)
|
||||
|
||||
def get_attrs(self, node):
|
||||
"""return visible attributes, possibly with class name"""
|
||||
attrs = []
|
||||
properties = [
|
||||
(n, m)
|
||||
for n, m in node.items()
|
||||
if isinstance(m, astroid.FunctionDef) and decorated_with_property(m)
|
||||
]
|
||||
for node_name, associated_nodes in (
|
||||
list(node.instance_attrs_type.items())
|
||||
+ list(node.locals_type.items())
|
||||
+ properties
|
||||
):
|
||||
if not self.show_attr(node_name):
|
||||
continue
|
||||
names = self.class_names(associated_nodes)
|
||||
if names:
|
||||
node_name = "%s : %s" % (node_name, ", ".join(names))
|
||||
attrs.append(node_name)
|
||||
return sorted(attrs)
|
||||
|
||||
def get_methods(self, node):
|
||||
"""return visible methods"""
|
||||
methods = [
|
||||
m
|
||||
for m in node.values()
|
||||
if isinstance(m, astroid.FunctionDef)
|
||||
and not decorated_with_property(m)
|
||||
and self.show_attr(m.name)
|
||||
]
|
||||
return sorted(methods, key=lambda n: n.name)
|
||||
|
||||
def add_object(self, title, node):
|
||||
"""create a diagram object
|
||||
"""
|
||||
assert node not in self._nodes
|
||||
ent = DiagramEntity(title, node)
|
||||
self._nodes[node] = ent
|
||||
self.objects.append(ent)
|
||||
|
||||
def class_names(self, nodes):
|
||||
"""return class names if needed in diagram"""
|
||||
names = []
|
||||
for node in nodes:
|
||||
if isinstance(node, astroid.Instance):
|
||||
node = node._proxied
|
||||
if (
|
||||
isinstance(node, astroid.ClassDef)
|
||||
and hasattr(node, "name")
|
||||
and not self.has_node(node)
|
||||
):
|
||||
if node.name not in names:
|
||||
node_name = node.name
|
||||
names.append(node_name)
|
||||
return names
|
||||
|
||||
def nodes(self):
|
||||
"""return the list of underlying nodes
|
||||
"""
|
||||
return self._nodes.keys()
|
||||
|
||||
def has_node(self, node):
|
||||
"""return true if the given node is included in the diagram
|
||||
"""
|
||||
return node in self._nodes
|
||||
|
||||
def object_from_node(self, node):
|
||||
"""return the diagram object mapped to node
|
||||
"""
|
||||
return self._nodes[node]
|
||||
|
||||
def classes(self):
|
||||
"""return all class nodes in the diagram"""
|
||||
return [o for o in self.objects if isinstance(o.node, astroid.ClassDef)]
|
||||
|
||||
def classe(self, name):
|
||||
"""return a class by its name, raise KeyError if not found
|
||||
"""
|
||||
for klass in self.classes():
|
||||
if klass.node.name == name:
|
||||
return klass
|
||||
raise KeyError(name)
|
||||
|
||||
def extract_relationships(self):
|
||||
"""extract relation ships between nodes in the diagram
|
||||
"""
|
||||
for obj in self.classes():
|
||||
node = obj.node
|
||||
obj.attrs = self.get_attrs(node)
|
||||
obj.methods = self.get_methods(node)
|
||||
# shape
|
||||
if is_interface(node):
|
||||
obj.shape = "interface"
|
||||
else:
|
||||
obj.shape = "class"
|
||||
# inheritance link
|
||||
for par_node in node.ancestors(recurs=False):
|
||||
try:
|
||||
par_obj = self.object_from_node(par_node)
|
||||
self.add_relationship(obj, par_obj, "specialization")
|
||||
except KeyError:
|
||||
continue
|
||||
# implements link
|
||||
for impl_node in node.implements:
|
||||
try:
|
||||
impl_obj = self.object_from_node(impl_node)
|
||||
self.add_relationship(obj, impl_obj, "implements")
|
||||
except KeyError:
|
||||
continue
|
||||
# associations link
|
||||
for name, values in list(node.instance_attrs_type.items()) + list(
|
||||
node.locals_type.items()
|
||||
):
|
||||
for value in values:
|
||||
if value is astroid.Uninferable:
|
||||
continue
|
||||
if isinstance(value, astroid.Instance):
|
||||
value = value._proxied
|
||||
try:
|
||||
associated_obj = self.object_from_node(value)
|
||||
self.add_relationship(associated_obj, obj, "association", name)
|
||||
except KeyError:
|
||||
continue
|
||||
|
||||
|
||||
class PackageDiagram(ClassDiagram):
|
||||
"""package diagram handling
|
||||
"""
|
||||
|
||||
TYPE = "package"
|
||||
|
||||
def modules(self):
|
||||
"""return all module nodes in the diagram"""
|
||||
return [o for o in self.objects if isinstance(o.node, astroid.Module)]
|
||||
|
||||
def module(self, name):
|
||||
"""return a module by its name, raise KeyError if not found
|
||||
"""
|
||||
for mod in self.modules():
|
||||
if mod.node.name == name:
|
||||
return mod
|
||||
raise KeyError(name)
|
||||
|
||||
def get_module(self, name, node):
|
||||
"""return a module by its name, looking also for relative imports;
|
||||
raise KeyError if not found
|
||||
"""
|
||||
for mod in self.modules():
|
||||
mod_name = mod.node.name
|
||||
if mod_name == name:
|
||||
return mod
|
||||
# search for fullname of relative import modules
|
||||
package = node.root().name
|
||||
if mod_name == "%s.%s" % (package, name):
|
||||
return mod
|
||||
if mod_name == "%s.%s" % (package.rsplit(".", 1)[0], name):
|
||||
return mod
|
||||
raise KeyError(name)
|
||||
|
||||
def add_from_depend(self, node, from_module):
|
||||
"""add dependencies created by from-imports
|
||||
"""
|
||||
mod_name = node.root().name
|
||||
obj = self.module(mod_name)
|
||||
if from_module not in obj.node.depends:
|
||||
obj.node.depends.append(from_module)
|
||||
|
||||
def extract_relationships(self):
|
||||
"""extract relation ships between nodes in the diagram
|
||||
"""
|
||||
ClassDiagram.extract_relationships(self)
|
||||
for obj in self.classes():
|
||||
# ownership
|
||||
try:
|
||||
mod = self.object_from_node(obj.node.root())
|
||||
self.add_relationship(obj, mod, "ownership")
|
||||
except KeyError:
|
||||
continue
|
||||
for obj in self.modules():
|
||||
obj.shape = "package"
|
||||
# dependencies
|
||||
for dep_name in obj.node.depends:
|
||||
try:
|
||||
dep = self.get_module(dep_name, obj.node)
|
||||
except KeyError:
|
||||
continue
|
||||
self.add_relationship(obj, dep, "depends")
|
||||
@@ -0,0 +1,361 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2015-2019 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2017 Łukasz Rogalski <rogalski.91@gmail.com>
|
||||
# Copyright (c) 2018 ssolanki <sushobhitsolanki@gmail.com>
|
||||
# Copyright (c) 2018 Ville Skyttä <ville.skytta@iki.fi>
|
||||
# Copyright (c) 2019 Hugo van Kemenade <hugovk@users.noreply.github.com>
|
||||
# Copyright (c) 2019 Pierre Sassoulas <pierre.sassoulas@gmail.com>
|
||||
# Copyright (c) 2020 Anthony Sottile <asottile@umich.edu>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""
|
||||
Visitor doing some postprocessing on the astroid tree.
|
||||
Try to resolve definitions (namespace) dictionary, relationship...
|
||||
"""
|
||||
import collections
|
||||
import os
|
||||
import traceback
|
||||
|
||||
import astroid
|
||||
from astroid import bases, exceptions, manager, modutils, node_classes
|
||||
|
||||
from pylint.pyreverse import utils
|
||||
|
||||
|
||||
def _iface_hdlr(_):
|
||||
"""Handler used by interfaces to handle suspicious interface nodes."""
|
||||
return True
|
||||
|
||||
|
||||
def _astroid_wrapper(func, modname):
|
||||
print("parsing %s..." % modname)
|
||||
try:
|
||||
return func(modname)
|
||||
except exceptions.AstroidBuildingException as exc:
|
||||
print(exc)
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
def interfaces(node, herited=True, handler_func=_iface_hdlr):
|
||||
"""Return an iterator on interfaces implemented by the given class node."""
|
||||
try:
|
||||
implements = bases.Instance(node).getattr("__implements__")[0]
|
||||
except exceptions.NotFoundError:
|
||||
return
|
||||
if not herited and implements.frame() is not node:
|
||||
return
|
||||
found = set()
|
||||
missing = False
|
||||
for iface in node_classes.unpack_infer(implements):
|
||||
if iface is astroid.Uninferable:
|
||||
missing = True
|
||||
continue
|
||||
if iface not in found and handler_func(iface):
|
||||
found.add(iface)
|
||||
yield iface
|
||||
if missing:
|
||||
raise exceptions.InferenceError()
|
||||
|
||||
|
||||
class IdGeneratorMixIn:
|
||||
"""Mixin adding the ability to generate integer uid."""
|
||||
|
||||
def __init__(self, start_value=0):
|
||||
self.id_count = start_value
|
||||
|
||||
def init_counter(self, start_value=0):
|
||||
"""init the id counter
|
||||
"""
|
||||
self.id_count = start_value
|
||||
|
||||
def generate_id(self):
|
||||
"""generate a new identifier
|
||||
"""
|
||||
self.id_count += 1
|
||||
return self.id_count
|
||||
|
||||
|
||||
class Linker(IdGeneratorMixIn, utils.LocalsVisitor):
|
||||
"""Walk on the project tree and resolve relationships.
|
||||
|
||||
According to options the following attributes may be
|
||||
added to visited nodes:
|
||||
|
||||
* uid,
|
||||
a unique identifier for the node (on astroid.Project, astroid.Module,
|
||||
astroid.Class and astroid.locals_type). Only if the linker
|
||||
has been instantiated with tag=True parameter (False by default).
|
||||
|
||||
* Function
|
||||
a mapping from locals names to their bounded value, which may be a
|
||||
constant like a string or an integer, or an astroid node
|
||||
(on astroid.Module, astroid.Class and astroid.Function).
|
||||
|
||||
* instance_attrs_type
|
||||
as locals_type but for klass member attributes (only on astroid.Class)
|
||||
|
||||
* implements,
|
||||
list of implemented interface _objects_ (only on astroid.Class nodes)
|
||||
"""
|
||||
|
||||
def __init__(self, project, inherited_interfaces=0, tag=False):
|
||||
IdGeneratorMixIn.__init__(self)
|
||||
utils.LocalsVisitor.__init__(self)
|
||||
# take inherited interface in consideration or not
|
||||
self.inherited_interfaces = inherited_interfaces
|
||||
# tag nodes or not
|
||||
self.tag = tag
|
||||
# visited project
|
||||
self.project = project
|
||||
|
||||
def visit_project(self, node):
|
||||
"""visit a pyreverse.utils.Project node
|
||||
|
||||
* optionally tag the node with a unique id
|
||||
"""
|
||||
if self.tag:
|
||||
node.uid = self.generate_id()
|
||||
for module in node.modules:
|
||||
self.visit(module)
|
||||
|
||||
def visit_package(self, node):
|
||||
"""visit an astroid.Package node
|
||||
|
||||
* optionally tag the node with a unique id
|
||||
"""
|
||||
if self.tag:
|
||||
node.uid = self.generate_id()
|
||||
for subelmt in node.values():
|
||||
self.visit(subelmt)
|
||||
|
||||
def visit_module(self, node):
|
||||
"""visit an astroid.Module node
|
||||
|
||||
* set the locals_type mapping
|
||||
* set the depends mapping
|
||||
* optionally tag the node with a unique id
|
||||
"""
|
||||
if hasattr(node, "locals_type"):
|
||||
return
|
||||
node.locals_type = collections.defaultdict(list)
|
||||
node.depends = []
|
||||
if self.tag:
|
||||
node.uid = self.generate_id()
|
||||
|
||||
def visit_classdef(self, node):
|
||||
"""visit an astroid.Class node
|
||||
|
||||
* set the locals_type and instance_attrs_type mappings
|
||||
* set the implements list and build it
|
||||
* optionally tag the node with a unique id
|
||||
"""
|
||||
if hasattr(node, "locals_type"):
|
||||
return
|
||||
node.locals_type = collections.defaultdict(list)
|
||||
if self.tag:
|
||||
node.uid = self.generate_id()
|
||||
# resolve ancestors
|
||||
for baseobj in node.ancestors(recurs=False):
|
||||
specializations = getattr(baseobj, "specializations", [])
|
||||
specializations.append(node)
|
||||
baseobj.specializations = specializations
|
||||
# resolve instance attributes
|
||||
node.instance_attrs_type = collections.defaultdict(list)
|
||||
for assignattrs in node.instance_attrs.values():
|
||||
for assignattr in assignattrs:
|
||||
if not isinstance(assignattr, astroid.Unknown):
|
||||
self.handle_assignattr_type(assignattr, node)
|
||||
# resolve implemented interface
|
||||
try:
|
||||
node.implements = list(interfaces(node, self.inherited_interfaces))
|
||||
except astroid.InferenceError:
|
||||
node.implements = ()
|
||||
|
||||
def visit_functiondef(self, node):
|
||||
"""visit an astroid.Function node
|
||||
|
||||
* set the locals_type mapping
|
||||
* optionally tag the node with a unique id
|
||||
"""
|
||||
if hasattr(node, "locals_type"):
|
||||
return
|
||||
node.locals_type = collections.defaultdict(list)
|
||||
if self.tag:
|
||||
node.uid = self.generate_id()
|
||||
|
||||
link_project = visit_project
|
||||
link_module = visit_module
|
||||
link_class = visit_classdef
|
||||
link_function = visit_functiondef
|
||||
|
||||
def visit_assignname(self, node):
|
||||
"""visit an astroid.AssignName node
|
||||
|
||||
handle locals_type
|
||||
"""
|
||||
# avoid double parsing done by different Linkers.visit
|
||||
# running over the same project:
|
||||
if hasattr(node, "_handled"):
|
||||
return
|
||||
node._handled = True
|
||||
if node.name in node.frame():
|
||||
frame = node.frame()
|
||||
else:
|
||||
# the name has been defined as 'global' in the frame and belongs
|
||||
# there.
|
||||
frame = node.root()
|
||||
try:
|
||||
if not hasattr(frame, "locals_type"):
|
||||
# If the frame doesn't have a locals_type yet,
|
||||
# it means it wasn't yet visited. Visit it now
|
||||
# to add what's missing from it.
|
||||
if isinstance(frame, astroid.ClassDef):
|
||||
self.visit_classdef(frame)
|
||||
elif isinstance(frame, astroid.FunctionDef):
|
||||
self.visit_functiondef(frame)
|
||||
else:
|
||||
self.visit_module(frame)
|
||||
|
||||
current = frame.locals_type[node.name]
|
||||
values = set(node.infer())
|
||||
frame.locals_type[node.name] = list(set(current) | values)
|
||||
except astroid.InferenceError:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def handle_assignattr_type(node, parent):
|
||||
"""handle an astroid.assignattr node
|
||||
|
||||
handle instance_attrs_type
|
||||
"""
|
||||
try:
|
||||
values = set(node.infer())
|
||||
current = set(parent.instance_attrs_type[node.attrname])
|
||||
parent.instance_attrs_type[node.attrname] = list(current | values)
|
||||
except astroid.InferenceError:
|
||||
pass
|
||||
|
||||
def visit_import(self, node):
|
||||
"""visit an astroid.Import node
|
||||
|
||||
resolve module dependencies
|
||||
"""
|
||||
context_file = node.root().file
|
||||
for name in node.names:
|
||||
relative = modutils.is_relative(name[0], context_file)
|
||||
self._imported_module(node, name[0], relative)
|
||||
|
||||
def visit_importfrom(self, node):
|
||||
"""visit an astroid.ImportFrom node
|
||||
|
||||
resolve module dependencies
|
||||
"""
|
||||
basename = node.modname
|
||||
context_file = node.root().file
|
||||
if context_file is not None:
|
||||
relative = modutils.is_relative(basename, context_file)
|
||||
else:
|
||||
relative = False
|
||||
for name in node.names:
|
||||
if name[0] == "*":
|
||||
continue
|
||||
# analyze dependencies
|
||||
fullname = "%s.%s" % (basename, name[0])
|
||||
if fullname.find(".") > -1:
|
||||
try:
|
||||
fullname = modutils.get_module_part(fullname, context_file)
|
||||
except ImportError:
|
||||
continue
|
||||
if fullname != basename:
|
||||
self._imported_module(node, fullname, relative)
|
||||
|
||||
def compute_module(self, context_name, mod_path):
|
||||
"""return true if the module should be added to dependencies"""
|
||||
package_dir = os.path.dirname(self.project.path)
|
||||
if context_name == mod_path:
|
||||
return 0
|
||||
if modutils.is_standard_module(mod_path, (package_dir,)):
|
||||
return 1
|
||||
return 0
|
||||
|
||||
def _imported_module(self, node, mod_path, relative):
|
||||
"""Notify an imported module, used to analyze dependencies"""
|
||||
module = node.root()
|
||||
context_name = module.name
|
||||
if relative:
|
||||
mod_path = "%s.%s" % (".".join(context_name.split(".")[:-1]), mod_path)
|
||||
if self.compute_module(context_name, mod_path):
|
||||
# handle dependencies
|
||||
if not hasattr(module, "depends"):
|
||||
module.depends = []
|
||||
mod_paths = module.depends
|
||||
if mod_path not in mod_paths:
|
||||
mod_paths.append(mod_path)
|
||||
|
||||
|
||||
class Project:
|
||||
"""a project handle a set of modules / packages"""
|
||||
|
||||
def __init__(self, name=""):
|
||||
self.name = name
|
||||
self.path = None
|
||||
self.modules = []
|
||||
self.locals = {}
|
||||
self.__getitem__ = self.locals.__getitem__
|
||||
self.__iter__ = self.locals.__iter__
|
||||
self.values = self.locals.values
|
||||
self.keys = self.locals.keys
|
||||
self.items = self.locals.items
|
||||
|
||||
def add_module(self, node):
|
||||
self.locals[node.name] = node
|
||||
self.modules.append(node)
|
||||
|
||||
def get_module(self, name):
|
||||
return self.locals[name]
|
||||
|
||||
def get_children(self):
|
||||
return self.modules
|
||||
|
||||
def __repr__(self):
|
||||
return "<Project %r at %s (%s modules)>" % (
|
||||
self.name,
|
||||
id(self),
|
||||
len(self.modules),
|
||||
)
|
||||
|
||||
|
||||
def project_from_files(
|
||||
files, func_wrapper=_astroid_wrapper, project_name="no name", black_list=("CVS",)
|
||||
):
|
||||
"""return a Project from a list of files or modules"""
|
||||
# build the project representation
|
||||
astroid_manager = manager.AstroidManager()
|
||||
project = Project(project_name)
|
||||
for something in files:
|
||||
if not os.path.exists(something):
|
||||
fpath = modutils.file_from_modpath(something.split("."))
|
||||
elif os.path.isdir(something):
|
||||
fpath = os.path.join(something, "__init__.py")
|
||||
else:
|
||||
fpath = something
|
||||
ast = func_wrapper(astroid_manager.ast_from_file, fpath)
|
||||
if ast is None:
|
||||
continue
|
||||
project.path = project.path or ast.file
|
||||
project.add_module(ast)
|
||||
base_name = ast.name
|
||||
# recurse in package except if __init__ was explicitly given
|
||||
if ast.package and something.find("__init__") == -1:
|
||||
# recurse on others packages / modules if this is a package
|
||||
for fpath in modutils.get_module_files(
|
||||
os.path.dirname(ast.file), black_list
|
||||
):
|
||||
ast = func_wrapper(astroid_manager.ast_from_file, fpath)
|
||||
if ast is None or ast.name == base_name:
|
||||
continue
|
||||
project.add_module(ast)
|
||||
return project
|
||||
@@ -0,0 +1,217 @@
|
||||
# Copyright (c) 2008-2010, 2012-2014 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
|
||||
# Copyright (c) 2014 Brett Cannon <brett@python.org>
|
||||
# Copyright (c) 2014 Arun Persaud <arun@nubati.net>
|
||||
# Copyright (c) 2015-2019 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
|
||||
# Copyright (c) 2016 Alexander Pervakov <frost.nzcr4@jagmort.com>
|
||||
# Copyright (c) 2018 ssolanki <sushobhitsolanki@gmail.com>
|
||||
# Copyright (c) 2019 Hugo van Kemenade <hugovk@users.noreply.github.com>
|
||||
# Copyright (c) 2019 Pierre Sassoulas <pierre.sassoulas@gmail.com>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""
|
||||
%prog [options] <packages>
|
||||
|
||||
create UML diagrams for classes and modules in <packages>
|
||||
"""
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from pylint.config import ConfigurationMixIn
|
||||
from pylint.pyreverse import writer
|
||||
from pylint.pyreverse.diadefslib import DiadefsHandler
|
||||
from pylint.pyreverse.inspector import Linker, project_from_files
|
||||
from pylint.pyreverse.utils import insert_default_options
|
||||
|
||||
OPTIONS = (
|
||||
(
|
||||
"filter-mode",
|
||||
dict(
|
||||
short="f",
|
||||
default="PUB_ONLY",
|
||||
dest="mode",
|
||||
type="string",
|
||||
action="store",
|
||||
metavar="<mode>",
|
||||
help="""filter attributes and functions according to
|
||||
<mode>. Correct modes are :
|
||||
'PUB_ONLY' filter all non public attributes
|
||||
[DEFAULT], equivalent to PRIVATE+SPECIAL_A
|
||||
'ALL' no filter
|
||||
'SPECIAL' filter Python special functions
|
||||
except constructor
|
||||
'OTHER' filter protected and private
|
||||
attributes""",
|
||||
),
|
||||
),
|
||||
(
|
||||
"class",
|
||||
dict(
|
||||
short="c",
|
||||
action="append",
|
||||
metavar="<class>",
|
||||
dest="classes",
|
||||
default=[],
|
||||
help="create a class diagram with all classes related to <class>;\
|
||||
this uses by default the options -ASmy",
|
||||
),
|
||||
),
|
||||
(
|
||||
"show-ancestors",
|
||||
dict(
|
||||
short="a",
|
||||
action="store",
|
||||
metavar="<ancestor>",
|
||||
type="int",
|
||||
help="show <ancestor> generations of ancestor classes not in <projects>",
|
||||
),
|
||||
),
|
||||
(
|
||||
"all-ancestors",
|
||||
dict(
|
||||
short="A",
|
||||
default=None,
|
||||
help="show all ancestors off all classes in <projects>",
|
||||
),
|
||||
),
|
||||
(
|
||||
"show-associated",
|
||||
dict(
|
||||
short="s",
|
||||
action="store",
|
||||
metavar="<association_level>",
|
||||
type="int",
|
||||
help="show <association_level> levels of associated classes not in <projects>",
|
||||
),
|
||||
),
|
||||
(
|
||||
"all-associated",
|
||||
dict(
|
||||
short="S",
|
||||
default=None,
|
||||
help="show recursively all associated off all associated classes",
|
||||
),
|
||||
),
|
||||
(
|
||||
"show-builtin",
|
||||
dict(
|
||||
short="b",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="include builtin objects in representation of classes",
|
||||
),
|
||||
),
|
||||
(
|
||||
"module-names",
|
||||
dict(
|
||||
short="m",
|
||||
default=None,
|
||||
type="yn",
|
||||
metavar="[yn]",
|
||||
help="include module name in representation of classes",
|
||||
),
|
||||
),
|
||||
(
|
||||
"only-classnames",
|
||||
dict(
|
||||
short="k",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="don't show attributes and methods in the class boxes; \
|
||||
this disables -f values",
|
||||
),
|
||||
),
|
||||
(
|
||||
"output",
|
||||
dict(
|
||||
short="o",
|
||||
dest="output_format",
|
||||
action="store",
|
||||
default="dot",
|
||||
metavar="<format>",
|
||||
help="create a *.<format> output file if format available.",
|
||||
),
|
||||
),
|
||||
(
|
||||
"ignore",
|
||||
{
|
||||
"type": "csv",
|
||||
"metavar": "<file[,file...]>",
|
||||
"dest": "black_list",
|
||||
"default": ("CVS",),
|
||||
"help": "Add files or directories to the blacklist. They "
|
||||
"should be base names, not paths.",
|
||||
},
|
||||
),
|
||||
(
|
||||
"project",
|
||||
{
|
||||
"default": "",
|
||||
"type": "string",
|
||||
"short": "p",
|
||||
"metavar": "<project name>",
|
||||
"help": "set the project name.",
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _check_graphviz_available(output_format):
|
||||
"""check if we need graphviz for different output format"""
|
||||
try:
|
||||
subprocess.call(["dot", "-V"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
except OSError:
|
||||
print(
|
||||
"The output format '%s' is currently not available.\n"
|
||||
"Please install 'Graphviz' to have other output formats "
|
||||
"than 'dot' or 'vcg'." % output_format
|
||||
)
|
||||
sys.exit(32)
|
||||
|
||||
|
||||
class Run(ConfigurationMixIn):
|
||||
"""base class providing common behaviour for pyreverse commands"""
|
||||
|
||||
options = OPTIONS # type: ignore
|
||||
|
||||
def __init__(self, args):
|
||||
ConfigurationMixIn.__init__(self, usage=__doc__)
|
||||
insert_default_options()
|
||||
args = self.load_command_line_configuration()
|
||||
if self.config.output_format not in ("dot", "vcg"):
|
||||
_check_graphviz_available(self.config.output_format)
|
||||
|
||||
sys.exit(self.run(args))
|
||||
|
||||
def run(self, args):
|
||||
"""checking arguments and run project"""
|
||||
if not args:
|
||||
print(self.help())
|
||||
return 1
|
||||
# insert current working directory to the python path to recognize
|
||||
# dependencies to local modules even if cwd is not in the PYTHONPATH
|
||||
sys.path.insert(0, os.getcwd())
|
||||
try:
|
||||
project = project_from_files(
|
||||
args,
|
||||
project_name=self.config.project,
|
||||
black_list=self.config.black_list,
|
||||
)
|
||||
linker = Linker(project, tag=True)
|
||||
handler = DiadefsHandler(self.config)
|
||||
diadefs = handler.get_diadefs(project, linker)
|
||||
finally:
|
||||
sys.path.pop(0)
|
||||
|
||||
if self.config.output_format == "vcg":
|
||||
writer.VCGWriter(self.config).write(diadefs)
|
||||
else:
|
||||
writer.DotWriter(self.config).write(diadefs)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
Run(sys.argv[1:])
|
||||
@@ -0,0 +1,223 @@
|
||||
# Copyright (c) 2006, 2008, 2010, 2013-2014 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
|
||||
# Copyright (c) 2014 Brett Cannon <brett@python.org>
|
||||
# Copyright (c) 2014 Arun Persaud <arun@nubati.net>
|
||||
# Copyright (c) 2015-2019 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
|
||||
# Copyright (c) 2017 hippo91 <guillaume.peillex@gmail.com>
|
||||
# Copyright (c) 2018 ssolanki <sushobhitsolanki@gmail.com>
|
||||
# Copyright (c) 2019 Hugo van Kemenade <hugovk@users.noreply.github.com>
|
||||
# Copyright (c) 2020 Anthony Sottile <asottile@umich.edu>
|
||||
# Copyright (c) 2020 bernie gray <bfgray3@users.noreply.github.com>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""
|
||||
generic classes/functions for pyreverse core/extensions
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
########### pyreverse option utils ##############################
|
||||
|
||||
|
||||
RCFILE = ".pyreverserc"
|
||||
|
||||
|
||||
def get_default_options():
|
||||
"""
|
||||
Read config file and return list of options
|
||||
"""
|
||||
options = []
|
||||
home = os.environ.get("HOME", "")
|
||||
if home:
|
||||
rcfile = os.path.join(home, RCFILE)
|
||||
try:
|
||||
options = open(rcfile).read().split()
|
||||
except OSError:
|
||||
pass # ignore if no config file found
|
||||
return options
|
||||
|
||||
|
||||
def insert_default_options():
|
||||
"""insert default options to sys.argv
|
||||
"""
|
||||
options = get_default_options()
|
||||
options.reverse()
|
||||
for arg in options:
|
||||
sys.argv.insert(1, arg)
|
||||
|
||||
|
||||
# astroid utilities ###########################################################
|
||||
|
||||
SPECIAL = re.compile(r"^__[^\W_]+\w*__$")
|
||||
PRIVATE = re.compile(r"^__\w*[^\W_]+_?$")
|
||||
PROTECTED = re.compile(r"^_\w*$")
|
||||
|
||||
|
||||
def get_visibility(name):
|
||||
"""return the visibility from a name: public, protected, private or special
|
||||
"""
|
||||
if SPECIAL.match(name):
|
||||
visibility = "special"
|
||||
elif PRIVATE.match(name):
|
||||
visibility = "private"
|
||||
elif PROTECTED.match(name):
|
||||
visibility = "protected"
|
||||
|
||||
else:
|
||||
visibility = "public"
|
||||
return visibility
|
||||
|
||||
|
||||
ABSTRACT = re.compile(r"^.*Abstract.*")
|
||||
FINAL = re.compile(r"^[^\W\da-z]*$")
|
||||
|
||||
|
||||
def is_abstract(node):
|
||||
"""return true if the given class node correspond to an abstract class
|
||||
definition
|
||||
"""
|
||||
return ABSTRACT.match(node.name)
|
||||
|
||||
|
||||
def is_final(node):
|
||||
"""return true if the given class/function node correspond to final
|
||||
definition
|
||||
"""
|
||||
return FINAL.match(node.name)
|
||||
|
||||
|
||||
def is_interface(node):
|
||||
# bw compat
|
||||
return node.type == "interface"
|
||||
|
||||
|
||||
def is_exception(node):
|
||||
# bw compat
|
||||
return node.type == "exception"
|
||||
|
||||
|
||||
# Helpers #####################################################################
|
||||
|
||||
_CONSTRUCTOR = 1
|
||||
_SPECIAL = 2
|
||||
_PROTECTED = 4
|
||||
_PRIVATE = 8
|
||||
MODES = {
|
||||
"ALL": 0,
|
||||
"PUB_ONLY": _SPECIAL + _PROTECTED + _PRIVATE,
|
||||
"SPECIAL": _SPECIAL,
|
||||
"OTHER": _PROTECTED + _PRIVATE,
|
||||
}
|
||||
VIS_MOD = {
|
||||
"special": _SPECIAL,
|
||||
"protected": _PROTECTED,
|
||||
"private": _PRIVATE,
|
||||
"public": 0,
|
||||
}
|
||||
|
||||
|
||||
class FilterMixIn:
|
||||
"""filter nodes according to a mode and nodes' visibility
|
||||
"""
|
||||
|
||||
def __init__(self, mode):
|
||||
"init filter modes"
|
||||
__mode = 0
|
||||
for nummod in mode.split("+"):
|
||||
try:
|
||||
__mode += MODES[nummod]
|
||||
except KeyError as ex:
|
||||
print("Unknown filter mode %s" % ex, file=sys.stderr)
|
||||
self.__mode = __mode
|
||||
|
||||
def show_attr(self, node):
|
||||
"""return true if the node should be treated
|
||||
"""
|
||||
visibility = get_visibility(getattr(node, "name", node))
|
||||
return not self.__mode & VIS_MOD[visibility]
|
||||
|
||||
|
||||
class ASTWalker:
|
||||
"""a walker visiting a tree in preorder, calling on the handler:
|
||||
|
||||
* visit_<class name> on entering a node, where class name is the class of
|
||||
the node in lower case
|
||||
|
||||
* leave_<class name> on leaving a node, where class name is the class of
|
||||
the node in lower case
|
||||
"""
|
||||
|
||||
def __init__(self, handler):
|
||||
self.handler = handler
|
||||
self._cache = {}
|
||||
|
||||
def walk(self, node, _done=None):
|
||||
"""walk on the tree from <node>, getting callbacks from handler"""
|
||||
if _done is None:
|
||||
_done = set()
|
||||
if node in _done:
|
||||
raise AssertionError((id(node), node, node.parent))
|
||||
_done.add(node)
|
||||
self.visit(node)
|
||||
for child_node in node.get_children():
|
||||
assert child_node is not node
|
||||
self.walk(child_node, _done)
|
||||
self.leave(node)
|
||||
assert node.parent is not node
|
||||
|
||||
def get_callbacks(self, node):
|
||||
"""get callbacks from handler for the visited node"""
|
||||
klass = node.__class__
|
||||
methods = self._cache.get(klass)
|
||||
if methods is None:
|
||||
handler = self.handler
|
||||
kid = klass.__name__.lower()
|
||||
e_method = getattr(
|
||||
handler, "visit_%s" % kid, getattr(handler, "visit_default", None)
|
||||
)
|
||||
l_method = getattr(
|
||||
handler, "leave_%s" % kid, getattr(handler, "leave_default", None)
|
||||
)
|
||||
self._cache[klass] = (e_method, l_method)
|
||||
else:
|
||||
e_method, l_method = methods
|
||||
return e_method, l_method
|
||||
|
||||
def visit(self, node):
|
||||
"""walk on the tree from <node>, getting callbacks from handler"""
|
||||
method = self.get_callbacks(node)[0]
|
||||
if method is not None:
|
||||
method(node)
|
||||
|
||||
def leave(self, node):
|
||||
"""walk on the tree from <node>, getting callbacks from handler"""
|
||||
method = self.get_callbacks(node)[1]
|
||||
if method is not None:
|
||||
method(node)
|
||||
|
||||
|
||||
class LocalsVisitor(ASTWalker):
|
||||
"""visit a project by traversing the locals dictionary"""
|
||||
|
||||
def __init__(self):
|
||||
ASTWalker.__init__(self, self)
|
||||
self._visited = set()
|
||||
|
||||
def visit(self, node):
|
||||
"""launch the visit starting from the given node"""
|
||||
if node in self._visited:
|
||||
return None
|
||||
|
||||
self._visited.add(node)
|
||||
methods = self.get_callbacks(node)
|
||||
if methods[0] is not None:
|
||||
methods[0](node)
|
||||
if hasattr(node, "locals"): # skip Instance and other proxy
|
||||
for local_node in node.values():
|
||||
self.visit(local_node)
|
||||
if methods[1] is not None:
|
||||
return methods[1](node)
|
||||
return None
|
||||
@@ -0,0 +1,229 @@
|
||||
# Copyright (c) 2015-2018 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2015 Florian Bruhin <me@the-compiler.org>
|
||||
# Copyright (c) 2018 ssolanki <sushobhitsolanki@gmail.com>
|
||||
# Copyright (c) 2020 Anthony Sottile <asottile@umich.edu>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""Functions to generate files readable with Georg Sander's vcg
|
||||
(Visualization of Compiler Graphs).
|
||||
|
||||
You can download vcg at http://rw4.cs.uni-sb.de/~sander/html/gshome.html
|
||||
Note that vcg exists as a debian package.
|
||||
|
||||
See vcg's documentation for explanation about the different values that
|
||||
maybe used for the functions parameters.
|
||||
"""
|
||||
|
||||
ATTRS_VAL = {
|
||||
"algos": (
|
||||
"dfs",
|
||||
"tree",
|
||||
"minbackward",
|
||||
"left_to_right",
|
||||
"right_to_left",
|
||||
"top_to_bottom",
|
||||
"bottom_to_top",
|
||||
"maxdepth",
|
||||
"maxdepthslow",
|
||||
"mindepth",
|
||||
"mindepthslow",
|
||||
"mindegree",
|
||||
"minindegree",
|
||||
"minoutdegree",
|
||||
"maxdegree",
|
||||
"maxindegree",
|
||||
"maxoutdegree",
|
||||
),
|
||||
"booleans": ("yes", "no"),
|
||||
"colors": (
|
||||
"black",
|
||||
"white",
|
||||
"blue",
|
||||
"red",
|
||||
"green",
|
||||
"yellow",
|
||||
"magenta",
|
||||
"lightgrey",
|
||||
"cyan",
|
||||
"darkgrey",
|
||||
"darkblue",
|
||||
"darkred",
|
||||
"darkgreen",
|
||||
"darkyellow",
|
||||
"darkmagenta",
|
||||
"darkcyan",
|
||||
"gold",
|
||||
"lightblue",
|
||||
"lightred",
|
||||
"lightgreen",
|
||||
"lightyellow",
|
||||
"lightmagenta",
|
||||
"lightcyan",
|
||||
"lilac",
|
||||
"turquoise",
|
||||
"aquamarine",
|
||||
"khaki",
|
||||
"purple",
|
||||
"yellowgreen",
|
||||
"pink",
|
||||
"orange",
|
||||
"orchid",
|
||||
),
|
||||
"shapes": ("box", "ellipse", "rhomb", "triangle"),
|
||||
"textmodes": ("center", "left_justify", "right_justify"),
|
||||
"arrowstyles": ("solid", "line", "none"),
|
||||
"linestyles": ("continuous", "dashed", "dotted", "invisible"),
|
||||
}
|
||||
|
||||
# meaning of possible values:
|
||||
# O -> string
|
||||
# 1 -> int
|
||||
# list -> value in list
|
||||
GRAPH_ATTRS = {
|
||||
"title": 0,
|
||||
"label": 0,
|
||||
"color": ATTRS_VAL["colors"],
|
||||
"textcolor": ATTRS_VAL["colors"],
|
||||
"bordercolor": ATTRS_VAL["colors"],
|
||||
"width": 1,
|
||||
"height": 1,
|
||||
"borderwidth": 1,
|
||||
"textmode": ATTRS_VAL["textmodes"],
|
||||
"shape": ATTRS_VAL["shapes"],
|
||||
"shrink": 1,
|
||||
"stretch": 1,
|
||||
"orientation": ATTRS_VAL["algos"],
|
||||
"vertical_order": 1,
|
||||
"horizontal_order": 1,
|
||||
"xspace": 1,
|
||||
"yspace": 1,
|
||||
"layoutalgorithm": ATTRS_VAL["algos"],
|
||||
"late_edge_labels": ATTRS_VAL["booleans"],
|
||||
"display_edge_labels": ATTRS_VAL["booleans"],
|
||||
"dirty_edge_labels": ATTRS_VAL["booleans"],
|
||||
"finetuning": ATTRS_VAL["booleans"],
|
||||
"manhattan_edges": ATTRS_VAL["booleans"],
|
||||
"smanhattan_edges": ATTRS_VAL["booleans"],
|
||||
"port_sharing": ATTRS_VAL["booleans"],
|
||||
"edges": ATTRS_VAL["booleans"],
|
||||
"nodes": ATTRS_VAL["booleans"],
|
||||
"splines": ATTRS_VAL["booleans"],
|
||||
}
|
||||
NODE_ATTRS = {
|
||||
"title": 0,
|
||||
"label": 0,
|
||||
"color": ATTRS_VAL["colors"],
|
||||
"textcolor": ATTRS_VAL["colors"],
|
||||
"bordercolor": ATTRS_VAL["colors"],
|
||||
"width": 1,
|
||||
"height": 1,
|
||||
"borderwidth": 1,
|
||||
"textmode": ATTRS_VAL["textmodes"],
|
||||
"shape": ATTRS_VAL["shapes"],
|
||||
"shrink": 1,
|
||||
"stretch": 1,
|
||||
"vertical_order": 1,
|
||||
"horizontal_order": 1,
|
||||
}
|
||||
EDGE_ATTRS = {
|
||||
"sourcename": 0,
|
||||
"targetname": 0,
|
||||
"label": 0,
|
||||
"linestyle": ATTRS_VAL["linestyles"],
|
||||
"class": 1,
|
||||
"thickness": 0,
|
||||
"color": ATTRS_VAL["colors"],
|
||||
"textcolor": ATTRS_VAL["colors"],
|
||||
"arrowcolor": ATTRS_VAL["colors"],
|
||||
"backarrowcolor": ATTRS_VAL["colors"],
|
||||
"arrowsize": 1,
|
||||
"backarrowsize": 1,
|
||||
"arrowstyle": ATTRS_VAL["arrowstyles"],
|
||||
"backarrowstyle": ATTRS_VAL["arrowstyles"],
|
||||
"textmode": ATTRS_VAL["textmodes"],
|
||||
"priority": 1,
|
||||
"anchor": 1,
|
||||
"horizontal_order": 1,
|
||||
}
|
||||
|
||||
|
||||
# Misc utilities ###############################################################
|
||||
|
||||
|
||||
class VCGPrinter:
|
||||
"""A vcg graph writer.
|
||||
"""
|
||||
|
||||
def __init__(self, output_stream):
|
||||
self._stream = output_stream
|
||||
self._indent = ""
|
||||
|
||||
def open_graph(self, **args):
|
||||
"""open a vcg graph
|
||||
"""
|
||||
self._stream.write("%sgraph:{\n" % self._indent)
|
||||
self._inc_indent()
|
||||
self._write_attributes(GRAPH_ATTRS, **args)
|
||||
|
||||
def close_graph(self):
|
||||
"""close a vcg graph
|
||||
"""
|
||||
self._dec_indent()
|
||||
self._stream.write("%s}\n" % self._indent)
|
||||
|
||||
def node(self, title, **args):
|
||||
"""draw a node
|
||||
"""
|
||||
self._stream.write('%snode: {title:"%s"' % (self._indent, title))
|
||||
self._write_attributes(NODE_ATTRS, **args)
|
||||
self._stream.write("}\n")
|
||||
|
||||
def edge(self, from_node, to_node, edge_type="", **args):
|
||||
"""draw an edge from a node to another.
|
||||
"""
|
||||
self._stream.write(
|
||||
'%s%sedge: {sourcename:"%s" targetname:"%s"'
|
||||
% (self._indent, edge_type, from_node, to_node)
|
||||
)
|
||||
self._write_attributes(EDGE_ATTRS, **args)
|
||||
self._stream.write("}\n")
|
||||
|
||||
# private ##################################################################
|
||||
|
||||
def _write_attributes(self, attributes_dict, **args):
|
||||
"""write graph, node or edge attributes
|
||||
"""
|
||||
for key, value in args.items():
|
||||
try:
|
||||
_type = attributes_dict[key]
|
||||
except KeyError:
|
||||
raise Exception(
|
||||
"""no such attribute %s
|
||||
possible attributes are %s"""
|
||||
% (key, attributes_dict.keys())
|
||||
)
|
||||
|
||||
if not _type:
|
||||
self._stream.write('%s%s:"%s"\n' % (self._indent, key, value))
|
||||
elif _type == 1:
|
||||
self._stream.write("%s%s:%s\n" % (self._indent, key, int(value)))
|
||||
elif value in _type:
|
||||
self._stream.write("%s%s:%s\n" % (self._indent, key, value))
|
||||
else:
|
||||
raise Exception(
|
||||
"""value %s isn\'t correct for attribute %s
|
||||
correct values are %s"""
|
||||
% (value, key, _type)
|
||||
)
|
||||
|
||||
def _inc_indent(self):
|
||||
"""increment indentation
|
||||
"""
|
||||
self._indent = " %s" % self._indent
|
||||
|
||||
def _dec_indent(self):
|
||||
"""decrement indentation
|
||||
"""
|
||||
self._indent = self._indent[:-2]
|
||||
@@ -0,0 +1,217 @@
|
||||
# Copyright (c) 2008-2010, 2013-2014 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
|
||||
# Copyright (c) 2014 Arun Persaud <arun@nubati.net>
|
||||
# Copyright (c) 2015-2018, 2020 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2015 Mike Frysinger <vapier@gentoo.org>
|
||||
# Copyright (c) 2015 Florian Bruhin <me@the-compiler.org>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
|
||||
# Copyright (c) 2018, 2020 Anthony Sottile <asottile@umich.edu>
|
||||
# Copyright (c) 2018 ssolanki <sushobhitsolanki@gmail.com>
|
||||
# Copyright (c) 2019 Pierre Sassoulas <pierre.sassoulas@gmail.com>
|
||||
# Copyright (c) 2019 Kylian <development@goudcode.nl>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""Utilities for creating VCG and Dot diagrams"""
|
||||
|
||||
from pylint.graph import DotBackend
|
||||
from pylint.pyreverse.utils import is_exception
|
||||
from pylint.pyreverse.vcgutils import VCGPrinter
|
||||
|
||||
|
||||
class DiagramWriter:
|
||||
"""base class for writing project diagrams
|
||||
"""
|
||||
|
||||
def __init__(self, config, styles):
|
||||
self.config = config
|
||||
self.pkg_edges, self.inh_edges, self.imp_edges, self.association_edges = styles
|
||||
self.printer = None # defined in set_printer
|
||||
|
||||
def write(self, diadefs):
|
||||
"""write files for <project> according to <diadefs>
|
||||
"""
|
||||
for diagram in diadefs:
|
||||
basename = diagram.title.strip().replace(" ", "_")
|
||||
file_name = "%s.%s" % (basename, self.config.output_format)
|
||||
self.set_printer(file_name, basename)
|
||||
if diagram.TYPE == "class":
|
||||
self.write_classes(diagram)
|
||||
else:
|
||||
self.write_packages(diagram)
|
||||
self.close_graph()
|
||||
|
||||
def write_packages(self, diagram):
|
||||
"""write a package diagram"""
|
||||
# sorted to get predictable (hence testable) results
|
||||
for i, obj in enumerate(sorted(diagram.modules(), key=lambda x: x.title)):
|
||||
self.printer.emit_node(i, label=self.get_title(obj), shape="box")
|
||||
obj.fig_id = i
|
||||
# package dependencies
|
||||
for rel in diagram.get_relationships("depends"):
|
||||
self.printer.emit_edge(
|
||||
rel.from_object.fig_id, rel.to_object.fig_id, **self.pkg_edges
|
||||
)
|
||||
|
||||
def write_classes(self, diagram):
|
||||
"""write a class diagram"""
|
||||
# sorted to get predictable (hence testable) results
|
||||
for i, obj in enumerate(sorted(diagram.objects, key=lambda x: x.title)):
|
||||
self.printer.emit_node(i, **self.get_values(obj))
|
||||
obj.fig_id = i
|
||||
# inheritance links
|
||||
for rel in diagram.get_relationships("specialization"):
|
||||
self.printer.emit_edge(
|
||||
rel.from_object.fig_id, rel.to_object.fig_id, **self.inh_edges
|
||||
)
|
||||
# implementation links
|
||||
for rel in diagram.get_relationships("implements"):
|
||||
self.printer.emit_edge(
|
||||
rel.from_object.fig_id, rel.to_object.fig_id, **self.imp_edges
|
||||
)
|
||||
# generate associations
|
||||
for rel in diagram.get_relationships("association"):
|
||||
self.printer.emit_edge(
|
||||
rel.from_object.fig_id,
|
||||
rel.to_object.fig_id,
|
||||
label=rel.name,
|
||||
**self.association_edges
|
||||
)
|
||||
|
||||
def set_printer(self, file_name, basename):
|
||||
"""set printer"""
|
||||
raise NotImplementedError
|
||||
|
||||
def get_title(self, obj):
|
||||
"""get project title"""
|
||||
raise NotImplementedError
|
||||
|
||||
def get_values(self, obj):
|
||||
"""get label and shape for classes."""
|
||||
raise NotImplementedError
|
||||
|
||||
def close_graph(self):
|
||||
"""finalize the graph"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class DotWriter(DiagramWriter):
|
||||
"""write dot graphs from a diagram definition and a project
|
||||
"""
|
||||
|
||||
def __init__(self, config):
|
||||
styles = [
|
||||
dict(arrowtail="none", arrowhead="open"),
|
||||
dict(arrowtail="none", arrowhead="empty"),
|
||||
dict(arrowtail="node", arrowhead="empty", style="dashed"),
|
||||
dict(
|
||||
fontcolor="green", arrowtail="none", arrowhead="diamond", style="solid"
|
||||
),
|
||||
]
|
||||
DiagramWriter.__init__(self, config, styles)
|
||||
|
||||
def set_printer(self, file_name, basename):
|
||||
"""initialize DotWriter and add options for layout.
|
||||
"""
|
||||
layout = dict(rankdir="BT")
|
||||
self.printer = DotBackend(basename, additional_param=layout)
|
||||
self.file_name = file_name
|
||||
|
||||
def get_title(self, obj):
|
||||
"""get project title"""
|
||||
return obj.title
|
||||
|
||||
def get_values(self, obj):
|
||||
"""get label and shape for classes.
|
||||
|
||||
The label contains all attributes and methods
|
||||
"""
|
||||
label = obj.title
|
||||
if obj.shape == "interface":
|
||||
label = "«interface»\\n%s" % label
|
||||
if not self.config.only_classnames:
|
||||
label = r"%s|%s\l|" % (label, r"\l".join(obj.attrs))
|
||||
for func in obj.methods:
|
||||
if func.args.args:
|
||||
args = [arg.name for arg in func.args.args if arg.name != "self"]
|
||||
else:
|
||||
args = []
|
||||
label = r"%s%s(%s)\l" % (label, func.name, ", ".join(args))
|
||||
label = "{%s}" % label
|
||||
if is_exception(obj.node):
|
||||
return dict(fontcolor="red", label=label, shape="record")
|
||||
return dict(label=label, shape="record")
|
||||
|
||||
def close_graph(self):
|
||||
"""print the dot graph into <file_name>"""
|
||||
self.printer.generate(self.file_name)
|
||||
|
||||
|
||||
class VCGWriter(DiagramWriter):
|
||||
"""write vcg graphs from a diagram definition and a project
|
||||
"""
|
||||
|
||||
def __init__(self, config):
|
||||
styles = [
|
||||
dict(arrowstyle="solid", backarrowstyle="none", backarrowsize=0),
|
||||
dict(arrowstyle="solid", backarrowstyle="none", backarrowsize=10),
|
||||
dict(
|
||||
arrowstyle="solid",
|
||||
backarrowstyle="none",
|
||||
linestyle="dotted",
|
||||
backarrowsize=10,
|
||||
),
|
||||
dict(arrowstyle="solid", backarrowstyle="none", textcolor="green"),
|
||||
]
|
||||
DiagramWriter.__init__(self, config, styles)
|
||||
|
||||
def set_printer(self, file_name, basename):
|
||||
"""initialize VCGWriter for a UML graph"""
|
||||
self.graph_file = open(file_name, "w+")
|
||||
self.printer = VCGPrinter(self.graph_file)
|
||||
self.printer.open_graph(
|
||||
title=basename,
|
||||
layoutalgorithm="dfs",
|
||||
late_edge_labels="yes",
|
||||
port_sharing="no",
|
||||
manhattan_edges="yes",
|
||||
)
|
||||
self.printer.emit_node = self.printer.node
|
||||
self.printer.emit_edge = self.printer.edge
|
||||
|
||||
def get_title(self, obj):
|
||||
"""get project title in vcg format"""
|
||||
return r"\fb%s\fn" % obj.title
|
||||
|
||||
def get_values(self, obj):
|
||||
"""get label and shape for classes.
|
||||
|
||||
The label contains all attributes and methods
|
||||
"""
|
||||
if is_exception(obj.node):
|
||||
label = r"\fb\f09%s\fn" % obj.title
|
||||
else:
|
||||
label = r"\fb%s\fn" % obj.title
|
||||
if obj.shape == "interface":
|
||||
shape = "ellipse"
|
||||
else:
|
||||
shape = "box"
|
||||
if not self.config.only_classnames:
|
||||
attrs = obj.attrs
|
||||
methods = [func.name for func in obj.methods]
|
||||
# box width for UML like diagram
|
||||
maxlen = max(len(name) for name in [obj.title] + methods + attrs)
|
||||
line = "_" * (maxlen + 2)
|
||||
label = r"%s\n\f%s" % (label, line)
|
||||
for attr in attrs:
|
||||
label = r"%s\n\f08%s" % (label, attr)
|
||||
if attrs:
|
||||
label = r"%s\n\f%s" % (label, line)
|
||||
for func in methods:
|
||||
label = r"%s\n\f10%s()" % (label, func)
|
||||
return dict(label=label, shape=shape)
|
||||
|
||||
def close_graph(self):
|
||||
"""close graph and file"""
|
||||
self.printer.close_graph()
|
||||
self.graph_file.close()
|
||||
@@ -0,0 +1,36 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2006, 2010, 2012-2014 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
|
||||
# Copyright (c) 2012-2014 Google, Inc.
|
||||
# Copyright (c) 2012 FELD Boris <lothiraldan@gmail.com>
|
||||
# Copyright (c) 2014-2019 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2014 Brett Cannon <brett@python.org>
|
||||
# Copyright (c) 2014 Ricardo Gemignani <ricardo.gemignani@gmail.com>
|
||||
# Copyright (c) 2014 Arun Persaud <arun@nubati.net>
|
||||
# Copyright (c) 2015 Simu Toni <simutoni@gmail.com>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
|
||||
# Copyright (c) 2017 Kári Tristan Helgason <kthelgason@gmail.com>
|
||||
# Copyright (c) 2018 ssolanki <sushobhitsolanki@gmail.com>
|
||||
# Copyright (c) 2018 Sushobhit <31987769+sushobhit27@users.noreply.github.com>
|
||||
# Copyright (c) 2018 Ville Skyttä <ville.skytta@iki.fi>
|
||||
# Copyright (c) 2019 Pierre Sassoulas <pierre.sassoulas@gmail.com>
|
||||
# Copyright (c) 2020 Anthony Sottile <asottile@umich.edu>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""utilities methods and classes for reporters"""
|
||||
|
||||
|
||||
from pylint import utils
|
||||
from pylint.reporters.base_reporter import BaseReporter
|
||||
from pylint.reporters.collecting_reporter import CollectingReporter
|
||||
from pylint.reporters.json_reporter import JSONReporter
|
||||
from pylint.reporters.reports_handler_mix_in import ReportsHandlerMixIn
|
||||
|
||||
|
||||
def initialize(linter):
|
||||
"""initialize linter with reporters in this package """
|
||||
utils.register_plugins(linter, __path__[0])
|
||||
|
||||
|
||||
__all__ = ["BaseReporter", "ReportsHandlerMixIn", "JSONReporter", "CollectingReporter"]
|
||||
@@ -0,0 +1,64 @@
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
class BaseReporter:
|
||||
"""base class for reporters
|
||||
|
||||
symbols: show short symbolic names for messages.
|
||||
"""
|
||||
|
||||
extension = ""
|
||||
|
||||
def __init__(self, output=None):
|
||||
self.linter = None
|
||||
self.section = 0
|
||||
self.out = None
|
||||
self.out_encoding = None
|
||||
self.set_output(output)
|
||||
# Build the path prefix to strip to get relative paths
|
||||
self.path_strip_prefix = os.getcwd() + os.sep
|
||||
|
||||
def handle_message(self, msg):
|
||||
"""Handle a new message triggered on the current file."""
|
||||
|
||||
def set_output(self, output=None):
|
||||
"""set output stream"""
|
||||
self.out = output or sys.stdout
|
||||
|
||||
def writeln(self, string=""):
|
||||
"""write a line in the output buffer"""
|
||||
print(string, file=self.out)
|
||||
|
||||
def display_reports(self, layout):
|
||||
"""display results encapsulated in the layout tree"""
|
||||
self.section = 0
|
||||
if hasattr(layout, "report_id"):
|
||||
layout.children[0].children[0].data += " (%s)" % layout.report_id
|
||||
self._display(layout)
|
||||
|
||||
def _display(self, layout):
|
||||
"""display the layout"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def display_messages(self, layout):
|
||||
"""Hook for displaying the messages of the reporter
|
||||
|
||||
This will be called whenever the underlying messages
|
||||
needs to be displayed. For some reporters, it probably
|
||||
doesn't make sense to display messages as soon as they
|
||||
are available, so some mechanism of storing them could be used.
|
||||
This method can be implemented to display them after they've
|
||||
been aggregated.
|
||||
"""
|
||||
|
||||
# Event callbacks
|
||||
|
||||
def on_set_current_module(self, module, filepath):
|
||||
"""Hook called when a module starts to be analysed."""
|
||||
|
||||
def on_close(self, stats, previous_stats):
|
||||
"""Hook called when a module finished analyzing."""
|
||||
@@ -0,0 +1,19 @@
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
from pylint.reporters.base_reporter import BaseReporter
|
||||
|
||||
|
||||
class CollectingReporter(BaseReporter):
|
||||
"""collects messages"""
|
||||
|
||||
name = "collector"
|
||||
|
||||
def __init__(self):
|
||||
BaseReporter.__init__(self)
|
||||
self.messages = []
|
||||
|
||||
def handle_message(self, msg):
|
||||
self.messages.append(msg)
|
||||
|
||||
_display = None
|
||||
@@ -0,0 +1,61 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2014 Vlad Temian <vladtemian@gmail.com>
|
||||
# Copyright (c) 2015-2019 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
|
||||
# Copyright (c) 2017 guillaume2 <guillaume.peillex@gmail.col>
|
||||
# Copyright (c) 2019-2020 Pierre Sassoulas <pierre.sassoulas@gmail.com>
|
||||
# Copyright (c) 2019 Hugo van Kemenade <hugovk@users.noreply.github.com>
|
||||
# Copyright (c) 2020 Clément Pit-Claudel <cpitclaudel@users.noreply.github.com>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""JSON reporter"""
|
||||
import json
|
||||
import sys
|
||||
|
||||
from pylint.interfaces import IReporter
|
||||
from pylint.reporters.base_reporter import BaseReporter
|
||||
|
||||
|
||||
class JSONReporter(BaseReporter):
|
||||
"""Report messages and layouts in JSON."""
|
||||
|
||||
__implements__ = IReporter
|
||||
name = "json"
|
||||
extension = "json"
|
||||
|
||||
def __init__(self, output=None):
|
||||
BaseReporter.__init__(self, output or sys.stdout)
|
||||
self.messages = []
|
||||
|
||||
def handle_message(self, msg):
|
||||
"""Manage message of different type and in the context of path."""
|
||||
self.messages.append(
|
||||
{
|
||||
"type": msg.category,
|
||||
"module": msg.module,
|
||||
"obj": msg.obj,
|
||||
"line": msg.line,
|
||||
"column": msg.column,
|
||||
"path": msg.path,
|
||||
"symbol": msg.symbol,
|
||||
"message": msg.msg or "",
|
||||
"message-id": msg.msg_id,
|
||||
}
|
||||
)
|
||||
|
||||
def display_messages(self, layout):
|
||||
"""Launch layouts display"""
|
||||
print(json.dumps(self.messages, indent=4), file=self.out)
|
||||
|
||||
def display_reports(self, layout):
|
||||
"""Don't do anything in this reporter."""
|
||||
|
||||
def _display(self, layout):
|
||||
"""Do nothing."""
|
||||
|
||||
|
||||
def register(linter):
|
||||
"""Register the reporter classes with the linter."""
|
||||
linter.register_reporter(JSONReporter)
|
||||
@@ -0,0 +1,77 @@
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
import collections
|
||||
|
||||
from pylint.exceptions import EmptyReportError
|
||||
from pylint.reporters.ureports.nodes import Section
|
||||
|
||||
|
||||
class ReportsHandlerMixIn:
|
||||
"""a mix-in class containing all the reports and stats manipulation
|
||||
related methods for the main lint class
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._reports = collections.defaultdict(list)
|
||||
self._reports_state = {}
|
||||
|
||||
def report_order(self):
|
||||
""" Return a list of reports, sorted in the order
|
||||
in which they must be called.
|
||||
"""
|
||||
return list(self._reports)
|
||||
|
||||
def register_report(self, reportid, r_title, r_cb, checker):
|
||||
"""register a report
|
||||
|
||||
reportid is the unique identifier for the report
|
||||
r_title the report's title
|
||||
r_cb the method to call to make the report
|
||||
checker is the checker defining the report
|
||||
"""
|
||||
reportid = reportid.upper()
|
||||
self._reports[checker].append((reportid, r_title, r_cb))
|
||||
|
||||
def enable_report(self, reportid):
|
||||
"""disable the report of the given id"""
|
||||
reportid = reportid.upper()
|
||||
self._reports_state[reportid] = True
|
||||
|
||||
def disable_report(self, reportid):
|
||||
"""disable the report of the given id"""
|
||||
reportid = reportid.upper()
|
||||
self._reports_state[reportid] = False
|
||||
|
||||
def report_is_enabled(self, reportid):
|
||||
"""return true if the report associated to the given identifier is
|
||||
enabled
|
||||
"""
|
||||
return self._reports_state.get(reportid, True)
|
||||
|
||||
def make_reports(self, stats, old_stats):
|
||||
"""render registered reports"""
|
||||
sect = Section("Report", "%s statements analysed." % (self.stats["statement"]))
|
||||
for checker in self.report_order():
|
||||
for reportid, r_title, r_cb in self._reports[checker]:
|
||||
if not self.report_is_enabled(reportid):
|
||||
continue
|
||||
report_sect = Section(r_title)
|
||||
try:
|
||||
r_cb(report_sect, stats, old_stats)
|
||||
except EmptyReportError:
|
||||
continue
|
||||
report_sect.report_id = reportid
|
||||
sect.append(report_sect)
|
||||
return sect
|
||||
|
||||
def add_stats(self, **kwargs):
|
||||
"""add some stats entries to the statistic dictionary
|
||||
raise an AssertionError if there is a key conflict
|
||||
"""
|
||||
for key, value in kwargs.items():
|
||||
if key[-1] == "_":
|
||||
key = key[:-1]
|
||||
assert key not in self.stats
|
||||
self.stats[key] = value
|
||||
return self.stats
|
||||
@@ -0,0 +1,249 @@
|
||||
# Copyright (c) 2006-2007, 2010-2014 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
|
||||
# Copyright (c) 2012-2014 Google, Inc.
|
||||
# Copyright (c) 2014 Brett Cannon <brett@python.org>
|
||||
# Copyright (c) 2014 Arun Persaud <arun@nubati.net>
|
||||
# Copyright (c) 2015-2018 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2015 Florian Bruhin <me@the-compiler.org>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
|
||||
# Copyright (c) 2016 y2kbugger <y2kbugger@users.noreply.github.com>
|
||||
# Copyright (c) 2018-2019 Nick Drozd <nicholasdrozd@gmail.com>
|
||||
# Copyright (c) 2018 Sushobhit <31987769+sushobhit27@users.noreply.github.com>
|
||||
# Copyright (c) 2018 Jace Browning <jacebrowning@gmail.com>
|
||||
# Copyright (c) 2019-2020 Pierre Sassoulas <pierre.sassoulas@gmail.com>
|
||||
# Copyright (c) 2019 Hugo van Kemenade <hugovk@users.noreply.github.com>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""Plain text reporters:
|
||||
|
||||
:text: the default one grouping messages by module
|
||||
:colorized: an ANSI colorized text reporter
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import warnings
|
||||
|
||||
from pylint import utils
|
||||
from pylint.interfaces import IReporter
|
||||
from pylint.reporters import BaseReporter
|
||||
from pylint.reporters.ureports.text_writer import TextWriter
|
||||
|
||||
TITLE_UNDERLINES = ["", "=", "-", "."]
|
||||
|
||||
ANSI_PREFIX = "\033["
|
||||
ANSI_END = "m"
|
||||
ANSI_RESET = "\033[0m"
|
||||
ANSI_STYLES = {
|
||||
"reset": "0",
|
||||
"bold": "1",
|
||||
"italic": "3",
|
||||
"underline": "4",
|
||||
"blink": "5",
|
||||
"inverse": "7",
|
||||
"strike": "9",
|
||||
}
|
||||
ANSI_COLORS = {
|
||||
"reset": "0",
|
||||
"black": "30",
|
||||
"red": "31",
|
||||
"green": "32",
|
||||
"yellow": "33",
|
||||
"blue": "34",
|
||||
"magenta": "35",
|
||||
"cyan": "36",
|
||||
"white": "37",
|
||||
}
|
||||
|
||||
|
||||
def _get_ansi_code(color=None, style=None):
|
||||
"""return ansi escape code corresponding to color and style
|
||||
|
||||
:type color: str or None
|
||||
:param color:
|
||||
the color name (see `ANSI_COLORS` for available values)
|
||||
or the color number when 256 colors are available
|
||||
|
||||
:type style: str or None
|
||||
:param style:
|
||||
style string (see `ANSI_COLORS` for available values). To get
|
||||
several style effects at the same time, use a coma as separator.
|
||||
|
||||
:raise KeyError: if an unexistent color or style identifier is given
|
||||
|
||||
:rtype: str
|
||||
:return: the built escape code
|
||||
"""
|
||||
ansi_code = []
|
||||
if style:
|
||||
style_attrs = utils._splitstrip(style)
|
||||
for effect in style_attrs:
|
||||
ansi_code.append(ANSI_STYLES[effect])
|
||||
if color:
|
||||
if color.isdigit():
|
||||
ansi_code.extend(["38", "5"])
|
||||
ansi_code.append(color)
|
||||
else:
|
||||
ansi_code.append(ANSI_COLORS[color])
|
||||
if ansi_code:
|
||||
return ANSI_PREFIX + ";".join(ansi_code) + ANSI_END
|
||||
return ""
|
||||
|
||||
|
||||
def colorize_ansi(msg, color=None, style=None):
|
||||
"""colorize message by wrapping it with ansi escape codes
|
||||
|
||||
:type msg: str or unicode
|
||||
:param msg: the message string to colorize
|
||||
|
||||
:type color: str or None
|
||||
:param color:
|
||||
the color identifier (see `ANSI_COLORS` for available values)
|
||||
|
||||
:type style: str or None
|
||||
:param style:
|
||||
style string (see `ANSI_COLORS` for available values). To get
|
||||
several style effects at the same time, use a coma as separator.
|
||||
|
||||
:raise KeyError: if an unexistent color or style identifier is given
|
||||
|
||||
:rtype: str or unicode
|
||||
:return: the ansi escaped string
|
||||
"""
|
||||
# If both color and style are not defined, then leave the text as is
|
||||
if color is None and style is None:
|
||||
return msg
|
||||
escape_code = _get_ansi_code(color, style)
|
||||
# If invalid (or unknown) color, don't wrap msg with ansi codes
|
||||
if escape_code:
|
||||
return "%s%s%s" % (escape_code, msg, ANSI_RESET)
|
||||
return msg
|
||||
|
||||
|
||||
class TextReporter(BaseReporter):
|
||||
"""Reports messages and layouts in plain text"""
|
||||
|
||||
__implements__ = IReporter
|
||||
name = "text"
|
||||
extension = "txt"
|
||||
line_format = "{path}:{line}:{column}: {msg_id}: {msg} ({symbol})"
|
||||
|
||||
def __init__(self, output=None):
|
||||
BaseReporter.__init__(self, output)
|
||||
self._modules = set()
|
||||
self._template = None
|
||||
|
||||
def on_set_current_module(self, module, filepath):
|
||||
self._template = str(self.linter.config.msg_template or self.line_format)
|
||||
|
||||
def write_message(self, msg):
|
||||
"""Convenience method to write a formatted message with class default template"""
|
||||
self.writeln(msg.format(self._template))
|
||||
|
||||
def handle_message(self, msg):
|
||||
"""manage message of different type and in the context of path"""
|
||||
if msg.module not in self._modules:
|
||||
if msg.module:
|
||||
self.writeln("************* Module %s" % msg.module)
|
||||
self._modules.add(msg.module)
|
||||
else:
|
||||
self.writeln("************* ")
|
||||
self.write_message(msg)
|
||||
|
||||
def _display(self, layout):
|
||||
"""launch layouts display"""
|
||||
print(file=self.out)
|
||||
TextWriter().format(layout, self.out)
|
||||
|
||||
|
||||
class ParseableTextReporter(TextReporter):
|
||||
"""a reporter very similar to TextReporter, but display messages in a form
|
||||
recognized by most text editors :
|
||||
|
||||
<filename>:<linenum>:<msg>
|
||||
"""
|
||||
|
||||
name = "parseable"
|
||||
line_format = "{path}:{line}: [{msg_id}({symbol}), {obj}] {msg}"
|
||||
|
||||
def __init__(self, output=None):
|
||||
warnings.warn(
|
||||
"%s output format is deprecated. This is equivalent "
|
||||
"to --msg-template=%s" % (self.name, self.line_format),
|
||||
DeprecationWarning,
|
||||
)
|
||||
TextReporter.__init__(self, output)
|
||||
|
||||
|
||||
class VSTextReporter(ParseableTextReporter):
|
||||
"""Visual studio text reporter"""
|
||||
|
||||
name = "msvs"
|
||||
line_format = "{path}({line}): [{msg_id}({symbol}){obj}] {msg}"
|
||||
|
||||
|
||||
class ColorizedTextReporter(TextReporter):
|
||||
"""Simple TextReporter that colorizes text output"""
|
||||
|
||||
name = "colorized"
|
||||
COLOR_MAPPING = {
|
||||
"I": ("green", None),
|
||||
"C": (None, "bold"),
|
||||
"R": ("magenta", "bold, italic"),
|
||||
"W": ("magenta", None),
|
||||
"E": ("red", "bold"),
|
||||
"F": ("red", "bold, underline"),
|
||||
"S": ("yellow", "inverse"), # S stands for module Separator
|
||||
}
|
||||
|
||||
def __init__(self, output=None, color_mapping=None):
|
||||
TextReporter.__init__(self, output)
|
||||
self.color_mapping = color_mapping or dict(ColorizedTextReporter.COLOR_MAPPING)
|
||||
ansi_terms = ["xterm-16color", "xterm-256color"]
|
||||
if os.environ.get("TERM") not in ansi_terms:
|
||||
if sys.platform == "win32":
|
||||
# pylint: disable=import-error,import-outside-toplevel
|
||||
import colorama
|
||||
|
||||
self.out = colorama.AnsiToWin32(self.out)
|
||||
|
||||
def _get_decoration(self, msg_id):
|
||||
"""Returns the tuple color, style associated with msg_id as defined
|
||||
in self.color_mapping
|
||||
"""
|
||||
try:
|
||||
return self.color_mapping[msg_id[0]]
|
||||
except KeyError:
|
||||
return None, None
|
||||
|
||||
def handle_message(self, msg):
|
||||
"""manage message of different types, and colorize output
|
||||
using ansi escape codes
|
||||
"""
|
||||
if msg.module not in self._modules:
|
||||
color, style = self._get_decoration("S")
|
||||
if msg.module:
|
||||
modsep = colorize_ansi(
|
||||
"************* Module %s" % msg.module, color, style
|
||||
)
|
||||
else:
|
||||
modsep = colorize_ansi("************* %s" % msg.module, color, style)
|
||||
self.writeln(modsep)
|
||||
self._modules.add(msg.module)
|
||||
color, style = self._get_decoration(msg.C)
|
||||
|
||||
msg = msg._replace(
|
||||
**{
|
||||
attr: colorize_ansi(getattr(msg, attr), color, style)
|
||||
for attr in ("msg", "symbol", "category", "C")
|
||||
}
|
||||
)
|
||||
self.write_message(msg)
|
||||
|
||||
|
||||
def register(linter):
|
||||
"""Register the reporter classes with the linter."""
|
||||
linter.register_reporter(TextReporter)
|
||||
linter.register_reporter(ParseableTextReporter)
|
||||
linter.register_reporter(VSTextReporter)
|
||||
linter.register_reporter(ColorizedTextReporter)
|
||||
@@ -0,0 +1,97 @@
|
||||
# Copyright (c) 2015-2016, 2018 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2018 ssolanki <sushobhitsolanki@gmail.com>
|
||||
# Copyright (c) 2018 Sushobhit <31987769+sushobhit27@users.noreply.github.com>
|
||||
# Copyright (c) 2018 Anthony Sottile <asottile@umich.edu>
|
||||
# Copyright (c) 2019 Pierre Sassoulas <pierre.sassoulas@gmail.com>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""Universal report objects and some formatting drivers.
|
||||
|
||||
A way to create simple reports using python objects, primarily designed to be
|
||||
formatted as text and html.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
from io import StringIO
|
||||
|
||||
|
||||
class BaseWriter:
|
||||
"""base class for ureport writers"""
|
||||
|
||||
def format(self, layout, stream=None, encoding=None):
|
||||
"""format and write the given layout into the stream object
|
||||
|
||||
unicode policy: unicode strings may be found in the layout;
|
||||
try to call stream.write with it, but give it back encoded using
|
||||
the given encoding if it fails
|
||||
"""
|
||||
if stream is None:
|
||||
stream = sys.stdout
|
||||
if not encoding:
|
||||
encoding = getattr(stream, "encoding", "UTF-8")
|
||||
self.encoding = encoding or "UTF-8"
|
||||
self.out = stream
|
||||
self.begin_format()
|
||||
layout.accept(self)
|
||||
self.end_format()
|
||||
|
||||
def format_children(self, layout):
|
||||
"""recurse on the layout children and call their accept method
|
||||
(see the Visitor pattern)
|
||||
"""
|
||||
for child in getattr(layout, "children", ()):
|
||||
child.accept(self)
|
||||
|
||||
def writeln(self, string=""):
|
||||
"""write a line in the output buffer"""
|
||||
self.write(string + os.linesep)
|
||||
|
||||
def write(self, string):
|
||||
"""write a string in the output buffer"""
|
||||
self.out.write(string)
|
||||
|
||||
def begin_format(self):
|
||||
"""begin to format a layout"""
|
||||
self.section = 0
|
||||
|
||||
def end_format(self):
|
||||
"""finished to format a layout"""
|
||||
|
||||
def get_table_content(self, table):
|
||||
"""trick to get table content without actually writing it
|
||||
|
||||
return an aligned list of lists containing table cells values as string
|
||||
"""
|
||||
result = [[]]
|
||||
cols = table.cols
|
||||
for cell in self.compute_content(table):
|
||||
if cols == 0:
|
||||
result.append([])
|
||||
cols = table.cols
|
||||
cols -= 1
|
||||
result[-1].append(cell)
|
||||
# fill missing cells
|
||||
while len(result[-1]) < cols:
|
||||
result[-1].append("")
|
||||
return result
|
||||
|
||||
def compute_content(self, layout):
|
||||
"""trick to compute the formatting of children layout before actually
|
||||
writing it
|
||||
|
||||
return an iterator on strings (one for each child element)
|
||||
"""
|
||||
# Patch the underlying output stream with a fresh-generated stream,
|
||||
# which is used to store a temporary representation of a child
|
||||
# node.
|
||||
out = self.out
|
||||
try:
|
||||
for child in layout.children:
|
||||
stream = StringIO()
|
||||
self.out = stream
|
||||
child.accept(self)
|
||||
yield stream.getvalue()
|
||||
finally:
|
||||
self.out = out
|
||||
@@ -0,0 +1,189 @@
|
||||
# Copyright (c) 2015-2016, 2018 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2018 ssolanki <sushobhitsolanki@gmail.com>
|
||||
# Copyright (c) 2018 Sushobhit <31987769+sushobhit27@users.noreply.github.com>
|
||||
# Copyright (c) 2018 Nick Drozd <nicholasdrozd@gmail.com>
|
||||
# Copyright (c) 2020 Anthony Sottile <asottile@umich.edu>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""Micro reports objects.
|
||||
|
||||
A micro report is a tree of layout and content objects.
|
||||
"""
|
||||
|
||||
|
||||
class VNode:
|
||||
def __init__(self, nid=None):
|
||||
self.id = nid
|
||||
# navigation
|
||||
self.parent = None
|
||||
self.children = []
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.children)
|
||||
|
||||
def append(self, child):
|
||||
"""add a node to children"""
|
||||
self.children.append(child)
|
||||
child.parent = self
|
||||
|
||||
def insert(self, index, child):
|
||||
"""insert a child node"""
|
||||
self.children.insert(index, child)
|
||||
child.parent = self
|
||||
|
||||
def _get_visit_name(self):
|
||||
"""
|
||||
return the visit name for the mixed class. When calling 'accept', the
|
||||
method <'visit_' + name returned by this method> will be called on the
|
||||
visitor
|
||||
"""
|
||||
try:
|
||||
# pylint: disable=no-member
|
||||
return self.TYPE.replace("-", "_")
|
||||
# pylint: disable=broad-except
|
||||
except Exception:
|
||||
return self.__class__.__name__.lower()
|
||||
|
||||
def accept(self, visitor, *args, **kwargs):
|
||||
func = getattr(visitor, "visit_%s" % self._get_visit_name())
|
||||
return func(self, *args, **kwargs)
|
||||
|
||||
def leave(self, visitor, *args, **kwargs):
|
||||
func = getattr(visitor, "leave_%s" % self._get_visit_name())
|
||||
return func(self, *args, **kwargs)
|
||||
|
||||
|
||||
class BaseLayout(VNode):
|
||||
"""base container node
|
||||
|
||||
attributes
|
||||
* children : components in this table (i.e. the table's cells)
|
||||
"""
|
||||
|
||||
def __init__(self, children=(), **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
for child in children:
|
||||
if isinstance(child, VNode):
|
||||
self.append(child)
|
||||
else:
|
||||
self.add_text(child)
|
||||
|
||||
def append(self, child):
|
||||
"""overridden to detect problems easily"""
|
||||
assert child not in self.parents()
|
||||
VNode.append(self, child)
|
||||
|
||||
def parents(self):
|
||||
"""return the ancestor nodes"""
|
||||
assert self.parent is not self
|
||||
if self.parent is None:
|
||||
return []
|
||||
return [self.parent] + self.parent.parents()
|
||||
|
||||
def add_text(self, text):
|
||||
"""shortcut to add text data"""
|
||||
self.children.append(Text(text))
|
||||
|
||||
|
||||
# non container nodes #########################################################
|
||||
|
||||
|
||||
class Text(VNode):
|
||||
"""a text portion
|
||||
|
||||
attributes :
|
||||
* data : the text value as an encoded or unicode string
|
||||
"""
|
||||
|
||||
def __init__(self, data, escaped=True, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
# if isinstance(data, unicode):
|
||||
# data = data.encode('ascii')
|
||||
assert isinstance(data, str), data.__class__
|
||||
self.escaped = escaped
|
||||
self.data = data
|
||||
|
||||
|
||||
class VerbatimText(Text):
|
||||
"""a verbatim text, display the raw data
|
||||
|
||||
attributes :
|
||||
* data : the text value as an encoded or unicode string
|
||||
"""
|
||||
|
||||
|
||||
# container nodes #############################################################
|
||||
|
||||
|
||||
class Section(BaseLayout):
|
||||
"""a section
|
||||
|
||||
attributes :
|
||||
* BaseLayout attributes
|
||||
|
||||
a title may also be given to the constructor, it'll be added
|
||||
as a first element
|
||||
a description may also be given to the constructor, it'll be added
|
||||
as a first paragraph
|
||||
"""
|
||||
|
||||
def __init__(self, title=None, description=None, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
if description:
|
||||
self.insert(0, Paragraph([Text(description)]))
|
||||
if title:
|
||||
self.insert(0, Title(children=(title,)))
|
||||
|
||||
|
||||
class EvaluationSection(Section):
|
||||
def __init__(self, message, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
title = Paragraph()
|
||||
title.append(Text("-" * len(message)))
|
||||
self.append(title)
|
||||
|
||||
message_body = Paragraph()
|
||||
message_body.append(Text(message))
|
||||
self.append(message_body)
|
||||
|
||||
|
||||
class Title(BaseLayout):
|
||||
"""a title
|
||||
|
||||
attributes :
|
||||
* BaseLayout attributes
|
||||
|
||||
A title must not contains a section nor a paragraph!
|
||||
"""
|
||||
|
||||
|
||||
class Paragraph(BaseLayout):
|
||||
"""a simple text paragraph
|
||||
|
||||
attributes :
|
||||
* BaseLayout attributes
|
||||
|
||||
A paragraph must not contains a section !
|
||||
"""
|
||||
|
||||
|
||||
class Table(BaseLayout):
|
||||
"""some tabular data
|
||||
|
||||
attributes :
|
||||
* BaseLayout attributes
|
||||
* cols : the number of columns of the table (REQUIRED)
|
||||
* rheaders : the first row's elements are table's header
|
||||
* cheaders : the first col's elements are table's header
|
||||
* title : the table's optional title
|
||||
"""
|
||||
|
||||
def __init__(self, cols, title=None, rheaders=0, cheaders=0, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
assert isinstance(cols, int)
|
||||
self.cols = cols
|
||||
self.title = title
|
||||
self.rheaders = rheaders
|
||||
self.cheaders = cheaders
|
||||
@@ -0,0 +1,96 @@
|
||||
# Copyright (c) 2015-2016, 2018-2019 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2018, 2020 Anthony Sottile <asottile@umich.edu>
|
||||
# Copyright (c) 2019 Hugo van Kemenade <hugovk@users.noreply.github.com>
|
||||
# Copyright (c) 2019 Pierre Sassoulas <pierre.sassoulas@gmail.com>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""Text formatting drivers for ureports"""
|
||||
|
||||
from pylint.reporters.ureports import BaseWriter
|
||||
|
||||
TITLE_UNDERLINES = ["", "=", "-", "`", ".", "~", "^"]
|
||||
BULLETS = ["*", "-"]
|
||||
|
||||
|
||||
class TextWriter(BaseWriter):
|
||||
"""format layouts as text
|
||||
(ReStructured inspiration but not totally handled yet)
|
||||
"""
|
||||
|
||||
def begin_format(self):
|
||||
super().begin_format()
|
||||
self.list_level = 0
|
||||
|
||||
def visit_section(self, layout):
|
||||
"""display a section as text
|
||||
"""
|
||||
self.section += 1
|
||||
self.writeln()
|
||||
self.format_children(layout)
|
||||
self.section -= 1
|
||||
self.writeln()
|
||||
|
||||
def visit_evaluationsection(self, layout):
|
||||
"""Display an evaluation section as a text."""
|
||||
self.section += 1
|
||||
self.format_children(layout)
|
||||
self.section -= 1
|
||||
self.writeln()
|
||||
|
||||
def visit_title(self, layout):
|
||||
title = "".join(list(self.compute_content(layout)))
|
||||
self.writeln(title)
|
||||
try:
|
||||
self.writeln(TITLE_UNDERLINES[self.section] * len(title))
|
||||
except IndexError:
|
||||
print("FIXME TITLE TOO DEEP. TURNING TITLE INTO TEXT")
|
||||
|
||||
def visit_paragraph(self, layout):
|
||||
"""enter a paragraph"""
|
||||
self.format_children(layout)
|
||||
self.writeln()
|
||||
|
||||
def visit_table(self, layout):
|
||||
"""display a table as text"""
|
||||
table_content = self.get_table_content(layout)
|
||||
# get columns width
|
||||
cols_width = [0] * len(table_content[0])
|
||||
for row in table_content:
|
||||
for index, col in enumerate(row):
|
||||
cols_width[index] = max(cols_width[index], len(col))
|
||||
self.default_table(layout, table_content, cols_width)
|
||||
self.writeln()
|
||||
|
||||
def default_table(self, layout, table_content, cols_width):
|
||||
"""format a table"""
|
||||
cols_width = [size + 1 for size in cols_width]
|
||||
format_strings = " ".join(["%%-%ss"] * len(cols_width))
|
||||
format_strings = format_strings % tuple(cols_width)
|
||||
format_strings = format_strings.split(" ")
|
||||
table_linesep = "\n+" + "+".join(["-" * w for w in cols_width]) + "+\n"
|
||||
headsep = "\n+" + "+".join(["=" * w for w in cols_width]) + "+\n"
|
||||
|
||||
self.write(table_linesep)
|
||||
for index, line in enumerate(table_content):
|
||||
self.write("|")
|
||||
for line_index, at_index in enumerate(line):
|
||||
self.write(format_strings[line_index] % at_index)
|
||||
self.write("|")
|
||||
if index == 0 and layout.rheaders:
|
||||
self.write(headsep)
|
||||
else:
|
||||
self.write(table_linesep)
|
||||
|
||||
def visit_verbatimtext(self, layout):
|
||||
"""display a verbatim layout as text (so difficult ;)
|
||||
"""
|
||||
self.writeln("::\n")
|
||||
for line in layout.data.splitlines():
|
||||
self.writeln(" " + line)
|
||||
self.writeln()
|
||||
|
||||
def visit_text(self, layout):
|
||||
"""add some text"""
|
||||
self.write("%s" % layout.data)
|
||||
@@ -0,0 +1,625 @@
|
||||
# Copyright (c) 2012-2014 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
|
||||
# Copyright (c) 2012 FELD Boris <lothiraldan@gmail.com>
|
||||
# Copyright (c) 2013-2018, 2020 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2013-2014 Google, Inc.
|
||||
# Copyright (c) 2013 buck@yelp.com <buck@yelp.com>
|
||||
# Copyright (c) 2014 LCD 47 <lcd047@gmail.com>
|
||||
# Copyright (c) 2014 Brett Cannon <brett@python.org>
|
||||
# Copyright (c) 2014 Ricardo Gemignani <ricardo.gemignani@gmail.com>
|
||||
# Copyright (c) 2014 Arun Persaud <arun@nubati.net>
|
||||
# Copyright (c) 2015 Pavel Roskin <proski@gnu.org>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
|
||||
# Copyright (c) 2016 Derek Gustafson <degustaf@gmail.com>
|
||||
# Copyright (c) 2016 Roy Williams <roy.williams.iii@gmail.com>
|
||||
# Copyright (c) 2016 xmo-odoo <xmo-odoo@users.noreply.github.com>
|
||||
# Copyright (c) 2017 Bryce Guinta <bryce.paul.guinta@gmail.com>
|
||||
# Copyright (c) 2018 ssolanki <sushobhitsolanki@gmail.com>
|
||||
# Copyright (c) 2018 Sushobhit <31987769+sushobhit27@users.noreply.github.com>
|
||||
# Copyright (c) 2019 Mr. Senko <atodorov@mrsenko.com>
|
||||
# Copyright (c) 2019 Hugo van Kemenade <hugovk@users.noreply.github.com>
|
||||
# Copyright (c) 2019 Pierre Sassoulas <pierre.sassoulas@gmail.com>
|
||||
# Copyright (c) 2020 Anthony Sottile <asottile@umich.edu>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""functional/non regression tests for pylint"""
|
||||
import collections
|
||||
import configparser
|
||||
import contextlib
|
||||
import csv
|
||||
import functools
|
||||
import operator
|
||||
import platform
|
||||
import re
|
||||
import sys
|
||||
import tempfile
|
||||
import tokenize
|
||||
from glob import glob
|
||||
from io import StringIO
|
||||
from os import close, getcwd, linesep, remove, sep, write
|
||||
from os.path import abspath, basename, dirname, exists, join, splitext
|
||||
|
||||
import astroid
|
||||
import pytest
|
||||
|
||||
from pylint import checkers, interfaces
|
||||
from pylint.lint import PyLinter
|
||||
from pylint.reporters import BaseReporter
|
||||
from pylint.utils import ASTWalker
|
||||
|
||||
# Utils
|
||||
|
||||
SYS_VERS_STR = "%d%d%d" % sys.version_info[:3]
|
||||
TITLE_UNDERLINES = ["", "=", "-", "."]
|
||||
PREFIX = abspath(dirname(__file__))
|
||||
|
||||
|
||||
def _get_tests_info(input_dir, msg_dir, prefix, suffix):
|
||||
"""get python input examples and output messages
|
||||
|
||||
We use following conventions for input files and messages:
|
||||
for different inputs:
|
||||
test for python >= x.y -> input = <name>_pyxy.py
|
||||
test for python < x.y -> input = <name>_py_xy.py
|
||||
for one input and different messages:
|
||||
message for python >= x.y -> message = <name>_pyxy.txt
|
||||
lower versions -> message with highest num
|
||||
"""
|
||||
result = []
|
||||
for fname in glob(join(input_dir, prefix + "*" + suffix)):
|
||||
infile = basename(fname)
|
||||
fbase = splitext(infile)[0]
|
||||
# filter input files :
|
||||
pyrestr = fbase.rsplit("_py", 1)[-1] # like _26 or 26
|
||||
if pyrestr.isdigit(): # '24', '25'...
|
||||
if SYS_VERS_STR < pyrestr:
|
||||
continue
|
||||
if pyrestr.startswith("_") and pyrestr[1:].isdigit():
|
||||
# skip test for higher python versions
|
||||
if SYS_VERS_STR >= pyrestr[1:]:
|
||||
continue
|
||||
messages = glob(join(msg_dir, fbase + "*.txt"))
|
||||
# the last one will be without ext, i.e. for all or upper versions:
|
||||
if messages:
|
||||
for outfile in sorted(messages, reverse=True):
|
||||
py_rest = outfile.rsplit("_py", 1)[-1][:-4]
|
||||
if py_rest.isdigit() and SYS_VERS_STR >= py_rest:
|
||||
break
|
||||
else:
|
||||
# This will provide an error message indicating the missing filename.
|
||||
outfile = join(msg_dir, fbase + ".txt")
|
||||
result.append((infile, outfile))
|
||||
return result
|
||||
|
||||
|
||||
class TestReporter(BaseReporter):
|
||||
"""reporter storing plain text messages"""
|
||||
|
||||
__implements__ = interfaces.IReporter
|
||||
|
||||
def __init__(self): # pylint: disable=super-init-not-called
|
||||
|
||||
self.message_ids = {}
|
||||
self.reset()
|
||||
self.path_strip_prefix = getcwd() + sep
|
||||
|
||||
def reset(self):
|
||||
self.out = StringIO()
|
||||
self.messages = []
|
||||
|
||||
def handle_message(self, msg):
|
||||
"""manage message of different type and in the context of path """
|
||||
obj = msg.obj
|
||||
line = msg.line
|
||||
msg_id = msg.msg_id
|
||||
msg = msg.msg
|
||||
self.message_ids[msg_id] = 1
|
||||
if obj:
|
||||
obj = ":%s" % obj
|
||||
sigle = msg_id[0]
|
||||
if linesep != "\n":
|
||||
# 2to3 writes os.linesep instead of using
|
||||
# the previosly used line separators
|
||||
msg = msg.replace("\r\n", "\n")
|
||||
self.messages.append("%s:%3s%s: %s" % (sigle, line, obj, msg))
|
||||
|
||||
def finalize(self):
|
||||
self.messages.sort()
|
||||
for msg in self.messages:
|
||||
print(msg, file=self.out)
|
||||
result = self.out.getvalue()
|
||||
self.reset()
|
||||
return result
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def on_set_current_module(self, module, filepath):
|
||||
pass
|
||||
|
||||
# pylint: enable=unused-argument
|
||||
|
||||
def display_reports(self, layout):
|
||||
"""ignore layouts"""
|
||||
|
||||
_display = None
|
||||
|
||||
|
||||
class MinimalTestReporter(BaseReporter):
|
||||
def handle_message(self, msg):
|
||||
self.messages.append(msg)
|
||||
|
||||
def on_set_current_module(self, module, filepath):
|
||||
self.messages = []
|
||||
|
||||
_display = None
|
||||
|
||||
|
||||
class Message(
|
||||
collections.namedtuple("Message", ["msg_id", "line", "node", "args", "confidence"])
|
||||
):
|
||||
def __new__(cls, msg_id, line=None, node=None, args=None, confidence=None):
|
||||
return tuple.__new__(cls, (msg_id, line, node, args, confidence))
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, Message):
|
||||
if self.confidence and other.confidence:
|
||||
return super().__eq__(other)
|
||||
return self[:-1] == other[:-1]
|
||||
return NotImplemented # pragma: no cover
|
||||
|
||||
__hash__ = None
|
||||
|
||||
|
||||
class UnittestLinter:
|
||||
"""A fake linter class to capture checker messages."""
|
||||
|
||||
# pylint: disable=unused-argument, no-self-use
|
||||
|
||||
def __init__(self):
|
||||
self._messages = []
|
||||
self.stats = {}
|
||||
|
||||
def release_messages(self):
|
||||
try:
|
||||
return self._messages
|
||||
finally:
|
||||
self._messages = []
|
||||
|
||||
def add_message(
|
||||
self, msg_id, line=None, node=None, args=None, confidence=None, col_offset=None
|
||||
):
|
||||
# Do not test col_offset for now since changing Message breaks everything
|
||||
self._messages.append(Message(msg_id, line, node, args, confidence))
|
||||
|
||||
def is_message_enabled(self, *unused_args, **unused_kwargs):
|
||||
return True
|
||||
|
||||
def add_stats(self, **kwargs):
|
||||
for name, value in kwargs.items():
|
||||
self.stats[name] = value
|
||||
return self.stats
|
||||
|
||||
@property
|
||||
def options_providers(self):
|
||||
return linter.options_providers
|
||||
|
||||
|
||||
def set_config(**kwargs):
|
||||
"""Decorator for setting config values on a checker."""
|
||||
|
||||
def _wrapper(fun):
|
||||
@functools.wraps(fun)
|
||||
def _forward(self):
|
||||
for key, value in kwargs.items():
|
||||
setattr(self.checker.config, key, value)
|
||||
if isinstance(self, CheckerTestCase):
|
||||
# reopen checker in case, it may be interested in configuration change
|
||||
self.checker.open()
|
||||
fun(self)
|
||||
|
||||
return _forward
|
||||
|
||||
return _wrapper
|
||||
|
||||
|
||||
class CheckerTestCase:
|
||||
"""A base testcase class for unit testing individual checker classes."""
|
||||
|
||||
CHECKER_CLASS = None
|
||||
CONFIG = {}
|
||||
|
||||
def setup_method(self):
|
||||
self.linter = UnittestLinter()
|
||||
self.checker = self.CHECKER_CLASS(self.linter) # pylint: disable=not-callable
|
||||
for key, value in self.CONFIG.items():
|
||||
setattr(self.checker.config, key, value)
|
||||
self.checker.open()
|
||||
|
||||
@contextlib.contextmanager
|
||||
def assertNoMessages(self):
|
||||
"""Assert that no messages are added by the given method."""
|
||||
with self.assertAddsMessages():
|
||||
yield
|
||||
|
||||
@contextlib.contextmanager
|
||||
def assertAddsMessages(self, *messages):
|
||||
"""Assert that exactly the given method adds the given messages.
|
||||
|
||||
The list of messages must exactly match *all* the messages added by the
|
||||
method. Additionally, we check to see whether the args in each message can
|
||||
actually be substituted into the message string.
|
||||
"""
|
||||
yield
|
||||
got = self.linter.release_messages()
|
||||
msg = "Expected messages did not match actual.\n" "Expected:\n%s\nGot:\n%s" % (
|
||||
"\n".join(repr(m) for m in messages),
|
||||
"\n".join(repr(m) for m in got),
|
||||
)
|
||||
assert list(messages) == got, msg
|
||||
|
||||
def walk(self, node):
|
||||
"""recursive walk on the given node"""
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(self.checker)
|
||||
walker.walk(node)
|
||||
|
||||
|
||||
# Init
|
||||
test_reporter = TestReporter()
|
||||
linter = PyLinter()
|
||||
linter.set_reporter(test_reporter)
|
||||
linter.config.persistent = 0
|
||||
checkers.initialize(linter)
|
||||
|
||||
|
||||
def _tokenize_str(code):
|
||||
return list(tokenize.generate_tokens(StringIO(code).readline))
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _create_tempfile(content=None):
|
||||
"""Create a new temporary file.
|
||||
|
||||
If *content* parameter is given, then it will be written
|
||||
in the temporary file, before passing it back.
|
||||
This is a context manager and should be used with a *with* statement.
|
||||
"""
|
||||
# Can't use tempfile.NamedTemporaryFile here
|
||||
# because on Windows the file must be closed before writing to it,
|
||||
# see http://bugs.python.org/issue14243
|
||||
file_handle, tmp = tempfile.mkstemp()
|
||||
if content:
|
||||
# erff
|
||||
write(file_handle, bytes(content, "ascii"))
|
||||
try:
|
||||
yield tmp
|
||||
finally:
|
||||
close(file_handle)
|
||||
remove(tmp)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _create_file_backed_module(code):
|
||||
"""Create an astroid module for the given code, backed by a real file."""
|
||||
with _create_tempfile() as temp:
|
||||
module = astroid.parse(code)
|
||||
module.file = temp
|
||||
yield module
|
||||
|
||||
|
||||
class NoFileError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class OutputLine(
|
||||
collections.namedtuple(
|
||||
"OutputLine", ["symbol", "lineno", "object", "msg", "confidence"]
|
||||
)
|
||||
):
|
||||
@classmethod
|
||||
def from_msg(cls, msg):
|
||||
return cls(
|
||||
msg.symbol,
|
||||
msg.line,
|
||||
msg.obj or "",
|
||||
msg.msg.replace("\r\n", "\n"),
|
||||
msg.confidence.name
|
||||
if msg.confidence != interfaces.UNDEFINED
|
||||
else interfaces.HIGH.name,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_csv(cls, row):
|
||||
confidence = row[4] if len(row) == 5 else interfaces.HIGH.name
|
||||
return cls(row[0], int(row[1]), row[2], row[3], confidence)
|
||||
|
||||
def to_csv(self):
|
||||
if self.confidence == interfaces.HIGH.name:
|
||||
return self[:-1]
|
||||
|
||||
return self
|
||||
|
||||
|
||||
# Common sub-expressions.
|
||||
_MESSAGE = {"msg": r"[a-z][a-z\-]+"}
|
||||
# Matches a #,
|
||||
# - followed by a comparison operator and a Python version (optional),
|
||||
# - followed by a line number with a +/- (optional),
|
||||
# - followed by a list of bracketed message symbols.
|
||||
# Used to extract expected messages from testdata files.
|
||||
_EXPECTED_RE = re.compile(
|
||||
r"\s*#\s*(?:(?P<line>[+-]?[0-9]+):)?"
|
||||
r"(?:(?P<op>[><=]+) *(?P<version>[0-9.]+):)?"
|
||||
r"\s*\[(?P<msgs>%(msg)s(?:,\s*%(msg)s)*)\]" % _MESSAGE
|
||||
)
|
||||
|
||||
|
||||
def parse_python_version(ver_str):
|
||||
return tuple(int(digit) for digit in ver_str.split("."))
|
||||
|
||||
|
||||
class FunctionalTestReporter(BaseReporter): # pylint: disable=abstract-method
|
||||
def handle_message(self, msg):
|
||||
self.messages.append(msg)
|
||||
|
||||
def on_set_current_module(self, module, filepath):
|
||||
self.messages = []
|
||||
|
||||
def display_reports(self, layout):
|
||||
"""Ignore layouts and don't call self._display()."""
|
||||
|
||||
|
||||
class FunctionalTestFile:
|
||||
"""A single functional test case file with options."""
|
||||
|
||||
_CONVERTERS = {
|
||||
"min_pyver": parse_python_version,
|
||||
"max_pyver": parse_python_version,
|
||||
"requires": lambda s: s.split(","),
|
||||
}
|
||||
|
||||
def __init__(self, directory, filename):
|
||||
self._directory = directory
|
||||
self.base = filename.replace(".py", "")
|
||||
self.options = {
|
||||
"min_pyver": (2, 5),
|
||||
"max_pyver": (4, 0),
|
||||
"requires": [],
|
||||
"except_implementations": [],
|
||||
"exclude_platforms": [],
|
||||
}
|
||||
self._parse_options()
|
||||
|
||||
def __repr__(self):
|
||||
return "FunctionalTest:{}".format(self.base)
|
||||
|
||||
def _parse_options(self):
|
||||
cp = configparser.ConfigParser()
|
||||
cp.add_section("testoptions")
|
||||
try:
|
||||
cp.read(self.option_file)
|
||||
except NoFileError:
|
||||
pass
|
||||
|
||||
for name, value in cp.items("testoptions"):
|
||||
conv = self._CONVERTERS.get(name, lambda v: v)
|
||||
self.options[name] = conv(value)
|
||||
|
||||
@property
|
||||
def option_file(self):
|
||||
return self._file_type(".rc")
|
||||
|
||||
@property
|
||||
def module(self):
|
||||
package = basename(self._directory)
|
||||
return ".".join([package, self.base])
|
||||
|
||||
@property
|
||||
def expected_output(self):
|
||||
return self._file_type(".txt", check_exists=False)
|
||||
|
||||
@property
|
||||
def source(self):
|
||||
return self._file_type(".py")
|
||||
|
||||
def _file_type(self, ext, check_exists=True):
|
||||
name = join(self._directory, self.base + ext)
|
||||
if not check_exists or exists(name):
|
||||
return name
|
||||
raise NoFileError("Cannot find '{}'.".format(name))
|
||||
|
||||
|
||||
_OPERATORS = {">": operator.gt, "<": operator.lt, ">=": operator.ge, "<=": operator.le}
|
||||
|
||||
|
||||
def parse_expected_output(stream):
|
||||
return [OutputLine.from_csv(row) for row in csv.reader(stream, "test")]
|
||||
|
||||
|
||||
def get_expected_messages(stream):
|
||||
"""Parses a file and get expected messages.
|
||||
|
||||
:param stream: File-like input stream.
|
||||
:type stream: enumerable
|
||||
:returns: A dict mapping line,msg-symbol tuples to the count on this line.
|
||||
:rtype: dict
|
||||
"""
|
||||
messages = collections.Counter()
|
||||
for i, line in enumerate(stream):
|
||||
match = _EXPECTED_RE.search(line)
|
||||
if match is None:
|
||||
continue
|
||||
line = match.group("line")
|
||||
if line is None:
|
||||
line = i + 1
|
||||
elif line.startswith("+") or line.startswith("-"):
|
||||
line = i + 1 + int(line)
|
||||
else:
|
||||
line = int(line)
|
||||
|
||||
version = match.group("version")
|
||||
op = match.group("op")
|
||||
if version:
|
||||
required = parse_python_version(version)
|
||||
if not _OPERATORS[op](sys.version_info, required):
|
||||
continue
|
||||
|
||||
for msg_id in match.group("msgs").split(","):
|
||||
messages[line, msg_id.strip()] += 1
|
||||
return messages
|
||||
|
||||
|
||||
def multiset_difference(left_op, right_op):
|
||||
"""Takes two multisets and compares them.
|
||||
|
||||
A multiset is a dict with the cardinality of the key as the value.
|
||||
|
||||
:param left_op: The expected entries.
|
||||
:type left_op: set
|
||||
:param right_op: Actual entries.
|
||||
:type right_op: set
|
||||
|
||||
:returns: The two multisets of missing and unexpected messages.
|
||||
:rtype: tuple
|
||||
"""
|
||||
missing = left_op.copy()
|
||||
missing.subtract(right_op)
|
||||
unexpected = {}
|
||||
for key, value in list(missing.items()):
|
||||
if value <= 0:
|
||||
missing.pop(key)
|
||||
if value < 0:
|
||||
unexpected[key] = -value
|
||||
return missing, unexpected
|
||||
|
||||
|
||||
class LintModuleTest:
|
||||
maxDiff = None
|
||||
|
||||
def __init__(self, test_file):
|
||||
_test_reporter = FunctionalTestReporter()
|
||||
self._linter = PyLinter()
|
||||
self._linter.set_reporter(_test_reporter)
|
||||
self._linter.config.persistent = 0
|
||||
checkers.initialize(self._linter)
|
||||
self._linter.disable("I")
|
||||
try:
|
||||
self._linter.read_config_file(test_file.option_file)
|
||||
self._linter.load_config_file()
|
||||
except NoFileError:
|
||||
pass
|
||||
self._test_file = test_file
|
||||
|
||||
def setUp(self):
|
||||
if self._should_be_skipped_due_to_version():
|
||||
pytest.skip(
|
||||
"Test cannot run with Python %s." % (sys.version.split(" ")[0],)
|
||||
)
|
||||
missing = []
|
||||
for req in self._test_file.options["requires"]:
|
||||
try:
|
||||
__import__(req)
|
||||
except ImportError:
|
||||
missing.append(req)
|
||||
if missing:
|
||||
pytest.skip("Requires %s to be present." % (",".join(missing),))
|
||||
if self._test_file.options["except_implementations"]:
|
||||
implementations = [
|
||||
item.strip()
|
||||
for item in self._test_file.options["except_implementations"].split(",")
|
||||
]
|
||||
implementation = platform.python_implementation()
|
||||
if implementation in implementations:
|
||||
pytest.skip(
|
||||
"Test cannot run with Python implementation %r" % (implementation,)
|
||||
)
|
||||
if self._test_file.options["exclude_platforms"]:
|
||||
platforms = [
|
||||
item.strip()
|
||||
for item in self._test_file.options["exclude_platforms"].split(",")
|
||||
]
|
||||
if sys.platform.lower() in platforms:
|
||||
pytest.skip("Test cannot run on platform %r" % (sys.platform,))
|
||||
|
||||
def _should_be_skipped_due_to_version(self):
|
||||
return (
|
||||
sys.version_info < self._test_file.options["min_pyver"]
|
||||
or sys.version_info > self._test_file.options["max_pyver"]
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return "%s (%s.%s)" % (
|
||||
self._test_file.base,
|
||||
self.__class__.__module__,
|
||||
self.__class__.__name__,
|
||||
)
|
||||
|
||||
def _open_expected_file(self):
|
||||
return open(self._test_file.expected_output)
|
||||
|
||||
def _open_source_file(self):
|
||||
if self._test_file.base == "invalid_encoded_data":
|
||||
return open(self._test_file.source)
|
||||
if "latin1" in self._test_file.base:
|
||||
return open(self._test_file.source, encoding="latin1")
|
||||
return open(self._test_file.source, encoding="utf8")
|
||||
|
||||
def _get_expected(self):
|
||||
with self._open_source_file() as fobj:
|
||||
expected_msgs = get_expected_messages(fobj)
|
||||
|
||||
if expected_msgs:
|
||||
with self._open_expected_file() as fobj:
|
||||
expected_output_lines = parse_expected_output(fobj)
|
||||
else:
|
||||
expected_output_lines = []
|
||||
return expected_msgs, expected_output_lines
|
||||
|
||||
def _get_received(self):
|
||||
messages = self._linter.reporter.messages
|
||||
messages.sort(key=lambda m: (m.line, m.symbol, m.msg))
|
||||
received_msgs = collections.Counter()
|
||||
received_output_lines = []
|
||||
for msg in messages:
|
||||
assert (
|
||||
msg.symbol != "fatal"
|
||||
), "Pylint analysis failed because of '{}'".format(msg.msg)
|
||||
received_msgs[msg.line, msg.symbol] += 1
|
||||
received_output_lines.append(OutputLine.from_msg(msg))
|
||||
return received_msgs, received_output_lines
|
||||
|
||||
def _runTest(self):
|
||||
modules_to_check = [self._test_file.source]
|
||||
self._linter.check(modules_to_check)
|
||||
expected_messages, expected_text = self._get_expected()
|
||||
received_messages, received_text = self._get_received()
|
||||
|
||||
if expected_messages != received_messages:
|
||||
msg = ['Wrong results for file "%s":' % (self._test_file.base)]
|
||||
missing, unexpected = multiset_difference(
|
||||
expected_messages, received_messages
|
||||
)
|
||||
if missing:
|
||||
msg.append("\nExpected in testdata:")
|
||||
msg.extend(" %3d: %s" % msg for msg in sorted(missing))
|
||||
if unexpected:
|
||||
msg.append("\nUnexpected in testdata:")
|
||||
msg.extend(" %3d: %s" % msg for msg in sorted(unexpected))
|
||||
pytest.fail("\n".join(msg))
|
||||
self._check_output_text(expected_messages, expected_text, received_text)
|
||||
|
||||
@classmethod
|
||||
def _split_lines(cls, expected_messages, lines):
|
||||
emitted, omitted = [], []
|
||||
for msg in lines:
|
||||
if (msg[1], msg[0]) in expected_messages:
|
||||
emitted.append(msg)
|
||||
else:
|
||||
omitted.append(msg)
|
||||
return emitted, omitted
|
||||
|
||||
def _check_output_text(self, expected_messages, expected_lines, received_lines):
|
||||
expected_lines = self._split_lines(expected_messages, expected_lines)[0]
|
||||
assert (
|
||||
expected_lines == received_lines
|
||||
), "Expected test lines did not match for test: {}".format(self._test_file.base)
|
||||
@@ -0,0 +1,66 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2006-2014 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
|
||||
# Copyright (c) 2009 Vincent
|
||||
# Copyright (c) 2009 Mads Kiilerich <mads@kiilerich.com>
|
||||
# Copyright (c) 2012-2014 Google, Inc.
|
||||
# Copyright (c) 2014-2018 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2014-2015 Michal Nowikowski <godfryd@gmail.com>
|
||||
# Copyright (c) 2014 LCD 47 <lcd047@gmail.com>
|
||||
# Copyright (c) 2014 Brett Cannon <brett@python.org>
|
||||
# Copyright (c) 2014 Arun Persaud <arun@nubati.net>
|
||||
# Copyright (c) 2014 Damien Nozay <damien.nozay@gmail.com>
|
||||
# Copyright (c) 2015 Aru Sahni <arusahni@gmail.com>
|
||||
# Copyright (c) 2015 Florian Bruhin <me@the-compiler.org>
|
||||
# Copyright (c) 2015 Simu Toni <simutoni@gmail.com>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
|
||||
# Copyright (c) 2016 Łukasz Rogalski <rogalski.91@gmail.com>
|
||||
# Copyright (c) 2016 Moises Lopez <moylop260@vauxoo.com>
|
||||
# Copyright (c) 2016 Glenn Matthews <glenn@e-dad.net>
|
||||
# Copyright (c) 2016 Glenn Matthews <glmatthe@cisco.com>
|
||||
# Copyright (c) 2016 Ashley Whetter <ashley@awhetter.co.uk>
|
||||
# Copyright (c) 2016 xmo-odoo <xmo-odoo@users.noreply.github.com>
|
||||
# Copyright (c) 2017, 2020 Anthony Sottile <asottile@umich.edu>
|
||||
# Copyright (c) 2017 Pierre Sassoulas <pierre.sassoulas@cea.fr>
|
||||
# Copyright (c) 2017-2018 Bryce Guinta <bryce.paul.guinta@gmail.com>
|
||||
# Copyright (c) 2017-2018 hippo91 <guillaume.peillex@gmail.com>
|
||||
# Copyright (c) 2017 Chris Lamb <chris@chris-lamb.co.uk>
|
||||
# Copyright (c) 2017 Thomas Hisch <t.hisch@gmail.com>
|
||||
# Copyright (c) 2017 Mikhail Fesenko <proggga@gmail.com>
|
||||
# Copyright (c) 2017 Craig Citro <craigcitro@gmail.com>
|
||||
# Copyright (c) 2017 Ville Skyttä <ville.skytta@iki.fi>
|
||||
# Copyright (c) 2018-2019 Pierre Sassoulas <pierre.sassoulas@gmail.com>
|
||||
# Copyright (c) 2018 Pierre Sassoulas <pierre.sassoulas@wisebim.fr>
|
||||
# Copyright (c) 2018 ssolanki <sushobhitsolanki@gmail.com>
|
||||
# Copyright (c) 2018 Bryce Guinta <bryce.guinta@protonmail.com>
|
||||
# Copyright (c) 2018 Sushobhit <31987769+sushobhit27@users.noreply.github.com>
|
||||
# Copyright (c) 2018 Reverb C <reverbc@users.noreply.github.com>
|
||||
# Copyright (c) 2018 Nick Drozd <nicholasdrozd@gmail.com>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""some various utilities and helper classes, most of them used in the
|
||||
main pylint class
|
||||
"""
|
||||
|
||||
from pylint.utils.ast_walker import ASTWalker
|
||||
from pylint.utils.file_state import FileState
|
||||
from pylint.utils.utils import (
|
||||
_basename_in_blacklist_re,
|
||||
_check_csv,
|
||||
_format_option_value,
|
||||
_splitstrip,
|
||||
_unquote,
|
||||
decoding_stream,
|
||||
deprecated_option,
|
||||
expand_modules,
|
||||
format_section,
|
||||
get_global_option,
|
||||
get_module_and_frameid,
|
||||
get_rst_section,
|
||||
get_rst_title,
|
||||
normalize_text,
|
||||
register_plugins,
|
||||
safe_decode,
|
||||
tokenize_module,
|
||||
)
|
||||
@@ -0,0 +1,77 @@
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
import collections
|
||||
|
||||
from astroid import nodes
|
||||
|
||||
|
||||
class ASTWalker:
|
||||
def __init__(self, linter):
|
||||
# callbacks per node types
|
||||
self.nbstatements = 0
|
||||
self.visit_events = collections.defaultdict(list)
|
||||
self.leave_events = collections.defaultdict(list)
|
||||
self.linter = linter
|
||||
|
||||
def _is_method_enabled(self, method):
|
||||
if not hasattr(method, "checks_msgs"):
|
||||
return True
|
||||
for msg_desc in method.checks_msgs:
|
||||
if self.linter.is_message_enabled(msg_desc):
|
||||
return True
|
||||
return False
|
||||
|
||||
def add_checker(self, checker):
|
||||
"""walk to the checker's dir and collect visit and leave methods"""
|
||||
vcids = set()
|
||||
lcids = set()
|
||||
visits = self.visit_events
|
||||
leaves = self.leave_events
|
||||
for member in dir(checker):
|
||||
cid = member[6:]
|
||||
if cid == "default":
|
||||
continue
|
||||
if member.startswith("visit_"):
|
||||
v_meth = getattr(checker, member)
|
||||
# don't use visit_methods with no activated message:
|
||||
if self._is_method_enabled(v_meth):
|
||||
visits[cid].append(v_meth)
|
||||
vcids.add(cid)
|
||||
elif member.startswith("leave_"):
|
||||
l_meth = getattr(checker, member)
|
||||
# don't use leave_methods with no activated message:
|
||||
if self._is_method_enabled(l_meth):
|
||||
leaves[cid].append(l_meth)
|
||||
lcids.add(cid)
|
||||
visit_default = getattr(checker, "visit_default", None)
|
||||
if visit_default:
|
||||
for cls in nodes.ALL_NODE_CLASSES:
|
||||
cid = cls.__name__.lower()
|
||||
if cid not in vcids:
|
||||
visits[cid].append(visit_default)
|
||||
# for now we have no "leave_default" method in Pylint
|
||||
|
||||
def walk(self, astroid):
|
||||
"""call visit events of astroid checkers for the given node, recurse on
|
||||
its children, then leave events.
|
||||
"""
|
||||
cid = astroid.__class__.__name__.lower()
|
||||
|
||||
# Detect if the node is a new name for a deprecated alias.
|
||||
# In this case, favour the methods for the deprecated
|
||||
# alias if any, in order to maintain backwards
|
||||
# compatibility.
|
||||
visit_events = self.visit_events.get(cid, ())
|
||||
leave_events = self.leave_events.get(cid, ())
|
||||
|
||||
if astroid.is_statement:
|
||||
self.nbstatements += 1
|
||||
# generate events for this node on each checker
|
||||
for callback in visit_events or ():
|
||||
callback(astroid)
|
||||
# recurse on children
|
||||
for child in astroid.get_children():
|
||||
self.walk(child)
|
||||
for callback in leave_events or ():
|
||||
callback(astroid)
|
||||
@@ -0,0 +1,136 @@
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
import collections
|
||||
|
||||
from astroid import nodes
|
||||
|
||||
from pylint.constants import MSG_STATE_SCOPE_MODULE, WarningScope
|
||||
|
||||
|
||||
class FileState:
|
||||
"""Hold internal state specific to the currently analyzed file"""
|
||||
|
||||
def __init__(self, modname=None):
|
||||
self.base_name = modname
|
||||
self._module_msgs_state = {}
|
||||
self._raw_module_msgs_state = {}
|
||||
self._ignored_msgs = collections.defaultdict(set)
|
||||
self._suppression_mapping = {}
|
||||
self._effective_max_line_number = None
|
||||
|
||||
def collect_block_lines(self, msgs_store, module_node):
|
||||
"""Walk the AST to collect block level options line numbers."""
|
||||
for msg, lines in self._module_msgs_state.items():
|
||||
self._raw_module_msgs_state[msg] = lines.copy()
|
||||
orig_state = self._module_msgs_state.copy()
|
||||
self._module_msgs_state = {}
|
||||
self._suppression_mapping = {}
|
||||
self._effective_max_line_number = module_node.tolineno
|
||||
self._collect_block_lines(msgs_store, module_node, orig_state)
|
||||
|
||||
def _collect_block_lines(self, msgs_store, node, msg_state):
|
||||
"""Recursively walk (depth first) AST to collect block level options
|
||||
line numbers.
|
||||
"""
|
||||
for child in node.get_children():
|
||||
self._collect_block_lines(msgs_store, child, msg_state)
|
||||
first = node.fromlineno
|
||||
last = node.tolineno
|
||||
# first child line number used to distinguish between disable
|
||||
# which are the first child of scoped node with those defined later.
|
||||
# For instance in the code below:
|
||||
#
|
||||
# 1. def meth8(self):
|
||||
# 2. """test late disabling"""
|
||||
# 3. pylint: disable=not-callable
|
||||
# 4. print(self.blip)
|
||||
# 5. pylint: disable=no-member
|
||||
# 6. print(self.bla)
|
||||
#
|
||||
# E1102 should be disabled from line 1 to 6 while E1101 from line 5 to 6
|
||||
#
|
||||
# this is necessary to disable locally messages applying to class /
|
||||
# function using their fromlineno
|
||||
if (
|
||||
isinstance(node, (nodes.Module, nodes.ClassDef, nodes.FunctionDef))
|
||||
and node.body
|
||||
):
|
||||
firstchildlineno = node.body[0].fromlineno
|
||||
else:
|
||||
firstchildlineno = last
|
||||
for msgid, lines in msg_state.items():
|
||||
for lineno, state in list(lines.items()):
|
||||
original_lineno = lineno
|
||||
if first > lineno or last < lineno:
|
||||
continue
|
||||
# Set state for all lines for this block, if the
|
||||
# warning is applied to nodes.
|
||||
message_definitions = msgs_store.get_message_definitions(msgid)
|
||||
for message_definition in message_definitions:
|
||||
if message_definition.scope == WarningScope.NODE:
|
||||
if lineno > firstchildlineno:
|
||||
state = True
|
||||
first_, last_ = node.block_range(lineno)
|
||||
else:
|
||||
first_ = lineno
|
||||
last_ = last
|
||||
for line in range(first_, last_ + 1):
|
||||
# do not override existing entries
|
||||
if line in self._module_msgs_state.get(msgid, ()):
|
||||
continue
|
||||
if line in lines: # state change in the same block
|
||||
state = lines[line]
|
||||
original_lineno = line
|
||||
if not state:
|
||||
self._suppression_mapping[(msgid, line)] = original_lineno
|
||||
try:
|
||||
self._module_msgs_state[msgid][line] = state
|
||||
except KeyError:
|
||||
self._module_msgs_state[msgid] = {line: state}
|
||||
del lines[lineno]
|
||||
|
||||
def set_msg_status(self, msg, line, status):
|
||||
"""Set status (enabled/disable) for a given message at a given line"""
|
||||
assert line > 0
|
||||
try:
|
||||
self._module_msgs_state[msg.msgid][line] = status
|
||||
except KeyError:
|
||||
self._module_msgs_state[msg.msgid] = {line: status}
|
||||
|
||||
def handle_ignored_message(
|
||||
self, state_scope, msgid, line, node, args, confidence
|
||||
): # pylint: disable=unused-argument
|
||||
"""Report an ignored message.
|
||||
|
||||
state_scope is either MSG_STATE_SCOPE_MODULE or MSG_STATE_SCOPE_CONFIG,
|
||||
depending on whether the message was disabled locally in the module,
|
||||
or globally. The other arguments are the same as for add_message.
|
||||
"""
|
||||
if state_scope == MSG_STATE_SCOPE_MODULE:
|
||||
try:
|
||||
orig_line = self._suppression_mapping[(msgid, line)]
|
||||
self._ignored_msgs[(msgid, orig_line)].add(line)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def iter_spurious_suppression_messages(self, msgs_store):
|
||||
for warning, lines in self._raw_module_msgs_state.items():
|
||||
for line, enable in lines.items():
|
||||
if not enable and (warning, line) not in self._ignored_msgs:
|
||||
# ignore cyclic-import check which can show false positives
|
||||
# here due to incomplete context
|
||||
if warning != "R0401":
|
||||
yield "useless-suppression", line, (
|
||||
msgs_store.get_msg_display_string(warning),
|
||||
)
|
||||
# don't use iteritems here, _ignored_msgs may be modified by add_message
|
||||
for (warning, from_), lines in list(self._ignored_msgs.items()):
|
||||
for line in lines:
|
||||
yield "suppressed-message", line, (
|
||||
msgs_store.get_msg_display_string(warning),
|
||||
from_,
|
||||
)
|
||||
|
||||
def get_effective_max_line_number(self):
|
||||
return self._effective_max_line_number
|
||||
@@ -0,0 +1,132 @@
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
import re
|
||||
from collections import namedtuple
|
||||
from typing import Generator, List
|
||||
|
||||
# Allow stopping after the first semicolon/hash encountered,
|
||||
# so that an option can be continued with the reasons
|
||||
# why it is active or disabled.
|
||||
OPTION_RGX = r"""
|
||||
\s* # Any number of whithespace
|
||||
\#? # One or zero hash
|
||||
.* # Anything (as much as possible)
|
||||
(\s* # Beginning of first matched group and any number of whitespaces
|
||||
\# # Beginning of comment
|
||||
.*? # Anything (as little as possible)
|
||||
\bpylint: # pylint word and column
|
||||
\s* # Any number of whitespaces
|
||||
([^;#\n]+)) # Anything except semicolon or hash or newline (it is the second matched group)
|
||||
# and end of the first matched group
|
||||
[;#]{0,1}""" # From 0 to 1 repetition of semicolon or hash
|
||||
OPTION_PO = re.compile(OPTION_RGX, re.VERBOSE)
|
||||
|
||||
|
||||
PragmaRepresenter = namedtuple("PragmaRepresenter", "action messages")
|
||||
|
||||
|
||||
ATOMIC_KEYWORDS = frozenset(("disable-all", "skip-file"))
|
||||
MESSAGE_KEYWORDS = frozenset(("disable-msg", "enable-msg", "disable", "enable"))
|
||||
# sorted is necessary because sets are unordered collections and ALL_KEYWORDS
|
||||
# string should not vary between executions
|
||||
# reverse is necessary in order to have the longest keywords first, so that, for example,
|
||||
# 'disable' string should not be matched instead of 'disable-all'
|
||||
ALL_KEYWORDS = "|".join(
|
||||
sorted(ATOMIC_KEYWORDS | MESSAGE_KEYWORDS, key=len, reverse=True)
|
||||
)
|
||||
|
||||
|
||||
TOKEN_SPECIFICATION = [
|
||||
("KEYWORD", r"\b({:s})\b".format(ALL_KEYWORDS)),
|
||||
("MESSAGE_STRING", r"[0-9A-Za-z\-\_]{2,}"), # Identifiers
|
||||
("ASSIGN", r"="), # Assignment operator
|
||||
("MESSAGE_NUMBER", r"[CREIWF]{1}\d*"),
|
||||
]
|
||||
|
||||
TOK_REGEX = "|".join(
|
||||
"(?P<{:s}>{:s})".format(token_name, token_rgx)
|
||||
for token_name, token_rgx in TOKEN_SPECIFICATION
|
||||
)
|
||||
|
||||
|
||||
def emit_pragma_representer(action, messages):
|
||||
if not messages and action in MESSAGE_KEYWORDS:
|
||||
raise InvalidPragmaError(
|
||||
"The keyword is not followed by message identifier", action
|
||||
)
|
||||
return PragmaRepresenter(action, messages)
|
||||
|
||||
|
||||
class PragmaParserError(Exception):
|
||||
"""
|
||||
A class for exceptions thrown by pragma_parser module
|
||||
"""
|
||||
|
||||
def __init__(self, message, token):
|
||||
"""
|
||||
:args message: explain the reason why the exception has been thrown
|
||||
:args token: token concerned by the exception
|
||||
"""
|
||||
self.message = message
|
||||
self.token = token
|
||||
super().__init__(self.message)
|
||||
|
||||
|
||||
class UnRecognizedOptionError(PragmaParserError):
|
||||
"""
|
||||
Thrown in case the of a valid but unrecognized option
|
||||
"""
|
||||
|
||||
|
||||
class InvalidPragmaError(PragmaParserError):
|
||||
"""
|
||||
Thrown in case the pragma is invalid
|
||||
"""
|
||||
|
||||
|
||||
def parse_pragma(pylint_pragma: str) -> Generator[PragmaRepresenter, None, None]:
|
||||
action = None
|
||||
messages = [] # type: List[str]
|
||||
assignment_required = False
|
||||
previous_token = ""
|
||||
|
||||
for mo in re.finditer(TOK_REGEX, pylint_pragma):
|
||||
kind = mo.lastgroup
|
||||
value = mo.group()
|
||||
|
||||
if kind == "ASSIGN":
|
||||
if not assignment_required:
|
||||
if action:
|
||||
# A keyword has been found previously but doesn't support assignement
|
||||
raise UnRecognizedOptionError(
|
||||
"The keyword doesn't support assignment", action
|
||||
)
|
||||
if previous_token:
|
||||
# Something found previously but not a known keyword
|
||||
raise UnRecognizedOptionError(
|
||||
"The keyword is unknown", previous_token
|
||||
)
|
||||
# Nothing at all detected before this assignment
|
||||
raise InvalidPragmaError("Missing keyword before assignment", "")
|
||||
assignment_required = False
|
||||
elif assignment_required:
|
||||
raise InvalidPragmaError("The = sign is missing after the keyword", action)
|
||||
elif kind == "KEYWORD":
|
||||
if action:
|
||||
yield emit_pragma_representer(action, messages)
|
||||
action = value
|
||||
messages = list()
|
||||
assignment_required = action in MESSAGE_KEYWORDS
|
||||
elif kind in ("MESSAGE_STRING", "MESSAGE_NUMBER"):
|
||||
messages.append(value)
|
||||
assignment_required = False
|
||||
else:
|
||||
raise RuntimeError("Token not recognized")
|
||||
|
||||
previous_token = value
|
||||
|
||||
if action:
|
||||
yield emit_pragma_representer(action, messages)
|
||||
else:
|
||||
raise UnRecognizedOptionError("The keyword is unknown", previous_token)
|
||||
@@ -0,0 +1,400 @@
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
import codecs
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import textwrap
|
||||
import tokenize
|
||||
|
||||
from astroid import Module, modutils
|
||||
|
||||
from pylint.constants import PY_EXTS
|
||||
|
||||
DEFAULT_LINE_LENGTH = 79
|
||||
|
||||
|
||||
def normalize_text(text, line_len=DEFAULT_LINE_LENGTH, indent=""):
|
||||
"""Wrap the text on the given line length."""
|
||||
return "\n".join(
|
||||
textwrap.wrap(
|
||||
text, width=line_len, initial_indent=indent, subsequent_indent=indent
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def get_module_and_frameid(node):
|
||||
"""return the module name and the frame id in the module"""
|
||||
frame = node.frame()
|
||||
module, obj = "", []
|
||||
while frame:
|
||||
if isinstance(frame, Module):
|
||||
module = frame.name
|
||||
else:
|
||||
obj.append(getattr(frame, "name", "<lambda>"))
|
||||
try:
|
||||
frame = frame.parent.frame()
|
||||
except AttributeError:
|
||||
frame = None
|
||||
obj.reverse()
|
||||
return module, ".".join(obj)
|
||||
|
||||
|
||||
def get_rst_title(title, character):
|
||||
"""Permit to get a title formatted as ReStructuredText test (underlined with a chosen character)."""
|
||||
return "%s\n%s\n" % (title, character * len(title))
|
||||
|
||||
|
||||
def get_rst_section(section, options, doc=None):
|
||||
"""format an options section using as a ReStructuredText formatted output"""
|
||||
result = ""
|
||||
if section:
|
||||
result += get_rst_title(section, "'")
|
||||
if doc:
|
||||
formatted_doc = normalize_text(doc)
|
||||
result += "%s\n\n" % formatted_doc
|
||||
for optname, optdict, value in options:
|
||||
help_opt = optdict.get("help")
|
||||
result += ":%s:\n" % optname
|
||||
if help_opt:
|
||||
formatted_help = normalize_text(help_opt, indent=" ")
|
||||
result += "%s\n" % formatted_help
|
||||
if value:
|
||||
value = str(_format_option_value(optdict, value))
|
||||
result += "\n Default: ``%s``\n" % value.replace("`` ", "```` ``")
|
||||
return result
|
||||
|
||||
|
||||
def safe_decode(line, encoding, *args, **kwargs):
|
||||
"""return decoded line from encoding or decode with default encoding"""
|
||||
try:
|
||||
return line.decode(encoding or sys.getdefaultencoding(), *args, **kwargs)
|
||||
except LookupError:
|
||||
return line.decode(sys.getdefaultencoding(), *args, **kwargs)
|
||||
|
||||
|
||||
def decoding_stream(stream, encoding, errors="strict"):
|
||||
try:
|
||||
reader_cls = codecs.getreader(encoding or sys.getdefaultencoding())
|
||||
except LookupError:
|
||||
reader_cls = codecs.getreader(sys.getdefaultencoding())
|
||||
return reader_cls(stream, errors)
|
||||
|
||||
|
||||
def tokenize_module(module):
|
||||
with module.stream() as stream:
|
||||
readline = stream.readline
|
||||
return list(tokenize.tokenize(readline))
|
||||
|
||||
|
||||
def _basename_in_blacklist_re(base_name, black_list_re):
|
||||
"""Determines if the basename is matched in a regex blacklist
|
||||
|
||||
:param str base_name: The basename of the file
|
||||
:param list black_list_re: A collection of regex patterns to match against.
|
||||
Successful matches are blacklisted.
|
||||
|
||||
:returns: `True` if the basename is blacklisted, `False` otherwise.
|
||||
:rtype: bool
|
||||
"""
|
||||
for file_pattern in black_list_re:
|
||||
if file_pattern.match(base_name):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _modpath_from_file(filename, is_namespace, path=None):
|
||||
def _is_package_cb(path, parts):
|
||||
return modutils.check_modpath_has_init(path, parts) or is_namespace
|
||||
|
||||
return modutils.modpath_from_file_with_callback(
|
||||
filename, path=path, is_package_cb=_is_package_cb
|
||||
)
|
||||
|
||||
|
||||
def get_python_path(filepath):
|
||||
dirname = os.path.realpath(os.path.expanduser(filepath))
|
||||
if not os.path.isdir(dirname):
|
||||
dirname = os.path.dirname(dirname)
|
||||
while True:
|
||||
if not os.path.exists(os.path.join(dirname, "__init__.py")):
|
||||
return dirname
|
||||
old_dirname = dirname
|
||||
dirname = os.path.dirname(dirname)
|
||||
if old_dirname == dirname:
|
||||
return os.getcwd()
|
||||
return None
|
||||
|
||||
|
||||
def expand_modules(files_or_modules, black_list, black_list_re):
|
||||
"""take a list of files/modules/packages and return the list of tuple
|
||||
(file, module name) which have to be actually checked
|
||||
"""
|
||||
result = []
|
||||
errors = []
|
||||
path = sys.path.copy()
|
||||
|
||||
for something in files_or_modules:
|
||||
if os.path.basename(something) in black_list:
|
||||
continue
|
||||
if _basename_in_blacklist_re(os.path.basename(something), black_list_re):
|
||||
continue
|
||||
|
||||
module_path = get_python_path(something)
|
||||
additional_search_path = [".", module_path] + path
|
||||
if os.path.exists(something):
|
||||
# this is a file or a directory
|
||||
try:
|
||||
modname = ".".join(
|
||||
modutils.modpath_from_file(something, path=additional_search_path)
|
||||
)
|
||||
except ImportError:
|
||||
modname = os.path.splitext(os.path.basename(something))[0]
|
||||
if os.path.isdir(something):
|
||||
filepath = os.path.join(something, "__init__.py")
|
||||
else:
|
||||
filepath = something
|
||||
else:
|
||||
# suppose it's a module or package
|
||||
modname = something
|
||||
try:
|
||||
filepath = modutils.file_from_modpath(
|
||||
modname.split("."), path=additional_search_path
|
||||
)
|
||||
if filepath is None:
|
||||
continue
|
||||
except (ImportError, SyntaxError) as ex:
|
||||
# The SyntaxError is a Python bug and should be
|
||||
# removed once we move away from imp.find_module: http://bugs.python.org/issue10588
|
||||
errors.append({"key": "fatal", "mod": modname, "ex": ex})
|
||||
continue
|
||||
|
||||
filepath = os.path.normpath(filepath)
|
||||
modparts = (modname or something).split(".")
|
||||
|
||||
try:
|
||||
spec = modutils.file_info_from_modpath(
|
||||
modparts, path=additional_search_path
|
||||
)
|
||||
except ImportError:
|
||||
# Might not be acceptable, don't crash.
|
||||
is_namespace = False
|
||||
is_directory = os.path.isdir(something)
|
||||
else:
|
||||
is_namespace = modutils.is_namespace(spec)
|
||||
is_directory = modutils.is_directory(spec)
|
||||
|
||||
if not is_namespace:
|
||||
result.append(
|
||||
{
|
||||
"path": filepath,
|
||||
"name": modname,
|
||||
"isarg": True,
|
||||
"basepath": filepath,
|
||||
"basename": modname,
|
||||
}
|
||||
)
|
||||
|
||||
has_init = (
|
||||
not (modname.endswith(".__init__") or modname == "__init__")
|
||||
and os.path.basename(filepath) == "__init__.py"
|
||||
)
|
||||
if has_init or is_namespace or is_directory:
|
||||
for subfilepath in modutils.get_module_files(
|
||||
os.path.dirname(filepath), black_list, list_all=is_namespace
|
||||
):
|
||||
if filepath == subfilepath:
|
||||
continue
|
||||
if _basename_in_blacklist_re(
|
||||
os.path.basename(subfilepath), black_list_re
|
||||
):
|
||||
continue
|
||||
|
||||
modpath = _modpath_from_file(
|
||||
subfilepath, is_namespace, path=additional_search_path
|
||||
)
|
||||
submodname = ".".join(modpath)
|
||||
result.append(
|
||||
{
|
||||
"path": subfilepath,
|
||||
"name": submodname,
|
||||
"isarg": False,
|
||||
"basepath": filepath,
|
||||
"basename": modname,
|
||||
}
|
||||
)
|
||||
return result, errors
|
||||
|
||||
|
||||
def register_plugins(linter, directory):
|
||||
"""load all module and package in the given directory, looking for a
|
||||
'register' function in each one, used to register pylint checkers
|
||||
"""
|
||||
imported = {}
|
||||
for filename in os.listdir(directory):
|
||||
base, extension = os.path.splitext(filename)
|
||||
if base in imported or base == "__pycache__":
|
||||
continue
|
||||
if (
|
||||
extension in PY_EXTS
|
||||
and base != "__init__"
|
||||
or (not extension and os.path.isdir(os.path.join(directory, base)))
|
||||
):
|
||||
try:
|
||||
module = modutils.load_module_from_file(
|
||||
os.path.join(directory, filename)
|
||||
)
|
||||
except ValueError:
|
||||
# empty module name (usually emacs auto-save files)
|
||||
continue
|
||||
except ImportError as exc:
|
||||
print(
|
||||
"Problem importing module %s: %s" % (filename, exc), file=sys.stderr
|
||||
)
|
||||
else:
|
||||
if hasattr(module, "register"):
|
||||
module.register(linter)
|
||||
imported[base] = 1
|
||||
|
||||
|
||||
def get_global_option(checker, option, default=None):
|
||||
""" Retrieve an option defined by the given *checker* or
|
||||
by all known option providers.
|
||||
|
||||
It will look in the list of all options providers
|
||||
until the given *option* will be found.
|
||||
If the option wasn't found, the *default* value will be returned.
|
||||
"""
|
||||
# First, try in the given checker's config.
|
||||
# After that, look in the options providers.
|
||||
|
||||
try:
|
||||
return getattr(checker.config, option.replace("-", "_"))
|
||||
except AttributeError:
|
||||
pass
|
||||
for provider in checker.linter.options_providers:
|
||||
for options in provider.options:
|
||||
if options[0] == option:
|
||||
return getattr(provider.config, option.replace("-", "_"))
|
||||
return default
|
||||
|
||||
|
||||
def deprecated_option(
|
||||
shortname=None, opt_type=None, help_msg=None, deprecation_msg=None
|
||||
):
|
||||
def _warn_deprecated(option, optname, *args): # pylint: disable=unused-argument
|
||||
if deprecation_msg:
|
||||
sys.stderr.write(deprecation_msg % (optname,))
|
||||
|
||||
option = {
|
||||
"help": help_msg,
|
||||
"hide": True,
|
||||
"type": opt_type,
|
||||
"action": "callback",
|
||||
"callback": _warn_deprecated,
|
||||
"deprecated": True,
|
||||
}
|
||||
if shortname:
|
||||
option["shortname"] = shortname
|
||||
return option
|
||||
|
||||
|
||||
def _splitstrip(string, sep=","):
|
||||
"""return a list of stripped string by splitting the string given as
|
||||
argument on `sep` (',' by default). Empty string are discarded.
|
||||
|
||||
>>> _splitstrip('a, b, c , 4,,')
|
||||
['a', 'b', 'c', '4']
|
||||
>>> _splitstrip('a')
|
||||
['a']
|
||||
>>> _splitstrip('a,\nb,\nc,')
|
||||
['a', 'b', 'c']
|
||||
|
||||
:type string: str or unicode
|
||||
:param string: a csv line
|
||||
|
||||
:type sep: str or unicode
|
||||
:param sep: field separator, default to the comma (',')
|
||||
|
||||
:rtype: str or unicode
|
||||
:return: the unquoted string (or the input string if it wasn't quoted)
|
||||
"""
|
||||
return [word.strip() for word in string.split(sep) if word.strip()]
|
||||
|
||||
|
||||
def _unquote(string):
|
||||
"""remove optional quotes (simple or double) from the string
|
||||
|
||||
:type string: str or unicode
|
||||
:param string: an optionally quoted string
|
||||
|
||||
:rtype: str or unicode
|
||||
:return: the unquoted string (or the input string if it wasn't quoted)
|
||||
"""
|
||||
if not string:
|
||||
return string
|
||||
if string[0] in "\"'":
|
||||
string = string[1:]
|
||||
if string[-1] in "\"'":
|
||||
string = string[:-1]
|
||||
return string
|
||||
|
||||
|
||||
def _check_csv(value):
|
||||
if isinstance(value, (list, tuple)):
|
||||
return value
|
||||
return _splitstrip(value)
|
||||
|
||||
|
||||
def _comment(string):
|
||||
"""return string as a comment"""
|
||||
lines = [line.strip() for line in string.splitlines()]
|
||||
return "# " + ("%s# " % os.linesep).join(lines)
|
||||
|
||||
|
||||
def _format_option_value(optdict, value):
|
||||
"""return the user input's value from a 'compiled' value"""
|
||||
if isinstance(value, (list, tuple)):
|
||||
value = ",".join(_format_option_value(optdict, item) for item in value)
|
||||
elif isinstance(value, dict):
|
||||
value = ",".join("%s:%s" % (k, v) for k, v in value.items())
|
||||
elif hasattr(value, "match"): # optdict.get('type') == 'regexp'
|
||||
# compiled regexp
|
||||
value = value.pattern
|
||||
elif optdict.get("type") == "yn":
|
||||
value = "yes" if value else "no"
|
||||
elif isinstance(value, str) and value.isspace():
|
||||
value = "'%s'" % value
|
||||
return value
|
||||
|
||||
|
||||
def format_section(stream, section, options, doc=None):
|
||||
"""format an options section using the INI format"""
|
||||
if doc:
|
||||
print(_comment(doc), file=stream)
|
||||
print("[%s]" % section, file=stream)
|
||||
_ini_format(stream, options)
|
||||
|
||||
|
||||
def _ini_format(stream, options):
|
||||
"""format options using the INI format"""
|
||||
for optname, optdict, value in options:
|
||||
value = _format_option_value(optdict, value)
|
||||
help_opt = optdict.get("help")
|
||||
if help_opt:
|
||||
help_opt = normalize_text(help_opt, indent="# ")
|
||||
print(file=stream)
|
||||
print(help_opt, file=stream)
|
||||
else:
|
||||
print(file=stream)
|
||||
if value is None:
|
||||
print("#%s=" % optname, file=stream)
|
||||
else:
|
||||
value = str(value).strip()
|
||||
if re.match(r"^([\w-]+,)+[\w-]+$", str(value)):
|
||||
separator = "\n " + " " * len(optname)
|
||||
value = separator.join(x + "," for x in str(value).split(","))
|
||||
# remove trailing ',' from last element of the list
|
||||
value = value[:-1]
|
||||
print("%s=%s" % (optname, value), file=stream)
|
||||
Reference in New Issue
Block a user