Source code for sierra.core.experiment.definition

# Copyright 2022 John Harwell, All rights reserved.
#
#  SPDX-License-Identifier: MIT

"""
Functionality for reading, writing, etc., experiment definitions.

Format-specific bits handled at a lower level via plugin; this is generic
functionality for experiment definitions.
"""

# Core packages
import pathlib
import typing as tp
import logging
import pickle

# 3rd party packages

# Project packages
from sierra.core import types


[docs] class WriterConfig: """Config for writing :class:`~sierra.core.experiment.definition.BaseExpDef`. Different parts of the AST can be written to multiple files, as configured. The order of operations for the applying the config should be: - Extraction of subtree - Renaming subtree root - Adding new children - Adding grafts Attributes: values: Dict with the following possible key, value pairs: - ``src_parent`` - The parent of the root of the AST specifying a sub-tree to write out as a child of ``new_children_parent``, or ``None`` to write out entire AST. This key is required. If ``None`` omitted then then tree rooted at ``src_tag`` is written out. Otherwise, the subtree rooted at ``<src_parent>/<src_tag>`` is written out instead. - ``src_tag`` - Unique query path expression for the child element within ``src_parent`` to write out; that is this tag is the root of the sub-tree within the experiment definition to write out. This key is required. - ``rename_to`` - String to rename the root of the AST written out. This key is optional, and should be processed *after* ``{src_parent, src_tag}``. - ``new_children_parent`` - Unique query path expression for the parent element to create new child elements under via ``new_children``. This key is optional; can be omitted or set to ``None``. - ``new_children`` - Ordered List of :class:`~sierra.core.experiment.definition.ElementAdd` objects to use to create new child elements under ``new_children_parent``. Must form a tree with a single root when added in order. - ``opath_leaf`` - Additional bits added to whatever the opath file stem that is set for the :class:`~sierra.core.experiment.definition.BaseExpDef` instance. This key is optional. Can be used to add an extension; this is helpful because some engines require input files to have a certain extension, and SIERRA strips out the extension passed to ``--expdef-template`` used as the bases for creating experiments. - ``child_grafts_parent`` - Unique query path expression for the parent element to for grafting elements under via ``child_grafts``. This path expression is *relative* to ``<src_tag>`` due to ordering. This key is optional; can be omitted or set to ``None``. - ``child_grafts`` - Additional bits of the current AST to add under ``child_grafts_parent`` in the written out experiment definition, specified as a list of query path strings. This key is optional. """ def __init__(self, values: list[dict]) -> None: assert isinstance(values, list), "values must be a list of dicts" self.values = values def add(self, value: dict) -> None: self.values.append(value)
[docs] class BaseExpDef: """Base class for experiment definitions.""" def __init__( self, input_fpath: pathlib.Path, write_config: tp.Optional[WriterConfig] = None ) -> None: pass
[docs] def write(self, base_opath: pathlib.Path) -> None: """Write the definition stored in the object to the filesystem.""" raise NotImplementedError
[docs] def n_mods(self) -> tuple[int, int]: """ Get the # (adds, changes) as a tuple. """ raise NotImplementedError
[docs] def flatten(self, keys: list[str]) -> None: """ Replace the specified filepath attributes with their contents. Filepaths are interpreted relative to the directory in which the original experiment definition template resides, and assumed to be defined as such. """ raise NotImplementedError
[docs] def attr_get(self, path: str, attr: str) -> tp.Union[str, int, float, None]: """Retrieve the specified attribute of the element at the specified path. If it does not exist, None is returned. """ raise NotImplementedError
[docs] def attr_change( self, path: str, attr: str, value: tp.Union[str, int, float], noprint: bool = False, ) -> bool: """Change the specified attribute of the element at the specified path. Only the attribute of the *FIRST* element matching the specified path is changed. Arguments: path: An expression uniquely identifying the element containing the attribute to change. The element must exist or an error will be raised. attr: An expression uniquely identify the attribute to change within the enclosing element. value: The value to set the attribute to. """ raise NotImplementedError
[docs] def attr_add( self, path: str, attr: str, value: tp.Union[str, int, float], noprint: bool = False, ) -> bool: """Add the specified attribute to the element matching the specified path. Only the *FIRST* element matching the specified path searching from the tree root is modified. Arguments: path: An expression uniquely identifying the element containing the attribute to add. The element must exist or an error will be raised. attr: An expression uniquely identifying the attribute to change within the enclosing element. value: The value to set the attribute to. """ raise NotImplementedError
[docs] def has_element(self, path: str) -> bool: """Determine if the element uniquely identified by ``path`` exists.""" raise NotImplementedError
[docs] def has_attr(self, path: str, attr: str) -> bool: """Determine if the attribute uniquely identified by ``path`` exists.""" raise NotImplementedError
[docs] def element_change(self, path: str, tag: str, value: str) -> bool: """ Change the specified tag of the element matching the specified path. Arguments: path: An expression uniquely identifying the element containing the tag to change. The element must exist or an error will be raised. tag: An expression uniquely identifying the tag to change within the enclosing element. value: The value to set the tag to. """ raise NotImplementedError
[docs] def element_remove(self, path: str, tag: str, noprint: bool = False) -> bool: """Remove the specified child ``tag`` in the enclosing parent. If more than one tag matches, only one is removed. If the path does not exist, nothing is done. Arguments: path: An expression uniquely identifying the element containing the tag to remove. The element must exist or an error will be raised. tag: An expression uniquely identifying the tag to remove within the enclosing element. """ raise NotImplementedError
[docs] def element_remove_all(self, path: str, tag: str, noprint: bool = False) -> bool: """Remove the specified child tag(s) in the enclosing parent. If more than one tag matches in the parent, all matching child tags are removed. Arguments: path: An expression uniquely identifying the element containing the tag(s) to remove. The element must exist or an error will be raised. tag: An expression uniquely identifying the tag to remove within the enclosing element. """ raise NotImplementedError
[docs] def element_add( self, path: str, tag: str, attr: tp.Optional[types.StrDict] = None, allow_dup: bool = True, noprint: bool = False, ) -> bool: """ Add tag name as a child element of enclosing parent. """ raise NotImplementedError
[docs] class AttrChange: """ Specification for a change to an existing expdef attribute. """ def __init__(self, path: str, attr: str, value: tp.Union[str, int, float]) -> None: self.path = path self.attr = attr self.value = value def __iter__(self): yield from [self.path, self.attr, self.value] def __repr__(self) -> str: return self.path + "/" + self.attr + ": " + str(self.value)
[docs] class NullMod: """ Specification for a null-change (no change) to an existing expdef. """ def __init__(self) -> None: pass def __iter__(self): yield from []
[docs] class ElementRm: """ Specification for removal of an existing expdef tag. """ def __init__(self, path: str, tag: str): """ Init the object. Arguments: path: The path to the **parent** of the tag you want to remove, in relevant syntax. tag: The name of the tag to remove. """ self.path = path self.tag = tag def __iter__(self): yield from [self.path, self.tag] def __repr__(self) -> str: return self.path + "/" + self.tag
[docs] class ElementAdd: """ Specification for adding a new expdef tag. The tag may be added idempotently, or duplicates can be allowed. """ @staticmethod def as_root(tag: str, attr: types.StrDict) -> "ElementAdd": return ElementAdd("", tag, attr, False, True) def __init__( self, path: str, tag: str, attr: types.StrDict, allow_dup: bool, as_root: bool = False, ): """ Init the object. Arguments: path: The path to the **parent** tag you want to add a new tag under, in appropriate syntax. If None, then the tag will be added as the root tag. tag: The name of the tag to add. attr: A dictionary of (attribute, value) pairs to also create as children of the new tag when creating the new tag. """ self.path = path self.tag = tag self.attr = attr self.allow_dup = allow_dup self.as_root_elt = as_root def __iter__(self): yield from [self.path, self.tag, self.attr] def __repr__(self) -> str: return self.path + "/" + self.tag + ": " + str(self.attr)
[docs] class AttrChangeSet: """ Data structure for :class:`AttrChange` objects. The order in which attributes are changed doesn't matter from the standpoint of correctness (i.e., different orders won't cause crashes). """
[docs] @staticmethod def unpickle(fpath: pathlib.Path) -> "AttrChangeSet": """Unpickle changes. You don't know how many there are, so go until you get an exception. """ exp_def = AttrChangeSet() try: with fpath.open("rb") as f: while True: exp_def |= AttrChangeSet(*pickle.load(f)) except EOFError: pass return exp_def
def __init__(self, *args: tp.Union[AttrChange, NullMod]) -> None: self.changes = set(args) self.logger = logging.getLogger(__name__) def __len__(self) -> int: return len(self.changes) def __iter__(self) -> tp.Iterator[tp.Union[AttrChange, NullMod]]: return iter(self.changes) def __ior__(self, other: "AttrChangeSet") -> "AttrChangeSet": self.changes |= other.changes return self def __or__(self, other: "AttrChangeSet") -> "AttrChangeSet": new = AttrChangeSet(*self.changes) new |= other return new def __repr__(self) -> str: return str(self.changes) def add(self, chg: AttrChange) -> None: self.changes.add(chg) def pickle(self, fpath: pathlib.Path, delete: bool = False) -> None: from sierra.core import utils # noqa: PLC0415 if delete and utils.path_exists(fpath): fpath.unlink() with fpath.open("ab") as f: utils.pickle_dump(self.changes, f)
[docs] class ElementRmList: """ Data structure for :class:`ElementRm` objects. The order in which tags are removed matters (i.e., if you remove dependent tags in the wrong order you will get an exception), hence the list representation. """ def __init__(self, *args: ElementRm) -> None: self.rms = list(args) def __len__(self) -> int: return len(self.rms) def __iter__(self) -> tp.Iterator[ElementRm]: return iter(self.rms) def __repr__(self) -> str: return str(self.rms) def extend(self, other: "ElementRmList") -> None: self.rms.extend(other.rms) def append(self, other: ElementRm) -> None: self.rms.append(other) def pickle(self, fpath: pathlib.Path, delete: bool = False) -> None: from sierra.core import utils # noqa: PLC0415 if delete and utils.path_exists(fpath): fpath.unlink() with fpath.open("ab") as f: utils.pickle_dump(self.rms, f)
[docs] class ElementAddList: """ Data structure for :class:`ElementAdd` objects. The order in which tags are added matters (i.e., if you add dependent tags in the wrong order you will get an exception), hence the list representation. """
[docs] @staticmethod def unpickle(fpath: pathlib.Path) -> tp.Optional["ElementAddList"]: """Unpickle modifications. You don't know how many there are, so go until you get an exception. """ exp_def = ElementAddList() try: with fpath.open("rb") as f: while True: exp_def.append(*pickle.load(f)) except EOFError: pass return exp_def
def __init__(self, *args: ElementAdd) -> None: self.adds = list(args) def __len__(self) -> int: return len(self.adds) def __iter__(self) -> tp.Iterator[ElementAdd]: return iter(self.adds) def __repr__(self) -> str: return str(self.adds) def extend(self, other: "ElementAddList") -> None: self.adds.extend(other.adds) def append(self, other: ElementAdd) -> None: self.adds.append(other) def prepend(self, other: ElementAdd) -> None: self.adds.insert(0, other) def pickle(self, fpath: pathlib.Path, delete: bool = False) -> None: from sierra.core import utils # noqa: PLC0415 if delete and utils.path_exists(fpath): fpath.unlink() with fpath.open("ab") as f: utils.pickle_dump(self.adds, f)
__all__ = [ "AttrChange", "AttrChangeSet", "BaseExpDef", "ElementAdd", "ElementAddList", "ElementRm", "ElementRmList", "NullMod", "WriterConfig", ]