diff --git a/nf_core/components/install.py b/nf_core/components/install.py index 8350159320..282ce58b22 100644 --- a/nf_core/components/install.py +++ b/nf_core/components/install.py @@ -169,9 +169,13 @@ def install(self, component: str | dict[str, str], silent: bool = False) -> bool # Install included modules and subworkflows self.install_included_components(component_dir) - # Regenerate container configuration files for the pipeline when modules are installed - if self.component_type == "modules": - try_generate_container_configs(self.directory, component_dir, component) + # Regenerate container configs once per top-level invocation + # (recursive installs pass silent=True). + if not silent: + if self.component_type == "modules": + try_generate_container_configs(self.directory, component_dir, component) + else: + try_generate_container_configs(self.directory) if not silent: modules_json.load() diff --git a/nf_core/components/update.py b/nf_core/components/update.py index e679643735..6357982fd1 100644 --- a/nf_core/components/update.py +++ b/nf_core/components/update.py @@ -309,9 +309,6 @@ def update(self, component=None, silent=False, updated=None, check_diff_exist=Tr self.modules_json.update(self.component_type, modules_repo, component, version, installed_by=None) updated.append(component) - # Regenerate container configuration files for the pipeline when modules are updated - if self.component_type == "modules": - try_generate_container_configs(self.directory) recursive_update = True modules_to_update, subworkflows_to_update = self.get_components_to_update(component) if not silent and len(modules_to_update + subworkflows_to_update) > 0 and not self.update_all: @@ -356,6 +353,11 @@ def update(self, component=None, silent=False, updated=None, check_diff_exist=Tr self.modules_json.load() self.modules_json.dump(run_prettier=True) + # Regenerate container configs once per top-level invocation + # (recursive updates pass silent=True; skip in --save-diff dry runs). + if not silent and updated and not self.save_diff_fn: + try_generate_container_configs(self.directory) + return exit_value def get_single_component_info(self, component): diff --git a/tests/modules/test_install.py b/tests/modules/test_install.py index 92d30a494f..94cf913acb 100644 --- a/tests/modules/test_install.py +++ b/tests/modules/test_install.py @@ -1,4 +1,5 @@ from pathlib import Path +from unittest import mock import pytest @@ -44,6 +45,12 @@ def test_modules_install_trimgalore_twice(self): self.mods_install.install("trimgalore") assert self.mods_install.install("trimgalore") is True + @mock.patch("nf_core.components.install.try_generate_container_configs") + def test_modules_install_regenerates_container_configs_once(self, mock_gen): + """Container config regen runs exactly once for a top-level module install.""" + assert self.mods_install.install("trimgalore") is not False + assert mock_gen.call_count == 1 + def test_modules_install_from_gitlab(self): """Test installing a module from GitLab""" assert self.mods_install_gitlab.install("fastqc") is True diff --git a/tests/subworkflows/test_install.py b/tests/subworkflows/test_install.py index 91263d2847..2f25e2553e 100644 --- a/tests/subworkflows/test_install.py +++ b/tests/subworkflows/test_install.py @@ -1,4 +1,5 @@ from pathlib import Path +from unittest import mock import pytest @@ -191,6 +192,12 @@ def test_subworkflows_install_tracking_added_super_subworkflow(self): ]["installed_by"] ) == sorted(["subworkflows", "bam_sort_stats_samtools"]) + @mock.patch("nf_core.components.install.try_generate_container_configs") + def test_subworkflows_install_regenerates_container_configs_once(self, mock_gen): + """Container config regen runs once for a top-level subworkflow install with module deps.""" + assert self.subworkflow_install.install("bam_sort_stats_samtools") is not False + assert mock_gen.call_count == 1 + def test_subworkflows_install_alternate_remote(self): """Test installing a module from a different remote with the same organization path""" install_obj = SubworkflowInstall( diff --git a/tests/subworkflows/test_update.py b/tests/subworkflows/test_update.py index 936b61c782..b5dc1e96c4 100644 --- a/tests/subworkflows/test_update.py +++ b/tests/subworkflows/test_update.py @@ -32,6 +32,25 @@ def test_install_and_update(self): assert update_obj.update("bam_stats_samtools") is True assert cmp_component(tmpdir, sw_path) is True + @mock.patch("nf_core.components.update.try_generate_container_configs") + def test_subworkflow_update_regenerates_container_configs_once(self, mock_gen): + """Container config regen runs once for a top-level update, not per linked component.""" + assert self.subworkflow_install_old.install("fastq_align_bowtie2") + mock_gen.reset_mock() + update_obj = SubworkflowUpdate(self.pipeline_dir, show_diff=False, update_deps=True) + assert update_obj.update("fastq_align_bowtie2") is True + assert mock_gen.call_count == 1 + + @mock.patch("nf_core.components.update.try_generate_container_configs") + def test_subworkflow_update_save_diff_skips_container_configs(self, mock_gen): + """``--save-diff`` is a dry run; container configs must not be regenerated.""" + assert self.subworkflow_install_old.install("fastq_align_bowtie2") + mock_gen.reset_mock() + patch_path = Path(self.pipeline_dir, "fastq_align_bowtie2.patch") + update_obj = SubworkflowUpdate(self.pipeline_dir, save_diff_fn=patch_path, update_deps=True) + assert update_obj.update("fastq_align_bowtie2") is True + assert mock_gen.call_count == 0 + def test_install_at_hash_and_update(self): """Installs an old version of a subworkflow in the pipeline and updates it""" assert self.subworkflow_install_old.install("fastq_align_bowtie2")