Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added patch command for subworkflows #2861

Open
wants to merge 16 commits into
base: dev
Choose a base branch
from
37 changes: 37 additions & 0 deletions nf_core/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1685,6 +1685,43 @@ def subworkflows_install(ctx, subworkflow, dir, prompt, force, sha):
sys.exit(1)


# nf-core subworkflows patch
@subworkflows.command("patch")
@click.pass_context
@click.argument("tool", type=str, required=False, metavar="<tool> or <tool/subtool>")
@click.option(
"-d",
"--dir",
type=click.Path(exists=True),
default=".",
help=r"Pipeline directory. [dim]\[default: current working directory][/]",
)
@click.option("-r", "--remove", is_flag=True, default=False)
def subworkflows_patch(ctx, tool, dir, remove):
"""
Create a patch file for minor changes in a subworkflow

Checks if a subworkflow has been modified locally and creates a patch file
describing how the module has changed from the remote version
"""
from nf_core.subworkflows import SubworkflowPatch

try:
subworkflow_patch = SubworkflowPatch(
dir,
ctx.obj["modules_repo_url"],
ctx.obj["modules_repo_branch"],
ctx.obj["modules_repo_no_pull"],
)
if remove:
subworkflow_patch.remove(tool)
else:
subworkflow_patch.patch(tool)
except (UserWarning, LookupError) as e:
log.error(e)
sys.exit(1)


# nf-core subworkflows remove
@subworkflows.command("remove")
@click.pass_context
Expand Down
1 change: 1 addition & 0 deletions nf_core/subworkflows/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
from .install import SubworkflowInstall
from .lint import SubworkflowLint
from .list import SubworkflowList
from .patch import SubworkflowPatch
from .remove import SubworkflowRemove
from .update import SubworkflowUpdate
10 changes: 10 additions & 0 deletions nf_core/subworkflows/patch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import logging

from nf_core.components.patch import ComponentPatch

log = logging.getLogger(__name__)


class SubworkflowPatch(ComponentPatch):
def __init__(self, pipeline_dir, remote_url=None, branch=None, no_pull=False, installed_by=False):
super().__init__(pipeline_dir, "subworkflows", remote_url, branch, no_pull, installed_by)
212 changes: 212 additions & 0 deletions tests/subworkflows/patch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
import os
import tempfile
from pathlib import Path
from unittest import mock

import pytest

import nf_core.components.components_command
import nf_core.subworkflows

from ..utils import (
GITLAB_SUBWORKFLOWS_BRANCH,
GITLAB_URL,
GITLAB_REPO
)

# TODO: #Change this for the correct SUCCEED_SHA
SUCCEED_SHA = "????"
ORG_SHA = "002623ccc88a3b0cb302c7d8f13792a95354d9f2"


"""
Test the 'nf-core subworkflows patch' command
"""


def setup_patch(pipeline_dir, modify_subworkflow):
# Install the subworkflow bam_sort_stats_samtools
install_obj = nf_core.subworkflows.SubworkflowInstall(
pipeline_dir, prompt=False, force=False, remote_url=GITLAB_URL, branch=GITLAB_SUBWORKFLOWS_BRANCH, sha=ORG_SHA
)

# Install the module
install_obj.install("bam_sort_stats_samtools")

if modify_subworkflow:
# Modify the subworkflow
subworkflow_path = Path(pipeline_dir, "subworkflows", GITLAB_REPO, "bam_sort_stats_samtools")
modify_main_nf(subworkflow_path / "main.nf")


def modify_main_nf(path):
"""Modify a file to test patch creation"""
with open(path) as fh:
lines = fh.readlines()
# We want a patch file that looks something like:
# - ch_fasta // channel: [ val(meta), path(fasta) ]
for line_index in range(len(lines)):
if lines[line_index] == " ch_fasta // channel: [ val(meta), path(fasta) ]\n":
to_pop = line_index
lines.pop(to_pop)
with open(path, "w") as fh:
fh.writelines(lines)


def test_create_patch_no_change(self):
"""Test creating a patch when there is a change to the module"""
setup_patch(self.pipeline_dir, False)

