diff --git a/.editorconfig b/.editorconfig index 449f446a3b..5aa8697d30 100644 --- a/.editorconfig +++ b/.editorconfig @@ -20,3 +20,11 @@ indent_style = unset [**/Makefile] indent_style = unset + +[tests/__snapshots__/*] +charset = unset +end_of_line = unset +insert_final_newline = unset +trim_trailing_whitespace = unset +indent_style = unset +indent_size = unset diff --git a/.github/actions/create-lint-wf/action.yml b/.github/actions/create-lint-wf/action.yml index 1741b934f2..5b7dbfffeb 100644 --- a/.github/actions/create-lint-wf/action.yml +++ b/.github/actions/create-lint-wf/action.yml @@ -27,7 +27,7 @@ runs: run: | mkdir -p create-lint-wf && cd create-lint-wf export NXF_WORK=$(pwd) - nf-core --log-file log.txt create -n testpipeline -d "This pipeline is for testing" -a "Testing McTestface" --plain + nf-core --log-file log.txt pipelines create -n testpipeline -d "This pipeline is for testing" -a "Testing McTestface" # Try syncing it before we change anything - name: nf-core sync diff --git a/.github/workflows/create-test-lint-wf-template.yml b/.github/workflows/create-test-lint-wf-template.yml index 5f5d545199..c0da74c374 100644 --- a/.github/workflows/create-test-lint-wf-template.yml +++ b/.github/workflows/create-test-lint-wf-template.yml @@ -103,7 +103,7 @@ jobs: - name: create a pipeline from the template ${{ matrix.TEMPLATE }} run: | cd create-test-lint-wf - nf-core --log-file log.txt create -n testpipeline -d "This pipeline is for testing" -a "Testing McTestface" --template-yaml ${{ matrix.TEMPLATE }} + nf-core --log-file log.txt pipelines create -n testpipeline -d "This pipeline is for testing" -a "Testing McTestface" --template-yaml ${{ matrix.TEMPLATE }} - name: run the pipeline run: | diff --git a/.github/workflows/create-test-wf.yml b/.github/workflows/create-test-wf.yml index d68bfae71c..d4cd60ab4b 100644 --- a/.github/workflows/create-test-wf.yml +++ b/.github/workflows/create-test-wf.yml @@ -67,7 +67,7 @@ jobs: run: | mkdir create-test-wf && cd create-test-wf export NXF_WORK=$(pwd) - nf-core --log-file log.txt create -n testpipeline -d "This pipeline is for testing" -a "Testing McTestface" --plain + nf-core --log-file log.txt pipelines create -n testpipeline -d "This pipeline is for testing" -a "Testing McTestface" nextflow run nf-core-testpipeline -profile test,self_hosted_runner --outdir ./results - name: Upload log file artifact diff --git a/.gitignore b/.gitignore index 271fdb14e3..a3721da86e 100644 --- a/.gitignore +++ b/.gitignore @@ -115,3 +115,6 @@ ENV/ pip-wheel-metadata .vscode .*.sw? + +# Textual +snapshot_report.html diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 03fbb7bedf..b618e00837 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.2.2 + rev: v0.3.1 hooks: - id: ruff # linter args: [--fix, --exit-non-zero-on-fix] # sort imports and fix @@ -9,6 +9,8 @@ repos: rev: "v3.1.0" hooks: - id: prettier + additional_dependencies: + - prettier@3.2.5 - repo: https://github.com/editorconfig-checker/editorconfig-checker.python rev: "2.7.3" diff --git a/CHANGELOG.md b/CHANGELOG.md index 14ff84e937..2b7ff2ef6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ### Template +- Update templates to use nf-core/setup-nextflow v2 + ### Linting ### Components @@ -12,6 +14,7 @@ - Update CI to use nf-core/setup-nextflow v2 - Changelog bot: handle also patch version before dev suffix ([#2820](https://github.com/nf-core/tools/pull/2820)) +- update prettier to 3.2.5 ([#2830](https://github.com/nf-core/tools/pull/2830)) - Update GitHub Actions ([#2827](https://github.com/nf-core/tools/pull/2827)) ## [v2.13.1 - Tin Puppy Patch](https://github.com/nf-core/tools/releases/tag/2.13) - [2024-02-29] diff --git a/MANIFEST.in b/MANIFEST.in index 5ec177b783..68f115d97f 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -9,3 +9,4 @@ include nf_core/assets/logo/nf-core-repo-logo-base-lightbg.png include nf_core/assets/logo/nf-core-repo-logo-base-darkbg.png include nf_core/assets/logo/placeholder_logo.svg include nf_core/assets/logo/MavenPro-Bold.ttf +include nf_core/pipelines/create/create.tcss diff --git a/nf_core/__init__.py b/nf_core/__init__.py index d96be73f3d..2d4fe45a0a 100644 --- a/nf_core/__init__.py +++ b/nf_core/__init__.py @@ -1,4 +1,4 @@ -""" Main nf_core module file. +"""Main nf_core module file. Shouldn't do much, as everything is under subcommands. """ diff --git a/nf_core/__main__.py b/nf_core/__main__.py index 7d2d083fa9..e29f8d208f 100644 --- a/nf_core/__main__.py +++ b/nf_core/__main__.py @@ -1,5 +1,6 @@ #!/usr/bin/env python -""" nf-core: Helper tools for use with nf-core Nextflow pipelines. """ +"""nf-core: Helper tools for use with nf-core Nextflow pipelines.""" + import logging import os import sys @@ -35,6 +36,7 @@ "commands": [ "list", "launch", + "configs", "create-params-file", "download", "licences", @@ -44,7 +46,7 @@ { "name": "Commands for developers", "commands": [ - "create", + "pipelines", "lint", "modules", "subworkflows", @@ -55,6 +57,18 @@ ], }, ], + "nf-core configs": [ + { + "name": "Config commands", + "commands": ["create"], + }, + ], + "nf-core pipelines": [ + { + "name": "Pipeline commands", + "commands": ["create"], + }, + ], "nf-core modules": [ { "name": "For pipelines", @@ -302,6 +316,35 @@ def launch( sys.exit(1) +# nf-core configs +@nf_core_cli.group() +@click.pass_context +def configs(ctx): + """ + Commands to manage nf-core pipelines. + """ + # ensure that ctx.obj exists and is a dict (in case `cli()` is called + # by means other than the `if` block below) + ctx.ensure_object(dict) + + +@configs.command("create") +def create_configs(): + """ + Command to interactively create a nextflow config + """ + from nf_core.configs.create import ConfigsCreateApp + + try: + log.info("Launching interactive nf-core configs creation tool.") + app = ConfigsCreateApp() + app.run() + sys.exit(app.return_code or 0) + except UserWarning as e: + log.error(e) + sys.exit(1) + + # nf-core create-params-file @nf_core_cli.command() @click.argument("pipeline", required=False, metavar="") @@ -466,53 +509,6 @@ def licences(pipeline, json): sys.exit(1) -# nf-core create -@nf_core_cli.command() -@click.option( - "-n", - "--name", - type=str, - help="The name of your new pipeline", -) -@click.option("-d", "--description", type=str, help="A short description of your pipeline") -@click.option("-a", "--author", type=str, help="Name of the main author(s)") -@click.option("--version", type=str, default="1.0dev", help="The initial version number to use") -@click.option( - "-f", - "--force", - is_flag=True, - default=False, - help="Overwrite output directory if it already exists", -) -@click.option("-o", "--outdir", help="Output directory for new pipeline (default: pipeline name)") -@click.option("-t", "--template-yaml", help="Pass a YAML file to customize the template") -@click.option("--plain", is_flag=True, help="Use the standard nf-core template") -def create(name, description, author, version, force, outdir, template_yaml, plain): - """ - Create a new pipeline using the nf-core template. - - Uses the nf-core template to make a skeleton Nextflow pipeline with all required - files, boilerplate code and best-practices. - """ - from nf_core.create import PipelineCreate - - try: - create_obj = PipelineCreate( - name, - description, - author, - version=version, - force=force, - outdir=outdir, - template_yaml_path=template_yaml, - plain=plain, - ) - create_obj.init_pipeline() - except UserWarning as e: - log.error(e) - sys.exit(1) - - # nf-core lint @nf_core_cli.command() @click.option( @@ -630,6 +626,123 @@ def lint( sys.exit(1) +# nf-core pipelines subcommands +@nf_core_cli.group() +@click.pass_context +def pipelines(ctx): + """ + Commands to manage nf-core pipelines. + """ + # ensure that ctx.obj exists and is a dict (in case `cli()` is called + # by means other than the `if` block below) + ctx.ensure_object(dict) + + +# nf-core pipelines create +@pipelines.command("create") +@click.pass_context +@click.option( + "-n", + "--name", + type=str, + help="The name of your new pipeline", +) +@click.option("-d", "--description", type=str, help="A short description of your pipeline") +@click.option("-a", "--author", type=str, help="Name of the main author(s)") +@click.option("--version", type=str, default="1.0.0dev", help="The initial version number to use") +@click.option( + "-f", + "--force", + is_flag=True, + default=False, + help="Overwrite output directory if it already exists", +) +@click.option("-o", "--outdir", help="Output directory for new pipeline (default: pipeline name)") +@click.option("-t", "--template-yaml", help="Pass a YAML file to customize the template") +@click.option( + "--organisation", + type=str, + default="nf-core", + help="The name of the GitHub organisation where the pipeline will be hosted (default: nf-core)", +) +def create_pipeline(ctx, name, description, author, version, force, outdir, template_yaml, organisation): + """ + Create a new pipeline using the nf-core template. + + Uses the nf-core template to make a skeleton Nextflow pipeline with all required + files, boilerplate code and best-practices. + \n\n + Run without any command line arguments to use an interactive interface. + """ + from nf_core.pipelines.create import PipelineCreateApp + from nf_core.pipelines.create.create import PipelineCreate + + if (name and description and author) or (template_yaml): + # If all command arguments are used, run without the interactive interface + try: + create_obj = PipelineCreate( + name, + description, + author, + version=version, + force=force, + outdir=outdir, + organisation=organisation, + ) + create_obj.init_pipeline() + except UserWarning as e: + log.error(e) + sys.exit(1) + elif name or description or author or version != "1.0.0dev" or force or outdir or organisation != "nf-core": + log.error( + "Command arguments are not accepted in interactive mode.\n" + "Run with all command line arguments to avoid using an interactive interface" + "or run without any command line arguments to use an interactive interface." + ) + sys.exit(1) + else: + log.info( + "Launching interactive nf-core pipeline creation tool." + "\nRun with all command line arguments to avoid using an interactive interface." + ) + app = PipelineCreateApp() + app.run() + sys.exit(app.return_code or 0) + + +# nf-core create (deprecated) +@nf_core_cli.command(hidden=True, deprecated=True) +@click.option( + "-n", + "--name", + type=str, + help="The name of your new pipeline", +) +@click.option("-d", "--description", type=str, help="A short description of your pipeline") +@click.option("-a", "--author", type=str, help="Name of the main author(s)") +@click.option("--version", type=str, help="The initial version number to use") +@click.option( + "-f", + "--force", + is_flag=True, + default=False, + help="Overwrite output directory if it already exists", +) +@click.option("-o", "--outdir", help="Output directory for new pipeline (default: pipeline name)") +@click.option("-t", "--template-yaml", help="Pass a YAML file to customize the template") +@click.option("--plain", is_flag=True, help="Use the standard nf-core template") +def create(name, description, author, version, force, outdir, template_yaml, plain): + """ + DEPRECATED + Create a new pipeline using the nf-core template. + + Uses the nf-core template to make a skeleton Nextflow pipeline with all required + files, boilerplate code and best-practices. + """ + log.error("[bold][green]nf-core create[/] command is deprecated. Use [green]nf-core pipelines create[/].[/]") + sys.exit(0) + + # nf-core modules subcommands @nf_core_cli.group() @click.option( diff --git a/nf_core/components/components_test.py b/nf_core/components/components_test.py index f1a9e7c401..9b81f54f06 100644 --- a/nf_core/components/components_test.py +++ b/nf_core/components/components_test.py @@ -2,7 +2,6 @@ The ComponentsTest class handles the generation and testing of nf-test snapshots. """ - import logging import os import re @@ -91,9 +90,9 @@ def run(self) -> None: """Run build steps""" self.check_inputs() os.environ["NFT_DIFF"] = "pdiff" # set nf-test differ to pdiff to get a better diff output - os.environ[ - "NFT_DIFF_ARGS" - ] = "--line-numbers --expand-tabs=2" # taken from https://code.askimed.com/nf-test/docs/assertions/snapshots/#snapshot-differences + os.environ["NFT_DIFF_ARGS"] = ( + "--line-numbers --expand-tabs=2" # taken from https://code.askimed.com/nf-test/docs/assertions/snapshots/#snapshot-differences + ) with nf_core.utils.set_wd(Path(self.dir)): self.check_snapshot_stability() if len(self.errors) > 0: diff --git a/nf_core/components/create.py b/nf_core/components/create.py index c4b477a0ab..d2169e3a72 100644 --- a/nf_core/components/create.py +++ b/nf_core/components/create.py @@ -2,7 +2,6 @@ The ComponentCreate class handles generating of module and subworkflow templates """ - import glob import json import logging diff --git a/nf_core/components/lint/__init__.py b/nf_core/components/lint/__init__.py index 3c2fb9dde3..c99934bca3 100644 --- a/nf_core/components/lint/__init__.py +++ b/nf_core/components/lint/__init__.py @@ -3,7 +3,6 @@ in nf-core pipelines """ - import logging import operator import os diff --git a/nf_core/components/nfcore_component.py b/nf_core/components/nfcore_component.py index 2f73afe9d3..d9731ba7c9 100644 --- a/nf_core/components/nfcore_component.py +++ b/nf_core/components/nfcore_component.py @@ -1,6 +1,7 @@ """ The NFCoreComponent class holds information and utility functions for a single module or subworkflow """ + import logging import re from pathlib import Path diff --git a/nf_core/configs/__init__.py b/nf_core/configs/__init__.py new file mode 100644 index 0000000000..95c830c1b4 --- /dev/null +++ b/nf_core/configs/__init__.py @@ -0,0 +1 @@ +from .create import ConfigsCreateApp diff --git a/nf_core/configs/create/__init__.py b/nf_core/configs/create/__init__.py new file mode 100644 index 0000000000..c5d6b023b0 --- /dev/null +++ b/nf_core/configs/create/__init__.py @@ -0,0 +1,138 @@ +"""A Textual app to create a config.""" + +import logging + +from textual.app import App +from textual.widgets import Button + +from nf_core.configs.create.configstype import ChooseConfigsType +from nf_core.configs.create.containertype import ChooseContainerType +from nf_core.configs.create.envmodule import ChooseHpcModuleFunctionality +from nf_core.configs.create.final import FinalScreen +from nf_core.configs.create.infratype import ChooseInfraConfigType +from nf_core.configs.create.loggingscreen import LoggingScreen +from nf_core.configs.create.maxparams import MaxparamsOptions +from nf_core.configs.create.miscquestions import ChooseMiscOptions +from nf_core.configs.create.nfcoredetails import NfcoreDetails +from nf_core.configs.create.otherlocations import ChooseOtherLocations +from nf_core.configs.create.schedulertype import ChooseSchedulerType +from nf_core.configs.create.utils import ( + CreateConfig, + CustomLogHandler, + LoggingConsole, +) +from nf_core.configs.create.welcome import WelcomeScreen + +log_handler = CustomLogHandler(console=LoggingConsole(classes="log_console"), rich_tracebacks=True, markup=True) +logging.basicConfig( + level="INFO", + handlers=[log_handler], + format="%(message)s", +) +log_handler.setLevel("INFO") + + +class ConfigsCreateApp(App[CreateConfig]): + """A Textual app to create nf-core configs.""" + + CSS_PATH = "create.tcss" + TITLE = "nf-core configs create" + SUB_TITLE = "Create a new nextflow config with an interactive interface" + BINDINGS = [ + ("d", "toggle_dark", "Toggle dark mode"), + ("q", "quit", "Quit"), + ] + + ## New question sections go here + SCREENS = { + "welcome": WelcomeScreen(), + "choose_type": ChooseConfigsType(), + "choose_infra": ChooseInfraConfigType(), + "choose_scheduler": ChooseSchedulerType(), + "choose_hpcenvmodules": ChooseHpcModuleFunctionality(), + "nfcore_details": NfcoreDetails(), + "choose_container": ChooseContainerType(), + "choose_maxparams": MaxparamsOptions(), + "choose_otherlocations": ChooseOtherLocations(), + "miscquestions": ChooseMiscOptions(), + "final": FinalScreen(), + } + + # Initialise config data as empty + TEMPLATE_CONFIG = CreateConfig() + + # Initialise configs type + CONFIGS_TYPE = None + INFRA_TYPE = None + + # Log handler + LOG_HANDLER = log_handler + # Logging state + LOGGING_STATE = None + + def on_mount(self) -> None: + self.push_screen("welcome") + + ## TODO: Create pydantic model to define question order + ## based on previous answers + def on_button_pressed(self, event: Button.Pressed) -> None: + """Handle all button pressed events.""" + ## Initialisation + if event.button.id == "start": + self.push_screen("choose_type") + + ## INFRASTRUCTURE CONFIGS + ## Infrastructure config basic information + elif event.button.id == "type_infrastructure": + self.CONFIGS_TYPE = "infrastructure" + self.push_screen("choose_infra") + + ## HPC specific questions + ## TODO: only send to hpcenvmoduiles if `type_hpc` [use pydantic stored data] + elif event.button.id == "infratype_continue": + self.push_screen("choose_hpcenvmodules") + + elif event.button.id == "schedulertype_continue": + self.push_screen("nfcore_details") + ## nf-core specific questions + ## TODO: only send to nfcore_details if `for_nfcore_pipelines` [use pydantic stored data] + elif event.button.id == "envmodule_continue": + self.push_screen("choose_scheduler") + + elif event.button.id == "nfcoredetails_continue": + self.push_screen("choose_container") + + elif event.button.id == "containertype_continue": + self.push_screen("choose_maxparams") + + elif event.button.id == "maxparams_continue": + self.push_screen("choose_otherlocations") + + elif event.button.id == "otherlocations_continue": + self.push_screen("miscquestions") + + elif event.button.id == "miscquestions_continue": + self.push_screen("final") + + ## PIPELINE CONFIGS + elif event.button.id == "type_pipeline": + self.CONFIGS_TYPE = "pipeline" + self.push_screen("choose_infra") + + ## WRAPUP + elif event.button.id == "show_logging": + # Set logging state to repo created to see the button for closing the logging screen + self.LOGGING_STATE = "repo created" + self.switch_screen(LoggingScreen()) + + if event.button.id == "close_app": + self.exit(return_code=0) + + if event.button.id == "back": + self.pop_screen() + + print(self.CONFIGS_TYPE) + + def action_toggle_dark(self) -> None: + """An action to toggle dark mode.""" + self.dark: bool = not self.dark diff --git a/nf_core/configs/create/configstype.py b/nf_core/configs/create/configstype.py new file mode 100644 index 0000000000..3edf2628ec --- /dev/null +++ b/nf_core/configs/create/configstype.py @@ -0,0 +1,42 @@ +from textual.app import ComposeResult +from textual.containers import Center +from textual.screen import Screen +from textual.widgets import Button, Footer, Header, Markdown + +markdown_intro = """ +# Infrastructure or pipelines? + +Next, we need to know what kind of config this will be. + +Choose _"infrastructure"_ if: + +* You want your config to apply to _any_ nextflow or nf-core pipeline +* The config should just set maximum resources, schedulers, containers etc. + +Choose _"pipeline"_ if: + +* You want your config to tweak pipeline specific resources +""" + +markdown_details = """ +## Not sure what to pick? + +If you want to use the config for many users, or on a HPC, your best bet is to start with an infrastructure config. + +If you want to use the config for yourself on your laptop, or adjust a specific pipeline run, choose pipeline. +""" + + +class ChooseConfigsType(Screen): + """Choose whether this will be an infrastructure or pipeline config.""" + + def compose(self) -> ComposeResult: + yield Header() + yield Footer() + yield Markdown(markdown_intro) + yield Center( + Button("infrastructure", id="type_infrastructure", variant="success"), + Button("pipeline", id="type_pipeline", variant="primary"), + classes="cta", + ) + yield Markdown(markdown_details) diff --git a/nf_core/configs/create/containertype.py b/nf_core/configs/create/containertype.py new file mode 100644 index 0000000000..5ab9b66e24 --- /dev/null +++ b/nf_core/configs/create/containertype.py @@ -0,0 +1,73 @@ +from textual.app import ComposeResult +from textual.containers import Center, Horizontal +from textual.screen import Screen +from textual.widgets import Button, Footer, Header, Markdown, Select + +from nf_core.configs.create.utils import TextInputWithHelp + +markdown_select = """ +# What software environment to use? + +For reproducibility purposes, Nextflow prefers to get the software used within a pipeline from a software environment or container engine. + +Examples of these include `conda`, `docker`, `singularity`/`apptainer`, `charliecloud`, etc. + +Here you can specify which container engine the config will use. +""" + +markdown_cache = """ +# Where to store software environment files and images? + +By default Nextflow will download the software environment files and container images each time the pipeline is run into the run's `work/` directory. + +It is highly recommended to instead specify a _cache_ directory for software environments. +This location will store files and container images downloaded by Nextflow the first time they are requested. +Subsequent runs will then re-use these files, saving time and bandwidth. +""" + + +class ChooseContainerType(Screen): + """Choose which software environment source (e.g. conda or container engine) to use all configs.""" + + def compose(self) -> ComposeResult: + yield Header() + yield Footer() + yield Markdown(markdown_select) + yield Center( + Select( + [ + ("local (not recommended)", "conttype_local"), + ("conda", "conttype_conda"), + ("docker", "conttype_docker"), + ("singularity", "conttype_singularity"), + ("apptainer", "conttype_apptainer"), + ("charliecloud", "conttype_charliecloud"), + ("podman", "conttype_podman"), + ("sarus", "conttype_sarus"), + ("shifter", "conttype_shifter"), + ], + id="container_type", + ), + classes="cta", + ) + yield Markdown(markdown_cache) + yield Center( + TextInputWithHelp( + field_id="containercache_location", + placeholder="", + description="Absolute path to the container cache directory", + markdown="", + ), + classes="cta", + ) + + yield Horizontal( + Center( + Button("Back", id="back", variant="default"), + classes="cta", + ), + Center( + Button("Next", id="containertype_continue", variant="success"), + classes="cta", + ), + ) diff --git a/nf_core/configs/create/create.py b/nf_core/configs/create/create.py new file mode 100644 index 0000000000..d8ecc3bf98 --- /dev/null +++ b/nf_core/configs/create/create.py @@ -0,0 +1,14 @@ +import json + +from nf_core.configs.create.utils import CreateConfig + + +class ConfigCreate: + def __init__(self, template_config: CreateConfig): + self.template_config = template_config + + ## TODO make this into a proper function here so that the exported `dict` + ## is an actual nf-core config + def write_to_file(self): + with open("file.txt", "w+") as file: + file.write(json.dumps(dict(self.template_config))) diff --git a/nf_core/configs/create/create.tcss b/nf_core/configs/create/create.tcss new file mode 100644 index 0000000000..46b3989017 --- /dev/null +++ b/nf_core/configs/create/create.tcss @@ -0,0 +1,106 @@ +#logo { + text-align:center; +} +.cta { + layout: horizontal; + margin-bottom: 1; +} +.cta Button { + margin-left: 3; + margin-right: 3; +} + +.custom_grid { + height: auto; +} +.custom_grid Switch { + width: auto; +} +.custom_grid Static { + width: 1fr; + margin: 1 8; +} +.custom_grid Button { + width: auto; +} + +.field_help { + padding: 1 1 0 1; + color: $text-muted; + text-style: italic; +} +.validation_msg { + padding: 0 1; + color: $error; +} +.-valid { + border: tall $success-darken-3; +} + +Horizontal{ + width: 100%; + height: auto; +} +.column { + width: 1fr; +} + +HorizontalScroll { + width: 100%; +} +.feature_subtitle { + color: grey; +} + +Vertical{ + height: auto; +} + +/* Display help messages */ + +.help_box { + background: #333333; + padding: 1 5; + margin: 1 10; + overflow-y: auto; + transition: height 50ms; + display: none; + height: 0; +} +.displayed .help_box { + display: block; + height: 12; +} +#show_help { + display: block; +} +#hide_help { + display: none; +} +.displayed #show_help { + display: none; +} +.displayed #hide_help { + display: block; +} + +/* Show password */ + +#show_password { + display: block; +} +#hide_password { + display: none; +} +.displayed #show_password { + display: none; +} +.displayed #hide_password { + display: block; +} + +/* Logging console */ + +.log_console { + height: auto; +} diff --git a/nf_core/configs/create/envmodule.py b/nf_core/configs/create/envmodule.py new file mode 100644 index 0000000000..db8415a572 --- /dev/null +++ b/nf_core/configs/create/envmodule.py @@ -0,0 +1,44 @@ +from textual.app import ComposeResult +from textual.containers import Center +from textual.screen import Screen +from textual.widgets import Button, Footer, Header, Markdown + +from nf_core.configs.create.utils import ConfigFeature + +markdown_help_text = """ +Some HPC infrastructure uses module environments to load centrally installed software. +These are loaded with a command such as: + +```bash +module load nextflow +```` + +If you need to load Nextflow and/or container engine (Singularity, Apptainer, Charliecloud etc.) via a module environment, specify this here. + +If you skip this step, you must make sure all required software for running Nextflow pipelines is available in the your environment. +""" + + +class ChooseHpcModuleFunctionality(Screen): + """Does your HPC infrastructure use module environments?""" + + def compose(self) -> ComposeResult: + yield Header() + yield Footer() + yield Markdown( + """ +# HPC specific questions +""" + ) + yield ConfigFeature( + markdown_help_text, + "On", + "Activate module environments for loading Nextflow and required dependencies (singularity, apptainer, docker, etc.)", + "use_modules", + False, + ) + yield Center( + Button("Back", id="back", variant="default"), + Button("Next", id="envmodule_continue", variant="success"), + classes="cta", + ) diff --git a/nf_core/configs/create/final.py b/nf_core/configs/create/final.py new file mode 100644 index 0000000000..20c8d13e05 --- /dev/null +++ b/nf_core/configs/create/final.py @@ -0,0 +1,43 @@ +from textual import on +from textual.app import ComposeResult +from textual.containers import Center +from textual.screen import Screen +from textual.widgets import Button, Footer, Header, Markdown + +from nf_core.configs.create.create import ( + ConfigCreate, +) +from nf_core.configs.create.utils import ( + TextInput, +) + + +class FinalScreen(Screen): + """A welcome screen for the app.""" + + def compose(self) -> ComposeResult: + yield Header() + yield Footer() + yield Markdown( + """ +# Final step +""" + ) + yield TextInput( + "savelocation", + ".", + "In which directory would you like to save the config?", + ".", + classes="row", + ) + yield Center(Button("Save and close!", id="close_app", variant="success"), classes="cta") + + def _create_config(self) -> None: + """Create the config.""" + create_obj = ConfigCreate(template_config=self.parent.TEMPLATE_CONFIG) + create_obj.write_to_file() + + @on(Button.Pressed, "#close_app") + def on_button_pressed(self, event: Button.Pressed) -> None: + """Save fields to the config.""" + self._create_config() diff --git a/nf_core/configs/create/infratype.py b/nf_core/configs/create/infratype.py new file mode 100644 index 0000000000..20bf29d055 --- /dev/null +++ b/nf_core/configs/create/infratype.py @@ -0,0 +1,57 @@ +from textual.app import ComposeResult +from textual.containers import Center, Horizontal +from textual.screen import Screen +from textual.widgets import Button, Footer, Header, Markdown, Select + +from nf_core.configs.create.utils import ConfigFeature + +markdown_intro = """ +# What type of infrastructure is the config for? + +Different computational infrastructure types require different information. + +The current options that are available to you: + +- Local: a single laptop/desktop machine, or a single server node with no scheduling system +- HPC: a multi-node server that has a scheduling system such as SLURM, Grid Engine, PBS etc. +""" + + +class ChooseInfraConfigType(Screen): + """Choose whether this infrastructure config will be for a local machine or HPC clusters.""" + + def compose(self) -> ComposeResult: + yield Header() + yield Footer() + yield Markdown(markdown_intro) + yield Center( + Select([("HPC", "type_hpc"), ("Local", "type_local")], id="infra_type"), + # Select("local", id="type_local"), + classes="cta", + ) + yield Markdown( + """ +# For which type of pipelines will the config be used for? + +Configs for nf-core pipelines require extra details to be included in the config. + +If you plan to use the config with nf-core configs, please indicate below. +""" + ) + yield ConfigFeature( + "", + "On", + "Activate to specify config will be used with nf-core pipelines", + "for_nfcore_pipelines", + True, + ) + yield Horizontal( + Center( + Button("Back", id="back", variant="default"), + classes="cta", + ), + Center( + Button("Next", id="infratype_continue", variant="success"), + classes="cta", + ), + ) diff --git a/nf_core/configs/create/loggingscreen.py b/nf_core/configs/create/loggingscreen.py new file mode 100644 index 0000000000..fe763bad23 --- /dev/null +++ b/nf_core/configs/create/loggingscreen.py @@ -0,0 +1,45 @@ +from textwrap import dedent + +from textual.app import ComposeResult +from textual.containers import Center +from textual.screen import Screen +from textual.widgets import Button, Footer, Header, Markdown, Static + +markdown = """ +Visualising logging output. +""" + + +class LoggingScreen(Screen): + """A screen to show the final logs.""" + + def compose(self) -> ComposeResult: + yield Header() + yield Footer() + yield Markdown( + dedent( + """ + # Logging + """ + ) + ) + yield Static( + f"\n[green]{' ' * 40},--.[grey39]/[green],-." + + "\n[blue] ___ __ __ __ ___ [green]/,-._.--~\\" + + "\n[blue]|\ | |__ __ / ` / \ |__) |__ [yellow] } {" + + "\n[blue] | \| | \__, \__/ | \ |___ [green]\`-._,-`-," + + "\n[green] `._,._,'\n", + id="logo", + ) + yield Markdown(markdown) + yield Center(self.parent.LOG_HANDLER.console) + if self.parent.LOGGING_STATE == "config created": + yield Center( + Button("Close App", id="close_app", variant="success"), + classes="cta", + ) + else: + yield Center( + Button("Continue", id="close_screen", variant="success", disabled=True), + classes="cta", + ) diff --git a/nf_core/configs/create/maxparams.py b/nf_core/configs/create/maxparams.py new file mode 100644 index 0000000000..ea9b7c4878 --- /dev/null +++ b/nf_core/configs/create/maxparams.py @@ -0,0 +1,63 @@ +"""A Textual app to create a pipeline.""" + +from textwrap import dedent + +from textual.app import ComposeResult +from textual.containers import Center +from textual.screen import Screen +from textual.widgets import Button, Footer, Header, Markdown + +from nf_core.configs.create.utils import TextInputWithHelp + + +class MaxparamsOptions(Screen): + """Name, description, author, etc.""" + + def compose(self) -> ComposeResult: + default_cpus = 8 + default_memory = 32 + default_time = 24 + + yield Header() + yield Footer() + yield Markdown( + dedent( + """ + # Specify maximum resource parameters + + These values are used to set the absolute maximum resources that CAN be requested by a pipeline. + + They do not _increase_ resources requests (for this, create a `pipeline` config), but rather act as a cap. + """ + ) + ) + yield TextInputWithHelp( + "max_cpus", + str(default_cpus), + "Maximum CPUs", + "Define the maximum number of CPUs that the computing infrastructure has. This is the maximum number of CPUs of a laptop or desktop, the number of CPUs on the largest node on a HPC cluster.", + default=str(default_cpus), + classes="row", + ) + yield TextInputWithHelp( + "max_mem", + str(default_memory), + "Maximum memory (GB)", + "Define the maximum number of memory (RAM) that the computing infrastructure has. This is the RAM of a laptop or desktop, or the RAM on the largest node on a HPC cluster.", + default=str(default_memory), + classes="row", + ) + yield TextInputWithHelp( + "max_time", + str(default_time), + "Maximum wall time (hours)", + "Define the maximum length a step of a pipeline can run for. Set to something sensible for a laptop or desktop, set to value of the partition with the longest walltime on a HPC cluster.", + default=str(default_time), + classes="custom_grid", + ) + + yield Center( + Button("Back", id="back", variant="default"), + Button("Next", id="maxparams_continue", variant="success"), + classes="cta", + ) diff --git a/nf_core/configs/create/miscquestions.py b/nf_core/configs/create/miscquestions.py new file mode 100644 index 0000000000..871772f9ac --- /dev/null +++ b/nf_core/configs/create/miscquestions.py @@ -0,0 +1,45 @@ +from textual.app import ComposeResult +from textual.containers import Center, Horizontal +from textual.screen import Screen +from textual.widgets import Button, Footer, Header, Markdown + +from nf_core.configs.create.utils import ConfigFeature, TextInputWithHelp + +markdown_intro = """ +# Miscellaneous questions + +This section contains questions that are specify various options that do not fall under the main categories of Nextflow configuration scopes. +""" + + +class ChooseMiscOptions(Screen): + """Choose whether this infrastructure config will be for a local machine or HPC clusters.""" + + def compose(self) -> ComposeResult: + yield Header() + yield Footer() + yield Markdown(markdown_intro) + yield ConfigFeature( + "", + "On", + "Activate automatic clean up of `work/` directories on successful pipeline completion", + "for_nfcore_pipelines", + True, + ) + yield TextInputWithHelp( + field_id="noretries", + placeholder="2", + description="No. of retries Nextflow should attempt to re-run a failed process (with increased resource requests)", + markdown="TODO", + default="2", + ) + yield Horizontal( + Center( + Button("Back", id="back", variant="default"), + classes="cta", + ), + Center( + Button("Next", id="miscquestions_continue", variant="success"), + classes="cta", + ), + ) diff --git a/nf_core/configs/create/nfcoredetails.py b/nf_core/configs/create/nfcoredetails.py new file mode 100644 index 0000000000..c5ff175a4a --- /dev/null +++ b/nf_core/configs/create/nfcoredetails.py @@ -0,0 +1,111 @@ +"""A Textual app to create a config.""" + +import logging +from textwrap import dedent + +from textual import on +from textual.app import ComposeResult +from textual.containers import Center, Horizontal +from textual.screen import Screen +from textual.widgets import Button, Footer, Header, Input, Markdown + +from nf_core.configs.create.utils import TextInputWithHelp +from nf_core.pipelines.create.utils import ( + CreateConfig, +) + +log = logging.getLogger(__name__) + + +class NfcoreDetails(Screen): + """Name, description, author, etc.""" + + ## This allows dynamic updating of different fields, to add additional 'live' updates: + ## extend function by copy updating `query_one` commands + def on_input_changed(self, event: Input.Changed): + ## Retrieve input keys + input_config = self.query_one("#config_name", TextInputWithHelp) + default_config = input_config.query_one(Input).value + + ## Extract existing field for updating, and modify update + description_to_populate = self.query_one("#config_description", TextInputWithHelp) + description_to_populate.query_one(Input).value = ( + default_config + " infrastructure config created with nf-core/tools." + ) + + def compose(self) -> ComposeResult: + yield Header() + yield Footer() + yield Markdown( + dedent( + """ + # Details for nf-core configs + """ + ) + ) + with Horizontal(): + yield TextInputWithHelp( + "config_name", + "The name of the config, as would be called from the Nextflow command-line `-profile`. Typically lower case, no spaces. E.g. uppmax.", + "Config Name", + "Long form help text goes here", + classes="row", + ) + + yield TextInputWithHelp( + "config_author", + "Author(s)", + "Name of the main author / authors", + "Long form help text goes here", + ) + + yield TextInputWithHelp( + "config_handle", + "@OctoCat", + "Github handle of the main author / authors", + "Long form help text goes here", + ) + + yield TextInputWithHelp( + "config_description", + "Config built by nf-core/tools.", + "A short description of your config.", + "Long form help text goes here", + ) + + yield TextInputWithHelp( + "config_url", + "https://nf-co.re", + "URL to institution hosting the infrastructure (e.g. HPC)", + "Long form help text goes here", + ) + + yield Center( + Button("Back", id="back", variant="default"), + Button("Next", id="nfcoredetails_continue", variant="success"), + classes="cta", + ) + + ## TODO: is there a way to block pressing Center button if validation doesn't pass? Or do that right at the end? + @on(Button.Pressed) + def on_button_pressed(self, event: Button.Pressed) -> None: + """Save fields to the config.""" + config = {} + for text_input in self.query("TextInputWithHelp"): + this_input = text_input.query_one(Input) + validation_result = this_input.validate(this_input.value) + config[text_input.field_id] = this_input.value + if not validation_result.is_valid: + text_input.query_one(".validation_msg").update("\n".join(validation_result.failure_descriptions)) + else: + text_input.query_one(".validation_msg").update("") + + self.parent.TEMPLATE_CONFIG.__dict__.update( + {"config_name": self.query_one("#config_name", TextInputWithHelp).query_one(Input).value} + ) + print(self.parent.TEMPLATE_CONFIG) + try: + self.parent.TEMPLATE_CONFIG = CreateConfig(**config) + + except ValueError: + pass diff --git a/nf_core/configs/create/otherlocations.py b/nf_core/configs/create/otherlocations.py new file mode 100644 index 0000000000..c13c20cc0b --- /dev/null +++ b/nf_core/configs/create/otherlocations.py @@ -0,0 +1,47 @@ +"""A Textual app to create a pipeline.""" + +from textwrap import dedent + +from textual.app import ComposeResult +from textual.containers import Center, Horizontal +from textual.screen import Screen +from textual.widgets import Button, Footer, Header, Markdown + +from nf_core.configs.create.utils import TextInputWithHelp + + +class ChooseOtherLocations(Screen): + """Name, description, author, etc.""" + + def compose(self) -> ComposeResult: + yield Header() + yield Footer() + yield Markdown( + dedent( + """ + # Additional locations + These are optional locations that can be specified in the Nextflow config to ensure pipelines read or write to the correct locations in different contexts. + """ + ) + ) + with Horizontal(): + yield TextInputWithHelp( + "igenomescache_location", + "", + "Location of existing iGenomes cache", + "Long form help text goes here", + classes="row", + ) + + yield TextInputWithHelp( + "scratch_location", + "", + "Path of alternative scratch location", + "Long form help text goes here", + ) + + yield Center( + Button("Back", id="back", variant="default"), + Button("Next", id="otherlocations_continue", variant="success"), + classes="cta", + ) diff --git a/nf_core/configs/create/schedulertype.py b/nf_core/configs/create/schedulertype.py new file mode 100644 index 0000000000..09ad321fb8 --- /dev/null +++ b/nf_core/configs/create/schedulertype.py @@ -0,0 +1,50 @@ +from textual.app import ComposeResult +from textual.containers import Center, Horizontal +from textual.screen import Screen +from textual.widgets import Button, Footer, Header, Markdown, Select + +markdown_intro = """ +# What job scheduler (i.e. grid executor) is running on your HPC system? + +A job scheduler receives requests from all its users to run their jobs and then decides when and where to run each job on the available computing resources. It will determine where a pipeline process is run and supervise its execution. Nextflow supports many different job schedulers. See [documentation](https://www.nextflow.io/docs/latest/executor.html). +""" + +class ChooseSchedulerType(Screen): + """Select the job scheduler running on your system.""" + + def compose(self) -> ComposeResult: + yield Header() + yield Footer() + yield Markdown(markdown_intro) + yield Center( + Select( + [ + ("local (not recommended)", "scheduler_local"), + ("bridge", "scheduler_bridge"), + ("flux", "scheduler_flux"), + ("condor", "scheduler_htcondor"), + ("hyperqueue", "scheduler_hyperqueue"), + ("ignite", "scheduler_ignite"), + ("lsf", "scheduler_lsf"), + ("moab", "scheduler_moab"), + ("nsqii", "scheduler_nsqii"), + ("oar", "scheduler_oar"), + ("pbs", "scheduler_pbs"), + ("pbspro", "scheduler_pbspro"), + ("sge", "scheduler_sge"), + ("slurm", "scheduler_slurm"), + ], + id="scheduler_type", + ), + classes="cta", + ) + yield Horizontal( + Center( + Button("Back", id="back", variant="default"), + classes="cta", + ), + Center( + Button("Next", id="schedulertype_continue", variant="success"), + classes="cta", + ), + ) \ No newline at end of file diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py new file mode 100644 index 0000000000..c1376c7de3 --- /dev/null +++ b/nf_core/configs/create/utils.py @@ -0,0 +1,304 @@ +import re +from logging import LogRecord +from pathlib import Path +from typing import Optional, Union + +from pydantic import BaseModel, ConfigDict, ValidationError, field_validator +from rich.logging import RichHandler +from textual import on +from textual._context import active_app +from textual.app import ComposeResult +from textual.containers import HorizontalScroll +from textual.message import Message +from textual.validation import ValidationResult, Validator +from textual.widget import Widget +from textual.widgets import Button, Input, Markdown, RichLog, Static, Switch + + +class CreateConfig(BaseModel): + """Pydantic model for the nf-core create config.""" + + config_type: Optional[str] = None + infrastructure_type: Optional[str] = None + for_nfcore_pipelines: Optional[bool] = None + use_modules: Optional[bool] = None + config_name: Optional[str] = None + config_author: Optional[str] = None + config_handle: Optional[str] = None + config_description: Optional[str] = None + config_url: Optional[str] = None + containercache_location: Optional[str] = None + igenomescache_location: Optional[str] = None + scratch_location: Optional[str] = None + savelocation: Optional[str] = None + max_cpus: Optional[str] = None + max_mem: Optional[str] = None + max_time: Optional[str] = None + noretries: Optional[str] = None + + model_config = ConfigDict(extra="allow") + + @field_validator("config_name") + @classmethod + def name_nospecialchars(cls, v: str) -> str: + """Check that the config name is simple.""" + if not re.match(r"^[a-z]+$", v): + raise ValueError("Must be lowercase without punctuation.") + return v + + @field_validator("config_handle") + @classmethod + def handle_suffix(cls, v: str) -> str: + """Check that the config author's handle is in handle format .""" + ## regex taken from: https://github.com/shinnn/github-username-regex + if not re.match(r"^@[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}$", v): + raise ValueError( + "Must be a valid GitHub user handle, starting with @, no punctuation, and max 39 characters." + ) + return v + + @field_validator("config_url") + @classmethod + def valid_url(cls, v: str) -> str: + """Check that the config institutional URL is valid .""" + if not re.match( + r"^https?:\/\/.*", + v, + ): + raise ValueError("Must be a valid URL") + return v + + @field_validator( + "containercache_location", + "igenomescache_location", + "scratch_location", + "savelocation", + ) + @classmethod + def valid_path(cls, v: str) -> str: + """Check that a path is valid.""" + if not Path(v).is_dir(): + raise ValueError("Must be a valid absolute path on your filesystem.") + return v + + @field_validator("max_cpus", "max_mem", "max_time", "noretries") + @classmethod + def valid_number(cls, v: str) -> str: + """Check that a number is valid.""" + if not re.match(r"^[0-9]+$", v): + raise ValueError("Must be a valid integer number.") + return v + + +class TextInput(Static): + """Widget for text inputs. + + Provides standard interface for a text input with short help text + and validation messages. + """ + + def __init__(self, field_id, placeholder, description, default=None, password=None, **kwargs) -> None: + """Initialise the widget with our values. + + Pass on kwargs upstream for standard usage.""" + super().__init__(**kwargs) + self.field_id: str = field_id + self.id: str = field_id + self.placeholder: str = placeholder + self.description: str = description + self.default: str = default + self.password: bool = password + + def compose(self) -> ComposeResult: + yield Static(self.description, classes="field_help") + yield Input( + placeholder=self.placeholder, + validators=[ValidateConfig(self.field_id)], + value=self.default, + password=self.password, + ) + yield Static(classes="validation_msg") + + @on(Input.Changed) + @on(Input.Submitted) + def show_invalid_reasons(self, event: Union[Input.Changed, Input.Submitted]) -> None: + """Validate the text input and show errors if invalid.""" + if not event.validation_result.is_valid: + self.query_one(".validation_msg").update("\n".join(event.validation_result.failure_descriptions)) + else: + self.query_one(".validation_msg").update("") + + +class TextInputWithHelp(Static): + """Widget for text inputs. + + Provides standard interface for a text input with short and optional long help text + and validation messages. + """ + + def __init__( + self, + field_id, + placeholder, + description, + markdown, + default=None, + password=None, + **kwargs, + ) -> None: + """Initialise the widget with our values. + + Pass on kwargs upstream for standard usage.""" + super().__init__(**kwargs) + self.field_id: str = field_id + self.id: str = field_id + self.placeholder: str = placeholder + self.description: str = description + self.markdown = markdown + self.default: str = default + self.password: bool = password + + ## Dynamic updating of question contents + def on_button_pressed(self, event: Button.Pressed) -> None: + """When the button is pressed, change the type of the button.""" + if event.button.id == "show_help": + self.add_class("displayed") + elif event.button.id == "hide_help": + self.remove_class("displayed") + + ## Define the layout of the question + def compose(self) -> ComposeResult: + yield Static(self.description, classes="field_help") + yield HorizontalScroll( + Input( + placeholder=self.placeholder, + validators=[ValidateConfig(self.field_id)], + value=self.default, + password=self.password, + ), + Button("Show help", id="show_help", variant="primary"), + Button("Hide help", id="hide_help"), + HelpText(markdown=self.markdown, classes="help_box"), + Static(classes="validation_msg"), + classes="custom_grid", + ) + + @on(Input.Changed) + @on(Input.Submitted) + def show_invalid_reasons(self, event: Union[Input.Changed, Input.Submitted]) -> None: + """Validate the text input and show errors if invalid.""" + if not event.validation_result.is_valid: + self.query_one(".validation_msg").update("\n".join(event.validation_result.failure_descriptions)) + else: + self.query_one(".validation_msg").update("") + + +class ValidateConfig(Validator): + """Validate any config value, using Pydantic.""" + + def __init__(self, key) -> None: + """Initialise the validator with the model key to validate.""" + super().__init__() + self.key = key + + def validate(self, value: str) -> ValidationResult: + """Try creating a Pydantic object with this key set to this value. + + If it fails, return the error messages.""" + try: + CreateConfig(**{f"{self.key}": value}) + return self.success() + except ValidationError as e: + return self.failure(", ".join([err["msg"] for err in e.errors()])) + + +class HelpText(Markdown): + """A class to show a text box with help text.""" + + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + + def show(self) -> None: + """Method to show the help text box.""" + self.add_class("displayed") + + def hide(self) -> None: + """Method to hide the help text box.""" + self.remove_class("displayed") + + +class ConfigFeature(Static): + """Widget for the activation of config features.""" + + def __init__( + self, + markdown: str, + title: str, + subtitle: str, + field_id: str, + default=bool, + **kwargs, + ) -> None: + super().__init__(**kwargs) + self.markdown = markdown + self.title = title + self.subtitle = subtitle + self.field_id = field_id + self.default = False + + def on_button_pressed(self, event: Button.Pressed) -> None: + """When the button is pressed, change the type of the button.""" + if event.button.id == "show_help": + self.add_class("displayed") + elif event.button.id == "hide_help": + self.remove_class("displayed") + + def compose(self) -> ComposeResult: + """ + Create child widgets. + + Displayed row with a switch, a short text description and a help button. + Hidden row with a help text box. + """ + yield HorizontalScroll( + Switch(value=self.default, id=self.field_id), + Static(self.title, classes="feature_title"), + Static(self.subtitle, classes="feature_subtitle"), + Button("Show help", id="show_help", variant="primary"), + Button("Hide help", id="hide_help"), + classes="custom_grid", + ) + yield HelpText(markdown=self.markdown, classes="help_box") + + +class LoggingConsole(RichLog): + file = False + console: Widget + + def print(self, content): + self.write(content) + + +class CustomLogHandler(RichHandler): + """A Logging handler which extends RichHandler to write to a Widget and handle a Textual App.""" + + def emit(self, record: LogRecord) -> None: + """Invoked by logging.""" + try: + _app = active_app.get() + except LookupError: + pass + else: + super().emit(record) + + +class ShowLogs(Message): + """Custom message to show the logging messages.""" + + pass + + +## Functions +def change_select_disabled(app, widget_id: str, disabled: bool) -> None: + """Change the disabled state of a widget.""" + app.get_widget_by_id(widget_id).disabled = disabled diff --git a/nf_core/configs/create/welcome.py b/nf_core/configs/create/welcome.py new file mode 100644 index 0000000000..de63d6eed4 --- /dev/null +++ b/nf_core/configs/create/welcome.py @@ -0,0 +1,42 @@ +from textwrap import dedent + +from textual.app import ComposeResult +from textual.containers import Center +from textual.screen import Screen +from textual.widgets import Button, Footer, Header, Markdown, Static + +markdown = """ +This app will help you create a new nextflow configuration file. + +It allows you to create both nextflow configs for both infrastructure and pipelines. + +Infrastructure configs are used to define the computational environment in which nf-core pipelines are run, e.g. what memory or CPUs are available, if there is a scheduler, which container engine to use. +Pipeline configs are used to tweak the resources of different steps of a particular pipeline to suit your data, e.g. step X should request only 8.GB of memory. + +While both types of configs can be used in your own pipeline runs (passing the file to Nextflow with `-c`), they can also be added to the centralised [nf-core/configs](https://github.com/nf-core/configs) repository, where they can be used by anyone running nf-core pipelines directly with `-profile`. +""" + + +class WelcomeScreen(Screen): + """A welcome screen for the app.""" + + def compose(self) -> ComposeResult: + yield Header() + yield Footer() + yield Markdown( + dedent( + """ + # Create a nextflow config + """ + ) + ) + yield Static( + f"\n[green]{' ' * 40},--.[grey39]/[green],-." + + "\n[blue] ___ __ __ __ ___ [green]/,-._.--~\\" + + "\n[blue]|\ | |__ __ / ` / \ |__) |__ [yellow] } {" + + "\n[blue] | \| | \__, \__/ | \ |___ [green]\`-._,-`-," + + "\n[green] `._,._,'\n", + id="logo", + ) + yield Markdown(markdown) + yield Center(Button("Let's go!", id="start", variant="success"), classes="cta") diff --git a/nf_core/launch.py b/nf_core/launch.py index 25bb4c150c..bc0cd58aec 100644 --- a/nf_core/launch.py +++ b/nf_core/launch.py @@ -1,5 +1,4 @@ -""" Launch a pipeline, interactively collecting params """ - +"""Launch a pipeline, interactively collecting params""" import copy import json diff --git a/nf_core/licences.py b/nf_core/licences.py index a8a35334dd..be737280f8 100644 --- a/nf_core/licences.py +++ b/nf_core/licences.py @@ -1,6 +1,5 @@ """Lists software licences for a given workflow.""" - import json import logging import os diff --git a/nf_core/lint/files_unchanged.py b/nf_core/lint/files_unchanged.py index 3a3a0cb74a..7e1d9011e9 100644 --- a/nf_core/lint/files_unchanged.py +++ b/nf_core/lint/files_unchanged.py @@ -8,7 +8,7 @@ import yaml -import nf_core.create +import nf_core.pipelines.create.create log = logging.getLogger(__name__) @@ -110,7 +110,7 @@ def files_unchanged(self) -> Dict[str, Union[List[str], bool]]: ] # Only show error messages from pipeline creation - logging.getLogger("nf_core.create").setLevel(logging.ERROR) + logging.getLogger("nf_core.pipelines.create").setLevel(logging.ERROR) # Generate a new pipeline with nf-core create that we can compare to tmp_dir = tempfile.mkdtemp() @@ -127,9 +127,9 @@ def files_unchanged(self) -> Dict[str, Union[List[str], bool]]: with open(template_yaml_path, "w") as fh: yaml.dump(template_yaml, fh, default_flow_style=False) - test_pipeline_dir = Path(tmp_dir, f"{prefix}-{short_name}") - create_obj = nf_core.create.PipelineCreate( - None, None, None, no_git=True, outdir=test_pipeline_dir, template_yaml_path=template_yaml_path + test_pipeline_dir = os.path.join(tmp_dir, f"{prefix}-{short_name}") + create_obj = nf_core.pipelines.create.create.PipelineCreate( + None, None, None, no_git=True, outdir=test_pipeline_dir, template_config=template_yaml_path ) create_obj.init_pipeline() diff --git a/nf_core/lint/nextflow_config.py b/nf_core/lint/nextflow_config.py index d3e29d2363..2e142cde6e 100644 --- a/nf_core/lint/nextflow_config.py +++ b/nf_core/lint/nextflow_config.py @@ -245,10 +245,9 @@ def nextflow_config(self): raise AssertionError() except (AssertionError, IndexError): failed.append( - "Config variable ``manifest.homePage`` did not begin with https://github.com/nf-core/:\n {}".format( - manifest_homepage - ) + f"Config variable ``manifest.homePage`` did not begin with https://github.com/nf-core/:\n {manifest_homepage}" ) + else: passed.append("Config variable ``manifest.homePage`` began with https://github.com/nf-core/") diff --git a/nf_core/list.py b/nf_core/list.py index 67d1a76878..658f4dc6d2 100644 --- a/nf_core/list.py +++ b/nf_core/list.py @@ -1,6 +1,5 @@ """Lists available nf-core pipelines and versions.""" - import json import logging import os diff --git a/nf_core/modules/bump_versions.py b/nf_core/modules/bump_versions.py index b9003be974..9b54174d5a 100644 --- a/nf_core/modules/bump_versions.py +++ b/nf_core/modules/bump_versions.py @@ -3,7 +3,6 @@ or for a single module """ - import logging import os import re diff --git a/nf_core/modules/lint/__init__.py b/nf_core/modules/lint/__init__.py index 866e6312aa..b2816dab6a 100644 --- a/nf_core/modules/lint/__init__.py +++ b/nf_core/modules/lint/__init__.py @@ -6,7 +6,6 @@ nf-core modules lint """ - import logging import os diff --git a/nf_core/modules/lint/module_changes.py b/nf_core/modules/lint/module_changes.py index ee8cabebe1..eb76f4b88b 100644 --- a/nf_core/modules/lint/module_changes.py +++ b/nf_core/modules/lint/module_changes.py @@ -1,6 +1,7 @@ """ Check whether the content of a module has changed compared to the original repository """ + import shutil import tempfile from pathlib import Path diff --git a/nf_core/modules/lint/module_tests.py b/nf_core/modules/lint/module_tests.py index 520f8cf0a2..b1a611d70a 100644 --- a/nf_core/modules/lint/module_tests.py +++ b/nf_core/modules/lint/module_tests.py @@ -1,6 +1,7 @@ """ Lint the tests of a module in nf-core/modules """ + import json import logging from pathlib import Path diff --git a/nf_core/params_file.py b/nf_core/params_file.py index 267fe7086a..78798b065e 100644 --- a/nf_core/params_file.py +++ b/nf_core/params_file.py @@ -1,5 +1,4 @@ -""" Create a YAML parameter file """ - +"""Create a YAML parameter file""" import json import logging diff --git a/nf_core/pipeline-template/.github/CONTRIBUTING.md b/nf_core/pipeline-template/.github/CONTRIBUTING.md index 6244a6544e..3f541162d3 100644 --- a/nf_core/pipeline-template/.github/CONTRIBUTING.md +++ b/nf_core/pipeline-template/.github/CONTRIBUTING.md @@ -9,7 +9,7 @@ Please use the pre-filled template to save time. However, don't be put off by this template - other more general issues and suggestions are welcome! Contributions to the code are even more welcome ;) -{% if branded -%} +{% if is_nfcore -%} > [!NOTE] > If you need help using or modifying {{ name }} then the best place to ask is on the nf-core Slack [#{{ short_name }}](https://nfcore.slack.com/channels/{{ short_name }}) channel ([join our Slack here](https://nf-co.re/join/slack)). @@ -63,7 +63,7 @@ These tests are run both with the latest available version of `Nextflow` and als - Fix the bug, and bump version (X.Y.Z+1). - A PR should be made on `master` from patch to directly this particular bug. -{% if branded -%} +{% if is_nfcore -%} ## Getting help diff --git a/nf_core/pipeline-template/.github/PULL_REQUEST_TEMPLATE.md b/nf_core/pipeline-template/.github/PULL_REQUEST_TEMPLATE.md index d04335275a..ed01b34edc 100644 --- a/nf_core/pipeline-template/.github/PULL_REQUEST_TEMPLATE.md +++ b/nf_core/pipeline-template/.github/PULL_REQUEST_TEMPLATE.md @@ -16,7 +16,7 @@ Learn more about contributing: [CONTRIBUTING.md](https://github.com/{{ name }}/t - [ ] This comment contains a description of changes (with reason). - [ ] If you've fixed a bug or added code that should be tested, add tests! - [ ] If you've added a new tool - have you followed the pipeline conventions in the [contribution docs](https://github.com/{{ name }}/tree/master/.github/CONTRIBUTING.md) - {%- if branded %} + {%- if is_nfcore %} - [ ] If necessary, also make a PR on the {{ name }} _branch_ on the [nf-core/test-datasets](https://github.com/nf-core/test-datasets) repository. {%- endif %} - [ ] Make sure your code lints (`nf-core lint`). diff --git a/nf_core/pipeline-template/.github/workflows/ci.yml b/nf_core/pipeline-template/.github/workflows/ci.yml index 84c727f60d..3880b2c4dc 100644 --- a/nf_core/pipeline-template/.github/workflows/ci.yml +++ b/nf_core/pipeline-template/.github/workflows/ci.yml @@ -31,7 +31,7 @@ jobs: uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 - name: Install Nextflow - uses: nf-core/setup-nextflow@v1 + uses: nf-core/setup-nextflow@v2 with: version: "{% raw %}${{ matrix.NXF_VER }}{% endraw %}" diff --git a/nf_core/pipeline-template/.github/workflows/download_pipeline.yml b/nf_core/pipeline-template/.github/workflows/download_pipeline.yml index dcd7caabfc..4fdec4e243 100644 --- a/nf_core/pipeline-template/.github/workflows/download_pipeline.yml +++ b/nf_core/pipeline-template/.github/workflows/download_pipeline.yml @@ -28,7 +28,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Install Nextflow - uses: nf-core/setup-nextflow@v1 + uses: nf-core/setup-nextflow@v2 - uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5 with: diff --git a/nf_core/pipeline-template/.github/workflows/linting.yml b/nf_core/pipeline-template/.github/workflows/linting.yml index 59b85f95fc..612467ff6e 100644 --- a/nf_core/pipeline-template/.github/workflows/linting.yml +++ b/nf_core/pipeline-template/.github/workflows/linting.yml @@ -35,7 +35,7 @@ jobs: uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 - name: Install Nextflow - uses: nf-core/setup-nextflow@v1 + uses: nf-core/setup-nextflow@v2 - uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5 with: diff --git a/nf_core/pipeline-template/.pre-commit-config.yaml b/nf_core/pipeline-template/.pre-commit-config.yaml index af57081f60..4dc0f1dcd7 100644 --- a/nf_core/pipeline-template/.pre-commit-config.yaml +++ b/nf_core/pipeline-template/.pre-commit-config.yaml @@ -3,6 +3,9 @@ repos: rev: "v3.1.0" hooks: - id: prettier + additional_dependencies: + - prettier@3.2.5 + - repo: https://github.com/editorconfig-checker/editorconfig-checker.python rev: "2.7.3" hooks: diff --git a/nf_core/pipeline-template/README.md b/nf_core/pipeline-template/README.md index a3b141cf38..81853093db 100644 --- a/nf_core/pipeline-template/README.md +++ b/nf_core/pipeline-template/README.md @@ -1,4 +1,4 @@ -{% if branded -%} +{% if is_nfcore -%}

@@ -11,7 +11,7 @@ {% if github_badges -%} [![GitHub Actions CI Status](https://github.com/{{ name }}/actions/workflows/ci.yml/badge.svg)](https://github.com/{{ name }}/actions/workflows/ci.yml) [![GitHub Actions Linting Status](https://github.com/{{ name }}/actions/workflows/linting.yml/badge.svg)](https://github.com/{{ name }}/actions/workflows/linting.yml){% endif -%} -{% if branded -%}[![AWS CI](https://img.shields.io/badge/CI%20tests-full%20size-FF9900?labelColor=000000&logo=Amazon%20AWS)](https://nf-co.re/{{ short_name }}/results){% endif -%} +{% if is_nfcore -%}[![AWS CI](https://img.shields.io/badge/CI%20tests-full%20size-FF9900?labelColor=000000&logo=Amazon%20AWS)](https://nf-co.re/{{ short_name }}/results){% endif -%} {%- if github_badges -%} [![Cite with Zenodo](http://img.shields.io/badge/DOI-10.5281/zenodo.XXXXXXX-1073c8?labelColor=000000)](https://doi.org/10.5281/zenodo.XXXXXXX) [![nf-test](https://img.shields.io/badge/unit_tests-nf--test-337ab7.svg)](https://www.nf-test.com) @@ -23,10 +23,10 @@ [![Launch on Seqera Platform](https://img.shields.io/badge/Launch%20%F0%9F%9A%80-Seqera%20Platform-%234256e7)](https://tower.nf/launch?pipeline=https://github.com/{{ name }}) {% endif -%} -{%- if branded -%}[![Get help on Slack](http://img.shields.io/badge/slack-nf--core%20%23{{ short_name }}-4A154B?labelColor=000000&logo=slack)](https://nfcore.slack.com/channels/{{ short_name }}){% endif -%} -{%- if branded -%}[![Follow on Twitter](http://img.shields.io/badge/twitter-%40nf__core-1DA1F2?labelColor=000000&logo=twitter)](https://twitter.com/nf_core){% endif -%} -{%- if branded -%}[![Follow on Mastodon](https://img.shields.io/badge/mastodon-nf__core-6364ff?labelColor=FFFFFF&logo=mastodon)](https://mstdn.science/@nf_core){% endif -%} -{%- if branded -%}[![Watch on YouTube](http://img.shields.io/badge/youtube-nf--core-FF0000?labelColor=000000&logo=youtube)](https://www.youtube.com/c/nf-core) +{%- if is_nfcore -%}[![Get help on Slack](http://img.shields.io/badge/slack-nf--core%20%23{{ short_name }}-4A154B?labelColor=000000&logo=slack)](https://nfcore.slack.com/channels/{{ short_name }}){% endif -%} +{%- if is_nfcore -%}[![Follow on Twitter](http://img.shields.io/badge/twitter-%40nf__core-1DA1F2?labelColor=000000&logo=twitter)](https://twitter.com/nf_core){% endif -%} +{%- if is_nfcore -%}[![Follow on Mastodon](https://img.shields.io/badge/mastodon-nf__core-6364ff?labelColor=FFFFFF&logo=mastodon)](https://mstdn.science/@nf_core){% endif -%} +{%- if is_nfcore -%}[![Watch on YouTube](http://img.shields.io/badge/youtube-nf--core-FF0000?labelColor=000000&logo=youtube)](https://www.youtube.com/c/nf-core) {% endif -%} @@ -83,7 +83,7 @@ nextflow run {{ name }} \ > Please provide pipeline parameters via the CLI or Nextflow `-params-file` option. Custom config files including those provided by the `-c` Nextflow option can be used to provide any configuration _**except for parameters**_; > see [docs](https://nf-co.re/usage/configuration#custom-configuration-files). -{% if branded -%} +{% if is_nfcore -%} For more details and further functionality, please refer to the [usage documentation](https://nf-co.re/{{ short_name }}/usage) and the [parameter documentation](https://nf-co.re/{{ short_name }}/parameters). @@ -107,7 +107,7 @@ We thank the following people for their extensive assistance in the development If you would like to contribute to this pipeline, please see the [contributing guidelines](.github/CONTRIBUTING.md). -{% if branded -%} +{% if is_nfcore -%} For further information or help, don't hesitate to get in touch on the [Slack `#{{ short_name }}` channel](https://nfcore.slack.com/channels/{{ short_name }}) (you can join with [this invite](https://nf-co.re/join/slack)). {% endif -%} @@ -121,7 +121,7 @@ For further information or help, don't hesitate to get in touch on the [Slack `# An extensive list of references for the tools used by the pipeline can be found in the [`CITATIONS.md`](CITATIONS.md) file. -{% if branded -%} +{% if is_nfcore -%} You can cite the `nf-core` publication as follows: {% else -%} diff --git a/nf_core/pipeline-template/assets/email_template.txt b/nf_core/pipeline-template/assets/email_template.txt index 25b12e8ce0..7927d45034 100644 --- a/nf_core/pipeline-template/assets/email_template.txt +++ b/nf_core/pipeline-template/assets/email_template.txt @@ -1,4 +1,4 @@ -{% if branded -%} +{% if is_nfcore -%} ---------------------------------------------------- ,--./,-. ___ __ __ __ ___ /,-._.--~\\ diff --git a/nf_core/pipeline-template/assets/multiqc_config.yml b/nf_core/pipeline-template/assets/multiqc_config.yml index b13b7ae074..cd4e539b31 100644 --- a/nf_core/pipeline-template/assets/multiqc_config.yml +++ b/nf_core/pipeline-template/assets/multiqc_config.yml @@ -1,11 +1,11 @@ report_comment: > {% if 'dev' in version -%} This report has been generated by the {{ name }} - analysis pipeline.{% if branded %} For information about how to interpret these results, please see the + analysis pipeline.{% if is_nfcore %} For information about how to interpret these results, please see the documentation.{% endif %} {%- else %} This report has been generated by the {{ name }} - analysis pipeline.{% if branded %} For information about how to interpret these results, please see the + analysis pipeline.{% if is_nfcore %} For information about how to interpret these results, please see the documentation.{% endif %} {% endif %} report_section_order: diff --git a/nf_core/pipeline-template/docs/README.md b/nf_core/pipeline-template/docs/README.md index e94889c53d..9a237c1ad4 100644 --- a/nf_core/pipeline-template/docs/README.md +++ b/nf_core/pipeline-template/docs/README.md @@ -6,7 +6,7 @@ The {{ name }} documentation is split into the following pages: - An overview of how the pipeline works, how to run it and a description of all of the different command-line flags. - [Output](output.md) - An overview of the different results produced by the pipeline and how to interpret them. - {%- if branded %} + {%- if is_nfcore %} You can find a lot more documentation about installing, configuring and running nf-core pipelines on the website: [https://nf-co.re](https://nf-co.re) {% else %} diff --git a/nf_core/pipeline-template/docs/usage.md b/nf_core/pipeline-template/docs/usage.md index c908d3d38c..3d280b2b5f 100644 --- a/nf_core/pipeline-template/docs/usage.md +++ b/nf_core/pipeline-template/docs/usage.md @@ -1,6 +1,6 @@ # {{ name }}: Usage -{% if branded -%} +{% if is_nfcore -%} ## :warning: Please read this documentation on the nf-core website: [https://nf-co.re/{{ short_name }}/usage](https://nf-co.re/{{ short_name }}/usage) diff --git a/nf_core/pipeline-template/main.nf b/nf_core/pipeline-template/main.nf index 2590f7467b..1fd6a5b275 100644 --- a/nf_core/pipeline-template/main.nf +++ b/nf_core/pipeline-template/main.nf @@ -4,7 +4,7 @@ {{ name }} ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Github : https://github.com/{{ name }} -{%- if branded %} +{%- if is_nfcore %} Website: https://nf-co.re/{{ short_name }} Slack : https://nfcore.slack.com/channels/{{ short_name }} {%- endif %} diff --git a/nf_core/pipeline-template/nextflow_schema.json b/nf_core/pipeline-template/nextflow_schema.json index 77b1a7a070..2303767c20 100644 --- a/nf_core/pipeline-template/nextflow_schema.json +++ b/nf_core/pipeline-template/nextflow_schema.json @@ -20,7 +20,7 @@ "mimetype": "text/csv", "pattern": "^\\S+\\.csv$", "description": "Path to comma-separated file containing information about the samples in the experiment.", - "help_text": "You will need to create a design file with information about the samples in your experiment before running the pipeline. Use this parameter to specify its location. It has to be a comma-separated file with 3 columns, and a header row.{% if branded %} See [usage docs](https://nf-co.re/{{ short_name }}/usage#samplesheet-input).{% endif %}", + "help_text": "You will need to create a design file with information about the samples in your experiment before running the pipeline. Use this parameter to specify its location. It has to be a comma-separated file with 3 columns, and a header row.{% if is_nfcore %} See [usage docs](https://nf-co.re/{{ short_name }}/usage#samplesheet-input).{% endif %}", "fa_icon": "fas fa-file-csv" }, "outdir": { diff --git a/nf_core/pipelines/__init__.py b/nf_core/pipelines/__init__.py new file mode 100644 index 0000000000..bc981c449f --- /dev/null +++ b/nf_core/pipelines/__init__.py @@ -0,0 +1 @@ +from .create import PipelineCreateApp diff --git a/nf_core/pipelines/create/__init__.py b/nf_core/pipelines/create/__init__.py new file mode 100644 index 0000000000..feff20659d --- /dev/null +++ b/nf_core/pipelines/create/__init__.py @@ -0,0 +1,100 @@ +"""A Textual app to create a pipeline.""" +import logging + +from textual.app import App +from textual.widgets import Button + +from nf_core.pipelines.create.basicdetails import BasicDetails +from nf_core.pipelines.create.custompipeline import CustomPipeline +from nf_core.pipelines.create.error import ExistError +from nf_core.pipelines.create.finaldetails import FinalDetails +from nf_core.pipelines.create.githubexit import GithubExit +from nf_core.pipelines.create.githubrepo import GithubRepo +from nf_core.pipelines.create.githubrepoquestion import GithubRepoQuestion +from nf_core.pipelines.create.loggingscreen import LoggingScreen +from nf_core.pipelines.create.nfcorepipeline import NfcorePipeline +from nf_core.pipelines.create.pipelinetype import ChoosePipelineType +from nf_core.pipelines.create.utils import ( + CreateConfig, + CustomLogHandler, + LoggingConsole, +) +from nf_core.pipelines.create.welcome import WelcomeScreen + +log_handler = CustomLogHandler(console=LoggingConsole(classes="log_console"), rich_tracebacks=True, markup=True) +logging.basicConfig( + level="INFO", + handlers=[log_handler], + format="%(message)s", +) +log_handler.setLevel("INFO") + + +class PipelineCreateApp(App[CreateConfig]): + """A Textual app to manage stopwatches.""" + + CSS_PATH = "create.tcss" + TITLE = "nf-core create" + SUB_TITLE = "Create a new pipeline with the nf-core pipeline template" + BINDINGS = [ + ("d", "toggle_dark", "Toggle dark mode"), + ("q", "quit", "Quit"), + ] + SCREENS = { + "welcome": WelcomeScreen(), + "basic_details": BasicDetails(), + "choose_type": ChoosePipelineType(), + "type_custom": CustomPipeline(), + "type_nfcore": NfcorePipeline(), + "final_details": FinalDetails(), + "github_repo_question": GithubRepoQuestion(), + "github_repo": GithubRepo(), + "github_exit": GithubExit(), + "error_screen": ExistError(), + } + + # Initialise config as empty + TEMPLATE_CONFIG = CreateConfig() + + # Initialise pipeline type + PIPELINE_TYPE = None + + # Log handler + LOG_HANDLER = log_handler + # Logging state + LOGGING_STATE = None + + def on_mount(self) -> None: + self.push_screen("welcome") + + def on_button_pressed(self, event: Button.Pressed) -> None: + """Handle all button pressed events.""" + if event.button.id == "start": + self.push_screen("choose_type") + elif event.button.id == "type_nfcore": + self.PIPELINE_TYPE = "nfcore" + self.push_screen("basic_details") + elif event.button.id == "type_custom": + self.PIPELINE_TYPE = "custom" + self.push_screen("basic_details") + elif event.button.id == "continue": + self.push_screen("final_details") + elif event.button.id == "github_repo": + self.push_screen("github_repo") + elif event.button.id == "close_screen": + # Switch screen (not push) to allow viewing old logging messages + self.switch_screen("github_repo_question") + elif event.button.id == "exit": + self.push_screen("github_exit") + elif event.button.id == "show_logging": + # Set logging state to repo created to see the button for closing the logging screen + self.LOGGING_STATE = "repo created" + self.switch_screen(LoggingScreen()) + if event.button.id == "close_app": + self.exit(return_code=0) + if event.button.id == "back": + self.pop_screen() + + def action_toggle_dark(self) -> None: + """An action to toggle dark mode.""" + self.dark: bool = not self.dark diff --git a/nf_core/pipelines/create/basicdetails.py b/nf_core/pipelines/create/basicdetails.py new file mode 100644 index 0000000000..e4f36e4035 --- /dev/null +++ b/nf_core/pipelines/create/basicdetails.py @@ -0,0 +1,78 @@ +"""A Textual app to create a pipeline.""" +from textwrap import dedent + +from textual import on +from textual.app import ComposeResult +from textual.containers import Center, Horizontal +from textual.screen import Screen +from textual.widgets import Button, Footer, Header, Input, Markdown + +from nf_core.pipelines.create.utils import CreateConfig, TextInput + + +class BasicDetails(Screen): + """Name, description, author, etc.""" + + def compose(self) -> ComposeResult: + yield Header() + yield Footer() + yield Markdown( + dedent( + """ + # Basic details + """ + ) + ) + with Horizontal(): + yield TextInput( + "org", + "Organisation", + "GitHub organisation", + "nf-core", + classes="column", + disabled=self.parent.PIPELINE_TYPE == "nfcore", + ) + yield TextInput( + "name", + "Pipeline Name", + "Workflow name", + classes="column", + ) + + yield TextInput( + "description", + "Description", + "A short description of your pipeline.", + ) + yield TextInput( + "author", + "Author(s)", + "Name of the main author / authors", + ) + yield Center( + Button("Back", id="back", variant="default"), + Button("Next", id="next", variant="success"), + classes="cta", + ) + + @on(Button.Pressed) + def on_button_pressed(self, event: Button.Pressed) -> None: + """Save fields to the config.""" + config = {} + for text_input in self.query("TextInput"): + this_input = text_input.query_one(Input) + validation_result = this_input.validate(this_input.value) + config[text_input.field_id] = this_input.value + if not validation_result.is_valid: + text_input.query_one(".validation_msg").update("\n".join(validation_result.failure_descriptions)) + else: + text_input.query_one(".validation_msg").update("") + try: + self.parent.TEMPLATE_CONFIG = CreateConfig(**config) + if event.button.id == "next": + if self.parent.PIPELINE_TYPE == "nfcore": + self.parent.push_screen("type_nfcore") + elif self.parent.PIPELINE_TYPE == "custom": + self.parent.push_screen("type_custom") + except ValueError: + pass diff --git a/nf_core/create.py b/nf_core/pipelines/create/create.py similarity index 65% rename from nf_core/create.py rename to nf_core/pipelines/create/create.py index c094d33a22..9347b7e678 100644 --- a/nf_core/create.py +++ b/nf_core/pipelines/create/create.py @@ -1,17 +1,17 @@ """Creates a nf-core pipeline matching the current organization's specification based on a template. """ + import configparser import logging import os import re import shutil -import sys from pathlib import Path +from typing import Optional, Union import git import jinja2 -import questionary import yaml import nf_core @@ -19,6 +19,7 @@ import nf_core.utils from nf_core.create_logo import create_logo from nf_core.lint_utils import run_prettier_on_file +from nf_core.pipelines.create.utils import CreateConfig log = logging.getLogger(__name__) @@ -30,31 +31,52 @@ class PipelineCreate: name (str): Name for the pipeline. description (str): Description for the pipeline. author (str): Authors name of the pipeline. - version (str): Version flag. Semantic versioning only. Defaults to `1.0dev`. + version (str): Version flag. Semantic versioning only. Defaults to `1.0.0dev`. no_git (bool): Prevents the creation of a local Git repository for the pipeline. Defaults to False. - force (bool): Overwrites a given workflow directory with the same name. Defaults to False. + force (bool): Overwrites a given workflow directory with the same name. Defaults to False. Used for tests and sync command. May the force be with you. outdir (str): Path to the local output directory. - template_yaml_path (str): Path to template.yml file for pipeline creation settings. - plain (bool): If true the Git repository will be initialized plain. + template_config (str|CreateConfig): Path to template.yml file for pipeline creation settings. or pydantic model with the customisation for pipeline creation settings. + organisation (str): Name of the GitHub organisation to create the pipeline. Will be the prefix of the pipeline. + from_config_file (bool): If true the pipeline will be created from the `.nf-core.yml` config file. Used for tests and sync command. default_branch (str): Specifies the --initial-branch name. """ def __init__( self, - name, - description, - author, - version="1.0dev", - no_git=False, - force=False, - outdir=None, - template_yaml_path=None, - plain=False, - default_branch=None, + name: Optional[str] = None, + description: Optional[str] = None, + author: Optional[str] = None, + version: str = "1.0.0dev", + no_git: bool = False, + force: bool = False, + outdir: Optional[str] = None, + template_config: Optional[Union[str, CreateConfig, Path]] = None, + organisation: str = "nf-core", + from_config_file: bool = False, + default_branch: Optional[str] = None, ): - self.template_params, skip_paths_keys, self.template_yaml = self.create_param_dict( - name, description, author, version, template_yaml_path, plain, outdir if outdir else "." + if isinstance(template_config, CreateConfig): + self.config = template_config + elif from_config_file: + # Try reading config file + _, config_yml = nf_core.utils.load_tools_config(outdir if outdir else ".") + # Obtain a CreateConfig object from `.nf-core.yml` config file + if "template" in config_yml: + self.config = CreateConfig(**config_yml["template"]) + else: + raise UserWarning("The template configuration was not provided in '.nf-core.yml'.") + elif (name and description and author) or ( + template_config and (isinstance(template_config, str) or isinstance(template_config, Path)) + ): + # Obtain a CreateConfig object from the template yaml file + self.config = self.check_template_yaml_info(template_config, name, description, author) + self.update_config(organisation, version, force, outdir) + else: + raise UserWarning("The template configuration was not provided.") + + self.jinja_params, skip_paths = self.obtain_jinja_params_dict( + self.config.skip_features or [], self.config.outdir ) skippable_paths = { @@ -64,7 +86,7 @@ def __init__( ], "ci": [".github/workflows/"], "igenomes": ["conf/igenomes.config"], - "branded": [ + "is_nfcore": [ ".github/ISSUE_TEMPLATE/config", "CODE_OF_CONDUCT.md", ".github/workflows/awsfulltest.yml", @@ -72,176 +94,154 @@ def __init__( ], } # Get list of files we're skipping with the supplied skip keys - self.skip_paths = set(sp for k in skip_paths_keys for sp in skippable_paths[k]) + self.skip_paths = set(sp for k in skip_paths for sp in skippable_paths[k]) # Set convenience variables - self.name = self.template_params["name"] + self.name = self.config.name # Set fields used by the class methods - self.no_git = ( - no_git if self.template_params["github"] else True - ) # Set to True if template was configured without github hosting + self.no_git = no_git self.default_branch = default_branch - self.force = force + self.force = self.config.force if outdir is None: - outdir = os.path.join(os.getcwd(), self.template_params["name_noslash"]) + outdir = os.path.join(os.getcwd(), self.jinja_params["name_noslash"]) self.outdir = Path(outdir) - def create_param_dict(self, name, description, author, version, template_yaml_path, plain, pipeline_dir): - """Creates a dictionary of parameters for the new pipeline. + def check_template_yaml_info(self, template_yaml, name, description, author): + """Ensure that the provided template yaml file contains the necessary information. Args: + template_yaml (str): Template yaml file. name (str): Name for the pipeline. description (str): Description for the pipeline. author (str): Authors name of the pipeline. - version (str): Version flag. - template_yaml_path (str): Path to YAML file containing template parameters. - plain (bool): If true the pipeline template will be initialized plain, without customisation. + + Returns: + CreateConfig: Pydantic model for the nf-core create config. + + Raises: + UserWarning: if template yaml file does not contain all the necessary information. + UserWarning: if template yaml file does not exist. + """ + # Obtain template customization info from template yaml file or `.nf-core.yml` config file + config = CreateConfig() + if template_yaml: + try: + with open(template_yaml) as f: + template_yaml = yaml.safe_load(f) + config = CreateConfig(**template_yaml) + except FileNotFoundError: + raise UserWarning(f"Template YAML file '{template_yaml}' not found.") + + missing_fields = [] + if config.name is None and name is None: + missing_fields.append("name") + elif config.name is None: + config.name = name + if config.description is None and description is None: + missing_fields.append("description") + elif config.description is None: + config.description = description + if config.author is None and author is None: + missing_fields.append("author") + elif config.author is None: + config.author = author + if len(missing_fields) > 0: + raise UserWarning( + f"Template YAML file does not contain the following required fields: {', '.join(missing_fields)}" + ) + + return config + + def update_config(self, organisation, version, force, outdir): + """Updates the config file with arguments provided through command line. + + Args: + organisation (str): Name of the GitHub organisation to create the pipeline. + version (str): Version of the pipeline. + force (bool): Overwrites a given workflow directory with the same name. + outdir (str): Path to the local output directory. + """ + if self.config.org is None: + self.config.org = organisation + if self.config.version is None: + self.config.version = version if version else "1.0.0dev" + if self.config.force is None: + self.config.force = force if force else False + if self.config.outdir is None: + self.config.outdir = outdir if outdir else "." + if self.config.is_nfcore is None: + self.config.is_nfcore = organisation == "nf-core" + + def obtain_jinja_params_dict(self, features_to_skip, pipeline_dir): + """Creates a dictionary of parameters for the new pipeline. + + Args: + features_to_skip (list): List of template features/areas to skip. pipeline_dir (str): Path to the pipeline directory. + + Returns: + jinja_params (dict): Dictionary of template areas to skip with values true/false. + skip_paths (list): List of template areas which contain paths to skip. """ # Try reading config file _, config_yml = nf_core.utils.load_tools_config(pipeline_dir) - # Obtain template customization info from template yaml file or `.nf-core.yml` config file - try: - if template_yaml_path is not None: - with open(template_yaml_path) as f: - template_yaml = yaml.safe_load(f) - elif "template" in config_yml: - template_yaml = config_yml["template"] - else: - template_yaml = {} - except FileNotFoundError: - raise UserWarning(f"Template YAML file '{template_yaml_path}' not found.") - - param_dict = {} - # Get the necessary parameters either from the template or command line arguments - param_dict["name"] = self.get_param("name", name, template_yaml, template_yaml_path) - param_dict["description"] = self.get_param("description", description, template_yaml, template_yaml_path) - param_dict["author"] = self.get_param("author", author, template_yaml, template_yaml_path) - - if "version" in template_yaml: - if version is not None: - log.info(f"Overriding --version with version found in {template_yaml_path}") - version = template_yaml["version"] - param_dict["version"] = version - # Define the different template areas, and what actions to take for each # if they are skipped template_areas = { - "github": {"name": "GitHub hosting", "file": True, "content": False}, - "ci": {"name": "GitHub CI", "file": True, "content": False}, - "github_badges": {"name": "GitHub badges", "file": False, "content": True}, - "igenomes": {"name": "iGenomes config", "file": True, "content": True}, - "nf_core_configs": {"name": "nf-core/configs", "file": False, "content": True}, + "github": {"file": True, "content": False}, + "ci": {"file": True, "content": False}, + "github_badges": {"file": False, "content": True}, + "igenomes": {"file": True, "content": True}, + "nf_core_configs": {"file": False, "content": True}, } - # Once all necessary parameters are set, check if the user wants to customize the template more - if template_yaml_path is None and not plain: - customize_template = questionary.confirm( - "Do you want to customize which parts of the template are used?", - style=nf_core.utils.nfcore_question_style, - default=False, - ).unsafe_ask() - if customize_template: - template_yaml.update(self.customize_template(template_areas)) - - # Now look in the template for more options, otherwise default to nf-core defaults - param_dict["prefix"] = template_yaml.get("prefix", "nf-core") - param_dict["branded"] = param_dict["prefix"] == "nf-core" - - skip_paths = [] if param_dict["branded"] else ["branded"] + # Set the parameters for the jinja template + jinja_params = self.config.model_dump() + # Add template areas to jinja params and create list of areas with paths to skip + skip_paths = [] for t_area in template_areas: - areas_to_skip = template_yaml.get("skip", []) - if isinstance(areas_to_skip, str): - areas_to_skip = [areas_to_skip] - if t_area in areas_to_skip: + if t_area in features_to_skip: if template_areas[t_area]["file"]: skip_paths.append(t_area) - param_dict[t_area] = False + jinja_params[t_area] = False else: - param_dict[t_area] = True - # If github is selected, exclude also github_badges - if not param_dict["github"]: - param_dict["github_badges"] = False + jinja_params[t_area] = True + + # Add is_nfcore as an area to skip for non-nf-core pipelines, to skip all nf-core files + if not jinja_params["is_nfcore"]: + skip_paths.append("is_nfcore") # Set the last parameters based on the ones provided - param_dict["short_name"] = ( - param_dict["name"].lower().replace(r"/\s+/", "-").replace(f"{param_dict['prefix']}/", "").replace("/", "-") + jinja_params["short_name"] = ( + jinja_params["name"].lower().replace(r"/\s+/", "-").replace(f"{jinja_params['org']}/", "").replace("/", "-") ) - param_dict["name"] = f"{param_dict['prefix']}/{param_dict['short_name']}" - param_dict["name_noslash"] = param_dict["name"].replace("/", "-") - param_dict["prefix_nodash"] = param_dict["prefix"].replace("-", "") - param_dict["name_docker"] = param_dict["name"].replace(param_dict["prefix"], param_dict["prefix_nodash"]) - param_dict["logo_light"] = f"nf-core-{param_dict['short_name']}_logo_light.png" - param_dict["logo_dark"] = f"nf-core-{param_dict['short_name']}_logo_dark.png" - param_dict["version"] = version + jinja_params["name"] = f"{jinja_params['org']}/{jinja_params['short_name']}" + jinja_params["name_noslash"] = jinja_params["name"].replace("/", "-") + jinja_params["prefix_nodash"] = jinja_params["org"].replace("-", "") + jinja_params["name_docker"] = jinja_params["name"].replace(jinja_params["org"], jinja_params["prefix_nodash"]) + jinja_params["logo_light"] = f"{jinja_params['name_noslash']}_logo_light.png" + jinja_params["logo_dark"] = f"{jinja_params['name_noslash']}_logo_dark.png" if ( "lint" in config_yml and "nextflow_config" in config_yml["lint"] and "manifest.name" in config_yml["lint"]["nextflow_config"] ): - return param_dict, skip_paths, template_yaml + return jinja_params, skip_paths # Check that the pipeline name matches the requirements - if not re.match(r"^[a-z]+$", param_dict["short_name"]): - if param_dict["prefix"] == "nf-core": + if not re.match(r"^[a-z]+$", jinja_params["short_name"]): + if jinja_params["is_nfcore"]: raise UserWarning("[red]Invalid workflow name: must be lowercase without punctuation.") else: log.warning( "Your workflow name is not lowercase without punctuation. This may cause Nextflow errors.\nConsider changing the name to avoid special characters." ) - return param_dict, skip_paths, template_yaml - - def customize_template(self, template_areas): - """Customizes the template parameters. - - Args: - template_areas (list): List of available template areas to skip. - """ - template_yaml = {} - prefix = questionary.text("Pipeline prefix", style=nf_core.utils.nfcore_question_style).unsafe_ask() - while not re.match(r"^[a-zA-Z_][a-zA-Z0-9-_]*$", prefix): - log.error("[red]Pipeline prefix cannot start with digit or hyphen and cannot contain punctuation.[/red]") - prefix = questionary.text( - "Please provide a new pipeline prefix", style=nf_core.utils.nfcore_question_style - ).unsafe_ask() - template_yaml["prefix"] = prefix - - choices = [{"name": template_areas[area]["name"], "value": area} for area in template_areas] - template_yaml["skip"] = questionary.checkbox( - "Skip template areas?", choices=choices, style=nf_core.utils.nfcore_question_style - ).unsafe_ask() - return template_yaml - - def get_param(self, param_name, passed_value, template_yaml, template_yaml_path): - if param_name in template_yaml: - if passed_value is not None: - log.info(f"overriding --{param_name} with name found in {template_yaml_path}") - passed_value = template_yaml[param_name] - if passed_value is None: - passed_value = getattr(self, f"prompt_wf_{param_name}")() - return passed_value - - def prompt_wf_name(self): - wf_name = questionary.text("Workflow name", style=nf_core.utils.nfcore_question_style).unsafe_ask() - while not re.match(r"^[a-z]+$", wf_name): - log.error("[red]Invalid workflow name: must be lowercase without punctuation.") - wf_name = questionary.text( - "Please provide a new workflow name", style=nf_core.utils.nfcore_question_style - ).unsafe_ask() - return wf_name - - def prompt_wf_description(self): - wf_description = questionary.text("Description", style=nf_core.utils.nfcore_question_style).unsafe_ask() - return wf_description - - def prompt_wf_author(self): - wf_author = questionary.text("Author", style=nf_core.utils.nfcore_question_style).unsafe_ask() - return wf_author + return jinja_params, skip_paths def init_pipeline(self): """Creates the nf-core pipeline.""" @@ -253,7 +253,7 @@ def init_pipeline(self): if not self.no_git: self.git_init_pipeline() - if self.template_params["branded"]: + if self.config.is_nfcore: log.info( "[green bold]!!!!!! IMPORTANT !!!!!!\n\n" "[green not bold]If you are interested in adding your pipeline to the nf-core community,\n" @@ -273,7 +273,7 @@ def render_template(self): else: log.error(f"Output directory '{self.outdir}' exists!") log.info("Use -f / --force to overwrite existing files") - sys.exit(1) + raise UserWarning(f"Output directory '{self.outdir}' exists!") else: os.makedirs(self.outdir) @@ -281,15 +281,15 @@ def render_template(self): env = jinja2.Environment( loader=jinja2.PackageLoader("nf_core", "pipeline-template"), keep_trailing_newline=True ) - template_dir = os.path.join(os.path.dirname(__file__), "pipeline-template") - object_attrs = self.template_params + template_dir = os.path.join(os.path.dirname(nf_core.__file__), "pipeline-template") + object_attrs = self.jinja_params object_attrs["nf_core_version"] = nf_core.__version__ # Can't use glob.glob() as need recursive hidden dotfiles - https://stackoverflow.com/a/58126417/713980 template_files = list(Path(template_dir).glob("**/*")) template_files += list(Path(template_dir).glob("*")) ignore_strs = [".pyc", "__pycache__", ".pyo", ".pyd", ".DS_Store", ".egg"] - short_name = self.template_params["short_name"] + short_name = self.jinja_params["short_name"] rename_files = { "workflows/pipeline.nf": f"workflows/{short_name}.nf", "subworkflows/local/utils_nfcore_pipeline_pipeline/main.nf": f"subworkflows/local/utils_nfcore_{short_name}_pipeline/main.nf", @@ -347,14 +347,14 @@ def render_template(self): os.chmod(output_path, template_stat.st_mode) # Remove all unused parameters in the nextflow schema - if not self.template_params["igenomes"] or not self.template_params["nf_core_configs"]: + if not self.jinja_params["igenomes"] or not self.jinja_params["nf_core_configs"]: self.update_nextflow_schema() - if self.template_params["branded"]: + if self.config.is_nfcore: # Make a logo and save it, if it is a nf-core pipeline self.make_pipeline_logo() else: - if self.template_params["github"]: + if self.jinja_params["github"]: # Remove field mentioning nf-core docs # in the github bug report template self.remove_nf_core_in_bug_report_template() @@ -362,10 +362,10 @@ def render_template(self): # Update the .nf-core.yml with linting configurations self.fix_linting() - if self.template_yaml: + if self.config: config_fn, config_yml = nf_core.utils.load_tools_config(self.outdir) with open(self.outdir / config_fn, "w") as fh: - config_yml.update(template=self.template_yaml) + config_yml.update(template=self.config.model_dump()) yaml.safe_dump(config_yml, fh) log.debug(f"Dumping pipeline template yml to pipeline config file '{config_fn.name}'") run_prettier_on_file(self.outdir / config_fn) @@ -409,7 +409,7 @@ def fix_linting(self): for a customized pipeline. """ # Create a lint config - short_name = self.template_params["short_name"] + short_name = self.jinja_params["short_name"] lint_config = { "files_exist": [ "CODE_OF_CONDUCT.md", @@ -434,7 +434,7 @@ def fix_linting(self): } # Add GitHub hosting specific configurations - if not self.template_params["github"]: + if not self.jinja_params["github"]: lint_config["files_exist"].extend( [ ".github/ISSUE_TEMPLATE/bug_report.yml", @@ -460,7 +460,7 @@ def fix_linting(self): ) # Add CI specific configurations - if not self.template_params["ci"]: + if not self.jinja_params["ci"]: lint_config["files_exist"].extend( [ ".github/workflows/branch.yml", @@ -471,7 +471,7 @@ def fix_linting(self): ) # Add custom config specific configurations - if not self.template_params["nf_core_configs"]: + if not self.jinja_params["nf_core_configs"]: lint_config["files_exist"].extend(["conf/igenomes.config"]) lint_config["nextflow_config"].extend( [ @@ -483,15 +483,15 @@ def fix_linting(self): ) # Add igenomes specific configurations - if not self.template_params["igenomes"]: + if not self.jinja_params["igenomes"]: lint_config["files_exist"].extend(["conf/igenomes.config"]) # Add github badges specific configurations - if not self.template_params["github_badges"] or not self.template_params["github"]: + if not self.jinja_params["github_badges"] or not self.jinja_params["github"]: lint_config["readme"] = ["nextflow_badge"] - # If the pipeline is unbranded - if not self.template_params["branded"]: + # If the pipeline is not nf-core + if not self.config.is_nfcore: lint_config["files_unchanged"].extend([".github/ISSUE_TEMPLATE/bug_report.yml"]) # Add the lint content to the preexisting nf-core config @@ -505,11 +505,11 @@ def fix_linting(self): def make_pipeline_logo(self): """Fetch a logo for the new pipeline from the nf-core website""" email_logo_path = Path(self.outdir) / "assets" - create_logo(text=self.template_params["short_name"], dir=email_logo_path, theme="light", force=self.force) + create_logo(text=self.jinja_params["short_name"], dir=email_logo_path, theme="light", force=self.force) for theme in ["dark", "light"]: readme_logo_path = Path(self.outdir) / "docs" / "images" create_logo( - text=self.template_params["short_name"], dir=readme_logo_path, width=600, theme=theme, force=self.force + text=self.jinja_params["short_name"], dir=readme_logo_path, width=600, theme=theme, force=self.force ) def git_init_pipeline(self): @@ -553,10 +553,9 @@ def git_init_pipeline(self): repo.git.branch("TEMPLATE") repo.git.branch("dev") else: - log.error( + raise UserWarning( "Branches 'TEMPLATE' and 'dev' already exist. Use --force to overwrite existing branches." ) - sys.exit(1) log.info( "Done. Remember to add a remote and push to GitHub:\n" f"[white on grey23] cd {self.outdir} \n" diff --git a/nf_core/pipelines/create/create.tcss b/nf_core/pipelines/create/create.tcss new file mode 100644 index 0000000000..46b3989017 --- /dev/null +++ b/nf_core/pipelines/create/create.tcss @@ -0,0 +1,106 @@ +#logo { + text-align:center; +} +.cta { + layout: horizontal; + margin-bottom: 1; +} +.cta Button { + margin-left: 3; + margin-right: 3; +} + +.custom_grid { + height: auto; +} +.custom_grid Switch { + width: auto; +} +.custom_grid Static { + width: 1fr; + margin: 1 8; +} +.custom_grid Button { + width: auto; +} + +.field_help { + padding: 1 1 0 1; + color: $text-muted; + text-style: italic; +} +.validation_msg { + padding: 0 1; + color: $error; +} +.-valid { + border: tall $success-darken-3; +} + +Horizontal{ + width: 100%; + height: auto; +} +.column { + width: 1fr; +} + +HorizontalScroll { + width: 100%; +} +.feature_subtitle { + color: grey; +} + +Vertical{ + height: auto; +} + +/* Display help messages */ + +.help_box { + background: #333333; + padding: 1 5; + margin: 1 10; + overflow-y: auto; + transition: height 50ms; + display: none; + height: 0; +} +.displayed .help_box { + display: block; + height: 12; +} +#show_help { + display: block; +} +#hide_help { + display: none; +} +.displayed #show_help { + display: none; +} +.displayed #hide_help { + display: block; +} + +/* Show password */ + +#show_password { + display: block; +} +#hide_password { + display: none; +} +.displayed #show_password { + display: none; +} +.displayed #hide_password { + display: block; +} + +/* Logging console */ + +.log_console { + height: auto; +} diff --git a/nf_core/pipelines/create/custompipeline.py b/nf_core/pipelines/create/custompipeline.py new file mode 100644 index 0000000000..6fe878469b --- /dev/null +++ b/nf_core/pipelines/create/custompipeline.py @@ -0,0 +1,98 @@ +from textwrap import dedent + +from textual import on +from textual.app import ComposeResult +from textual.containers import Center, ScrollableContainer +from textual.screen import Screen +from textual.widgets import Button, Footer, Header, Markdown, Switch + +from nf_core.pipelines.create.utils import PipelineFeature, markdown_genomes + +markdown_ci = """ +Nf-core provides a set of Continuous Integration (CI) tests for Github. +When you open a pull request (PR) on your pipeline repository, these tests will run automatically. + +There are different types of tests: +* Linting tests check that your code is formatted correctly and that it adheres to nf-core standards + For code linting they will use [prettier](https://prettier.io/). +* Pipeline tests run your pipeline on a small dataset to check that it works + These tests are run with a small test dataset on GitHub and a larger test dataset on AWS +* Marking old issues as stale +""" + +markdown_badges = """ +The pipeline `README.md` will include badges for: +* AWS CI Tests +* Zenodo DOI +* Nextflow +* Conda +* Docker +* Singularity +* Launching on Nextflow Tower +""" + +markdown_configuration = """ +Nf-core has a repository with a collection of configuration profiles. + +Those config files define a set of parameters which are specific to compute environments at different Institutions. +They can be used within all nf-core pipelines. +If you are likely to be running nf-core pipelines regularly it is a good idea to use or create a custom config file for your organisation. + +For more information about nf-core configuration profiles, see the [nf-core/configs repository](https://github.com/nf-core/configs) +""" + + +class CustomPipeline(Screen): + """Select if the pipeline will use genomic data.""" + + def compose(self) -> ComposeResult: + yield Header() + yield Footer() + yield Markdown( + dedent( + """ + # Template features + """ + ) + ) + yield ScrollableContainer( + PipelineFeature( + markdown_genomes, + "Use reference genomes", + "The pipeline will be configured to use a copy of the most common reference genome files from iGenomes", + "igenomes", + ), + PipelineFeature( + markdown_ci, + "Add Github CI tests", + "The pipeline will include several GitHub actions for Continuous Integration (CI) testing", + "ci", + ), + PipelineFeature( + markdown_badges, + "Add Github badges", + "The README.md file of the pipeline will include GitHub badges", + "github_badges", + ), + PipelineFeature( + markdown_configuration, + "Add configuration files", + "The pipeline will include configuration profiles containing custom parameters requried to run nf-core pipelines at different institutions", + "nf_core_configs", + ), + ) + yield Center( + Button("Back", id="back", variant="default"), + Button("Continue", id="continue", variant="success"), + classes="cta", + ) + + @on(Button.Pressed, "#continue") + def on_button_pressed(self, event: Button.Pressed) -> None: + """Save fields to the config.""" + skip = [] + for feature_input in self.query("PipelineFeature"): + this_switch = feature_input.query_one(Switch) + if not this_switch.value: + skip.append(this_switch.id) + self.parent.TEMPLATE_CONFIG.__dict__.update({"skip_features": skip, "is_nfcore": False}) diff --git a/nf_core/pipelines/create/error.py b/nf_core/pipelines/create/error.py new file mode 100644 index 0000000000..922b5ed544 --- /dev/null +++ b/nf_core/pipelines/create/error.py @@ -0,0 +1,40 @@ +from textwrap import dedent + +from textual.app import ComposeResult +from textual.containers import Center +from textual.screen import Screen +from textual.widgets import Button, Footer, Header, Markdown, Static + + +class ExistError(Screen): + """A screen to show the final text and exit the app - when an error ocurred.""" + + def compose(self) -> ComposeResult: + yield Header() + yield Footer() + yield Markdown( + dedent( + """ + # Pipeline exists + """ + ) + ) + yield Static( + f"\n[green]{' ' * 40},--.[grey39]/[green],-." + + "\n[blue] ___ __ __ __ ___ [green]/,-._.--~\\" + + "\n[blue]|\ | |__ __ / ` / \ |__) |__ [yellow] } {" + + "\n[blue] | \| | \__, \__/ | \ |___ [green]\`-._,-`-," + + "\n[green] `._,._,'\n", + id="logo", + ) + + completed_text_markdown = f""" + A pipeline '`{self.parent.TEMPLATE_CONFIG.outdir + "/" + self.parent.TEMPLATE_CONFIG.org + "-" + self.parent.TEMPLATE_CONFIG.name}`' already exists. + Please select a different name or `force` the creation of the pipeline to override the existing one. + """ + + yield Markdown(dedent(completed_text_markdown)) + yield Center( + Button("Close App", id="close_app", variant="success"), + classes="cta", + ) diff --git a/nf_core/pipelines/create/finaldetails.py b/nf_core/pipelines/create/finaldetails.py new file mode 100644 index 0000000000..5af28cffa6 --- /dev/null +++ b/nf_core/pipelines/create/finaldetails.py @@ -0,0 +1,106 @@ +"""A Textual app to create a pipeline.""" +from textwrap import dedent + +from textual import on, work +from textual.app import ComposeResult +from textual.containers import Center, Horizontal, Vertical +from textual.message import Message +from textual.screen import Screen +from textual.widgets import Button, Footer, Header, Input, Markdown, Static, Switch + +from nf_core.pipelines.create.create import PipelineCreate +from nf_core.pipelines.create.loggingscreen import LoggingScreen +from nf_core.pipelines.create.utils import ShowLogs, TextInput, change_select_disabled + + +class FinalDetails(Screen): + """Name, description, author, etc.""" + + def compose(self) -> ComposeResult: + yield Header() + yield Footer() + yield Markdown( + dedent( + """ + # Final details + """ + ) + ) + + with Horizontal(): + yield TextInput( + "version", + "Version", + "First version of the pipeline", + "1.0.0dev", + classes="column", + ) + yield TextInput( + "outdir", + "Output directory", + "Path to the output directory where the pipeline will be created", + ".", + classes="column", + ) + with Horizontal(): + yield Switch(value=False, id="force") + with Vertical(): + yield Static("Force", classes="custom_grid") + yield Static( + "If the pipeline output directory exists, remove it and continue.", + classes="feature_subtitle", + ) + + yield Center( + Button("Back", id="back", variant="default"), + Button("Finish", id="finish", variant="success"), + classes="cta", + ) + + @on(Button.Pressed, "#finish") + def on_button_pressed(self, event: Button.Pressed) -> None: + """Save fields to the config.""" + new_config = {} + for text_input in self.query("TextInput"): + this_input = text_input.query_one(Input) + validation_result = this_input.validate(this_input.value) + new_config[text_input.field_id] = this_input.value + if not validation_result.is_valid: + text_input.query_one(".validation_msg").update("\n".join(validation_result.failure_descriptions)) + else: + text_input.query_one(".validation_msg").update("") + try: + self.parent.TEMPLATE_CONFIG.__dict__.update(new_config) + except ValueError: + pass + + this_switch = self.query_one(Switch) + try: + self.parent.TEMPLATE_CONFIG.__dict__.update({"force": this_switch.value}) + except ValueError: + pass + + # Create the new pipeline + self._create_pipeline() + self.parent.LOGGING_STATE = "pipeline created" + self.parent.switch_screen(LoggingScreen()) + + class PipelineExists(Message): + """Custom message to indicate that the pipeline already exists.""" + + pass + + @on(PipelineExists) + def show_pipeline_error(self) -> None: + self.parent.switch_screen("error_screen") + + @work(thread=True, exclusive=True) + def _create_pipeline(self) -> None: + """Create the pipeline.""" + self.post_message(ShowLogs()) + create_obj = PipelineCreate(template_config=self.parent.TEMPLATE_CONFIG) + try: + create_obj.init_pipeline() + self.parent.call_from_thread(change_select_disabled, self.parent, "close_screen", False) + except UserWarning: + self.post_message(self.PipelineExists()) diff --git a/nf_core/pipelines/create/githubexit.py b/nf_core/pipelines/create/githubexit.py new file mode 100644 index 0000000000..9b2c54912e --- /dev/null +++ b/nf_core/pipelines/create/githubexit.py @@ -0,0 +1,50 @@ +from textwrap import dedent + +from textual.app import ComposeResult +from textual.containers import Center +from textual.screen import Screen +from textual.widgets import Button, Footer, Header, Markdown, Static + +exit_help_text_markdown = """ +If you would like to create the GitHub repository later, you can do it manually by following these steps: + +1. Create a new GitHub repository +2. Add the remote to your local repository +```bash +cd +git remote add origin git@github.com:/.git +``` +3. Push the code to the remote +```bash +git push --all origin +``` +""" + + +class GithubExit(Screen): + """A screen to show a help text when a GitHub repo is NOT created.""" + + def compose(self) -> ComposeResult: + yield Header() + yield Footer() + yield Markdown( + dedent( + """ + # HowTo create a GitHub repository + """ + ) + ) + yield Static( + f"\n[green]{' ' * 40},--.[grey39]/[green],-." + + "\n[blue] ___ __ __ __ ___ [green]/,-._.--~\\" + + "\n[blue]|\ | |__ __ / ` / \ |__) |__ [yellow] } {" + + "\n[blue] | \| | \__, \__/ | \ |___ [green]\`-._,-`-," + + "\n[green] `._,._,'\n", + id="logo", + ) + yield Markdown(exit_help_text_markdown) + yield Center( + Button("Close App", id="close_app", variant="success"), + Button("Show Logging", id="show_logging", variant="primary"), + classes="cta", + ) diff --git a/nf_core/pipelines/create/githubrepo.py b/nf_core/pipelines/create/githubrepo.py new file mode 100644 index 0000000000..c8a02e609a --- /dev/null +++ b/nf_core/pipelines/create/githubrepo.py @@ -0,0 +1,223 @@ +import logging +import os +from pathlib import Path +from textwrap import dedent + +import git +import yaml +from github import Github, GithubException, UnknownObjectException +from textual import work +from textual.app import ComposeResult +from textual.containers import Center, Horizontal, Vertical +from textual.screen import Screen +from textual.widgets import Button, Footer, Header, Input, Markdown, Static, Switch + +from nf_core.pipelines.create.loggingscreen import LoggingScreen +from nf_core.pipelines.create.utils import ShowLogs, TextInput + +log = logging.getLogger(__name__) + +github_text_markdown = """ +# Create a GitHub repo + +After creating the pipeline template locally, we can create a GitHub repository and push the code to it. +""" +repo_config_markdown = """ +Please select the the GitHub repository settings: +""" + + +class GithubRepo(Screen): + """Create a GitHub repository and push all branches.""" + + def compose(self) -> ComposeResult: + yield Header() + yield Footer() + yield Markdown( + dedent( + """ + # Create GitHub repository + """ + ) + ) + yield Markdown(dedent(github_text_markdown)) + with Horizontal(): + gh_user, gh_token = self._get_github_credentials() + yield TextInput( + "gh_username", + "GitHub username", + "Your GitHub username", + default=gh_user[0] if gh_user is not None else "GitHub username", + classes="column", + ) + yield TextInput( + "token", + "GitHub token", + "Your GitHub personal access token for login.", + default=gh_token if gh_token is not None else "GitHub token", + password=True, + classes="column", + ) + yield Button("Show", id="show_password") + yield Button("Hide", id="hide_password") + yield Markdown(dedent(repo_config_markdown)) + with Horizontal(): + yield Switch(value=False, id="private") + with Vertical(): + yield Static("Private", classes="") + yield Static("Select if the new GitHub repo must be private.", classes="feature_subtitle") + with Horizontal(): + yield Switch(value=True, id="push") + with Vertical(): + yield Static("Push files", classes="custom_grid") + yield Static( + "Select if you would like to push all the pipeline template files to your GitHub repo\nand all the branches required to keep the pipeline up to date with new releases of nf-core.", + classes="feature_subtitle", + ) + yield Center( + Button("Back", id="back", variant="default"), + Button("Create GitHub repo", id="create_github", variant="success"), + Button("Finish without creating a repo", id="exit", variant="primary"), + classes="cta", + ) + + def on_button_pressed(self, event: Button.Pressed) -> None: + """Create a GitHub repo or show help message and exit""" + if event.button.id == "show_password": + self.add_class("displayed") + text_input = self.query_one("#token", TextInput) + text_input.query_one(Input).password = False + elif event.button.id == "hide_password": + self.remove_class("displayed") + text_input = self.query_one("#token", TextInput) + text_input.query_one(Input).password = True + elif event.button.id == "create_github": + # Create a GitHub repo + + # Save GitHub username and token + github_variables = {} + for text_input in self.query("TextInput"): + this_input = text_input.query_one(Input) + github_variables[text_input.field_id] = this_input.value + # Save GitHub repo config + for switch_input in self.query("Switch"): + github_variables[switch_input.id] = switch_input.value + + # Pipeline git repo + pipeline_repo = git.Repo.init( + Path(self.parent.TEMPLATE_CONFIG.outdir) + / Path(self.parent.TEMPLATE_CONFIG.org + "-" + self.parent.TEMPLATE_CONFIG.name) + ) + + # GitHub authentication + if github_variables["token"]: + github_auth = self._github_authentication(github_variables["gh_username"], github_variables["token"]) + else: + raise UserWarning( + f"Could not authenticate to GitHub with user name '{github_variables['gh_username']}'." + "Please provide an authentication token or set the environment variable 'GITHUB_AUTH_TOKEN'." + ) + + user = github_auth.get_user() + org = None + # Make sure that the authentication was successful + try: + user.login + log.debug("GitHub authentication successful") + except GithubException: + raise UserWarning( + f"Could not authenticate to GitHub with user name '{github_variables['gh_username']}'." + "Please make sure that the provided user name and token are correct." + ) + + # Check if organisation exists + # If the organisation is nf-core or it doesn't exist, the repo will be created in the user account + if self.parent.TEMPLATE_CONFIG.org != "nf-core": + try: + org = github_auth.get_organization(self.parent.TEMPLATE_CONFIG.org) + log.info( + f"Repo will be created in the GitHub organisation account '{self.parent.TEMPLATE_CONFIG.org}'" + ) + except UnknownObjectException: + pass + + # Create the repo + try: + if org: + self._create_repo_and_push( + org, pipeline_repo, github_variables["private"], github_variables["push"] + ) + else: + # Create the repo in the user's account + log.info( + f"Repo will be created in the GitHub organisation account '{github_variables['gh_username']}'" + ) + self._create_repo_and_push( + user, pipeline_repo, github_variables["private"], github_variables["push"] + ) + log.info(f"GitHub repository '{self.parent.TEMPLATE_CONFIG.name}' created successfully") + except UserWarning as e: + log.info(f"There was an error with message: {e}") + self.parent.switch_screen("github_exit") + + self.parent.LOGGING_STATE = "repo created" + self.parent.switch_screen(LoggingScreen()) + + @work(thread=True, exclusive=True) + def _create_repo_and_push(self, org, pipeline_repo, private, push): + """Create a GitHub repository and push all branches.""" + self.post_message(ShowLogs()) + # Check if repo already exists + try: + repo = org.get_repo(self.parent.TEMPLATE_CONFIG.name) + # Check if it has a commit history + try: + repo.get_commits().totalCount + raise UserWarning(f"GitHub repository '{self.parent.TEMPLATE_CONFIG.name}' already exists") + except GithubException: + # Repo is empty + repo_exists = True + except UserWarning as e: + # Repo already exists + log.info(e) + return + except UnknownObjectException: + # Repo doesn't exist + repo_exists = False + + # Create the repo + if not repo_exists: + repo = org.create_repo( + self.parent.TEMPLATE_CONFIG.name, description=self.parent.TEMPLATE_CONFIG.description, private=private + ) + + # Add the remote and push + try: + pipeline_repo.create_remote("origin", repo.clone_url) + except git.exc.GitCommandError: + # Remote already exists + pass + if push: + pipeline_repo.remotes.origin.push(all=True).raise_if_error() + + def _github_authentication(self, gh_username, gh_token): + """Authenticate to GitHub""" + log.debug(f"Authenticating GitHub as {gh_username}") + github_auth = Github(gh_username, gh_token) + return github_auth + + def _get_github_credentials(self): + """Get GitHub credentials""" + gh_user = None + gh_token = None + # Use gh CLI config if installed + gh_cli_config_fn = os.path.expanduser("~/.config/gh/hosts.yml") + if os.path.exists(gh_cli_config_fn): + with open(gh_cli_config_fn) as fh: + gh_cli_config = yaml.safe_load(fh) + gh_user = (gh_cli_config["github.com"]["user"],) + gh_token = gh_cli_config["github.com"]["oauth_token"] + # If gh CLI not installed, try to get credentials from environment variables + elif os.environ.get("GITHUB_TOKEN") is not None: + gh_token = self.auth = os.environ["GITHUB_TOKEN"] + return (gh_user, gh_token) diff --git a/nf_core/pipelines/create/githubrepoquestion.py b/nf_core/pipelines/create/githubrepoquestion.py new file mode 100644 index 0000000000..ded33d188a --- /dev/null +++ b/nf_core/pipelines/create/githubrepoquestion.py @@ -0,0 +1,36 @@ +import logging +from textwrap import dedent + +from textual.app import ComposeResult +from textual.containers import Center +from textual.screen import Screen +from textual.widgets import Button, Footer, Header, Markdown + +log = logging.getLogger(__name__) + +github_text_markdown = """ +After creating the pipeline template locally, we can create a GitHub repository and push the code to it. + +Do you want to create a GitHub repository? +""" + + +class GithubRepoQuestion(Screen): + """Ask if the user wants to create a GitHub repository.""" + + def compose(self) -> ComposeResult: + yield Header() + yield Footer() + yield Markdown( + dedent( + """ + # Create GitHub repository + """ + ) + ) + yield Markdown(dedent(github_text_markdown)) + yield Center( + Button("Create GitHub repo", id="github_repo", variant="success"), + Button("Finish without creating a repo", id="exit", variant="primary"), + classes="cta", + ) diff --git a/nf_core/pipelines/create/loggingscreen.py b/nf_core/pipelines/create/loggingscreen.py new file mode 100644 index 0000000000..68b65619c5 --- /dev/null +++ b/nf_core/pipelines/create/loggingscreen.py @@ -0,0 +1,45 @@ +from textwrap import dedent + +from textual.app import ComposeResult +from textual.containers import Center +from textual.screen import Screen +from textual.widgets import Button, Footer, Header, Markdown, Static + +markdown = """ +Visualising logging output. +""" + + +class LoggingScreen(Screen): + """A screen to show the final logs.""" + + def compose(self) -> ComposeResult: + yield Header() + yield Footer() + yield Markdown( + dedent( + """ + # Logging + """ + ) + ) + yield Static( + f"\n[green]{' ' * 40},--.[grey39]/[green],-." + + "\n[blue] ___ __ __ __ ___ [green]/,-._.--~\\" + + "\n[blue]|\ | |__ __ / ` / \ |__) |__ [yellow] } {" + + "\n[blue] | \| | \__, \__/ | \ |___ [green]\`-._,-`-," + + "\n[green] `._,._,'\n", + id="logo", + ) + yield Markdown(markdown) + yield Center(self.parent.LOG_HANDLER.console) + if self.parent.LOGGING_STATE == "repo created": + yield Center( + Button("Close App", id="close_app", variant="success"), + classes="cta", + ) + else: + yield Center( + Button("Continue", id="close_screen", variant="success", disabled=True), + classes="cta", + ) diff --git a/nf_core/pipelines/create/nfcorepipeline.py b/nf_core/pipelines/create/nfcorepipeline.py new file mode 100644 index 0000000000..8306e93263 --- /dev/null +++ b/nf_core/pipelines/create/nfcorepipeline.py @@ -0,0 +1,47 @@ +from textwrap import dedent + +from textual import on +from textual.app import ComposeResult +from textual.containers import Center, ScrollableContainer +from textual.screen import Screen +from textual.widgets import Button, Footer, Header, Markdown, Switch + +from nf_core.pipelines.create.utils import PipelineFeature, markdown_genomes + + +class NfcorePipeline(Screen): + """Select if the pipeline will use genomic data.""" + + def compose(self) -> ComposeResult: + yield Header() + yield Footer() + yield Markdown( + dedent( + """ + # Template features + """ + ) + ) + yield ScrollableContainer( + PipelineFeature( + markdown_genomes, + "Use reference genomes", + "The pipeline will be configured to use a copy of the most common reference genome files from iGenomes", + "igenomes", + ), + ) + yield Center( + Button("Back", id="back", variant="default"), + Button("Continue", id="continue", variant="success"), + classes="cta", + ) + + @on(Button.Pressed, "#continue") + def on_button_pressed(self, event: Button.Pressed) -> None: + """Save fields to the config.""" + skip = [] + for feature_input in self.query("PipelineFeature"): + this_switch = feature_input.query_one(Switch) + if not this_switch.value: + skip.append(this_switch.id) + self.parent.TEMPLATE_CONFIG.__dict__.update({"skip_features": skip, "is_nfcore": True}) diff --git a/nf_core/pipelines/create/pipelinetype.py b/nf_core/pipelines/create/pipelinetype.py new file mode 100644 index 0000000000..98d5acc97a --- /dev/null +++ b/nf_core/pipelines/create/pipelinetype.py @@ -0,0 +1,49 @@ +from textual.app import ComposeResult +from textual.containers import Center +from textual.screen import Screen +from textual.widgets import Button, Footer, Header, Markdown + +markdown_intro = """ +# To nf-core or not to nf-core? + +Next, we need to know what kind of pipeline this will be. + +Choose _"nf-core"_ if: + +* You want your pipeline to be part of the nf-core community +* You think that there's an outside chance that it ever _could_ be part of nf-core + +Choose _"Custom"_ if: + +* Your pipeline will _never_ be part of nf-core +* You want full control over *all* features that are included from the template + (including those that are mandatory for nf-core). +""" + +markdown_details = """ +## Not sure? What's the difference? + +Choosing _"nf-core"_ effectively pre-selects the following template features: + +* GitHub Actions Continuous Integration (CI) configuration for the following: + * Small-scale (GitHub) and large-scale (AWS) tests + * Code format linting with prettier + * Auto-fix functionality using @nf-core-bot + * Marking old issues as stale +* Inclusion of shared nf-core config profiles +""" + + +class ChoosePipelineType(Screen): + """Choose whether this will be an nf-core pipeline or not.""" + + def compose(self) -> ComposeResult: + yield Header() + yield Footer() + yield Markdown(markdown_intro) + yield Center( + Button("nf-core", id="type_nfcore", variant="success"), + Button("Custom", id="type_custom", variant="primary"), + classes="cta", + ) + yield Markdown(markdown_details) diff --git a/nf_core/pipelines/create/utils.py b/nf_core/pipelines/create/utils.py new file mode 100644 index 0000000000..a1c9089523 --- /dev/null +++ b/nf_core/pipelines/create/utils.py @@ -0,0 +1,221 @@ +import re +from logging import LogRecord +from pathlib import Path +from typing import Optional, Union + +from pydantic import BaseModel, ConfigDict, ValidationError, field_validator +from rich.logging import RichHandler +from textual import on +from textual._context import active_app +from textual.app import ComposeResult +from textual.containers import HorizontalScroll +from textual.message import Message +from textual.validation import ValidationResult, Validator +from textual.widget import Widget +from textual.widgets import Button, Input, Markdown, RichLog, Static, Switch + + +class CreateConfig(BaseModel): + """Pydantic model for the nf-core create config.""" + + org: Optional[str] = None + name: Optional[str] = None + description: Optional[str] = None + author: Optional[str] = None + version: Optional[str] = None + force: Optional[bool] = None + outdir: Optional[str] = None + skip_features: Optional[list] = None + is_nfcore: Optional[bool] = None + + model_config = ConfigDict(extra="allow") + + @field_validator("name") + @classmethod + def name_nospecialchars(cls, v: str) -> str: + """Check that the pipeline name is simple.""" + if not re.match(r"^[a-z]+$", v): + raise ValueError("Must be lowercase without punctuation.") + return v + + @field_validator("org", "description", "author", "version", "outdir") + @classmethod + def notempty(cls, v: str) -> str: + """Check that string values are not empty.""" + if v.strip() == "": + raise ValueError("Cannot be left empty.") + return v + + @field_validator("version") + @classmethod + def version_nospecialchars(cls, v: str) -> str: + """Check that the pipeline version is simple.""" + if not re.match(r"^([0-9]+)(\.?([0-9]+))*(dev)?$", v): + raise ValueError( + "Must contain at least one number, and can be prefixed by 'dev'. Do not use a 'v' prefix or spaces." + ) + return v + + @field_validator("outdir") + @classmethod + def path_valid(cls, v: str) -> str: + """Check that a path is valid.""" + if not Path(v).is_dir(): + raise ValueError("Must be a valid path.") + return v + + +class TextInput(Static): + """Widget for text inputs. + + Provides standard interface for a text input with help text + and validation messages. + """ + + def __init__(self, field_id, placeholder, description, default=None, password=None, **kwargs) -> None: + """Initialise the widget with our values. + + Pass on kwargs upstream for standard usage.""" + super().__init__(**kwargs) + self.field_id: str = field_id + self.id: str = field_id + self.placeholder: str = placeholder + self.description: str = description + self.default: str = default + self.password: bool = password + + def compose(self) -> ComposeResult: + yield Static(self.description, classes="field_help") + yield Input( + placeholder=self.placeholder, + validators=[ValidateConfig(self.field_id)], + value=self.default, + password=self.password, + ) + yield Static(classes="validation_msg") + + @on(Input.Changed) + @on(Input.Submitted) + def show_invalid_reasons(self, event: Union[Input.Changed, Input.Submitted]) -> None: + """Validate the text input and show errors if invalid.""" + if not event.validation_result.is_valid: + self.query_one(".validation_msg").update("\n".join(event.validation_result.failure_descriptions)) + else: + self.query_one(".validation_msg").update("") + + +class ValidateConfig(Validator): + """Validate any config value, using Pydantic.""" + + def __init__(self, key) -> None: + """Initialise the validator with the model key to validate.""" + super().__init__() + self.key = key + + def validate(self, value: str) -> ValidationResult: + """Try creating a Pydantic object with this key set to this value. + + If it fails, return the error messages.""" + try: + CreateConfig(**{f"{self.key}": value}) + return self.success() + except ValidationError as e: + return self.failure(", ".join([err["msg"] for err in e.errors()])) + + +class HelpText(Markdown): + """A class to show a text box with help text.""" + + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + + def show(self) -> None: + """Method to show the help text box.""" + self.add_class("displayed") + + def hide(self) -> None: + """Method to hide the help text box.""" + self.remove_class("displayed") + + +class PipelineFeature(Static): + """Widget for the selection of pipeline features.""" + + def __init__(self, markdown: str, title: str, subtitle: str, field_id: str, **kwargs) -> None: + super().__init__(**kwargs) + self.markdown = markdown + self.title = title + self.subtitle = subtitle + self.field_id = field_id + + def on_button_pressed(self, event: Button.Pressed) -> None: + """When the button is pressed, change the type of the button.""" + if event.button.id == "show_help": + self.add_class("displayed") + elif event.button.id == "hide_help": + self.remove_class("displayed") + + def compose(self) -> ComposeResult: + """ + Create child widgets. + + Displayed row with a switch, a short text description and a help button. + Hidden row with a help text box. + """ + yield HorizontalScroll( + Switch(value=True, id=self.field_id), + Static(self.title, classes="feature_title"), + Static(self.subtitle, classes="feature_subtitle"), + Button("Show help", id="show_help", variant="primary"), + Button("Hide help", id="hide_help"), + classes="custom_grid", + ) + yield HelpText(markdown=self.markdown, classes="help_box") + + +class LoggingConsole(RichLog): + file = False + console: Widget + + def print(self, content): + self.write(content) + + +class CustomLogHandler(RichHandler): + """A Logging handler which extends RichHandler to write to a Widget and handle a Textual App.""" + + def emit(self, record: LogRecord) -> None: + """Invoked by logging.""" + try: + _app = active_app.get() + except LookupError: + pass + else: + super().emit(record) + + +class ShowLogs(Message): + """Custom message to show the logging messages.""" + + pass + + +## Functions +def change_select_disabled(app, widget_id: str, disabled: bool) -> None: + """Change the disabled state of a widget.""" + app.get_widget_by_id(widget_id).disabled = disabled + + +## Markdown text to reuse in different screens +markdown_genomes = """ +Nf-core pipelines are configured to use a copy of the most common reference genome files. + +By selecting this option, your pipeline will include a configuration file specifying the paths to these files. + +The required code to use these files will also be included in the template. +When the pipeline user provides an appropriate genome key, +the pipeline will automatically download the required reference files. + +For more information about reference genomes in nf-core pipelines, +see the [nf-core docs](https://nf-co.re/docs/usage/reference_genomes). +""" diff --git a/nf_core/pipelines/create/welcome.py b/nf_core/pipelines/create/welcome.py new file mode 100644 index 0000000000..cb0d7468cb --- /dev/null +++ b/nf_core/pipelines/create/welcome.py @@ -0,0 +1,53 @@ +from textwrap import dedent + +from textual.app import ComposeResult +from textual.containers import Center +from textual.screen import Screen +from textual.widgets import Button, Footer, Header, Markdown, Static + +markdown = """ +# nf-core create + +This app will help you create a new nf-core pipeline. +It uses the nf-core pipeline template, which is kept +within the [nf-core/tools repository](https://github.com/nf-core/tools). + +Using this tool is mandatory when making a pipeline that may +be part of the nf-core community collection at some point. +However, this tool can also be used to create pipelines that will +never be part of nf-core. You can still benefit from the community +best practices for your own workflow. + +If you are planning to add a pipeline to the nf-core community, you need to be part of that community! +Please join us on Slack [https://nf-co.re/join](https://nf-co.re/join), +and ask to be added to the GitHub association through the #github-invitations channel. + +Come and discuss your plans with the nf-core community as early as possible. +Ideally before you make a start on your pipeline! +These topics are specifically discussed in the [#new-pipelines](https://nfcore.slack.com/channels/new-pipelines) channel. +""" + + +class WelcomeScreen(Screen): + """A welcome screen for the app.""" + + def compose(self) -> ComposeResult: + yield Header() + yield Footer() + yield Markdown( + dedent( + """ + # Create a pipeline from the nf-core template + """ + ) + ) + yield Static( + f"\n[green]{' ' * 40},--.[grey39]/[green],-." + + "\n[blue] ___ __ __ __ ___ [green]/,-._.--~\\" + + "\n[blue]|\ | |__ __ / ` / \ |__) |__ [yellow] } {" + + "\n[blue] | \| | \__, \__/ | \ |___ [green]\`-._,-`-," + + "\n[green] `._,._,'\n", + id="logo", + ) + yield Markdown(markdown) + yield Center(Button("Let's go!", id="start", variant="success"), classes="cta") diff --git a/nf_core/schema.py b/nf_core/schema.py index df04dc5a1e..373f8bbaa1 100644 --- a/nf_core/schema.py +++ b/nf_core/schema.py @@ -1,5 +1,4 @@ -""" Code to deal with pipeline JSON Schema """ - +"""Code to deal with pipeline JSON Schema""" import copy import json @@ -84,16 +83,13 @@ def load_lint_schema(self): self.get_schema_defaults() self.validate_default_params() if len(self.invalid_nextflow_config_default_parameters) > 0: + params = "\n --".join( + [f"{param}: {msg}" for param, msg in self.invalid_nextflow_config_default_parameters.items()] + ) log.info( - "[red][✗] Invalid default parameters found:\n --{}\n\nNOTE: Use null in config for no default.".format( - "\n --".join( - [ - f"{param}: {msg}" - for param, msg in self.invalid_nextflow_config_default_parameters.items() - ] - ) - ) + f"[red][✗] Invalid default parameters found:\n {params} \n\nNOTE: Use null in config for no default." ) + else: log.info(f"[green][✓] Pipeline schema looks valid[/] [dim](found {num_params} params)") except json.decoder.JSONDecodeError as e: @@ -282,9 +278,9 @@ def validate_default_params(self): if param in self.pipeline_params: self.validate_config_default_parameter(param, group_properties[param], self.pipeline_params[param]) else: - self.invalid_nextflow_config_default_parameters[ - param - ] = "Not in pipeline parameters. Check `nextflow.config`." + self.invalid_nextflow_config_default_parameters[param] = ( + "Not in pipeline parameters. Check `nextflow.config`." + ) # Go over ungrouped params if any exist ungrouped_properties = self.schema.get("properties") @@ -297,9 +293,9 @@ def validate_default_params(self): param, ungrouped_properties[param], self.pipeline_params[param] ) else: - self.invalid_nextflow_config_default_parameters[ - param - ] = "Not in pipeline parameters. Check `nextflow.config`." + self.invalid_nextflow_config_default_parameters[param] = ( + "Not in pipeline parameters. Check `nextflow.config`." + ) def validate_config_default_parameter(self, param, schema_param, config_default): """ @@ -314,9 +310,9 @@ def validate_config_default_parameter(self, param, schema_param, config_default) ): # Check that we are not deferring the execution of this parameter in the schema default with squiggly brakcets if schema_param["type"] != "string" or "{" not in schema_param["default"]: - self.invalid_nextflow_config_default_parameters[ - param - ] = f"Schema default (`{schema_param['default']}`) does not match the config default (`{config_default}`)" + self.invalid_nextflow_config_default_parameters[param] = ( + f"Schema default (`{schema_param['default']}`) does not match the config default (`{config_default}`)" + ) return # if default is null, we're good @@ -326,28 +322,28 @@ def validate_config_default_parameter(self, param, schema_param, config_default) # Check variable types in nextflow.config if schema_param["type"] == "string": if str(config_default) in ["false", "true", "''"]: - self.invalid_nextflow_config_default_parameters[ - param - ] = f"String should not be set to `{config_default}`" + self.invalid_nextflow_config_default_parameters[param] = ( + f"String should not be set to `{config_default}`" + ) if schema_param["type"] == "boolean": if str(config_default) not in ["false", "true"]: - self.invalid_nextflow_config_default_parameters[ - param - ] = f"Booleans should only be true or false, not `{config_default}`" + self.invalid_nextflow_config_default_parameters[param] = ( + f"Booleans should only be true or false, not `{config_default}`" + ) if schema_param["type"] == "integer": try: int(config_default) except ValueError: - self.invalid_nextflow_config_default_parameters[ - param - ] = f"Does not look like an integer: `{config_default}`" + self.invalid_nextflow_config_default_parameters[param] = ( + f"Does not look like an integer: `{config_default}`" + ) if schema_param["type"] == "number": try: float(config_default) except ValueError: - self.invalid_nextflow_config_default_parameters[ - param - ] = f"Does not look like a number (float): `{config_default}`" + self.invalid_nextflow_config_default_parameters[param] = ( + f"Does not look like a number (float): `{config_default}`" + ) def validate_schema(self, schema=None): """ @@ -647,17 +643,13 @@ def build_schema(self, pipeline_dir, no_prompts, web_only, url): # Extra help for people running offline if "Could not connect" in e.args[0]: log.info( - "If you're working offline, now copy your schema ({}) and paste at https://nf-co.re/pipeline_schema_builder".format( - self.schema_filename - ) + f"If you're working offline, now copy your schema ({self.schema_filename}) and paste at https://nf-co.re/pipeline_schema_builder" ) log.info("When you're finished, you can paste the edited schema back into the same file") if self.web_schema_build_web_url: log.info( "To save your work, open {}\n" - "Click the blue 'Finished' button, copy the schema and paste into this file: {}".format( - self.web_schema_build_web_url, self.schema_filename - ) + f"Click the blue 'Finished' button, copy the schema and paste into this file: { self.web_schema_build_web_url, self.schema_filename}" ) return False diff --git a/nf_core/subworkflows/lint/__init__.py b/nf_core/subworkflows/lint/__init__.py index 3a87190422..96d2e0ab92 100644 --- a/nf_core/subworkflows/lint/__init__.py +++ b/nf_core/subworkflows/lint/__init__.py @@ -6,7 +6,6 @@ nf-core subworkflows lint """ - import logging import os diff --git a/nf_core/subworkflows/lint/subworkflow_changes.py b/nf_core/subworkflows/lint/subworkflow_changes.py index b7fa13d931..a9c9616a21 100644 --- a/nf_core/subworkflows/lint/subworkflow_changes.py +++ b/nf_core/subworkflows/lint/subworkflow_changes.py @@ -1,6 +1,7 @@ """ Check whether the content of a subworkflow has changed compared to the original repository """ + from pathlib import Path import nf_core.modules.modules_repo diff --git a/nf_core/subworkflows/lint/subworkflow_tests.py b/nf_core/subworkflows/lint/subworkflow_tests.py index f7284320ea..796a56d018 100644 --- a/nf_core/subworkflows/lint/subworkflow_tests.py +++ b/nf_core/subworkflows/lint/subworkflow_tests.py @@ -1,6 +1,7 @@ """ Lint the tests of a subworkflow in nf-core/modules """ + import json import logging from pathlib import Path diff --git a/nf_core/sync.py b/nf_core/sync.py index 995baeacd2..d1dee9a545 100644 --- a/nf_core/sync.py +++ b/nf_core/sync.py @@ -1,5 +1,4 @@ -"""Synchronise a pipeline TEMPLATE branch with the template. -""" +"""Synchronise a pipeline TEMPLATE branch with the template.""" import json import logging @@ -16,8 +15,8 @@ from git import GitCommandError, InvalidGitRepositoryError import nf_core -import nf_core.create import nf_core.list +import nf_core.pipelines.create.create import nf_core.utils log = logging.getLogger(__name__) @@ -251,7 +250,7 @@ def make_template_pipeline(self): log.info("Making a new template pipeline using pipeline variables") # Only show error messages from pipeline creation - logging.getLogger("nf_core.create").setLevel(logging.ERROR) + logging.getLogger("nf_core.pipelines.create").setLevel(logging.ERROR) # Re-write the template yaml info from .nf-core.yml config if "template" in self.config_yml: @@ -259,7 +258,7 @@ def make_template_pipeline(self): yaml.safe_dump(self.config_yml, config_path) try: - nf_core.create.PipelineCreate( + nf_core.pipelines.create.create.PipelineCreate( name=self.wf_config["manifest.name"].strip('"').strip("'"), description=self.wf_config["manifest.description"].strip('"').strip("'"), version=self.wf_config["manifest.version"].strip('"').strip("'"), @@ -267,7 +266,6 @@ def make_template_pipeline(self): force=True, outdir=self.pipeline_dir, author=self.wf_config["manifest.author"].strip('"').strip("'"), - plain=True, ).init_pipeline() except Exception as err: # Reset to where you were to prevent git getting messed up. diff --git a/nf_core/utils.py b/nf_core/utils.py index e1778b55b3..8ea6f418aa 100644 --- a/nf_core/utils.py +++ b/nf_core/utils.py @@ -1,6 +1,7 @@ """ Common utility functions for the nf-core python package. """ + import concurrent.futures import datetime import errno diff --git a/requirements-dev.txt b/requirements-dev.txt index 9fbb49c10c..fa98655aed 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -6,8 +6,14 @@ responses ruff Sphinx sphinx-rtd-theme +textual-dev>=1.2.1 +mypy +types-PyYAML +types-requests types-jsonschema types-Markdown types-PyYAML types-requests types-setuptools +pytest-textual-snapshot +ruff diff --git a/requirements.txt b/requirements.txt index e4319d2352..8f5132d980 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ click filetype GitPython +PyGithub jinja2 jsonschema>=3.0 markdown>=3.3 @@ -9,6 +10,7 @@ pillow pdiff pre-commit prompt_toolkit>=3.0.3 +pydantic>=2.2.1 pytest-workflow>=1.6.0 pytest>=7.0.0 pyyaml @@ -19,4 +21,6 @@ requests_cache rich-click>=1.6.1 rich>=13.3.1 tabulate +textual>=0.47.1 trogon +pdiff diff --git a/tests/__snapshots__/test_create_app.ambr b/tests/__snapshots__/test_create_app.ambr new file mode 100644 index 0000000000..b0a306adc2 --- /dev/null +++ b/tests/__snapshots__/test_create_app.ambr @@ -0,0 +1,3216 @@ +# serializer version: 1 +# name: test_basic_details_custom + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + nf-core create + + + + + + + + + + nf-core create — Create a new pipeline with the nf-core pipeline template + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + Basic details + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + + + GitHub organisationWorkflow name + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + nf-corePipeline Name + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + A short description of your pipeline. + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Description + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + Name of the main author / authors + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Author(s) + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + BackNext + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + + + + + + + + +  D  Toggle dark mode  Q  Quit  + + + + + ''' +# --- +# name: test_basic_details_nfcore + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + nf-core create + + + + + + + + + + nf-core create — Create a new pipeline with the nf-core pipeline template + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + Basic details + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + + + GitHub organisationWorkflow name + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + nf-corePipeline Name + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + A short description of your pipeline. + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Description + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + Name of the main author / authors + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Author(s) + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + BackNext + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + + + + + + + + +  D  Toggle dark mode  Q  Quit  + + + + + ''' +# --- +# name: test_choose_type + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + nf-core create + + + + + + + + + + nf-core create — Create a new pipeline with the nf-core pipeline template + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + To nf-core or not to nf-core? + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Next, we need to know what kind of pipeline this will be. + + Choose "nf-core" if: + + ● You want your pipeline to be part of the nf-core community + ● You think that there's an outside chance that it ever could be part of nf-core + + Choose "Custom" if: + + ● Your pipeline will never be part of nf-core + ● You want full control over all features that are included from the template (including  + those that are mandatory for nf-core). + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + nf-coreCustom + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +                             Not sure? What's the difference?                             + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Choosing "nf-core" effectively pre-selects the following template features: + + ● GitHub Actions Continuous Integration (CI) configuration for the following: + ▪ Small-scale (GitHub) and large-scale (AWS) tests + ▪ Code format linting with prettier + ▪ Auto-fix functionality using @nf-core-bot + ▪ Marking old issues as stale + ● Inclusion of shared nf-core config profiles + + + + + + + + + + + + +  D  Toggle dark mode  Q  Quit  + + + + + ''' +# --- +# name: test_customisation_help + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + nf-core create + + + + + + + + + + nf-core create — Create a new pipeline with the nf-core pipeline template + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + Template features + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Use reference The pipeline will beHide help + ▁▁▁▁▁▁▁▁genomesconfigured to use a ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + copy of the most  + common reference  + genome files from  + iGenomes + + + + Nf-core pipelines are configured to use a copy of the most + common reference genome files. + + By selecting this option, your pipeline will include a  + configuration file specifying the paths to these files. + + The required code to use these files will also be included + in the template. When the pipeline user provides an ▆▆ + appropriate genome key, the pipeline will automatically  + download the required reference files. + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Add Github CI testsThe pipeline will Show help + ▁▁▁▁▁▁▁▁include several ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + GitHub actions for  + Continuous  + Integration (CI)  + testing + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▂▂ + Add Github badgesThe README.md file Show help + ▁▁▁▁▁▁▁▁of the pipeline will▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + include GitHub  + badges + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Add configuration The pipeline will Show help + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + BackContinue + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +  D  Toggle dark mode  Q  Quit  + + + + + ''' +# --- +# name: test_final_details + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + nf-core create + + + + + + + + + + nf-core create — Create a new pipeline with the nf-core pipeline template + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + Final details + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + + + First version of the pipelinePath to the output directory where the pipeline  + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔will be created + 1.0.0dev▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁. + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + ▔▔▔▔▔▔▔▔Force + If the pipeline output directory exists, remove it and continue. + ▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + BackFinish + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +  D  Toggle dark mode  Q  Quit  + + + + + ''' +# --- +# name: test_github_details + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + nf-core create + + + + + + + + + + nf-core create — Create a new pipeline with the nf-core pipeline template + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + Create GitHub repository + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + Create a GitHub repo + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + After creating the pipeline template locally, we can create a GitHub repository and push the + code to it. + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Your GitHub usernameYour GitHub personal access token for Show + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔login.▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + GitHub username▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁GitHub token + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + Please select the the GitHub repository settings: + + + ▔▔▔▔▔▔▔▔Private + Select if the new GitHub repo must be private. + ▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔Push files + Select if you would like to push all the pipeline template files to your GitHub repo + ▁▁▁▁▁▁▁▁and all the branches required to keep the pipeline up to date with new releases of  + nf-core. + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + BackCreate GitHub repoFinish without creating a repo + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + +  D  Toggle dark mode  Q  Quit  + + + + + ''' +# --- +# name: test_github_exit_message + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + nf-core create + + + + + + + + + + nf-core create — Create a new pipeline with the nf-core pipeline template + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + HowTo create a GitHub repository + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + + +                                         ,--./,-. +         ___     __   __   __   ___     /,-._.--~\ + |\ | |__  __ /  ` /  \ |__) |__         }  { +    | \| |       \__, \__/ |  \ |___     \`-._,-`-, +                                        `._,._,' + + If you would like to create the GitHub repository later, you can do it manually by following + these steps: + +  1. Create a new GitHub repository +  2. Add the remote to your local repository + + + cd<pipeline_directory> + gitremoteaddorigingit@github.com:<username>/<repo_name>.git + + +  3. Push the code to the remote + + + gitpush--allorigin + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Close AppShow Logging + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + +  D  Toggle dark mode  Q  Quit  + + + + + ''' +# --- +# name: test_github_question + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + nf-core create + + + + + + + + + + nf-core create — Create a new pipeline with the nf-core pipeline template + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + Create GitHub repository + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + + After creating the pipeline template locally, we can create a GitHub repository and push the + code to it. + + Do you want to create a GitHub repository? + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Create GitHub repoFinish without creating a repo + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +  D  Toggle dark mode  Q  Quit  + + + + + ''' +# --- +# name: test_type_custom + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + nf-core create + + + + + + + + + + nf-core create — Create a new pipeline with the nf-core pipeline template + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + Template features + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Use reference genomesThe pipeline will be Show help + ▁▁▁▁▁▁▁▁configured to use a ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + copy of the most  + common reference  + genome files from  + iGenomes + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Add Github CI testsThe pipeline will Show help + ▁▁▁▁▁▁▁▁include several ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + GitHub actions for  + Continuous  + Integration (CI)  + testing + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Add Github badgesThe README.md file ofShow help + ▁▁▁▁▁▁▁▁the pipeline will ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + include GitHub badges + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Add configuration The pipeline will Show help + ▁▁▁▁▁▁▁▁filesinclude configuration▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + profiles containing  + custom parameters  + requried to run  + nf-core pipelines at  + different  + institutions + + + + + + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + BackContinue + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +  D  Toggle dark mode  Q  Quit  + + + + + ''' +# --- +# name: test_type_nfcore + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + nf-core create + + + + + + + + + + nf-core create — Create a new pipeline with the nf-core pipeline template + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + Template features + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Use reference genomesThe pipeline will be Show help + ▁▁▁▁▁▁▁▁configured to use a ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + copy of the most  + common reference  + genome files from  + iGenomes + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + BackContinue + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +  D  Toggle dark mode  Q  Quit  + + + + + ''' +# --- +# name: test_type_nfcore_validation + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + nf-core create + + + + + + + + + + nf-core create — Create a new pipeline with the nf-core pipeline template + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + Basic details + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + + + GitHub organisationWorkflow name + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + nf-corePipeline Name + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + Value error, Must be lowercase without  + punctuation. + + A short description of your pipeline. + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Description + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + Value error, Cannot be left empty. + + Name of the main author / authors + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Author(s) + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + Value error, Cannot be left empty. + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + BackNext + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + + + + + + + +  D  Toggle dark mode  Q  Quit  + + + + + ''' +# --- +# name: test_welcome + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + nf-core create + + + + + + + + + + nf-core create — Create a new pipeline with the nf-core pip… + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + Create a pipeline from the nf-core template + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + + +                                         ,--./,-. +         ___     __   __   __   ___     /,-._.--~\ + |\ | |__  __ /  ` /  \ |__) |__         }  { +    | \| |       \__, \__/ |  \ |___     \`-._,-`-, +                                        `._,._,' + ▇▇ + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + nf-core create + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + This app will help you create a new nf-core pipeline. It uses the  + nf-core pipeline template, which is kept within the nf-core/tools  + repository. + +  D Toggle dark mode  Q  Quit  + + + + + ''' +# --- diff --git a/tests/components/generate_snapshot.py b/tests/components/generate_snapshot.py index c5067d7210..50024a8ebb 100644 --- a/tests/components/generate_snapshot.py +++ b/tests/components/generate_snapshot.py @@ -1,4 +1,5 @@ """Test generate a snapshot""" + import json from pathlib import Path from unittest.mock import MagicMock diff --git a/tests/components/snapshot_test.py b/tests/components/snapshot_test.py index d774618476..b3fc259770 100644 --- a/tests/components/snapshot_test.py +++ b/tests/components/snapshot_test.py @@ -1,4 +1,5 @@ """Test the 'modules test' or 'subworkflows test' command which runs nf-test test.""" + import shutil from pathlib import Path diff --git a/tests/data/pipeline_create_template.yml b/tests/data/pipeline_create_template.yml index 12e48e9c27..0ed534aa10 100644 --- a/tests/data/pipeline_create_template.yml +++ b/tests/data/pipeline_create_template.yml @@ -1 +1,6 @@ -prefix: testprefix +name: test +description: just for 4w3s0m3 tests +author: Chuck Norris +version: 1.0.0 +force: True +org: testprefix diff --git a/tests/data/pipeline_create_template_skip.yml b/tests/data/pipeline_create_template_skip.yml index b69175e0bb..ed498cb732 100644 --- a/tests/data/pipeline_create_template_skip.yml +++ b/tests/data/pipeline_create_template_skip.yml @@ -1,5 +1,11 @@ -prefix: testprefix -skip: +name: test +description: just for 4w3s0m3 tests +author: Chuck Norris +version: 1.0.0 +force: True +org: testprefix +is_nfcore: False +skip_features: - github - ci - github_badges diff --git a/tests/lint/multiqc_config.py b/tests/lint/multiqc_config.py index 721560ce81..84eba15940 100644 --- a/tests/lint/multiqc_config.py +++ b/tests/lint/multiqc_config.py @@ -48,7 +48,7 @@ def test_multiqc_incorrect_export_plots(self): # Reset the file with open(Path(new_pipeline, "assets", "multiqc_config.yml"), "w") as fh: yaml.safe_dump(mqc_yml_tmp, fh) - assert result["failed"] == ["'assets/multiqc_config.yml' does not contain 'export_plots: true'."] + assert "'assets/multiqc_config.yml' does not contain 'export_plots: true'." in result["failed"] def test_multiqc_config_report_comment_fail(self): @@ -103,4 +103,5 @@ def test_multiqc_config_report_comment_release_succeed(self): # lint again lint_obj._load() result = lint_obj.multiqc_config() - assert "'assets/multiqc_config.yml' contains a matching 'report_comment'." in result["passed"] + print(result["passed"]) + assert "'assets/multiqc_config.yml' contains `report_comment`" in result["passed"] diff --git a/tests/lint/nextflow_config.py b/tests/lint/nextflow_config.py index fa85568f14..c9a1ae1e4a 100644 --- a/tests/lint/nextflow_config.py +++ b/tests/lint/nextflow_config.py @@ -2,8 +2,8 @@ import re from pathlib import Path -import nf_core.create import nf_core.lint +import nf_core.pipelines.create.create def test_nextflow_config_example_pass(self): diff --git a/tests/lint/template_strings.py b/tests/lint/template_strings.py index ac0ae01681..50c956b217 100644 --- a/tests/lint/template_strings.py +++ b/tests/lint/template_strings.py @@ -1,8 +1,8 @@ import subprocess from pathlib import Path -import nf_core.create import nf_core.lint +import nf_core.pipelines.create def test_template_strings(self): @@ -16,7 +16,6 @@ def test_template_strings(self): lint_obj = nf_core.lint.PipelineLint(new_pipeline) lint_obj._load() result = lint_obj.template_strings() - print(result["failed"]) assert len(result["failed"]) == 1 assert len(result["ignored"]) == 0 diff --git a/tests/lint/version_consistency.py b/tests/lint/version_consistency.py index c682800646..4763020fb9 100644 --- a/tests/lint/version_consistency.py +++ b/tests/lint/version_consistency.py @@ -1,5 +1,5 @@ -import nf_core.create import nf_core.lint +import nf_core.pipelines.create.create def test_version_consistency(self): @@ -11,4 +11,4 @@ def test_version_consistency(self): result = lint_obj.version_consistency() assert result["passed"] == ["Version tags are numeric and consistent between container, release tag and config."] - assert result["failed"] == ["manifest.version was not numeric: 1.0dev!"] + assert result["failed"] == ["manifest.version was not numeric: 1.0.0dev!"] diff --git a/tests/modules/patch.py b/tests/modules/patch.py index dc939c7ea7..513ea8a433 100644 --- a/tests/modules/patch.py +++ b/tests/modules/patch.py @@ -349,7 +349,7 @@ def test_remove_patch(self): "modules", REPO_NAME, BISMARK_ALIGN, patch_fn ) - with mock.patch.object(nf_core.create.questionary, "confirm") as mock_questionary: + with mock.patch.object(nf_core.components.patch.questionary, "confirm") as mock_questionary: mock_questionary.unsafe_ask.return_value = True patch_obj.remove(BISMARK_ALIGN) # Check that the diff file has been removed diff --git a/tests/test_bump_version.py b/tests/test_bump_version.py index 658a2339d4..059e18e92e 100644 --- a/tests/test_bump_version.py +++ b/tests/test_bump_version.py @@ -1,11 +1,11 @@ -"""Some tests covering the bump_version code. -""" +"""Some tests covering the bump_version code.""" + import os import yaml import nf_core.bump_version -import nf_core.create +import nf_core.pipelines.create.create import nf_core.utils @@ -16,8 +16,8 @@ def test_bump_pipeline_version(datafiles, tmp_path): # Get a workflow and configs test_pipeline_dir = os.path.join(tmp_path, "nf-core-testpipeline") - create_obj = nf_core.create.PipelineCreate( - "testpipeline", "This is a test pipeline", "Test McTestFace", no_git=True, outdir=test_pipeline_dir, plain=True + create_obj = nf_core.pipelines.create.create.PipelineCreate( + "testpipeline", "This is a test pipeline", "Test McTestFace", no_git=True, outdir=test_pipeline_dir ) create_obj.init_pipeline() pipeline_obj = nf_core.utils.Pipeline(test_pipeline_dir) @@ -36,8 +36,8 @@ def test_dev_bump_pipeline_version(datafiles, tmp_path): """Test that making a release works with a dev name and a leading v""" # Get a workflow and configs test_pipeline_dir = os.path.join(tmp_path, "nf-core-testpipeline") - create_obj = nf_core.create.PipelineCreate( - "testpipeline", "This is a test pipeline", "Test McTestFace", no_git=True, outdir=test_pipeline_dir, plain=True + create_obj = nf_core.pipelines.create.create.PipelineCreate( + "testpipeline", "This is a test pipeline", "Test McTestFace", no_git=True, outdir=test_pipeline_dir ) create_obj.init_pipeline() pipeline_obj = nf_core.utils.Pipeline(test_pipeline_dir) @@ -55,8 +55,8 @@ def test_dev_bump_pipeline_version(datafiles, tmp_path): def test_bump_nextflow_version(datafiles, tmp_path): # Get a workflow and configs test_pipeline_dir = os.path.join(tmp_path, "nf-core-testpipeline") - create_obj = nf_core.create.PipelineCreate( - "testpipeline", "This is a test pipeline", "Test McTestFace", no_git=True, outdir=test_pipeline_dir, plain=True + create_obj = nf_core.pipelines.create.create.PipelineCreate( + "testpipeline", "This is a test pipeline", "Test McTestFace", no_git=True, outdir=test_pipeline_dir ) create_obj.init_pipeline() pipeline_obj = nf_core.utils.Pipeline(test_pipeline_dir) diff --git a/tests/test_cli.py b/tests/test_cli.py index 1261e3a9e9..28be5c2a73 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,4 +1,4 @@ -""" Tests covering the command-line code. +"""Tests covering the command-line code. Most tests check the cli arguments are passed along and that some action is taken. @@ -229,21 +229,17 @@ def test_licences_log_error(self, mock_lic): assert error_txt in captured_logs.output[-1] assert captured_logs.records[-1].levelname == "ERROR" - @mock.patch("nf_core.create.PipelineCreate") + @mock.patch("nf_core.pipelines.create.create.PipelineCreate") def test_create(self, mock_create): """Test nf-core pipeline is created and cli parameters are passed on.""" params = { - "name": "pipeline name", + "name": "pipelinename", "description": "pipeline description", "author": "Kalle Anka", - "version": "1.2.3", - "force": None, "outdir": "/path/outdir", - "template-yaml": "file.yaml", - "plain": None, } - cmd = ["create"] + self.assemble_params(params) + cmd = ["pipelines", "create"] + self.assemble_params(params) result = self.invoke_cli(cmd) assert result.exit_code == 0 @@ -251,14 +247,38 @@ def test_create(self, mock_create): params["name"], params["description"], params["author"], - version=params["version"], force="force" in params, + version="1.0.0dev", outdir=params["outdir"], - template_yaml_path=params["template-yaml"], - plain="plain" in params, + organisation="nf-core", ) mock_create.return_value.init_pipeline.assert_called_once() + @mock.patch("nf_core.pipelines.create.create.PipelineCreate") + def test_create_error(self, mock_create): + """Test `nf-core pipelines create` run without providing all the arguments thorws an error.""" + params = { + "name": "pipelinename", + } + + cmd = ["pipelines", "create"] + self.assemble_params(params) + result = self.invoke_cli(cmd) + + assert result.exit_code == 1 + assert "Command arguments are not accepted in interactive mode." in result.output + + @mock.patch("nf_core.pipelines.create.PipelineCreateApp") + def test_create_app(self, mock_create): + """Test `nf-core pipelines create` runs an App.""" + cmd = ["pipelines", "create"] + result = self.invoke_cli(cmd) + + assert result.return_value == (0 or None) + assert "Launching interactive nf-core pipeline creation tool." in result.output + + mock_create.assert_called_once_with() + mock_create.return_value.run.assert_called_once() + @mock.patch("nf_core.utils.is_pipeline_directory") @mock.patch("nf_core.lint.run_linting") def test_lint(self, mock_lint, mock_is_pipeline): diff --git a/tests/test_components.py b/tests/test_components.py index b7f67eb51d..eaf999c3c3 100644 --- a/tests/test_components.py +++ b/tests/test_components.py @@ -1,5 +1,4 @@ -""" Tests covering the modules commands -""" +"""Tests covering the modules commands""" import os import shutil diff --git a/tests/test_create.py b/tests/test_create.py index 1cc073cb54..313b6f5354 100644 --- a/tests/test_create.py +++ b/tests/test_create.py @@ -1,14 +1,13 @@ -"""Some tests covering the pipeline creation sub command. -""" +"""Some tests covering the pipeline creation sub command.""" + import os import unittest from pathlib import Path -from unittest import mock import git import yaml -import nf_core.create +import nf_core.pipelines.create.create from .utils import with_temporary_folder @@ -26,25 +25,24 @@ def setUp(self): self.default_branch = "default" def test_pipeline_creation(self): - pipeline = nf_core.create.PipelineCreate( + pipeline = nf_core.pipelines.create.create.PipelineCreate( name=self.pipeline_name, description=self.pipeline_description, author=self.pipeline_author, version=self.pipeline_version, no_git=False, force=True, - plain=True, default_branch=self.default_branch, ) - assert pipeline.template_params["name"] == self.pipeline_name - assert pipeline.template_params["description"] == self.pipeline_description - assert pipeline.template_params["author"] == self.pipeline_author - assert pipeline.template_params["version"] == self.pipeline_version + assert pipeline.config.name == self.pipeline_name + assert pipeline.config.description == self.pipeline_description + assert pipeline.config.author == self.pipeline_author + assert pipeline.config.version == self.pipeline_version @with_temporary_folder def test_pipeline_creation_initiation(self, tmp_path): - pipeline = nf_core.create.PipelineCreate( + pipeline = nf_core.pipelines.create.create.PipelineCreate( name=self.pipeline_name, description=self.pipeline_description, author=self.pipeline_author, @@ -52,7 +50,6 @@ def test_pipeline_creation_initiation(self, tmp_path): no_git=False, force=True, outdir=tmp_path, - plain=True, default_branch=self.default_branch, ) pipeline.init_pipeline() @@ -60,20 +57,14 @@ def test_pipeline_creation_initiation(self, tmp_path): assert f" {self.default_branch}\n" in git.Repo.init(pipeline.outdir).git.branch() assert not os.path.exists(os.path.join(pipeline.outdir, "pipeline_template.yml")) with open(os.path.join(pipeline.outdir, ".nf-core.yml")) as fh: - assert "template" not in fh.read() + assert "template" in fh.read() @with_temporary_folder def test_pipeline_creation_initiation_with_yml(self, tmp_path): - pipeline = nf_core.create.PipelineCreate( - name=self.pipeline_name, - description=self.pipeline_description, - author=self.pipeline_author, - version=self.pipeline_version, + pipeline = nf_core.pipelines.create.create.PipelineCreate( no_git=False, - force=True, outdir=tmp_path, - template_yaml_path=PIPELINE_TEMPLATE_YML, - plain=True, + template_config=PIPELINE_TEMPLATE_YML, default_branch=self.default_branch, ) pipeline.init_pipeline() @@ -86,23 +77,12 @@ def test_pipeline_creation_initiation_with_yml(self, tmp_path): with open(os.path.join(pipeline.outdir, ".nf-core.yml")) as fh: nfcore_yml = yaml.safe_load(fh) assert "template" in nfcore_yml - assert nfcore_yml["template"] == yaml.safe_load(PIPELINE_TEMPLATE_YML.read_text()) + assert yaml.safe_load(PIPELINE_TEMPLATE_YML.read_text()).items() <= nfcore_yml["template"].items() - @mock.patch.object(nf_core.create.PipelineCreate, "customize_template") - @mock.patch.object(nf_core.create.questionary, "confirm") @with_temporary_folder - def test_pipeline_creation_initiation_customize_template(self, mock_questionary, mock_customize, tmp_path): - mock_questionary.unsafe_ask.return_value = True - mock_customize.return_value = {"prefix": "testprefix"} - pipeline = nf_core.create.PipelineCreate( - name=self.pipeline_name, - description=self.pipeline_description, - author=self.pipeline_author, - version=self.pipeline_version, - no_git=False, - force=True, - outdir=tmp_path, - default_branch=self.default_branch, + def test_pipeline_creation_initiation_customize_template(self, tmp_path): + pipeline = nf_core.pipelines.create.create.PipelineCreate( + outdir=tmp_path, template_config=PIPELINE_TEMPLATE_YML, default_branch=self.default_branch ) pipeline.init_pipeline() assert os.path.isdir(os.path.join(pipeline.outdir, ".git")) @@ -114,24 +94,16 @@ def test_pipeline_creation_initiation_customize_template(self, mock_questionary, with open(os.path.join(pipeline.outdir, ".nf-core.yml")) as fh: nfcore_yml = yaml.safe_load(fh) assert "template" in nfcore_yml - assert nfcore_yml["template"] == yaml.safe_load(PIPELINE_TEMPLATE_YML.read_text()) + assert yaml.safe_load(PIPELINE_TEMPLATE_YML.read_text()).items() <= nfcore_yml["template"].items() @with_temporary_folder def test_pipeline_creation_with_yml_skip(self, tmp_path): - pipeline = nf_core.create.PipelineCreate( - name=self.pipeline_name, - description=self.pipeline_description, - author=self.pipeline_author, - version=self.pipeline_version, - no_git=False, - force=True, + pipeline = nf_core.pipelines.create.create.PipelineCreate( outdir=tmp_path, - template_yaml_path=PIPELINE_TEMPLATE_YML_SKIP, - plain=True, + template_config=PIPELINE_TEMPLATE_YML_SKIP, default_branch=self.default_branch, ) pipeline.init_pipeline() - assert not os.path.isdir(os.path.join(pipeline.outdir, ".git")) # Check pipeline template yml has been dumped to `.nf-core.yml` and matches input assert not os.path.exists(os.path.join(pipeline.outdir, "pipeline_template.yml")) @@ -139,7 +111,7 @@ def test_pipeline_creation_with_yml_skip(self, tmp_path): with open(os.path.join(pipeline.outdir, ".nf-core.yml")) as fh: nfcore_yml = yaml.safe_load(fh) assert "template" in nfcore_yml - assert nfcore_yml["template"] == yaml.safe_load(PIPELINE_TEMPLATE_YML_SKIP.read_text()) + assert yaml.safe_load(PIPELINE_TEMPLATE_YML.read_text()).items() <= nfcore_yml["template"].items() # Check that some of the skipped files are not present assert not os.path.exists(os.path.join(pipeline.outdir, "CODE_OF_CONDUCT.md")) diff --git a/tests/test_create_app.py b/tests/test_create_app.py new file mode 100644 index 0000000000..b6b05ab58f --- /dev/null +++ b/tests/test_create_app.py @@ -0,0 +1,286 @@ +""" Test Pipeline Create App """ +from unittest import mock + +import pytest + +from nf_core.pipelines.create import PipelineCreateApp + + +@pytest.mark.asyncio +async def test_app_bindings(): + """Test that the app bindings work.""" + app = PipelineCreateApp() + async with app.run_test() as pilot: + # Test pressing the D key + assert app.dark + await pilot.press("d") + assert not app.dark + await pilot.press("d") + assert app.dark + + # Test pressing the Q key + await pilot.press("q") + assert app.return_code == 0 + + +def test_welcome(snap_compare): + """Test snapshot for the first screen in the app. The welcome screen.""" + assert snap_compare("../nf_core/pipelines/create/__init__.py") + + +def test_choose_type(snap_compare): + """Test snapshot for the choose_type screen. + Steps to get to this screen: + screen welcome > press start > + screen choose_type + """ + + async def run_before(pilot) -> None: + await pilot.click("#start") + + assert snap_compare("../nf_core/pipelines/create/__init__.py", terminal_size=(100, 50), run_before=run_before) + + +def test_basic_details_nfcore(snap_compare): + """Test snapshot for the basic_details screen of an nf-core pipeline. + Steps to get to this screen: + screen welcome > press start > + screen choose_type > press nf-core > + screen basic_details + """ + + async def run_before(pilot) -> None: + await pilot.click("#start") + await pilot.click("#type_nfcore") + + assert snap_compare("../nf_core/pipelines/create/__init__.py", terminal_size=(100, 50), run_before=run_before) + + +def test_basic_details_custom(snap_compare): + """Test snapshot for the basic_details screen of a custom pipeline. + Steps to get to this screen: + screen welcome > press start > + screen choose_type > press custom > + screen basic_details + """ + + async def run_before(pilot) -> None: + await pilot.click("#start") + await pilot.click("#type_custom") + + assert snap_compare("../nf_core/pipelines/create/__init__.py", terminal_size=(100, 50), run_before=run_before) + + +def test_type_nfcore(snap_compare): + """Test snapshot for the type_nfcore screen. + Steps to get to this screen: + screen welcome > press start > + screen choose_type > press nf-core > + screen basic_details > enter pipeline details > press next > + screen type_nfcore + """ + + async def run_before(pilot) -> None: + await pilot.click("#start") + await pilot.click("#type_nfcore") + await pilot.click("#name") + await pilot.press("m", "y", "p", "i", "p", "e", "l", "i", "n", "e") + await pilot.press("tab") + await pilot.press("A", " ", "c", "o", "o", "l", " ", "d", "e", "s", "c", "r", "i", "p", "t", "i", "o", "n") + await pilot.press("tab") + await pilot.press("M", "e") + await pilot.click("#next") + + assert snap_compare("../nf_core/pipelines/create/__init__.py", terminal_size=(100, 50), run_before=run_before) + + +def test_type_nfcore_validation(snap_compare): + """Test snapshot for the type_nfcore screen. + Validation errors should appear when input fields are empty. + Steps to get to this screen: + screen welcome > press start > + screen choose_type > press nf-core > + screen basic_details > press next > + ERRORS + """ + + async def run_before(pilot) -> None: + await pilot.click("#start") + await pilot.click("#type_nfcore") + await pilot.click("#next") + + assert snap_compare("../nf_core/pipelines/create/__init__.py", terminal_size=(100, 50), run_before=run_before) + + +def test_type_custom(snap_compare): + """Test snapshot for the type_custom screen. + Steps to get to this screen: + screen welcome > press start > + screen choose_type > press custom > + screen basic_details > enter pipeline details > press next > + screen type_custom + """ + + async def run_before(pilot) -> None: + await pilot.click("#start") + await pilot.click("#type_custom") + await pilot.click("#name") + await pilot.press("tab") + await pilot.press("m", "y", "p", "i", "p", "e", "l", "i", "n", "e") + await pilot.press("tab") + await pilot.press("A", " ", "c", "o", "o", "l", " ", "d", "e", "s", "c", "r", "i", "p", "t", "i", "o", "n") + await pilot.press("tab") + await pilot.press("M", "e") + await pilot.click("#next") + + assert snap_compare("../nf_core/pipelines/create/__init__.py", terminal_size=(100, 50), run_before=run_before) + + +def test_final_details(snap_compare): + """Test snapshot for the final_details screen. + Steps to get to this screen: + screen welcome > press start > + screen choose_type > press nf-core > + screen basic_details > enter pipeline details > press next > + screen type_nfcore > press continue > + screen final_details + """ + + async def run_before(pilot) -> None: + await pilot.click("#start") + await pilot.click("#type_nfcore") + await pilot.click("#name") + await pilot.press("m", "y", "p", "i", "p", "e", "l", "i", "n", "e") + await pilot.press("tab") + await pilot.press("A", " ", "c", "o", "o", "l", " ", "d", "e", "s", "c", "r", "i", "p", "t", "i", "o", "n") + await pilot.press("tab") + await pilot.press("M", "e") + await pilot.click("#next") + await pilot.click("#continue") + + assert snap_compare("../nf_core/pipelines/create/__init__.py", terminal_size=(100, 50), run_before=run_before) + + +def test_customisation_help(snap_compare): + """Test snapshot for the type_custom screen - showing help messages. + Steps to get to this screen: + screen welcome > press start > + screen choose_type > press nf-core > + screen basic_details > enter pipeline details > press next > + screen type_custom > press Show more + """ + + async def run_before(pilot) -> None: + await pilot.click("#start") + await pilot.click("#type_custom") + await pilot.click("#name") + await pilot.press("tab") + await pilot.press("m", "y", "p", "i", "p", "e", "l", "i", "n", "e") + await pilot.press("tab") + await pilot.press("A", " ", "c", "o", "o", "l", " ", "d", "e", "s", "c", "r", "i", "p", "t", "i", "o", "n") + await pilot.press("tab") + await pilot.press("M", "e") + await pilot.click("#next") + await pilot.click("#igenomes") + await pilot.press("tab") + await pilot.press("enter") + + assert snap_compare("../nf_core/pipelines/create/__init__.py", terminal_size=(100, 50), run_before=run_before) + + +@mock.patch("nf_core.pipelines.create.create.PipelineCreate.init_pipeline", return_value=None) +def test_github_question(mock_init_pipeline, snap_compare): + """Test snapshot for the github_repo_question screen. + Steps to get to this screen: + screen welcome > press start > + screen choose_type > press nf-core > + screen basic_details > enter pipeline details > press next > + screen type_nfcore > press continue > + screen final_details > press finish > close logging screen > + screen github_repo_question + """ + + async def run_before(pilot) -> None: + await pilot.click("#start") + await pilot.click("#type_nfcore") + await pilot.click("#name") + await pilot.press("m", "y", "p", "i", "p", "e", "l", "i", "n", "e") + await pilot.press("tab") + await pilot.press("A", " ", "c", "o", "o", "l", " ", "d", "e", "s", "c", "r", "i", "p", "t", "i", "o", "n") + await pilot.press("tab") + await pilot.press("M", "e") + await pilot.click("#next") + await pilot.click("#continue") + await pilot.click("#finish") + await pilot.click("#close_screen") + + assert snap_compare("../nf_core/pipelines/create/__init__.py", terminal_size=(100, 50), run_before=run_before) + + +@mock.patch("nf_core.pipelines.create.create.PipelineCreate.init_pipeline", return_value=None) +def test_github_details(mock_init_pipeline, snap_compare): + """Test snapshot for the github_repo screen. + Steps to get to this screen: + screen welcome > press start > + screen choose_type > press nf-core > + screen basic_details > enter pipeline details > press next > + screen type_nfcore > press continue > + screen final_details > press finish > close logging screen > + screen github_repo_question > press create repo > + screen github_repo + """ + + async def run_before(pilot) -> None: + delete = ["backspace"] * 50 + await pilot.click("#start") + await pilot.click("#type_nfcore") + await pilot.click("#name") + await pilot.press("m", "y", "p", "i", "p", "e", "l", "i", "n", "e") + await pilot.press("tab") + await pilot.press("A", " ", "c", "o", "o", "l", " ", "d", "e", "s", "c", "r", "i", "p", "t", "i", "o", "n") + await pilot.press("tab") + await pilot.press("M", "e") + await pilot.click("#next") + await pilot.click("#continue") + await pilot.click("#finish") + await pilot.click("#close_screen") + await pilot.click("#github_repo") + await pilot.click("#gh_username") + await pilot.press(*delete) # delete field automatically filled using github CLI + await pilot.press("tab") + await pilot.press(*delete) + + assert snap_compare("../nf_core/pipelines/create/__init__.py", terminal_size=(100, 50), run_before=run_before) + + +@mock.patch("nf_core.pipelines.create.create.PipelineCreate.init_pipeline", return_value=None) +def test_github_exit_message(mock_init_pipeline, snap_compare): + """Test snapshot for the github_exit screen. + Steps to get to this screen: + screen welcome > press start > + screen choose_type > press nf-core > + screen basic_details > enter pipeline details > press next > + screen type_nfcore > press continue > + screen final_details > press finish > close logging screen > + screen github_repo_question > press create repo > + screen github_repo > press exit (close without creating a repo) > + screen github_exit + """ + + async def run_before(pilot) -> None: + await pilot.click("#start") + await pilot.click("#type_nfcore") + await pilot.click("#name") + await pilot.press("m", "y", "p", "i", "p", "e", "l", "i", "n", "e") + await pilot.press("tab") + await pilot.press("A", " ", "c", "o", "o", "l", " ", "d", "e", "s", "c", "r", "i", "p", "t", "i", "o", "n") + await pilot.press("tab") + await pilot.press("M", "e") + await pilot.click("#next") + await pilot.click("#continue") + await pilot.click("#finish") + await pilot.click("#close_screen") + await pilot.click("#github_repo") + await pilot.click("#exit") + + assert snap_compare("../nf_core/pipelines/create/__init__.py", terminal_size=(100, 50), run_before=run_before) diff --git a/tests/test_download.py b/tests/test_download.py index d823040247..48ba88f3c9 100644 --- a/tests/test_download.py +++ b/tests/test_download.py @@ -1,5 +1,4 @@ -"""Tests for the download subcommand of nf-core tools -""" +"""Tests for the download subcommand of nf-core tools""" import os import re @@ -11,7 +10,7 @@ import pytest -import nf_core.create +import nf_core.pipelines.create.create import nf_core.utils from nf_core.download import ContainerError, DownloadWorkflow, WorkflowRepo from nf_core.synced_repo import SyncedRepo @@ -109,13 +108,12 @@ def test_download_configs(self, outdir): def test_wf_use_local_configs(self, tmp_path): # Get a workflow and configs test_pipeline_dir = os.path.join(tmp_path, "nf-core-testpipeline") - create_obj = nf_core.create.PipelineCreate( + create_obj = nf_core.pipelines.create.create.PipelineCreate( "testpipeline", "This is a test pipeline", "Test McTestFace", no_git=True, outdir=test_pipeline_dir, - plain=True, ) create_obj.init_pipeline() diff --git a/tests/test_launch.py b/tests/test_launch.py index dc8d6b147c..043055a2d5 100644 --- a/tests/test_launch.py +++ b/tests/test_launch.py @@ -1,5 +1,4 @@ -""" Tests covering the pipeline launch code. -""" +"""Tests covering the pipeline launch code.""" import json import os @@ -9,8 +8,8 @@ import pytest -import nf_core.create import nf_core.launch +import nf_core.pipelines.create.create from .utils import create_tmp_pipeline, with_temporary_file, with_temporary_folder @@ -66,8 +65,8 @@ def test_get_pipeline_schema(self): def test_make_pipeline_schema(self, tmp_path): """Create a workflow, but delete the schema file, then try to load it""" test_pipeline_dir = os.path.join(tmp_path, "wf") - create_obj = nf_core.create.PipelineCreate( - "testpipeline", "", "", outdir=test_pipeline_dir, no_git=True, plain=True + create_obj = nf_core.pipelines.create.create.PipelineCreate( + "testpipeline", "a description", "Me", outdir=test_pipeline_dir, no_git=True ) create_obj.init_pipeline() os.remove(os.path.join(test_pipeline_dir, "nextflow_schema.json")) diff --git a/tests/test_licenses.py b/tests/test_licenses.py index 4fb58a107c..8023c9e891 100644 --- a/tests/test_licenses.py +++ b/tests/test_licenses.py @@ -1,5 +1,4 @@ -"""Some tests covering the pipeline creation sub command. -""" +"""Some tests covering the pipeline creation sub command.""" # import json # import os # import tempfile diff --git a/tests/test_lint.py b/tests/test_lint.py index 9839265892..31923ea889 100644 --- a/tests/test_lint.py +++ b/tests/test_lint.py @@ -1,5 +1,5 @@ -"""Some tests covering the linting code. -""" +"""Some tests covering the linting code.""" + import fnmatch import json import os @@ -9,8 +9,8 @@ import yaml -import nf_core.create import nf_core.lint +import nf_core.pipelines.create.create from .utils import with_temporary_folder @@ -21,13 +21,13 @@ class TestLint(unittest.TestCase): def setUp(self): """Function that runs at start of tests for common resources - Use nf_core.create() to make a pipeline that we can use for testing + Use nf_core.pipelines.create() to make a pipeline that we can use for testing """ self.tmp_dir = tempfile.mkdtemp() self.test_pipeline_dir = os.path.join(self.tmp_dir, "nf-core-testpipeline") - self.create_obj = nf_core.create.PipelineCreate( - "testpipeline", "This is a test pipeline", "Test McTestFace", outdir=self.test_pipeline_dir, plain=True + self.create_obj = nf_core.pipelines.create.create.PipelineCreate( + "testpipeline", "This is a test pipeline", "Test McTestFace", outdir=self.test_pipeline_dir ) self.create_obj.init_pipeline() diff --git a/tests/test_list.py b/tests/test_list.py index c1f51e03e0..c78276b41d 100644 --- a/tests/test_list.py +++ b/tests/test_list.py @@ -1,5 +1,4 @@ -""" Tests covering the workflow listing code. -""" +"""Tests covering the workflow listing code.""" import json import os diff --git a/tests/test_modules.py b/tests/test_modules.py index 539d3dcc57..7955d8efa7 100644 --- a/tests/test_modules.py +++ b/tests/test_modules.py @@ -1,5 +1,4 @@ -""" Tests covering the modules commands -""" +"""Tests covering the modules commands""" import os import shutil @@ -10,8 +9,8 @@ import responses import yaml -import nf_core.create import nf_core.modules +import nf_core.pipelines.create.create from .utils import ( GITLAB_BRANCH_TEST_BRANCH, @@ -86,6 +85,7 @@ def setUp(self): # Set up the schema self.tmp_dir, self.template_dir, self.pipeline_name, self.pipeline_dir = create_tmp_pipeline() + # Set up install objects self.mods_install = nf_core.modules.ModuleInstall(self.pipeline_dir, prompt=False, force=True) self.mods_install_old = nf_core.modules.ModuleInstall( diff --git a/tests/test_params_file.py b/tests/test_params_file.py index 13c82f5188..673139c3b2 100644 --- a/tests/test_params_file.py +++ b/tests/test_params_file.py @@ -4,7 +4,7 @@ import tempfile from pathlib import Path -import nf_core.create +import nf_core.pipelines.create.create import nf_core.schema from nf_core.params_file import ParamsFileBuilder @@ -21,8 +21,8 @@ def setup_class(cls): # Create a test pipeline in temp directory cls.tmp_dir = tempfile.mkdtemp() cls.template_dir = os.path.join(cls.tmp_dir, "wf") - create_obj = nf_core.create.PipelineCreate( - "testpipeline", "", "", outdir=cls.template_dir, no_git=True, plain=True + create_obj = nf_core.pipelines.create.create.PipelineCreate( + "testpipeline", "a description", "Me", outdir=cls.template_dir, no_git=True ) create_obj.init_pipeline() diff --git a/tests/test_refgenie.py b/tests/test_refgenie.py index 5440c1c477..23cc0dd14a 100644 --- a/tests/test_refgenie.py +++ b/tests/test_refgenie.py @@ -1,5 +1,4 @@ -""" Tests covering the refgenie integration code -""" +"""Tests covering the refgenie integration code""" import os import shlex diff --git a/tests/test_schema.py b/tests/test_schema.py index 89fcc98b66..d7f4f4d648 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -1,5 +1,4 @@ -""" Tests covering the pipeline schema code. -""" +"""Tests covering the pipeline schema code.""" import json import os @@ -12,7 +11,7 @@ import requests import yaml -import nf_core.create +import nf_core.pipelines.create.create import nf_core.schema from .utils import with_temporary_file, with_temporary_folder @@ -29,8 +28,8 @@ def setUp(self): # Create a test pipeline in temp directory self.tmp_dir = tempfile.mkdtemp() self.template_dir = os.path.join(self.tmp_dir, "wf") - create_obj = nf_core.create.PipelineCreate( - "testpipeline", "", "", outdir=self.template_dir, no_git=True, plain=True + create_obj = nf_core.pipelines.create.create.PipelineCreate( + "testpipeline", "a description", "Me", outdir=self.template_dir, no_git=True ) create_obj.init_pipeline() diff --git a/tests/test_subworkflows.py b/tests/test_subworkflows.py index cd0bd21146..23ec98db46 100644 --- a/tests/test_subworkflows.py +++ b/tests/test_subworkflows.py @@ -1,13 +1,12 @@ -""" Tests covering the subworkflows commands -""" +"""Tests covering the subworkflows commands""" import os import shutil import unittest from pathlib import Path -import nf_core.create import nf_core.modules +import nf_core.pipelines.create.create import nf_core.subworkflows from .utils import ( @@ -51,6 +50,7 @@ def setUp(self): # Set up the pipeline structure self.tmp_dir, self.template_dir, self.pipeline_name, self.pipeline_dir = create_tmp_pipeline() + # Set up the nf-core/modules repo dummy self.nfcore_modules = create_modules_repo_dummy(self.tmp_dir) diff --git a/tests/test_sync.py b/tests/test_sync.py index 51a27653ab..f15676f08b 100644 --- a/tests/test_sync.py +++ b/tests/test_sync.py @@ -1,5 +1,4 @@ -""" Tests covering the sync command -""" +"""Tests covering the sync command""" import json import os @@ -12,7 +11,7 @@ import git import pytest -import nf_core.create +import nf_core.pipelines.create.create import nf_core.sync from .utils import with_temporary_folder @@ -26,8 +25,8 @@ def setUp(self): self.tmp_dir = tempfile.mkdtemp() self.pipeline_dir = os.path.join(self.tmp_dir, "testpipeline") default_branch = "master" - self.create_obj = nf_core.create.PipelineCreate( - "testing", "test pipeline", "tester", outdir=self.pipeline_dir, plain=True, default_branch=default_branch + self.create_obj = nf_core.pipelines.create.create.PipelineCreate( + "testing", "test pipeline", "tester", outdir=self.pipeline_dir, default_branch=default_branch ) self.create_obj.init_pipeline() self.remote_path = os.path.join(self.tmp_dir, "remote_repo") diff --git a/tests/test_utils.py b/tests/test_utils.py index 4b5ab19fce..85f4e3c548 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,5 +1,4 @@ -""" Tests covering for utility functions. -""" +"""Tests covering for utility functions.""" import os import shutil @@ -11,8 +10,8 @@ import pytest import requests -import nf_core.create import nf_core.list +import nf_core.pipelines.create.create import nf_core.utils from .utils import with_temporary_folder @@ -35,17 +34,16 @@ class TestUtils(unittest.TestCase): def setUp(self): """Function that runs at start of tests for common resources - Use nf_core.create() to make a pipeline that we can use for testing + Use nf_core.pipelines.create() to make a pipeline that we can use for testing """ self.tmp_dir = tempfile.mkdtemp() self.test_pipeline_dir = os.path.join(self.tmp_dir, "nf-core-testpipeline") - self.create_obj = nf_core.create.PipelineCreate( + self.create_obj = nf_core.pipelines.create.create.PipelineCreate( "testpipeline", "This is a test pipeline", "Test McTestFace", no_git=True, outdir=self.test_pipeline_dir, - plain=True, ) self.create_obj.init_pipeline() # Base Pipeline object on this directory diff --git a/tests/utils.py b/tests/utils.py index 89c1328818..9a0fd0896f 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -9,8 +9,8 @@ import responses -import nf_core.create import nf_core.modules +import nf_core.pipelines.create.create OLD_TRIMGALORE_SHA = "9b7a3bdefeaad5d42324aa7dd50f87bea1b04386" OLD_TRIMGALORE_BRANCH = "mimic-old-trimgalore" @@ -102,8 +102,8 @@ def create_tmp_pipeline() -> Tuple[str, str, str, str]: pipeline_name = "mypipeline" pipeline_dir = os.path.join(tmp_dir, pipeline_name) - nf_core.create.PipelineCreate( - pipeline_name, "it is mine", "me", no_git=True, outdir=pipeline_dir, plain=True + nf_core.pipelines.create.create.PipelineCreate( + pipeline_name, "it is mine", "me", no_git=True, outdir=pipeline_dir ).init_pipeline() # return values to instance variables for later use in test methods