Source code for bootlace.table.base

import inspect
from abc import ABC
from abc import abstractmethod
from collections.abc import Iterable
from collections.abc import Mapping
from typing import Any
from typing import ClassVar

import attrs
from dominate import tags
from dominate.dom_tag import dom_tag
from dominate.util import text

from bootlace.icon import Icon
from bootlace.util import as_tag
from bootlace.util import maybe
from bootlace.util import Tag


[docs] @attrs.define class Heading: """A heading for a table column.""" #: The text of the heading text: str #: The icon for the heading, in place of the text icon: Icon | None = attrs.field(default=None, converter=maybe(Icon)) # type: ignore
[docs] def __tag__(self) -> tags.html_tag: if self.icon: return tags.a( as_tag(self.icon), href="#", data_bs_toggle="tooltip", data_bs_title=self.text, cls="link-dark" ) return tags.span(self.text)
[docs] @attrs.define class ColumnBase(ABC): """Base class for table columns. Subclasses must implement the :meth:`cell` method.""" #: The heading for the column heading: Heading = attrs.field(converter=maybe(Heading)) # type: ignore name: str | None = None th = Tag(tags.th) td = Tag(tags.td) def __set_name__(self, owner: type, name: str) -> None: self.name = self.name or name @property def attribute(self) -> str: """The attribute name for the column.""" if self.name is None: raise ValueError("column must be named in Table or name= parameter must be provided") return self.name
[docs] def attribute_value(self, value: Any) -> Any: """Return the value of the attribute for the given object.""" return getattr(value, self.attribute)
[docs] def contents(self, value: Any, format: str | None = None) -> Any: """Return the contents of the cell for the column, using an HTML comment if the attribute value is None.""" contents = self.attribute_value(value) if contents is None: return tags.comment(f"No value for {self.name}") if format: return text(format.format(contents)) return text(str(contents))
[docs] @abstractmethod def cell(self, value: Any) -> dom_tag: """Return the cell for the column as an HTML tag.""" raise NotImplementedError("Subclasses must implement this method")
def __th__(self) -> dom_tag: return self.th(as_tag(self.heading), scope="col", __pretty=False) def __td__(self, value: Any) -> dom_tag: return self.td(self.cell(value), __pretty=False)
def is_instance_or_subclass(val: Any, class_: type) -> bool: """Return True if ``val`` is either a subclass or instance of ``class_``.""" try: return issubclass(val, class_) except TypeError: return isinstance(val, class_) def _get_columns(attrs: Mapping[str, Any]) -> dict[str, ColumnBase]: return { column_name: column_value for column_name, column_value in attrs.items() if is_instance_or_subclass(column_value, ColumnBase) } class TableMetaclass(type): columns: dict[str, ColumnBase] def __new__(mcls, name: str, bases: tuple[type, ...], namespace: dict[str, Any]) -> type: cls = super().__new__(mcls, name, bases, namespace) cls.columns = mcls.get_declared_columns(cls) cls.columns.update(_get_columns(namespace)) return cls @classmethod def get_declared_columns(mcls, cls: type) -> dict[str, ColumnBase]: mro = inspect.getmro(cls) # Loop over mro in reverse to maintain correct order of fields columns: dict[str, ColumnBase] = {} column_gen = ( _get_columns( getattr(base, "_declared_columns", base.__dict__), ) for base in mro[:0:-1] ) for column_set in column_gen: columns.update(column_set) return columns
[docs] class Table(metaclass=TableMetaclass): """Base class for class-defined tables. Subclasses should define columns as class attributes, e.g.: class MyTable(Table): name = Column(Heading("Name")) age = Column(Heading("Age")) Use :meth:`render` to render a table from a list of items as :class:`dominate.tags.table`. """ columns: ClassVar[dict[str, ColumnBase]] table = Tag(tags.table) thead = Tag(tags.thead) tbody = Tag(tags.tbody) tr = Tag(tags.tr) def __init__(self, decorated_classes: Iterable[str] | None = None) -> None: if decorated_classes is None: self.decorated_classes = set() else: self.decorated_classes = set(decorated_classes) def __table__(self, items: list[Any]) -> tags.html_tag: table = self.table(cls="table") table.classes.add(*self.decorated_classes) table.add(self.__thead__()) table.add(self.__tbody__(items)) return table def __thead__(self) -> tags.html_tag: thead = self.thead() for column in self.columns.values(): thead.add(column.__th__()) return thead def __tr__(self, item: Any) -> tags.html_tag: id = getattr(item, "id", None) name = item.__class__.__name__.lower() tr = self.tr(id=f"{name}-{id}" if id else None) for column in self.columns.values(): tr.add(column.__td__(item)) return tr def __tbody__(self, items: list[Any]) -> tags.html_tag: tbody = self.tbody() for item in items: tbody.add(self.__tr__(item)) return tbody
[docs] def __call__(self, items: list[Any]) -> tags.html_tag: return self.__table__(items)