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.
6
Directives for table elements.
9
__docformat__ = 'reStructuredText'
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
23
class Table(Directive):
26
Generic table base class.
29
optional_arguments = 1
30
final_argument_whitespace = True
31
option_spec = {'class': directives.class_option,
32
'name': directives.unchanged}
37
title_text = self.arguments[0]
38
text_nodes, messages = self.state.inline_text(title_text,
40
title = nodes.title(title_text, '', *text_nodes)
44
return title, messages
46
def process_header_option(self):
47
source = self.state_machine.get_source(self.lineno - 1)
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(),
54
table_head.extend(rows)
55
return table_head, max_header_cols
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)
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)
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)
98
col_widths = [100 // max_cols] * max_cols
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)
106
def extend_short_rows_with_empty_cells(self, columns, parts):
109
if len(row) < columns:
110
row.extend([(0, 0, 0, [])] * (columns - len(row)))
113
class RSTTable(Table):
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)
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)
132
table_node['classes'] += self.options.get('class', [])
133
self.add_name(table_node)
135
table_node.insert(0, title)
136
return [table_node] + messages
139
class CSVTable(Table):
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,}
159
class DocutilsDialect(csv.Dialect):
161
"""CSV dialect for `csv_table` directive."""
166
skipinitialspace = True
167
lineterminator = '\n'
168
quoting = csv.QUOTE_MINIMAL
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)
183
class HeaderDialect(csv.Dialect):
185
"""CSV dialect to use for the "header" option data."""
191
skipinitialspace = True
192
lineterminator = '\n'
193
quoting = csv.QUOTE_MINIMAL
195
def check_requirements(self):
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)
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)
231
table = (col_widths, table_head, table_body)
232
table_node = self.state.build_table(table, self.content_offset,
234
table_node['classes'] += self.options.get('class', [])
235
self.add_name(table_node)
237
table_node.insert(0, title)
238
return [table_node] + messages
240
def get_csv_data(self):
242
Get CSV data from the directive content, from an external
243
file, or from a URL reference.
245
encoding = self.options.get(
246
'encoding', self.state.document.settings.input_encoding)
247
error_handler = self.state.document.settings.input_encoding_error_handler
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),
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)
273
self.state.document.settings.record_dependencies.add(source)
274
csv_file = io.FileInput(source_path=source,
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),
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.
291
source = self.options['url']
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),
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()
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
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')
321
def decode_from_csv(s):
323
def encode_for_csv(s):
325
decode_from_csv = staticmethod(decode_from_csv)
326
encode_for_csv = staticmethod(encode_for_csv)
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],
335
for row in csv_reader:
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
348
class ListTable(Table):
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
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}
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),
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)
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)
386
table_node.insert(0, title)
387
return [table_node] + messages
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),
396
raise SystemMessagePropagation(error)
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)
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
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 '
419
% (self.name, item_index + 1, len(item[0]), num_cols),
420
nodes.literal_block(self.block_text, self.block_text),
422
raise SystemMessagePropagation(error)
424
num_cols = len(item[0])
425
col_widths = self.get_column_widths(num_cols)
426
return num_cols, col_widths
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))
432
for col_width in col_widths:
433
colspec = nodes.colspec(colwidth=col_width)
435
colspec.attributes['stub'] = 1
439
for row in table_data:
440
row_node = nodes.row()
442
entry = nodes.entry()
445
rows.append(row_node)
447
thead = nodes.thead()
448
thead.extend(rows[:header_rows])
450
tbody = nodes.tbody()
451
tbody.extend(rows[header_rows:])