Source code for varats.paper_mgmt.artefacts
"""
This module allows to attach :class:`artefact definitions<Artefact>` to a.
:class:`paper config<varats.paper.paper_config>`. This way, the artefacts,
like :class:`plots<PlotArtefact>` or result tables, can be generated from
result files automatically.
Typically, a paper config has a file ``artefacts.yaml`` that manages artefact
definitions.
"""
import abc
import logging
import typing as tp
from abc import ABC
from functools import lru_cache
from pathlib import Path
from varats.base.version_header import VersionHeader
from varats.utils.settings import vara_cfg
from varats.utils.yaml_util import load_yaml, store_as_yaml
if tp.TYPE_CHECKING:
from rich.progress import Progress # pylint: disable=unused-import
import varats.paper.case_study as cs # pylint: disable=unused-import
import varats.paper.paper_config as pc # pylint: disable=unused-import
LOG = logging.getLogger(__name__)
[docs]
class ArtefactFileInfo():
"""Class containing metadata about a file generated by an artefact."""
def __init__(
self, file_name: str, case_study: tp.Optional['cs.CaseStudy'] = None
):
self.__file_name = file_name
self.__case_study = case_study
@property
def file_name(self) -> str:
"""The name of the generated file."""
return self.__file_name
@property
def case_study(self) -> tp.Optional['cs.CaseStudy']:
"""The used case study if available."""
return self.__case_study
[docs]
class Artefact(ABC):
"""
An ``Artefact`` contains all information that is necessary to generate a
certain artefact. Subclasses of this class specify concrete artefact types,
like :class:`plots<PlotArtefact>`, that require additional attributes.
Args:
name: name of this artefact
output_dir: output dir relative to config value
'artefacts/artefacts_dir'
"""
ARTEFACT_TYPE = "Artefact"
ARTEFACT_TYPE_VERSION = 0
ARTEFACT_TYPES: tp.Dict[str, tp.Type['Artefact']] = {}
def __init__(self, name: str, output_dir: Path) -> None:
self.__name = name
self.__output_dir = output_dir
@classmethod
def __init_subclass__(
cls, *, artefact_type: str, artefact_type_version: int, **kwargs: tp.Any
) -> None:
"""Register Artefact implementations."""
super().__init_subclass__(**kwargs)
cls.ARTEFACT_TYPE = artefact_type
cls.ARTEFACT_TYPE_VERSION = artefact_type_version
cls.ARTEFACT_TYPES[cls.ARTEFACT_TYPE] = cls
[docs]
@staticmethod
def base_output_dir() -> Path:
"""Base output dir for artefacts."""
return Path(str(vara_cfg()['artefacts']['artefacts_dir'])
) / Path(str(vara_cfg()['paper_config']['current_config']))
@property
def name(self) -> str:
"""
The name of this artefact.
This uniquely identifies an artefact in an
:class:`Artefacts` collection.
"""
return self.__name
@property
def output_dir(self) -> Path:
"""Absolute path to the artefact's output directory."""
return Artefact.base_output_dir() / self.__output_dir
[docs]
def get_dict(self) -> tp.Dict[str, tp.Any]:
"""
Construct a dict from this artefact for easy export to yaml.
Subclasses should first call this function on ``super()`` and then
extend the returned dict with their own properties.
Returns:
A dict representation of this artefact.
"""
return {
'artefact_type': self.ARTEFACT_TYPE,
'artefact_type_version': self.ARTEFACT_TYPE_VERSION,
'name': self.__name,
'output_dir': str(self.__output_dir)
}
[docs]
@staticmethod
@abc.abstractmethod
def create_artefact(
name: str, output_dir: Path, **kwargs: tp.Any
) -> 'Artefact':
"""
Instantiate an artefact from its dict representation.
Args:
name: name of this artefact
output_dir: output dir relative to config value
'artefacts/artefacts_dir'
**kwargs: artefact-specific arguments
Returns:
an instantiated artefact
"""
[docs]
@abc.abstractmethod
def generate_artefact(
self, progress: tp.Optional["Progress"] = None
) -> None:
"""Generate the specified artefact."""
[docs]
@abc.abstractmethod
def get_artefact_file_infos(self) -> tp.List[ArtefactFileInfo]:
"""
Retrieve information about files generated by this artefact.
Returns:
a list of file info objects
"""
_ARTEFACTS_FILE_NAME = 'artefacts.yaml'
_ARTEFACTS_FILE_VERSION = 2
[docs]
class Artefacts:
r"""A collection of :class:`Artefact`\ s."""
def __init__(
self, file_path: Path, artefacts: tp.Iterable[Artefact]
) -> None:
self.__file_path = file_path
self.__artefacts = {artefact.name: artefact for artefact in artefacts}
@property
def artefacts(self) -> tp.Iterable[Artefact]:
r"""An iterator of the :class:`Artefact`\ s in this collection."""
return self.__artefacts.values()
[docs]
def get_artefact(self, name: str) -> tp.Optional[Artefact]:
"""
Lookup an artefact by its name.
Args:
name: the name of the artefact to retrieve
Returns:
the artefact with the name ``name`` if available, else ``None``
"""
return self.__artefacts.get(name)
[docs]
def add_artefact(self, artefact: Artefact) -> None:
"""
Add an :class:`Artefact` to this collection.
If there already exists an artefact with the same name it is overridden.
Args:
artefact: the artefact to add
"""
self.__artefacts[artefact.name] = artefact
[docs]
def store(self) -> None:
"""Store artefacts in their artefacts file."""
store_as_yaml(
self.__file_path, [
VersionHeader.from_version_number(
'Artefacts', _ARTEFACTS_FILE_VERSION
), self
]
)
def __iter__(self) -> tp.Iterator[Artefact]:
return self.__artefacts.values().__iter__()
def __len__(self) -> int:
return len(self.__artefacts)
[docs]
def get_dict(
self
) -> tp.Dict[str, tp.List[tp.Dict[str, tp.Union[str, int]]]]:
"""Construct a dict from these artefacts for easy export to yaml."""
return {
'artefacts': [artefact.get_dict() for artefact in self.artefacts]
}
[docs]
@lru_cache(maxsize=1)
def load_artefacts(paper_config: 'pc.PaperConfig') -> Artefacts:
"""
Load the artefacts for a paper config.
Args:
paper_config: the paper config to load the artefacts for
Returns:
the artefacts object for the given paper config
"""
file_path = paper_config.path / _ARTEFACTS_FILE_NAME
if file_path.exists():
return load_artefacts_from_file(file_path)
return Artefacts(file_path, [])
[docs]
def load_artefacts_from_file(file_path: Path) -> Artefacts:
"""
Load an artefacts file.
Args:
file_path: path to the artefacts file
Returns:
the artefacts created from the given file
"""
documents = load_yaml(file_path)
version_header = VersionHeader(next(documents))
version_header.raise_if_not_type("Artefacts")
version_header.raise_if_version_is_less_than(_ARTEFACTS_FILE_VERSION)
raw_artefacts = next(documents)
artefacts: tp.List[Artefact] = []
for raw_artefact in raw_artefacts.pop('artefacts'):
artefact_type_name = raw_artefact.pop('artefact_type')
artefact_type = Artefact.ARTEFACT_TYPES.get(artefact_type_name, None)
artefact_type_version = raw_artefact.pop('artefact_type_version')
name = raw_artefact.pop('name')
output_dir = raw_artefact.pop('output_dir')
if not artefact_type:
LOG.warning(
f"Skipping artefact of unknown type '{artefact_type_name}'"
)
continue
if artefact_type_version < artefact_type.ARTEFACT_TYPE_VERSION:
LOG.warning(
f"Skipping artefact {name} because it uses an outdated "
f"version of {artefact_type_name}."
)
continue
artefacts.append(
artefact_type.create_artefact(name, output_dir, **raw_artefact)
)
return Artefacts(file_path, artefacts)
[docs]
def initialize_artefact_types() -> None:
"""Import plots and tables module to register artefact types."""
import varats.plot.plots # pylint: disable=C0415,unused-import
import varats.table.tables # pylint: disable=C0415,unused-import