Source code for gain.genomic_resources.cli_errors

"""How `grr_manage` reports a failure that belongs to ONE resource.

Its own module because more than one command needs it and they must agree:
a repository-wide command that dies on the first bad resource reports half
a repository and hides the rest (gain#364, gain#503).

It is imported from the statistics package (`statistics.region_fold`), so
it may import nothing of GAIn beyond the logging shim and the leaves it
names an exception from -- anything more can reach `histogram`, which
imports that package's base class, and close a cycle. The architecture
suite pins the allowlist and says what the cycle is (gain#1293).
"""
from __future__ import annotations

from cerberus.schema import SchemaError

from gain import logging
from gain.genomic_resources.dvc import UnsupportedDvcDirectoryOutputError
from gain.genomic_resources.resource_errors import HistogramError

logger = logging.getLogger("grr_manage")

# `OSError` subsumes `FileNotFoundError`: a resource file is undescribable
# for more reasons than being absent -- a symlink loop, a target whose
# parent is not a directory, a DVC cache this run may not traverse. Each is
# a fault of the RESOURCE, and each used to abort the whole run (gain#503).
# `HistogramError` is the same tier: a histogram of THIS resource that is
# absent (an unpulled DVC blob) or unreadable, whose message carries the
# file to `dvc pull`.
# `UnsupportedDvcDirectoryOutputError` too: a `dvc add <dir>` output is a
# fault of the one resource that declares it. The pre-flight
# (`cli_dvc.refuse_dvc_directory_outputs`) still refuses a whole run before
# it writes anything; this tier is for the paths with no pre-flight above
# them -- `list` builds a missing manifest on demand and can now meet the
# refusal (#721) -- where one refused resource must not truncate the
# report on the healthy ones. (The repository-index walk handles the same
# refusal itself, in `_manifest_for_repository_index`.)
RESOURCE_ERRORS = (
    ValueError, SchemaError, OSError, HistogramError,
    UnsupportedDvcDirectoryOutputError)


[docs] def report_resource_failure( err: Exception, action: str, resource_id: str, ) -> None: """Report a failed operation on one resource, at the right tier. ``action`` names what could not be done -- never the phase the failure happened in. A handler that wraps several operations cannot know which one raised, and naming the wrong one sends the reader looking in the wrong place (gain#364); the cause, which is always carried, says it. """ # LOG014 is suppressed rather than obeyed: every caller is an exception # handler, which is exactly what makes `exc_info` meaningful here -- the # linter cannot see that through the call. if isinstance(err, RESOURCE_ERRORS): # `str(err)` is empty for an exception raised without a message -- # `raise ValueError()`, or a bare `assert` under `python -O`. The # class name is then the only thing left that says anything about # the cause, and this issue is exactly about losing it (gain#364). logger.error( "%s <%s>: %s", action, resource_id, str(err) or type(err).__name__) logger.debug( "%s <%s> failed", action, resource_id, exc_info=True) # ruff: ignore[exc-info-outside-except-handler] return logger.error( "%s <%s>: unexpected internal error", action, resource_id, exc_info=True) # ruff: ignore[exc-info-outside-except-handler]