__init__.py 7.86 KB
Newer Older
Nicolas Chauvat's avatar
Nicolas Chauvat committed
1
# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
2
3
4
5
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of logilab-common.
#
6
7
8
9
# 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.
10
11
12
13
14
15
#
# logilab-common 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.
#
16
17
# You should have received a copy of the GNU Lesser General Public License
# along with logilab-common.  If not, see <http://www.gnu.org/licenses/>.
18
"""Universal report objects and some formatting drivers.
root's avatar
root committed
19

20
A way to create simple reports using python objects, primarily designed to be
21
22
23
formatted as text and html.
"""
__docformat__ = "restructuredtext en"
root's avatar
root committed
24
25
26

import sys

27
from typing import Any, Optional, Union, List as ListType, Generator, Tuple, Callable, TextIO
Laurent Peuch's avatar
Laurent Peuch committed
28

29
from logilab.common.compat import StringIO
Sylvain's avatar
Sylvain committed
30
from logilab.common.textutils import linesep
31
from logilab.common.tree import VNode
Laurent Peuch's avatar
Laurent Peuch committed
32
from logilab.common.ureports.nodes import Table, Section, Link, Paragraph, Title, Text
root's avatar
root committed
33

34
35
from logilab.common.ureports.nodes import VerbatimText, Image, Span, List  # noqa

Sylvain's avatar
Sylvain committed
36

root's avatar
root committed
37
38
39
40
41
42
43
44
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
45

46

root's avatar
root committed
47
48
49
50
51
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):
Laurent Peuch's avatar
Laurent Peuch committed
52
            return " ".join([node.data for node in get_nodes(child, Text)])
53

54

root's avatar
root committed
55
56
57
58
def build_summary(layout, level=1):
    """make a summary for the report, including X level"""
    assert level > 0
    level -= 1
59
    summary = ListType(klass="summary")
root's avatar
root committed
60
61
62
63
64
65
66
    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:
Laurent Peuch's avatar
Laurent Peuch committed
67
68
            child.id = label.replace(" ", "-")
        node = Link("#" + child.id, label=label or child.id)
root's avatar
root committed
69
70
71
72
73
74
75
        # FIXME: Three following lines produce not very compliant
        # docbook: there are some useless <para><para>. 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)
Laurent Peuch's avatar
Laurent Peuch committed
76
77
78
    #         summary.append(node)
    #         if level and [n for n in child.children if isinstance(n, Section)]:
    #             summary.append(build_summary(child, level))
root's avatar
root committed
79
80
81
82
83
    return summary


class BaseWriter(object):
    """base class for ureport writers"""
84

Laurent Peuch's avatar
Laurent Peuch committed
85
86
87
88
89
90
    def format(
        self,
        layout: Any,
        stream: Optional[Union[StringIO, TextIO]] = None,
        encoding: Optional[Any] = None,
    ) -> None:
root's avatar
root committed
91
92
93
94
95
96
97
98
99
        """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:
Laurent Peuch's avatar
Laurent Peuch committed
100
101
            encoding = getattr(stream, "encoding", "UTF-8")
        self.encoding = encoding or "UTF-8"
102
        self.__compute_funcs: ListType[Tuple[Callable[[str], Any], Callable[[str], Any]]] = []
root's avatar
root committed
103
104
105
106
        self.out = stream
        self.begin_format(layout)
        layout.accept(self)
        self.end_format(layout)
107

Laurent Peuch's avatar
Laurent Peuch committed
108
    def format_children(self, layout: Union["Paragraph", "Section", "Title"]) -> None:
root's avatar
root committed
109
110
111
        """recurse on the layout children and call their accept method
        (see the Visitor pattern)
        """
Laurent Peuch's avatar
Laurent Peuch committed
112
        for child in getattr(layout, "children", ()):
root's avatar
root committed
113
114
            child.accept(self)

Laurent Peuch's avatar
Laurent Peuch committed
115
    def writeln(self, string: str = "") -> None:
root's avatar
root committed
116
117
118
        """write a line in the output buffer"""
        self.write(string + linesep)

119
    def write(self, string: str) -> None:
root's avatar
root committed
120
121
122
123
        """write a string in the output buffer"""
        try:
            self.out.write(string)
        except UnicodeEncodeError:
124
125
126
            # mypy: Argument 1 to "write" of "IO" has incompatible type "bytes"; expected "str"
            # probably a python3 port issue?
            self.out.write(string.encode(self.encoding))  # type: ignore
root's avatar
root committed
127

128
    def begin_format(self, layout: Any) -> None:
root's avatar
root committed
129
130
        """begin to format a layout"""
        self.section = 0
131

132
    def end_format(self, layout: Any) -> None:
root's avatar
root committed
133
134
        """finished to format a layout"""

135
    def get_table_content(self, table: Table) -> ListType[ListType[str]]:
root's avatar
root committed
136
137
138
139
        """trick to get table content without actually writing it

        return an aligned list of lists containing table cells values as string
        """
140
        result: ListType[ListType[str]] = [[]]
141
142
143
144
        # mypy: "Table" has no attribute "cols"
        # dynamic attribute
        cols = table.cols  # type: ignore

root's avatar
root committed
145
146
147
        for cell in self.compute_content(table):
            if cols == 0:
                result.append([])
148
149
150
151
                # mypy: "Table" has no attribute "cols"
                # dynamic attribute
                cols = table.cols  # type: ignore

root's avatar
root committed
152
153
            cols -= 1
            result[-1].append(cell)
154

root's avatar
root committed
155
156
        # fill missing cells
        while len(result[-1]) < cols:
Laurent Peuch's avatar
Laurent Peuch committed
157
            result[-1].append("")
158

root's avatar
root committed
159
160
        return result

161
    def compute_content(self, layout: VNode) -> Generator[str, Any, None]:
root's avatar
root committed
162
163
164
165
166
167
        """trick to compute the formatting of children layout before actually
        writing it

        return an iterator on strings (one for each child element)
        """
        # use cells !
168
        def write(data: str) -> None:
root's avatar
root committed
169
170
171
            try:
                stream.write(data)
            except UnicodeEncodeError:
172
173
174
175
176
                # mypy: Argument 1 to "write" of "TextIOWrapper" has incompatible type "bytes";
                # mypy: expected "str"
                # error from porting to python3?
                stream.write(data.encode(self.encoding))  # type: ignore

Laurent Peuch's avatar
Laurent Peuch committed
177
        def writeln(data: str = "") -> None:
root's avatar
root committed
178
            try:
Laurent Peuch's avatar
Laurent Peuch committed
179
                stream.write(data + linesep)
root's avatar
root committed
180
            except UnicodeEncodeError:
181
182
                # mypy: Unsupported operand types for + ("bytes" and "str")
                # error from porting to python3?
Laurent Peuch's avatar
Laurent Peuch committed
183
                stream.write(data.encode(self.encoding) + linesep)  # type: ignore
184
185
186
187
188
189
190
191

        # mypy: Cannot assign to a method
        # this really looks like black dirty magic since self.write is reused elsewhere in the code
        # especially since self.write and self.writeln are conditionally
        # deleted at the end of this function
        self.write = write  # type: ignore
        self.writeln = writeln  # type: ignore

root's avatar
root committed
192
        self.__compute_funcs.append((write, writeln))
193

194
        # mypy: Item "Table" of "Union[ListType[Any], Table, Title]" has no attribute "children"
195
196
        # dynamic attribute?
        for child in layout.children:  # type: ignore
197
            stream = StringIO()
198

root's avatar
root committed
199
            child.accept(self)
200

root's avatar
root committed
201
            yield stream.getvalue()
202

root's avatar
root committed
203
        self.__compute_funcs.pop()
204

root's avatar
root committed
205
        try:
206
207
208
            # mypy: Cannot assign to a method
            # even more black dirty magic
            self.write, self.writeln = self.__compute_funcs[-1]  # type: ignore
root's avatar
root committed
209
210
211
212
        except IndexError:
            del self.write
            del self.writeln

Laurent Peuch's avatar
Laurent Peuch committed
213

Laurent Peuch's avatar
Laurent Peuch committed
214
215
from logilab.common.ureports.text_writer import TextWriter  # noqa
from logilab.common.ureports.html_writer import HTMLWriter  # noqa