# Try creating a patch file
patch_obj = nf_core.subworkflows.SubworkflowPatch(self.pipeline_dir, GITLAB_URL, GITLAB_SUBWORKFLOWS_BRANCH)
with pytest.raises(UserWarning):
patch_obj.patch("bam_sort_stats_samtools")

subworkflow_path = Path(self.pipeline_dir, "subworkflows", GITLAB_REPO, "bam_sort_stats_samtools")

# Check that no patch file has been added to the directory
assert set(os.listdir(subworkflow_path)) == {"main.nf", "meta.yml"}


def test_create_patch_change(self):
"""Test creating a patch when there is no change to the subworkflow"""
setup_patch(self.pipeline_dir, True)

# Try creating a patch file
patch_obj = nf_core.subworkflows.SubworkflowPatch(self.pipeline_dir, GITLAB_URL, GITLAB_SUBWORKFLOWS_BRANCH)
patch_obj.patch("bam_sort_stats_samtools")

subworkflow_path = Path(self.pipeline_dir, "subworkflows", GITLAB_REPO, "bam_sort_stats_samtools")

patch_fn = f"{'-'.join('bam_sort_stats_samtools')}.diff"
# Check that a patch file with the correct name has been created
assert set(os.listdir(subworkflow_path)) == {"main.nf", "meta.yml", patch_fn}

# Check that the correct lines are in the patch file
with open(subworkflow_path / patch_fn) as fh:
patch_lines = fh.readlines()
subworkflow_relpath = subworkflow_path.relative_to(self.pipeline_dir)
assert f"--- {subworkflow_relpath / 'main.nf'}\n" in patch_lines, subworkflow_relpath / "main.nf"
assert f"+++ {subworkflow_relpath / 'main.nf'}\n" in patch_lines
assert "- ch_fasta // channel: [ val(meta), path(fasta) ]" in patch_lines


def test_create_patch_try_apply_successful(self):
"""Test creating a patch file and applying it to a new version of the the files"""
setup_patch(self.pipeline_dir, True)
subworkflow_relpath = Path("subworkflows", GITLAB_REPO, "bam_sort_stats_samtools")
subworkflow_path = Path(self.pipeline_dir, subworkflow_relpath)

# Try creating a patch file
patch_obj = nf_core.subworkflows.SubworkflowPatch(self.pipeline_dir, GITLAB_URL, GITLAB_SUBWORKFLOWS_BRANCH)
patch_obj.patch("bam_sort_stats_samtools")

patch_fn = f"{'-'.join('bam_sort_stats_samtools')}.diff"
# Check that a patch file with the correct name has been created
assert set(os.listdir(subworkflow_path)) == {"main.nf", "meta.yml", patch_fn}

update_obj = nf_core.subworkflows.SubworkflowUpdate(
self.pipeline_dir, sha=SUCCEED_SHA, remote_url=GITLAB_URL, branch=GITLAB_SUBWORKFLOWS_BRANCH
)

# Install the new files
install_dir = Path(tempfile.mkdtemp())
update_obj.install_component_files("bam_sort_stats_samtools", SUCCEED_SHA, update_obj.modules_repo, install_dir)

# Try applying the patch
subworkflow_install_dir = install_dir / "bam_sort_stats_samtools"
patch_relpath = subworkflow_relpath / patch_fn
assert (
update_obj.try_apply_patch(
"bam_sort_stats_samtools", GITLAB_REPO, patch_relpath, subworkflow_path, subworkflow_install_dir
)
is True
)

# Move the files from the temporary directory
update_obj.move_files_from_tmp_dir("bam_sort_stats_samtools", install_dir, GITLAB_REPO, SUCCEED_SHA)

# Check that a patch file with the correct name has been created
assert set(os.listdir(subworkflow_path)) == {"main.nf", "meta.yml", patch_fn}

# Check that the correct lines are in the patch file
with open(subworkflow_path / patch_fn) as fh:
patch_lines = fh.readlines()
subworkflow_relpath = subworkflow_path.relative_to(self.pipeline_dir)
assert f"--- {subworkflow_relpath / 'main.nf'}\n" in patch_lines, subworkflow_relpath / "main.nf"
assert f"+++ {subworkflow_relpath / 'main.nf'}\n" in patch_lines
assert "- ch_fasta // channel: [ val(meta), path(fasta) ]" in patch_lines

