Merge "Remove pylint"
This commit is contained in:
7
pylintrc
7
pylintrc
@@ -1,7 +0,0 @@
|
|||||||
# pylintrc
|
|
||||||
#
|
|
||||||
# For trove we use the defaults, this file is just to shut up an
|
|
||||||
# annoying error message from pylint.
|
|
||||||
#
|
|
||||||
# Don't set pylint options here.
|
|
||||||
#
|
|
||||||
@@ -8,7 +8,6 @@ python-troveclient>=2.2.0 # Apache-2.0
|
|||||||
testtools>=2.2.0 # MIT
|
testtools>=2.2.0 # MIT
|
||||||
stestr>=1.1.0 # Apache-2.0
|
stestr>=1.1.0 # Apache-2.0
|
||||||
doc8>=0.8.1 # Apache-2.0
|
doc8>=0.8.1 # Apache-2.0
|
||||||
astroid==1.6.5 # LGPLv2.1
|
|
||||||
oslotest>=3.2.0 # Apache-2.0
|
oslotest>=3.2.0 # Apache-2.0
|
||||||
tenacity>=4.9.0 # Apache-2.0
|
tenacity>=4.9.0 # Apache-2.0
|
||||||
reno>=3.1.0 # Apache-2.0
|
reno>=3.1.0 # Apache-2.0
|
||||||
|
|||||||
@@ -1,170 +0,0 @@
|
|||||||
trove-pylint
|
|
||||||
------------
|
|
||||||
|
|
||||||
trove-pylint.py is a wrapper around pylint which allows for some
|
|
||||||
custom processing relevant to the trove source tree, and suitable to
|
|
||||||
run as a CI job for trove.
|
|
||||||
|
|
||||||
The purpose is to perform a lint check on the code and detect obvious
|
|
||||||
(lintable) errors and fix them.
|
|
||||||
|
|
||||||
How trove-pylint works
|
|
||||||
----------------------
|
|
||||||
|
|
||||||
trove-pylint is driven by a configuration file which is by default,
|
|
||||||
located in tools/trove-pylint.config. This file is a json dump of the
|
|
||||||
configuration. A default configuration file looks like this.
|
|
||||||
|
|
||||||
{
|
|
||||||
"include": ["*.py"],
|
|
||||||
"folder": "trove",
|
|
||||||
"options": ["--rcfile=./pylintrc", "-E"],
|
|
||||||
"ignored_files": ['trove/tests'],
|
|
||||||
"ignored_codes": [],
|
|
||||||
"ignored_messages": [],
|
|
||||||
"ignored_file_codes": [],
|
|
||||||
"ignored_file_messages": [],
|
|
||||||
"ignored_file_code_messages": [],
|
|
||||||
"always_error_messages": [
|
|
||||||
"Undefined variable '_'",
|
|
||||||
"Undefined variable '_LE'",
|
|
||||||
"Undefined variable '_LI'",
|
|
||||||
"Undefined variable '_LW'",
|
|
||||||
"Undefined variable '_LC'"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
include
|
|
||||||
-------
|
|
||||||
|
|
||||||
Provide a list of match specs (passed to fnmatch.fnmatch). The
|
|
||||||
default is only "*.py".
|
|
||||||
|
|
||||||
folder
|
|
||||||
------
|
|
||||||
|
|
||||||
Provide the name of the top level folder to lint. This is a single
|
|
||||||
value.
|
|
||||||
|
|
||||||
options
|
|
||||||
-------
|
|
||||||
|
|
||||||
These are the pylint launch options. The default is to specify an
|
|
||||||
rcfile and only errors. Specifying the rcfile is required, and the
|
|
||||||
file is a dummy, to suppress an annoying warning.
|
|
||||||
|
|
||||||
ignored_files
|
|
||||||
-------------
|
|
||||||
|
|
||||||
This is a list of paths that we wish to ignore. When a file is
|
|
||||||
considered for linting, if the path name begins with any of these
|
|
||||||
provided prefixes, the file will not be linted. We ignore the
|
|
||||||
tests directory because it has a high instance of false positives.
|
|
||||||
|
|
||||||
ignored_codes, ignored_messages, ignored_file_codes,
|
|
||||||
ignored_file_messages, and ignored_file_code_messages
|
|
||||||
-----------------------------------------------------
|
|
||||||
|
|
||||||
These settings identify specific failures that are to be
|
|
||||||
ignored. Each is a list, some are lists of single elements, others
|
|
||||||
are lists of lists.
|
|
||||||
|
|
||||||
ignored_codes, and ignored_messages are lists of single elements
|
|
||||||
that are to be ignored. You could specify either the code name, or
|
|
||||||
the code numeric representation. You must specify the exact
|
|
||||||
message.
|
|
||||||
|
|
||||||
ignored_file_codes and ignored_file_messages are lists of lists
|
|
||||||
where each element is a code and a message.
|
|
||||||
|
|
||||||
ignored_file_code_messages is a list of lists where each element
|
|
||||||
consists of a filename, an errorcode, a message, a line number and
|
|
||||||
a function name.
|
|
||||||
|
|
||||||
always_error_messages
|
|
||||||
---------------------
|
|
||||||
|
|
||||||
This is a list of messages which have a low chance of false
|
|
||||||
positives, which are always flagged as errors.
|
|
||||||
|
|
||||||
Using trove-pylint
|
|
||||||
------------------
|
|
||||||
|
|
||||||
You can check your code for errors by simply running:
|
|
||||||
|
|
||||||
tox -e pylint
|
|
||||||
|
|
||||||
or explicitly as:
|
|
||||||
|
|
||||||
tox -e pylint check
|
|
||||||
|
|
||||||
The equivalent result can be obtained by running the command:
|
|
||||||
|
|
||||||
tools/trove-pylint.py
|
|
||||||
|
|
||||||
or
|
|
||||||
|
|
||||||
tools/trove-pylint.py check
|
|
||||||
|
|
||||||
Running the tool directly may require installing addition pip
|
|
||||||
modules on your machine (such as pylint), so using 'tox' is the
|
|
||||||
preferred method.
|
|
||||||
|
|
||||||
|
|
||||||
For example, here is the result from such a run.
|
|
||||||
|
|
||||||
$ tox -e pylint check
|
|
||||||
ERROR: trove/common/extensions.py 575: E1003 bad-super-call, \
|
|
||||||
TroveExtensionMiddleware.__init__: Bad first argument \
|
|
||||||
'ExtensionMiddleware' given to super()
|
|
||||||
Check failed. 367 files processed, 1 had errors, 1 errors recorded.
|
|
||||||
|
|
||||||
I wish to ignore this error and keep going. To do this, I rebuild the
|
|
||||||
list of errors to ignore as follows.
|
|
||||||
|
|
||||||
$ tox -e pylint rebuild
|
|
||||||
Rebuild completed. 367 files processed, 177 exceptions recorded.
|
|
||||||
|
|
||||||
This caused the tool to add the following two things to the config file.
|
|
||||||
|
|
||||||
[
|
|
||||||
"trove/common/extensions.py",
|
|
||||||
"E1003",
|
|
||||||
"Bad first argument 'ExtensionMiddleware' given to super()",
|
|
||||||
"TroveExtensionMiddleware.__init__"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"trove/common/extensions.py",
|
|
||||||
"bad-super-call",
|
|
||||||
"Bad first argument 'ExtensionMiddleware' given to super()",
|
|
||||||
"TroveExtensionMiddleware.__init__"
|
|
||||||
],
|
|
||||||
|
|
||||||
With that done, I can recheck as shown below.
|
|
||||||
|
|
||||||
$ tox -e pylint
|
|
||||||
Check succeeded. 367 files processed
|
|
||||||
|
|
||||||
You can review the errors that are being currently ignored by reading
|
|
||||||
the file tools/trove-pylint.config.
|
|
||||||
|
|
||||||
If you want to fix some of these errors, identify the configuration(s)
|
|
||||||
that are causing those errors to be ignored, remove them and re-run the
|
|
||||||
check. Once you see that the errors are in fact being reported by the
|
|
||||||
tool, go ahead and fix the problem(s) and retest.
|
|
||||||
|
|
||||||
Known issues
|
|
||||||
------------
|
|
||||||
|
|
||||||
1. The tool appears to be very sensitive to the version(s) of pylint
|
|
||||||
and astroid. In testing, I've found that if the version of either of
|
|
||||||
these changes, you could either have a failure of the tool (exceptions
|
|
||||||
thrown, ...) or a different set of errors reported.
|
|
||||||
|
|
||||||
Refer to test-requirements.txt to see the versions currently being used.
|
|
||||||
|
|
||||||
If you run the tool on your machine and find that there are no errors,
|
|
||||||
but find that either the CI generates errors, or that the tool run
|
|
||||||
through tox generates errors, check what versions of astroid and
|
|
||||||
pylint are being run in each configuration.
|
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,350 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# Copyright 2016 Tesora, Inc.
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
import fnmatch
|
|
||||||
import json
|
|
||||||
from collections import OrderedDict
|
|
||||||
import io
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from pylint import lint
|
|
||||||
from pylint.reporters import text
|
|
||||||
|
|
||||||
DEFAULT_CONFIG_FILE = "tools/trove-pylint.config"
|
|
||||||
DEFAULT_IGNORED_FILES = ['trove/tests']
|
|
||||||
DEFAULT_IGNORED_CODES = []
|
|
||||||
DEFAULT_IGNORED_MESSAGES = []
|
|
||||||
DEFAULT_ALWAYS_ERROR = [
|
|
||||||
"Undefined variable '_'",
|
|
||||||
"Undefined variable '_LE'",
|
|
||||||
"Undefined variable '_LI'",
|
|
||||||
"Undefined variable '_LW'",
|
|
||||||
"Undefined variable '_LC'"]
|
|
||||||
|
|
||||||
MODE_CHECK = "check"
|
|
||||||
MODE_REBUILD = "rebuild"
|
|
||||||
|
|
||||||
class Config(object):
|
|
||||||
def __init__(self, filename=DEFAULT_CONFIG_FILE):
|
|
||||||
|
|
||||||
self.default_config = {
|
|
||||||
"include": ["*.py"],
|
|
||||||
"folder": "trove",
|
|
||||||
"options": ["--rcfile=./pylintrc", "-E"],
|
|
||||||
"ignored_files": DEFAULT_IGNORED_FILES,
|
|
||||||
"ignored_codes": DEFAULT_IGNORED_CODES,
|
|
||||||
"ignored_messages": DEFAULT_IGNORED_MESSAGES,
|
|
||||||
"ignored_file_codes": [],
|
|
||||||
"ignored_file_messages": [],
|
|
||||||
"ignored_file_code_messages": [],
|
|
||||||
"always_error_messages": DEFAULT_ALWAYS_ERROR
|
|
||||||
}
|
|
||||||
|
|
||||||
self.config = self.default_config
|
|
||||||
|
|
||||||
def sort_config(self):
|
|
||||||
sorted_config = OrderedDict()
|
|
||||||
for key in sorted(self.config.keys()):
|
|
||||||
value = self.get(key)
|
|
||||||
if isinstance(value, list) and not isinstance(value,str):
|
|
||||||
sorted_config[key] = sorted(value)
|
|
||||||
else:
|
|
||||||
sorted_config[key] = value
|
|
||||||
|
|
||||||
return sorted_config
|
|
||||||
|
|
||||||
def save(self, filename=DEFAULT_CONFIG_FILE):
|
|
||||||
if os.path.isfile(filename):
|
|
||||||
os.rename(filename, "%s~" % filename)
|
|
||||||
|
|
||||||
with open(filename, 'w') as fp:
|
|
||||||
json.dump(self.sort_config(), fp, encoding="utf-8",
|
|
||||||
indent=2, separators=(',', ': '))
|
|
||||||
|
|
||||||
def load(self, filename=DEFAULT_CONFIG_FILE):
|
|
||||||
with open(filename) as fp:
|
|
||||||
self.config = json.load(fp, encoding="utf-8")
|
|
||||||
|
|
||||||
def get(self, attribute):
|
|
||||||
return self.config[attribute]
|
|
||||||
|
|
||||||
def is_file_ignored(self, f):
|
|
||||||
if any(f.startswith(i)
|
|
||||||
for i in self.config['ignored_files']):
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def is_file_included(self, f):
|
|
||||||
if any(fnmatch.fnmatch(f, wc) for wc in self.config['include']):
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def is_always_error(self, message):
|
|
||||||
if message in self.config['always_error_messages']:
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def ignore(self, filename, code, codename, message):
|
|
||||||
# the high priority checks
|
|
||||||
if self.is_file_ignored(filename):
|
|
||||||
return True
|
|
||||||
|
|
||||||
# never ignore messages
|
|
||||||
if self.is_always_error(message):
|
|
||||||
return False
|
|
||||||
|
|
||||||
if code in self.config['ignored_codes']:
|
|
||||||
return True
|
|
||||||
|
|
||||||
if codename in self.config['ignored_codes']:
|
|
||||||
return True
|
|
||||||
|
|
||||||
if message and any(message.startswith(ignore_message)
|
|
||||||
for ignore_message
|
|
||||||
in self.config['ignored_messages']):
|
|
||||||
return True
|
|
||||||
|
|
||||||
if filename and message and (
|
|
||||||
[filename, message] in self.config['ignored_file_messages']):
|
|
||||||
return True
|
|
||||||
|
|
||||||
if filename and code and (
|
|
||||||
[filename, code] in self.config['ignored_file_codes']):
|
|
||||||
return True
|
|
||||||
|
|
||||||
if filename and codename and (
|
|
||||||
[filename, codename] in self.config['ignored_file_codes']):
|
|
||||||
return True
|
|
||||||
|
|
||||||
for fcm in self.config['ignored_file_code_messages']:
|
|
||||||
if filename != fcm[0]:
|
|
||||||
# This ignore rule is for a different file.
|
|
||||||
continue
|
|
||||||
if fcm[1] not in (code, codename):
|
|
||||||
# This ignore rule is for a different code or codename.
|
|
||||||
continue
|
|
||||||
if message.startswith(fcm[2]):
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def ignore_code(self, c):
|
|
||||||
_c = set(self.config['ignored_codes'])
|
|
||||||
_c.add(c)
|
|
||||||
self.config['ignored_codes'] = list(_c)
|
|
||||||
|
|
||||||
def ignore_files(self, f):
|
|
||||||
_c = set(self.config['ignored_files'])
|
|
||||||
_c.add(f)
|
|
||||||
self.config['ignored_files'] = list(_c)
|
|
||||||
|
|
||||||
def ignore_message(self, m):
|
|
||||||
_c = set(self.config['ignored_messages'])
|
|
||||||
_c.add(m)
|
|
||||||
self.config['ignored_messages'] = list(_c)
|
|
||||||
|
|
||||||
def ignore_file_code(self, f, c):
|
|
||||||
_c = set(self.config['ignored_file_codes'])
|
|
||||||
_c.add((f, c))
|
|
||||||
self.config['ignored_file_codes'] = list(_c)
|
|
||||||
|
|
||||||
def ignore_file_message(self, f, m):
|
|
||||||
_c = set(self.config['ignored_file_messages'])
|
|
||||||
_c.add((f, m))
|
|
||||||
self.config['ignored_file_messages'] = list(_c)
|
|
||||||
|
|
||||||
def ignore_file_code_message(self, f, c, m, fn):
|
|
||||||
_c = set(self.config['ignored_file_code_messages'])
|
|
||||||
_c.add((f, c, m, fn))
|
|
||||||
self.config['ignored_file_code_messages'] = list(_c)
|
|
||||||
|
|
||||||
def main():
|
|
||||||
if len(sys.argv) == 1 or sys.argv[1] == "check":
|
|
||||||
return check()
|
|
||||||
elif sys.argv[1] == "rebuild":
|
|
||||||
return rebuild()
|
|
||||||
elif sys.argv[1] == "initialize":
|
|
||||||
return initialize()
|
|
||||||
else:
|
|
||||||
return usage()
|
|
||||||
|
|
||||||
def usage():
|
|
||||||
print("Usage: %s [check|rebuild]" % sys.argv[0])
|
|
||||||
print("\tUse this tool to perform a lint check of the trove project.")
|
|
||||||
print("\t check: perform the lint check.")
|
|
||||||
print("\t rebuild: rebuild the list of exceptions to ignore.")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
class ParseableTextReporter(text.TextReporter):
|
|
||||||
name = 'parseable'
|
|
||||||
line_format = '{path}:{line}: [{msg_id}({symbol}), {obj}] {msg}'
|
|
||||||
|
|
||||||
# that's it folks
|
|
||||||
|
|
||||||
|
|
||||||
class LintRunner(object):
|
|
||||||
def __init__(self):
|
|
||||||
self.config = Config()
|
|
||||||
self.idline = re.compile("^[*]* Module .*")
|
|
||||||
self.detail = re.compile(r"(\S+):(\d+): \[(\S+)\((\S+)\),"
|
|
||||||
r" (\S+)?] (.*)")
|
|
||||||
|
|
||||||
def dolint(self, filename):
|
|
||||||
exceptions = set()
|
|
||||||
|
|
||||||
buffer = io.StringIO()
|
|
||||||
reporter = ParseableTextReporter(output=buffer)
|
|
||||||
options = list(self.config.get('options'))
|
|
||||||
options.append(filename)
|
|
||||||
lint.Run(options, reporter=reporter, exit=False)
|
|
||||||
|
|
||||||
output = buffer.getvalue()
|
|
||||||
buffer.close()
|
|
||||||
|
|
||||||
for line in output.splitlines():
|
|
||||||
if self.idline.match(line):
|
|
||||||
continue
|
|
||||||
|
|
||||||
if self.detail.match(line):
|
|
||||||
mo = self.detail.search(line)
|
|
||||||
tokens = mo.groups()
|
|
||||||
fn = tokens[0]
|
|
||||||
ln = tokens[1]
|
|
||||||
code = tokens[2]
|
|
||||||
codename = tokens[3]
|
|
||||||
func = tokens[4]
|
|
||||||
message = tokens[5]
|
|
||||||
|
|
||||||
if not self.config.ignore(fn, code, codename, message):
|
|
||||||
exceptions.add((fn, ln, code, codename, func, message))
|
|
||||||
|
|
||||||
return exceptions
|
|
||||||
|
|
||||||
def process(self, mode=MODE_CHECK):
|
|
||||||
files_processed = 0
|
|
||||||
files_with_errors = 0
|
|
||||||
errors_recorded = 0
|
|
||||||
exceptions_recorded = 0
|
|
||||||
all_exceptions = []
|
|
||||||
|
|
||||||
for (root, dirs, files) in os.walk(self.config.get('folder')):
|
|
||||||
# if we shouldn't even bother about this part of the
|
|
||||||
# directory structure, we can punt quietly
|
|
||||||
if self.config.is_file_ignored(root):
|
|
||||||
continue
|
|
||||||
|
|
||||||
# since we are walking top down, let's clean up the dirs
|
|
||||||
# that we will walk by eliminating any dirs that will
|
|
||||||
# end up getting ignored
|
|
||||||
for d in dirs:
|
|
||||||
p = os.path.join(root, d)
|
|
||||||
if self.config.is_file_ignored(p):
|
|
||||||
dirs.remove(d)
|
|
||||||
|
|
||||||
# check if we can ignore the file and process if not
|
|
||||||
for f in files:
|
|
||||||
p = os.path.join(root, f)
|
|
||||||
if self.config.is_file_ignored(p):
|
|
||||||
continue
|
|
||||||
|
|
||||||
if not self.config.is_file_included(f):
|
|
||||||
continue
|
|
||||||
|
|
||||||
files_processed += 1
|
|
||||||
exceptions = self.dolint(p)
|
|
||||||
file_had_errors = 0
|
|
||||||
|
|
||||||
for e in exceptions:
|
|
||||||
# what we do with this exception depents on the
|
|
||||||
# kind of exception, and the mode
|
|
||||||
if self.config.is_always_error(e[5]):
|
|
||||||
all_exceptions.append(e)
|
|
||||||
errors_recorded += 1
|
|
||||||
file_had_errors += 1
|
|
||||||
elif mode == MODE_REBUILD:
|
|
||||||
# parameters to ignore_file_code_message are
|
|
||||||
# filename, code, message and function
|
|
||||||
self.config.ignore_file_code_message(e[0], e[2], e[-1], e[4])
|
|
||||||
self.config.ignore_file_code_message(e[0], e[3], e[-1], e[4])
|
|
||||||
exceptions_recorded += 1
|
|
||||||
elif mode == MODE_CHECK:
|
|
||||||
all_exceptions.append(e)
|
|
||||||
errors_recorded += 1
|
|
||||||
file_had_errors += 1
|
|
||||||
|
|
||||||
if file_had_errors:
|
|
||||||
files_with_errors += 1
|
|
||||||
|
|
||||||
for e in sorted(all_exceptions):
|
|
||||||
print("ERROR: %s %s: %s %s, %s: %s" %
|
|
||||||
(e[0], e[1], e[2], e[3], e[4], e[5]))
|
|
||||||
|
|
||||||
return (files_processed, files_with_errors, errors_recorded,
|
|
||||||
exceptions_recorded)
|
|
||||||
|
|
||||||
def rebuild(self):
|
|
||||||
self.initialize()
|
|
||||||
(files_processed,
|
|
||||||
files_with_errors,
|
|
||||||
errors_recorded,
|
|
||||||
exceptions_recorded) = self.process(mode=MODE_REBUILD)
|
|
||||||
|
|
||||||
if files_with_errors > 0:
|
|
||||||
print("Rebuild failed. %s files processed, %s had errors, "
|
|
||||||
"%s errors recorded." % (
|
|
||||||
files_processed, files_with_errors, errors_recorded))
|
|
||||||
|
|
||||||
return 1
|
|
||||||
|
|
||||||
self.config.save()
|
|
||||||
print("Rebuild completed. %s files processed, %s exceptions recorded." %
|
|
||||||
(files_processed, exceptions_recorded))
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def check(self):
|
|
||||||
self.config.load()
|
|
||||||
(files_processed,
|
|
||||||
files_with_errors,
|
|
||||||
errors_recorded,
|
|
||||||
exceptions_recorded) = self.process(mode=MODE_CHECK)
|
|
||||||
|
|
||||||
if files_with_errors > 0:
|
|
||||||
print("Check failed. %s files processed, %s had errors, "
|
|
||||||
"%s errors recorded." % (
|
|
||||||
files_processed, files_with_errors, errors_recorded))
|
|
||||||
return 1
|
|
||||||
|
|
||||||
print("Check succeeded. %s files processed" % files_processed)
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def initialize(self):
|
|
||||||
self.config.save()
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def check():
|
|
||||||
exit(LintRunner().check())
|
|
||||||
|
|
||||||
def rebuild():
|
|
||||||
exit(LintRunner().rebuild())
|
|
||||||
|
|
||||||
def initialize():
|
|
||||||
exit(LintRunner().initialize())
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
Reference in New Issue
Block a user