Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
d1c21e8
Add TTY detection to prevent interactive prompts from hanging in non-…
ewels Mar 23, 2026
5139b59
Fix is_interactive() to check stdin/stdout/stderr, not just stderr
ewels Mar 23, 2026
33e8b59
Mock is_interactive in tests that rely on interactive prompts
ewels Mar 23, 2026
210adb9
Add TTY guard for pipelines create TUI app
ewels Mar 23, 2026
183d830
Fix remaining test failures: mock is_interactive in test_cli and test…
ewels Mar 23, 2026
04f96b7
Refactor TTY guards: add require_interactive() helper, standardize ex…
ewels Mar 23, 2026
9e2cd08
Fix test_datasets mock target after is_interactive import was removed
ewels Mar 23, 2026
9a720fa
Replace require_interactive() with no_prompts, auto-set from TTY
ewels Mar 31, 2026
2ba56b2
Merge branch 'dev' into modules-install-no-tty
ewels Mar 31, 2026
1e722be
Address PR review comments: fix message formatting and simplify logic
ewels Mar 31, 2026
c9d5d51
Restore no_prompts snapshot guard and add is_interactive check to sin…
ewels Mar 31, 2026
f939f30
Add tests for nf_core.utils.is_interactive
ewels Mar 31, 2026
527b33b
Replace per-test is_interactive mocks with global autouse fixture
ewels Mar 31, 2026
230aaa3
Fix test_create_app: restore is_interactive mock for Click CliRunner
ewels Mar 31, 2026
30faf6e
simplify tests
mashehu Apr 1, 2026
64cbe5a
add `no_prompt` argument to more commands
mashehu Apr 1, 2026
f220287
fix test_cli test
mashehu Apr 1, 2026
c9716ff
Merge branch 'dev' into modules-install-no-tty
ningyuxin1999 Apr 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 52 additions & 10 deletions nf_core/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -456,12 +456,13 @@ def command_pipelines_download(
default=False,
help="Show hidden params which don't normally need changing",
)
@click.option("-n", "--no-prompts", is_flag=True, default=False, help="Run without prompting for user input")
@click.pass_context
def command_pipelines_create_params_file(ctx, pipeline, revision, output, force, show_hidden):
def command_pipelines_create_params_file(ctx, pipeline, revision, output, force, show_hidden, no_prompts):
"""
Build a parameter file for a pipeline.
"""
pipelines_create_params_file(ctx, pipeline, revision, output, force, show_hidden)
pipelines_create_params_file(ctx, pipeline, revision, output, force, show_hidden, no_prompts)