# Check that 'main.nf' is updated correctly
with open(subworkflow_path / "main.nf") as fh:
main_nf_lines = fh.readlines()
# These lines should have been removed by the patch
assert " ch_fasta // channel: [ val(meta), path(fasta) ]\n" not in main_nf_lines


def test_create_patch_try_apply_failed(self):
"""Test creating a patch file and applying it to a new version of the the files"""
setup_patch(self.pipeline_dir, True)
subworkflow_relpath = Path("subworkflows", GITLAB_REPO, "bam_sort_stats_samtools")
subworkflow_path = Path(self.pipeline_dir, subworkflow_relpath)

# Try creating a patch file
patch_obj = nf_core.subworkflows.SubworkflowPatch(self.pipeline_dir, GITLAB_URL, GITLAB_SUBWORKFLOWS_BRANCH)
patch_obj.patch("bam_sort_stats_samtools")

patch_fn = f"{'-'.join('bam_sort_stats_samtools')}.diff"
# Check that a patch file with the correct name has been created
assert set(os.listdir(subworkflow_path)) == {"main.nf", "meta.yml", patch_fn}

update_obj = nf_core.subworkflows.SubworkflowUpdate(
self.pipeline_dir, sha=SUCCEED_SHA, remote_url=GITLAB_URL, branch=GITLAB_SUBWORKFLOWS_BRANCH
)

# Install the new files
install_dir = Path(tempfile.mkdtemp())
update_obj.install_component_files("bam_sort_stats_samtools", SUCCEED_SHA, update_obj.modules_repo, install_dir)

# Try applying the patch
subworkflow_install_dir = install_dir / "bam_sort_stats_samtools"
patch_relpath = subworkflow_relpath / patch_fn
assert (
update_obj.try_apply_patch(
"bam_sort_stats_samtools", GITLAB_REPO, patch_relpath, subworkflow_path, subworkflow_install_dir
)
is False
)


# TODO: create those two missing tests
def test_create_patch_update_success(self):
"""Test creating a patch file and updating a subworkflow when there is a diff conflict"""


def test_create_patch_update_fail(self):
"""
Test creating a patch file and the updating the subworkflow

Should have the same effect as 'test_create_patch_try_apply_successful'
but uses higher level api
"""


def test_remove_patch(self):
"""Test creating a patch when there is no change to the subworkflow"""
setup_patch(self.pipeline_dir, True)

# Try creating a patch file
patch_obj = nf_core.subworkflows.SubworkflowPatch(self.pipeline_dir, GITLAB_URL, GITLAB_SUBWORKFLOWS_BRANCH)
patch_obj.patch("bam_sort_stats_samtools")

subworkflow_path = Path(self.pipeline_dir, "subworkflows", GITLAB_REPO, "bam_sort_stats_samtools")

patch_fn = f"{'-'.join('bam_sort_stats_samtools')}.diff"
# Check that a patch file with the correct name has been created
assert set(os.listdir(subworkflow_path)) == {"main.nf", "meta.yml", patch_fn}

with mock.patch.object(nf_core.create.questionary, "confirm") as mock_questionary:
mock_questionary.unsafe_ask.return_value = True
patch_obj.remove("bam_sort_stats_samtools")
# Check that the diff file has been removed
assert set(os.listdir(subworkflow_path)) == {"main.nf", "meta.yml"}
9 changes: 9 additions & 0 deletions tests/test_subworkflows.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,15 @@ def tearDown(self):
test_subworkflows_list_remote,
test_subworkflows_list_remote_gitlab,
)
from .subworkflows.patch import ( # type: ignore[misc]
test_create_patch_change,
test_create_patch_no_change,
test_create_patch_try_apply_failed,
test_create_patch_try_apply_successful,
test_create_patch_update_fail,
test_create_patch_update_success,
test_remove_patch,
)
from .subworkflows.remove import ( # type: ignore[misc]
test_subworkflows_remove_included_subworkflow,
test_subworkflows_remove_one_of_two_subworkflow,
Expand Down