Source code for vivyd.data.container

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Any, Iterable
from textwrap import dedent
from json import dumps


[docs] @dataclass(kw_only=True) class Container(Iterable[tuple[str, Any]]): """ A base class for hierarchical containers that can hold other containers or general data objects. Parameters ---------- name : str A name for the container. This can be used to identify the container in a hierarchical structure. info : dict[str, Any] A dictionary containing arbitrary metadata about the container. This can include any relevant information that describes the container or its contents. Tip ___ You can easily iterate over the entries in a container, the same way you would iterate over a dictionary. For example, if `c` is a container, .. code-block:: python for key, entry in c: print(f"Key: {key}, Entry: {entry}") """ name : str """The name of the container.""" info : dict[str, Any] """A dictionary containing arbitrary metadata about the container.""" contents: dict[str, Any] = field(default_factory=dict, init=False) """A dictionary that holds the contents of the container.""" def _clean_info(self, info: dict[str, Any]) -> dict[str, Any]: """Recursively clean string fields in the info dictionary.""" cleaned = {} for key, value in info.items(): if isinstance(value, str): cleaned[key] = dedent(value).strip() elif isinstance(value, dict): cleaned[key] = self._clean_info(value) else: cleaned[key] = value return cleaned def __post_init__(self): object.__setattr__(self, "name", dedent(self.name).strip()) object.__setattr__(self, "info", self._clean_info(self.info))
[docs] def add(self, key: str, value: Any): """ Add an entry to the container. Parameters ---------- key : str The key for the entry. value : Any The value for the entry. Raises ------ ValueError If an entry with the specified key already exists in the container. Tip --- The method supports chainable calls, so you can add multiple contents in a single statement. For example: .. code-block:: python container.add(content1).add(content2).add(content3) """ if key in self.contents: raise ValueError(f"Entry with key '{key}' already exists in this container.") self.contents[key] = value
[docs] def keys(self) -> list[str]: """ Returns ------- list[str] A list of keys for the entries in this container. """ return list(self.contents.keys())
[docs] def tree(self, indent: int = 2) -> str: """ Parameters ---------- indent : int, optional The number of spaces to use for each level of indentation in the tree representation. Default is 2. Returns ------- str A string representing the hierarchical tree structure of the container and its contents. """ lines: list[str] = [] prefix = " " * indent lines.append(f"{prefix}<{self.__class__.__name__}> {self.name}") for item in self.contents.values(): if hasattr(item, "tree"): # Recursively print containers lines.append(item.tree(indent + 1)) return "\n".join(lines)
[docs] def print_tree(self): """Print the hierarchical tree of container entries, as defined by :meth:`tree`.""" print(self.tree())
[docs] def to_json(self, *, indent: int = 2, ensure_ascii: bool = False) -> str: """ Parameters ---------- indent : int, optional The number of spaces to use for each level of indentation in the JSON output. Default is 2. ensure_ascii : bool, optional Whether to escape non-ASCII characters in the JSON output. Default is `False`. Returns ------- str A JSON string representing the container and its contents. """ return dumps(self, indent=indent, ensure_ascii=ensure_ascii, default=lambda o: o.__dict__)
[docs] def print_json(self): """ Print the container and its contents as formatted JSON. Parameters ---------- indent : int, optional The number of spaces to use for each level of indentation in the JSON output. Default is 2. ensure_ascii : bool, optional Whether to escape non-ASCII characters in the JSON output. Default is `False`. """ print(self.to_json())
[docs] def get(self, key: str) -> Any: """ Parameters ---------- key : str The key of the entry to retrieve from the container. Returns ------- Any The value associated with the specified key in the container. Tip --- A container object can be directly indexed to access its contents. In other words, the following two lines are equivalent for a container `c`: .. code-block:: python c.get("k") c["k"] where `"k"` is the key of an entry in the container. """ if not key in self.contents: raise KeyError(f"No entry with key '{key}' found in this container.") return self.contents[key]
def __getitem__(self, key: str) -> Any: return self.get(key) def __str__(self) -> str: text = dumps(self, indent=2, ensure_ascii=False, default=lambda o: o.__dict__) return text.replace("\\n", "\n").replace("\\t", "\t") @property def __dict__(self) -> dict[str, Any]: return { "name" : self.name, "info" : self.info, "type" : self.__class__.__name__, "contents": [c.__dict__ for c in self.contents.values()], "keys" : self.keys(), } def __iter__(self): for key, value in self.contents.items(): yield key, value