Mercurial > repos > guerler > springsuite
comparison planemo/lib/python3.7/site-packages/docutils/parsers/rst/directives/tables.py @ 0:d30785e31577 draft
"planemo upload commit 6eee67778febed82ddd413c3ca40b3183a3898f1"
author | guerler |
---|---|
date | Fri, 31 Jul 2020 00:18:57 -0400 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:d30785e31577 |
---|---|
1 # $Id: tables.py 8377 2019-08-27 19:49:45Z 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 def align(argument): | |
24 return directives.choice(argument, ('left', 'center', 'right')) | |
25 | |
26 | |
27 class Table(Directive): | |
28 | |
29 """ | |
30 Generic table base class. | |
31 """ | |
32 | |
33 optional_arguments = 1 | |
34 final_argument_whitespace = True | |
35 option_spec = {'class': directives.class_option, | |
36 'name': directives.unchanged, | |
37 'align': align, | |
38 'width': directives.length_or_percentage_or_unitless, | |
39 'widths': directives.value_or(('auto', 'grid'), | |
40 directives.positive_int_list)} | |
41 has_content = True | |
42 | |
43 def make_title(self): | |
44 if self.arguments: | |
45 title_text = self.arguments[0] | |
46 text_nodes, messages = self.state.inline_text(title_text, | |
47 self.lineno) | |
48 title = nodes.title(title_text, '', *text_nodes) | |
49 (title.source, | |
50 title.line) = self.state_machine.get_source_and_line(self.lineno) | |
51 else: | |
52 title = None | |
53 messages = [] | |
54 return title, messages | |
55 | |
56 def process_header_option(self): | |
57 source = self.state_machine.get_source(self.lineno - 1) | |
58 table_head = [] | |
59 max_header_cols = 0 | |
60 if 'header' in self.options: # separate table header in option | |
61 rows, max_header_cols = self.parse_csv_data_into_rows( | |
62 self.options['header'].split('\n'), self.HeaderDialect(), | |
63 source) | |
64 table_head.extend(rows) | |
65 return table_head, max_header_cols | |
66 | |
67 def check_table_dimensions(self, rows, header_rows, stub_columns): | |
68 if len(rows) < header_rows: | |
69 error = self.state_machine.reporter.error( | |
70 '%s header row(s) specified but only %s row(s) of data ' | |
71 'supplied ("%s" directive).' | |
72 % (header_rows, len(rows), self.name), nodes.literal_block( | |
73 self.block_text, self.block_text), line=self.lineno) | |
74 raise SystemMessagePropagation(error) | |
75 if len(rows) == header_rows > 0: | |
76 error = self.state_machine.reporter.error( | |
77 'Insufficient data supplied (%s row(s)); no data remaining ' | |
78 'for table body, required by "%s" directive.' | |
79 % (len(rows), self.name), nodes.literal_block( | |
80 self.block_text, self.block_text), line=self.lineno) | |
81 raise SystemMessagePropagation(error) | |
82 for row in rows: | |
83 if len(row) < stub_columns: | |
84 error = self.state_machine.reporter.error( | |
85 '%s stub column(s) specified but only %s columns(s) of ' | |
86 'data supplied ("%s" directive).' % | |
87 (stub_columns, len(row), self.name), nodes.literal_block( | |
88 self.block_text, self.block_text), line=self.lineno) | |
89 raise SystemMessagePropagation(error) | |
90 if len(row) == stub_columns > 0: | |
91 error = self.state_machine.reporter.error( | |
92 'Insufficient data supplied (%s columns(s)); no data remaining ' | |
93 'for table body, required by "%s" directive.' | |
94 % (len(row), self.name), nodes.literal_block( | |
95 self.block_text, self.block_text), line=self.lineno) | |
96 raise SystemMessagePropagation(error) | |
97 | |
98 def set_table_width(self, table_node): | |
99 if 'width' in self.options: | |
100 table_node['width'] = self.options.get('width') | |
101 | |
102 @property | |
103 def widths(self): | |
104 return self.options.get('widths', '') | |
105 | |
106 def get_column_widths(self, max_cols): | |
107 if isinstance(self.widths, list): | |
108 if len(self.widths) != max_cols: | |
109 error = self.state_machine.reporter.error( | |
110 '"%s" widths do not match the number of columns in table ' | |
111 '(%s).' % (self.name, max_cols), nodes.literal_block( | |
112 self.block_text, self.block_text), line=self.lineno) | |
113 raise SystemMessagePropagation(error) | |
114 col_widths = self.widths | |
115 elif max_cols: | |
116 col_widths = [100 // max_cols] * max_cols | |
117 else: | |
118 error = self.state_machine.reporter.error( | |
119 'No table data detected in CSV file.', nodes.literal_block( | |
120 self.block_text, self.block_text), line=self.lineno) | |
121 raise SystemMessagePropagation(error) | |
122 return col_widths | |
123 | |
124 def extend_short_rows_with_empty_cells(self, columns, parts): | |
125 for part in parts: | |
126 for row in part: | |
127 if len(row) < columns: | |
128 row.extend([(0, 0, 0, [])] * (columns - len(row))) | |
129 | |
130 | |
131 class RSTTable(Table): | |
132 | |
133 def run(self): | |
134 if not self.content: | |
135 warning = self.state_machine.reporter.warning( | |
136 'Content block expected for the "%s" directive; none found.' | |
137 % self.name, nodes.literal_block( | |
138 self.block_text, self.block_text), line=self.lineno) | |
139 return [warning] | |
140 title, messages = self.make_title() | |
141 node = nodes.Element() # anonymous container for parsing | |
142 self.state.nested_parse(self.content, self.content_offset, node) | |
143 if len(node) != 1 or not isinstance(node[0], nodes.table): | |
144 error = self.state_machine.reporter.error( | |
145 'Error parsing content block for the "%s" directive: exactly ' | |
146 'one table expected.' % self.name, nodes.literal_block( | |
147 self.block_text, self.block_text), line=self.lineno) | |
148 return [error] | |
149 table_node = node[0] | |
150 table_node['classes'] += self.options.get('class', []) | |
151 self.set_table_width(table_node) | |
152 if 'align' in self.options: | |
153 table_node['align'] = self.options.get('align') | |
154 tgroup = table_node[0] | |
155 if isinstance(self.widths, list): | |
156 colspecs = [child for child in tgroup.children | |
157 if child.tagname == 'colspec'] | |
158 for colspec, col_width in zip(colspecs, self.widths): | |
159 colspec['colwidth'] = col_width | |
160 # @@@ the colwidths argument for <tgroup> is not part of the | |
161 # XML Exchange Table spec (https://www.oasis-open.org/specs/tm9901.htm) | |
162 # and hence violates the docutils.dtd. | |
163 if self.widths == 'auto': | |
164 table_node['classes'] += ['colwidths-auto'] | |
165 elif self.widths: # "grid" or list of integers | |
166 table_node['classes'] += ['colwidths-given'] | |
167 self.add_name(table_node) | |
168 if title: | |
169 table_node.insert(0, title) | |
170 return [table_node] + messages | |
171 | |
172 | |
173 class CSVTable(Table): | |
174 | |
175 option_spec = {'header-rows': directives.nonnegative_int, | |
176 'stub-columns': directives.nonnegative_int, | |
177 'header': directives.unchanged, | |
178 'width': directives.length_or_percentage_or_unitless, | |
179 'widths': directives.value_or(('auto', ), | |
180 directives.positive_int_list), | |
181 'file': directives.path, | |
182 'url': directives.uri, | |
183 'encoding': directives.encoding, | |
184 'class': directives.class_option, | |
185 'name': directives.unchanged, | |
186 'align': align, | |
187 # field delimiter char | |
188 'delim': directives.single_char_or_whitespace_or_unicode, | |
189 # treat whitespace after delimiter as significant | |
190 'keepspace': directives.flag, | |
191 # text field quote/unquote char: | |
192 'quote': directives.single_char_or_unicode, | |
193 # char used to escape delim & quote as-needed: | |
194 'escape': directives.single_char_or_unicode,} | |
195 | |
196 class DocutilsDialect(csv.Dialect): | |
197 | |
198 """CSV dialect for `csv_table` directive.""" | |
199 | |
200 delimiter = ',' | |
201 quotechar = '"' | |
202 doublequote = True | |
203 skipinitialspace = True | |
204 strict = True | |
205 lineterminator = '\n' | |
206 quoting = csv.QUOTE_MINIMAL | |
207 | |
208 def __init__(self, options): | |
209 if 'delim' in options: | |
210 self.delimiter = CSVTable.encode_for_csv(options['delim']) | |
211 if 'keepspace' in options: | |
212 self.skipinitialspace = False | |
213 if 'quote' in options: | |
214 self.quotechar = CSVTable.encode_for_csv(options['quote']) | |
215 if 'escape' in options: | |
216 self.doublequote = False | |
217 self.escapechar = CSVTable.encode_for_csv(options['escape']) | |
218 csv.Dialect.__init__(self) | |
219 | |
220 | |
221 class HeaderDialect(csv.Dialect): | |
222 | |
223 """CSV dialect to use for the "header" option data.""" | |
224 | |
225 delimiter = ',' | |
226 quotechar = '"' | |
227 escapechar = '\\' | |
228 doublequote = False | |
229 skipinitialspace = True | |
230 strict = True | |
231 lineterminator = '\n' | |
232 quoting = csv.QUOTE_MINIMAL | |
233 | |
234 def check_requirements(self): | |
235 pass | |
236 | |
237 def run(self): | |
238 try: | |
239 if (not self.state.document.settings.file_insertion_enabled | |
240 and ('file' in self.options | |
241 or 'url' in self.options)): | |
242 warning = self.state_machine.reporter.warning( | |
243 'File and URL access deactivated; ignoring "%s" ' | |
244 'directive.' % self.name, nodes.literal_block( | |
245 self.block_text, self.block_text), line=self.lineno) | |
246 return [warning] | |
247 self.check_requirements() | |
248 title, messages = self.make_title() | |
249 csv_data, source = self.get_csv_data() | |
250 table_head, max_header_cols = self.process_header_option() | |
251 rows, max_cols = self.parse_csv_data_into_rows( | |
252 csv_data, self.DocutilsDialect(self.options), source) | |
253 max_cols = max(max_cols, max_header_cols) | |
254 header_rows = self.options.get('header-rows', 0) | |
255 stub_columns = self.options.get('stub-columns', 0) | |
256 self.check_table_dimensions(rows, header_rows, stub_columns) | |
257 table_head.extend(rows[:header_rows]) | |
258 table_body = rows[header_rows:] | |
259 col_widths = self.get_column_widths(max_cols) | |
260 self.extend_short_rows_with_empty_cells(max_cols, | |
261 (table_head, table_body)) | |
262 except SystemMessagePropagation as detail: | |
263 return [detail.args[0]] | |
264 except csv.Error as detail: | |
265 message = str(detail) | |
266 if sys.version_info < (3, 0) and '1-character string' in message: | |
267 message += '\nwith Python 2.x this must be an ASCII character.' | |
268 error = self.state_machine.reporter.error( | |
269 'Error with CSV data in "%s" directive:\n%s' | |
270 % (self.name, message), nodes.literal_block( | |
271 self.block_text, self.block_text), line=self.lineno) | |
272 return [error] | |
273 table = (col_widths, table_head, table_body) | |
274 table_node = self.state.build_table(table, self.content_offset, | |
275 stub_columns, widths=self.widths) | |
276 table_node['classes'] += self.options.get('class', []) | |
277 if 'align' in self.options: | |
278 table_node['align'] = self.options.get('align') | |
279 self.set_table_width(table_node) | |
280 self.add_name(table_node) | |
281 if title: | |
282 table_node.insert(0, title) | |
283 return [table_node] + messages | |
284 | |
285 def get_csv_data(self): | |
286 """ | |
287 Get CSV data from the directive content, from an external | |
288 file, or from a URL reference. | |
289 """ | |
290 encoding = self.options.get( | |
291 'encoding', self.state.document.settings.input_encoding) | |
292 error_handler = self.state.document.settings.input_encoding_error_handler | |
293 if self.content: | |
294 # CSV data is from directive content. | |
295 if 'file' in self.options or 'url' in self.options: | |
296 error = self.state_machine.reporter.error( | |
297 '"%s" directive may not both specify an external file and' | |
298 ' have content.' % self.name, nodes.literal_block( | |
299 self.block_text, self.block_text), line=self.lineno) | |
300 raise SystemMessagePropagation(error) | |
301 source = self.content.source(0) | |
302 csv_data = self.content | |
303 elif 'file' in self.options: | |
304 # CSV data is from an external file. | |
305 if 'url' in self.options: | |
306 error = self.state_machine.reporter.error( | |
307 'The "file" and "url" options may not be simultaneously' | |
308 ' specified for the "%s" directive.' % self.name, | |
309 nodes.literal_block(self.block_text, self.block_text), | |
310 line=self.lineno) | |
311 raise SystemMessagePropagation(error) | |
312 source_dir = os.path.dirname( | |
313 os.path.abspath(self.state.document.current_source)) | |
314 source = os.path.normpath(os.path.join(source_dir, | |
315 self.options['file'])) | |
316 source = utils.relative_path(None, source) | |
317 try: | |
318 self.state.document.settings.record_dependencies.add(source) | |
319 csv_file = io.FileInput(source_path=source, | |
320 encoding=encoding, | |
321 error_handler=error_handler) | |
322 csv_data = csv_file.read().splitlines() | |
323 except IOError as error: | |
324 severe = self.state_machine.reporter.severe( | |
325 u'Problems with "%s" directive path:\n%s.' | |
326 % (self.name, SafeString(error)), | |
327 nodes.literal_block(self.block_text, self.block_text), | |
328 line=self.lineno) | |
329 raise SystemMessagePropagation(severe) | |
330 elif 'url' in self.options: | |
331 # CSV data is from a URL. | |
332 # Do not import urllib2 at the top of the module because | |
333 # it may fail due to broken SSL dependencies, and it takes | |
334 # about 0.15 seconds to load. | |
335 if sys.version_info >= (3, 0): | |
336 from urllib.request import urlopen | |
337 from urllib.error import URLError | |
338 else: | |
339 from urllib2 import urlopen, URLError | |
340 | |
341 source = self.options['url'] | |
342 try: | |
343 csv_text = urlopen(source).read() | |
344 except (URLError, IOError, OSError, ValueError) as error: | |
345 severe = self.state_machine.reporter.severe( | |
346 'Problems with "%s" directive URL "%s":\n%s.' | |
347 % (self.name, self.options['url'], SafeString(error)), | |
348 nodes.literal_block(self.block_text, self.block_text), | |
349 line=self.lineno) | |
350 raise SystemMessagePropagation(severe) | |
351 csv_file = io.StringInput( | |
352 source=csv_text, source_path=source, encoding=encoding, | |
353 error_handler=(self.state.document.settings.\ | |
354 input_encoding_error_handler)) | |
355 csv_data = csv_file.read().splitlines() | |
356 else: | |
357 error = self.state_machine.reporter.warning( | |
358 'The "%s" directive requires content; none supplied.' | |
359 % self.name, nodes.literal_block( | |
360 self.block_text, self.block_text), line=self.lineno) | |
361 raise SystemMessagePropagation(error) | |
362 return csv_data, source | |
363 | |
364 if sys.version_info < (3, 0): | |
365 # 2.x csv module doesn't do Unicode | |
366 def decode_from_csv(s): | |
367 return s.decode('utf-8') | |
368 def encode_for_csv(s): | |
369 return s.encode('utf-8') | |
370 else: | |
371 def decode_from_csv(s): | |
372 return s | |
373 def encode_for_csv(s): | |
374 return s | |
375 decode_from_csv = staticmethod(decode_from_csv) | |
376 encode_for_csv = staticmethod(encode_for_csv) | |
377 | |
378 def parse_csv_data_into_rows(self, csv_data, dialect, source): | |
379 # csv.py doesn't do Unicode; encode temporarily as UTF-8 | |
380 csv_reader = csv.reader([self.encode_for_csv(line + '\n') | |
381 for line in csv_data], | |
382 dialect=dialect) | |
383 rows = [] | |
384 max_cols = 0 | |
385 for row in csv_reader: | |
386 row_data = [] | |
387 for cell in row: | |
388 # decode UTF-8 back to Unicode | |
389 cell_text = self.decode_from_csv(cell) | |
390 cell_data = (0, 0, 0, statemachine.StringList( | |
391 cell_text.splitlines(), source=source)) | |
392 row_data.append(cell_data) | |
393 rows.append(row_data) | |
394 max_cols = max(max_cols, len(row)) | |
395 return rows, max_cols | |
396 | |
397 | |
398 class ListTable(Table): | |
399 | |
400 """ | |
401 Implement tables whose data is encoded as a uniform two-level bullet list. | |
402 For further ideas, see | |
403 http://docutils.sf.net/docs/dev/rst/alternatives.html#list-driven-tables | |
404 """ | |
405 | |
406 option_spec = {'header-rows': directives.nonnegative_int, | |
407 'stub-columns': directives.nonnegative_int, | |
408 'width': directives.length_or_percentage_or_unitless, | |
409 'widths': directives.value_or(('auto', ), | |
410 directives.positive_int_list), | |
411 'class': directives.class_option, | |
412 'name': directives.unchanged, | |
413 'align': align} | |
414 | |
415 def run(self): | |
416 if not self.content: | |
417 error = self.state_machine.reporter.error( | |
418 'The "%s" directive is empty; content required.' % self.name, | |
419 nodes.literal_block(self.block_text, self.block_text), | |
420 line=self.lineno) | |
421 return [error] | |
422 title, messages = self.make_title() | |
423 node = nodes.Element() # anonymous container for parsing | |
424 self.state.nested_parse(self.content, self.content_offset, node) | |
425 try: | |
426 num_cols, col_widths = self.check_list_content(node) | |
427 table_data = [[item.children for item in row_list[0]] | |
428 for row_list in node[0]] | |
429 header_rows = self.options.get('header-rows', 0) | |
430 stub_columns = self.options.get('stub-columns', 0) | |
431 self.check_table_dimensions(table_data, header_rows, stub_columns) | |
432 except SystemMessagePropagation as detail: | |
433 return [detail.args[0]] | |
434 table_node = self.build_table_from_list(table_data, col_widths, | |
435 header_rows, stub_columns) | |
436 if 'align' in self.options: | |
437 table_node['align'] = self.options.get('align') | |
438 table_node['classes'] += self.options.get('class', []) | |
439 self.set_table_width(table_node) | |
440 self.add_name(table_node) | |
441 if title: | |
442 table_node.insert(0, title) | |
443 return [table_node] + messages | |
444 | |
445 def check_list_content(self, node): | |
446 if len(node) != 1 or not isinstance(node[0], nodes.bullet_list): | |
447 error = self.state_machine.reporter.error( | |
448 'Error parsing content block for the "%s" directive: ' | |
449 'exactly one bullet list expected.' % self.name, | |
450 nodes.literal_block(self.block_text, self.block_text), | |
451 line=self.lineno) | |
452 raise SystemMessagePropagation(error) | |
453 list_node = node[0] | |
454 num_cols = 0 | |
455 # Check for a uniform two-level bullet list: | |
456 for item_index in range(len(list_node)): | |
457 item = list_node[item_index] | |
458 if len(item) != 1 or not isinstance(item[0], nodes.bullet_list): | |
459 error = self.state_machine.reporter.error( | |
460 'Error parsing content block for the "%s" directive: ' | |
461 'two-level bullet list expected, but row %s does not ' | |
462 'contain a second-level bullet list.' | |
463 % (self.name, item_index + 1), nodes.literal_block( | |
464 self.block_text, self.block_text), line=self.lineno) | |
465 raise SystemMessagePropagation(error) | |
466 elif item_index: | |
467 if len(item[0]) != num_cols: | |
468 error = self.state_machine.reporter.error( | |
469 'Error parsing content block for the "%s" directive: ' | |
470 'uniform two-level bullet list expected, but row %s ' | |
471 'does not contain the same number of items as row 1 ' | |
472 '(%s vs %s).' | |
473 % (self.name, item_index + 1, len(item[0]), num_cols), | |
474 nodes.literal_block(self.block_text, self.block_text), | |
475 line=self.lineno) | |
476 raise SystemMessagePropagation(error) | |
477 else: | |
478 num_cols = len(item[0]) | |
479 col_widths = self.get_column_widths(num_cols) | |
480 return num_cols, col_widths | |
481 | |
482 def build_table_from_list(self, table_data, col_widths, header_rows, stub_columns): | |
483 table = nodes.table() | |
484 if self.widths == 'auto': | |
485 table['classes'] += ['colwidths-auto'] | |
486 elif self.widths: # "grid" or list of integers | |
487 table['classes'] += ['colwidths-given'] | |
488 tgroup = nodes.tgroup(cols=len(col_widths)) | |
489 table += tgroup | |
490 for col_width in col_widths: | |
491 colspec = nodes.colspec() | |
492 if col_width is not None: | |
493 colspec.attributes['colwidth'] = col_width | |
494 if stub_columns: | |
495 colspec.attributes['stub'] = 1 | |
496 stub_columns -= 1 | |
497 tgroup += colspec | |
498 rows = [] | |
499 for row in table_data: | |
500 row_node = nodes.row() | |
501 for cell in row: | |
502 entry = nodes.entry() | |
503 entry += cell | |
504 row_node += entry | |
505 rows.append(row_node) | |
506 if header_rows: | |
507 thead = nodes.thead() | |
508 thead.extend(rows[:header_rows]) | |
509 tgroup += thead | |
510 tbody = nodes.tbody() | |
511 tbody.extend(rows[header_rows:]) | |
512 tgroup += tbody | |
513 return table |