Source code for gain.genomic_resources.testing.resource_meta

"""The ``meta:`` block shared by every GRR test-data builder.

Every resource type may carry a ``meta:`` block -- ``summary``,
``description`` and a free-form ``labels:`` mapping -- and it is read back
through the resource itself (``get_summary`` / ``get_description`` /
``get_labels``), by ``grr_manage``/``grr_browse``, the resource statistics
and the docs rendering.

Because it is a property of a *resource*, not of any one resource type,
:class:`MetaMixin` carries it once for all of the builders in
:mod:`~.testing.builders` and :mod:`.data_frame_builder` instead of every
``_render_config`` growing its own copy of the block.  A builder mixes it in
and then either

* appends :meth:`MetaMixin.render_meta` to the config text it renders
  itself, or
* calls :meth:`MetaMixin.append_meta_into` after delegating the config
  writing to a ``setup_*`` helper (the reference-genome path).

Both are no-ops until one of :meth:`MetaMixin.with_meta`,
:meth:`MetaMixin.with_labels`, :meth:`MetaMixin.with_raw_labels` or
:meth:`MetaMixin.with_raw_meta` is called, so a builder that does not use
them realizes byte-identical output.

It lives in its own module (like :mod:`.score_specs`) so the builder DSL can
keep growing without ``builders.py`` turning into an unreadable slab.
"""
from __future__ import annotations

import copy
import dataclasses
import pathlib
from typing import Any, Self

import yaml

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


[docs] @dataclasses.dataclass(frozen=True) class MetaMixin: """Immutable ``meta:`` state shared by every resource builder. A frozen dataclass carrying the ``meta:`` fields, so a builder that mixes it in gains ``with_meta``/``with_labels`` -- and the rendering behind them -- without redeclaring either. Nothing is declared by default, which is what keeps the block absent from the rendered config unless it was asked for. A value and a *declared* flag rather than a value alone: ``labels:`` and ``meta:`` can each be declared as an explicit YAML ``null``, and that renders differently from not declaring them at all -- the one emits the key with nothing after it, the other emits no key. The flag carries that distinction, so the value field holds exactly what the caller passed and stays comparable, copyable and picklable. """ meta_summary: str | None = None meta_description: str | None = None # ``Any``, not ``dict[str, Any] | None``: `meta.labels` is free-form # YAML and ``with_raw_labels`` exists to declare whatever it allows. meta_labels: Any = None meta_labels_declared: bool = False #: The whole ``meta:`` block, when ``with_raw_meta`` replaced it. meta_raw: Any = None meta_raw_declared: bool = False
[docs] def with_meta( self, *, summary: str | None = None, description: str | None = None, ) -> Self: """Declare the resource's ``summary`` and/or ``description``. Only the fields passed are set, so the two can be declared in separate calls without the second one clearing the first. Calling with neither is a validation error rather than a silent no-op -- emitting an empty ``meta:`` block is never what was meant. """ if summary is None and description is None: raise ResourceValidationError( "with_meta requires at least one of summary or description") return dataclasses.replace( self, meta_summary=( self.meta_summary if summary is None else summary), meta_description=( self.meta_description if description is None else description), )
[docs] def with_labels(self, **labels: Any) -> Self: """Emit a ``labels:`` mapping inside the resource's ``meta:`` block. Keys are passed through verbatim, e.g. ``with_labels(reference_genome="genome")`` -- the label vocabulary is a convention of the GRR content, not of the resource schema, so the builder models none of it. The mapping REPLACES any previously declared one (the :meth:`~.builders._TableScoreBuilder` ``with_chrom_mapping`` precedent) and is deep-copied, so neither the mapping nor a mutable value inside it stays shared with the caller. """ return self.with_raw_labels(labels)
[docs] def with_raw_labels(self, labels: Any) -> Self: """Emit ``labels:`` as an arbitrary YAML value, mapping or not. ``meta.labels`` is free-form YAML, so what a curator writes there is not necessarily a mapping -- a scalar, a list and an explicit ``null`` are all things a resource can carry, and each one has to be expressible for the readers of ``meta.labels`` to be tested against it (gain#654). :meth:`with_labels` takes keyword arguments and therefore only ever builds a mapping; this is its sibling for everything else, ``with_raw_labels(None)`` being the explicit ``labels: null`` spelling. The value REPLACES any previously declared one and is deep-copied, on the same terms. """ return dataclasses.replace( self, meta_labels=copy.deepcopy(labels), meta_labels_declared=True, )
[docs] def with_raw_meta(self, meta: Any) -> Self: """Emit the whole ``meta:`` block as an arbitrary YAML value. ``meta:`` is as free-form as the ``labels:`` inside it, so a resource can declare it as a scalar or a list too, and a reader of ``meta.labels`` has to be tested against that shape as well (gain#654). Where :meth:`with_raw_labels` replaces one field, this replaces the block: whatever is passed becomes the value of ``meta:`` verbatim, and any ``summary``/``description``/``labels`` declared alongside it is not rendered. ``with_raw_meta(None)`` is the explicit ``meta: null`` spelling, as ``with_raw_labels(None)`` is for the field. Deep-copied on the same terms as its sibling. """ return dataclasses.replace( self, meta_raw=copy.deepcopy(meta), meta_raw_declared=True, )
[docs] def render_meta(self) -> str: """Render the ``meta:`` block, or ``""`` when nothing was declared. Emitted through ``yaml.safe_dump`` so a summary or a label value carrying a colon, a newline or leading whitespace stays the string it was authored as. """ if self.meta_raw_declared: return yaml.safe_dump( {"meta": self.meta_raw}, default_flow_style=False, sort_keys=False) meta: dict[str, Any] = {} if self.meta_summary is not None: meta["summary"] = self.meta_summary if self.meta_description is not None: meta["description"] = self.meta_description if self.meta_labels_declared: meta["labels"] = self.meta_labels if not meta: return "" return yaml.safe_dump( {"meta": meta}, default_flow_style=False, sort_keys=False)
[docs] def append_meta_into(self, resource_dir: pathlib.Path) -> None: """Append the ``meta:`` block to an already-written resource config. For the builders that delegate the whole ``genomic_resource.yaml`` to a ``setup_*`` helper (``setup_genome``/``setup_genome_bgz``) rather than rendering it themselves. A no-op when no meta was declared, so the delegated config is left byte-identical. """ append_config_block(resource_dir, self.render_meta())
[docs] def append_config_block(resource_dir: pathlib.Path, rendered: str) -> None: """Append a rendered YAML block to an already-written resource config. The shared tail for every builder that delegates the whole ``genomic_resource.yaml`` to a ``setup_*`` helper and then has to add a key the helper does not write. A no-op for an empty block, so the delegated config is left byte-identical. """ if not rendered: return config_path = resource_dir / GR_CONF_FILE_NAME config = config_path.read_text() if not config.endswith("\n"): config += "\n" config_path.write_text(config + rendered)