diff --git a/nireports/assembler/report.py b/nireports/assembler/report.py index b58fcfc..145906b 100644 --- a/nireports/assembler/report.py +++ b/nireports/assembler/report.py @@ -36,6 +36,7 @@ from nireports.assembler import data from nireports.assembler.reportlet import Reportlet +from nireports.exceptions import NiReportsException, ReportletException # Add a new figures spec try: @@ -339,6 +340,7 @@ def index(self, config): This method also places figures in their final location. """ + exceptions = [] # Initialize a BIDS layout _indexer = BIDSLayoutIndexer( config_filename=data.load("nipreps.json"), @@ -361,22 +363,25 @@ def index(self, config): orderings = [s for s in subrep_cfg.get("ordering", "").strip().split(",") if s] entities, list_combos = self._process_orderings(orderings, layout.get(**bids_filters)) + reportlets = [] + if not list_combos: # E.g. this is an anatomical reportlet - reportlets = [ - Reportlet( - layout, - config=cfg, - out_dir=out_dir, - bids_filters=bids_filters, - metadata=metadata, - ) - for cfg in subrep_cfg["reportlets"] - ] + for cfg in subrep_cfg["reportlets"]: + try: + rlet = Reportlet( + layout, + config=cfg, + out_dir=out_dir, + bids_filters=bids_filters, + metadata=metadata, + ) + reportlets.append(rlet) + except ReportletException as e: + exceptions.append(e) list_combos = subrep_cfg.get("nested", False) else: # Do not use dictionary for queries, as we need to preserve ordering # of ordering columns. - reportlets = [] for c in list_combos: # do not display entities with the value None. c_filt = [ @@ -389,20 +394,30 @@ def index(self, config): for cfg in subrep_cfg["reportlets"]: cfg["bids"].update({entities[i]: c[i] for i in range(len(c))}) - rlet = Reportlet( - layout, - config=cfg, - out_dir=out_dir, - bids_filters=bids_filters, - metadata=metadata, - ) - if not rlet.is_empty(): - rlet.title = title - title = None - reportlets.append(rlet) + try: + rlet = Reportlet( + layout, + config=cfg, + out_dir=out_dir, + bids_filters=bids_filters, + metadata=metadata, + ) + if not rlet.is_empty(): + rlet.title = title + title = None + reportlets.append(rlet) + except ReportletException as e: + exceptions.append(e) # Filter out empty reportlets reportlets = [r for r in reportlets if not r.is_empty()] + + # When support python < 3.11 dropped we can use ExceptionGroups + if exceptions: + raise NiReportsException( + ("There were errors generating report {self}", *exceptions) + ) + if reportlets: sub_report = SubReport( subrep_cfg["name"], @@ -445,6 +460,9 @@ def process_plugins(self, config, metadata=None): ], ) + def __str__(self): + return f"" + def generate_report(self): """Once the Report has been indexed, the final HTML can be generated""" env = jinja2.Environment( diff --git a/nireports/assembler/reportlet.py b/nireports/assembler/reportlet.py index 2fba341..b9b4afa 100644 --- a/nireports/assembler/reportlet.py +++ b/nireports/assembler/reportlet.py @@ -32,6 +32,7 @@ from nireports.assembler import data from nireports.assembler.misc import dict2html, read_crashfile +from nireports.exceptions import RequiredReportletException IMG_SNIPPET = """\
@@ -446,6 +447,9 @@ def __init__(self, layout, config=None, out_dir=None, bids_filters=None, metadat boiler_tabs.append("") self.components.append(("\n".join(boiler_tabs + boiler_body), desc_text)) + if config.get("required", False) and self.is_empty(): + raise RequiredReportletException(config) + def is_empty(self): """Determine whether the reportlet has no components.""" return len(self.components) == 0 diff --git a/nireports/assembler/tests/test_report.py b/nireports/assembler/tests/test_report.py index 97ff7a4..b5d7b49 100644 --- a/nireports/assembler/tests/test_report.py +++ b/nireports/assembler/tests/test_report.py @@ -36,6 +36,7 @@ from nireports.assembler import data from nireports.assembler.report import Report +from nireports.exceptions import NiReportsException summary_meta = { "Summary": { @@ -141,6 +142,19 @@ def test_report2(bids_sessions): ) +def test_missing_reportlet(test_report1, bids_sessions): + out_dir = tempfile.mkdtemp() + report = test_report1 + settings = yaml.safe_load(data.load.readable("default.yml").read_text()) + settings["root"] = str(Path(bids_sessions) / "nireports") + settings["out_dir"] = str(Path(out_dir) / "nireports") + settings["run_uuid"] = "fakeuuid" + settings["sections"][0]["reportlets"][0]["required"] = True + settings["sections"][0]["reportlets"][0]["bids"] = {"datatype": "fake"} + with pytest.raises(NiReportsException, match="No content found"): + report.index(settings) + + @pytest.mark.parametrize( "orderings,expected_entities,expected_value_combos", [ diff --git a/nireports/exceptions.py b/nireports/exceptions.py new file mode 100644 index 0000000..6b20dcb --- /dev/null +++ b/nireports/exceptions.py @@ -0,0 +1,37 @@ +# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- +# vi: set ft=python sts=4 ts=4 sw=4 et: +# +# Copyright 2024 The NiPreps Developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# We support and encourage derived works from this project, please read +# about our expectations at +# +# https://www.nipreps.org/community/licensing/ +# +"""Exceptions issued by the assembler.""" + +class NiReportsException(Exception): + pass + + +class ReportletException(NiReportsException): + pass + + +class RequiredReportletException(ReportletException): + def __init__(self, config): + message = (f"No content found while generated reportlet listed as required with the" + f"following config: {config}") + self.args = (message)