Source code for plumbum.colorlib.factories

"""
Color-related factories. They produce Styles.
"""

from __future__ import annotations

__lazy_modules__ = {f"{__spec__.parent}.names", "functools", "operator"}

import functools
import operator
import sys
import typing
from typing import Any, Generic, TextIO, TypeVar

from .names import color_names, default_styles
from .styles import ColorNotFound, Style

if typing.TYPE_CHECKING:
    from collections.abc import Iterable, Iterator, Mapping

    from plumbum._compat.typing import Self

__all__ = ["ColorFactory", "StyleFactory"]


def __dir__() -> list[str]:
    return list(__all__)


S = TypeVar("S", bound=Style)


[docs] class ColorFactory(Generic[S]): """This creates color names given fg = True/False. It usually will be called as part of a StyleFactory."""
[docs] def __init__(self, fg: bool, style: type[S]): self._fg = fg self._style = style self.reset = style.from_color(style.color_class(fg=fg)) # Adding the color name shortcuts for foreground colors for item in color_names[:16]: setattr( self, item, style.from_color(style.color_class.from_simple(item, fg=fg)) )
[docs] def __getattr__(self, item: str) -> S: """Full color names work, but do not populate __dir__.""" try: return self._style.from_color(self._style.color_class(item, fg=self._fg)) except ColorNotFound: raise AttributeError(item) from None
[docs] def full(self, name: str | int) -> S: """Gets the style for a color, using standard name procedure: either full color name, html code, or number.""" return self._style.from_color( self._style.color_class.from_full(name, fg=self._fg) )
[docs] def simple(self, name: str | int) -> S: """Return the extended color scheme color for a value or name.""" return self._style.from_color( self._style.color_class.from_simple(name, fg=self._fg) )
[docs] def rgb(self, r: str | int, g: int | None = None, b: int | None = None) -> S: """Return the extended color scheme color for a value.""" if g is None and b is None and isinstance(r, str): return self.hex(r) return self._style.from_color(self._style.color_class(r, g, b, fg=self._fg))
[docs] def hex(self, hexcode: str) -> S: """Return the extended color scheme color for a value.""" return self._style.from_color( self._style.color_class.from_hex(hexcode, fg=self._fg) )
[docs] def ansi(self, ansiseq: str) -> S: """Make a style from an ansi text sequence""" return self._style.from_ansi(ansiseq)
@typing.overload def __getitem__(self, val: slice) -> list[S]: ... @typing.overload def __getitem__(self, val: tuple[int, int, int] | str | int) -> S: ...
[docs] def __getitem__(self, val: slice | tuple[int, int, int] | str | int) -> list[S] | S: """\ Shortcut to provide way to access colors numerically or by slice. If end <= 16, will stay to simple ANSI version.""" if isinstance(val, slice): (start, stop, stride) = val.indices(256) if stop <= 16: return [self.simple(v) for v in range(start, stop, stride)] return [self.full(v) for v in range(start, stop, stride)] if isinstance(val, tuple): return self.rgb(*val) try: return self.full(val) except ColorNotFound: assert not isinstance(val, int) return self.hex(val)
[docs] def __call__( self, val_or_r: str | int | None = None, g: int | None = None, b: int | None = None, ) -> S: """Shortcut to provide way to access colors.""" if val_or_r is None or (isinstance(val_or_r, str) and val_or_r == ""): return self._style() if isinstance(val_or_r, self._style): return self._style(val_or_r) if isinstance(val_or_r, str) and "\033" in val_or_r: return self.ansi(val_or_r) return self._style.from_color( self._style.color_class(val_or_r, g, b, fg=self._fg) )
[docs] def __iter__(self) -> Iterator[S]: """Iterates through all colors in extended colorset.""" return (self.full(i) for i in range(256))
[docs] def __invert__(self) -> S: """Allows clearing a color with ~""" return self.reset
[docs] def __enter__(self) -> Self: """This will reset the color on leaving the with statement.""" return self
[docs] def __exit__(self, _type: object, _value: object, _traceback: object) -> None: """This resets a FG/BG color or all styles, due to different definition of RESET for the factories.""" self.reset.now()
[docs] def __repr__(self) -> str: """Simple representation of the class by name.""" return f"<{self.__class__.__name__}>"
[docs] class StyleFactory(ColorFactory[S]): """Factory for styles. Holds font styles, FG and BG objects representing colors, and imitates the FG ColorFactory to a large degree."""
[docs] def __init__(self, style: type[S]): super().__init__(True, style) self.fg = ColorFactory(True, style) self.bg = ColorFactory(False, style) self.do_nothing = style() self.reset = style(reset=True) for item in style.attribute_names: setattr(self, item, style(attributes={item: True})) self.load_stylesheet(default_styles)
@property def use_color(self) -> int: """Shortcut for setting color usage on Style""" return self._style.use_color @use_color.setter def use_color(self, val: int) -> None: self._style.use_color = val
[docs] def from_ansi(self, ansi_sequence: str) -> S: """Calling this is a shortcut for creating a style from an ANSI sequence.""" return self._style.from_ansi(ansi_sequence)
[docs] def from_ansi_string(self, ansi_string: str) -> Iterator[S | str]: """Calling this is a shortcut for creating a style from an ANSI string. .. versionadded:: 2.0 """ return self._style.from_ansi_string(ansi_string)
[docs] def sequence_to_string(self, sequence: Iterable[S | str]) -> str: """Converts a sequence of styles and strings into a single string with ANSI codes. .. versionadded:: 2.0 """ return self._style.sequence_to_string(sequence)
@property def stdout(self) -> TextIO: """This is a shortcut for getting stdout from a class without an instance.""" return self._style._stdout if self._style._stdout is not None else sys.stdout @stdout.setter def stdout(self, newout: TextIO) -> None: self._style._stdout = newout
[docs] def get_colors_from_string(self, color: str = "") -> S: """ Sets color based on string, use `.` or space for separator, and numbers, fg/bg, htmlcodes, etc all accepted (as strings). """ names = color.replace(".", " ").split() # TODO: check the logic here to see what prev can be prev: Any = self styleslist: list[S] = [] for name in names: try: prev = getattr(prev, name) except AttributeError: try: prev = prev(int(name)) except (ColorNotFound, ValueError): prev = prev(name) if isinstance(prev, self._style): styleslist.append(prev) prev = self if styleslist: prev = functools.reduce(operator.and_, styleslist) return prev if isinstance(prev, self._style) else prev.reset
[docs] def filter(self, colored_string: str) -> str: """Filters out colors in a string, returning only the name.""" if isinstance(colored_string, self._style): return colored_string return self._style.string_filter_ansi(colored_string)
[docs] def contains_colors(self, colored_string: str) -> bool: """Checks to see if a string contains colors.""" return self._style.string_contains_colors(colored_string)
[docs] def extract(self, colored_string: str) -> S: """Gets colors from an ansi string, returns those colors""" return self._style.from_ansi(colored_string, True)
def load_stylesheet(self, stylesheet: Mapping[str, str] | None = None) -> None: if stylesheet is None: stylesheet = default_styles for item in stylesheet: setattr(self, item, self.get_colors_from_string(stylesheet[item]))