~ubuntu-branches/ubuntu/jaunty/python-docutils/jaunty

« back to all changes in this revision

Viewing changes to docutils/parsers/rst/directives/tables.py

  • Committer: Bazaar Package Importer
  • Author(s): Simon McVittie
  • Date: 2008-07-24 10:39:53 UTC
  • mfrom: (1.1.4 upstream) (3.1.7 intrepid)
  • Revision ID: james.westby@ubuntu.com-20080724103953-8gh4uezg17g9ysgy
Tags: 0.5-2
* Upload docutils 0.5 to unstable
* Update rst.el to upstream Subversion r5596, which apparently fixes
  all its performance problems (17_speed_up_rst_el.dpatch, closes: #474941)

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Authors: David Goodger, David Priest
2
 
# Contact: goodger@python.org
3
 
# Revision: $Revision: 3900 $
4
 
# Date: $Date: 2005-09-24 17:11:20 +0200 (Sat, 24 Sep 2005) $
 
1
# $Id: tables.py 4667 2006-07-12 21:40:56Z wiemann $
 
2
# Authors: David Goodger <goodger@python.org>; David Priest
5
3
# Copyright: This module has been placed in the public domain.
6
4
 
7
5
"""
15
13
import os.path
16
14
from docutils import io, nodes, statemachine, utils
17
15
from docutils.utils import SystemMessagePropagation
 
16
from docutils.parsers.rst import Directive
18
17
from docutils.parsers.rst import directives
19
18
 
20
19
try:
22
21
except ImportError:
23
22
    csv = None
24
23
 
25
 
try:
26
 
    import urllib2
27
 
except ImportError:
28
 
    urllib2 = None
29
 
 
30
 
try:
31
 
    True
32
 
except NameError:                       # Python 2.2 & 2.1 compatibility
33
 
    True = not 0
34
 
    False = not 1
35
 
 
36
 
 
37
 
def table(name, arguments, options, content, lineno,
38
 
          content_offset, block_text, state, state_machine):
39
 
    if not content:
40
 
        warning = state_machine.reporter.warning(
41
 
            'Content block expected for the "%s" directive; none found.'
42
 
            % name, nodes.literal_block(block_text, block_text),
43
 
            line=lineno)
44
 
        return [warning]
45
 
    title, messages = make_title(arguments, state, lineno)
46
 
    node = nodes.Element()          # anonymous container for parsing
47
 
    state.nested_parse(content, content_offset, node)
48
 
    if len(node) != 1 or not isinstance(node[0], nodes.table):
49
 
        error = state_machine.reporter.error(
50
 
            'Error parsing content block for the "%s" directive: '
51
 
            'exactly one table expected.'
52
 
            % name, nodes.literal_block(block_text, block_text),
53
 
            line=lineno)
54
 
        return [error]
55
 
    table_node = node[0]
56
 
    table_node['classes'] += options.get('class', [])
57
 
    if title:
58
 
        table_node.insert(0, title)
59
 
    return [table_node] + messages
60
 
 
61
 
table.arguments = (0, 1, 1)
62
 
table.options = {'class': directives.class_option}
63
 
table.content = 1
64
 
 
65
 
def make_title(arguments, state, lineno):
66
 
    if arguments:
67
 
        title_text = arguments[0]
68
 
        text_nodes, messages = state.inline_text(title_text, lineno)
69
 
        title = nodes.title(title_text, '', *text_nodes)
70
 
    else:
71
 
        title = None
72
 
        messages = []
73
 
    return title, messages
74
 
 
75
 
 
76
 
if csv:
77
 
    class DocutilsDialect(csv.Dialect):
78
 
 
79
 
        """CSV dialect for `csv_table` directive function."""
80
 
 
81
 
        delimiter = ','
82
 
        quotechar = '"'
83
 
        doublequote = True
84
 
        skipinitialspace = True
85
 
        lineterminator = '\n'
86
 
        quoting = csv.QUOTE_MINIMAL
87
 
 
88
 
        def __init__(self, options):
89
 
            if options.has_key('delim'):
90
 
                self.delimiter = str(options['delim'])
91
 
            if options.has_key('keepspace'):
92
 
                self.skipinitialspace = False
93
 
            if options.has_key('quote'):
94
 
                self.quotechar = str(options['quote'])
95
 
            if options.has_key('escape'):
96
 
                self.doublequote = False
97
 
                self.escapechar = str(options['escape'])
98
 
            csv.Dialect.__init__(self)
99
 
 
100
 
 
101
 
    class HeaderDialect(csv.Dialect):
102
 
 
103
 
        """CSV dialect to use for the "header" option data."""
104
 
 
105
 
        delimiter = ','
106
 
        quotechar = '"'
107
 
        escapechar = '\\'
108
 
        doublequote = False
109
 
        skipinitialspace = True
110
 
        lineterminator = '\n'
111
 
        quoting = csv.QUOTE_MINIMAL
112
 
 
113
 
 
114
 
def csv_table(name, arguments, options, content, lineno,
115
 
             content_offset, block_text, state, state_machine):
116
 
    try:
117
 
        if ( not state.document.settings.file_insertion_enabled
118
 
             and (options.has_key('file') or options.has_key('url')) ):
119
 
            warning = state_machine.reporter.warning(
120
 
                'File and URL access deactivated; ignoring "%s" directive.' %
121
 
                name, nodes.literal_block(block_text,block_text), line=lineno)
 
24
 
 
25
class Table(Directive):
 
26
 
 
27
    """
 
28
    Generic table base class.
 
29
    """
 
30
 
 
31
    required_arguments = 0
 
32
    optional_arguments = 1
 
33
    final_argument_whitespace = True
 
34
    option_spec = {'class': directives.class_option}
 
35
    has_content = True
 
36
 
 
37
    def make_title(self):
 
38
        if self.arguments:
 
39
            title_text = self.arguments[0]
 
40
            text_nodes, messages = self.state.inline_text(title_text,
 
41
                                                          self.lineno)
 
42
            title = nodes.title(title_text, '', *text_nodes)
 
43
        else:
 
44
            title = None
 
45
            messages = []
 
46
        return title, messages
 
47
 
 
48
    def process_header_option(self):
 
49
        source = self.state_machine.get_source(self.lineno - 1)
 
50
        table_head = []
 
51
        max_header_cols = 0
 
52
        if self.options.has_key('header'):   # separate table header in option
 
53
            rows, max_header_cols = self.parse_csv_data_into_rows(
 
54
                self.options['header'].split('\n'), self.HeaderDialect(),
 
55
                source)
 
56
            table_head.extend(rows)
 
57
        return table_head, max_header_cols
 
58
 
 
59
    def check_table_dimensions(self, rows, header_rows, stub_columns):
 
60
        if len(rows) < header_rows:
 
61
            error = self.state_machine.reporter.error(
 
62
                '%s header row(s) specified but only %s row(s) of data '
 
63
                'supplied ("%s" directive).'
 
64
                % (header_rows, len(rows), self.name), nodes.literal_block(
 
65
                self.block_text, self.block_text), line=self.lineno)
 
66
            raise SystemMessagePropagation(error)
 
67
        if len(rows) == header_rows > 0:
 
68
            error = self.state_machine.reporter.error(
 
69
                'Insufficient data supplied (%s row(s)); no data remaining '
 
70
                'for table body, required by "%s" directive.'
 
71
                % (len(rows), self.name), nodes.literal_block(
 
72
                self.block_text, self.block_text), line=self.lineno)
 
73
            raise SystemMessagePropagation(error)
 
74
        for row in rows:
 
75
            if len(row) < stub_columns:
 
76
                error = self.state_machine.reporter.error(
 
77
                    '%s stub column(s) specified but only %s columns(s) of '
 
78
                    'data supplied ("%s" directive).' %
 
79
                    (stub_columns, len(row), self.name), nodes.literal_block(
 
80
                    self.block_text, self.block_text), line=self.lineno)
 
81
                raise SystemMessagePropagation(error)
 
82
            if len(row) == stub_columns > 0:
 
83
                error = self.state_machine.reporter.error(
 
84
                    'Insufficient data supplied (%s columns(s)); no data remaining '
 
85
                    'for table body, required by "%s" directive.'
 
86
                    % (len(row), self.name), nodes.literal_block(
 
87
                    self.block_text, self.block_text), line=self.lineno)
 
88
                raise SystemMessagePropagation(error)
 
89
 
 
90
    def get_column_widths(self, max_cols):
 
91
        if self.options.has_key('widths'):
 
92
            col_widths = self.options['widths']
 
93
            if len(col_widths) != max_cols:
 
94
                error = self.state_machine.reporter.error(
 
95
                    '"%s" widths do not match the number of columns in table '
 
96
                    '(%s).' % (self.name, max_cols), nodes.literal_block(
 
97
                    self.block_text, self.block_text), line=self.lineno)
 
98
                raise SystemMessagePropagation(error)
 
99
        elif max_cols:
 
100
            col_widths = [100 // max_cols] * max_cols
 
101
        else:
 
102
            error = self.state_machine.reporter.error(
 
103
                'No table data detected in CSV file.', nodes.literal_block(
 
104
                self.block_text, self.block_text), line=self.lineno)
 
105
            raise SystemMessagePropagation(error)
 
106
        return col_widths
 
107
 
 
108
    def extend_short_rows_with_empty_cells(self, columns, parts):
 
109
        for part in parts:
 
110
            for row in part:
 
111
                if len(row) < columns:
 
112
                    row.extend([(0, 0, 0, [])] * (columns - len(row)))
 
113
 
 
114
 
 
115
class RSTTable(Table):
 
116
 
 
117
    def run(self):
 
118
        if not self.content:
 
119
            warning = self.state_machine.reporter.warning(
 
120
                'Content block expected for the "%s" directive; none found.'
 
121
                % self.name, nodes.literal_block(
 
122
                self.block_text, self.block_text), line=self.lineno)
122
123
            return [warning]
123
 
        check_requirements(name, lineno, block_text, state_machine)
124
 
        title, messages = make_title(arguments, state, lineno)
125
 
        csv_data, source = get_csv_data(
126
 
            name, options, content, lineno, block_text, state, state_machine)
127
 
        table_head, max_header_cols = process_header_option(
128
 
            options, state_machine, lineno)
129
 
        rows, max_cols = parse_csv_data_into_rows(
130
 
            csv_data, DocutilsDialect(options), source, options)
131
 
        max_cols = max(max_cols, max_header_cols)
132
 
        header_rows = options.get('header-rows', 0) # default 0
133
 
        stub_columns = options.get('stub-columns', 0) # default 0
134
 
        check_table_dimensions(
135
 
            rows, header_rows, stub_columns, name, lineno,
136
 
            block_text, state_machine)
137
 
        table_head.extend(rows[:header_rows])
138
 
        table_body = rows[header_rows:]
139
 
        col_widths = get_column_widths(
140
 
            max_cols, name, options, lineno, block_text, state_machine)
141
 
        extend_short_rows_with_empty_cells(max_cols, (table_head, table_body))
142
 
    except SystemMessagePropagation, detail:
143
 
        return [detail.args[0]]
144
 
    except csv.Error, detail:
145
 
        error = state_machine.reporter.error(
146
 
            'Error with CSV data in "%s" directive:\n%s' % (name, detail),
147
 
            nodes.literal_block(block_text, block_text), line=lineno)
148
 
        return [error]
149
 
    table = (col_widths, table_head, table_body)
150
 
    table_node = state.build_table(table, content_offset, stub_columns)
151
 
    table_node['classes'] += options.get('class', [])
152
 
    if title:
153
 
        table_node.insert(0, title)
154
 
    return [table_node] + messages
155
 
 
156
 
csv_table.arguments = (0, 1, 1)
157
 
csv_table.options = {'header-rows': directives.nonnegative_int,
158
 
                     'stub-columns': directives.nonnegative_int,
159
 
                     'header': directives.unchanged,
160
 
                     'widths': directives.positive_int_list,
161
 
                     'file': directives.path,
162
 
                     'url': directives.uri,
163
 
                     'encoding': directives.encoding,
164
 
                     'class': directives.class_option,
165
 
                     # field delimiter char
166
 
                     'delim': directives.single_char_or_whitespace_or_unicode,
167
 
                     # treat whitespace after delimiter as significant
168
 
                     'keepspace': directives.flag,
169
 
                     # text field quote/unquote char:
170
 
                     'quote': directives.single_char_or_unicode,
171
 
                     # char used to escape delim & quote as-needed:
172
 
                     'escape': directives.single_char_or_unicode,}
173
 
csv_table.content = 1
174
 
 
175
 
def check_requirements(name, lineno, block_text, state_machine):
176
 
    if not csv:
177
 
        error = state_machine.reporter.error(
178
 
            'The "%s" directive is not compatible with this version of '
179
 
            'Python (%s).  Requires the "csv" module, new in Python 2.3.'
180
 
            % (name, sys.version.split()[0]),
181
 
            nodes.literal_block(block_text, block_text), line=lineno)
182
 
        raise SystemMessagePropagation(error)
183
 
 
184
 
def get_csv_data(name, options, content, lineno, block_text,
185
 
                 state, state_machine):
186
 
    """
187
 
    CSV data can come from the directive content, from an external file, or
188
 
    from a URL reference.
189
 
    """
190
 
    encoding = options.get('encoding', state.document.settings.input_encoding)
191
 
    if content:                         # CSV data is from directive content
192
 
        if options.has_key('file') or options.has_key('url'):
193
 
            error = state_machine.reporter.error(
194
 
                  '"%s" directive may not both specify an external file and '
195
 
                  'have content.' % name,
196
 
                  nodes.literal_block(block_text, block_text), line=lineno)
197
 
            raise SystemMessagePropagation(error)
198
 
        source = content.source(0)
199
 
        csv_data = content
200
 
    elif options.has_key('file'):       # CSV data is from an external file
201
 
        if options.has_key('url'):
202
 
            error = state_machine.reporter.error(
203
 
                  'The "file" and "url" options may not be simultaneously '
204
 
                  'specified for the "%s" directive.' % name,
205
 
                  nodes.literal_block(block_text, block_text), line=lineno)
206
 
            raise SystemMessagePropagation(error)
207
 
        source_dir = os.path.dirname(
208
 
            os.path.abspath(state.document.current_source))
209
 
        source = os.path.normpath(os.path.join(source_dir, options['file']))
210
 
        source = utils.relative_path(None, source)
 
124
        title, messages = self.make_title()
 
125
        node = nodes.Element()          # anonymous container for parsing
 
126
        self.state.nested_parse(self.content, self.content_offset, node)
 
127
        if len(node) != 1 or not isinstance(node[0], nodes.table):
 
128
            error = self.state_machine.reporter.error(
 
129
                'Error parsing content block for the "%s" directive: exactly '
 
130
                'one table expected.' % self.name, nodes.literal_block(
 
131
                self.block_text, self.block_text), line=self.lineno)
 
132
            return [error]
 
133
        table_node = node[0]
 
134
        table_node['classes'] += self.options.get('class', [])
 
135
        if title:
 
136
            table_node.insert(0, title)
 
137
        return [table_node] + messages
 
138
 
 
139
 
 
140
class CSVTable(Table):
 
141
 
 
142
    option_spec = {'header-rows': directives.nonnegative_int,
 
143
                   'stub-columns': directives.nonnegative_int,
 
144
                   'header': directives.unchanged,
 
145
                   'widths': directives.positive_int_list,
 
146
                   'file': directives.path,
 
147
                   'url': directives.uri,
 
148
                   'encoding': directives.encoding,
 
149
                   'class': directives.class_option,
 
150
                   # field delimiter char
 
151
                   'delim': directives.single_char_or_whitespace_or_unicode,
 
152
                   # treat whitespace after delimiter as significant
 
153
                   'keepspace': directives.flag,
 
154
                   # text field quote/unquote char:
 
155
                   'quote': directives.single_char_or_unicode,
 
156
                   # char used to escape delim & quote as-needed:
 
157
                   'escape': directives.single_char_or_unicode,}
 
158
 
 
159
    if csv:
 
160
 
 
161
        class DocutilsDialect(csv.Dialect):
 
162
 
 
163
            """CSV dialect for `csv_table` directive."""
 
164
 
 
165
            delimiter = ','
 
166
            quotechar = '"'
 
167
            doublequote = True
 
168
            skipinitialspace = True
 
169
            lineterminator = '\n'
 
170
            quoting = csv.QUOTE_MINIMAL
 
171
 
 
172
            def __init__(self, options):
 
173
                if options.has_key('delim'):
 
174
                    self.delimiter = str(options['delim'])
 
175
                if options.has_key('keepspace'):
 
176
                    self.skipinitialspace = False
 
177
                if options.has_key('quote'):
 
178
                    self.quotechar = str(options['quote'])
 
179
                if options.has_key('escape'):
 
180
                    self.doublequote = False
 
181
                    self.escapechar = str(options['escape'])
 
182
                csv.Dialect.__init__(self)
 
183
 
 
184
 
 
185
        class HeaderDialect(csv.Dialect):
 
186
 
 
187
            """CSV dialect to use for the "header" option data."""
 
188
 
 
189
            delimiter = ','
 
190
            quotechar = '"'
 
191
            escapechar = '\\'
 
192
            doublequote = False
 
193
            skipinitialspace = True
 
194
            lineterminator = '\n'
 
195
            quoting = csv.QUOTE_MINIMAL
 
196
 
 
197
    def check_requirements(self):
 
198
        if not csv:
 
199
            error = self.state_machine.reporter.error(
 
200
                'The "%s" directive is not compatible with this version of '
 
201
                'Python (%s).  Requires the "csv" module, new in Python 2.3.'
 
202
                % (self.name, sys.version.split()[0]), nodes.literal_block(
 
203
                self.block_text, self.block_text), line=self.lineno)
 
204
            raise SystemMessagePropagation(error)
 
205
 
 
206
    def run(self):
211
207
        try:
212
 
            state.document.settings.record_dependencies.add(source)
213
 
            csv_file = io.FileInput(
214
 
                source_path=source, encoding=encoding,
215
 
                error_handler
216
 
                    =state.document.settings.input_encoding_error_handler,
217
 
                handle_io_errors=None)
 
208
            if (not self.state.document.settings.file_insertion_enabled
 
209
                and (self.options.has_key('file')
 
210
                     or self.options.has_key('url'))):
 
211
                warning = self.state_machine.reporter.warning(
 
212
                    'File and URL access deactivated; ignoring "%s" '
 
213
                    'directive.' % self.name, nodes.literal_block(
 
214
                    self.block_text, self.block_text), line=self.lineno)
 
215
                return [warning]
 
216
            self.check_requirements()
 
217
            title, messages = self.make_title()
 
218
            csv_data, source = self.get_csv_data()
 
219
            table_head, max_header_cols = self.process_header_option()
 
220
            rows, max_cols = self.parse_csv_data_into_rows(
 
221
                csv_data, self.DocutilsDialect(self.options), source)
 
222
            max_cols = max(max_cols, max_header_cols)
 
223
            header_rows = self.options.get('header-rows', 0)
 
224
            stub_columns = self.options.get('stub-columns', 0)
 
225
            self.check_table_dimensions(rows, header_rows, stub_columns)
 
226
            table_head.extend(rows[:header_rows])
 
227
            table_body = rows[header_rows:]
 
228
            col_widths = self.get_column_widths(max_cols)
 
229
            self.extend_short_rows_with_empty_cells(max_cols,
 
230
                                                    (table_head, table_body))
 
231
        except SystemMessagePropagation, detail:
 
232
            return [detail.args[0]]
 
233
        except csv.Error, detail:
 
234
            error = self.state_machine.reporter.error(
 
235
                'Error with CSV data in "%s" directive:\n%s'
 
236
                % (self.name, detail), nodes.literal_block(
 
237
                self.block_text, self.block_text), line=self.lineno)
 
238
            return [error]
 
239
        table = (col_widths, table_head, table_body)
 
240
        table_node = self.state.build_table(table, self.content_offset,
 
241
                                            stub_columns)
 
242
        table_node['classes'] += self.options.get('class', [])
 
243
        if title:
 
244
            table_node.insert(0, title)
 
245
        return [table_node] + messages
 
246
 
 
247
    def get_csv_data(self):
 
248
        """
 
249
        Get CSV data from the directive content, from an external
 
250
        file, or from a URL reference.
 
251
        """
 
252
        encoding = self.options.get(
 
253
            'encoding', self.state.document.settings.input_encoding)
 
254
        if self.content:
 
255
            # CSV data is from directive content.
 
256
            if self.options.has_key('file') or self.options.has_key('url'):
 
257
                error = self.state_machine.reporter.error(
 
258
                    '"%s" directive may not both specify an external file and'
 
259
                    ' have content.' % self.name, nodes.literal_block(
 
260
                    self.block_text, self.block_text), line=self.lineno)
 
261
                raise SystemMessagePropagation(error)
 
262
            source = self.content.source(0)
 
263
            csv_data = self.content
 
264
        elif self.options.has_key('file'):
 
265
            # CSV data is from an external file.
 
266
            if self.options.has_key('url'):
 
267
                error = self.state_machine.reporter.error(
 
268
                      'The "file" and "url" options may not be simultaneously'
 
269
                      ' specified for the "%s" directive.' % self.name,
 
270
                      nodes.literal_block(self.block_text, self.block_text),
 
271
                      line=self.lineno)
 
272
                raise SystemMessagePropagation(error)
 
273
            source_dir = os.path.dirname(
 
274
                os.path.abspath(self.state.document.current_source))
 
275
            source = os.path.normpath(os.path.join(source_dir,
 
276
                                                   self.options['file']))
 
277
            source = utils.relative_path(None, source)
 
278
            try:
 
279
                self.state.document.settings.record_dependencies.add(source)
 
280
                csv_file = io.FileInput(
 
281
                    source_path=source, encoding=encoding,
 
282
                    error_handler=(self.state.document.settings.\
 
283
                                   input_encoding_error_handler),
 
284
                    handle_io_errors=None)
 
285
                csv_data = csv_file.read().splitlines()
 
286
            except IOError, error:
 
287
                severe = self.state_machine.reporter.severe(
 
288
                    'Problems with "%s" directive path:\n%s.'
 
289
                    % (self.name, error), nodes.literal_block(
 
290
                    self.block_text, self.block_text), line=self.lineno)
 
291
                raise SystemMessagePropagation(severe)
 
292
        elif self.options.has_key('url'):
 
293
            # CSV data is from a URL.
 
294
            # Do not import urllib2 at the top of the module because
 
295
            # it may fail due to broken SSL dependencies, and it takes
 
296
            # about 0.15 seconds to load.
 
297
            import urllib2
 
298
            source = self.options['url']
 
299
            try:
 
300
                csv_text = urllib2.urlopen(source).read()
 
301
            except (urllib2.URLError, IOError, OSError, ValueError), error:
 
302
                severe = self.state_machine.reporter.severe(
 
303
                      'Problems with "%s" directive URL "%s":\n%s.'
 
304
                      % (self.name, self.options['url'], error),
 
305
                      nodes.literal_block(self.block_text, self.block_text),
 
306
                      line=self.lineno)
 
307
                raise SystemMessagePropagation(severe)
 
308
            csv_file = io.StringInput(
 
309
                source=csv_text, source_path=source, encoding=encoding,
 
310
                error_handler=(self.state.document.settings.\
 
311
                               input_encoding_error_handler))
218
312
            csv_data = csv_file.read().splitlines()
219
 
        except IOError, error:
220
 
            severe = state_machine.reporter.severe(
221
 
                  'Problems with "%s" directive path:\n%s.' % (name, error),
222
 
                  nodes.literal_block(block_text, block_text), line=lineno)
223
 
            raise SystemMessagePropagation(severe)
224
 
    elif options.has_key('url'):        # CSV data is from a URL
225
 
        if not urllib2:
226
 
            severe = state_machine.reporter.severe(
227
 
                  'Problems with the "%s" directive and its "url" option: '
228
 
                  'unable to access the required functionality (from the '
229
 
                  '"urllib2" module).' % name,
230
 
                  nodes.literal_block(block_text, block_text), line=lineno)
231
 
            raise SystemMessagePropagation(severe)
232
 
        source = options['url']
233
 
        try:
234
 
            csv_text = urllib2.urlopen(source).read()
235
 
        except (urllib2.URLError, IOError, OSError, ValueError), error:
236
 
            severe = state_machine.reporter.severe(
237
 
                  'Problems with "%s" directive URL "%s":\n%s.'
238
 
                  % (name, options['url'], error),
239
 
                  nodes.literal_block(block_text, block_text), line=lineno)
240
 
            raise SystemMessagePropagation(severe)
241
 
        csv_file = io.StringInput(
242
 
            source=csv_text, source_path=source, encoding=encoding,
243
 
            error_handler=state.document.settings.input_encoding_error_handler)
244
 
        csv_data = csv_file.read().splitlines()
245
 
    else:
246
 
        error = state_machine.reporter.warning(
247
 
            'The "%s" directive requires content; none supplied.' % (name),
248
 
            nodes.literal_block(block_text, block_text), line=lineno)
249
 
        raise SystemMessagePropagation(error)
250
 
    return csv_data, source
251
 
 
252
 
def process_header_option(options, state_machine, lineno):
253
 
    source = state_machine.get_source(lineno - 1)
254
 
    table_head = []
255
 
    max_header_cols = 0
256
 
    if options.has_key('header'):       # separate table header in option
257
 
        rows, max_header_cols = parse_csv_data_into_rows(
258
 
            options['header'].split('\n'), HeaderDialect(), source, options)
259
 
        table_head.extend(rows)
260
 
    return table_head, max_header_cols
261
 
 
262
 
def parse_csv_data_into_rows(csv_data, dialect, source, options):
263
 
    # csv.py doesn't do Unicode; encode temporarily as UTF-8
264
 
    csv_reader = csv.reader([line.encode('utf-8') for line in csv_data],
265
 
                            dialect=dialect)
266
 
    rows = []
267
 
    max_cols = 0
268
 
    for row in csv_reader:
269
 
        row_data = []
270
 
        for cell in row:
271
 
            # decode UTF-8 back to Unicode
272
 
            cell_text = unicode(cell, 'utf-8')
273
 
            cell_data = (0, 0, 0, statemachine.StringList(
274
 
                cell_text.splitlines(), source=source))
275
 
            row_data.append(cell_data)
276
 
        rows.append(row_data)
277
 
        max_cols = max(max_cols, len(row))
278
 
    return rows, max_cols
279
 
 
280
 
def check_table_dimensions(rows, header_rows, stub_columns, name, lineno,
281
 
                           block_text, state_machine):
282
 
    if len(rows) < header_rows:
283
 
        error = state_machine.reporter.error(
284
 
            '%s header row(s) specified but only %s row(s) of data supplied '
285
 
            '("%s" directive).' % (header_rows, len(rows), name),
286
 
            nodes.literal_block(block_text, block_text), line=lineno)
287
 
        raise SystemMessagePropagation(error)
288
 
    if len(rows) == header_rows > 0:
289
 
        error = state_machine.reporter.error(
290
 
            'Insufficient data supplied (%s row(s)); no data remaining for '
291
 
            'table body, required by "%s" directive.' % (len(rows), name),
292
 
            nodes.literal_block(block_text, block_text), line=lineno)
293
 
        raise SystemMessagePropagation(error)
294
 
    for row in rows:
295
 
        if len(row) < stub_columns:
296
 
            error = state_machine.reporter.error(
297
 
                '%s stub column(s) specified but only %s columns(s) of data '
298
 
                'supplied ("%s" directive).' % (stub_columns, len(row), name),
299
 
                nodes.literal_block(block_text, block_text), line=lineno)
300
 
            raise SystemMessagePropagation(error)
301
 
        if len(row) == stub_columns > 0:
302
 
            error = state_machine.reporter.error(
303
 
                'Insufficient data supplied (%s columns(s)); no data remaining '
304
 
                'for table body, required by "%s" directive.'
305
 
                % (len(row), name),
306
 
                nodes.literal_block(block_text, block_text), line=lineno)
307
 
            raise SystemMessagePropagation(error)
308
 
 
309
 
def get_column_widths(max_cols, name, options, lineno, block_text,
310
 
                      state_machine):
311
 
    if options.has_key('widths'):
312
 
        col_widths = options['widths']
313
 
        if len(col_widths) != max_cols:
314
 
            error = state_machine.reporter.error(
315
 
              '"%s" widths do not match the number of columns in table (%s).'
316
 
              % (name, max_cols),
317
 
              nodes.literal_block(block_text, block_text), line=lineno)
318
 
            raise SystemMessagePropagation(error)
319
 
    elif max_cols:
320
 
        col_widths = [100 / max_cols] * max_cols
321
 
    else:
322
 
        error = state_machine.reporter.error(
323
 
            'No table data detected in CSV file.',
324
 
            nodes.literal_block(block_text, block_text), line=lineno)
325
 
        raise SystemMessagePropagation(error)
326
 
    return col_widths
327
 
 
328
 
def extend_short_rows_with_empty_cells(columns, parts):
329
 
    for part in parts:
330
 
        for row in part:
331
 
            if len(row) < columns:
332
 
                row.extend([(0, 0, 0, [])] * (columns - len(row)))
333
 
 
334
 
def list_table(name, arguments, options, content, lineno,
335
 
               content_offset, block_text, state, state_machine):
 
313
        else:
 
314
            error = self.state_machine.reporter.warning(
 
315
                'The "%s" directive requires content; none supplied.'
 
316
                % self.name, nodes.literal_block(
 
317
                self.block_text, self.block_text), line=self.lineno)
 
318
            raise SystemMessagePropagation(error)
 
319
        return csv_data, source
 
320
 
 
321
    def parse_csv_data_into_rows(self, csv_data, dialect, source):
 
322
        # csv.py doesn't do Unicode; encode temporarily as UTF-8
 
323
        csv_reader = csv.reader([(line.encode('utf-8') + '\n')
 
324
                                for line in csv_data],
 
325
                                dialect=dialect)
 
326
        rows = []
 
327
        max_cols = 0
 
328
        for row in csv_reader:
 
329
            row_data = []
 
330
            for cell in row:
 
331
                # decode UTF-8 back to Unicode
 
332
                cell_text = unicode(cell, 'utf-8')
 
333
                cell_data = (0, 0, 0, statemachine.StringList(
 
334
                    cell_text.splitlines(), source=source))
 
335
                row_data.append(cell_data)
 
336
            rows.append(row_data)
 
337
            max_cols = max(max_cols, len(row))
 
338
        return rows, max_cols
 
339
 
 
340
 
 
341
class ListTable(Table):
 
342
 
336
343
    """
337
344
    Implement tables whose data is encoded as a uniform two-level bullet list.
338
345
    For further ideas, see
339
346
    http://docutils.sf.net/docs/dev/rst/alternatives.html#list-driven-tables
340
347
    """ 
341
 
    if not content:
342
 
        error = state_machine.reporter.error(
343
 
            'The "%s" directive is empty; content required.' % name,
344
 
            nodes.literal_block(block_text, block_text), line=lineno)
345
 
        return [error]
346
 
    title, messages = make_title(arguments, state, lineno)
347
 
    node = nodes.Element()          # anonymous container for parsing
348
 
    state.nested_parse(content, content_offset, node)
349
 
    try:
350
 
        num_cols, col_widths = check_list_content(
351
 
            node, name, options, content, lineno, block_text, state_machine)
352
 
        table_data = [[item.children for item in row_list[0]]
353
 
                      for row_list in node[0]]
354
 
        header_rows = options.get('header-rows', 0) # default 0
355
 
        stub_columns = options.get('stub-columns', 0) # default 0
356
 
        check_table_dimensions(
357
 
            table_data, header_rows, stub_columns, name, lineno,
358
 
            block_text, state_machine)
359
 
    except SystemMessagePropagation, detail:
360
 
        return [detail.args[0]]
361
 
    table_node = build_table_from_list(table_data, col_widths,
362
 
                                       header_rows, stub_columns)
363
 
    table_node['classes'] += options.get('class', [])
364
 
    if title:
365
 
        table_node.insert(0, title)
366
 
    return [table_node] + messages
367
 
 
368
 
list_table.arguments = (0, 1, 1)
369
 
list_table.options = {'header-rows': directives.nonnegative_int,
370
 
                      'stub-columns': directives.nonnegative_int,
371
 
                      'widths': directives.positive_int_list,
372
 
                      'class': directives.class_option}
373
 
list_table.content = 1
374
 
 
375
 
def check_list_content(node, name, options, content, lineno, block_text,
376
 
                       state_machine):
377
 
    if len(node) != 1 or not isinstance(node[0], nodes.bullet_list):
378
 
        error = state_machine.reporter.error(
379
 
            'Error parsing content block for the "%s" directive: '
380
 
            'exactly one bullet list expected.' % name,
381
 
            nodes.literal_block(block_text, block_text), line=lineno)
382
 
        raise SystemMessagePropagation(error)
383
 
    list_node = node[0]
384
 
    # Check for a uniform two-level bullet list:
385
 
    for item_index in range(len(list_node)):
386
 
        item = list_node[item_index]
387
 
        if len(item) != 1 or not isinstance(item[0], nodes.bullet_list):
388
 
            error = state_machine.reporter.error(
 
348
 
 
349
    option_spec = {'header-rows': directives.nonnegative_int,
 
350
                   'stub-columns': directives.nonnegative_int,
 
351
                   'widths': directives.positive_int_list,
 
352
                   'class': directives.class_option}
 
353
 
 
354
    def run(self):
 
355
        if not self.content:
 
356
            error = self.state_machine.reporter.error(
 
357
                'The "%s" directive is empty; content required.' % self.name,
 
358
                nodes.literal_block(self.block_text, self.block_text),
 
359
                line=self.lineno)
 
360
            return [error]
 
361
        title, messages = self.make_title()
 
362
        node = nodes.Element()          # anonymous container for parsing
 
363
        self.state.nested_parse(self.content, self.content_offset, node)
 
364
        try:
 
365
            num_cols, col_widths = self.check_list_content(node)
 
366
            table_data = [[item.children for item in row_list[0]]
 
367
                          for row_list in node[0]]
 
368
            header_rows = self.options.get('header-rows', 0)
 
369
            stub_columns = self.options.get('stub-columns', 0)
 
370
            self.check_table_dimensions(table_data, header_rows, stub_columns)
 
371
        except SystemMessagePropagation, detail:
 
372
            return [detail.args[0]]
 
373
        table_node = self.build_table_from_list(table_data, col_widths,
 
374
                                                header_rows, stub_columns)
 
375
        table_node['classes'] += self.options.get('class', [])
 
376
        if title:
 
377
            table_node.insert(0, title)
 
378
        return [table_node] + messages
 
379
 
 
380
    def check_list_content(self, node):
 
381
        if len(node) != 1 or not isinstance(node[0], nodes.bullet_list):
 
382
            error = self.state_machine.reporter.error(
389
383
                'Error parsing content block for the "%s" directive: '
390
 
                'two-level bullet list expected, but row %s does not contain '
391
 
                'a second-level bullet list.' % (name, item_index + 1),
392
 
                nodes.literal_block(block_text, block_text), line=lineno)
 
384
                'exactly one bullet list expected.' % self.name,
 
385
                nodes.literal_block(self.block_text, self.block_text),
 
386
                line=self.lineno)
393
387
            raise SystemMessagePropagation(error)
394
 
        elif item_index:
395
 
            # ATTN pychecker users: num_cols is guaranteed to be set in the
396
 
            # "else" clause below for item_index==0, before this branch is
397
 
            # triggered.
398
 
            if len(item[0]) != num_cols:
399
 
                error = state_machine.reporter.error(
 
388
        list_node = node[0]
 
389
        # Check for a uniform two-level bullet list:
 
390
        for item_index in range(len(list_node)):
 
391
            item = list_node[item_index]
 
392
            if len(item) != 1 or not isinstance(item[0], nodes.bullet_list):
 
393
                error = self.state_machine.reporter.error(
400
394
                    'Error parsing content block for the "%s" directive: '
401
 
                    'uniform two-level bullet list expected, but row %s does '
402
 
                    'not contain the same number of items as row 1 (%s vs %s).'
403
 
                    % (name, item_index + 1, len(item[0]), num_cols),
404
 
                    nodes.literal_block(block_text, block_text), line=lineno)
 
395
                    'two-level bullet list expected, but row %s does not '
 
396
                    'contain a second-level bullet list.'
 
397
                    % (self.name, item_index + 1), nodes.literal_block(
 
398
                    self.block_text, self.block_text), line=self.lineno)
405
399
                raise SystemMessagePropagation(error)
406
 
        else:
407
 
            num_cols = len(item[0])
408
 
    col_widths = get_column_widths(
409
 
        num_cols, name, options, lineno, block_text, state_machine)
410
 
    if len(col_widths) != num_cols:
411
 
        error = state_machine.reporter.error(
412
 
            'Error parsing "widths" option of the "%s" directive: '
413
 
            'number of columns does not match the table data (%s vs %s).'
414
 
            % (name, len(col_widths), num_cols),
415
 
            nodes.literal_block(block_text, block_text), line=lineno)
416
 
        raise SystemMessagePropagation(error)
417
 
    return num_cols, col_widths
 
400
            elif item_index:
 
401
                # ATTN pychecker users: num_cols is guaranteed to be set in the
 
402
                # "else" clause below for item_index==0, before this branch is
 
403
                # triggered.
 
404
                if len(item[0]) != num_cols:
 
405
                    error = self.state_machine.reporter.error(
 
406
                        'Error parsing content block for the "%s" directive: '
 
407
                        'uniform two-level bullet list expected, but row %s '
 
408
                        'does not contain the same number of items as row 1 '
 
409
                        '(%s vs %s).'
 
410
                        % (self.name, item_index + 1, len(item[0]), num_cols),
 
411
                        nodes.literal_block(self.block_text, self.block_text),
 
412
                        line=self.lineno)
 
413
                    raise SystemMessagePropagation(error)
 
414
            else:
 
415
                num_cols = len(item[0])
 
416
        col_widths = self.get_column_widths(num_cols)
 
417
        return num_cols, col_widths
418
418
 
419
 
def build_table_from_list(table_data, col_widths, header_rows, stub_columns):
420
 
    table = nodes.table()
421
 
    tgroup = nodes.tgroup(cols=len(col_widths))
422
 
    table += tgroup
423
 
    for col_width in col_widths:
424
 
        colspec = nodes.colspec(colwidth=col_width)
425
 
        if stub_columns:
426
 
            colspec.attributes['stub'] = 1
427
 
            stub_columns -= 1
428
 
        tgroup += colspec
429
 
    rows = []
430
 
    for row in table_data:
431
 
        row_node = nodes.row()
432
 
        for cell in row:
433
 
            entry = nodes.entry()
434
 
            entry += cell
435
 
            row_node += entry
436
 
        rows.append(row_node)
437
 
    if header_rows:
438
 
        thead = nodes.thead()
439
 
        thead.extend(rows[:header_rows])
440
 
        tgroup += thead
441
 
    tbody = nodes.tbody()
442
 
    tbody.extend(rows[header_rows:])
443
 
    tgroup += tbody
444
 
    return table
 
419
    def build_table_from_list(self, table_data, col_widths, header_rows, stub_columns):
 
420
        table = nodes.table()
 
421
        tgroup = nodes.tgroup(cols=len(col_widths))
 
422
        table += tgroup
 
423
        for col_width in col_widths:
 
424
            colspec = nodes.colspec(colwidth=col_width)
 
425
            if stub_columns:
 
426
                colspec.attributes['stub'] = 1
 
427
                stub_columns -= 1
 
428
            tgroup += colspec
 
429
        rows = []
 
430
        for row in table_data:
 
431
            row_node = nodes.row()
 
432
            for cell in row:
 
433
                entry = nodes.entry()
 
434
                entry += cell
 
435
                row_node += entry
 
436
            rows.append(row_node)
 
437
        if header_rows:
 
438
            thead = nodes.thead()
 
439
            thead.extend(rows[:header_rows])
 
440
            tgroup += thead
 
441
        tbody = nodes.tbody()
 
442
        tbody.extend(rows[header_rows:])
 
443
        tgroup += tbody
 
444
        return table