Deploy site

This commit is contained in:
Gitea Actions
2025-06-11 03:00:30 +02:00
commit b4a252bc51
2329 changed files with 367195 additions and 0 deletions

View File

@ -0,0 +1,53 @@
from dataproperty import Align, Format
from ._cell import Cell
from ._font import FontSize, FontStyle, FontWeight
from ._style import DecorationLine, Style, ThousandSeparator, VerticalAlign
from ._styler import (
GFMarkdownStyler,
HtmlStyler,
LatexStyler,
MarkdownStyler,
NullStyler,
ReStructuredTextStyler,
TextStyler,
get_align_char,
)
from ._styler_interface import StylerInterface
from ._theme import (
CheckStyleFilterKeywordArgsFunc,
ColSeparatorStyleFilterFunc,
StyleFilterFunc,
Theme,
fetch_theme,
list_themes,
)
__all__ = (
"Align",
"Format",
"Cell",
"FontSize",
"FontStyle",
"FontWeight",
"Style",
"ThousandSeparator",
"VerticalAlign",
"DecorationLine",
"GFMarkdownStyler",
"HtmlStyler",
"LatexStyler",
"MarkdownStyler",
"NullStyler",
"ReStructuredTextStyler",
"StylerInterface",
"TextStyler",
"CheckStyleFilterKeywordArgsFunc",
"ColSeparatorStyleFilterFunc",
"StyleFilterFunc",
"Theme",
"get_align_char",
"fetch_theme",
"list_themes",
)

View File

@ -0,0 +1,30 @@
from dataclasses import dataclass
from typing import Any
from ._style import Style
@dataclass(frozen=True)
class Cell:
"""
A data class representing a cell in a table.
"""
row: int
"""row index. ``-1`` means that the table header row."""
col: int
"""column index."""
value: Any
"""data for the cell."""
default_style: Style
"""default |Style| for the cell."""
def is_header_row(self) -> bool:
"""
Return |True| if the cell is a header.
"""
return self.row < 0

View File

@ -0,0 +1,23 @@
from enum import Enum, unique
@unique
class FontSize(Enum):
NONE = "none"
TINY = "tiny"
SMALL = "small"
MEDIUM = "medium"
LARGE = "large"
@unique
class FontStyle(Enum):
NORMAL = "normal"
ITALIC = "italic"
TYPEWRITER = "typewriter"
@unique
class FontWeight(Enum):
NORMAL = "normal"
BOLD = "bold"

View File

