Initial commit
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
# Copyright (c) 2008, 2012 LOGILAB S.A. (Paris, FRANCE) <[email protected]>
|
||||
# Copyright (c) 2014, 2016-2017 Claudiu Popa <[email protected]>
|
||||
# Copyright (c) 2014 Arun Persaud <[email protected]>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <[email protected]>
|
||||
# Copyright (c) 2018 Nick Drozd <[email protected]>
|
||||
|
||||
# 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 .__pkginfo__ import version as __version__
|
||||
|
||||
|
||||
def run_pylint():
|
||||
"""run pylint"""
|
||||
from pylint.lint import Run
|
||||
|
||||
try:
|
||||
Run(sys.argv[1:])
|
||||
except KeyboardInterrupt:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def run_epylint():
|
||||
"""run pylint"""
|
||||
from pylint.epylint import Run
|
||||
|
||||
Run()
|
||||
|
||||
|
||||
def run_pyreverse():
|
||||
"""run pyreverse"""
|
||||
from pylint.pyreverse.main import Run
|
||||
|
||||
Run(sys.argv[1:])
|
||||
|
||||
|
||||
def run_symilar():
|
||||
"""run symilar"""
|
||||
from pylint.checkers.similar import Run
|
||||
|
||||
Run(sys.argv[1:])
|
||||
@@ -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
|
||||
|
||||
#!/usr/bin/env python
|
||||
import pylint
|
||||
|
||||
pylint.run_pylint()
|
||||
@@ -0,0 +1,93 @@
|
||||
# Copyright (c) 2006-2015 LOGILAB S.A. (Paris, FRANCE) <[email protected]>
|
||||
# Copyright (c) 2010 Julien Jehannet <[email protected]>
|
||||
# Copyright (c) 2013-2014 Google, Inc.
|
||||
# Copyright (c) 2014-2018 Claudiu Popa <[email protected]>
|
||||
# Copyright (c) 2014 Brett Cannon <[email protected]>
|
||||
# Copyright (c) 2014 Ricardo Gemignani <[email protected]>
|
||||
# Copyright (c) 2014 Arun Persaud <[email protected]>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <[email protected]>
|
||||
# Copyright (c) 2016 Moises Lopez <[email protected]>
|
||||
# Copyright (c) 2016 Florian Bruhin <[email protected]>
|
||||
# Copyright (c) 2016 Jakub Wilk <[email protected]>
|
||||
# Copyright (c) 2017-2018 Hugo <[email protected]>
|
||||
# Copyright (c) 2018 Sushobhit <[email protected]>
|
||||
# Copyright (c) 2018 Ashley Whetter <[email protected]>
|
||||
|
||||
# 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=W0622,C0103
|
||||
"""pylint packaging information"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
from os.path import join
|
||||
|
||||
modname = distname = "pylint"
|
||||
|
||||
numversion = (2, 3, 1)
|
||||
dev_version = None
|
||||
string_version = ".".join(str(num) for num in numversion)
|
||||
|
||||
if dev_version:
|
||||
version = string_version + "-dev" + str(dev_version)
|
||||
else:
|
||||
version = string_version
|
||||
|
||||
install_requires = ["astroid>=2.2.0,<3", "isort>=4.2.5,<5", "mccabe>=0.6,<0.7"]
|
||||
|
||||
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:[email protected]"
|
||||
author = "Python Code Quality Authority"
|
||||
author_email = "[email protected]"
|
||||
|
||||
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.4",
|
||||
"Programming Language :: Python :: 3.5",
|
||||
"Programming Language :: Python :: 3.6",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"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")
|
||||
]
|
||||
|
||||
include_dirs = [join("pylint", "test")]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,134 @@
|
||||
# Copyright (c) 2006-2014 LOGILAB S.A. (Paris, FRANCE) <[email protected]>
|
||||
# Copyright (c) 2013-2014 Google, Inc.
|
||||
# Copyright (c) 2013 [email protected] <[email protected]>
|
||||
# Copyright (c) 2014-2017 Claudiu Popa <[email protected]>
|
||||
# Copyright (c) 2014 Brett Cannon <[email protected]>
|
||||
# Copyright (c) 2014 Arun Persaud <[email protected]>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <[email protected]>
|
||||
# Copyright (c) 2016 Moises Lopez <[email protected]>
|
||||
# Copyright (c) 2017-2018 Bryce Guinta <[email protected]>
|
||||
|
||||
# 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 !
|
||||
|
||||
"""
|
||||
|
||||
import sys
|
||||
import tokenize
|
||||
import warnings
|
||||
from typing import Any
|
||||
|
||||
from pylint.config import OptionsProviderMixIn
|
||||
from pylint.reporters import diff_string
|
||||
from pylint.utils import register_plugins
|
||||
from pylint.interfaces import UNDEFINED
|
||||
|
||||
|
||||
def table_lines_from_stats(stats, old_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]
|
||||
format = str # pylint: disable=redefined-builtin
|
||||
if isinstance(new, float):
|
||||
format = lambda num: "%.3f" % num
|
||||
old = old_stats.get(m_type)
|
||||
if old is not None:
|
||||
diff_str = diff_string(old, new)
|
||||
old = format(old)
|
||||
else:
|
||||
old, diff_str = "NC", "NC"
|
||||
lines += (m_type.replace("_", " "), format(new), old, diff_str)
|
||||
return lines
|
||||
|
||||
|
||||
class BaseChecker(OptionsProviderMixIn):
|
||||
"""base class for checkers"""
|
||||
|
||||
# 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 ckecker 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
|
||||
|
||||
linter is an object implementing ILinter
|
||||
"""
|
||||
if self.name is not None:
|
||||
self.name = self.name.lower()
|
||||
OptionsProviderMixIn.__init__(self)
|
||||
self.linter = linter
|
||||
|
||||
def add_message(
|
||||
self,
|
||||
msg_id,
|
||||
line=None,
|
||||
node=None,
|
||||
args=None,
|
||||
confidence=UNDEFINED,
|
||||
col_offset=None,
|
||||
):
|
||||
"""add a message of a given type"""
|
||||
self.linter.add_message(msg_id, line, node, args, confidence, col_offset)
|
||||
|
||||
# dummy methods implementing the IChecker interface
|
||||
|
||||
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()
|
||||
|
||||
|
||||
def initialize(linter):
|
||||
"""initialize linter with checkers in this package """
|
||||
register_plugins(linter, __path__[0])
|
||||
|
||||
|
||||
__all__ = ("BaseChecker", "BaseTokenChecker", "initialize")
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,92 @@
|
||||
# Copyright (c) 2015-2018 Claudiu Popa <[email protected]>
|
||||
# Copyright (c) 2017 Derek Gustafson <[email protected]>
|
||||
|
||||
# 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
|
||||
from astroid import exceptions
|
||||
|
||||
from pylint import checkers
|
||||
from pylint.checkers import utils as checker_utils
|
||||
from pylint import interfaces
|
||||
from pylint import 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
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,530 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2006, 2009-2010, 2012-2015 LOGILAB S.A. (Paris, FRANCE) <[email protected]>
|
||||
# Copyright (c) 2012, 2014 Google, Inc.
|
||||
# Copyright (c) 2014-2018 Claudiu Popa <[email protected]>
|
||||
# Copyright (c) 2014 Arun Persaud <[email protected]>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <[email protected]>
|
||||
# Copyright (c) 2016 Łukasz Rogalski <[email protected]>
|
||||
# Copyright (c) 2017 ahirnish <[email protected]>
|
||||
# Copyright (c) 2018 Mike Frysinger <[email protected]>
|
||||
# Copyright (c) 2018 Mark Miller <[email protected]>
|
||||
# Copyright (c) 2018 Ashley Whetter <[email protected]>
|
||||
# Copyright (c) 2018 Ville Skyttä <[email protected]>
|
||||
# Copyright (c) 2018 Jakub Wilk <[email protected]>
|
||||
|
||||
# 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"""
|
||||
|
||||
from collections import defaultdict
|
||||
import re
|
||||
|
||||
import astroid
|
||||
from astroid import If, BoolOp
|
||||
from astroid import decorators
|
||||
|
||||
from pylint.interfaces import IAstroidChecker
|
||||
from pylint.checkers import BaseChecker
|
||||
from pylint.checkers.utils import check_messages
|
||||
from pylint import utils
|
||||
|
||||
|
||||
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}$")
|
||||
DATACLASS_DECORATOR = "dataclass"
|
||||
DATACLASS_IMPORT = "dataclasses"
|
||||
TYPING_NAMEDTUPLE = "typing.NamedTuple"
|
||||
|
||||
|
||||
def _is_typing_namedtuple(node: astroid.ClassDef) -> bool:
|
||||
"""Check if a class node is a typing.NamedTuple class"""
|
||||
for base in node.ancestors():
|
||||
if base.qname() == TYPING_NAMEDTUPLE:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _is_enum_class(node: astroid.ClassDef) -> bool:
|
||||
"""Check if a class definition defines an Enum class.
|
||||
|
||||
:param node: The class node to check.
|
||||
:type node: astroid.ClassDef
|
||||
|
||||
:returns: True if the given node represents an Enum class. False otherwise.
|
||||
:rtype: bool
|
||||
"""
|
||||
for base in node.bases:
|
||||
try:
|
||||
inferred_bases = base.inferred()
|
||||
except astroid.InferenceError:
|
||||
continue
|
||||
|
||||
for ancestor in inferred_bases:
|
||||
if not isinstance(ancestor, astroid.ClassDef):
|
||||
continue
|
||||
|
||||
if ancestor.name == "Enum" and ancestor.root().name == "enum":
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _is_dataclass(node: astroid.ClassDef) -> bool:
|
||||
"""Check if a class definition defines a Python 3.7+ dataclass
|
||||
|
||||
:param node: The class node to check.
|
||||
:type node: astroid.ClassDef
|
||||
|
||||
:returns: True if the given node represents a dataclass class. False otherwise.
|
||||
:rtype: bool
|
||||
"""
|
||||
if not node.decorators:
|
||||
return False
|
||||
|
||||
root_locals = 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 == DATACLASS_DECORATOR and DATACLASS_DECORATOR 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.",
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
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_enum_class(node)
|
||||
or _is_dataclass(node)
|
||||
or _is_typing_namedtuple(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,561 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2006-2011, 2013-2014 LOGILAB S.A. (Paris, FRANCE) <[email protected]>
|
||||
# Copyright (c) 2011-2014 Google, Inc.
|
||||
# Copyright (c) 2012 Tim Hatch <[email protected]>
|
||||
# Copyright (c) 2013-2018 Claudiu Popa <[email protected]>
|
||||
# Copyright (c) 2014 Brett Cannon <[email protected]>
|
||||
# Copyright (c) 2014 Arun Persaud <[email protected]>
|
||||
# Copyright (c) 2015 Rene Zhang <[email protected]>
|
||||
# Copyright (c) 2015 Florian Bruhin <[email protected]>
|
||||
# Copyright (c) 2015 Steven Myint <[email protected]>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <[email protected]>
|
||||
# Copyright (c) 2016 Erik <[email protected]>
|
||||
# Copyright (c) 2016 Jakub Wilk <[email protected]>
|
||||
# Copyright (c) 2017 Łukasz Rogalski <[email protected]>
|
||||
# Copyright (c) 2017 Martin von Gagern <[email protected]>
|
||||
# Copyright (c) 2018 Mike Frysinger <[email protected]>
|
||||
# Copyright (c) 2018 ssolanki <[email protected]>
|
||||
# Copyright (c) 2018 Alexander Todorov <[email protected]>
|
||||
# Copyright (c) 2018 Ville Skyttä <[email protected]>
|
||||
|
||||
# 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 sys
|
||||
import typing
|
||||
|
||||
import astroid
|
||||
|
||||
from pylint import checkers
|
||||
from pylint.checkers import utils
|
||||
from pylint import interfaces
|
||||
|
||||
|
||||
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', 'infered 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 infered in stmt.infer(context):
|
||||
if infered is astroid.Uninferable:
|
||||
continue
|
||||
yield stmt, infered
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
PY3K = sys.version_info >= (3, 0)
|
||||
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)
|
||||
else:
|
||||
self._checker.add_message("nonstandard-exception", node=self._node)
|
||||
|
||||
def visit_tuple(self, tuple_node):
|
||||
if PY3K or not tuple_node.elts:
|
||||
self._checker.add_message("raising-bad-type", node=self._node, args="tuple")
|
||||
return
|
||||
|
||||
# On Python 2, using the following is not an error:
|
||||
# raise (ZeroDivisionError, None)
|
||||
# raise (ZeroDivisionError, )
|
||||
# What's left to do is to check that the first
|
||||
# argument is indeed an exception. Verifying the other arguments
|
||||
# is not the scope of this check.
|
||||
first = tuple_node.elts[0]
|
||||
inferred = utils.safe_infer(first)
|
||||
if not inferred or inferred is astroid.Uninferable:
|
||||
return
|
||||
|
||||
if (
|
||||
isinstance(inferred, astroid.Instance)
|
||||
and inferred.__class__.__name__ != "Instance"
|
||||
):
|
||||
# TODO: explain why
|
||||
self.visit_default(tuple_node)
|
||||
else:
|
||||
self.visit(inferred)
|
||||
|
||||
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(ExceptionsChecker, self).open()
|
||||
|
||||
@utils.check_messages(
|
||||
"nonstandard-exception",
|
||||
"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 PY3K and 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 infered 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):
|
||||
exceptions = []
|
||||
if handler.type:
|
||||
exceptions_in_handler = utils.safe_infer(handler.type)
|
||||
if isinstance(exceptions_in_handler, astroid.Tuple):
|
||||
exceptions = {
|
||||
exception
|
||||
for exception in exceptions_in_handler.elts
|
||||
if isinstance(exception, astroid.Name)
|
||||
}
|
||||
elif exceptions_in_handler:
|
||||
exceptions = [exceptions_in_handler]
|
||||
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
|
||||
|
||||
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)
|
||||
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,934 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2006-2015 LOGILAB S.A. (Paris, FRANCE) <[email protected]>
|
||||
# Copyright (c) 2012-2014 Google, Inc.
|
||||
# Copyright (c) 2013 [email protected] <[email protected]>
|
||||
# Copyright (c) 2014-2018 Claudiu Popa <[email protected]>
|
||||
# Copyright (c) 2014 Brett Cannon <[email protected]>
|
||||
# Copyright (c) 2014 Arun Persaud <[email protected]>
|
||||
# Copyright (c) 2015-2016 Moises Lopez <[email protected]>
|
||||
# Copyright (c) 2015 Dmitry Pribysh <[email protected]>
|
||||
# Copyright (c) 2015 Cezar <[email protected]>
|
||||
# Copyright (c) 2015 Florian Bruhin <[email protected]>
|
||||
# Copyright (c) 2015 Noam Yorav-Raphael <[email protected]>
|
||||
# Copyright (c) 2015 James Morgensen <[email protected]>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <[email protected]>
|
||||
# Copyright (c) 2016 Jared Garst <[email protected]>
|
||||
# Copyright (c) 2016 Maik Röder <[email protected]>
|
||||
# Copyright (c) 2016 Glenn Matthews <[email protected]>
|
||||
# Copyright (c) 2016 Ashley Whetter <[email protected]>
|
||||
# Copyright (c) 2017 hippo91 <[email protected]>
|
||||
# Copyright (c) 2017 Michka Popoff <[email protected]>
|
||||
# Copyright (c) 2017 Łukasz Rogalski <[email protected]>
|
||||
# Copyright (c) 2017 Erik Wright <[email protected]>
|
||||
# Copyright (c) 2018 Mike Frysinger <[email protected]>
|
||||
# Copyright (c) 2018 Sushobhit <[email protected]>
|
||||
# Copyright (c) 2018 Marianna Polatoglou <[email protected]>
|
||||
|
||||
# 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
|
||||
from distutils import sysconfig
|
||||
import os
|
||||
import sys
|
||||
import copy
|
||||
|
||||
import astroid
|
||||
from astroid import are_exclusive, decorators
|
||||
from astroid.modutils import get_module_part, is_standard_module
|
||||
import isort
|
||||
|
||||
from pylint.interfaces import IAstroidChecker
|
||||
from pylint.utils import get_global_option
|
||||
from pylint.exceptions import EmptyReportError
|
||||
from pylint.checkers import BaseChecker
|
||||
from pylint.checkers.utils import (
|
||||
check_messages,
|
||||
node_ignores_exception,
|
||||
is_from_fallback_block,
|
||||
)
|
||||
from pylint.graph import get_cycles, DotBackend
|
||||
from pylint.reporters.ureports.nodes import VerbatimText, Paragraph
|
||||
|
||||
|
||||
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 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", "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.",
|
||||
),
|
||||
"W0403": (
|
||||
"Relative import %r, should be %r",
|
||||
"relative-import",
|
||||
"Used when an import relative to the package directory is detected.",
|
||||
{"maxversion": (3, 0)},
|
||||
),
|
||||
"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.",
|
||||
),
|
||||
"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",
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
DEFAULT_STANDARD_LIBRARY = ()
|
||||
DEFAULT_KNOWN_THIRD_PARTY = ("enchant",)
|
||||
|
||||
|
||||
class ImportsChecker(BaseChecker):
|
||||
"""checks for
|
||||
* external modules dependencies
|
||||
* relative / wildcard imports
|
||||
* cyclic imports
|
||||
* uses of deprecated modules
|
||||
"""
|
||||
|
||||
__implements__ = IAstroidChecker
|
||||
|
||||
name = "imports"
|
||||
msgs = MSGS
|
||||
priority = -2
|
||||
|
||||
if sys.version_info < (3, 5):
|
||||
deprecated_modules = ("optparse",)
|
||||
else:
|
||||
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.",
|
||||
},
|
||||
),
|
||||
(
|
||||
"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.",
|
||||
},
|
||||
),
|
||||
(
|
||||
"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.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=[])
|
||||
|
||||
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)
|
||||
|
||||
modnode = node.root()
|
||||
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)
|
||||
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._check_relative_import(modnode, node, imported_module, name)
|
||||
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_wildcard_imports(node, imported_module)
|
||||
self._check_same_line_imports(node)
|
||||
self._check_reimport(node, basename=basename, level=node.level)
|
||||
|
||||
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
|
||||
modnode = node.root()
|
||||
self._check_relative_import(modnode, node, imported_module, basename)
|
||||
|
||||
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 imports are grouped by package within a given category
|
||||
met = set()
|
||||
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
|
||||
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 _check_relative_import(
|
||||
self, modnode, importnode, importedmodnode, importedasname
|
||||
):
|
||||
"""check relative import. node is either an Import or From node, modname
|
||||
the imported module name.
|
||||
"""
|
||||
if not self.linter.is_message_enabled("relative-import"):
|
||||
return None
|
||||
if importedmodnode.file is None:
|
||||
return False # built-in module
|
||||
if modnode is importedmodnode:
|
||||
return False # module importing itself
|
||||
if modnode.absolute_import_activated() or getattr(importnode, "level", None):
|
||||
return False
|
||||
if importedmodnode.name != importedasname:
|
||||
# this must be a relative import...
|
||||
self.add_message(
|
||||
"relative-import",
|
||||
args=(importedasname, importedmodnode.name),
|
||||
node=importnode,
|
||||
)
|
||||
return None
|
||||
return None
|
||||
|
||||
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 = get_module_part(importedmodname, module_file)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
if context_name == importedmodname:
|
||||
self.add_message("import-self", node=node)
|
||||
|
||||
elif not 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_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 depedency 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
|
||||
|
||||
@decorators.cached
|
||||
def _external_dependencies_info(self):
|
||||
"""return cached external dependencies information or build and
|
||||
cache them
|
||||
"""
|
||||
return self._filter_dependencies_graph(internal=False)
|
||||
|
||||
@decorators.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 register(linter):
|
||||
"""required method to auto register this checker """
|
||||
linter.register_checker(ImportsChecker(linter))
|
||||
@@ -0,0 +1,375 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2009-2011, 2013-2014 LOGILAB S.A. (Paris, FRANCE) <[email protected]>
|
||||
# Copyright (c) 2009, 2012, 2014 Google, Inc.
|
||||
# Copyright (c) 2012 Mike Bryant <[email protected]>
|
||||
# Copyright (c) 2014 Brett Cannon <[email protected]>
|
||||
# Copyright (c) 2014 Arun Persaud <[email protected]>
|
||||
# Copyright (c) 2015-2018 Claudiu Popa <[email protected]>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <[email protected]>
|
||||
# Copyright (c) 2016 Chris Murray <[email protected]>
|
||||
# Copyright (c) 2016 Ashley Whetter <[email protected]>
|
||||
# Copyright (c) 2017 guillaume2 <[email protected]>
|
||||
# Copyright (c) 2017 Łukasz Rogalski <[email protected]>
|
||||
# Copyright (c) 2018 Mike Frysinger <[email protected]>
|
||||
# Copyright (c) 2018 Mariatta Wijaya <[email protected]>
|
||||
# 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
|
||||
from pylint import interfaces
|
||||
from pylint.checkers import utils
|
||||
from pylint.checkers.utils import check_messages
|
||||
|
||||
|
||||
MSGS = {
|
||||
"W1201": (
|
||||
"Specify string format arguments as logging function parameters",
|
||||
"logging-not-lazy",
|
||||
"Used when a logging statement has a call form of "
|
||||
'"logging.<logging method>(format_string % (format_args...))". '
|
||||
"Such calls should leave string interpolation to the logging "
|
||||
"method itself and be written "
|
||||
'"logging.<logging method>(format_string, format_args...)" '
|
||||
"so that the program may avoid incurring the cost of the "
|
||||
"interpolation in those cases in which no message will be "
|
||||
"logged. For more, see "
|
||||
"http://www.python.org/dev/peps/pep-0282/.",
|
||||
),
|
||||
"W1202": (
|
||||
"Use % formatting in logging functions and pass the % "
|
||||
"parameters as arguments",
|
||||
"logging-format-interpolation",
|
||||
"Used when a logging statement has a call form of "
|
||||
'"logging.<logging method>(format_string.format(format_args...))"'
|
||||
". Such calls should use % formatting instead, but leave "
|
||||
"interpolation to the logging function by passing the parameters "
|
||||
"as arguments.",
|
||||
),
|
||||
"W1203": (
|
||||
"Use % formatting in logging functions and pass the % "
|
||||
"parameters as arguments",
|
||||
"logging-fstring-interpolation",
|
||||
"Used when a logging statement has a call form of "
|
||||
'"logging.method(f"..."))"'
|
||||
". Such calls should use % formatting instead, but leave "
|
||||
"interpolation to the logging function by passing the parameters "
|
||||
"as arguments.",
|
||||
),
|
||||
"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": "Format style used to check logging format string. "
|
||||
"`old` means using % formatting, while `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)
|
||||
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.FormattedValue, astroid.JoinedStr)
|
||||
):
|
||||
self.add_message("logging-fstring-interpolation", node=node)
|
||||
|
||||
@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)
|
||||
|
||||
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
|
||||
if not isinstance(format_string, str):
|
||||
# If the log format is constant non-string (e.g. logging.debug(5)),
|
||||
# ensure there are no arguments.
|
||||
required_num_args = 0
|
||||
else:
|
||||
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(
|
||||
set(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.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,182 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2006, 2009-2013 LOGILAB S.A. (Paris, FRANCE) <[email protected]>
|
||||
# Copyright (c) 2012-2014 Google, Inc.
|
||||
# Copyright (c) 2014-2018 Claudiu Popa <[email protected]>
|
||||
# Copyright (c) 2014 Brett Cannon <[email protected]>
|
||||
# Copyright (c) 2014 Alexandru Coman <[email protected]>
|
||||
# Copyright (c) 2014 Arun Persaud <[email protected]>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <[email protected]>
|
||||
# Copyright (c) 2016 Łukasz Rogalski <[email protected]>
|
||||
# Copyright (c) 2016 glegoux <[email protected]>
|
||||
# Copyright (c) 2017-2018 hippo91 <[email protected]>
|
||||
# Copyright (c) 2017 Mikhail Fesenko <[email protected]>
|
||||
# Copyright (c) 2018 Ville Skyttä <[email protected]>
|
||||
|
||||
# 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)"""
|
||||
|
||||
# pylint: disable=W0511
|
||||
|
||||
import tokenize
|
||||
|
||||
import re
|
||||
|
||||
from pylint.interfaces import IRawChecker, ITokenChecker
|
||||
from pylint.checkers import BaseChecker
|
||||
from pylint.utils import OPTION_RGX, MessagesHandlerMixIn
|
||||
|
||||
|
||||
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.",
|
||||
),
|
||||
"W0512": (
|
||||
'Cannot decode using encoding "%s", unexpected byte at position %d',
|
||||
"invalid-encoded-data",
|
||||
"Used when a source line cannot be decoded using the specified "
|
||||
"source file encoding.",
|
||||
{"maxversion": (3, 0)},
|
||||
),
|
||||
}
|
||||
|
||||
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."
|
||||
),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
def open(self):
|
||||
super().open()
|
||||
self._fixme_pattern = re.compile(
|
||||
r"#\s*(%s)\b" % "|".join(map(re.escape, self.config.notes)), re.I
|
||||
)
|
||||
|
||||
def _check_encoding(self, lineno, line, file_encoding):
|
||||
try:
|
||||
return line.decode(file_encoding)
|
||||
except UnicodeDecodeError as ex:
|
||||
self.add_message(
|
||||
"invalid-encoded-data", line=lineno, args=(file_encoding, ex.args[2])
|
||||
)
|
||||
except LookupError as ex:
|
||||
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_RGX.search(comment_text)
|
||||
if disable_option_match:
|
||||
try:
|
||||
_, value = disable_option_match.group(1).split("=", 1)
|
||||
values = [_val.strip().upper() for _val in value.split(",")]
|
||||
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.string,
|
||||
)
|
||||
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,142 @@
|
||||
# Copyright (c) 2006, 2008-2011, 2013-2014 LOGILAB S.A. (Paris, FRANCE) <[email protected]>
|
||||
# Copyright (c) 2012-2014 Google, Inc.
|
||||
# Copyright (c) 2013-2018 Claudiu Popa <[email protected]>
|
||||
# Copyright (c) 2014 Michal Nowikowski <[email protected]>
|
||||
# Copyright (c) 2014 Brett Cannon <[email protected]>
|
||||
# Copyright (c) 2014 Arun Persaud <[email protected]>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <[email protected]>
|
||||
# Copyright (c) 2016 Alexander Todorov <[email protected]>
|
||||
# Copyright (c) 2016 Jakub Wilk <[email protected]>
|
||||
|
||||
# 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 sys
|
||||
|
||||
import astroid
|
||||
|
||||
from pylint.interfaces import IAstroidChecker
|
||||
from pylint.checkers import BaseChecker
|
||||
from pylint.checkers.utils import check_messages, node_frame_class, has_known_bases
|
||||
|
||||
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.",
|
||||
),
|
||||
"E1004": (
|
||||
"Missing argument to super()",
|
||||
"missing-super-argument",
|
||||
"Used when the super builtin didn't receive an argument.",
|
||||
{"maxversion": (3, 0)},
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
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", "missing-super-argument")
|
||||
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
|
||||
|
||||
if not klass.newstyle and has_known_bases(klass):
|
||||
# super should not be used on an old style class
|
||||
continue
|
||||
else:
|
||||
# super first arg should be the class
|
||||
if not call.args:
|
||||
if sys.version_info[0] == 3:
|
||||
# unless Python 3
|
||||
continue
|
||||
else:
|
||||
self.add_message("missing-super-argument", node=call)
|
||||
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 infered
|
||||
# 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,125 @@
|
||||
# Copyright (c) 2007, 2010, 2013, 2015 LOGILAB S.A. (Paris, FRANCE) <[email protected]>
|
||||
# Copyright (c) 2013 Google, Inc.
|
||||
# Copyright (c) 2014 Arun Persaud <[email protected]>
|
||||
# Copyright (c) 2015-2017 Claudiu Popa <[email protected]>
|
||||
# Copyright (c) 2015 Mike Frysinger <[email protected]>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <[email protected]>
|
||||
# Copyright (c) 2016 Glenn Matthews <[email protected]>
|
||||
|
||||
# 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.interfaces import ITokenChecker
|
||||
from pylint.exceptions import EmptyReportError
|
||||
from pylint.checkers import BaseTokenChecker
|
||||
from pylint.reporters import diff_string
|
||||
from pylint.reporters.ureports.nodes import Table
|
||||
|
||||
|
||||
def report_raw_stats(sect, stats, old_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
|
||||
old = old_stats.get(key, None)
|
||||
if old is not None:
|
||||
diff_str = diff_string(old, total)
|
||||
else:
|
||||
old, diff_str = "NC", "NC"
|
||||
lines += (node_type, str(total), "%.2f" % percent, str(old), diff_str)
|
||||
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,449 @@
|
||||
# Copyright (c) 2006, 2008-2014 LOGILAB S.A. (Paris, FRANCE) <[email protected]>
|
||||
# Copyright (c) 2012 Ry4an Brase <[email protected]>
|
||||
# Copyright (c) 2012 Google, Inc.
|
||||
# Copyright (c) 2012 Anthony VEREZ <[email protected]>
|
||||
# Copyright (c) 2014-2018 Claudiu Popa <[email protected]>
|
||||
# Copyright (c) 2014 Brett Cannon <[email protected]>
|
||||
# Copyright (c) 2014 Arun Persaud <[email protected]>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <[email protected]>
|
||||
# Copyright (c) 2017 Anthony Sottile <[email protected]>
|
||||
# Copyright (c) 2017 Mikhail Fesenko <[email protected]>
|
||||
# Copyright (c) 2018 ssolanki <[email protected]>
|
||||
|
||||
# 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=W0622
|
||||
"""a similarities / code duplication command line tool and pylint checker
|
||||
"""
|
||||
|
||||
from __future__ import print_function
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from itertools import groupby
|
||||
|
||||
import astroid
|
||||
|
||||
from pylint.utils import decoding_stream
|
||||
from pylint.interfaces import IRawChecker
|
||||
from pylint.checkers import BaseChecker, table_lines_from_stats
|
||||
from pylint.reporters.ureports.nodes import Table
|
||||
|
||||
|
||||
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)
|
||||
for lineset, idx in couples:
|
||||
print("==%s:%s" % (lineset.name, idx))
|
||||
# pylint: disable=W0631
|
||||
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. / 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 :]:
|
||||
for sim in self._find_common(lineset, lineset2):
|
||||
yield sim
|
||||
|
||||
|
||||
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 (line.startswith('"""') or line.startswith("'''")):
|
||||
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:
|
||||
# XXX should use regex in checkers/format to avoid cutting
|
||||
# at a "#" in a string
|
||||
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 = []
|
||||
for lineset, idx in couples:
|
||||
msg.append("==%s:%s" % (lineset.name, idx))
|
||||
msg.sort()
|
||||
# pylint: disable=W0631
|
||||
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. / 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:]
|
||||
from getopt import getopt
|
||||
|
||||
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,403 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2014-2017 Claudiu Popa <[email protected]>
|
||||
# Copyright (c) 2014 Michal Nowikowski <[email protected]>
|
||||
# Copyright (c) 2014 LOGILAB S.A. (Paris, FRANCE) <[email protected]>
|
||||
# Copyright (c) 2015 Pavel Roskin <[email protected]>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <[email protected]>
|
||||
# Copyright (c) 2016-2017 Pedro Algarvio <[email protected]>
|
||||
# Copyright (c) 2016 Alexander Todorov <[email protected]>
|
||||
# Copyright (c) 2017 Łukasz Rogalski <[email protected]>
|
||||
# Copyright (c) 2017 Mikhail Fesenko <[email protected]>
|
||||
# Copyright (c) 2018 Mike Frysinger <[email protected]>
|
||||
# Copyright (c) 2018 Sushobhit <[email protected]>
|
||||
# Copyright (c) 2018 Anthony Sottile <[email protected]>
|
||||
|
||||
# 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 tokenize
|
||||
import re
|
||||
|
||||
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
|
||||
|
||||
|
||||
from pylint.interfaces import ITokenChecker, IAstroidChecker
|
||||
from pylint.checkers import BaseTokenChecker
|
||||
from pylint.checkers.utils import check_messages
|
||||
|
||||
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 working install 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 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 "
|
||||
"indicated private dictionary in "
|
||||
"--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
|
||||
if line.strip().startswith("#"):
|
||||
line = line.strip()[1:]
|
||||
starts_with_comment = True
|
||||
else:
|
||||
starts_with_comment = False
|
||||
for word, _ in self.tokenizer(line.strip()):
|
||||
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 :]
|
||||
|
||||
m = re.search(r"(\W|^)(%s)(\W|$)" % word, line)
|
||||
if m:
|
||||
# Start position of second group in regex.
|
||||
col = m.regs[2][0]
|
||||
else:
|
||||
col = line.index(word)
|
||||
|
||||
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,441 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2013-2014 LOGILAB S.A. (Paris, FRANCE) <[email protected]>
|
||||
# Copyright (c) 2013-2014 Google, Inc.
|
||||
# Copyright (c) 2014-2018 Claudiu Popa <[email protected]>
|
||||
# Copyright (c) 2014 Cosmin Poieana <[email protected]>
|
||||
# Copyright (c) 2014 Vlad Temian <[email protected]>
|
||||
# Copyright (c) 2014 Arun Persaud <[email protected]>
|
||||
# Copyright (c) 2015 Cezar <[email protected]>
|
||||
# Copyright (c) 2015 Chris Rebert <[email protected]>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <[email protected]>
|
||||
# Copyright (c) 2016 Jared Garst <[email protected]>
|
||||
# Copyright (c) 2017 Renat Galimov <[email protected]>
|
||||
# Copyright (c) 2017 Martin <[email protected]>
|
||||
# Copyright (c) 2017 Christopher Zurcher <[email protected]>
|
||||
# Copyright (c) 2017 Łukasz Rogalski <[email protected]>
|
||||
# Copyright (c) 2018 Banjamin Freeman <[email protected]>
|
||||
# Copyright (c) 2018 Ioana Tagirta <[email protected]>
|
||||
|
||||
# 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.interfaces import IAstroidChecker
|
||||
from pylint.checkers import BaseChecker
|
||||
from pylint.checkers import utils
|
||||
|
||||
|
||||
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"
|
||||
|
||||
if sys.version_info >= (3, 0):
|
||||
OPEN_MODULE = "_io"
|
||||
else:
|
||||
OPEN_MODULE = "__builtin__"
|
||||
|
||||
|
||||
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",
|
||||
),
|
||||
}
|
||||
|
||||
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.getargvalues",
|
||||
"inspect.formatargspec",
|
||||
"inspect.formatargvalues",
|
||||
"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_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",
|
||||
)
|
||||
def visit_call(self, node):
|
||||
"""Visit a Call node."""
|
||||
try:
|
||||
for inferred in node.func.infer():
|
||||
if inferred is astroid.Uninferable:
|
||||
continue
|
||||
elif 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)
|
||||
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 infered.
|
||||
If so, emit boolean-datetime warning.
|
||||
"""
|
||||
try:
|
||||
infered = next(node.infer())
|
||||
except astroid.InferenceError:
|
||||
return
|
||||
if isinstance(infered, Instance) and infered.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,763 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2009 Charles Hebert <[email protected]>
|
||||
# Copyright (c) 2010-2014 LOGILAB S.A. (Paris, FRANCE) <[email protected]>
|
||||
# Copyright (c) 2010 Daniel Harding <[email protected]>
|
||||
# Copyright (c) 2012-2014 Google, Inc.
|
||||
# Copyright (c) 2013-2018 Claudiu Popa <[email protected]>
|
||||
# Copyright (c) 2014 Brett Cannon <[email protected]>
|
||||
# Copyright (c) 2014 Arun Persaud <[email protected]>
|
||||
# Copyright (c) 2015 Rene Zhang <[email protected]>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <[email protected]>
|
||||
# Copyright (c) 2016, 2018 Jakub Wilk <[email protected]>
|
||||
# Copyright (c) 2016 Peter Dawyndt <[email protected]>
|
||||
# Copyright (c) 2017 Łukasz Rogalski <[email protected]>
|
||||
# Copyright (c) 2017 Ville Skyttä <[email protected]>
|
||||
# Copyright (c) 2018 Nick Drozd <[email protected]>
|
||||
# Copyright (c) 2018 Anthony Sottile <[email protected]>
|
||||
|
||||
|
||||
# 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 sys
|
||||
import tokenize
|
||||
import numbers
|
||||
from collections import Counter
|
||||
|
||||
import astroid
|
||||
from astroid.arguments import CallSite
|
||||
from astroid.node_classes import Const
|
||||
from pylint.interfaces import ITokenChecker, IAstroidChecker, IRawChecker
|
||||
from pylint.checkers import BaseChecker, BaseTokenChecker
|
||||
from pylint.checkers import utils
|
||||
from pylint.checkers.utils import check_messages
|
||||
|
||||
_AST_NODE_STR_TYPES = ("__builtin__.unicode", "__builtin__.str", "builtins.str")
|
||||
|
||||
_PY3K = sys.version_info[:2] >= (3, 0)
|
||||
_PY27 = sys.version_info[:2] == (2, 7)
|
||||
|
||||
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",
|
||||
),
|
||||
}
|
||||
|
||||
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(*MSGS)
|
||||
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 e:
|
||||
c = format_string[e.index]
|
||||
self.add_message(
|
||||
"bad-format-character", node=node, args=(c, ord(c), e.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(*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 = 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. """
|
||||
# TODO: skip (for now) format 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.
|
||||
# For more details, see issue 287.
|
||||
#
|
||||
# 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 infered, 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 = next(argname.infer())
|
||||
except astroid.InferenceError:
|
||||
continue
|
||||
if not specifiers or argument is astroid.Uninferable:
|
||||
# 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.",
|
||||
),
|
||||
"W1403": (
|
||||
"Implicit string concatenation found in %s",
|
||||
"implicit-str-concat-in-sequence",
|
||||
"String literals are implicitly concatenated in a "
|
||||
"literal iterable definition : "
|
||||
"maybe a comma is missing ?",
|
||||
),
|
||||
}
|
||||
options = (
|
||||
(
|
||||
"check-str-concat-over-line-jumps",
|
||||
{
|
||||
"default": False,
|
||||
"type": "yn",
|
||||
"metavar": "<y_or_n>",
|
||||
"help": "This flag controls whether the "
|
||||
"implicit-str-concat-in-sequence should generate a warning "
|
||||
"on implicit string concatenation in sequences defined over "
|
||||
"several lines.",
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
# Characters that have a special meaning after a backslash in either
|
||||
# Unicode or byte strings.
|
||||
ESCAPE_CHARACTERS = "abfnrtvx\n\r\t\\'\"01234567"
|
||||
|
||||
# TODO(mbp): Octal characters are quite an edge case today; people may
|
||||
# prefer a separate warning where they occur. \0 should be allowed.
|
||||
|
||||
# Characters that have a special meaning after a backslash but only in
|
||||
# Unicode strings.
|
||||
UNICODE_ESCAPE_CHARACTERS = "uUN"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(StringConstantChecker, self).__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)
|
||||
|
||||
@check_messages(*(msgs.keys()))
|
||||
def visit_list(self, node):
|
||||
self.check_for_concatenated_strings(node, "list")
|
||||
|
||||
@check_messages(*(msgs.keys()))
|
||||
def visit_set(self, node):
|
||||
self.check_for_concatenated_strings(node, "set")
|
||||
|
||||
@check_messages(*(msgs.keys()))
|
||||
def visit_tuple(self, node):
|
||||
self.check_for_concatenated_strings(node, "tuple")
|
||||
|
||||
def check_for_concatenated_strings(self, iterable_node, iterable_type):
|
||||
for elt in iterable_node.elts:
|
||||
if isinstance(elt, Const) and elt.pytype() in _AST_NODE_STR_TYPES:
|
||||
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-in-sequence",
|
||||
line=elt.lineno,
|
||||
args=(iterable_type,),
|
||||
)
|
||||
|
||||
def process_string_token(self, token, start_row):
|
||||
quote_char = None
|
||||
index = None
|
||||
for index, c in enumerate(token):
|
||||
if c in "'\"":
|
||||
quote_char = c
|
||||
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>
|
||||
#
|
||||
# TODO(mbp): Maybe give a separate warning about the rarely-used
|
||||
# \a \b \v \f?
|
||||
#
|
||||
# TODO(mbp): We could give the column of the problem character, but
|
||||
# add_message doesn't seem to have a way to pass it through at present.
|
||||
i = 0
|
||||
while True:
|
||||
i = string_body.find("\\", i)
|
||||
if i == -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[i + 1]
|
||||
match = string_body[i : i + 2]
|
||||
if next_char in self.UNICODE_ESCAPE_CHARACTERS:
|
||||
if "u" in prefix:
|
||||
pass
|
||||
elif (_PY3K or self._unicode_literals) and "b" not in prefix:
|
||||
pass # unicode by default
|
||||
else:
|
||||
self.add_message(
|
||||
"anomalous-unicode-escape-in-string",
|
||||
line=start_row,
|
||||
args=(match,),
|
||||
)
|
||||
elif next_char not in self.ESCAPE_CHARACTERS:
|
||||
self.add_message(
|
||||
"anomalous-backslash-in-string", line=start_row, args=(match,)
|
||||
)
|
||||
# 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.
|
||||
i += 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]
|
||||
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,931 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2006-2010, 2012-2014 LOGILAB S.A. (Paris, FRANCE) <[email protected]>
|
||||
# Copyright (c) 2008 [email protected] <[email protected]>
|
||||
# Copyright (c) 2010 Julien Jehannet <[email protected]>
|
||||
# Copyright (c) 2013 Google, Inc.
|
||||
# Copyright (c) 2013 John McGehee <[email protected]>
|
||||
# Copyright (c) 2014-2018 Claudiu Popa <[email protected]>
|
||||
# Copyright (c) 2014 Brett Cannon <[email protected]>
|
||||
# Copyright (c) 2014 Arun Persaud <[email protected]>
|
||||
# Copyright (c) 2015 Aru Sahni <[email protected]>
|
||||
# Copyright (c) 2015 John Kirkham <[email protected]>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <[email protected]>
|
||||
# Copyright (c) 2016 Erik <[email protected]>
|
||||
# Copyright (c) 2016 Alexander Todorov <[email protected]>
|
||||
# Copyright (c) 2016 Moises Lopez <[email protected]>
|
||||
# Copyright (c) 2017-2018 Ville Skyttä <[email protected]>
|
||||
# Copyright (c) 2017 hippo91 <[email protected]>
|
||||
# Copyright (c) 2017 ahirnish <[email protected]>
|
||||
# Copyright (c) 2017 Łukasz Rogalski <[email protected]>
|
||||
# Copyright (c) 2018 Bryce Guinta <[email protected]>
|
||||
# Copyright (c) 2018 ssolanki <[email protected]>
|
||||
# Copyright (c) 2018 Sushobhit <[email protected]>
|
||||
# Copyright (c) 2018 Anthony Sottile <[email protected]>
|
||||
# Copyright (c) 2018 Gary Tyler McLeod <[email protected]>
|
||||
# Copyright (c) 2018 Konstantin <[email protected]>
|
||||
# Copyright (c) 2018 Nick Drozd <[email protected]>
|
||||
|
||||
# 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)
|
||||
"""
|
||||
from __future__ import print_function
|
||||
|
||||
# TODO(cpopa): this module contains the logic for the
|
||||
# configuration parser and for the command line parser,
|
||||
# but it's really coupled to optparse's internals.
|
||||
# The code was copied almost verbatim from logilab.common,
|
||||
# in order to not depend on it anymore and it will definitely
|
||||
# need a cleanup. It could be completely reengineered as well.
|
||||
|
||||
import contextlib
|
||||
import collections
|
||||
import copy
|
||||
import io
|
||||
import optparse
|
||||
import os
|
||||
import pickle
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
|
||||
import configparser
|
||||
|
||||
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, _PICK_LOAD) as stream:
|
||||
return pickle.load(stream)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
return {}
|
||||
|
||||
|
||||
if sys.version_info < (3, 0):
|
||||
_PICK_DUMP, _PICK_LOAD = "w", "r"
|
||||
else:
|
||||
_PICK_DUMP, _PICK_LOAD = "wb", "rb"
|
||||
|
||||
|
||||
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, _PICK_DUMP) as stream:
|
||||
pickle.dump(results, stream)
|
||||
except (IOError, OSError) as ex:
|
||||
print("Unable to create file %s: %s" % (data_file, ex), file=sys.stderr)
|
||||
|
||||
|
||||
def find_pylintrc():
|
||||
"""search the pylint rc file and return its path if it find it, else None
|
||||
"""
|
||||
# is there a pylint rc file in the current directory ?
|
||||
if os.path.exists("pylintrc"):
|
||||
return os.path.abspath("pylintrc")
|
||||
if os.path.exists(".pylintrc"):
|
||||
return os.path.abspath(".pylintrc")
|
||||
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, ".."))
|
||||
if os.path.isfile(os.path.join(curdir, "pylintrc")):
|
||||
return os.path.join(curdir, "pylintrc")
|
||||
if os.path.isfile(os.path.join(curdir, ".pylintrc")):
|
||||
return os.path.join(curdir, ".pylintrc")
|
||||
if "PYLINTRC" in os.environ and os.path.exists(os.environ["PYLINTRC"]):
|
||||
pylintrc = os.environ["PYLINTRC"]
|
||||
else:
|
||||
user_home = os.path.expanduser("~")
|
||||
if user_home in ("~", "/root"):
|
||||
pylintrc = ".pylintrc"
|
||||
else:
|
||||
pylintrc = os.path.join(user_home, ".pylintrc")
|
||||
if not os.path.isfile(pylintrc):
|
||||
pylintrc = os.path.join(user_home, ".config", "pylintrc")
|
||||
if not os.path.isfile(pylintrc):
|
||||
if os.path.isfile("/etc/pylintrc"):
|
||||
pylintrc = "/etc/pylintrc"
|
||||
else:
|
||||
pylintrc = None
|
||||
return pylintrc
|
||||
|
||||
|
||||
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:
|
||||
# FIXME
|
||||
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", pkginfo.modname),
|
||||
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
|
||||
# pylint: disable=unused-argument
|
||||
def helpfunc(option, opt, val, p, level=helplevel):
|
||||
print(self.help(level))
|
||||
sys.exit(0)
|
||||
|
||||
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 IOError("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:
|
||||
parser = self.cfgfile_parser
|
||||
|
||||
# Use this encoding in order to strip the BOM marker, if any.
|
||||
with io.open(config_file, "r", encoding="utf_8_sig") as fp:
|
||||
parser.read_file(fp)
|
||||
|
||||
# normalize sections'title
|
||||
for sect, values in list(parser._sections.items()):
|
||||
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):
|
||||
# TODO handle here undeclared options appearing in the config file
|
||||
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()
|
||||
|
||||
|
||||
class OptionsProviderMixIn:
|
||||
"""Mixin to provide options to an OptionsManager"""
|
||||
|
||||
# those attributes should be overridden
|
||||
priority = -1
|
||||
name = "default"
|
||||
options = ()
|
||||
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,196 @@
|
||||
# -*- 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) <[email protected]>
|
||||
# Copyright (c) 2014 Jakob Normark <[email protected]>
|
||||
# Copyright (c) 2014 Brett Cannon <[email protected]>
|
||||
# Copyright (c) 2014 Manuel Vázquez Acosta <[email protected]>
|
||||
# Copyright (c) 2014 Derek Harland <[email protected]>
|
||||
# Copyright (c) 2014 Arun Persaud <[email protected]>
|
||||
# Copyright (c) 2015-2017 Claudiu Popa <[email protected]>
|
||||
# Copyright (c) 2015 Mihai Balint <[email protected]>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <[email protected]>
|
||||
# Copyright (c) 2017 hippo91 <[email protected]>
|
||||
# Copyright (c) 2017 Daniela Plascencia <[email protected]>
|
||||
# Copyright (c) 2018 Sushobhit <[email protected]>
|
||||
# Copyright (c) 2018 Ryan McGuire <[email protected]>
|
||||
# Copyright (c) 2018 thernstig <[email protected]>
|
||||
# Copyright (c) 2018 Radostin Stoyanov <[email protected]>
|
||||
|
||||
# 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.
|
||||
"""
|
||||
from __future__ import print_function
|
||||
|
||||
import os
|
||||
import os.path as osp
|
||||
import sys
|
||||
import shlex
|
||||
from subprocess import Popen, PIPE
|
||||
from io import StringIO
|
||||
|
||||
|
||||
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)
|
||||
"""
|
||||
# Create command line to call pylint
|
||||
epylint_part = [sys.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,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2016-2017 Claudiu Popa <[email protected]>
|
||||
# Copyright (c) 2016 Glenn Matthews <[email protected]>
|
||||
# Copyright (c) 2017 Łukasz Rogalski <[email protected]>
|
||||
# Copyright (c) 2018 Ville Skyttä <[email protected]>
|
||||
# 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)"""
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,794 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2016-2018 Ashley Whetter <[email protected]>
|
||||
# Copyright (c) 2016-2017 Claudiu Popa <[email protected]>
|
||||
# Copyright (c) 2016 Yuri Bochkarev <[email protected]>
|
||||
# Copyright (c) 2016 Glenn Matthews <[email protected]>
|
||||
# Copyright (c) 2016 Moises Lopez <[email protected]>
|
||||
# Copyright (c) 2017 hippo91 <[email protected]>
|
||||
# Copyright (c) 2017 Mitar <[email protected]>
|
||||
# Copyright (c) 2018 ssolanki <[email protected]>
|
||||
# Copyright (c) 2018 Anthony Sottile <[email protected]>
|
||||
# Copyright (c) 2018 Mitchell T.H. Young <[email protected]>
|
||||
# Copyright (c) 2018 Adrian Chirieac <[email protected]>
|
||||
|
||||
# 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."""
|
||||
|
||||
from __future__ import absolute_import, print_function
|
||||
|
||||
import re
|
||||
|
||||
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 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_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_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_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_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.add(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+or\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_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.add(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,73 @@
|
||||
# Copyright (c) 2016 Claudiu Popa <[email protected]>
|
||||
|
||||
# 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 sys
|
||||
|
||||
import astroid
|
||||
from pylint.checkers import BaseChecker
|
||||
from pylint.checkers.utils import check_messages
|
||||
from pylint.interfaces import IAstroidChecker
|
||||
|
||||
|
||||
BAD_FUNCTIONS = ["map", "filter"]
|
||||
if sys.version_info < (3, 0):
|
||||
BAD_FUNCTIONS.append("input")
|
||||
# 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,23 @@
|
||||
# Copyright (c) 2014-2015 Bruno Daniel <[email protected]>
|
||||
# Copyright (c) 2015-2016 Claudiu Popa <[email protected]>
|
||||
# Copyright (c) 2016 Ashley Whetter <[email protected]>
|
||||
|
||||
# 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,74 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2015 LOGILAB S.A. (Paris, FRANCE) <[email protected]>
|
||||
# Copyright (c) 2016-2017 Claudiu Popa <[email protected]>
|
||||
# Copyright (c) 2016 Glenn Matthews <[email protected]>
|
||||
# Copyright (c) 2018 Ville Skyttä <[email protected]>
|
||||
|
||||
# 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 ITokenChecker, IAstroidChecker
|
||||
|
||||
|
||||
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, _):
|
||||
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 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2016 Alexander Todorov <[email protected]>
|
||||
# Copyright (c) 2017 Claudiu Popa <[email protected]>
|
||||
|
||||
# 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 interfaces
|
||||
from pylint import checkers
|
||||
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,498 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2014-2015 Bruno Daniel <[email protected]>
|
||||
# Copyright (c) 2015-2017 Claudiu Popa <[email protected]>
|
||||
# Copyright (c) 2016-2018 Ashley Whetter <[email protected]>
|
||||
# Copyright (c) 2016 Glenn Matthews <[email protected]>
|
||||
# Copyright (c) 2016 Glenn Matthews <[email protected]>
|
||||
# Copyright (c) 2016 Moises Lopez <[email protected]>
|
||||
# Copyright (c) 2017 Ville Skyttä <[email protected]>
|
||||
# Copyright (c) 2017 John Paraskevopoulos <[email protected]>
|
||||
# Copyright (c) 2018 Anthony Sottile <[email protected]>
|
||||
# Copyright (c) 2018 Sushobhit <[email protected]>
|
||||
# Copyright (c) 2018 Adam Dangoor <[email protected]>
|
||||
|
||||
# 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
|
||||
"""
|
||||
from __future__ import print_function, division, absolute_import
|
||||
|
||||
import astroid
|
||||
|
||||
from pylint.interfaces import IAstroidChecker
|
||||
from pylint.checkers import BaseChecker
|
||||
from pylint.checkers import utils as checker_utils
|
||||
import pylint.extensions._check_docs_utils as utils
|
||||
|
||||
|
||||
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", "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", "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", "missing-param-doc")]},
|
||||
),
|
||||
"W9016": (
|
||||
'"%s" missing in parameter type documentation',
|
||||
"missing-type-doc",
|
||||
"Please add parameter type declarations for all parameters.",
|
||||
{"old_names": [("W9004", "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:
|
||||
self.add_message("missing-yield-type-doc", node=func_node)
|
||||
|
||||
def visit_yieldfrom(self, node):
|
||||
self.visit_yield(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: str
|
||||
|
||||
: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
|
||||
|
||||
def _compare_missing_args(found_argument_names, message_id, not_needed_names):
|
||||
"""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
|
||||
"""
|
||||
if not tolerate_missing_params:
|
||||
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(found_argument_names, message_id, not_needed_names):
|
||||
"""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
|
||||
"""
|
||||
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,
|
||||
)
|
||||
|
||||
_compare_missing_args(
|
||||
params_with_doc, "missing-param-doc", self.not_needed_param_in_docstring
|
||||
)
|
||||
|
||||
for index, arg_name in enumerate(arguments_node.args):
|
||||
if arguments_node.annotations[index]:
|
||||
params_with_type.add(arg_name.name)
|
||||
|
||||
_compare_missing_args(
|
||||
params_with_type, "missing-type-doc", not_needed_type_in_docstring
|
||||
)
|
||||
|
||||
_compare_different_args(
|
||||
params_with_doc, "differing-param-doc", self.not_needed_param_in_docstring
|
||||
)
|
||||
_compare_different_args(
|
||||
params_with_type, "differing-type-doc", not_needed_type_in_docstring
|
||||
)
|
||||
|
||||
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,89 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2016-2017 Claudiu Popa <[email protected]>
|
||||
# Copyright (c) 2016 Łukasz Rogalski <[email protected]>
|
||||
# Copyright (c) 2016 Luis Escobar <[email protected]>
|
||||
|
||||
# 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.interfaces import IAstroidChecker, HIGH
|
||||
from pylint.checkers.utils import check_messages
|
||||
|
||||
|
||||
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 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2016 Alexander Todorov <[email protected]>
|
||||
# Copyright (c) 2017 Claudiu Popa <[email protected]>
|
||||
|
||||
# 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 interfaces
|
||||
from pylint import checkers
|
||||
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,201 @@
|
||||
# Copyright (c) 2016-2017 Claudiu Popa <[email protected]>
|
||||
# Copyright (c) 2016 Moises Lopez <[email protected]>
|
||||
# Copyright (c) 2017 hippo91 <[email protected]>
|
||||
|
||||
# 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 __future__ import absolute_import
|
||||
|
||||
from mccabe import (
|
||||
PathGraph as Mccabe_PathGraph,
|
||||
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(PathGraph, self).__init__(name="", entity="", lineno=1)
|
||||
self.root = node
|
||||
|
||||
|
||||
class PathGraphingAstVisitor(Mccabe_PathGraphingAstVisitor):
|
||||
def __init__(self):
|
||||
super(PathGraphingAstVisitor, self).__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:
|
||||
className = klass.__name__
|
||||
meth = getattr(self.visitor, "visit" + className, 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
|
||||
): # pylint: disable=unused-argument
|
||||
"""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 le in loose_ends:
|
||||
self.graph.connect(le, 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,90 @@
|
||||
# -*- coding: utf-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
|
||||
|
||||
"""Looks for overlapping exceptions."""
|
||||
|
||||
import astroid
|
||||
|
||||
from pylint import interfaces
|
||||
from pylint import checkers
|
||||
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))
|
||||
@@ -0,0 +1,116 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2016-2017 Claudiu Popa <[email protected]>
|
||||
# Copyright (c) 2016 Glenn Matthews <[email protected]>
|
||||
# Copyright (c) 2018 Sushobhit <[email protected]>
|
||||
# Copyright (c) 2018 Ville Skyttä <[email protected]>
|
||||
|
||||
# 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,198 @@
|
||||
# Copyright (c) 2015-2017 Claudiu Popa <[email protected]>
|
||||
# Copyright (c) 2015 Florian Bruhin <[email protected]>
|
||||
# Copyright (c) 2016 Ashley Whetter <[email protected]>
|
||||
# Copyright (c) 2018 ssolanki <[email protected]>
|
||||
|
||||
# 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 os.path as osp
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import codecs
|
||||
|
||||
|
||||
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, dotfile=None, mapfile=None):
|
||||
"""Generates a graph file.
|
||||
|
||||
:param str outputfile: filename and path [defaults to graphname.png]
|
||||
:param str dotfile: filename and path [defaults to graphname.dot]
|
||||
:param str mapfile: filename and path
|
||||
|
||||
:rtype: str
|
||||
:return: a path to the generated file
|
||||
"""
|
||||
import subprocess # introduced in py 2.4
|
||||
|
||||
name = self.graphname
|
||||
if not dotfile:
|
||||
# if 'outputfile' is a dot file use it as 'dotfile'
|
||||
if outputfile and outputfile.endswith(".dot"):
|
||||
dotfile = outputfile
|
||||
else:
|
||||
dotfile = "%s.dot" % name
|
||||
if outputfile is not None:
|
||||
storedir, _, target = target_info_from_filename(outputfile)
|
||||
if target != "dot":
|
||||
pdot, dot_sourcepath = tempfile.mkstemp(".dot", name)
|
||||
os.close(pdot)
|
||||
else:
|
||||
dot_sourcepath = osp.join(storedir, dotfile)
|
||||
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,102 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2009-2010, 2012-2013 LOGILAB S.A. (Paris, FRANCE) <[email protected]>
|
||||
# Copyright (c) 2013-2014 Google, Inc.
|
||||
# Copyright (c) 2014 Michal Nowikowski <[email protected]>
|
||||
# Copyright (c) 2014 Arun Persaud <[email protected]>
|
||||
# Copyright (c) 2015-2017 Claudiu Popa <[email protected]>
|
||||
# Copyright (c) 2015 Florian Bruhin <[email protected]>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <[email protected]>
|
||||
# Copyright (c) 2018 ssolanki <[email protected]>
|
||||
# Copyright (c) 2018 Ville Skyttä <[email protected]>
|
||||
|
||||
# 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")
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,9 @@
|
||||
|
||||
# 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 $"
|
||||
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,238 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2006, 2008-2010, 2013-2014 LOGILAB S.A. (Paris, FRANCE) <[email protected]>
|
||||
# Copyright (c) 2014 Brett Cannon <[email protected]>
|
||||
# Copyright (c) 2014 Arun Persaud <[email protected]>
|
||||
# Copyright (c) 2015-2018 Claudiu Popa <[email protected]>
|
||||
# Copyright (c) 2015 Florian Bruhin <[email protected]>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <[email protected]>
|
||||
# Copyright (c) 2016 Ashley Whetter <[email protected]>
|
||||
# Copyright (c) 2017 Łukasz Rogalski <[email protected]>
|
||||
# Copyright (c) 2018 ssolanki <[email protected]>
|
||||
# Copyright (c) 2018 Sushobhit <[email protected]>
|
||||
# Copyright (c) 2018 Ville Skyttä <[email protected]>
|
||||
|
||||
# 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 PackageDiagram, ClassDiagram
|
||||
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,267 @@
|
||||
# Copyright (c) 2006, 2008-2010, 2012-2014 LOGILAB S.A. (Paris, FRANCE) <[email protected]>
|
||||
# Copyright (c) 2014-2018 Claudiu Popa <[email protected]>
|
||||
# Copyright (c) 2014 Brett Cannon <[email protected]>
|
||||
# Copyright (c) 2014 Arun Persaud <[email protected]>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <[email protected]>
|
||||
# Copyright (c) 2018 ssolanki <[email protected]>
|
||||
|
||||
# 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.pyreverse.utils import is_interface, FilterMixIn
|
||||
from pylint.checkers.utils import decorated_with_property
|
||||
|
||||
|
||||
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,368 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2015-2018 Claudiu Popa <[email protected]>
|
||||
# Copyright (c) 2017 Łukasz Rogalski <[email protected]>
|
||||
# Copyright (c) 2018 ssolanki <[email protected]>
|
||||
# Copyright (c) 2018 Ville Skyttä <[email protected]>
|
||||
|
||||
# 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...
|
||||
"""
|
||||
from __future__ import print_function
|
||||
|
||||
import collections
|
||||
import os
|
||||
import traceback
|
||||
|
||||
import astroid
|
||||
from astroid import bases
|
||||
from astroid import exceptions
|
||||
from astroid import manager
|
||||
from astroid import modutils
|
||||
from astroid import 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."""
|
||||
# FIXME: what if __implements__ = (MyIFace, MyParent.__implements__)...
|
||||
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:
|
||||
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:
|
||||
# TODO: don't use get_module_part,
|
||||
# missing package precedence
|
||||
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
|
||||
# XXX why is first file defining the project.path ?
|
||||
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,221 @@
|
||||
# Copyright (c) 2008-2010, 2012-2014 LOGILAB S.A. (Paris, FRANCE) <[email protected]>
|
||||
# Copyright (c) 2014 Brett Cannon <[email protected]>
|
||||
# Copyright (c) 2014 Arun Persaud <[email protected]>
|
||||
# Copyright (c) 2015-2018 Claudiu Popa <[email protected]>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <[email protected]>
|
||||
# Copyright (c) 2016 Alexander Pervakov <[email protected]>
|
||||
|
||||
# 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>
|
||||
"""
|
||||
from __future__ import print_function
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from pylint.config import ConfigurationMixIn
|
||||
from pylint.pyreverse.inspector import Linker, project_from_files
|
||||
from pylint.pyreverse.diadefslib import DiadefsHandler
|
||||
from pylint.pyreverse import writer
|
||||
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",
|
||||
),
|
||||
),
|
||||
# TODO : generate dependencies like in pylint
|
||||
# ("package-dependencies",
|
||||
# dict(short="M", action="store", metavar='<package_depth>', type='int',
|
||||
# help='show <package_depth> module dependencies beyond modules in \
|
||||
# <projects> (for the package diagram)')),
|
||||
(
|
||||
"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,221 @@
|
||||
# Copyright (c) 2006, 2008, 2010, 2013-2014 LOGILAB S.A. (Paris, FRANCE) <[email protected]>
|
||||
# Copyright (c) 2014 Brett Cannon <[email protected]>
|
||||
# Copyright (c) 2014 Arun Persaud <[email protected]>
|
||||
# Copyright (c) 2015-2017 Claudiu Popa <[email protected]>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <[email protected]>
|
||||
# Copyright (c) 2017 hippo91 <[email protected]>
|
||||
# Copyright (c) 2018 ssolanki <[email protected]>
|
||||
|
||||
# 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
|
||||
"""
|
||||
from __future__ import print_function
|
||||
|
||||
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 IOError:
|
||||
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("^__[A-Za-z0-9]+[A-Za-z0-9_]*__$")
|
||||
PRIVATE = re.compile("^__[_A-Za-z0-9]*[A-Za-z0-9]+_?$")
|
||||
PROTECTED = re.compile("^_[_A-Za-z0-9]*$")
|
||||
|
||||
|
||||
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("^.*Abstract.*")
|
||||
FINAL = re.compile("^[A-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 = {}
|
||||
|
||||
def visit(self, node):
|
||||
"""launch the visit starting from the given node"""
|
||||
if node in self._visited:
|
||||
return None
|
||||
self._visited[node] = 1 # FIXME: use set ?
|
||||
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
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user