From 4a1c791a46d90d8ef2d74cc8fbd291fc1b52deb6 Mon Sep 17 00:00:00 2001 From: Claudiu Popa Date: Sat, 5 Sep 2015 15:24:35 +0300 Subject: Bring logilab-common's ureports into pylint.reporters. With this change, we moved away from depending on logilab-common, having in Pylint all the components that were used from logilab-common. The API should be considered an implementation detail and can change at some point in the future. Closes issue #621. --- ChangeLog | 8 ++ pylint/__pkginfo__.py | 1 - pylint/checkers/base.py | 3 +- pylint/checkers/exceptions.py | 4 +- pylint/checkers/imports.py | 3 +- pylint/checkers/raw_metrics.py | 4 +- pylint/checkers/similar.py | 3 +- pylint/lint.py | 14 +- pylint/reporters/__init__.py | 4 +- pylint/reporters/guireporter.py | 2 +- pylint/reporters/html.py | 4 +- pylint/reporters/text.py | 2 +- pylint/reporters/ureports/__init__.py | 124 ++++++++++++++++ pylint/reporters/ureports/html_writer.py | 127 +++++++++++++++++ pylint/reporters/ureports/nodes.py | 183 ++++++++++++++++++++++++ pylint/reporters/ureports/text_writer.py | 143 +++++++++++++++++++ pylint/reporters/ureports/tree.py | 235 +++++++++++++++++++++++++++++++ pylint/test/unittest_reporting.py | 3 +- pylint/utils.py | 3 +- tox.ini | 2 - 20 files changed, 842 insertions(+), 30 deletions(-) create mode 100644 pylint/reporters/ureports/__init__.py create mode 100644 pylint/reporters/ureports/html_writer.py create mode 100644 pylint/reporters/ureports/nodes.py create mode 100644 pylint/reporters/ureports/text_writer.py create mode 100644 pylint/reporters/ureports/tree.py diff --git a/ChangeLog b/ChangeLog index e969f62..c5b7f34 100644 --- a/ChangeLog +++ b/ChangeLog @@ -275,6 +275,14 @@ ChangeLog for Pylint what exception will be raised and it will not be compatible with Python 3 anyhow. Closes issue #633. + * Bring logilab-common's ureports into pylint.reporters. + + With this change, we moved away from depending on logilab-common, + having in Pylint all the components that were used from logilab-common. + The API should be considered an implementation detail and can change at + some point in the future. + Closes issue #621. + 2015-03-14 -- 1.4.3 diff --git a/pylint/__pkginfo__.py b/pylint/__pkginfo__.py index 392ab03..edac893 100644 --- a/pylint/__pkginfo__.py +++ b/pylint/__pkginfo__.py @@ -26,7 +26,6 @@ numversion = (1, 5, 0) version = '.'.join([str(num) for num in numversion]) install_requires = [ - 'logilab-common >= 0.53.0', 'astroid >= 1.3.6', 'six', ] diff --git a/pylint/checkers/base.py b/pylint/checkers/base.py index 0c3f67a..e8c9aa7 100644 --- a/pylint/checkers/base.py +++ b/pylint/checkers/base.py @@ -24,8 +24,6 @@ import re import six from six.moves import zip # pylint: disable=redefined-builtin -from logilab.common.ureports import Table - import astroid import astroid.bases import astroid.scoped_nodes @@ -47,6 +45,7 @@ from pylint.checkers.utils import ( error_of_type, unimplemented_abstract_methods, ) +from pylint.reporters.ureports.nodes import Table # regex for class/function/variable/constant name diff --git a/pylint/checkers/exceptions.py b/pylint/checkers/exceptions.py index 7359e96..c068c98 100644 --- a/pylint/checkers/exceptions.py +++ b/pylint/checkers/exceptions.py @@ -152,7 +152,7 @@ class ExceptionsChecker(BaseChecker): 'raising-bad-type', 'raising-non-exception', 'notimplemented-raised', 'bad-exception-context') def visit_raise(self, node): - """visit raise possibly inferring value""" + """visit raise possibly inferring value""" if node.exc is None: self._check_misplaced_bare_raise(node) return @@ -187,7 +187,7 @@ class ExceptionsChecker(BaseChecker): expected = (astroid.ExceptHandler,) if (not current or not isinstance(current.parent, expected)): - self.add_message('misplaced-bare-raise', node=node) + self.add_message('misplaced-bare-raise', node=node) def _check_bad_exception_context(self, node): """Verify that the exception context is properly set. diff --git a/pylint/checkers/imports.py b/pylint/checkers/imports.py index c1688df..2992991 100644 --- a/pylint/checkers/imports.py +++ b/pylint/checkers/imports.py @@ -20,8 +20,6 @@ from collections import defaultdict import six -from logilab.common.ureports import VerbatimText, Paragraph - import astroid from astroid import are_exclusive from astroid.modutils import get_module_part, is_standard_module @@ -31,6 +29,7 @@ from pylint.utils import EmptyReport, get_global_option from pylint.checkers import BaseChecker from pylint.checkers.utils import check_messages, node_ignores_exception from pylint.graph import get_cycles, DotBackend +from pylint.reporters.ureports.nodes import VerbatimText, Paragraph def _get_import_name(importnode, modname): diff --git a/pylint/checkers/raw_metrics.py b/pylint/checkers/raw_metrics.py index 71fecf6..1cfff83 100644 --- a/pylint/checkers/raw_metrics.py +++ b/pylint/checkers/raw_metrics.py @@ -25,12 +25,12 @@ import tokenize #if not hasattr(tokenize, 'NL'): # raise ValueError("tokenize.NL doesn't exist -- tokenize module too old") -from logilab.common.ureports import Table - from pylint.interfaces import ITokenChecker from pylint.utils import EmptyReport 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 diff --git a/pylint/checkers/similar.py b/pylint/checkers/similar.py index 9542077..e500d2c 100644 --- a/pylint/checkers/similar.py +++ b/pylint/checkers/similar.py @@ -20,10 +20,9 @@ from __future__ import print_function import sys from collections import defaultdict -from logilab.common.ureports import Table - from pylint.interfaces import IRawChecker from pylint.checkers import BaseChecker, table_lines_from_stats +from pylint.reporters.ureports.nodes import Table import six from six.moves import zip diff --git a/pylint/lint.py b/pylint/lint.py index 16913f4..698f3e6 100644 --- a/pylint/lint.py +++ b/pylint/lint.py @@ -42,7 +42,6 @@ import warnings import astroid from astroid.__pkginfo__ import version as astroid_version from astroid import modutils -from logilab.common import ureports import six from pylint import checkers @@ -51,6 +50,7 @@ from pylint import reporters from pylint import utils from pylint import config from pylint.__pkginfo__ import version +from pylint.reporters.ureports import nodes as report_nodes MANAGER = astroid.MANAGER @@ -969,7 +969,7 @@ class PyLinter(config.OptionsManagerMixIn, filename = 'pylint_global.' + self.reporter.extension self.reporter.set_output(open(filename, 'w')) else: - sect = ureports.Section() + sect = report_nodes.Section() if self.config.reports or self.config.output_format == 'html': self.reporter.display_results(sect) # save results if persistent run @@ -980,7 +980,7 @@ class PyLinter(config.OptionsManagerMixIn, # No output will be emitted for the html # reporter if the file doesn't exist, so emit # the results here. - self.reporter.display_results(ureports.Section()) + self.reporter.display_results(report_nodes.Section()) self.reporter.on_close(self.stats, {}) # specific reports ######################################################## @@ -1003,7 +1003,7 @@ class PyLinter(config.OptionsManagerMixIn, pnote = previous_stats.get('global_note') if pnote is not None: msg += ' (previous run: %.2f/10, %+.2f)' % (pnote, note - pnote) - sect.append(ureports.Text(msg)) + sect.append(report_nodes.Text(msg)) # some reporting functions #################################################### @@ -1013,7 +1013,7 @@ def report_total_messages_stats(sect, stats, previous_stats): lines += checkers.table_lines_from_stats(stats, previous_stats, ('convention', 'refactor', 'warning', 'error')) - sect.append(ureports.Table(children=lines, cols=4, rheaders=1)) + sect.append(report_nodes.Table(children=lines, cols=4, rheaders=1)) def report_messages_stats(sect, stats, _): """make messages type report""" @@ -1027,7 +1027,7 @@ def report_messages_stats(sect, stats, _): lines = ('message id', 'occurrences') for value, msg_id in in_order: lines += (msg_id, str(value)) - sect.append(ureports.Table(children=lines, cols=2, rheaders=1)) + sect.append(report_nodes.Table(children=lines, cols=2, rheaders=1)) def report_messages_by_module_stats(sect, stats, _): """make errors / warnings by modules report""" @@ -1063,7 +1063,7 @@ def report_messages_by_module_stats(sect, stats, _): lines.append('%.2f' % val) if len(lines) == 5: raise utils.EmptyReport() - sect.append(ureports.Table(children=lines, cols=5, rheaders=1)) + sect.append(report_nodes.Table(children=lines, cols=5, rheaders=1)) # utilities ################################################################### diff --git a/pylint/reporters/__init__.py b/pylint/reporters/__init__.py index 7103ade..5c4437a 100644 --- a/pylint/reporters/__init__.py +++ b/pylint/reporters/__init__.py @@ -21,9 +21,6 @@ import warnings import six - -from pylint import utils - CMPS = ['=', '-', '+'] # py3k has no more cmp builtin @@ -138,4 +135,5 @@ class CollectingReporter(BaseReporter): def initialize(linter): """initialize linter with reporters in this package """ + from pylint import utils utils.register_plugins(linter, __path__[0]) diff --git a/pylint/reporters/guireporter.py b/pylint/reporters/guireporter.py index 4ad4ebb..32f5d22 100644 --- a/pylint/reporters/guireporter.py +++ b/pylint/reporters/guireporter.py @@ -4,7 +4,7 @@ import sys from pylint.interfaces import IReporter from pylint.reporters import BaseReporter -from logilab.common.ureports import TextWriter +from pylint.reporters.ureports.text_writer import TextWriter class GUIReporter(BaseReporter): diff --git a/pylint/reporters/html.py b/pylint/reporters/html.py index 1e050d3..b2214b1 100644 --- a/pylint/reporters/html.py +++ b/pylint/reporters/html.py @@ -17,10 +17,10 @@ import itertools import string import sys -from logilab.common.ureports import HTMLWriter, Section, Table - from pylint.interfaces import IReporter from pylint.reporters import BaseReporter +from pylint.reporters.ureports.html_writer import HTMLWriter +from pylint.reporters.ureports.nodes import Section, Table class HTMLReporter(BaseReporter): diff --git a/pylint/reporters/text.py b/pylint/reporters/text.py index 22b0609..78b1099 100644 --- a/pylint/reporters/text.py +++ b/pylint/reporters/text.py @@ -22,11 +22,11 @@ import warnings import sys import six -from logilab.common.ureports import TextWriter from pylint.interfaces import IReporter from pylint.reporters import BaseReporter from pylint import utils +from pylint.reporters.ureports.text_writer import TextWriter TITLE_UNDERLINES = ['', '=', '-', '.'] diff --git a/pylint/reporters/ureports/__init__.py b/pylint/reporters/ureports/__init__.py new file mode 100644 index 0000000..0da4051 --- /dev/null +++ b/pylint/reporters/ureports/__init__.py @@ -0,0 +1,124 @@ +# copyright 2003-2015 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr +# +# This file is part of pylint. +# +# pylint is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 2.1 of the License, or (at your option) any +# later version. +# +# pylint is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License along +# with pylint. If not, see . +"""Universal report objects and some formatting drivers. + +A way to create simple reports using python objects, primarily designed to be +formatted as text and html. +""" +import os +import sys + +import six + + +# pylint: disable=method-hidden; Weird API in compute_content. + +class BaseWriter(object): + """base class for ureport writers""" + + def format(self, layout, stream=None, encoding=None): + """format and write the given layout into the stream object + + unicode policy: unicode strings may be found in the layout; + try to call stream.write with it, but give it back encoded using + the given encoding if it fails + """ + if stream is None: + stream = sys.stdout + if not encoding: + encoding = getattr(stream, 'encoding', 'UTF-8') + self.encoding = encoding or 'UTF-8' + self.__compute_funcs = [] + self.out = stream + self.begin_format() + layout.accept(self) + self.end_format() + + def format_children(self, layout): + """recurse on the layout children and call their accept method + (see the Visitor pattern) + """ + for child in getattr(layout, 'children', ()): + child.accept(self) + + def writeln(self, string=u''): + """write a line in the output buffer""" + self.write(string + os.linesep) + + def write(self, string): + """write a string in the output buffer""" + try: + self.out.write(string) + except UnicodeEncodeError: + self.out.write(string.encode(self.encoding)) + + def begin_format(self): + """begin to format a layout""" + self.section = 0 + + def end_format(self): + """finished to format a layout""" + + def get_table_content(self, table): + """trick to get table content without actually writing it + + return an aligned list of lists containing table cells values as string + """ + result = [[]] + cols = table.cols + for cell in self.compute_content(table): + if cols == 0: + result.append([]) + cols = table.cols + cols -= 1 + result[-1].append(cell) + # fill missing cells + while len(result[-1]) < cols: + result[-1].append(u'') + return result + + def compute_content(self, layout): + """trick to compute the formatting of children layout before actually + writing it + + return an iterator on strings (one for each child element) + """ + # use cells ! + def write(data): + try: + stream.write(data) + except UnicodeEncodeError: + stream.write(data.encode(self.encoding)) + def writeln(data=u''): + try: + stream.write(data + os.linesep) + except UnicodeEncodeError: + stream.write(data.encode(self.encoding) + os.linesep) + self.write = write + self.writeln = writeln + self.__compute_funcs.append((write, writeln)) + for child in layout.children: + stream = six.StringIO() + child.accept(self) + yield stream.getvalue() + self.__compute_funcs.pop() + try: + self.write, self.writeln = self.__compute_funcs[-1] + except IndexError: + del self.write + del self.writeln diff --git a/pylint/reporters/ureports/html_writer.py b/pylint/reporters/ureports/html_writer.py new file mode 100644 index 0000000..005ac62 --- /dev/null +++ b/pylint/reporters/ureports/html_writer.py @@ -0,0 +1,127 @@ +# copyright 2003-2015 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr +# +# This file is part of pylint. +# +# pylint is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 2.1 of the License, or (at your option) any +# later version. +# +# pylint is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License along +# with pylint. If not, see . +"""HTML formatting drivers for ureports""" + +from pylint.reporters.ureports import BaseWriter + + +class HTMLWriter(BaseWriter): + """format layouts as HTML""" + + def __init__(self, snippet=None): + super(HTMLWriter, self).__init__() + self.snippet = snippet + + @staticmethod + def handle_attrs(layout): + """get an attribute string from layout member attributes""" + attrs = u'' + klass = getattr(layout, 'klass', None) + if klass: + attrs += u' class="%s"' % klass + nid = getattr(layout, 'id', None) + if nid: + attrs += u' id="%s"' % nid + return attrs + + def begin_format(self): + """begin to format a layout""" + super(HTMLWriter, self).begin_format() + if self.snippet is None: + self.writeln(u'') + self.writeln(u'') + + def end_format(self): + """finished to format a layout""" + if self.snippet is None: + self.writeln(u'') + self.writeln(u'') + + def visit_section(self, layout): + """display a section as html, using div + h[section level]""" + self.section += 1 + self.writeln(u'' % self.handle_attrs(layout)) + self.format_children(layout) + self.writeln(u'') + self.section -= 1 + + def visit_title(self, layout): + """display a title using """ + self.write(u'' % (self.section, self.handle_attrs(layout))) + self.format_children(layout) + self.writeln(u'' % self.section) + + def visit_table(self, layout): + """display a table as html""" + self.writeln(u'' % self.handle_attrs(layout)) + table_content = self.get_table_content(layout) + for i, row in enumerate(table_content): + if i == 0 and layout.rheaders: + self.writeln(u'') + elif i+1 == len(table_content) and layout.rrheaders: + self.writeln(u'') + else: + self.writeln(u'' % (i%2 and 'even' or 'odd')) + for j, cell in enumerate(row): + cell = cell or u' ' + if (layout.rheaders and i == 0) or \ + (layout.cheaders and j == 0) or \ + (layout.rrheaders and i+1 == len(table_content)) or \ + (layout.rcheaders and j+1 == len(row)): + self.writeln(u'%s' % cell) + else: + self.writeln(u'%s' % cell) + self.writeln(u'') + self.writeln(u'') + + def visit_list(self, layout): + """display a list as html""" + self.writeln(u'' % self.handle_attrs(layout)) + for row in list(self.compute_content(layout)): + self.writeln(u'
  • %s
  • ' % row) + self.writeln(u'') + + def visit_paragraph(self, layout): + """display links (using

    )""" + self.write(u'

    ') + self.format_children(layout) + self.write(u'

    ') + + def visit_span(self, layout): + """display links (using

    )""" + self.write(u'' % self.handle_attrs(layout)) + self.format_children(layout) + self.write(u'') + + def visit_link(self, layout): + """display links (using )""" + self.write(u' %s' % (layout.url, + self.handle_attrs(layout), + layout.label)) + def visit_verbatimtext(self, layout): + """display verbatim text (using

    )"""
    +        self.write(u'
    ')
    +        self.write(layout.data.replace(u'&', u'&').replace(u'<', u'<'))
    +        self.write(u'
    ') + + def visit_text(self, layout): + """add some text""" + data = layout.data + if layout.escaped: + data = data.replace(u'&', u'&').replace(u'<', u'<') + self.write(data) diff --git a/pylint/reporters/ureports/nodes.py b/pylint/reporters/ureports/nodes.py new file mode 100644 index 0000000..01cbcb7 --- /dev/null +++ b/pylint/reporters/ureports/nodes.py @@ -0,0 +1,183 @@ +# copyright 2003-2015 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr +# +# This file is part of pylint. +# +# pylint is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 2.1 of the License, or (at your option) any +# later version. +# +# pylint is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License along +# with pylint. If not, see . +"""Micro reports objects. + +A micro report is a tree of layout and content objects. +""" + +from six import string_types + +from pylint.reporters.ureports.tree import VNode + + +class BaseComponent(VNode): + """base report component + + attributes + * id : the component's optional id + * klass : the component's optional klass + """ + def __init__(self, id=None, klass=None): + super(BaseComponent, self).__init__(id) + self.klass = klass + + +class BaseLayout(BaseComponent): + """base container node + + attributes + * BaseComponent attributes + * children : components in this table (i.e. the table's cells) + """ + def __init__(self, children=(), **kwargs): + super(BaseLayout, self).__init__(**kwargs) + for child in children: + if isinstance(child, BaseComponent): + self.append(child) + else: + self.add_text(child) + + def append(self, child): + """overridden to detect problems easily""" + assert child not in self.parents() + VNode.append(self, child) + + def parents(self): + """return the ancestor nodes""" + assert self.parent is not self + if self.parent is None: + return [] + return [self.parent] + self.parent.parents() + + def add_text(self, text): + """shortcut to add text data""" + self.children.append(Text(text)) + + +# non container nodes ######################################################### + +class Text(BaseComponent): + """a text portion + + attributes : + * BaseComponent attributes + * data : the text value as an encoded or unicode string + """ + def __init__(self, data, escaped=True, **kwargs): + super(Text, self).__init__(**kwargs) + #if isinstance(data, unicode): + # data = data.encode('ascii') + assert isinstance(data, string_types), data.__class__ + self.escaped = escaped + self.data = data + + +class VerbatimText(Text): + """a verbatim text, display the raw data + + attributes : + * BaseComponent attributes + * data : the text value as an encoded or unicode string + """ + + +class Link(BaseComponent): + """a labelled link + + attributes : + * BaseComponent attributes + * url : the link's target (REQUIRED) + * label : the link's label as a string (use the url by default) + """ + def __init__(self, url, label=None, **kwargs): + super(Link, self).__init__(**kwargs) + assert url + self.url = url + self.label = label or url + + +# container nodes ############################################################# + +class Section(BaseLayout): + """a section + + attributes : + * BaseLayout attributes + + a title may also be given to the constructor, it'll be added + as a first element + a description may also be given to the constructor, it'll be added + as a first paragraph + """ + def __init__(self, title=None, description=None, **kwargs): + super(Section, self).__init__(**kwargs) + if description: + self.insert(0, Paragraph([Text(description)])) + if title: + self.insert(0, Title(children=(title,))) + + +class Title(BaseLayout): + """a title + + attributes : + * BaseLayout attributes + + A title must not contains a section nor a paragraph! + """ + + +class Paragraph(BaseLayout): + """a simple text paragraph + + attributes : + * BaseLayout attributes + + A paragraph must not contains a section ! + """ + + +class Table(BaseLayout): + """some tabular data + + attributes : + * BaseLayout attributes + * cols : the number of columns of the table (REQUIRED) + * rheaders : the first row's elements are table's header + * cheaders : the first col's elements are table's header + * title : the table's optional title + """ + def __init__(self, cols, title=None, + rheaders=0, cheaders=0, rrheaders=0, rcheaders=0, + **kwargs): + super(Table, self).__init__(**kwargs) + assert isinstance(cols, int) + self.cols = cols + self.title = title + self.rheaders = rheaders + self.cheaders = cheaders + self.rrheaders = rrheaders + self.rcheaders = rcheaders + + +class List(BaseLayout): + """some list data + + attributes : + * BaseLayout attributes + """ diff --git a/pylint/reporters/ureports/text_writer.py b/pylint/reporters/ureports/text_writer.py new file mode 100644 index 0000000..545f999 --- /dev/null +++ b/pylint/reporters/ureports/text_writer.py @@ -0,0 +1,143 @@ +# copyright 2003-2015 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr +# +# This file is part of logilab-common. +# +# pylint is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 2.1 of the License, or (at your option) any +# later version. +# +# pylint is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License along +# with pylint. If not, see . +"""Text formatting drivers for ureports""" + +from __future__ import print_function + +import os + +from six.moves import range + +from pylint.reporters.ureports import BaseWriter + + +TITLE_UNDERLINES = [u'', u'=', u'-', u'`', u'.', u'~', u'^'] +BULLETS = [u'*', u'-'] + +class TextWriter(BaseWriter): + """format layouts as text + (ReStructured inspiration but not totally handled yet) + """ + def begin_format(self): + super(TextWriter, self).begin_format() + self.list_level = 0 + self.pending_urls = [] + + def visit_section(self, layout): + """display a section as text + """ + self.section += 1 + self.writeln() + self.format_children(layout) + if self.pending_urls: + self.writeln() + for label, url in self.pending_urls: + self.writeln(u'.. _`%s`: %s' % (label, url)) + self.pending_urls = [] + self.section -= 1 + self.writeln() + + def visit_title(self, layout): + title = u''.join(list(self.compute_content(layout))) + self.writeln(title) + try: + self.writeln(TITLE_UNDERLINES[self.section] * len(title)) + except IndexError: + print("FIXME TITLE TOO DEEP. TURNING TITLE INTO TEXT") + + def visit_paragraph(self, layout): + """enter a paragraph""" + self.format_children(layout) + self.writeln() + + def visit_span(self, layout): + """enter a span""" + self.format_children(layout) + + def visit_table(self, layout): + """display a table as text""" + table_content = self.get_table_content(layout) + # get columns width + cols_width = [0]*len(table_content[0]) + for row in table_content: + for index in range(len(row)): + col = row[index] + cols_width[index] = max(cols_width[index], len(col)) + if layout.klass == 'field': + self.field_table(layout, table_content, cols_width) + else: + self.default_table(layout, table_content, cols_width) + self.writeln() + + def default_table(self, layout, table_content, cols_width): + """format a table""" + cols_width = [size+1 for size in cols_width] + format_strings = u' '.join([u'%%-%ss'] * len(cols_width)) + format_strings = format_strings % tuple(cols_width) + format_strings = format_strings.split(' ') + table_linesep = u'\n+' + u'+'.join([u'-'*w for w in cols_width]) + u'+\n' + headsep = u'\n+' + u'+'.join([u'='*w for w in cols_width]) + u'+\n' + # FIXME: layout.cheaders + self.write(table_linesep) + for i in range(len(table_content)): + self.write(u'|') + line = table_content[i] + for j in range(len(line)): + self.write(format_strings[j] % line[j]) + self.write(u'|') + if i == 0 and layout.rheaders: + self.write(headsep) + else: + self.write(table_linesep) + + def field_table(self, layout, table_content, cols_width): + """special case for field table""" + assert layout.cols == 2 + format_string = u'%s%%-%ss: %%s' % (os.linesep, cols_width[0]) + for field, value in table_content: + self.write(format_string % (field, value)) + + def visit_list(self, layout): + """display a list layout as text""" + bullet = BULLETS[self.list_level % len(BULLETS)] + indent = ' ' * self.list_level + self.list_level += 1 + for child in layout.children: + self.write(u'%s%s%s ' % (os.linesep, indent, bullet)) + child.accept(self) + self.list_level -= 1 + + def visit_link(self, layout): + """add a hyperlink""" + if layout.label != layout.url: + self.write(u'`%s`_' % layout.label) + self.pending_urls.append((layout.label, layout.url)) + else: + self.write(layout.url) + + def visit_verbatimtext(self, layout): + """display a verbatim layout as text (so difficult ;) + """ + self.writeln(u'::\n') + for line in layout.data.splitlines(): + self.writeln(u' ' + line) + self.writeln() + + def visit_text(self, layout): + """add some text""" + self.write(u'%s' % layout.data) diff --git a/pylint/reporters/ureports/tree.py b/pylint/reporters/ureports/tree.py new file mode 100644 index 0000000..99965f2 --- /dev/null +++ b/pylint/reporters/ureports/tree.py @@ -0,0 +1,235 @@ +# copyright 2003-2015 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr +# +# This file is part of pylint. +# +# logilab-common is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 2.1 of the License, or (at your option) any +# later version. +# +# pylint is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License along +# with pylint. If not, see . + + +class NodeNotFound(Exception): + """raised when a node has not been found""" + +EX_SIBLING_NOT_FOUND = "No such sibling as '%s'" +EX_CHILD_NOT_FOUND = "No such child as '%s'" +EX_NODE_NOT_FOUND = "No such node as '%s'" + + +# Base node ################################################################### + +class Node(object): + """a basic tree node, characterized by an id""" + + def __init__(self, nid=None): + self.id = nid + # navigation + self.parent = None + self.children = [] + + def __iter__(self): + return iter(self.children) + + def __str__(self, indent=0): + s = ['%s%s %s' % (' '*indent, self.__class__.__name__, self.id)] + indent += 2 + for child in self.children: + try: + s.append(child.__str__(indent)) + except TypeError: + s.append(child.__str__()) + return '\n'.join(s) + + def is_leaf(self): + return not self.children + + def append(self, child): + """add a node to children""" + self.children.append(child) + child.parent = self + + def remove(self, child): + """remove a child node""" + self.children.remove(child) + child.parent = None + + def insert(self, index, child): + """insert a child node""" + self.children.insert(index, child) + child.parent = self + + def replace(self, old_child, new_child): + """replace a child node with another""" + i = self.children.index(old_child) + self.children.pop(i) + self.children.insert(i, new_child) + new_child.parent = self + + def get_sibling(self, nid): + """return the sibling node that has given id""" + try: + return self.parent.get_child_by_id(nid) + except NodeNotFound: + raise NodeNotFound(EX_SIBLING_NOT_FOUND % nid) + + def next_sibling(self): + """ + return the next sibling for this node if any + """ + parent = self.parent + if parent is None: + # root node has no sibling + return None + index = parent.children.index(self) + try: + return parent.children[index+1] + except IndexError: + return None + + def previous_sibling(self): + """ + return the previous sibling for this node if any + """ + parent = self.parent + if parent is None: + # root node has no sibling + return None + index = parent.children.index(self) + if index > 0: + return parent.children[index-1] + return None + + def get_node_by_id(self, nid): + """ + return node in whole hierarchy that has given id + """ + root = self.root() + try: + return root.get_child_by_id(nid, 1) + except NodeNotFound: + raise NodeNotFound(EX_NODE_NOT_FOUND % nid) + + def get_child_by_id(self, nid, recurse=None): + """ + return child of given id + """ + if self.id == nid: + return self + for c in self.children: + if recurse: + try: + return c.get_child_by_id(nid, 1) + except NodeNotFound: + continue + if c.id == nid: + return c + raise NodeNotFound(EX_CHILD_NOT_FOUND % nid) + + def get_child_by_path(self, path): + """ + return child of given path (path is a list of ids) + """ + if len(path) > 0 and path[0] == self.id: + if len(path) == 1: + return self + else: + for c in self.children: + try: + return c.get_child_by_path(path[1:]) + except NodeNotFound: + pass + raise NodeNotFound(EX_CHILD_NOT_FOUND % path) + + def depth(self): + """ + return depth of this node in the tree + """ + if self.parent is not None: + return 1 + self.parent.depth() + else: + return 0 + + def depth_down(self): + """ + return depth of the tree from this node + """ + if self.children: + return 1 + max([c.depth_down() for c in self.children]) + return 1 + + def width(self): + """ + return the width of the tree from this node + """ + return len(self.leaves()) + + def root(self): + """ + return the root node of the tree + """ + if self.parent is not None: + return self.parent.root() + return self + + def leaves(self): + """ + return a list with all the leaves nodes descendant from this node + """ + leaves = [] + if self.children: + for child in self.children: + leaves += child.leaves() + return leaves + else: + return [self] + + def flatten(self, _list=None): + """ + return a list with all the nodes descendant from this node + """ + if _list is None: + _list = [] + _list.append(self) + for c in self.children: + c.flatten(_list) + return _list + + def lineage(self): + """ + return list of parents up to root node + """ + lst = [self] + if self.parent is not None: + lst.extend(self.parent.lineage()) + return lst + + +class VNode(Node): + + def get_visit_name(self): + """ + return the visit name for the mixed class. When calling 'accept', the + method <'visit_' + name returned by this method> will be called on the + visitor + """ + try: + return self.TYPE.replace('-', '_') + except Exception: + return self.__class__.__name__.lower() + + def accept(self, visitor, *args, **kwargs): + func = getattr(visitor, 'visit_%s' % self.get_visit_name()) + return func(self, *args, **kwargs) + + def leave(self, visitor, *args, **kwargs): + func = getattr(visitor, 'leave_%s' % self.get_visit_name()) + return func(self, *args, **kwargs) diff --git a/pylint/test/unittest_reporting.py b/pylint/test/unittest_reporting.py index ee9c326..11c5087 100644 --- a/pylint/test/unittest_reporting.py +++ b/pylint/test/unittest_reporting.py @@ -17,7 +17,6 @@ import unittest import warnings import six -from logilab.common.ureports import Section from pylint import __pkginfo__ from pylint.lint import PyLinter @@ -25,6 +24,8 @@ from pylint import checkers from pylint.reporters import BaseReporter from pylint.reporters.text import TextReporter, ParseableTextReporter from pylint.reporters.html import HTMLReporter +from pylint.reporters.ureports.nodes import Section + HERE = abspath(dirname(__file__)) INPUTDIR = join(HERE, 'input') diff --git a/pylint/utils.py b/pylint/utils.py index 356868b..ea844b2 100644 --- a/pylint/utils.py +++ b/pylint/utils.py @@ -30,13 +30,12 @@ from os.path import dirname, basename, splitext, exists, isdir, join, normpath import six from six.moves import zip # pylint: disable=redefined-builtin -from logilab.common.ureports import Section - from astroid import nodes, Module from astroid.modutils import modpath_from_file, get_module_files, \ file_from_modpath, load_module_from_file from pylint.interfaces import IRawChecker, ITokenChecker, UNDEFINED, implements +from pylint.reporters.ureports.nodes import Section class UnknownMessage(Exception): diff --git a/tox.ini b/tox.ini index 655822a..859e97f 100644 --- a/tox.ini +++ b/tox.ini @@ -5,7 +5,6 @@ envlist = py27, py33, pylint [testenv:pylint] deps = - logilab-common hg+https://bitbucket.org/logilab/astroid/ six hg+https://bitbucket.org/logilab/pylint @@ -13,7 +12,6 @@ commands = pylint -rn --rcfile={toxinidir}/pylintrc {envsitepackagesdir}/pylint [testenv] deps = - logilab-common hg+https://bitbucket.org/logilab/astroid/ six commands = python -Wi -m unittest discover -s {envsitepackagesdir}/pylint/test/ -p {posargs:*test_*}.py -- cgit v1.2.1