Fix syntax error with Python <= 2.6.
[docutils.git] / docutils / docutils / parsers / rst / directives / tables.py
blob21c65af655deba988f05bbab5ff624510fffebdf
1 # $Id$
2 # Authors: David Goodger <goodger@python.org>; David Priest
3 # Copyright: This module has been placed in the public domain.
5 """
6 Directives for table elements.
7 """
9 __docformat__ = 'reStructuredText'
12 import sys
13 import os.path
14 import csv
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):
25 """
26 Generic table base class.
27 """
29 optional_arguments = 1
30 final_argument_whitespace = True
31 option_spec = {'class': directives.class_option,
32 'name': directives.unchanged,
33 'widths': directives.value_or(('auto', 'grid'),
34 directives.positive_int_list)}
35 has_content = True
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
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 'header' in self.options: # 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
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)
90 @property
91 def widths(self):
92 return self.options.get('widths', 'auto')
94 def get_column_widths(self, max_cols):
95 if type(self.widths) == list:
96 if len(self.widths) != max_cols:
97 error = self.state_machine.reporter.error(
98 '"%s" widths do not match the number of columns in table '
99 '(%s).' % (self.name, max_cols), nodes.literal_block(
100 self.block_text, self.block_text), line=self.lineno)
101 raise SystemMessagePropagation(error)
102 col_widths = self.widths
103 elif max_cols:
104 col_widths = [100 // max_cols] * max_cols
105 else:
106 error = self.state_machine.reporter.error(
107 'No table data detected in CSV file.', nodes.literal_block(
108 self.block_text, self.block_text), line=self.lineno)
109 raise SystemMessagePropagation(error)
110 if self.widths == 'auto':
111 widths = 'auto'
112 else:
113 widths = 'given'
114 return widths, col_widths
116 def extend_short_rows_with_empty_cells(self, columns, parts):
117 for part in parts:
118 for row in part:
119 if len(row) < columns:
120 row.extend([(0, 0, 0, [])] * (columns - len(row)))
123 class RSTTable(Table):
125 def run(self):
126 if not self.content:
127 warning = self.state_machine.reporter.warning(
128 'Content block expected for the "%s" directive; none found.'
129 % self.name, nodes.literal_block(
130 self.block_text, self.block_text), line=self.lineno)
131 return [warning]
132 title, messages = self.make_title()
133 node = nodes.Element() # anonymous container for parsing
134 self.state.nested_parse(self.content, self.content_offset, node)
135 if len(node) != 1 or not isinstance(node[0], nodes.table):
136 error = self.state_machine.reporter.error(
137 'Error parsing content block for the "%s" directive: exactly '
138 'one table expected.' % self.name, nodes.literal_block(
139 self.block_text, self.block_text), line=self.lineno)
140 return [error]
141 table_node = node[0]
142 table_node['classes'] += self.options.get('class', [])
143 tgroup = table_node[0]
144 if type(self.widths) == list:
145 colspecs = [child for child in tgroup.children
146 if child.tagname == 'colspec']
147 for colspec, col_width in zip(colspecs, self.widths):
148 colspec['colwidth'] = col_width
149 if self.widths == 'auto':
150 tgroup['colwidths'] = 'auto'
151 else:
152 tgroup['colwidths'] = 'given'
153 self.add_name(table_node)
154 if title:
155 table_node.insert(0, title)
156 return [table_node] + messages
159 class CSVTable(Table):
161 option_spec = {'header-rows': directives.nonnegative_int,
162 'stub-columns': directives.nonnegative_int,
163 'header': directives.unchanged,
164 'widths': directives.value_or(('auto', ),
165 directives.positive_int_list),
166 'file': directives.path,
167 'url': directives.uri,
168 'encoding': directives.encoding,
169 'class': directives.class_option,
170 'name': directives.unchanged,
171 # field delimiter char
172 'delim': directives.single_char_or_whitespace_or_unicode,
173 # treat whitespace after delimiter as significant
174 'keepspace': directives.flag,
175 # text field quote/unquote char:
176 'quote': directives.single_char_or_unicode,
177 # char used to escape delim & quote as-needed:
178 'escape': directives.single_char_or_unicode,}
180 class DocutilsDialect(csv.Dialect):
182 """CSV dialect for `csv_table` directive."""
184 delimiter = ','
185 quotechar = '"'
186 doublequote = True
187 skipinitialspace = True
188 strict = True
189 lineterminator = '\n'
190 quoting = csv.QUOTE_MINIMAL
192 def __init__(self, options):
193 if 'delim' in options:
194 self.delimiter = CSVTable.encode_for_csv(options['delim'])
195 if 'keepspace' in options:
196 self.skipinitialspace = False
197 if 'quote' in options:
198 self.quotechar = CSVTable.encode_for_csv(options['quote'])
199 if 'escape' in options:
200 self.doublequote = False
201 self.escapechar = CSVTable.encode_for_csv(options['escape'])
202 csv.Dialect.__init__(self)
205 class HeaderDialect(csv.Dialect):
207 """CSV dialect to use for the "header" option data."""
209 delimiter = ','
210 quotechar = '"'
211 escapechar = '\\'
212 doublequote = False
213 skipinitialspace = True
214 strict = True
215 lineterminator = '\n'
216 quoting = csv.QUOTE_MINIMAL
218 def check_requirements(self):
219 pass
221 def run(self):
222 try:
223 if (not self.state.document.settings.file_insertion_enabled
224 and ('file' in self.options
225 or 'url' in self.options)):
226 warning = self.state_machine.reporter.warning(
227 'File and URL access deactivated; ignoring "%s" '
228 'directive.' % self.name, nodes.literal_block(
229 self.block_text, self.block_text), line=self.lineno)
230 return [warning]
231 self.check_requirements()
232 title, messages = self.make_title()
233 csv_data, source = self.get_csv_data()
234 table_head, max_header_cols = self.process_header_option()
235 rows, max_cols = self.parse_csv_data_into_rows(
236 csv_data, self.DocutilsDialect(self.options), source)
237 max_cols = max(max_cols, max_header_cols)
238 header_rows = self.options.get('header-rows', 0)
239 stub_columns = self.options.get('stub-columns', 0)
240 self.check_table_dimensions(rows, header_rows, stub_columns)
241 table_head.extend(rows[:header_rows])
242 table_body = rows[header_rows:]
243 widths, col_widths = self.get_column_widths(max_cols)
244 self.extend_short_rows_with_empty_cells(max_cols,
245 (table_head, table_body))
246 except SystemMessagePropagation, detail:
247 return [detail.args[0]]
248 except csv.Error, detail:
249 message = str(detail)
250 if sys.version_info < (3,) and '1-character string' in message:
251 message += '\nwith Python 2.x this must be an ASCII character.'
252 error = self.state_machine.reporter.error(
253 'Error with CSV data in "%s" directive:\n%s'
254 % (self.name, message), nodes.literal_block(
255 self.block_text, self.block_text), line=self.lineno)
256 return [error]
257 table = (col_widths, table_head, table_body)
258 table_node = self.state.build_table(table, self.content_offset,
259 stub_columns, widths=widths)
260 table_node['classes'] += self.options.get('class', [])
261 self.add_name(table_node)
262 if title:
263 table_node.insert(0, title)
264 return [table_node] + messages
266 def get_csv_data(self):
268 Get CSV data from the directive content, from an external
269 file, or from a URL reference.
271 encoding = self.options.get(
272 'encoding', self.state.document.settings.input_encoding)
273 error_handler = self.state.document.settings.input_encoding_error_handler
274 if self.content:
275 # CSV data is from directive content.
276 if 'file' in self.options or 'url' in self.options:
277 error = self.state_machine.reporter.error(
278 '"%s" directive may not both specify an external file and'
279 ' have content.' % self.name, nodes.literal_block(
280 self.block_text, self.block_text), line=self.lineno)
281 raise SystemMessagePropagation(error)
282 source = self.content.source(0)
283 csv_data = self.content
284 elif 'file' in self.options:
285 # CSV data is from an external file.
286 if 'url' in self.options:
287 error = self.state_machine.reporter.error(
288 'The "file" and "url" options may not be simultaneously'
289 ' specified for the "%s" directive.' % self.name,
290 nodes.literal_block(self.block_text, self.block_text),
291 line=self.lineno)
292 raise SystemMessagePropagation(error)
293 source_dir = os.path.dirname(
294 os.path.abspath(self.state.document.current_source))
295 source = os.path.normpath(os.path.join(source_dir,
296 self.options['file']))
297 source = utils.relative_path(None, source)
298 try:
299 self.state.document.settings.record_dependencies.add(source)
300 csv_file = io.FileInput(source_path=source,
301 encoding=encoding,
302 error_handler=error_handler)
303 csv_data = csv_file.read().splitlines()
304 except IOError, error:
305 severe = self.state_machine.reporter.severe(
306 u'Problems with "%s" directive path:\n%s.'
307 % (self.name, SafeString(error)),
308 nodes.literal_block(self.block_text, self.block_text),
309 line=self.lineno)
310 raise SystemMessagePropagation(severe)
311 elif 'url' in self.options:
312 # CSV data is from a URL.
313 # Do not import urllib2 at the top of the module because
314 # it may fail due to broken SSL dependencies, and it takes
315 # about 0.15 seconds to load.
316 import urllib2
317 source = self.options['url']
318 try:
319 csv_text = urllib2.urlopen(source).read()
320 except (urllib2.URLError, IOError, OSError, ValueError), error:
321 severe = self.state_machine.reporter.severe(
322 'Problems with "%s" directive URL "%s":\n%s.'
323 % (self.name, self.options['url'], SafeString(error)),
324 nodes.literal_block(self.block_text, self.block_text),
325 line=self.lineno)
326 raise SystemMessagePropagation(severe)
327 csv_file = io.StringInput(
328 source=csv_text, source_path=source, encoding=encoding,
329 error_handler=(self.state.document.settings.\
330 input_encoding_error_handler))
331 csv_data = csv_file.read().splitlines()
332 else:
333 error = self.state_machine.reporter.warning(
334 'The "%s" directive requires content; none supplied.'
335 % self.name, nodes.literal_block(
336 self.block_text, self.block_text), line=self.lineno)
337 raise SystemMessagePropagation(error)
338 return csv_data, source
340 if sys.version_info < (3,):
341 # 2.x csv module doesn't do Unicode
342 def decode_from_csv(s):
343 return s.decode('utf-8')
344 def encode_for_csv(s):
345 return s.encode('utf-8')
346 else:
347 def decode_from_csv(s):
348 return s
349 def encode_for_csv(s):
350 return s
351 decode_from_csv = staticmethod(decode_from_csv)
352 encode_for_csv = staticmethod(encode_for_csv)
354 def parse_csv_data_into_rows(self, csv_data, dialect, source):
355 # csv.py doesn't do Unicode; encode temporarily as UTF-8
356 csv_reader = csv.reader([self.encode_for_csv(line + '\n')
357 for line in csv_data],
358 dialect=dialect)
359 rows = []
360 max_cols = 0
361 for row in csv_reader:
362 row_data = []
363 for cell in row:
364 # decode UTF-8 back to Unicode
365 cell_text = self.decode_from_csv(cell)
366 cell_data = (0, 0, 0, statemachine.StringList(
367 cell_text.splitlines(), source=source))
368 row_data.append(cell_data)
369 rows.append(row_data)
370 max_cols = max(max_cols, len(row))
371 return rows, max_cols
374 class ListTable(Table):
377 Implement tables whose data is encoded as a uniform two-level bullet list.
378 For further ideas, see
379 http://docutils.sf.net/docs/dev/rst/alternatives.html#list-driven-tables
382 option_spec = {'header-rows': directives.nonnegative_int,
383 'stub-columns': directives.nonnegative_int,
384 'widths': directives.value_or(('auto', ),
385 directives.positive_int_list),
386 'class': directives.class_option,
387 'name': directives.unchanged}
389 def run(self):
390 if not self.content:
391 error = self.state_machine.reporter.error(
392 'The "%s" directive is empty; content required.' % self.name,
393 nodes.literal_block(self.block_text, self.block_text),
394 line=self.lineno)
395 return [error]
396 title, messages = self.make_title()
397 node = nodes.Element() # anonymous container for parsing
398 self.state.nested_parse(self.content, self.content_offset, node)
399 try:
400 num_cols, widths, col_widths = self.check_list_content(node)
401 table_data = [[item.children for item in row_list[0]]
402 for row_list in node[0]]
403 header_rows = self.options.get('header-rows', 0)
404 stub_columns = self.options.get('stub-columns', 0)
405 self.check_table_dimensions(table_data, header_rows, stub_columns)
406 except SystemMessagePropagation, detail:
407 return [detail.args[0]]
408 table_node = self.build_table_from_list(table_data, widths, col_widths,
409 header_rows, stub_columns)
410 table_node['classes'] += self.options.get('class', [])
411 self.add_name(table_node)
412 if title:
413 table_node.insert(0, title)
414 return [table_node] + messages
416 def check_list_content(self, node):
417 if len(node) != 1 or not isinstance(node[0], nodes.bullet_list):
418 error = self.state_machine.reporter.error(
419 'Error parsing content block for the "%s" directive: '
420 'exactly one bullet list expected.' % self.name,
421 nodes.literal_block(self.block_text, self.block_text),
422 line=self.lineno)
423 raise SystemMessagePropagation(error)
424 list_node = node[0]
425 # Check for a uniform two-level bullet list:
426 for item_index in range(len(list_node)):
427 item = list_node[item_index]
428 if len(item) != 1 or not isinstance(item[0], nodes.bullet_list):
429 error = self.state_machine.reporter.error(
430 'Error parsing content block for the "%s" directive: '
431 'two-level bullet list expected, but row %s does not '
432 'contain a second-level bullet list.'
433 % (self.name, item_index + 1), nodes.literal_block(
434 self.block_text, self.block_text), line=self.lineno)
435 raise SystemMessagePropagation(error)
436 elif item_index:
437 # ATTN pychecker users: num_cols is guaranteed to be set in the
438 # "else" clause below for item_index==0, before this branch is
439 # triggered.
440 if len(item[0]) != num_cols:
441 error = self.state_machine.reporter.error(
442 'Error parsing content block for the "%s" directive: '
443 'uniform two-level bullet list expected, but row %s '
444 'does not contain the same number of items as row 1 '
445 '(%s vs %s).'
446 % (self.name, item_index + 1, len(item[0]), num_cols),
447 nodes.literal_block(self.block_text, self.block_text),
448 line=self.lineno)
449 raise SystemMessagePropagation(error)
450 else:
451 num_cols = len(item[0])
452 widths, col_widths = self.get_column_widths(num_cols)
453 return num_cols, widths, col_widths
455 def build_table_from_list(self, table_data, widths, col_widths, header_rows,
456 stub_columns):
457 table = nodes.table()
458 tgroup = nodes.tgroup(cols=len(col_widths), colwidths=widths)
459 table += tgroup
460 for col_width in col_widths:
461 colspec = nodes.colspec()
462 if col_width is not None:
463 colspec.attributes['colwidth'] = col_width
464 if stub_columns:
465 colspec.attributes['stub'] = 1
466 stub_columns -= 1
467 tgroup += colspec
468 rows = []
469 for row in table_data:
470 row_node = nodes.row()
471 for cell in row:
472 entry = nodes.entry()
473 entry += cell
474 row_node += entry
475 rows.append(row_node)
476 if header_rows:
477 thead = nodes.thead()
478 thead.extend(rows[:header_rows])
479 tgroup += thead
480 tbody = nodes.tbody()
481 tbody.extend(rows[header_rows:])
482 tgroup += tbody
483 return table