Source code for svgen.color.theme

"""
A module for working with color themes.
"""

# built-in
from collections import UserDict
from collections.abc import Mapping
from typing import MutableMapping, NamedTuple, Optional, cast

# third-party
from vcorelib.dict import GenericStrDict
from vcorelib.io import ARBITER, DEFAULT_INCLUDES_KEY
from vcorelib.namespace import Namespace, NamespaceMixin
from vcorelib.paths import Pathlike, normalize

# internal
from svgen.color import Color, Colorlike

ColorTokens = dict[str, Color]


[docs] class ColorToken(NamedTuple): """A simple pairing of a color with a token.""" token: str color: Color def __eq__(self, other) -> bool: """Determine if a color token is equivalent to something else.""" if hasattr(other, "color"): other = other.color return bool(self.color == other) def __str__(self) -> str: """Get this token as a color string.""" return str(self.color)
[docs] @staticmethod def create(key: Colorlike) -> "ColorToken": """Create a color token from a color.""" return ColorToken(str(key), Color.create(key))
[docs] class ColorTheme( UserDict, # type: ignore NamespaceMixin, MutableMapping[str, Color], ): """A class implementing a theme color interface.""" data: ColorTokens def __init__( self, name: str, initialdata: dict[str, Colorlike] = None, namespace: Namespace = None, ) -> None: """Initialize this color theme.""" NamespaceMixin.__init__(self, namespace=namespace) self.name = name self.push_name(name) if initialdata is None: initialdata = {} # Ensure we're storing real color objects. UserDict.__init__( self, { self.namespace(key): Color.create(val) for key, val in initialdata.items() }, )
[docs] def lookup(self, key: str) -> tuple[str, Optional[Color]]: """Attempt to find an existing color in this theme by key.""" result = None token = key # Always check the current namespace first. namespaced = self.namespace(key) if namespaced in self.data: result = self.data[namespaced] token = namespaced # Check the global namespace. elif key in self.data: result = self.data[key] return token, result
def __getitem__(self, key: str) -> Color: """Get a color from this theme.""" key, _ = self.lookup(key) return self.data[key]
[docs] def resolve(self, key: str, strict: bool = False) -> ColorToken: """Attempt to resolve a color key as a theme color.""" token, color = self.lookup(key) if color is None: assert not strict, f"Can't resolve key '{key}' as a color!" color = Color.from_ctor(key) return ColorToken(token, color)
[docs] def create(self, color: Colorlike) -> Color: """ Attempt to create a color through this theme so that existing tokens can be used as color aliases. """ if isinstance(color, str): _, color = self.resolve(color) if color is not None: return color return Color.create(color)
[docs] def add(self, key: str, color: Colorlike) -> ColorToken: """Add a new color to this theme.""" namespaced = self.namespace(key) color = self.create(color) if namespaced in self: assert self.data[namespaced] == color, ( f"Can't add over color '{namespaced}'! " f"{self.data[namespaced]} != {color}" ) else: self.data[namespaced] = color return ColorToken(namespaced, color)
[docs] def add_mapping(self, data: GenericStrDict) -> None: """Add a mapping of tokens and colors to this theme.""" for key, val in data.items(): key = str(key) # Ensure that we recurse into dictionaries. if isinstance(val, Mapping): with self.names_pushed(key): self.add_mapping(cast(GenericStrDict, val)) else: # Add leaf nodes as actual colors. self.add(key, val)
[docs] @staticmethod def from_path(path: Pathlike) -> "ColorTheme": """Load a color theme from a data file on disk.""" path = normalize(path) assert path.is_file(), f"No file '{path}'!" theme = ColorTheme(path.with_suffix("").name) theme.add_mapping( ARBITER.decode( path, includes_key=DEFAULT_INCLUDES_KEY, require_success=True ).data ) return theme
@property def size(self) -> int: """Get the number of colors in this theme.""" return len(self.data.keys())