From 58d954b00e8207a206fed16b7a6e9698ac0c25eb Mon Sep 17 00:00:00 2001 From: Ross Blair Date: Fri, 17 May 2024 13:12:33 -0500 Subject: [PATCH] Interpret 'required' key when present in a reportlets config dictionary. If its true and no files/figures were found then raise an exception. Report indexer will catch these exceptions and raise a single exception at the end of its run with all of the collected reportlet exceptions. --- nireports/assembler/report.py | 60 +++++++++++++++--------- nireports/assembler/reportlet.py | 5 ++ nireports/assembler/tests/test_report.py | 15 ++++++ nireports/exceptions.py | 9 ++++ 4 files changed, 67 insertions(+), 22 deletions(-) create mode 100644 nireports/exceptions.py diff --git a/nireports/assembler/report.py b/nireports/assembler/report.py index 0555fc6c..37fd1e59 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,28 @@ 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"], @@ -444,6 +457,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""" diff --git a/nireports/assembler/reportlet.py b/nireports/assembler/reportlet.py index d31d1231..7e37d0b0 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 SVG_SNIPPET = [ """\ @@ -421,6 +422,10 @@ 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 24d1b156..f10dc8fe 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, RequiredReportletException summary_meta = { "Summary": { @@ -138,6 +139,20 @@ def test_report2(bids_sessions): subject="01", ) +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 00000000..d39dc93c --- /dev/null +++ b/nireports/exceptions.py @@ -0,0 +1,9 @@ +class NiReportsException(Exception): + pass + +class ReportletException(NiReportsException): + pass + +class RequiredReportletException(ReportletException): + def __init__(self, config): + self.args = (f'No content found while generated reportlet listed as required with the following config: {config}',)