# nf-core pipelines launch
Expand Down Expand Up @@ -515,6 +516,7 @@ def command_pipelines_create_params_file(ctx, pipeline, revision, output, force,
default="https://nf-co.re/launch",
help="Customise the builder URL (for development work)",
)
@click.option("-n", "--no-prompts", is_flag=True, default=False, help="Run without prompting for user input")
@click.pass_context
def command_pipelines_launch(
ctx,
Expand All @@ -527,11 +529,14 @@ def command_pipelines_launch(
save_all,
show_hidden,
url,
no_prompts,
):
"""
Launch a pipeline using a web GUI or command line prompts.
"""
pipelines_launch(ctx, pipeline, id, revision, command_only, params_in, params_out, save_all, show_hidden, url)
pipelines_launch(
ctx, pipeline, id, revision, command_only, params_in, params_out, save_all, show_hidden, url, no_prompts
)


# nf-core pipelines list
Expand Down Expand Up @@ -626,14 +631,33 @@ def rocrate(
@click.option("-u", "--username", type=str, help="GitHub PR: auth username.")
@click.option("-t", "--template-yaml", help="Pass a YAML file to customize the template")
@click.option("-b", "--blog-post", type=str, help="Link to the blog post")
@click.option("-n", "--no-prompts", is_flag=True, default=False, help="Run without prompting for user input")
def command_pipelines_sync(
ctx, directory, from_branch, pull_request, github_repository, username, template_yaml, force_pr, blog_post
ctx,
directory,
from_branch,
pull_request,
github_repository,
username,
template_yaml,
force_pr,
blog_post,
no_prompts,
):
"""
Sync a pipeline [cyan i]TEMPLATE[/] branch with the nf-core template.
"""
pipelines_sync(
ctx, directory, from_branch, pull_request, github_repository, username, template_yaml, force_pr, blog_post
ctx,
directory,
from_branch,
pull_request,
github_repository,
username,
template_yaml,
force_pr,
blog_post,
no_prompts,
)


Expand Down Expand Up @@ -2142,14 +2166,27 @@ def command_create_logo(logo_text, directory, name, theme, width, format, force)
@click.option("-g", "--github-repository", type=str, help="GitHub PR: target repository.")
@click.option("-u", "--username", type=str, help="GitHub PR: auth username.")
@click.option("-t", "--template-yaml", help="Pass a YAML file to customize the template")
def command_sync(ctx, directory, from_branch, pull_request, github_repository, username, template_yaml, force_pr):
@click.option("-n", "--no-prompts", is_flag=True, default=False, help="Run without prompting for user input")
def command_sync(
ctx, directory, from_branch, pull_request, github_repository, username, template_yaml, force_pr, no_prompts
):
"""
Use `nf-core pipelines sync` instead.
"""
log.warning(
"The `[magenta]nf-core sync[/]` command is deprecated. Use `[magenta]nf-core pipelines sync[/]` instead."
)
pipelines_sync(ctx, directory, from_branch, pull_request, github_repository, username, template_yaml, force_pr)
pipelines_sync(
ctx,
directory,
from_branch,
pull_request,
github_repository,
username,
template_yaml,
force_pr,
no_prompts=no_prompts,
)


# nf-core bump-version (deprecated)
Expand Down Expand Up @@ -2250,6 +2287,7 @@ def command_list(ctx, keywords, sort, json, show_archived):
default="https://nf-co.re/launch",
help="Customise the builder URL (for development work)",
)
@click.option("-n", "--no-prompts", is_flag=True, default=False, help="Run without prompting for user input")
@click.pass_context
def command_launch(
ctx,
Expand All @@ -2262,14 +2300,17 @@ def command_launch(
save_all,
show_hidden,
url,
no_prompts,
):
"""
Use `nf-core pipelines launch` instead.
"""
log.warning(
"The `[magenta]nf-core launch[/]` command is deprecated. Use `[magenta]nf-core pipelines launch[/]` instead."
)
pipelines_launch(ctx, pipeline, id, revision, command_only, params_in, params_out, save_all, show_hidden, url)
pipelines_launch(
ctx, pipeline, id, revision, command_only, params_in, params_out, save_all, show_hidden, url, no_prompts
)


# nf-core create-params-file (deprecated)
Expand All @@ -2292,14 +2333,15 @@ def command_launch(
default=False,
help="Show hidden params which don't normally need changing",
)
def command_create_params_file(pipeline, revision, output, force, show_hidden):
@click.option("-n", "--no-prompts", is_flag=True, default=False, help="Run without prompting for user input")
def command_create_params_file(pipeline, revision, output, force, show_hidden, no_prompts):
"""
Use `nf-core pipelines create-params-file` instead.
"""
log.warning(
"The `[magenta]nf-core create-params-file[/]` command is deprecated. Use `[magenta]nf-core pipelines create-params-file[/]` instead."
)
pipelines_create_params_file(pipeline, revision, output, force, show_hidden)
pipelines_create_params_file(pipeline, revision, output, force, show_hidden, no_prompts)


# nf-core download (deprecated)
Expand Down
34 changes: 30 additions & 4 deletions nf_core/commands_pipelines.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import rich

import nf_core.utils
from nf_core.pipelines.params_file import ParamsFileBuilder
from nf_core.utils import rich_force_colors

Expand Down Expand Up @@ -53,6 +54,12 @@ def pipelines_create(ctx, name, description, author, version, force, outdir, tem
)
sys.exit(1)
else:
if not nf_core.utils.is_interactive():
log.error(
"No pipeline arguments provided and session is not interactive (no TTY detected). "
"Please provide at least --name, --description, and --author to run non-interactively."
)
sys.exit(1)
log.info("Launching interactive nf-core pipeline creation tool.")
app = PipelineCreateApp()
app.run()
Expand Down Expand Up @@ -206,7 +213,7 @@ def pipelines_download(


# nf-core pipelines create-params-file
def pipelines_create_params_file(ctx, pipeline, revision, output, force, show_hidden):
def pipelines_create_params_file(ctx, pipeline, revision, output, force, show_hidden, no_prompts=False):
"""
Build a parameter file for a pipeline.

Expand All @@ -218,7 +225,7 @@ def pipelines_create_params_file(ctx, pipeline, revision, output, force, show_hi
Run using a remote pipeline name (such as GitHub `user/repo` or a URL),
a local pipeline directory.
"""
builder = ParamsFileBuilder(pipeline, revision)
builder = ParamsFileBuilder(pipeline, revision, no_prompts)

if not builder.write_params_file(Path(output), show_hidden=show_hidden, force=force):
sys.exit(1)
Expand All @@ -236,6 +243,7 @@ def pipelines_launch(
save_all,
show_hidden,
url,
no_prompts=False,
):
"""
Launch a pipeline using a web GUI or command line prompts.
Expand All @@ -262,6 +270,7 @@ def pipelines_launch(
show_hidden,
url,
id,
no_prompts,
)
if not launcher.launch_pipeline():
sys.exit(1)
Expand Down Expand Up @@ -309,7 +318,16 @@ def pipelines_rocrate(

# nf-core pipelines sync
def pipelines_sync(
ctx, directory, from_branch, pull_request, github_repository, username, template_yaml, force_pr, blog_post
ctx,
directory,
from_branch,
pull_request,
github_repository,
username,
template_yaml,
force_pr,
blog_post,
no_prompts=False,
):
"""
Sync a pipeline [cyan i]TEMPLATE[/] branch with the nf-core template.
Expand All @@ -331,7 +349,15 @@ def pipelines_sync(
is_pipeline_directory(directory)
# Sync the given pipeline dir
sync_obj = PipelineSync(
directory, from_branch, pull_request, github_repository, username, template_yaml, force_pr, blog_post
directory,
from_branch,
pull_request,
github_repository,
username,
template_yaml,
force_pr,
blog_post,
no_prompts,
)
sync_obj.sync()
except (SyncExceptionError, PullRequestExceptionError) as e:
Expand Down
7 changes: 6 additions & 1 deletion nf_core/components/components_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,16 @@ def __init__(
self.directory: Path = Path(directory)
self.modules_repo = ModulesRepo(remote_url, branch, no_pull, hide_progress)
self.hide_progress: bool = hide_progress
self.no_prompts: bool = no_prompts
self.no_prompts: bool = no_prompts or not nf_core.utils.is_interactive()
self.repo_type: str | None = None
self.org: str = ""
self._configure_repo_and_paths()

def require_prompts(self, msg: str) -> None:
"""Raise UserWarning if prompts are disabled (via --no-prompts or non-interactive session)."""
if self.no_prompts:
raise UserWarning(f"{msg} and prompts are disabled.")

def _configure_repo_and_paths(self, nf_dir_req: bool = True) -> None:
"""
Determine the repo type and set some default paths.
Expand Down
2 changes: 1 addition & 1 deletion nf_core/components/components_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ def check_inputs(self) -> None:
except LookupError:
raise

assert self.component_name is not None # Set above by user input, prompt, or guard
self.component_dir = Path(self.component_type, self.modules_repo.repo_path, *self.component_name.split("/"))

# First, sanity check that the module directory exists
Expand Down Expand Up @@ -170,7 +171,6 @@ def display_nftest_output(self, nftest_out: bytes, nftest_err: bytes) -> None:
print("Displaying nf-test error")
if "Different Snapshot:" in nftest_err.decode():
log.error("nf-test failed due to differences in the snapshots")
# prompt to update snapshot
if self.no_prompts:
log.info("Updating snapshot")
self.update = True
Expand Down
4 changes: 3 additions & 1 deletion nf_core/components/components_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ def get_repo_info(directory: Path, use_prompt: bool | None = True) -> tuple[Path
# Check for org if modules repo
if repo_type == "modules":
org = getattr(tools_config, "org_path", "") or ""
if org == "":
if org == "" and use_prompt:
log.warning("Organisation path not defined in %s [key: org_path]", config_fn.name)
org = questionary.text(
"What is the organisation path under which modules and subworkflows are stored?",
Expand Down Expand Up @@ -107,6 +107,8 @@ def prompt_component_version_sha(
Returns:
git_sha (str): The selected version of the module/subworkflow
"""
if not nf_core.utils.is_interactive():
raise UserWarning("Cannot interactively select a version and session is not interactive (no TTY detected).")
older_commits_choice = questionary.Choice(
title=[("fg:ansiyellow", "older commits"), ("class:choice-default", "")], value=""
)
Expand Down
30 changes: 25 additions & 5 deletions nf_core/components/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,12 @@ def _get_bioconda_tool(self):
log.warning(
f"Could not find Conda dependency using the Anaconda API: '{self.tool_conda_name if self.tool_conda_name else self.component}'"
)
if self.no_prompts:
log.warning(
f"{e}\nBioconda package not found and prompts are disabled. "
"Building module without tool software and meta."
)
break
if rich.prompt.Confirm.ask("[violet]Do you want to enter a different Bioconda package name?"):
self.tool_conda_name = rich.prompt.Prompt.ask("[violet]Name of Bioconda package").strip()
continue
Expand Down Expand Up @@ -272,6 +278,7 @@ def _get_module_structure_components(self):
"For example: {}".format(", ".join(process_label_defaults))
)
while self.process_label is None:
self.require_prompts("Process label not provided.\nPlease provide the `--process-label` option")
self.process_label = questionary.autocomplete(
"Process resource label:",
choices=process_label_defaults,
Expand All @@ -287,6 +294,9 @@ def _get_module_structure_components(self):
"[link=https://github.com/nf-core/modules/blob/master/modules/nf-core/bwa/index/main.nf]indexing reference genome files[/link]."
)
while self.has_meta is None:
self.require_prompts(
"Meta map requirement not specified.\nPlease provide the `--has-meta` or `--no-meta` option"
)
self.has_meta = rich.prompt.Confirm.ask(
"[violet]Will the module require a meta map of sample information?",
default=True,
Expand Down Expand Up @@ -346,7 +356,7 @@ def _collect_name_prompt(self):
elif self.component_type == "subworkflows":
log.warning("Subworkflow name must be lower-case letters only, with no punctuation")
name_clean = re.sub(r"[^a-z\d/]", "", self.component.lower())
if rich.prompt.Confirm.ask(f"[violet]Change '{self.component}' to '{name_clean}'?"):
if self.no_prompts or rich.prompt.Confirm.ask(f"[violet]Change '{self.component}' to '{name_clean}'?"):
self.component = name_clean
else:
self.component = ""
Expand All @@ -363,6 +373,10 @@ def _collect_name_prompt(self):

# Prompt for new entry if we reset
if self.component == "":
self.require_prompts(
f"No {self.component_type[:-1]} name provided.\n"
f"Please provide the {self.component_type[:-1]} name as a command-line argument"
)
if self.component_type == "modules":
self.component = rich.prompt.Prompt.ask("[violet]Name of tool/subtool").strip()
elif self.component_type == "subworkflows":
Expand Down Expand Up @@ -435,6 +449,7 @@ def _get_username(self):
while self.author is None or not github_username_regex.match(self.author):
if self.author is not None and not github_username_regex.match(self.author):
log.warning("Does not look like a valid GitHub username (must start with an '@')!")
self.require_prompts("GitHub username not provided.\nPlease provide the `--author` option")
self.author = rich.prompt.Prompt.ask(
f"[violet]GitHub Username:[/]{' (@author)' if author_default is None else ''}",
default=author_default,
Expand Down Expand Up @@ -486,10 +501,15 @@ def _copy_old_files(self, component_old_path):
def _print_and_delete_pytest_files(self):
"""Prompt if pytest files should be deleted and printed to stdout"""
pytest_dir = Path(self.directory, "tests", self.component_type, self.org, self.component_dir)
if rich.prompt.Confirm.ask(
"[violet]Do you want to delete the pytest files?[/]\nPytest file 'main.nf' will be printed to standard output to allow migrating the tests manually to 'main.nf.test'.",
default=False,
):
if self.no_prompts:
log.info("Prompts disabled: skipping pytest file deletion prompt.")
delete_pytest = False
else:
delete_pytest = rich.prompt.Confirm.ask(
"[violet]Do you want to delete the pytest files?[/]\nPytest file 'main.nf' will be printed to standard output to allow migrating the tests manually to 'main.nf.test'.",
default=False,
)
if delete_pytest:
with open(pytest_dir / "main.nf") as fh:
log.info(fh.read())
if pytest_dir.is_symlink():
Expand Down
4 changes: 4 additions & 0 deletions nf_core/components/info.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ def init_mod_name(self, component: str | None) -> str:
module: str: Module name to check
"""
if component is None:
self.require_prompts(
f"No {self.component_type[:-1]} name provided.\n"
f"Please provide the {self.component_type[:-1]} name as a command-line argument"
)
self.local = questionary.confirm(
f"Is the {self.component_type[:-1]} locally installed?", style=nf_core.utils.nfcore_question_style
).unsafe_ask()
Expand Down
Loading
Loading