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
,
33 'widths': directives
.value_or(('auto', 'grid'),
34 directives
.positive_int_list
)}
39 title_text
= self
.arguments
[0]
40 text_nodes
, messages
= self
.state
.inline_text(title_text
,
42 title
= nodes
.title(title_text
, '', *text_nodes
)
46 return title
, messages
48 def process_header_option(self
):
49 source
= self
.state_machine
.get_source(self
.lineno
- 1)
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(),
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
)
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
)
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
104 col_widths
= [100 // max_cols
] * max_cols
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':
114 return widths
, col_widths
116 def extend_short_rows_with_empty_cells(self
, columns
, parts
):
119 if len(row
) < columns
:
120 row
.extend([(0, 0, 0, [])] * (columns
- len(row
)))
123 class RSTTable(Table
):
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
)
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
)
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'
152 tgroup
['colwidths'] = 'given'
153 self
.add_name(table_node
)
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."""
187 skipinitialspace
= 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."""
213 skipinitialspace
= True
215 lineterminator
= '\n'
216 quoting
= csv
.QUOTE_MINIMAL
218 def check_requirements(self
):
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
)
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
)
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
)
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
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
),
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
)
299 self
.state
.document
.settings
.record_dependencies
.add(source
)
300 csv_file
= io
.FileInput(source_path
=source
,
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
),
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.
317 source
= self
.options
['url']
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
),
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()
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')
347 def decode_from_csv(s
):
349 def encode_for_csv(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
],
361 for row
in csv_reader
:
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
}
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
),
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
)
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
)
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
),
423 raise SystemMessagePropagation(error
)
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
)
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
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 '
446 % (self
.name
, item_index
+ 1, len(item
[0]), num_cols
),
447 nodes
.literal_block(self
.block_text
, self
.block_text
),
449 raise SystemMessagePropagation(error
)
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
,
457 table
= nodes
.table()
458 tgroup
= nodes
.tgroup(cols
=len(col_widths
), colwidths
=widths
)
460 for col_width
in col_widths
:
461 colspec
= nodes
.colspec()
462 if col_width
is not None:
463 colspec
.attributes
['colwidth'] = col_width
465 colspec
.attributes
['stub'] = 1
469 for row
in table_data
:
470 row_node
= nodes
.row()
472 entry
= nodes
.entry()
475 rows
.append(row_node
)
477 thead
= nodes
.thead()
478 thead
.extend(rows
[:header_rows
])
480 tbody
= nodes
.tbody()
481 tbody
.extend(rows
[header_rows
:])