From 4a915c6af1efbe0b351ac023dffa636e79330e73 Mon Sep 17 00:00:00 2001 From: Wytamma Wirth Date: Sat, 16 Nov 2024 20:23:01 +1100 Subject: [PATCH 1/3] :sparkles: add snk edit command --- snk/main.py | 38 +++++++++++++++++++++++++++++++++----- snk/utils.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 5 deletions(-) create mode 100644 snk/utils.py diff --git a/snk/main.py b/snk/main.py index c20bcb6..01522b4 100644 --- a/snk/main.py +++ b/snk/main.py @@ -9,6 +9,7 @@ from .__about__ import __version__ from .errors import WorkflowExistsError, WorkflowNotFoundError from .nest import Nest +from .utils import open_text_editor app = typer.Typer() @@ -242,6 +243,38 @@ def list( console = Console() console.print(table) +@app.command() +def edit( + ctx: typer.Context, + workflow_name: str = typer.Argument( + ..., help="Name of the workflow to configure." + ), + path: bool = typer.Option( + False, "--path", "-p", help="Show the path to the snk.yaml file." + ), +): + """ + Access the snk.yaml configuration file for a workflow. + """ + nest = Nest(snk_home=ctx.obj.snk_home, bin_dir=ctx.obj.snk_bin) + try: + workflows = nest.workflows + except FileNotFoundError: + workflows = [] + workflow = next((w for w in workflows if w.name == workflow_name), None) + if not workflow: + typer.secho(f"Workflow '{workflow_name}' not found!", fg="red", err=True) + raise typer.Exit(1) + snk_config = SnkConfig.from_workflow_dir(workflow.path, create_if_not_exists=True) + snk_config.save() + if path: + typer.echo(snk_config._snk_config_path) + else: + try: + open_text_editor(snk_config._snk_config_path) + except Exception as e: + typer.secho(str(e), fg="red", err=True) + raise typer.Exit(1) # @app.command() # def run( @@ -259,11 +292,6 @@ def list( # """Generate annotations defaults from config file""" # raise NotImplementedError -# @app.command() -# def create(name: str): -# """Create a default project that can be installed with snk""" -# raise NotImplementedError - # @app.command() # def update(): # """ diff --git a/snk/utils.py b/snk/utils.py new file mode 100644 index 0000000..7bea2d2 --- /dev/null +++ b/snk/utils.py @@ -0,0 +1,33 @@ +def open_text_editor(file_path): + """ + Opens the system's default text editor to edit the specified file. + + Parameters: + file_path (str): The path to the file to be edited. + """ + import os + import platform + import subprocess + + if platform.system() == "Windows": + os.startfile(file_path) + elif platform.system() == "Darwin": # macOS + subprocess.call(("open", file_path)) + else: # Linux and other Unix-like systems + editors = ["nano", "vim", "vi"] + editor = None + for e in editors: + if ( + subprocess.call( + ["which", e], stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + == 0 + ): + editor = e + break + if editor: + subprocess.call([editor, file_path]) + else: + raise Exception( + "No suitable text editor found. Please install nano or vim." + ) \ No newline at end of file From c53a2b96bea565c5033d0f35eb53f3a6d15e3a95 Mon Sep 17 00:00:00 2001 From: Wytamma Wirth Date: Sat, 16 Nov 2024 20:23:22 +1100 Subject: [PATCH 2/3] :bulb: add snk edit docs --- docs/managing_workflows.md | 18 ++++++++++++++++++ docs/snk_config_file.md | 12 ++++++++++++ 2 files changed, 30 insertions(+) diff --git a/docs/managing_workflows.md b/docs/managing_workflows.md index 78f0c1d..f809d40 100644 --- a/docs/managing_workflows.md +++ b/docs/managing_workflows.md @@ -163,6 +163,24 @@ Proceed (Y/n)? y !!! Note Use `--force` to force uninstall without asking. +## Editing workflow CLI configuration + +The `snk edit` command is used to edit the CLI configuration of a installed workflow. This will open the configuration file in the default text editor. + +```bash +snk edit workflow +``` + +Use the `--path` flag to print the path to the configuration file instead of opening it. +```bash +snk edit --path workflow +``` + +!!! note + + The configuration file is a YAML file that contains the CLI configuration for the workflow. For more details on the CLI configuration file read the [snk config file docs](https://snk.wytamma.com/snk_config_file). + + ## Ejecting workflows The `cp -r $(workflow-name -p) workflow-name` command is used to eject the workflow from the package. This will copy the workflow files to the current working directory. This will allow you to modify the workflow and run it with the standard `snakemake` command. diff --git a/docs/snk_config_file.md b/docs/snk_config_file.md index c6dd866..c0839ef 100644 --- a/docs/snk_config_file.md +++ b/docs/snk_config_file.md @@ -6,6 +6,18 @@ title: Snk Config File The `snk.yaml` file serves as the main interface for configuring the Snk workflow CLI. Users can tailor the workflow's settings, specify required resources, and control the appearance of the command line interface by setting various options in the `snk.yaml` file. +## Modifying the `snk.yaml` File + +The `snk.yaml` file should be located in the root directory of the Snakemake workflow. It is used to configure the Snk CLI and provide additional information about the workflow. The `snk.yaml` file is written in YAML format and can be edited with any text editor. + +For convenience, you can use the `snk edit [WORKFLOW_NAME]` command to open the `snk.yaml` file in your default text editor. This command will create a new `snk.yaml` file if one does not already exist. + +```bash +snk edit workflow +``` + +```bash + ## Available Configuration Options The following options are available for configuration in `snk.yaml`: From 40a33602dfd9dd6d779a102df3c41a6ee9ac3d26 Mon Sep 17 00:00:00 2001 From: Wytamma Wirth Date: Sat, 16 Nov 2024 20:23:38 +1100 Subject: [PATCH 3/3] :white_check_mark: add snk edit tests --- tests/conftest.py | 2 +- tests/test_snk.py | 7 +++ tests/test_snk_edit_config.py | 105 ++++++++++++++++++++++++++++++++++ 3 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 tests/test_snk_edit_config.py diff --git a/tests/conftest.py b/tests/conftest.py index 65a1319..3670dd4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -41,7 +41,7 @@ def basic_runner(tmp_path_factory): return runner -@pytest.fixture(scope="session") +@pytest.fixture(scope="module") def local_workflow(tmp_path_factory): path = Path(tmp_path_factory.mktemp("snk")) (path / "home").mkdir() diff --git a/tests/test_snk.py b/tests/test_snk.py index 4f6ce62..20fe4d3 100644 --- a/tests/test_snk.py +++ b/tests/test_snk.py @@ -58,6 +58,12 @@ def test_snk_list(local_workflow: Workflow): assert "workflow" in result.stdout assert "editable" in result.stdout +def test_config_path_cli(local_workflow: Workflow): + snk_home = local_workflow.path.parent.parent + bin_dir = local_workflow.path.parent.parent.parent / "bin" + result = runner.invoke(app, ["--home", snk_home, "--bin", bin_dir, "edit", "--path", "workflow"]) + assert result.exit_code == 0 + assert "/workflows/workflow/snk.yaml" in result.stdout def test_snk_uninstall(local_workflow: Workflow): snk_home = local_workflow.path.parent.parent @@ -87,3 +93,4 @@ def test_import_create_cli(capsys): pass captured = capsys.readouterr() assert "Usage" in captured.out + diff --git a/tests/test_snk_edit_config.py b/tests/test_snk_edit_config.py new file mode 100644 index 0000000..6b72042 --- /dev/null +++ b/tests/test_snk_edit_config.py @@ -0,0 +1,105 @@ +import os # noqa: F401 +import platform # noqa: F401 +import subprocess +from pathlib import Path +from unittest.mock import call, patch + +import pytest +import typer # noqa: F401 +from snk_cli.workflow import Workflow +from typer.testing import CliRunner + +from snk.main import app + +runner = CliRunner() + + +@pytest.fixture +def mock_platform_system(): + with patch('platform.system') as mock: + yield mock + + +@pytest.fixture +def mock_os_startfile(): + # Only patch os.startfile if the platform is Windows + if hasattr(os, 'startfile'): + with patch('os.startfile') as mock: + yield mock + else: + yield None + + +@pytest.fixture +def mock_subprocess_call(): + with patch('subprocess.call') as mock: + yield mock + + +@pytest.mark.skipif(platform.system() != 'Windows', reason="Requires Windows") +def test_open_text_editor_windows( + mock_platform_system, mock_os_startfile, local_workflow: Workflow +): + mock_platform_system.return_value = 'Windows' + file_path = local_workflow.path / "snk.yaml" + + snk_home = local_workflow.path.parent.parent + bin_dir = local_workflow.path.parent.parent.parent / "bin" + res = runner.invoke(app, ["--home", snk_home, "--bin", bin_dir, "edit", "workflow"]) + + assert res.exit_code == 0, res.stderr + if mock_os_startfile: + mock_os_startfile.assert_called_once_with(file_path) + + +def test_open_text_editor_mac( + mock_platform_system, mock_subprocess_call, local_workflow: Workflow +): + mock_platform_system.return_value = 'Darwin' + file_path = local_workflow.path / "snk.yaml" + + snk_home = local_workflow.path.parent.parent + bin_dir = local_workflow.path.parent.parent.parent / "bin" + res = runner.invoke(app, ["--home", snk_home, "--bin", bin_dir, "edit", "workflow"]) + + assert res.exit_code == 0, res.stderr + mock_subprocess_call.assert_called_once_with(('open', file_path)) + + +def test_open_text_editor_linux( + mock_platform_system, mock_subprocess_call, local_workflow: Workflow +): + mock_platform_system.return_value = 'Linux' + file_path = local_workflow.path / "snk.yaml" + + with patch('subprocess.call') as mock_call: + mock_call.side_effect = [1, 1, 0, 0] # Mocking 'which' command results: nano not found, vim not found, vi found + + snk_home = local_workflow.path.parent.parent + bin_dir = local_workflow.path.parent.parent.parent / "bin" + res = runner.invoke(app, ["--home", snk_home, "--bin", bin_dir, "edit", "workflow"]) + assert res.exit_code == 0, res.stderr + mock_call.assert_has_calls([ + call(['which', 'nano'], stdout=subprocess.PIPE, stderr=subprocess.PIPE), + call(['which', 'vim'], stdout=subprocess.PIPE, stderr=subprocess.PIPE), + call(['which', 'vi'], stdout=subprocess.PIPE, stderr=subprocess.PIPE), + call(['vi', file_path]) + ]) + + +def test_open_text_editor_no_editor_found( + mock_platform_system, mock_subprocess_call, local_workflow: Workflow +): + mock_platform_system.return_value = 'Linux' + + with patch('subprocess.call') as mock_call: + mock_call.side_effect = [1, 1, 1] # Mocking 'which' command results: none of the editors found + + with patch('typer.secho') as mock_print: + snk_home = local_workflow.path.parent.parent + bin_dir = local_workflow.path.parent.parent.parent / "bin" + res = runner.invoke(app, ["--home", snk_home, "--bin", bin_dir, "edit", "workflow"]) + assert res.exit_code == 1, res.stderr + mock_print.assert_called_once_with( + "No suitable text editor found. Please install nano or vim.", fg='red', err=True + )