diff options
author | Ashley Whetter <ashley@awhetter.co.uk> | 2019-02-09 17:10:27 -0800 |
---|---|---|
committer | Ashley Whetter <ashley@awhetter.co.uk> | 2019-02-09 17:22:30 -0800 |
commit | 6d77b04c555cd24b3c7c7cbb28fae9be13b8441d (patch) | |
tree | 97c54f925cc81a02b39609152d305ebb6b3756bf | |
parent | ac46a39da7a69fb6487e0277e2d82244c7dadc00 (diff) | |
download | pylint-git-6d77b04c555cd24b3c7c7cbb28fae9be13b8441d.tar.gz |
Fixed reporting
-rw-r--r-- | pylint/checkers/__init__.py | 2 | ||||
-rw-r--r-- | pylint/checkers/base.py | 5 | ||||
-rw-r--r-- | pylint/checkers/imports.py | 161 | ||||
-rw-r--r-- | pylint/checkers/raw_metrics.py | 20 | ||||
-rw-r--r-- | pylint/checkers/similar.py | 5 | ||||
-rw-r--r-- | pylint/lint.py | 125 |
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__": |