summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAshley Whetter <ashley@awhetter.co.uk>2019-02-09 17:10:27 -0800
committerAshley Whetter <ashley@awhetter.co.uk>2019-02-09 17:22:30 -0800
commit6d77b04c555cd24b3c7c7cbb28fae9be13b8441d (patch)
tree97c54f925cc81a02b39609152d305ebb6b3756bf
parentac46a39da7a69fb6487e0277e2d82244c7dadc00 (diff)
downloadpylint-git-6d77b04c555cd24b3c7c7cbb28fae9be13b8441d.tar.gz
Fixed reporting
-rw-r--r--pylint/checkers/__init__.py2
-rw-r--r--pylint/checkers/base.py5
-rw-r--r--pylint/checkers/imports.py161
-rw-r--r--pylint/checkers/raw_metrics.py20
-rw-r--r--pylint/checkers/similar.py5
-rw-r--r--pylint/lint.py125
6 files changed, 175 insertions, 143 deletions
diff --git a/pylint/checkers/__init__.py b/pylint/checkers/__init__.py
index 40eaef680..cb76399d5 100644
--- a/pylint/checkers/__init__.py
+++ b/pylint/checkers/__init__.py
@@ -81,8 +81,6 @@ class BaseChecker(object):
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
diff --git a/pylint/checkers/base.py b/pylint/checkers/base.py
index 05de23ef8..2e370525c 100644
--- a/pylint/checkers/base.py
+++ b/pylint/checkers/base.py
@@ -352,7 +352,7 @@ def _has_abstract_methods(node):
return len(utils.unimplemented_abstract_methods(node)) > 0
-def report_by_type_stats(sect, stats, old_stats):
+def report_by_type_stats(sect, stats, old_stats, _, __):
"""make a report of
* percentage of different types documented
@@ -988,8 +988,6 @@ class BasicChecker(_BasicChecker):
),
}
- reports = (("RP0101", "Statistics by type", report_by_type_stats),)
-
def __init__(self, linter):
_BasicChecker.__init__(self, linter)
self.stats = None
@@ -2215,6 +2213,7 @@ def register(linter):
"""required method to auto register this checker"""
linter.register_checker(BasicErrorChecker(linter))
linter.register_checker(BasicChecker(linter))
+ linter.register_report("RP0101", "Statistics by type", report_by_type_stats)
linter.register_checker(NameChecker(linter))
linter.register_checker(DocStringChecker(linter))
linter.register_checker(PassChecker(linter))
diff --git a/pylint/checkers/imports.py b/pylint/checkers/imports.py
index 355fea27a..433b34e9e 100644
--- a/pylint/checkers/imports.py
+++ b/pylint/checkers/imports.py
@@ -36,7 +36,7 @@ import sys
import copy
import astroid
-from astroid import are_exclusive, decorators
+from astroid import are_exclusive
from astroid.modutils import get_module_part, is_standard_module
import isort
@@ -193,6 +193,88 @@ def _make_graph(filename, dep_info, sect, gtype):
sect.append(Paragraph("%simports graph has been written to %s" % (gtype, filename)))
+def _report_external_dependencies(sect, stats, _, __, states):
+ """return a verbatim layout for displaying dependencies"""
+ state = merge_states(states[0])
+ if not state:
+ raise EmptyReportError()
+ dep_info = _make_tree_defs(_external_dependencies_info(stats, state).items())
+ if not dep_info:
+ raise EmptyReportError()
+ tree_str = _repr_tree_defs(dep_info)
+ sect.append(VerbatimText(tree_str))
+
+
+def _report_dependencies_graph(sect, stats, _, global_config, states):
+ """write dependencies as a dot (graphviz) file"""
+ state = merge_states(states[0])
+ dep_info = stats["dependencies"]
+ if (
+ not dep_info
+ or not state
+ or not (
+ global_config.import_graph
+ or global_config.ext_import_graph
+ or global_config.int_import_graph
+ )
+ ):
+ raise EmptyReportError()
+ filename = global_config.import_graph
+ if filename:
+ _make_graph(filename, dep_info, sect, "")
+ filename = global_config.ext_import_graph
+ if filename:
+ _make_graph(
+ filename, _external_dependencies_info(stats, state), sect, "external "
+ )
+ filename = global_config.int_import_graph
+ if filename:
+ _make_graph(
+ filename, _internal_dependencies_info(stats, state), sect, "internal "
+ )
+
+
+def _external_dependencies_info(stats, state):
+ """return cached external dependencies information or build and
+ cache them
+ """
+ return _filter_dependencies_graph(stats, state, internal=False)
+
+
+def _internal_dependencies_info(stats, state):
+ """return cached internal dependencies information or build and
+ cache them
+ """
+ return _filter_dependencies_graph(stats, state, internal=True)
+
+
+def _filter_dependencies_graph(stats, state, internal):
+ """build the internal or the external depedency graph"""
+ graph = collections.defaultdict(set)
+ for importee, importers in stats["dependencies"].items():
+ for importer in importers:
+ package = state.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
+
+
+def merge_states(states):
+ if not states:
+ return None
+
+ state = states[0]
+ for other_state in states[1:]:
+ for key, value in other_state.import_graph.items():
+ state.import_graph[key].update(value)
+ state.module_pkg.update(other_state.module_pkg)
+ for key, value in other_state.excluded_edges.items():
+ state.excluded_edges[key].update(value)
+
+ return state
+
+
# the import checker itself ###################################################
MSGS = {
@@ -388,10 +470,6 @@ class ImportsChecker(BaseChecker):
self.stats = None
self._imports_stack = []
self._first_non_import_node = None
- self.reports = (
- ("RP0401", "External dependencies", self._report_external_dependencies),
- ("RP0402", "Modules dependencies graph", self._report_dependencies_graph),
- )
self._site_packages = self._compute_site_packages()
@@ -443,13 +521,7 @@ class ImportsChecker(BaseChecker):
def global_close(self, states):
"""called before visiting project (i.e set of modules)"""
if self.linter.is_message_enabled("cyclic-import") and states:
- state = states[0]
- for other_state in states[1:]:
- for key, value in other_state.import_graph.items():
- state.import_graph[key].update(value)
- state.module_pkg.update(other_state.module_pkg)
- for key, value in other_state.excluded_edges.items():
- state.excluded_edges[key].update(value)
+ state = merge_states(states)
graph = self._import_graph_without_ignored_edges(state)
vertices = list(graph)
@@ -869,59 +941,6 @@ class ImportsChecker(BaseChecker):
"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:
- # TODO: Needs a state!
- 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
@@ -943,3 +962,15 @@ class ImportsChecker(BaseChecker):
def register(linter):
"""required method to auto register this checker """
linter.register_checker(ImportsChecker(linter))
+ linter.register_report(
+ "RP0401",
+ "External dependencies",
+ _report_external_dependencies,
+ (ImportsChecker,),
+ )
+ linter.register_report(
+ "RP0402",
+ "Modules dependencies graph",
+ _report_dependencies_graph,
+ (ImportsChecker,),
+ )
diff --git a/pylint/checkers/raw_metrics.py b/pylint/checkers/raw_metrics.py
index 7ab165675..baeafef0d 100644
--- a/pylint/checkers/raw_metrics.py
+++ b/pylint/checkers/raw_metrics.py
@@ -25,7 +25,7 @@ from pylint.reporters import diff_string
from pylint.reporters.ureports.nodes import Table
-def report_raw_stats(sect, stats, old_stats):
+def report_raw_stats(sect, stats, old_stats, global_config, states):
"""calculate percentage of code / doc / comment / empty
"""
total_lines = stats["total_lines"]
@@ -63,8 +63,6 @@ class RawMetricsChecker(BaseTokenChecker):
options = ()
# messages
msgs = {} # type: Any
- # reports
- reports = (("RP0701", "Raw metrics", report_raw_stats),)
def __init__(self, linter):
BaseTokenChecker.__init__(self, linter)
@@ -72,13 +70,12 @@ class RawMetricsChecker(BaseTokenChecker):
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,
- )
+ self.stats = self.linter.stats
+ self.stats.setdefault("total_lines", 0)
+ self.stats.setdefault("code_lines", 0)
+ self.stats.setdefault("empty_lines", 0)
+ self.stats.setdefault("docstring_lines", 0)
+ self.stats.setdefault("comment_lines", 0)
def process_tokens(self, tokens):
"""update stats"""
@@ -123,3 +120,6 @@ def get_type(tokens, start_index):
def register(linter):
""" required method to auto register this checker """
linter.register_checker(RawMetricsChecker(linter))
+ linter.register_report(
+ "RP0701", "Raw metrics", report_raw_stats, [RawMetricsChecker]
+ )
diff --git a/pylint/checkers/similar.py b/pylint/checkers/similar.py
index aed489b14..daec23820 100644
--- a/pylint/checkers/similar.py
+++ b/pylint/checkers/similar.py
@@ -268,7 +268,7 @@ MSGS = {
}
-def report_similarities(sect, stats, old_stats):
+def report_similarities(sect, stats, old_stats, _, __):
"""make a layout with some stats about duplication"""
lines = ["", "now", "previous", "difference"]
lines += table_lines_from_stats(
@@ -329,8 +329,6 @@ class SimilarChecker(BaseChecker, Similar):
},
),
)
- # reports
- reports = (("RP0801", "Duplication", report_similarities),) # type: ignore
def __init__(self, linter=None):
BaseChecker.__init__(self, linter)
@@ -394,6 +392,7 @@ class SimilarChecker(BaseChecker, Similar):
def register(linter):
"""required method to auto register this checker """
linter.register_checker(SimilarChecker(linter))
+ linter.register_report("RP0801", "Duplication", report_similarities)
def usage(status=0):
diff --git a/pylint/lint.py b/pylint/lint.py
index 0bc2853ef..bf7ce7e09 100644
--- a/pylint/lint.py
+++ b/pylint/lint.py
@@ -135,7 +135,7 @@ def _merge_stats(stats):
# some reporting functions ####################################################
-def report_total_messages_stats(sect, stats, previous_stats):
+def report_total_messages_stats(sect, stats, previous_stats, _, __):
"""make total errors / warnings report"""
lines = ["type", "number", "previous", "difference"]
lines += checkers.table_lines_from_stats(
@@ -144,7 +144,7 @@ def report_total_messages_stats(sect, stats, previous_stats):
sect.append(report_nodes.Table(children=lines, cols=4, rheaders=1))
-def report_messages_stats(sect, stats, _):
+def report_messages_stats(sect, stats, _, __, ___):
"""make messages type report"""
if not stats["by_msg"]:
# don't print this report when we didn't detected any errors
@@ -152,7 +152,7 @@ def report_messages_stats(sect, stats, _):
in_order = sorted(
[
(value, msg_id)
- for msg_id, value in six.iteritems(stats["by_msg"])
+ for msg_id, value in stats["by_msg"].items()
if not msg_id.startswith("I")
]
)
@@ -163,7 +163,7 @@ def report_messages_stats(sect, stats, _):
sect.append(report_nodes.Table(children=lines, cols=2, rheaders=1))
-def report_messages_by_module_stats(sect, stats, _):
+def report_messages_by_module_stats(sect, stats, _, __, ____):
"""make errors / warnings by modules report"""
if len(stats["by_module"]) == 1:
# don't print this report when we are analysing a single module
@@ -171,7 +171,7 @@ def report_messages_by_module_stats(sect, stats, _):
by_mod = collections.defaultdict(dict)
for m_type in ("fatal", "error", "warning", "refactor", "convention"):
total = stats[m_type]
- for module in six.iterkeys(stats["by_module"]):
+ for module in stats["by_module"]:
mod_total = stats["by_module"][module][m_type]
if total == 0:
percent = 0
@@ -179,7 +179,7 @@ def report_messages_by_module_stats(sect, stats, _):
percent = float((mod_total) * 100) / total
by_mod[module][m_type] = percent
sorted_result = []
- for module, mod_info in six.iteritems(by_mod):
+ for module, mod_info in by_mod.items():
sorted_result.append(
(
mod_info["error"],
@@ -559,11 +559,7 @@ class PyLinter(utils.MessagesHandlerMixIn, checkers.BaseTokenChecker):
("Reports", "Options related to output formatting and reporting"),
)
- reports = (
- ("RP0001", "Messages by category", report_total_messages_stats),
- ("RP0002", "% errors / warnings by module", report_messages_by_module_stats),
- ("RP0003", "Messages", report_messages_stats),
- )
+ reports = ()
def __init__(self, config=None):
# some stuff has to be done before ancestors initialization...
@@ -856,11 +852,19 @@ def guess_lint_path(args):
class ReportRegistry:
def __init__(self):
- self.reports = collections.defaultdict(list)
+ self.reports = {}
self._reports_state = {}
super().__init__()
- def register_report(self, reportid, r_title, r_cb, checker):
+ self.register_report(
+ "RP0001", "Messages by category", report_total_messages_stats
+ )
+ self.register_report(
+ "RP0002", "% errors / warnings by module", report_messages_by_module_stats
+ )
+ self.register_report("RP0003", "Messages", report_messages_stats)
+
+ def register_report(self, reportid, r_title, r_cb, checker_classes=()):
"""Register a report
:param reportid: The unique identifier for the report.
@@ -869,11 +873,12 @@ class ReportRegistry:
:type r_title: str
:param r_cb: The method to call to make the report.
:type r_cb: callable
- :param checker: The checker defining the report.
- :type checker: BaseChecker
+ :param checker_classes: The checkers that create states
+ needed by this report.
+ :type checker_classes: class
"""
reportid = reportid.upper()
- self.reports[checker].append((reportid, r_title, r_cb))
+ self.reports[reportid] = (r_title, r_cb, checker_classes)
def enable_report(self, reportid):
"""Enable the report of the given id.
@@ -895,9 +900,8 @@ class ReportRegistry:
def disable_reporters(self):
"""Disable all reporters."""
- for _reporters in self.reports.values():
- for report_id, _, _ in _reporters:
- self.disable_report(report_id)
+ for report_id in self.reports:
+ self.disable_report(report_id)
def report_is_enabled(self, reportid):
"""Check if the report with the given id is enabled.
@@ -923,9 +927,6 @@ class PluginRegistry(utils.MessagesHandlerMixIn, ReportRegistry):
self._python3_porting_mode = False
self._error_mode = False
- for r_id, r_title, r_cb in PyLinter.reports:
- self.register_report(r_id, r_title, r_cb, PyLinter)
-
self.register_options(PyLinter.options)
self.msgs_store.register_messages_from_checker(PyLinter)
@@ -972,9 +973,6 @@ class PluginRegistry(utils.MessagesHandlerMixIn, ReportRegistry):
self._checkers[checker.name].append(checker)
- for r_id, r_title, r_cb in checker.reports:
- self.register_report(r_id, r_title, r_cb, checker)
-
self.register_options(checker.options)
if hasattr(checker, "msgs"):
@@ -1378,9 +1376,11 @@ group are mutually exclusive.",
with fix_import_path(self._global_config.module_or_package):
assert self._global_config.jobs == 1
- base_name, status_code = self.check(self._global_config.module_or_package)
+ base_name, all_states, status_code = self.check(
+ self._global_config.module_or_package
+ )
- self.generate_reports(base_name)
+ self.generate_reports(base_name, all_states)
if self._global_config.exit_zero:
sys.exit(0)
@@ -1435,7 +1435,7 @@ group are mutually exclusive.",
reporter_class = getattr(module, class_name)
return reporter_class
- def generate_reports(self, base_name):
+ def generate_reports(self, base_name, all_states):
"""close the whole package /module, it's time to make reports !
if persistent run, pickle results for later comparison
@@ -1449,7 +1449,9 @@ group are mutually exclusive.",
# XXX code below needs refactoring to be more reporter agnostic
self._reporter.on_close(self._plugin_registry.stats, previous_stats)
if self._global_config.reports:
- sect = self.make_reports(self._plugin_registry.stats, previous_stats)
+ sect = self.make_reports(
+ self._plugin_registry.stats, previous_stats, all_states
+ )
else:
sect = report_nodes.Section()
@@ -1466,19 +1468,19 @@ group are mutually exclusive.",
"""A list of reports, sorted in the order in which they must be called.
:returns: The list of reports.
- :rtype: list(BaseChecker or object)
+ :rtype: list(str)
"""
- reports = self._plugin_registry.reports
- reports = sorted(reports, key=lambda x: getattr(x, "name", ""))
- try:
- reports.remove(PyLinter)
- except ValueError:
- pass
- else:
- reports.append(PyLinter)
+ reports = sorted(self._plugin_registry.reports)
+ for final_report in ("RP0001", "RP0002", "RP0003"):
+ try:
+ reports.remove(final_report)
+ except ValueError:
+ pass
+ else:
+ reports.append(final_report)
return reports
- def make_reports(self, stats, old_stats):
+ def make_reports(self, stats, old_stats, all_states):
"""Render the registered reports.
:param stats: The statistics dictionary for this run.
@@ -1492,17 +1494,20 @@ group are mutually exclusive.",
sect = report_nodes.Section(
"Report", "%s statements analysed." % (stats["statement"])
)
- for checker in self.report_order():
- for reportid, r_title, r_cb in self._plugin_registry.reports[checker]:
- if not self.report_is_enabled(reportid):
- continue
- report_sect = report_nodes.Section(r_title)
- try:
- r_cb(report_sect, stats, old_stats)
- except EmptyReportError:
- continue
- report_sect.report_id = reportid
- sect.append(report_sect)
+ for report_id in self.report_order():
+ r_title, r_cb, checker_classes = self._plugin_registry.reports[report_id]
+ states = [
+ all_states.get(checker_class) for checker_class in checker_classes
+ ]
+ if not self._plugin_registry.report_is_enabled(report_id):
+ continue
+ report_sect = report_nodes.Section(r_title)
+ try:
+ r_cb(report_sect, stats, old_stats, self._global_config, states)
+ except exceptions.EmptyReportError:
+ continue
+ report_sect.report_id = report_id
+ sect.append(report_sect)
return sect
def _report_evaluation(self, base_name):
@@ -1539,15 +1544,15 @@ group are mutually exclusive.",
def prepare_checkers(self, linter):
"""return checkers needed for activated messages and reports"""
# get needed checkers
- neededcheckers = []
- for checker in self.get_checkers()[1:]:
- messages = set(
- msg for msg in checker.msgs if linter.is_message_enabled(msg)
- )
- if messages or any(
- self._plugin_registry.report_is_enabled(r[0]) for r in checker.reports
- ):
- neededcheckers.append(checker)
+ neededcheckers = set()
+ for checker_cls in self.get_checkers()[1:]:
+ if any(linter.is_message_enabled(msg) for msg in checker_cls.msgs):
+ neededcheckers.add(checker_cls)
+
+ for report_id, (_, _, checker_classes) in self._plugin_registry.reports.items():
+ if self._plugin_registry.report_is_enabled(report_id):
+ neededcheckers.update(checker_classes)
+
# Sort checkers by priority
neededcheckers = sorted(
neededcheckers, key=operator.attrgetter("priority"), reverse=True
@@ -1686,7 +1691,7 @@ group are mutually exclusive.",
all_stats.append(linter.stats)
self._plugin_registry.stats = _merge_stats(all_stats)
- return module_desc.basename, linter.msg_status
+ return module_desc.basename, all_states, linter.msg_status
if __name__ == "__main__":