Source code for meerkat.interactive.formatter.base

from __future__ import annotations

import collections
from abc import ABC, abstractmethod, abstractproperty
from typing import TYPE_CHECKING, Any, Dict, Iterator, List, Type, Union

import yaml

from meerkat.columns.deferred.base import DeferredCell
from meerkat.tools.utils import MeerkatDumper, MeerkatLoader

if TYPE_CHECKING:
    from meerkat.interactive.app.src.lib.component.abstract import BaseComponent


class Variant:
    def __init__(*args, **kwargs):
        pass


[docs]class FormatterGroup(collections.abc.Mapping): """A formatter group is a mapping from formatter placeholders to formatters. Data in a Meerkat column sometimes need to be displayed differently in different GUI contexts. For example, in a table, we display thumbnails of images, but in a carousel view, we display the full image. Because most components in Meerkat work on any data type, it is important that they are implemented in a formatter-agnostic way. So, instead of specifying formatters, components make requests for data specifying a *formatter placeholder*. For example, the {class}`mk.gui.Gallery` component requests data using the `thumbnail` formatter placeholder. For a specific column of data, we specify which formatters to use for each placeholder using a *formatter group*. A formatter group is a mapping from formatter placeholders to formatters. Each column in Meerkat has a `formatter_group` property. A column's formatter group controls how it will be displayed in different contexts in Meerkat GUIs. Args: base (FormatterGroup): The base formatter group to use. **kwargs: The formatters to add to the formatter group. """ def __init__(self, base: BaseFormatter = None, **kwargs): if base is None: from meerkat.interactive.formatter import TextFormatter # everything has a str method so this is a safe default base = TextFormatter() if not isinstance(base, BaseFormatter): raise TypeError("base must be a Formatter") for key, value in kwargs.items(): if key not in formatter_placeholders: raise ValueError( f"The key {key} is not a registered formatter " "placeholder. Use `mk.register_formatter_placeholder`" ) if not isinstance(value, BaseFormatter): raise TypeError( f"FormatterGroup values must be Formatters, not {type(value)}" ) # must provide a base formatter self._dict = dict(base=base, **kwargs) def __getitem__(self, key: Union[FormatterPlaceholder, str]) -> BaseFormatter: """Get the formatter for the given formatter placeholder. Args: key (FormatterPlaceholder): The formatter placeholder. Returns: (Formatter) The formatter for the formatter placeholder. """ if isinstance(key, str): if key in formatter_placeholders: key = formatter_placeholders[key] else: key = FormatterPlaceholder(key, []) if key.name in self._dict: return self._dict.__getitem__(key.name) for fallback in key.fallbacks: if fallback.name in self._dict: return self.__getitem__(fallback) return self._dict["base"] def __setitem__( self, key: Union[FormatterPlaceholder, str], value: BaseFormatter ) -> None: if key not in formatter_placeholders: raise ValueError( f"The key {key} is not a registered formatter " "placeholder. Use `mk.register_formatter_placeholder`" ) if not isinstance(value, BaseFormatter): raise TypeError( f"FormatterGroup values must be Formatters, not {type(value)}" ) return self._dict.__setitem__(key, value) def defer(self): return deferred_formatter_group(self) def __len__(self) -> int: return len(self._dict) def __iter__(self) -> Iterator: return iter(self._dict) def update(self, other: Union[FormatterGroup, Dict]): self._dict.update(other) def copy(self): new = self.__class__.__new__(self.__class__) new._dict = self._dict.copy() return new
[docs] @staticmethod def to_yaml(dumper: yaml.Dumper, data: BaseFormatter): """This function is called by the YAML dumper to convert a :class:`Formatter` object into a YAML node. It should not be called directly. """ data = { "class": type(data), "dict": data._dict, } return dumper.represent_mapping("!FormatterGroup", data)
[docs] @staticmethod def from_yaml(loader, node): """This function is called by the YAML loader to convert a YAML node into an :class:`Formatter` object. It should not be called directly. """ data = loader.construct_mapping(node) formatter = data["class"].__new__(data["class"]) formatter._dict = data["dict"] return formatter
MeerkatDumper.add_multi_representer(FormatterGroup, FormatterGroup.to_yaml) MeerkatLoader.add_constructor("!FormatterGroup", FormatterGroup.from_yaml) def deferred_formatter_group(group: FormatterGroup) -> FormatterGroup: """Wrap all formatters in a FormatterGroup with a DeferredFormatter. Args: group (FormatterGroup): The FormatterGroup to wrap. Returns: (FormatterGroup) A new FormatterGroup with all formatters wrapped in a DeferredFormatter. """ new_group = group.copy() for name, formatter in group.items(): new_group[name] = DeferredFormatter(formatter) return new_group class FormatterPlaceholder: def __init__( self, name: str, fallbacks: List[Union[str, FormatterPlaceholder]], description: str = "", ): global formatter_placeholders self.name = name self.fallbacks = [ fb if isinstance(fb, FormatterPlaceholder) else formatter_placeholders[fb] for fb in fallbacks ] if name != "base": self.fallbacks.append(FormatterPlaceholder("base", fallbacks=[])) self.description = description formatter_placeholders = { "base": FormatterPlaceholder("base", []), } def register_placeholder( name: str, fallbacks: List[FormatterPlaceholder] = [], description: str = "" ): """Register a new formatter placeholder. Args: name (str): The name of the formatter placeholder. fallbacks (List[FormatterPlaceholder]): The fallbacks for the formatter placeholder. description (str): A description of the formatter placeholder. """ if name in formatter_placeholders: raise ValueError(f"{name} is already a registered formatter placeholder") formatter_placeholders[name] = FormatterPlaceholder( name=name, fallbacks=fallbacks, description=description ) # register core formatter placeholders register_placeholder("small", fallbacks=[], description="A small version of the data.") register_placeholder( "tiny", fallbacks=["small"], description="A tiny version of the data." ) register_placeholder( "thumbnail", fallbacks=["small"], description="A thumbnail of the data." ) register_placeholder( "icon", fallbacks=["tiny"], description="An icon representing the data." ) register_placeholder( "tag", fallbacks=["tiny"], description="A small version of the data meant to go in a tag field.", ) register_placeholder( "full", fallbacks=["base"], description="A full version of the data.", )
[docs]class BaseFormatter(ABC): component_class: Type["BaseComponent"] data_prop: str = "data" static_encode: bool = False
[docs] def encode(self, cell: Any, **kwargs): """Encode the cell on the backend before sending it to the frontend. The cell is lazily loaded, so when used on a LambdaColumn, ``cell`` will be a ``LambdaCell``. This is important for displays that don't actually need to apply the lambda in order to display the value. """ return cell
@abstractproperty def props(self): return self._props @abstractmethod def _get_state(self) -> Dict[str, Any]: pass @abstractmethod def _set_state(self, state: Dict[str, Any]): pass
[docs] @staticmethod def to_yaml(dumper: yaml.Dumper, data: BaseFormatter): """This function is called by the YAML dumper to convert a :class:`Formatter` object into a YAML node. It should not be called directly. """ data = { "class": type(data), "state": data._get_state(), } return dumper.represent_mapping("!Formatter", data)
[docs] @staticmethod def from_yaml(loader, node): """This function is called by the YAML loader to convert a YAML node into an :class:`Formatter` object. It should not be called directly. """ data = loader.construct_mapping(node, deep=True) formatter = data["class"].__new__(data["class"]) formatter._set_state(data["state"]) return formatter
[docs] def html(self, cell: Any): """When not in interactive mode, objects are visualized using static html. This method should produce that static html for the cell. """ return str(cell)
class Formatter(BaseFormatter): # TODO: set the signature of the __init__ so it works with autocomplete and docs def __init__(self, **kwargs): for k in kwargs: if k not in self.component_class.prop_names: raise ValueError(f"{k} is not a valid prop for {self.component_class}") for prop_name, field in self.component_class.__fields__.items(): if field.name != self.data_prop and prop_name not in kwargs: if field.required: raise ValueError("""Missing required argument.""") kwargs[prop_name] = field.default self._props = kwargs def encode(self, cell: str): return cell @property def props(self) -> Dict[str, Any]: return self._props def _get_state(self) -> Dict[str, Any]: return { "_props": self._props, } def _set_state(self, state: Dict[str, Any]): self._props = state["_props"] MeerkatDumper.add_multi_representer(BaseFormatter, BaseFormatter.to_yaml) MeerkatLoader.add_constructor("!Formatter", BaseFormatter.from_yaml) class DeferredFormatter(BaseFormatter): def __init__(self, formatter: BaseFormatter): self.wrapped = formatter def encode(self, cell: DeferredCell, **kwargs): if self.wrapped.static_encode: return self.wrapped.encode(None, **kwargs) return self.wrapped.encode(cell(), **kwargs) @property def component_class(self): return self.wrapped.component_class @property def data_prop(self): return self.wrapped.data_prop @property def props(self): return self.wrapped.props def html(self, cell: DeferredCell): return self.wrapped.html(cell()) def _get_state(self): data = { "wrapped": { "class": self.wrapped.__class__, "state": self.wrapped._get_state(), }, } return data def _set_state(self, state: Dict[str, Any]): wrapped_state = state["wrapped"] wrapped = wrapped_state["class"].__new__(wrapped_state["class"]) wrapped._set_state(wrapped_state["state"]) self.wrapped = wrapped MeerkatDumper.add_multi_representer(DeferredFormatter, DeferredFormatter.to_yaml) MeerkatLoader.add_constructor("!DeferredFormatter", DeferredFormatter.from_yaml)