class DialectTableDirective(SphinxDirective): has_content = True # from ListTable final_argument_whitespace = True optional_arguments = 1 option_spec = { "header-rows": directives.nonnegative_int, "class": directives.class_option, "name": directives.unchanged, "align": align, "width": directives.length_or_percentage_or_unitless, "widths": directives.value_or(("auto", "grid"), directives.positive_int_list), } def run(self): node = dialecttable("") # generate a placeholder table since in process_dialect_table # there seem to be no access to state and state_machine text = [ "* - Database", " - :term:`Fully tested in CI`", " - :term:`Normal support`", " - :term:`Best effort`", # Mock row. Will be replaced in process_dialect_table "* - **placeholder**", " - placeholder", " - placeholder", " - placeholder", ] self.options["header-rows"] = 1 list_table = ListTable( name="list-table", arguments=self.arguments, options=self.options, content=StringList(text), lineno=self.lineno, content_offset=self.content_offset, block_text="", state=self.state, state_machine=self.state_machine, ) node.extend(list_table.run()) return [node]
class ListTable(Table): """ Implement tables whose data is encoded as a uniform two-level bullet list. For further ideas, see http://docutils.sf.net/docs/dev/rst/alternatives.html#list-driven-tables """ option_spec = { 'header-rows': directives.nonnegative_int, 'stub-columns': directives.nonnegative_int, 'width': directives.length_or_percentage_or_unitless, 'widths': directives.value_or(('auto', ), directives.positive_int_list), 'class': directives.class_option, 'name': directives.unchanged, 'align': align } def run(self): if not self.content: error = self.state_machine.reporter.error( 'The "%s" directive is empty; content required.' % self.name, nodes.literal_block(self.block_text, self.block_text), line=self.lineno) return [error] title, messages = self.make_title() node = nodes.Element() # anonymous container for parsing self.state.nested_parse(self.content, self.content_offset, node) try: num_cols, col_widths = self.check_list_content(node) table_data = [[item.children for item in row_list[0]] for row_list in node[0]] header_rows = self.options.get('header-rows', 0) stub_columns = self.options.get('stub-columns', 0) self.check_table_dimensions(table_data, header_rows, stub_columns) except SystemMessagePropagation, detail: return [detail.args[0]] table_node = self.build_table_from_list(table_data, col_widths, header_rows, stub_columns) if 'align' in self.options: table_node['align'] = self.options.get('align') table_node['classes'] += self.options.get('class', []) self.set_table_width(table_node) self.add_name(table_node) if title: table_node.insert(0, title) return [table_node] + messages
class ListTable(Table): """ Implement tables whose data is encoded as a uniform two-level bullet list. For further ideas, see http://docutils.sf.net/docs/dev/rst/alternatives.html#list-driven-tables """ option_spec = { 'header-rows': directives.nonnegative_int, 'stub-columns': directives.nonnegative_int, 'width': directives.length_or_percentage_or_unitless, 'widths': directives.value_or(('auto', ), directives.positive_int_list), 'class': directives.class_option, 'name': directives.unchanged, 'align': align } def run(self): if not self.content: error = self.state_machine.reporter.error( 'The "%s" directive is empty; content required.' % self.name, nodes.literal_block(self.block_text, self.block_text), line=self.lineno) return [error] title, messages = self.make_title() node = nodes.Element() # anonymous container for parsing self.state.nested_parse(self.content, self.content_offset, node) try: num_cols, col_widths = self.check_list_content(node) table_data = [[item.children for item in row_list[0]] for row_list in node[0]] header_rows = self.options.get('header-rows', 0) stub_columns = self.options.get('stub-columns', 0) self.check_table_dimensions(table_data, header_rows, stub_columns) except SystemMessagePropagation as detail: return [detail.args[0]] table_node = self.build_table_from_list(table_data, col_widths, header_rows, stub_columns) if 'align' in self.options: table_node['align'] = self.options.get('align') table_node['classes'] += self.options.get('class', []) self.set_table_width(table_node) self.add_name(table_node) if title: table_node.insert(0, title) return [table_node] + messages def check_list_content(self, node): if len(node) != 1 or not isinstance(node[0], nodes.bullet_list): error = self.state_machine.reporter.error( 'Error parsing content block for the "%s" directive: ' 'exactly one bullet list expected.' % self.name, nodes.literal_block(self.block_text, self.block_text), line=self.lineno) raise SystemMessagePropagation(error) list_node = node[0] # Check for a uniform two-level bullet list: for item_index in range(len(list_node)): item = list_node[item_index] if len(item) != 1 or not isinstance(item[0], nodes.bullet_list): error = self.state_machine.reporter.error( 'Error parsing content block for the "%s" directive: ' 'two-level bullet list expected, but row %s does not ' 'contain a second-level bullet list.' % (self.name, item_index + 1), nodes.literal_block(self.block_text, self.block_text), line=self.lineno) raise SystemMessagePropagation(error) elif item_index: # ATTN pychecker users: num_cols is guaranteed to be set in the # "else" clause below for item_index==0, before this branch is # triggered. if len(item[0]) != num_cols: error = self.state_machine.reporter.error( 'Error parsing content block for the "%s" directive: ' 'uniform two-level bullet list expected, but row %s ' 'does not contain the same number of items as row 1 ' '(%s vs %s).' % (self.name, item_index + 1, len(item[0]), num_cols), nodes.literal_block(self.block_text, self.block_text), line=self.lineno) raise SystemMessagePropagation(error) else: num_cols = len(item[0]) col_widths = self.get_column_widths(num_cols) return num_cols, col_widths def build_table_from_list(self, table_data, col_widths, header_rows, stub_columns): table = nodes.table() if self.widths == 'auto': table['classes'] += ['colwidths-auto'] elif self.widths: # "grid" or list of integers table['classes'] += ['colwidths-given'] tgroup = nodes.tgroup(cols=len(col_widths)) table += tgroup for col_width in col_widths: colspec = nodes.colspec() if col_width is not None: colspec.attributes['colwidth'] = col_width if stub_columns: colspec.attributes['stub'] = 1 stub_columns -= 1 tgroup += colspec rows = [] for row in table_data: row_node = nodes.row() for cell in row: entry = nodes.entry() entry += cell row_node += entry rows.append(row_node) if header_rows: thead = nodes.thead() thead.extend(rows[:header_rows]) tgroup += thead tbody = nodes.tbody() tbody.extend(rows[header_rows:]) tgroup += tbody return table
class Table(Directive): """ Generic table base class. """ optional_arguments = 1 final_argument_whitespace = True option_spec = { 'class': directives.class_option, 'name': directives.unchanged, 'align': align, 'width': directives.length_or_percentage_or_unitless, 'widths': directives.value_or(('auto', 'grid'), directives.positive_int_list) } has_content = True def make_title(self): if self.arguments: title_text = self.arguments[0] text_nodes, messages = self.state.inline_text( title_text, self.lineno) title = nodes.title(title_text, '', *text_nodes) (title.source, title.line) = self.state_machine.get_source_and_line(self.lineno) else: title = None messages = [] return title, messages def process_header_option(self): source = self.state_machine.get_source(self.lineno - 1) table_head = [] max_header_cols = 0 if 'header' in self.options: # separate table header in option rows, max_header_cols = self.parse_csv_data_into_rows( self.options['header'].split('\n'), self.HeaderDialect(), source) table_head.extend(rows) return table_head, max_header_cols def check_table_dimensions(self, rows, header_rows, stub_columns): if len(rows) < header_rows: error = self.state_machine.reporter.error( '%s header row(s) specified but only %s row(s) of data ' 'supplied ("%s" directive).' % (header_rows, len(rows), self.name), nodes.literal_block(self.block_text, self.block_text), line=self.lineno) raise SystemMessagePropagation(error) if len(rows) == header_rows > 0: error = self.state_machine.reporter.error( 'Insufficient data supplied (%s row(s)); no data remaining ' 'for table body, required by "%s" directive.' % (len(rows), self.name), nodes.literal_block(self.block_text, self.block_text), line=self.lineno) raise SystemMessagePropagation(error) for row in rows: if len(row) < stub_columns: error = self.state_machine.reporter.error( '%s stub column(s) specified but only %s columns(s) of ' 'data supplied ("%s" directive).' % (stub_columns, len(row), self.name), nodes.literal_block(self.block_text, self.block_text), line=self.lineno) raise SystemMessagePropagation(error) if len(row) == stub_columns > 0: error = self.state_machine.reporter.error( 'Insufficient data supplied (%s columns(s)); no data remaining ' 'for table body, required by "%s" directive.' % (len(row), self.name), nodes.literal_block(self.block_text, self.block_text), line=self.lineno) raise SystemMessagePropagation(error) def set_table_width(self, table_node): if 'width' in self.options: table_node['width'] = self.options.get('width') @property def widths(self): return self.options.get('widths', '') def get_column_widths(self, max_cols): if type(self.widths) == list: if len(self.widths) != max_cols: error = self.state_machine.reporter.error( '"%s" widths do not match the number of columns in table ' '(%s).' % (self.name, max_cols), nodes.literal_block(self.block_text, self.block_text), line=self.lineno) raise SystemMessagePropagation(error) col_widths = self.widths elif max_cols: col_widths = [100 // max_cols] * max_cols else: error = self.state_machine.reporter.error( 'No table data detected in CSV file.', nodes.literal_block(self.block_text, self.block_text), line=self.lineno) raise SystemMessagePropagation(error) return col_widths def extend_short_rows_with_empty_cells(self, columns, parts): for part in parts: for row in part: if len(row) < columns: row.extend([(0, 0, 0, [])] * (columns - len(row)))
class CSVTable(Table): option_spec = { 'header-rows': directives.nonnegative_int, 'stub-columns': directives.nonnegative_int, 'header': directives.unchanged, 'width': directives.length_or_percentage_or_unitless, 'widths': directives.value_or(('auto', ), directives.positive_int_list), 'file': directives.path, 'url': directives.uri, 'encoding': directives.encoding, 'class': directives.class_option, 'name': directives.unchanged, 'align': align, # field delimiter char 'delim': directives.single_char_or_whitespace_or_unicode, # treat whitespace after delimiter as significant 'keepspace': directives.flag, # text field quote/unquote char: 'quote': directives.single_char_or_unicode, # char used to escape delim & quote as-needed: 'escape': directives.single_char_or_unicode, } class DocutilsDialect(csv.Dialect): """CSV dialect for `csv_table` directive.""" delimiter = ',' quotechar = '"' doublequote = True skipinitialspace = True strict = True lineterminator = '\n' quoting = csv.QUOTE_MINIMAL def __init__(self, options): if 'delim' in options: self.delimiter = CSVTable.encode_for_csv(options['delim']) if 'keepspace' in options: self.skipinitialspace = False if 'quote' in options: self.quotechar = CSVTable.encode_for_csv(options['quote']) if 'escape' in options: self.doublequote = False self.escapechar = CSVTable.encode_for_csv(options['escape']) csv.Dialect.__init__(self) class HeaderDialect(csv.Dialect): """CSV dialect to use for the "header" option data.""" delimiter = ',' quotechar = '"' escapechar = '\\' doublequote = False skipinitialspace = True strict = True lineterminator = '\n' quoting = csv.QUOTE_MINIMAL def check_requirements(self): pass def run(self): try: if (not self.state.document.settings.file_insertion_enabled and ('file' in self.options or 'url' in self.options)): warning = self.state_machine.reporter.warning( 'File and URL access deactivated; ignoring "%s" ' 'directive.' % self.name, nodes.literal_block(self.block_text, self.block_text), line=self.lineno) return [warning] self.check_requirements() title, messages = self.make_title() csv_data, source = self.get_csv_data() table_head, max_header_cols = self.process_header_option() rows, max_cols = self.parse_csv_data_into_rows( csv_data, self.DocutilsDialect(self.options), source) max_cols = max(max_cols, max_header_cols) header_rows = self.options.get('header-rows', 0) stub_columns = self.options.get('stub-columns', 0) self.check_table_dimensions(rows, header_rows, stub_columns) table_head.extend(rows[:header_rows]) table_body = rows[header_rows:] col_widths = self.get_column_widths(max_cols) self.extend_short_rows_with_empty_cells(max_cols, (table_head, table_body)) except SystemMessagePropagation as detail: return [detail.args[0]] except csv.Error as detail: message = str(detail) if sys.version_info < (3, ) and '1-character string' in message: message += '\nwith Python 2.x this must be an ASCII character.' error = self.state_machine.reporter.error( 'Error with CSV data in "%s" directive:\n%s' % (self.name, message), nodes.literal_block(self.block_text, self.block_text), line=self.lineno) return [error] table = (col_widths, table_head, table_body) table_node = self.state.build_table(table, self.content_offset, stub_columns, widths=self.widths) table_node['classes'] += self.options.get('class', []) if 'align' in self.options: table_node['align'] = self.options.get('align') self.set_table_width(table_node) self.add_name(table_node) if title: table_node.insert(0, title) return [table_node] + messages def get_csv_data(self): """ Get CSV data from the directive content, from an external file, or from a URL reference. """ encoding = self.options.get( 'encoding', self.state.document.settings.input_encoding) error_handler = self.state.document.settings.input_encoding_error_handler if self.content: # CSV data is from directive content. if 'file' in self.options or 'url' in self.options: error = self.state_machine.reporter.error( '"%s" directive may not both specify an external file and' ' have content.' % self.name, nodes.literal_block(self.block_text, self.block_text), line=self.lineno) raise SystemMessagePropagation(error) source = self.content.source(0) csv_data = self.content elif 'file' in self.options: # CSV data is from an external file. if 'url' in self.options: error = self.state_machine.reporter.error( 'The "file" and "url" options may not be simultaneously' ' specified for the "%s" directive.' % self.name, nodes.literal_block(self.block_text, self.block_text), line=self.lineno) raise SystemMessagePropagation(error) source_dir = os.path.dirname( os.path.abspath(self.state.document.current_source)) source = os.path.normpath( os.path.join(source_dir, self.options['file'])) source = utils.relative_path(None, source) try: self.state.document.settings.record_dependencies.add(source) csv_file = io.FileInput(source_path=source, encoding=encoding, error_handler=error_handler) csv_data = csv_file.read().splitlines() except IOError as error: severe = self.state_machine.reporter.severe( 'Problems with "%s" directive path:\n%s.' % (self.name, SafeString(error)), nodes.literal_block(self.block_text, self.block_text), line=self.lineno) raise SystemMessagePropagation(severe) elif 'url' in self.options: # CSV data is from a URL. # Do not import urllib2 at the top of the module because # it may fail due to broken SSL dependencies, and it takes # about 0.15 seconds to load. import urllib.request, urllib.error, urllib.parse source = self.options['url'] try: csv_text = urllib.request.urlopen(source).read() except (urllib.error.URLError, IOError, OSError, ValueError) as error: severe = self.state_machine.reporter.severe( 'Problems with "%s" directive URL "%s":\n%s.' % (self.name, self.options['url'], SafeString(error)), nodes.literal_block(self.block_text, self.block_text), line=self.lineno) raise SystemMessagePropagation(severe) csv_file = io.StringInput( source=csv_text, source_path=source, encoding=encoding, error_handler=(self.state.document.settings.\ input_encoding_error_handler)) csv_data = csv_file.read().splitlines() else: error = self.state_machine.reporter.warning( 'The "%s" directive requires content; none supplied.' % self.name, nodes.literal_block(self.block_text, self.block_text), line=self.lineno) raise SystemMessagePropagation(error) return csv_data, source if sys.version_info < (3, ): # 2.x csv module doesn't do Unicode def decode_from_csv(s): return s.decode('utf-8') def encode_for_csv(s): return s.encode('utf-8') else: def decode_from_csv(s): return s def encode_for_csv(s): return s decode_from_csv = staticmethod(decode_from_csv) encode_for_csv = staticmethod(encode_for_csv) def parse_csv_data_into_rows(self, csv_data, dialect, source): # csv.py doesn't do Unicode; encode temporarily as UTF-8 csv_reader = csv.reader( [self.encode_for_csv(line + '\n') for line in csv_data], dialect=dialect) rows = [] max_cols = 0 for row in csv_reader: row_data = [] for cell in row: # decode UTF-8 back to Unicode cell_text = self.decode_from_csv(cell) cell_data = (0, 0, 0, statemachine.StringList(cell_text.splitlines(), source=source)) row_data.append(cell_data) rows.append(row_data) max_cols = max(max_cols, len(row)) return rows, max_cols
class CSVTable(Table): option_spec = { 'header-rows': directives.nonnegative_int, 'stub-columns': directives.nonnegative_int, 'header': directives.unchanged, 'widths': directives.value_or(('auto', ), directives.positive_int_list), 'file': directives.path, 'url': directives.uri, 'encoding': directives.encoding, 'class': directives.class_option, 'name': directives.unchanged, 'align': align, # field delimiter char 'delim': directives.single_char_or_whitespace_or_unicode, # treat whitespace after delimiter as significant 'keepspace': directives.flag, # text field quote/unquote char: 'quote': directives.single_char_or_unicode, # char used to escape delim & quote as-needed: 'escape': directives.single_char_or_unicode, } class DocutilsDialect(csv.Dialect): """CSV dialect for `csv_table` directive.""" delimiter = ',' quotechar = '"' doublequote = True skipinitialspace = True strict = True lineterminator = '\n' quoting = csv.QUOTE_MINIMAL def __init__(self, options): if 'delim' in options: self.delimiter = CSVTable.encode_for_csv(options['delim']) if 'keepspace' in options: self.skipinitialspace = False if 'quote' in options: self.quotechar = CSVTable.encode_for_csv(options['quote']) if 'escape' in options: self.doublequote = False self.escapechar = CSVTable.encode_for_csv(options['escape']) csv.Dialect.__init__(self) class HeaderDialect(csv.Dialect): """CSV dialect to use for the "header" option data.""" delimiter = ',' quotechar = '"' escapechar = '\\' doublequote = False skipinitialspace = True strict = True lineterminator = '\n' quoting = csv.QUOTE_MINIMAL def check_requirements(self): pass def run(self): try: if (not self.state.document.settings.file_insertion_enabled and ('file' in self.options or 'url' in self.options)): warning = self.state_machine.reporter.warning( 'File and URL access deactivated; ignoring "%s" ' 'directive.' % self.name, nodes.literal_block(self.block_text, self.block_text), line=self.lineno) return [warning] self.check_requirements() title, messages = self.make_title() csv_data, source = self.get_csv_data() table_head, max_header_cols = self.process_header_option() rows, max_cols = self.parse_csv_data_into_rows( csv_data, self.DocutilsDialect(self.options), source) max_cols = max(max_cols, max_header_cols) header_rows = self.options.get('header-rows', 0) stub_columns = self.options.get('stub-columns', 0) self.check_table_dimensions(rows, header_rows, stub_columns) table_head.extend(rows[:header_rows]) table_body = rows[header_rows:] widths, col_widths = self.get_column_widths(max_cols) self.extend_short_rows_with_empty_cells(max_cols, (table_head, table_body)) except SystemMessagePropagation, detail: return [detail.args[0]] except csv.Error, detail: message = str(detail) if sys.version_info < (3, ) and '1-character string' in message: message += '\nwith Python 2.x this must be an ASCII character.' error = self.state_machine.reporter.error( 'Error with CSV data in "%s" directive:\n%s' % (self.name, message), nodes.literal_block(self.block_text, self.block_text), line=self.lineno) return [error]
class ItemsTableDirective(Directive): """Format a data structure as a table. If the items of the sequence are themselves sequences, they will formatted as rows. """ required_arguments = 1 has_content = False option_spec = { TITLE_OPTION: unchanged_required, HEADER_ROWS_OPTION: directives.nonnegative_int, STUB_COLUMNS_OPTION: directives.nonnegative_int, HEADER_OPTION: unchanged, H_LEVEL_TITLES_OPTION: unchanged, V_LEVEL_TITLES_OPTION: unchanged, H_LEVEL_INDEXES_OPTION: unchanged_required, V_LEVEL_INDEXES_OPTION: unchanged_required, H_LEVEL_VISIBILITY_OPTION: unchanged, V_LEVEL_VISIBILITY_OPTION: unchanged, H_LEVEL_SORT_ORDERS_OPTION: unchanged, V_LEVEL_SORT_ORDERS_OPTION: unchanged, CELL_FORMATS_OPTION: unchanged_required, WIDTHS_OPTION: directives.value_or(("auto", "grid"), directives.positive_int_list), CLASS_OPTION: directives.class_option, ALIGN_OPTION: align, NAME_OPTION: unchanged, } @property def title(self): return self.options.get(TITLE_OPTION, "") @property def widths(self): return self.options.get(WIDTHS_OPTION, "") @property def header_rows(self): return self.options.get(HEADER_ROWS_OPTION, 0) @property def stub_columns(self): return self.options.get(STUB_COLUMNS_OPTION, 0) @property def v_level_titles(self): if V_LEVEL_TITLES_OPTION not in self.options: return None titles = self.options[V_LEVEL_TITLES_OPTION] titles_stream = StringIO(titles) reader = csv.reader(titles_stream, delimiter=",", quotechar='"', skipinitialspace=True, doublequote=True) titles_row = next(reader) stripped_titles = [cell.strip() for cell in titles_row] return stripped_titles @property def h_level_titles(self): if H_LEVEL_TITLES_OPTION not in self.options: return None titles = self.options[H_LEVEL_TITLES_OPTION] titles_stream = StringIO(titles) reader = csv.reader(titles_stream, delimiter=",", quotechar='"', skipinitialspace=True, doublequote=True) titles_row = next(reader) stripped_titles = [cell.strip() for cell in titles_row] return stripped_titles @property def v_level_indexes(self): text = self.options.get(V_LEVEL_INDEXES_OPTION, "") try: items = list(map(int, filter(None, text.split(",")))) except ValueError: raise self.error("Could not interpret option {} {!r}".format( V_LEVEL_INDEXES_OPTION, text)) return items or None @property def h_level_indexes(self): text = self.options.get(H_LEVEL_INDEXES_OPTION, "") try: items = list(map(int, filter(None, text.split(",")))) except ValueError: raise self.error("Could not interpret option {} {!r}".format( H_LEVEL_INDEXES_OPTION, text)) return items or None @property def v_level_visibility(self): text = self.options.get(V_LEVEL_VISIBILITY_OPTION, "") try: visibilities = list( map(lambda s: s.strip().lower(), filter(None, text.split(",")))) except ValueError: raise self.error("Could not interpret option {} {!r}".format( V_LEVEL_VISIBILITY_OPTION, text)) if not visibilities: return None try: return [VISIBILITIES[visibility] for visibility in visibilities] except KeyError: raise self.error( "Could not interpret option {} {!r}. Items must each be one of {}" .format( V_LEVEL_VISIBILITY_OPTION, text, list_conjunction(list(map(repr, VISIBILITIES.keys())), "or"), )) @property def h_level_visibility(self): text = self.options.get(H_LEVEL_VISIBILITY_OPTION, "") try: visibilities = list( map(lambda s: s.strip().lower(), filter(None, text.split(",")))) except ValueError: raise self.error("Could not interpret option {} {!r}".format( H_LEVEL_VISIBILITY_OPTION, text)) if not visibilities: return None try: return [VISIBILITIES[visibility] for visibility in visibilities] except KeyError: raise self.error( "Could not interpret option {} {!r}. Items must each be one of {}" .format( H_LEVEL_VISIBILITY_OPTION, text, list_conjunction(list(map(repr, VISIBILITIES.keys())), "or"), )) @property def v_level_sort_orders(self): text = self.options.get(V_LEVEL_SORT_ORDERS_OPTION, "") try: orders = list( map(lambda s: s.strip(), filter(None, text.split(",")))) except ValueError: raise self.error("Could not interpret option {} {!r}".format( V_LEVEL_SORT_ORDERS_OPTION, text)) if not orders: return None try: return [SORT_ORDERS[order] for order in orders] except KeyError: raise self.error( "Could not interpret option {} {!r}. Items must each be one of {}" .format(V_LEVEL_SORT_ORDERS_OPTION, text, ", ".join(SORT_ORDERS.keys()))) @property def h_level_sort_orders(self): text = self.options.get(H_LEVEL_SORT_ORDERS_OPTION, "") try: orders = list( map(lambda s: s.strip(), filter(None, text.split(",")))) except ValueError: raise self.error("Could not interpret option {} {!r}".format( H_LEVEL_SORT_ORDERS_OPTION, text)) if not orders: return None try: return [SORT_ORDERS[order] for order in orders] except KeyError: raise self.error( "Could not interpret option {} {!r}. Items must each be one of {}" .format(H_LEVEL_SORT_ORDERS_OPTION, text, ", ".join(SORT_ORDERS.keys()))) def make_title(self): title_text = self.title if title_text: text_nodes, messages = self.state.inline_text( title_text, self.lineno) title_node = nodes.title(title_text, '', *text_nodes) title_node.source, title_node.line = self.state_machine.get_source_and_line( self.lineno) else: title_node = None messages = [] return title_node, messages def get_column_widths(self, max_cols): if isinstance(self.widths, list): if len(self.widths) != max_cols: raise self.error( "{!s} widths tabulate not match the number of columns {!s} in table. ({} directive)." .format(self.widths, max_cols, self.name)) col_widths = self.widths elif max_cols: col_widths = [100.0 / max_cols] * max_cols else: raise self.error( "Something went wrong calculating column widths. ({} directive)." .format(self.name)) return col_widths def process_header_option(self): table_head = [] max_header_cols = 0 if HEADER_OPTION in self.options: # TODO: Have the option for this to be a Python object too. header = self.options[HEADER_OPTION] header_stream = StringIO(header) reader = csv.reader(header_stream, delimiter=",", quotechar='"') header_row = next(reader) stripped_header_row = [cell.strip() for cell in header_row] table_head.append(stripped_header_row) max_header_cols = len(header_row) return table_head, max_header_cols def interpret_obj( self, obj, v_level_indexes, h_level_indexes, v_level_visibility, h_level_visibility, v_level_sort_keys, h_level_sort_keys, v_level_titles, h_level_titles, ): """Interpret the given Python object as a table. Args: obj: A sequence (later a mapping, too) Returns: A list of lists represents rows of cells. Raises: TypeError: If the type couldn't be interpreted as a table. """ if not isinstance(obj, NonStringIterable): raise self.error( "Cannot make a table from object {!r}".format(obj)) rectangular_rows = tabulate( obj, v_level_indexes=v_level_indexes, h_level_indexes=h_level_indexes, v_level_visibility=v_level_visibility, h_level_visibility=h_level_visibility, v_level_sort_keys=v_level_sort_keys, h_level_sort_keys=h_level_sort_keys, v_level_titles=v_level_titles, h_level_titles=h_level_titles, ) assert is_rectangular(rectangular_rows) num_rows, num_cols = size(rectangular_rows) return rectangular_rows, num_cols def augment_cells(self, rows, source, *, span): return self.augment_cells_span( rows, source) if span else self.augment_cells_no_span( rows, source) def augment_cells_span(self, rows, source): # TODO: Hardwired str transform. # 4-tuple: morerows, morecols, offset, cellblock # - morerows: The number of additional rows this cells spans # - morecols: The number of additional columns this cell spans # - offset: Offset from the line-number at the start of the table # - cellblock: The contents of the cell return [[(0, span - 1, 0, StringList(str(cell).splitlines(), source=source)) for cell, span in run_length_encode(row)] for row in rows] def augment_cells_no_span(self, rows, source): """Convert each cell into a tuple suitable for consumption by build_table. """ # TODO: Hardwired str transform. # 4-tuple: morerows, morecols, offset, cellblock # - morerows: The number of additional rows this cells spans # - morecols: The number of additional columns this cell spans # - offset: Offset from the line-number at the start of the table # - cellblock: The contents of the cell return [[(0, 0, 0, StringList(str(cell).splitlines(), source=source)) for cell in row] for row in rows] def run(self): obj_name = self.arguments[0] try: prefixed_name, obj, parent, modname = import_by_name(obj_name) except ImportError: raise self.error( "Could not locate Python object {} ({} directive).".format( obj_name, self.name)) table_head, max_header_cols = self.process_header_option() rows, max_cols = self.interpret_obj( obj, self.v_level_indexes, self.h_level_indexes, self.v_level_visibility, self.h_level_visibility, self.v_level_sort_orders, self.h_level_sort_orders, self.v_level_titles, self.h_level_titles, ) max_cols = max(max_cols, max_header_cols) col_widths = self.get_column_widths(max_cols) table_head.extend(rows[:self.header_rows]) table_body = rows[self.header_rows:] table_head = self.augment_cells(table_head, source=prefixed_name, span=True) table_body = self.augment_cells(table_body, source=prefixed_name, span=False) table = (col_widths, table_head, table_body) table_node = self.state.build_table(table, self.content_offset, self.stub_columns, widths=self.widths) table_node["classes"] += self.options.get("class", []) if "align" in self.options: table_node["align"] = self.options.get("align") self.add_name(table_node) title_node, messages = self.make_title() if title_node: table_node.insert(0, title_node) return [table_node] + messages
class CoverityDefectListDirective(Directive): """ Directive to generate a list of defects. Syntax:: .. coverity-list:: title :col: list of columns to be displayed :widths: list of predefined column widths :chart: display chart that labels each allowed <<attribute>> value :checker: filter for only these checkers :impact: filter for only these impacts :kind: filter for only these kinds :classification: filter for only these classifications :action: filter for only these actions :component: filter for only these components :cwe: filter for only these CWE rating :cid: filter only these cid """ # Optional argument: title (whitespace allowed) optional_arguments = 1 final_argument_whitespace = True # Options option_spec = { 'class': directives.class_option, 'col': directives.unchanged, 'widths': directives.value_or(('auto', 'grid'), directives.positive_int_list), 'chart': directives.unchanged, 'checker': directives.unchanged, 'impact': directives.unchanged, 'kind': directives.unchanged, 'classification': directives.unchanged, 'action': directives.unchanged, 'component': directives.unchanged, 'cwe': directives.unchanged, 'cid': directives.unchanged, } # Content disallowed has_content = False def run(self): """ Processes the contents of the directive """ item_list_node = CoverityDefect() # Process title (optional argument) item_list_node['title'] = self.arguments[ 0] if self.arguments else 'Coverity report' # Process ``col`` option if 'col' in self.options: item_list_node['col'] = self.options['col'].split(',') elif 'chart' not in self.options: item_list_node['col'] = 'CID,Classification,Action,Comment'.split( ',') # use default colums else: item_list_node['col'] = [ ] # don't display a table if the ``chart`` option is present without ``col`` # Process ``widths`` option item_list_node['widths'] = self.options[ 'widths'] if 'widths' in self.options else '' # Process ``chart`` option if 'chart' in self.options: self._process_chart_option(item_list_node) else: item_list_node['chart'] = '' # Process the optional filters item_list_node['filters'] = { k: (self.options[k] if k in self.options else v) for (k, v) in item_list_node.filters.items() } return [item_list_node] def _process_chart_option(self, node): """ Processes the `chart` option. Args: node (CoverityDefect): CoverityDefect object used to store this directive's options and their parameters. """ if ':' in self.options['chart']: node['chart_attribute'] = self.options['chart'].split( ':')[0].capitalize() else: node['chart_attribute'] = 'Classification' parameters = self.options['chart'].split(':')[-1] # str node['chart'] = parameters.split(',') # list # try to convert parameters to int, in case a min slice size is defined instead of filter options try: node['min_slice_size'] = int(node['chart'][0]) node['chart'] = [] # only when a min slice size is defined except ValueError: node['min_slice_size'] = 1