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
:])