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]
Beispiel #2
0
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
Beispiel #3
0
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
Beispiel #4
0
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)))
Beispiel #5
0
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
Beispiel #6
0
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]
Beispiel #7
0
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