@ -0,0 +1,373 @@
import warnings
from enum import Enum, unique
from typing import Any, Final, Optional, Union
from dataproperty import Align
from tcolorpy import Color
from .._function import normalize_enum
from ._font import FontSize, FontStyle, FontWeight
@unique
class DecorationLine(Enum):
NONE = "none"
LINE_THROUGH = "line_through"
STRIKE = "strike"
UNDERLINE = "underline"
@unique
class ThousandSeparator(Enum):
NONE = "none" #: no thousands separator
COMMA = "comma" #: ``','`` as thousands separator
SPACE = "space" #: ``' '`` as thousands separator
UNDERSCORE = "underscore" #: ``'_'`` as thousands separator
@unique
class VerticalAlign(Enum):
BASELINE = (1 << 0, "baseline")
TOP = (1 << 1, "top")
MIDDLE = (1 << 2, "middle")
BOTTOM = (1 << 3, "bottom")
@property
def align_code(self) -> int:
return self.__align_code
@property
def align_str(self) -> str:
return self.__align_string
def __init__(self, code: int, string: str) -> None:
self.__align_code = code
self.__align_string = string
_s_to_ts: Final[dict[str, ThousandSeparator]] = {
"": ThousandSeparator.NONE,
",": ThousandSeparator.COMMA,
" ": ThousandSeparator.SPACE,
"_": ThousandSeparator.UNDERSCORE,
}
def _normalize_thousand_separator(value: Union[str, ThousandSeparator]) -> ThousandSeparator:
if isinstance(value, ThousandSeparator):
return value
thousand_separator = normalize_enum(
value,
ThousandSeparator,
default=ThousandSeparator.NONE,
validate=False,
)
if isinstance(thousand_separator, ThousandSeparator):
return thousand_separator
norm_value = _s_to_ts.get(value)
if norm_value is None:
raise ValueError(f"unknown thousand separator: {value}")
return norm_value
class Style:
"""Style specifier class for table elements.
Args:
color (Union[|str|, tcolorpy.Color, |None|]):
Text color for cells.
When using str, specify a color code (``"#XXXXXX"``) or a color name.
.. note::
In the current version, only applicable for part of text format writer classes.
fg_color (Union[|str|, tcolorpy.Color, |None|]):
Alias to :py:attr:`~.color`.
bg_color (Union[|str|, tcolorpy.Color, |None|]):
Background color for cells.
When using str, specify a color code (``"#XXXXXX"``) or a color name.
.. note::
In the current version, only applicable for part of text format writer classes.
align (|str| / :py:class:`~.style.Align`):
Horizontal text alignment for cells.
This can be only applied for text format writer classes.
Possible string values are:
- ``"auto"`` (default)
- Detect data type for each column and set alignment that appropriate
for the type automatically
- ``"left"``
- ``"right"``
- ``"center"``
vertical_align (|str| / :py:class:`~.style.VerticalAlign`):
Vertical text alignment for cells.
This can be only applied for HtmlTableWriter class.
Possible string values are:
- ``"baseline"`` (default)
- ``"top"``
- ``"middle"``
- ``"bottom"``
font_size (|str| / :py:class:`~.style.FontSize`):
Font size specification for cells in a column.
This can be only applied for HTML/Latex writer classes.
Possible string values are:
- ``"tiny"``
- ``"small"``
- ``"medium"``
- ``"large"``
- ``"none"`` (default: no font size specification)
font_weight (|str| / :py:class:`~.style.FontWeight`):
Font weight specification for cells in a column.
This can be only applied for HTML/Latex/Markdown writer classes.
Possible string values are:
- ``"normal"`` (default)
- ``"bold"``
font_style (|str| / :py:class:`~.style.FontStyle`):
Font style specification for cells in a column.
This can be applied only for HTML/Latex/Markdown writer classes.
Possible string values are:
- ``"normal"`` (default)
- ``"italic"``
- ``"typewriter"`` (only for Latex writer)
decoration_line (|str| / :py:class:`~.style.DecorationLine`)
Experiental.
Possible string values are:
- ``"line-through"``
- ``"strike"`` (alias for ``"line-through"``)
- ``"underline"``
- ``"none"`` (default)
thousand_separator (|str| / :py:class:`~.style.ThousandSeparator`):
Thousand separator specification for numbers in a column.
This can be only applied for text format writer classes.
Possible string values are:
- ``","``/``"comma"``
- ``" "``/``"space"``
- ``"_"``/``"underscore"``
- ``""``/``"none"`` (default)
Example:
:ref:`example-style`
"""
@property
def align(self) -> Align:
return self.__align
@align.setter
def align(self, value: Align) -> None:
self.__align = value
@property
def vertical_align(self) -> VerticalAlign:
return self.__valign
@property
def decoration_line(self) -> DecorationLine:
return self.__decoration_line
@property
def font_size(self) -> FontSize:
return self.__font_size
@property
def font_style(self) -> FontStyle:
return self.__font_style
@property
def font_weight(self) -> FontWeight:
return self.__font_weight
@property
def color(self) -> Optional[Color]:
return self.__fg_color
@property
def fg_color(self) -> Optional[Color]:
return self.__fg_color
@property
def bg_color(self) -> Optional[Color]:
return self.__bg_color
@property
def thousand_separator(self) -> ThousandSeparator:
return self.__thousand_separator
@property
def padding(self) -> Optional[int]:
return self.__padding
@padding.setter
def padding(self, value: Optional[int]) -> None:
self.__padding = value
def __init__(self, **kwargs: Any) -> None:
self.__kwargs = kwargs
self.__update_color(initialize=True)
self.__update_align(initialize=True)
self.__update_font(initialize=True)
self.__update_misc(initialize=True)
if self.__kwargs:
warnings.warn(f"unknown style attributes found: {self.__kwargs.keys()}", UserWarning)
def __repr__(self) -> str:
items = []
if self.align:
items.append(f"align={self.align.align_string}")
if self.padding is not None:
items.append(f"padding={self.padding}")
if self.vertical_align:
items.append(f"valign={self.vertical_align.align_str}")
if self.color:
items.append(f"color={self.color}")
if self.bg_color:
items.append(f"bg_color={self.bg_color}")
if self.decoration_line is not DecorationLine.NONE:
items.append(f"decoration_line={self.decoration_line.value}")
if self.font_size is not FontSize.NONE:
items.append(f"font_size={self.font_size.value}")
if self.font_style:
items.append(f"font_style={self.font_style.value}")
if self.font_weight:
items.append(f"font_weight={self.font_weight.value}")
if self.thousand_separator is not ThousandSeparator.NONE:
items.append(f"thousand_separator={self.thousand_separator.value}")
return "({})".format(", ".join(items))
def __eq__(self, other: Any) -> bool:
if self.__class__ is not other.__class__:
return False
return all(
[
self.align == other.align,
self.font_size == other.font_size,
self.font_style == other.font_style,
self.font_weight == other.font_weight,
self.thousand_separator == other.thousand_separator,
]
)
def __ne__(self, other: Any) -> bool:
if self.__class__ is not other.__class__:
return True
return not self.__eq__(other)
def update(self, **kwargs: Any) -> None:
"""Update specified style attributes."""
self.__kwargs = kwargs
self.__update_color(initialize=False)
self.__update_align(initialize=False)
self.__update_font(initialize=False)
self.__update_misc(initialize=False)
if self.__kwargs:
warnings.warn(f"unknown style attributes found: {self.__kwargs.keys()}", UserWarning)
def __update_color(self, initialize: bool) -> None:
fg_color = self.__kwargs.pop("color", None) or self.__kwargs.pop("fg_color", None)
if fg_color:
self.__fg_color: Optional[Color] = Color(fg_color)
elif initialize:
self.__fg_color = None
bg_color = self.__kwargs.pop("bg_color", None)
if bg_color:
self.__bg_color: Optional[Color] = Color(bg_color)
elif initialize:
self.__bg_color = None
def __update_font(self, initialize: bool) -> None:
font_size = self.__kwargs.pop("font_size", None)
if font_size:
self.__font_size = normalize_enum(
font_size,
FontSize,
validate=False,
default=FontSize.NONE,
)
elif initialize:
self.__font_size = FontSize.NONE
self.__validate_attr("font_size", (FontSize, str))
font_style = self.__kwargs.pop("font_style", None)
if font_style:
self.__font_style = normalize_enum(font_style, FontStyle, default=FontStyle.NORMAL)
elif initialize:
self.__font_style = FontStyle.NORMAL
self.__validate_attr("font_style", (FontStyle,))
font_weight = self.__kwargs.pop("font_weight", None)
if font_weight:
self.__font_weight = normalize_enum(font_weight, FontWeight, default=FontWeight.NORMAL)
elif initialize:
self.__font_weight = FontWeight.NORMAL
self.__validate_attr("font_weight", (FontWeight,))
def __update_align(self, initialize: bool) -> None:
align = self.__kwargs.pop("align", None)
if align:
self.__align = normalize_enum(align, Align, default=Align.AUTO)
elif initialize:
self.__align = Align.AUTO
self.__validate_attr("align", (Align,))
valign = self.__kwargs.pop("vertical_align", None)
if valign:
self.__valign = normalize_enum(valign, VerticalAlign, default=VerticalAlign.BASELINE)
elif initialize:
self.__valign = VerticalAlign.BASELINE
self.__validate_attr("vertical_align", (VerticalAlign,))
def __update_misc(self, initialize: bool) -> None:
padding = self.__kwargs.pop("padding", None)
if padding is not None:
self.__padding = padding
elif initialize:
self.__padding = None
decoration_line = self.__kwargs.pop("decoration_line", None)
if decoration_line:
self.__decoration_line = normalize_enum(
decoration_line, DecorationLine, default=DecorationLine.NONE
)
elif initialize:
self.__decoration_line = DecorationLine.NONE
self.__validate_attr("decoration_line", (DecorationLine,))
thousand_separator = self.__kwargs.pop("thousand_separator", None)
if thousand_separator:
self.__thousand_separator = _normalize_thousand_separator(thousand_separator)
elif initialize:
self.__thousand_separator = ThousandSeparator.NONE
self.__validate_attr("thousand_separator", (ThousandSeparator,))
def __validate_attr(self, attr_name: str, expected_types: tuple[type, ...]) -> None:
value = getattr(self, attr_name)
expected = " or ".join(c.__name__ for c in expected_types)
if not isinstance(value, expected_types):
raise TypeError(f"{attr_name} must be instance of {expected}: actual={type(value)}")

