Source code for gain.genomic_resources.testing.extra_files

"""Files a resource builder ships by name, beside what it renders itself.

A resource holds more than the files its builder authors: the statistics
build leaves ``statistics/`` behind, and what a resource's manifest lists
is what its readers and pages take to exist.  :class:`ExtraFilesMixin`
lets a test put such a file into a fixture resource by name -- a stand-in
histogram image, say -- without running the build that would have
written it, for a test whose subject is the file's *presence* (an
address, a manifest entry) rather than its content.  For the ``basic``
resource, which has no authored content of its own, the shipped files
ARE the payload.

A builder mixes it in and calls :meth:`ExtraFilesMixin.realize_files_into`
last, after everything it writes itself; a shipped file may then sit
beside the builder's own files but never over one.  It is a no-op until
:meth:`ExtraFilesMixin.with_file` is called, so a builder that does not use
it realizes byte-identical output.  Like :mod:`.resource_meta`, it lives in
its own module so the builder DSL can keep growing without ``builders.py``
turning into an unreadable slab.
"""
from __future__ import annotations

import dataclasses
import pathlib
from typing import Self

from gain.genomic_resources.repository import GR_CONF_FILE_NAME
from gain.genomic_resources.testing import setup_directories
from gain.genomic_resources.testing.score_specs import ResourceValidationError


[docs] @dataclasses.dataclass(frozen=True) class ExtraFilesMixin: """Immutable shipped-file state shared by the resource builders.""" files: tuple[tuple[str, str | bytes], ...] = ()
[docs] def with_file(self, filename: str, content: str | bytes) -> Self: """Ship ``content`` as ``filename`` inside the realized resource. ``filename`` is relative to the resource directory and may name a subdirectory (``statistics/histogram_score.png``), which is created. A name already shipped is replaced in place. The config is not a payload: it is rendered from the builder's own state, and a file under its name would silently win over all of it, so that name is refused here. A file the builder writes itself (its data, index or sidecar) is refused when the resource is realized, since which of those a builder writes is settled only by its other knobs. """ if filename == GR_CONF_FILE_NAME: raise ResourceValidationError( f"{filename!r} is the rendered config, not a payload; " f"declare meta with with_meta, or the whole block with " f"with_raw_meta") # A dict keeps a reassigned key in its slot, which is the # replace-in-place the docstring promises. files = dict(self.files) files[filename] = content return dataclasses.replace(self, files=tuple(files.items()))
[docs] def files_content(self) -> dict[str, str | bytes]: """The shipped files as ``setup_directories`` content.""" return dict(self.files)
[docs] def realize_files_into(self, resource_dir: pathlib.Path) -> None: """Write the shipped files into ``resource_dir``, last. Called after the builder has written everything of its own, so a shipped file that would land on one of those is refused rather than replacing it -- a payload the config does not describe. """ for filename, content in self.files: if (resource_dir / filename).exists(): raise ResourceValidationError( f"{filename!r} is a file this builder renders itself; " f"with_file ships payloads beside it, not over it") setup_directories(resource_dir / filename, content)