~ubuntu-branches/ubuntu/trusty/python-docutils/trusty

« back to all changes in this revision

Viewing changes to .pc/strict-csv-parser.diff/docutils/parsers/rst/directives/tables.py

  • Committer: Package Import Robot
  • Author(s): Dmitry Shachnev
  • Date: 2013-05-17 16:47:30 UTC
  • mfrom: (20.1.2 sid)
  • Revision ID: package-import@ubuntu.com-20130517164730-5ux7p59z0jdku6pf
Tags: 0.10-3ubuntu1
* Merge with Debian unstable, remaining changes:
  - Use dh_python2 instead of dh_pysupport.
  - Backport patch to support embedded aliases in references
    (support-aliases-in-references.diff).
* disable_py33_failing_tests.diff: dropped, the issue is now
  properly fixed in Debian.

Show diffs side-by-side

added added

removed removed

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