From 8b1e1c104bdff504b3e775b450432e6462b8d09b Mon Sep 17 00:00:00 2001 From: root Date: Wed, 26 Apr 2006 10:48:09 +0000 Subject: forget the past. forget the past. --- ureports/__init__.py | 173 +++++++++++++++++++++++++++++++++++++++ ureports/docbook_writer.py | 138 +++++++++++++++++++++++++++++++ ureports/html_writer.py | 131 +++++++++++++++++++++++++++++ ureports/nodes.py | 200 +++++++++++++++++++++++++++++++++++++++++++++ ureports/text_writer.py | 141 ++++++++++++++++++++++++++++++++ 5 files changed, 783 insertions(+) create mode 100644 ureports/__init__.py create mode 100644 ureports/docbook_writer.py create mode 100644 ureports/html_writer.py create mode 100644 ureports/nodes.py create mode 100644 ureports/text_writer.py (limited to 'ureports') diff --git a/ureports/__init__.py b/ureports/__init__.py new file mode 100644 index 0000000..b4c6c60 --- /dev/null +++ b/ureports/__init__.py @@ -0,0 +1,173 @@ +# Copyright (c) 2004-2005 LOGILAB S.A. (Paris, FRANCE). +# http://www.logilab.fr/ -- mailto:contact@logilab.fr +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program 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 General Public License for more details +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., +# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +""" Universal report objects and some formatting drivers + +a way to create simple reports using python objects, primarly designed to be +formatted as text and html +""" + +from __future__ import generators + +__revision__ = "$Id: __init__.py,v 1.8 2005-07-02 13:22:30 syt Exp $" + +import sys +from os import linesep +from cStringIO import StringIO +from StringIO import StringIO as UStringIO + + +def get_nodes(node, klass): + """return an iterator on all children node of the given klass""" + for child in node.children: + if isinstance(child, klass): + yield child + # recurse (FIXME: recursion controled by an option) + for grandchild in get_nodes(child, klass): + yield grandchild + +def layout_title(layout): + """try to return the layout's title as string, return None if not found + """ + for child in layout.children: + if isinstance(child, Title): + return ' '.join([node.data for node in get_nodes(child, Text)]) + +def build_summary(layout, level=1): + """make a summary for the report, including X level""" + assert level > 0 + level -= 1 + summary = List(klass='summary') + for child in layout.children: + if not isinstance(child, Section): + continue + label = layout_title(child) + if not label and not child.id: + continue + if not child.id: + child.id = label.replace(' ', '-') + node = Link('#'+child.id, label=label or child.id) + # FIXME: Three following lines produce not very compliant + # docbook: there are some useless . They might be + # replaced by the three commented lines but this then produces + # a bug in html display... + if level and [n for n in child.children if isinstance(n, Section)]: + node = Paragraph([node, build_summary(child, level)]) + summary.append(node) +# summary.append(node) +# if level and [n for n in child.children if isinstance(n, Section)]: +# summary.append(build_summary(child, level)) + return summary + + +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) + layout.accept(self) + self.end_format(layout) + + 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=''): + """write a line in the output buffer""" + self.write(string + 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, layout): + """begin to format a layout""" + self.section = 0 + + def end_format(self, layout): + """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('') + 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=''): + try: + stream.write(data+linesep) + except UnicodeEncodeError: + stream.write(data.encode(self.encoding)+linesep) + self.write = write + self.writeln = writeln + self.__compute_funcs.append((write, writeln)) + for child in layout.children: + stream = UStringIO() + 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 + + +from logilab.common.ureports.nodes import * +from logilab.common.ureports.text_writer import TextWriter +from logilab.common.ureports.html_writer import HTMLWriter diff --git a/ureports/docbook_writer.py b/ureports/docbook_writer.py new file mode 100644 index 0000000..5ce5760 --- /dev/null +++ b/ureports/docbook_writer.py @@ -0,0 +1,138 @@ +# Copyright (c) 2002-2004 LOGILAB S.A. (Paris, FRANCE). +# http://www.logilab.fr/ -- mailto:contact@logilab.fr +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program 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 General Public License for more details +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., +# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +"""HTML formatting drivers for ureports +""" + +__revision__ = "$Id: docbook_writer.py,v 1.4 2005-05-20 16:42:23 emb Exp $" + +from logilab.common.ureports import HTMLWriter + +class DocbookWriter(HTMLWriter): + """format layouts as HTML""" + + def begin_format(self, layout): + """begin to format a layout""" + super(HTMLWriter, self).begin_format(layout) + if self.snipet is None: + self.writeln('') + self.writeln(""" + +""") + + def end_format(self, layout): + """finished to format a layout""" + if self.snipet is None: + self.writeln('') + + def visit_section(self, layout): + """display a section (using (level 0) or
)""" + if self.section == 0: + tag = "chapter" + else: + tag = "section" + self.section += 1 + self.writeln(self._indent('<%s%s>' % (tag, self.handle_attrs(layout)))) + self.format_children(layout) + self.writeln(self._indent(''% tag)) + self.section -= 1 + + def visit_title(self, layout): + """display a title using """ + self.write(self._indent(' <title%s>' % self.handle_attrs(layout))) + self.format_children(layout) + self.writeln('') + + def visit_table(self, layout): + """display a table as html""" + self.writeln(self._indent(' %s' \ + % (self.handle_attrs(layout), layout.title))) + self.writeln(self._indent(' '% layout.cols)) + for i in range(layout.cols): + self.writeln(self._indent(' ' % i)) + + table_content = self.get_table_content(layout) + # write headers + if layout.cheaders: + self.writeln(self._indent(' ')) + self._write_row(table_content[0]) + self.writeln(self._indent(' ')) + table_content = table_content[1:] + elif layout.rcheaders: + self.writeln(self._indent(' ')) + self._write_row(table_content[-1]) + self.writeln(self._indent(' ')) + table_content = table_content[:-1] + # write body + self.writeln(self._indent(' ')) + for i in range(len(table_content)): + row = table_content[i] + self.writeln(self._indent(' ')) + for j in range(len(row)): + cell = row[j] or ' ' + self.writeln(self._indent(' %s' % cell)) + self.writeln(self._indent(' ')) + self.writeln(self._indent(' ')) + self.writeln(self._indent(' ')) + self.writeln(self._indent(' ')) + + def _write_row(self, row): + """write content of row (using )""" + self.writeln(' ') + for j in range(len(row)): + cell = row[j] or ' ' + self.writeln(' %s' % cell) + self.writeln(self._indent(' ')) + + def visit_list(self, layout): + """display a list (using )""" + self.writeln(self._indent(' ' % self.handle_attrs(layout))) + for row in list(self.compute_content(layout)): + self.writeln(' %s' % row) + self.writeln(self._indent(' ')) + + def visit_paragraph(self, layout): + """display links (using )""" + self.write(self._indent(' ')) + self.format_children(layout) + self.writeln('') + + def visit_span(self, layout): + """display links (using

)""" + #TODO: translate in docbook + self.write('' % self.handle_attrs(layout)) + self.format_children(layout) + self.write('') + + def visit_link(self, layout): + """display links (using )""" + self.write('%s' % (layout.url, + self.handle_attrs(layout), + layout.label)) + + def visit_verbatimtext(self, layout): + """display verbatim text (using )""" + self.writeln(self._indent(' ')) + self.write(layout.data.replace('&', '&').replace('<', '<')) + self.writeln(self._indent(' ')) + + def visit_text(self, layout): + """add some text""" + self.write(layout.data.replace('&', '&').replace('<', '<')) + + def _indent(self, string): + """correctly indent string according to section""" + return ' ' * 2*(self.section) + string diff --git a/ureports/html_writer.py b/ureports/html_writer.py new file mode 100644 index 0000000..33506d0 --- /dev/null +++ b/ureports/html_writer.py @@ -0,0 +1,131 @@ +# Copyright (c) 2004-2005 LOGILAB S.A. (Paris, FRANCE). +# http://www.logilab.fr/ -- mailto:contact@logilab.fr +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program 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 General Public License for more details +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., +# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +"""HTML formatting drivers for ureports +""" + +__revision__ = "$Id: html_writer.py,v 1.10 2006-03-08 09:47:29 katia Exp $" + +from cgi import escape + +from logilab.common.ureports import BaseWriter + + +class HTMLWriter(BaseWriter): + """format layouts as HTML""" + + def __init__(self, snipet=None): + super(HTMLWriter, self).__init__(self) + self.snipet = snipet + + def handle_attrs(self, layout): + """get an attribute string from layout member attributes""" + attrs = '' + klass = getattr(layout, 'klass', None) + if klass: + attrs += ' class="%s"' % klass + nid = getattr(layout, 'id', None) + if nid: + attrs += ' id="%s"' % nid + return attrs + + def begin_format(self, layout): + """begin to format a layout""" + super(HTMLWriter, self).begin_format(layout) + if self.snipet is None: + self.writeln('') + self.writeln('') + + def end_format(self, layout): + """finished to format a layout""" + if self.snipet is None: + self.writeln('') + self.writeln('') + + + def visit_section(self, layout): + """display a section as html, using div + h[section level]""" + self.section += 1 + self.writeln('' % self.handle_attrs(layout)) + self.format_children(layout) + self.writeln('') + self.section -= 1 + + def visit_title(self, layout): + """display a title using """ + self.write('' % (self.section, self.handle_attrs(layout))) + self.format_children(layout) + self.writeln('' % self.section) + + def visit_table(self, layout): + """display a table as html""" + self.writeln('' % self.handle_attrs(layout)) + table_content = self.get_table_content(layout) + for i in range(len(table_content)): + row = table_content[i] + if i == 0 and layout.rheaders: + self.writeln('') + elif i+1 == len(table_content) and layout.rrheaders: + self.writeln('') + else: + self.writeln('' % (i%2 and 'even' or 'odd')) + for j in range(len(row)): + cell = row[j] or ' ' + 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('%s' % cell) + else: + self.writeln('%s' % cell) + self.writeln('') + self.writeln('') + + def visit_list(self, layout): + """display a list as html""" + self.writeln('' % self.handle_attrs(layout)) + for row in list(self.compute_content(layout)): + self.writeln('

  • %s
  • ' % row) + self.writeln('') + + def visit_paragraph(self, layout): + """display links (using

    )""" + self.write('

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

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

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

    )"""
    +        self.write('
    ')
    +        self.write(layout.data.replace('&', '&').replace('<', '<'))
    +        self.write('
    ') + + def visit_text(self, layout): + """add some text""" + data = layout.data + if layout.escaped: + data = data.replace('&', '&').replace('<', '<') + self.write(data) diff --git a/ureports/nodes.py b/ureports/nodes.py new file mode 100644 index 0000000..d0829ae --- /dev/null +++ b/ureports/nodes.py @@ -0,0 +1,200 @@ +# Copyright (c) 2004-2005 LOGILAB S.A. (Paris, FRANCE). +# http://www.logilab.fr/ -- mailto:contact@logilab.fr +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program 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 General Public License for more details +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., +# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +"""Universal reports objects + +A Universal report is a tree of layout and content objects +""" + +__revision__ = "$Id: nodes.py,v 1.11 2006-03-08 09:47:38 katia Exp $" + +from logilab.common.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): + VNode.__init__(self, 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): + """overriden 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, (str, unicode)), 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 + + +class Image(BaseComponent): + """an embeded or a single image + + attributes : + * BaseComponent attributes + * filename : the image's filename (REQUIRED) + * stream : the stream object containing the image data (REQUIRED) + * title : the image's optional title + """ + def __init__(self, filename, stream, title=None, **kwargs): + super(Link, self).__init__(**kwargs) + assert filename + assert stream + self.filename = filename + self.stream = stream + self.title = title + + +# 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 Span(BaseLayout): + """a title + + attributes : + * BaseLayout attributes + + A span should only contains Text and Link nodes (in-line elements) + """ + +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/ureports/text_writer.py b/ureports/text_writer.py new file mode 100644 index 0000000..f0a9617 --- /dev/null +++ b/ureports/text_writer.py @@ -0,0 +1,141 @@ +# Copyright (c) 2004-2005 LOGILAB S.A. (Paris, FRANCE). +# http://www.logilab.fr/ -- mailto:contact@logilab.fr +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program 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 General Public License for more details +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., +# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +"""Text formatting drivers for ureports""" + +__revision__ = "$Id: text_writer.py,v 1.9 2005-11-22 13:13:13 syt Exp $" + +from os import linesep + +from logilab.common.ureports import BaseWriter + +TITLE_UNDERLINES = ['', '=', '-', '`', '.', '~', '^'] +BULLETS = ['*', '-'] + +class TextWriter(BaseWriter): + """format layouts as text + (ReStructured inspiration but not totally handled yet) + """ + def begin_format(self, layout): + super(TextWriter, self).begin_format(layout) + 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('.. _`%s`: %s' % (label, url)) + self.pending_urls = [] + self.section -= 1 + self.writeln() + + def visit_title(self, layout): + title = ''.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 = ' '.join(['%%-%ss'] * len(cols_width)) + format_strings = format_strings % tuple(cols_width) + format_strings = format_strings.split(' ') + table_linesep = '\n+' + '+'.join(['-'*w for w in cols_width]) + '+\n' + headsep = '\n+' + '+'.join(['='*w for w in cols_width]) + '+\n' + # FIXME: layout.cheaders + self.write(table_linesep) + for i in range(len(table_content)): + self.write('|') + line = table_content[i] + for j in range(len(line)): + self.write(format_strings[j] % line[j]) + self.write('|') + 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 = '%s%%-%ss: %%s' % (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('%s%s%s ' % (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('`%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('::\n') + for line in layout.data.splitlines(): + self.writeln(' ' + line) + self.writeln() + + def visit_text(self, layout): + """add some text""" + self.write(layout.data) + + -- cgit v1.2.1