BjoernLudwigPTB/pyxml2pdf

View on GitHub
src/pyxml2pdf/core/rows.py

Summary

Maintainability
A
0 mins
Test Coverage
A
96%
"""This module contains the base class to create table rows

Specifically it contains a class :class:`XMLCell` for unified styled cells and
a class :class:`XMLRow` for xml extracted data.
"""
from typing import cast, List, Set, Type

import defusedxml  # type: ignore
from reportlab.lib.styles import ParagraphStyle  # type: ignore
from reportlab.platypus import Paragraph, Table  # type: ignore

from pyxml2pdf.input.properties import (
    COLUMNS,
    FILTER_XMLTAG,
    IDENTIFIER_XMLTAG,
)  # type: ignore
from pyxml2pdf.styles.table_styles import XMLTableStyle
from pyxml2pdf.tables.builder import TableBuilder

# Monkeypatch standard library xml vulnerabilities.
defusedxml.defuse_stdlib()
from xml.etree.ElementTree import Element

__all__ = ["XMLCell", "XMLRow"]


class XMLCell(Paragraph):
    """This class represents the text of type reportlab.platypus.Paragraph in a cell

    It inherits from :class:`reportlab.platypus.Paragraph` and ensures the
    unified styling of all table cells.

    :py:class:`reportlab.platypus.Paragraph` is solely used with one
    certain style, which is supposed to be set as a class attribute during runtime.

    :param str text: the text to write into row
    """

    _style: ParagraphStyle

    def __init__(self, text: str) -> None:
        super().__init__(text, self.style)

    @property
    def style(self) -> ParagraphStyle:
        """The one for all stylesheet to style all cells"""
        return self._style

    @style.setter
    def style(self, value: ParagraphStyle) -> None:
        """Setter for the one for all stylesheet to style all cells"""
        self._style = value


class XMLRow(Element):
    """A wrapper class for :py:class:`xml.etree.ElementTree.Element`

    :py:class:`xml.etree.ElementTree.Element` is augmented with the table row
    representation and the attributes and methods to manipulate everything
    according to the final tables needs. A :py:class:`XMLRow` can only be initialized
    with an object of type :py:class:`xml.etree.ElementTree.Element`.

    :param xml.etree.ElementTree.Element element: the element to build the instance from
    """

    _table_builder: TableBuilder = TableBuilder()
    _table_style: XMLTableStyle = XMLTableStyle()

    _criteria: Set[str]
    _identifier: str
    _reduced_row: Table
    _cell_styler: Type[XMLCell] = XMLCell

    def __init__(self, element: Element) -> None:
        # Call Element constructor and extend ourselves by extending all children
        # tags to create an underlying copy of element.
        super().__init__(element.tag, element.attrib)
        self.extend(list(element))
        # Initialize needed objects especially for table creation.
        self._cell_styler.style = (  # type: ignore[method-assign]
            self._table_style.custom_styles["stylesheet"]["Normal"]
        )
        # Initialize definitely needed instance variables.
        self._criteria = self._init_criteria()
        self._identifier = self._concatenate_tags_content(IDENTIFIER_XMLTAG)
        self._init_full_row()

    def _init_criteria(self) -> Set[str]:
        """Initialize the list of criteria from the according xml tag's content"""
        criteria: str = self._concatenate_tags_content([FILTER_XMLTAG])
        return set(criteria.split(", "))

    def _concatenate_tags_content(
        self, cell_tags: List[str], separator: str = " - "
    ) -> str:
        """Form one string from the texts of a set of XML tags to fill a cell

        Form a string of the content for all desired XML tags by
        concatenating them together with a separator. This is especially necessary,
        since :py:mod:`reportlab.platypus.Paragraph` cannot handle `None`s as texts but
        handles as well the concatenation of XML tags' content, if `cell_tags`
        has more than one element. So we ensure the result to be at least an empty
        string.

        :param cell_tags: list of all tags for which the
            descriptive texts is wanted, even if it is just one
        :param separator: the separator in between the concatenated texts
        :returns: concatenated, separated texts of all tags for the current cell
        """
        return separator.join(
            [cast(str, self.findtext(tag)) for tag in cell_tags if self.findtext(tag)]
        )

    def _init_reduced_row(self, subtable_title):
        """Initializes the reduced version of the table row

        Create a table row in proper format but just containing a brief description
        of the element represented by the row and a reference to the fully described
        element at another place, namely the subtable with the given title.

        :param str subtable_title: title of the subtable which contains the full row
        """
        reduced_columns = [
            self._cell_styler(self._concatenate_tags_content(COLUMNS[0].tag)),
            self._cell_styler(f"Main entry in subtable '{subtable_title}'."),
        ]
        self._reduced_row = self._table_builder.create_fixedwidth_table(
            [reduced_columns],
            [
                self._table_style.column_widths[0],
                sum(self._table_style.column_widths[1:]),
            ],
        )

    def _init_full_row(self) -> List[XMLCell]:
        """Initialize the single table row containing all information from the XML input

        Extract interesting information from specified row tag's subtags and
        connect them into a nicely formatted row of a table.

        :return: the columns of any table representation
        :rtype: List[XMLCell]
        """
        table_columns = [
            self._cell_styler(self._concatenate_tags_content(column.tag))
            for column in COLUMNS
        ]
        self._full_row = self._table_builder.create_fixedwidth_table([table_columns])
        return table_columns

    def get_full_row(self, subtable_title: str) -> Table:
        """Return a table row with all the row's information

        This ensures, that in subclasses we can override this function and after
        handing over the full information, the reduced version with a reference to
        the subtable containing the full version can be created.

        See :class:`Event` for an example implementation of this pattern.

        :param subtable_title: the title of the subtable in which the row will
            be integrated
        :returns: a table row with all the element's information
        """
        self._init_reduced_row(subtable_title)
        return self._full_row

    @property
    def criteria(self) -> Set[str]:
        """Return the row's criteria

        :returns: a list of the row's criteria
        :rtype: Set[str]
        """
        return self._criteria

    @property
    def identifier(self) -> str:
        """Return the identifier of the row

        :returns: identifier
        :rtype: str
        """
        return self._identifier

    def get_table_row(self, subtable_title: str) -> Table:
        """Return the table row representation of the XML tag

        This is the API of :py:class:`XMLRow` for getting the
        table row representation of the row. It allows for reacting to the
        distribution of the XML tags content by creating a shorter version
        referencing the main subtable. See :meth:`get_full_row` for details.

        :param str subtable_title: the title of the subtable in which the row will
            be integrated
        :returns: a table row representation of the XML tag's content
        :rtype: Table
        """
        try:
            return self._reduced_row
        except AttributeError:
            return self.get_full_row(subtable_title)