From 85392c3c0501c97301b6e50961d8a46ba98a4869 Mon Sep 17 00:00:00 2001 From: Ross Blair Date: Fri, 17 May 2024 13:12:33 -0500 Subject: [PATCH 1/5] 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 b58fcfca..948a21b4 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 2fba341b..a63d6cdd 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,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 97ff7a4e..54a56103 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": { @@ -140,6 +141,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}',) From be1cb022b67d516d251b4afd85dc21cb75791cba Mon Sep 17 00:00:00 2001 From: Ross Blair Date: Fri, 17 May 2024 13:18:10 -0500 Subject: [PATCH 2/5] run ruff format --- nireports/assembler/report.py | 12 +++++++----- nireports/assembler/reportlet.py | 1 - nireports/assembler/tests/test_report.py | 15 +++++++-------- nireports/exceptions.py | 6 +++++- 4 files changed, 19 insertions(+), 15 deletions(-) diff --git a/nireports/assembler/report.py b/nireports/assembler/report.py index 948a21b4..145906b5 100644 --- a/nireports/assembler/report.py +++ b/nireports/assembler/report.py @@ -377,7 +377,7 @@ def index(self, config): ) reportlets.append(rlet) except ReportletException as e: - exceptions.append(e) + exceptions.append(e) list_combos = subrep_cfg.get("nested", False) else: # Do not use dictionary for queries, as we need to preserve ordering @@ -411,10 +411,12 @@ def index(self, config): # 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)) + raise NiReportsException( + ("There were errors generating report {self}", *exceptions) + ) if reportlets: sub_report = SubReport( @@ -457,9 +459,9 @@ def process_plugins(self, config, metadata=None): ) ], ) - + def __str__(self): - return f'' + 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 a63d6cdd..b9b4afa7 100644 --- a/nireports/assembler/reportlet.py +++ b/nireports/assembler/reportlet.py @@ -450,7 +450,6 @@ def __init__(self, layout, config=None, out_dir=None, bids_filters=None, metadat 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 54a56103..e07107a2 100644 --- a/nireports/assembler/tests/test_report.py +++ b/nireports/assembler/tests/test_report.py @@ -36,7 +36,7 @@ from nireports.assembler import data from nireports.assembler.report import Report -from nireports.exceptions import NiReportsException, RequiredReportletException +from nireports.exceptions import NiReportsException, RequiredReportletException summary_meta = { "Summary": { @@ -141,21 +141,20 @@ def test_report2(bids_sessions): subject="01", ) -def test_missing_reportlet( - test_report1, - 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'): + 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 index d39dc93c..5122f31b 100644 --- a/nireports/exceptions.py +++ b/nireports/exceptions.py @@ -1,9 +1,13 @@ 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}',) + self.args = ( + f"No content found while generated reportlet listed as required with the following config: {config}", + ) From bb6a6d9c5891e786ffce3ada27d02e05fca9614e Mon Sep 17 00:00:00 2001 From: Ross Blair Date: Fri, 17 May 2024 14:04:48 -0500 Subject: [PATCH 3/5] fix unused import --- nireports/assembler/tests/test_report.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nireports/assembler/tests/test_report.py b/nireports/assembler/tests/test_report.py index e07107a2..b5d7b498 100644 --- a/nireports/assembler/tests/test_report.py +++ b/nireports/assembler/tests/test_report.py @@ -36,7 +36,7 @@ from nireports.assembler import data from nireports.assembler.report import Report -from nireports.exceptions import NiReportsException, RequiredReportletException +from nireports.exceptions import NiReportsException summary_meta = { "Summary": { From 9f5eacd473066d7b8c5b6f1785ba284f0cc5d432 Mon Sep 17 00:00:00 2001 From: Ross Blair Date: Fri, 17 May 2024 14:11:35 -0500 Subject: [PATCH 4/5] fix too long line in exceptions.py --- nireports/exceptions.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nireports/exceptions.py b/nireports/exceptions.py index 5122f31b..3590a76a 100644 --- a/nireports/exceptions.py +++ b/nireports/exceptions.py @@ -8,6 +8,6 @@ class ReportletException(NiReportsException): class RequiredReportletException(ReportletException): def __init__(self, config): - self.args = ( - f"No content found while generated reportlet listed as required with the following config: {config}", - ) + message = (f"No content found while generated reportlet listed as required with the" + f"following config: {config}") + self.args = (message) From e9b3a824b9310d8c314ca86da84a87fa5f39eca4 Mon Sep 17 00:00:00 2001 From: Oscar Esteban Date: Tue, 2 Jul 2024 13:18:56 +0200 Subject: [PATCH 5/5] Update nireports/exceptions.py --- nireports/exceptions.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/nireports/exceptions.py b/nireports/exceptions.py index 3590a76a..6b20dcbc 100644 --- a/nireports/exceptions.py +++ b/nireports/exceptions.py @@ -1,3 +1,27 @@ +# 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