View File

@ -0,0 +1,331 @@
import re
from typing import TYPE_CHECKING, Any, Final, Optional
from dataproperty import Align
from tcolorpy import Color, tcolor
from ._font import FontSize, FontStyle, FontWeight
from ._style import DecorationLine, Style, ThousandSeparator
from ._styler_interface import StylerInterface
if TYPE_CHECKING:
from ..writer._table_writer import AbstractTableWriter
_align_char_mapping: Final[dict[Align, str]] = {
Align.AUTO: "<",
Align.LEFT: "<",
Align.RIGHT: ">",
Align.CENTER: "^",
}
def get_align_char(align: Align) -> str:
return _align_char_mapping[align]
def _to_latex_rgb(color: Color, value: str) -> str:
return r"\textcolor{" + color.color_code + "}{" + value + "}"
class AbstractStyler(StylerInterface):
def __init__(self, writer: "AbstractTableWriter") -> None:
self._writer = writer
self._font_size_map = self._get_font_size_map()
def get_font_size(self, style: Style) -> Optional[str]:
return self._font_size_map.get(style.font_size)
def get_additional_char_width(self, style: Style) -> int:
return 0
def apply(self, value: Any, style: Style) -> str:
return value
def apply_align(self, value: str, style: Style) -> str:
return value
def apply_terminal_style(self, value: str, style: Style) -> str:
return value
def _get_font_size_map(self) -> dict[FontSize, str]:
return {}
class NullStyler(AbstractStyler):
def get_font_size(self, style: Style) -> Optional[str]:
return ""
class TextStyler(AbstractStyler):
def apply_terminal_style(self, value: str, style: Style) -> str:
if not self._writer.enable_ansi_escape:
return value
ansi_styles = []
if style.decoration_line in (DecorationLine.STRIKE, DecorationLine.LINE_THROUGH):
ansi_styles.append("strike")
if style.decoration_line == DecorationLine.UNDERLINE:
ansi_styles.append("underline")
if style.font_weight == FontWeight.BOLD:
ansi_styles.append("bold")
if self._writer.colorize_terminal:
return tcolor(value, color=style.color, bg_color=style.bg_color, styles=ansi_styles)
return tcolor(value, styles=ansi_styles)
def __get_align_format(self, style: Style) -> str:
align_char = get_align_char(style.align)
format_items = ["{:" + align_char]
if style.padding is not None and style.padding > 0:
format_items.append(str(style.padding))
format_items.append("s}")
return "".join(format_items)
def apply_align(self, value: str, style: Style) -> str:
return self.__get_align_format(style).format(value)
def apply(self, value: str, style: Style) -> str:
if value:
if style.thousand_separator == ThousandSeparator.SPACE:
value = value.replace(",", " ")
elif style.thousand_separator == ThousandSeparator.UNDERSCORE:
value = value.replace(",", "_")
return value
class HtmlStyler(TextStyler):
def _get_font_size_map(self) -> dict[FontSize, str]:
return {
FontSize.TINY: "font-size:x-small",
FontSize.SMALL: "font-size:small",
FontSize.MEDIUM: "font-size:medium",
FontSize.LARGE: "font-size:large",
}
class LatexStyler(TextStyler):
class Command:
BOLD: Final = r"\bf"
ITALIC: Final = r"\it"
TYPEWRITER: Final = r"\tt"
UNDERLINE: Final = r"\underline"
STRIKEOUT: Final = r"\sout"
def get_additional_char_width(self, style: Style) -> int:
dummy_value = "d"
applied_value = self.apply(dummy_value, style)
return len(applied_value) - len(dummy_value)
def apply(self, value: Any, style: Style) -> str:
value = super().apply(value, style)
if not value:
return value
font_size = self.get_font_size(style)
commands = []
if font_size:
commands.append(font_size)
if style.font_weight == FontWeight.BOLD:
commands.append(self.Command.BOLD)
if style.font_style == FontStyle.ITALIC:
commands.append(self.Command.ITALIC)
elif style.font_style == FontStyle.TYPEWRITER:
commands.append(self.Command.TYPEWRITER)
if style.decoration_line in (DecorationLine.STRIKE, DecorationLine.LINE_THROUGH):
commands.append(self.Command.STRIKEOUT)
elif style.decoration_line == DecorationLine.UNDERLINE:
commands.append(self.Command.UNDERLINE)
for cmd in commands:
value = cmd + "{" + value + "}"
value = self.__apply_color(value, style)
return value
def __apply_color(self, value: str, style: Style) -> str:
if not style.fg_color:
return value
value = _to_latex_rgb(style.fg_color, value)
return value
def _get_font_size_map(self) -> dict[FontSize, str]:
return {
FontSize.TINY: r"\tiny",
FontSize.SMALL: r"\small",
FontSize.MEDIUM: r"\normalsize",
FontSize.LARGE: r"\large",
}
class MarkdownStyler(TextStyler):
def get_additional_char_width(self, style: Style) -> int:
width = 0
if style.font_weight == FontWeight.BOLD:
width += 4
if style.font_style == FontStyle.ITALIC:
width += 2
return width
def apply(self, value: Any, style: Style) -> str:
value = super().apply(value, style)
if not value:
return value
value = self._apply_font_weight(value, style)
value = self._apply_font_style(value, style)
return value
def _apply_font_weight(self, value: Any, style: Style) -> str:
if style.font_weight == FontWeight.BOLD:
value = f"**{value}**"
return value
def _apply_font_style(self, value: Any, style: Style) -> str:
if style.font_style == FontStyle.ITALIC:
value = f"_{value}_"
return value
class GFMarkdownStyler(MarkdownStyler):
"""
A styler class for GitHub Flavored Markdown
"""
def get_additional_char_width(self, style: Style) -> int:
width = super().get_additional_char_width(style)
if style.decoration_line in (DecorationLine.STRIKE, DecorationLine.LINE_THROUGH):
width += 4
if self.__use_latex(style):
dummy_value = "d"
value = self.apply(dummy_value, style)
width += len(value) - len(dummy_value)
return width
def apply(self, value: Any, style: Style) -> str:
value = super().apply(value, style)
if not value:
return value
use_latex = self.__use_latex(style)
if use_latex:
value = self.__escape_for_latex(value)
value = LatexStyler.Command.TYPEWRITER + "{" + value + "}"
value = self.__apply_decoration_line(value, style)
if use_latex:
value = r"$$" + self.__apply_color(value, style) + r"$$"
return value
def __use_latex(self, style: Style) -> bool:
return style.fg_color is not None
def __escape_for_latex(self, value: str) -> str:
value = re.sub(r"[\s_]", r"\\\\\g<0>", value)
return value.replace("-", r"\text{-}")
def __apply_decoration_line(self, value: str, style: Style) -> str:
use_latex = self.__use_latex(style)
if style.decoration_line in (DecorationLine.STRIKE, DecorationLine.LINE_THROUGH):
if use_latex:
value = r"\enclose{horizontalstrike}{" + value + "}"
else:
value = f"~~{value}~~"
elif style.decoration_line == DecorationLine.UNDERLINE:
if use_latex:
value = r"\underline{" + value + "}"
return value
def __apply_color(self, value: str, style: Style) -> str:
if not style.fg_color:
return value
return _to_latex_rgb(style.fg_color, value)
def _apply_font_weight(self, value: Any, style: Style) -> str:
if not self.__use_latex(style):
return super()._apply_font_weight(value, style)
if style.font_weight == FontWeight.BOLD:
value = LatexStyler.Command.BOLD + "{" + value + "}"
return value
def _apply_font_style(self, value: Any, style: Style) -> str:
if not self.__use_latex(style):
return super()._apply_font_style(value, style)
if style.font_style == FontStyle.ITALIC:
value = LatexStyler.Command.ITALIC + "{" + value + "}"
return value
class ReStructuredTextStyler(TextStyler):
def get_additional_char_width(self, style: Style) -> int:
from ..writer import RstCsvTableWriter
width = 0
if style.font_weight == FontWeight.BOLD:
width += 4
elif style.font_style == FontStyle.ITALIC:
width += 2
if (
style.thousand_separator == ThousandSeparator.COMMA
and self._writer.format_name == RstCsvTableWriter.FORMAT_NAME
):
width += 2
return width
def apply(self, value: Any, style: Style) -> str:
from ..writer import RstCsvTableWriter
value = super().apply(value, style)
if not value:
return value
if style.font_weight == FontWeight.BOLD:
value = f"**{value}**"
elif style.font_style == FontStyle.ITALIC:
# in reStructuredText, some custom style definition will be required to
# set for both bold and italic (currently not supported)
value = f"*{value}*"
if (
style.thousand_separator == ThousandSeparator.COMMA
and self._writer.format_name == RstCsvTableWriter.FORMAT_NAME
):
value = f'"{value}"'
return value

View File

@ -0,0 +1,26 @@
import abc
from typing import Any, Optional
from ._style import Style
class StylerInterface(metaclass=abc.ABCMeta):
@abc.abstractmethod
def apply(self, value: Any, style: Style) -> str: # pragma: no cover
raise NotImplementedError()
@abc.abstractmethod
def apply_align(self, value: str, style: Style) -> str: # pragma: no cover
raise NotImplementedError()
@abc.abstractmethod
def apply_terminal_style(self, value: str, style: Style) -> str: # pragma: no cover
raise NotImplementedError()
@abc.abstractmethod
def get_font_size(self, style: Style) -> Optional[str]: # pragma: no cover
raise NotImplementedError()
@abc.abstractmethod
def get_additional_char_width(self, style: Style) -> int: # pragma: no cover
raise NotImplementedError()

View File

@ -0,0 +1,93 @@
import importlib
import pkgutil
import re
from collections.abc import Sequence
from typing import Any, Final, NamedTuple, Optional, Protocol
from .._logger import logger
from ..style import Cell, Style
PLUGIN_NAME_PEFIX: Final = "pytablewriter"
PLUGIN_NAME_SUFFIX: Final = "theme"
KNOWN_PLUGINS: Final = (
f"{PLUGIN_NAME_PEFIX}_altrow_{PLUGIN_NAME_SUFFIX}",
f"{PLUGIN_NAME_PEFIX}_altcol_{PLUGIN_NAME_SUFFIX}",
)
class StyleFilterFunc(Protocol):
def __call__(self, cell: Cell, **kwargs: Any) -> Optional[Style]: ...
class ColSeparatorStyleFilterFunc(Protocol):
def __call__(
self, left_cell: Optional[Cell], right_cell: Optional[Cell], **kwargs: Any
) -> Optional[Style]: ...
class CheckStyleFilterKeywordArgsFunc(Protocol):
def __call__(self, **kwargs: Any) -> None: ...
class Theme(NamedTuple):
style_filter: Optional[StyleFilterFunc]
col_separator_style_filter: Optional[ColSeparatorStyleFilterFunc]
check_style_filter_kwargs: Optional[CheckStyleFilterKeywordArgsFunc]
def list_themes() -> Sequence[str]:
return list(load_ptw_plugins())
def load_ptw_plugins() -> dict[str, Theme]:
plugin_regexp: Final = re.compile(
rf"^{PLUGIN_NAME_PEFIX}[_-].+[_-]{PLUGIN_NAME_SUFFIX}", re.IGNORECASE
)
discovered_plugins: Final = {
name: importlib.import_module(name)
for _finder, name, _ispkg in pkgutil.iter_modules()
if plugin_regexp.search(name) is not None
}
logger.debug(f"discovered_plugins: {list(discovered_plugins)}")
themes: dict[str, Theme] = {}
for theme, plugin in discovered_plugins.items():
style_filter = plugin.style_filter if hasattr(plugin, "style_filter") else None
col_sep_style_filter = (
plugin.col_separator_style_filter
if hasattr(plugin, "col_separator_style_filter")
else None
)
check_kwargs_func = (
plugin.check_style_filter_kwargs
if hasattr(plugin, "check_style_filter_kwargs")
else None
)
themes[theme] = Theme(style_filter, col_sep_style_filter, check_kwargs_func)
return themes
def fetch_theme(plugin_name: str) -> Theme:
loaded_themes: Final = load_ptw_plugins()
theme_regexp: Final = re.compile(
rf"^{PLUGIN_NAME_PEFIX}[_-]{plugin_name}[_-]{PLUGIN_NAME_SUFFIX}", re.IGNORECASE
)
matched_theme = None
for loaded_theme in loaded_themes:
if theme_regexp.search(loaded_theme):
matched_theme = loaded_theme
break
else:
err_msgs = [f"{plugin_name} theme is not installed."]
if plugin_name in KNOWN_PLUGINS:
err_msgs.append(f"try 'pip install {plugin_name}' to install the theme.")
raise RuntimeError(" ".join(err_msgs))
return loaded_themes[matched_theme]