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 0bc5e432e7..9052c90ddc 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 0de7287a57..4ae2714534 100644 --- a/.github/workflows/create-test-lint-wf-template.yml +++ b/.github/workflows/create-test-lint-wf-template.yml @@ -84,29 +84,29 @@ jobs: run: | mkdir create-test-lint-wf export NXF_WORK=$(pwd) - printf "prefix: my-prefix\nskip: ['ci', 'github_badges', 'igenomes', 'nf_core_configs']" > create-test-lint-wf/template_skip_all.yml + printf "org: my-prefix\nskip: ['ci', 'github_badges', 'igenomes', 'nf_core_configs']" > create-test-lint-wf/template_skip_all.yml - name: Create template skip github_badges run: | - printf "prefix: my-prefix\nskip: github_badges" > create-test-lint-wf/template_skip_github_badges.yml + printf "org: my-prefix\nskip: github_badges" > create-test-lint-wf/template_skip_github_badges.yml - name: Create template skip igenomes run: | - printf "prefix: my-prefix\nskip: igenomes" > create-test-lint-wf/template_skip_igenomes.yml + printf "org: my-prefix\nskip: igenomes" > create-test-lint-wf/template_skip_igenomes.yml - name: Create template skip ci run: | - printf "prefix: my-prefix\nskip: ci" > create-test-lint-wf/template_skip_ci.yml + printf "org: my-prefix\nskip: ci" > create-test-lint-wf/template_skip_ci.yml - name: Create template skip nf_core_configs run: | - printf "prefix: my-prefix\nskip: nf_core_configs" > create-test-lint-wf/template_skip_nf_core_configs.yml + printf "org: my-prefix\nskip: nf_core_configs" > create-test-lint-wf/template_skip_nf_core_configs.yml # Create a pipeline from the template - 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 87cdf2e7bb..a95a477459 100644 --- a/.github/workflows/create-test-wf.yml +++ b/.github/workflows/create-test-wf.yml @@ -70,7 +70,11 @@ 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" + # echo current directory + pwd + # echo content of current directory + ls -la nextflow run nf-core-testpipeline -profile test,self_hosted_runner --outdir ./results - name: Upload log file artifact diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 70b9cfd0a8..4e873385e8 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -142,6 +142,13 @@ jobs: exit 1 fi + - name: Store snapshot report + uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4 + if: always() + with: + name: Snapshot Report ${{ matrix.test }} + path: ./snapshot_report.html + - name: Upload coverage uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4 with: 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/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/__main__.py b/nf_core/__main__.py index 147b0586b2..67af238b5c 100644 --- a/nf_core/__main__.py +++ b/nf_core/__main__.py @@ -17,7 +17,7 @@ from nf_core.download import DownloadError from nf_core.modules.modules_repo import NF_CORE_MODULES_REMOTE from nf_core.params_file import ParamsFileBuilder -from nf_core.utils import check_if_outdated, rich_force_colors, setup_nfcore_dir +from nf_core.utils import check_if_outdated, nfcore_logo, rich_force_colors, setup_nfcore_dir # Set up logging as the root logger # Submodules should all traverse back to this @@ -45,7 +45,7 @@ { "name": "Commands for developers", "commands": [ - "create", + "pipelines", "lint", "modules", "subworkflows", @@ -56,6 +56,12 @@ ], }, ], + "nf-core pipelines": [ + { + "name": "Pipeline commands", + "commands": ["create"], + }, + ], "nf-core modules": [ { "name": "For pipelines", @@ -115,25 +121,11 @@ def run_nf_core(): # print nf-core header if environment variable is not set if os.environ.get("_NF_CORE_COMPLETE") is None: # Print nf-core header - stderr.print(f"\n[green]{' ' * 42},--.[grey39]/[green],-.", highlight=False) - stderr.print( - "[blue] ___ __ __ __ ___ [green]/,-._.--~\\", - highlight=False, - ) - stderr.print( - r"[blue] |\ | |__ __ / ` / \ |__) |__ [yellow] } {", - highlight=False, - ) - stderr.print( - r"[blue] | \| | \__, \__/ | \ |___ [green]\`-._,-`-,", - highlight=False, - ) - stderr.print( - "[green] `._,._,'\n", - highlight=False, - ) + stderr.print("\n") + for line in nfcore_logo: + stderr.print(line, highlight=False) stderr.print( - f"[grey39] nf-core/tools version {__version__} - [link=https://nf-co.re]https://nf-co.re[/]", + f"\n[grey39] nf-core/tools version {__version__} - [link=https://nf-co.re]https://nf-co.re[/]", highlight=False, ) try: @@ -494,53 +486,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( @@ -658,6 +603,111 @@ 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, + template_config=template_yaml, + 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( + "[red]Partial arguments supplied.[/] " + "Run without [i]any[/] arguments for an interactive interface, " + "or with at least name + description + author to use non-interactively." + ) + sys.exit(1) + else: + log.info("Launching interactive nf-core pipeline creation tool.") + 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( + "The `[magenta]nf-core create[/]` command is deprecated. Use `[magenta]nf-core pipelines create[/]` instead." + ) + sys.exit(0) + + # nf-core modules subcommands @nf_core_cli.group() @click.option( diff --git a/nf_core/lint/files_unchanged.py b/nf_core/lint/files_unchanged.py index 014c2b5f09..fc044b6597 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__) @@ -109,7 +109,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() @@ -119,16 +119,16 @@ def files_unchanged(self) -> Dict[str, Union[List[str], bool]]: "name": short_name, "description": self.nf_config["manifest.description"].strip("\"'"), "author": self.nf_config["manifest.author"].strip("\"'"), - "prefix": prefix, + "org": prefix, } template_yaml_path = Path(tmp_dir, "template.yaml") 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/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 4f01a97993..9ad257a0b8 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/README.md b/nf_core/pipeline-template/README.md index 88e0f1719f..e6351b0c6f 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://cloud.seqera.io/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 d46dfca04c..6645e0f6cf 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 ae7c0b715f..18bad71b76 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..da6a693220 --- /dev/null +++ b/nf_core/pipelines/create/__init__.py @@ -0,0 +1,101 @@ +"""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.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, + show_time=False, + show_path=False, + 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(), + "logging": LoggingScreen(), + "github_repo_question": GithubRepoQuestion(), + "github_repo": GithubRepo(), + "github_exit": GithubExit(), + } + + # 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": + self.push_screen("github_repo_question") + elif event.button.id == "exit": + self.push_screen("github_exit") + 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..b88ede10d0 --- /dev/null +++ b/nf_core/pipelines/create/basicdetails.py @@ -0,0 +1,110 @@ +"""A Textual app to create a pipeline.""" + +from pathlib import Path +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, add_hide_class, remove_hide_class + +pipeline_exists_warn = """ +> ⚠️ **The pipeline you are trying to create already exists.** +> +> If you continue, you will **override** the existing pipeline. +> Please change the pipeline or organisation name to create a different pipeline. +""" + + +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 Markdown(dedent(pipeline_exists_warn), id="exist_warn", classes="hide") + yield Center( + Button("Back", id="back", variant="default"), + Button("Next", id="next", variant="success"), + classes="cta", + ) + + @on(Input.Changed) + @on(Input.Submitted) + def show_exists_warn(self): + """Check if the pipeline exists on every input change or submitted. + If the pipeline exists, show warning message saying that it will be overriden.""" + config = {} + for text_input in self.query("TextInput"): + this_input = text_input.query_one(Input) + config[text_input.field_id] = this_input.value + if Path(config["org"] + "-" + config["name"]).is_dir(): + remove_hide_class(self.parent, "exist_warn") + else: + add_hide_class(self.parent, "exist_warn") + + def on_screen_resume(self): + """Hide warn message on screen resume. + Update displayed value on screen resume.""" + add_hide_class(self.parent, "exist_warn") + for text_input in self.query("TextInput"): + if text_input.field_id == "org": + text_input.disabled = self.parent.PIPELINE_TYPE == "nfcore" + + @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 62% rename from nf_core/create.py rename to nf_core/pipelines/create/create.py index b420b1c86d..151ad83ff2 100644 --- a/nf_core/create.py +++ b/nf_core/pipelines/create/create.py @@ -7,12 +7,11 @@ 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 @@ -20,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__) @@ -31,31 +31,53 @@ 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, + is_interactive: bool = False, ): - 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 = { @@ -65,7 +87,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", @@ -73,176 +95,159 @@ 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 - if outdir is None: - outdir = os.path.join(os.getcwd(), self.template_params["name_noslash"]) - self.outdir = Path(outdir) + self.is_interactive = is_interactive + self.force = self.config.force + if self.config.outdir is None: + self.config.outdir = os.getcwd() + if self.config.outdir == ".": + self.outdir = Path(self.config.outdir, self.jinja_params["name_noslash"]).absolute() + else: + self.outdir = Path(self.config.outdir).absolute() - 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.") + + # Check required fields + 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 = self.config.org == "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.""" @@ -254,7 +259,7 @@ def init_pipeline(self): if not self.no_git: self.git_init_pipeline() - if self.template_params["branded"]: + if self.config.is_nfcore and not self.is_interactive: log.info( "[green bold]!!!!!! IMPORTANT !!!!!!\n\n" "[green not bold]If you are interested in adding your pipeline to the nf-core community,\n" @@ -265,7 +270,7 @@ def init_pipeline(self): def render_template(self): """Runs Jinja to create a new nf-core pipeline.""" - log.info(f"Creating new nf-core pipeline: '{self.name}'") + log.info(f"Creating new pipeline: '{self.name}'") # Check if the output directory exists if self.outdir.exists(): @@ -274,7 +279,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) @@ -282,15 +287,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", @@ -348,14 +353,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() @@ -363,10 +368,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) + with open(config_fn, "w") as fh: + 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) @@ -410,7 +415,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", @@ -435,7 +440,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", @@ -461,7 +466,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", @@ -472,7 +477,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( [ @@ -484,15 +489,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 @@ -506,11 +511,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): @@ -534,7 +539,7 @@ def git_init_pipeline(self): "Pipeline git repository will not be initialised." ) - log.info("Initialising pipeline git repository") + log.info("Initialising local pipeline git repository") repo = git.Repo.init(self.outdir) repo.git.add(A=True) repo.index.commit(f"initial template build from nf-core/tools, version {nf_core.__version__}") @@ -554,14 +559,16 @@ 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" - " git remote add origin git@github.com:USERNAME/REPO_NAME.git \n" - " git push --all origin " - ) - log.info("This will also push your newly created dev branch and the TEMPLATE branch for syncing.") + if self.is_interactive: + log.info(f"Pipeline created: ./{self.outdir.relative_to(Path.cwd())}") + else: + log.info( + "Done. Remember to add a remote and push to GitHub:\n" + f"[white on grey23] cd {self.outdir} \n" + " git remote add origin git@github.com:USERNAME/REPO_NAME.git \n" + " git push --all origin " + ) + log.info("This will also push your newly created dev branch and the TEMPLATE branch for syncing.") diff --git a/nf_core/pipelines/create/create.tcss b/nf_core/pipelines/create/create.tcss new file mode 100644 index 0000000000..67394a9de3 --- /dev/null +++ b/nf_core/pipelines/create/create.tcss @@ -0,0 +1,135 @@ +#logo { + width: 100%; + content-align-horizontal: center; + content-align-vertical: middle; +} +.cta { + layout: horizontal; + margin-bottom: 1; +} +.cta Button { + margin: 0 3; +} + +.pipeline-type-grid { + height: auto; + margin-bottom: 2; +} + +.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; +} + +.features-container { + padding: 0 4 1 4; +} + +/* Display help messages */ + +.help_box { + background: #333333; + padding: 1 3 0 3; + margin: 0 5 2 5; + 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; + background: #333333; + padding: 1 3; + margin: 0 4 2 4; +} + +.hide { + display: none; +} + +/* Layouts */ +.col-2 { + grid-size: 2 1; +} + +.ghrepo-cols { + margin: 0 4; +} +.ghrepo-cols Button { + margin-top: 2; +} diff --git a/nf_core/pipelines/create/custompipeline.py b/nf_core/pipelines/create/custompipeline.py new file mode 100644 index 0000000000..7d460db65d --- /dev/null +++ b/nf_core/pipelines/create/custompipeline.py @@ -0,0 +1,99 @@ +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", + ), + classes="features-container", + ) + 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/finaldetails.py b/nf_core/pipelines/create/finaldetails.py new file mode 100644 index 0000000000..bd15cf9ddd --- /dev/null +++ b/nf_core/pipelines/create/finaldetails.py @@ -0,0 +1,110 @@ +"""A Textual app to create a pipeline.""" + +from pathlib import Path +from textwrap import dedent + +from textual import on, work +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.create import PipelineCreate +from nf_core.pipelines.create.utils import ShowLogs, TextInput, add_hide_class, remove_hide_class + +pipeline_exists_warn = """ +> ⚠️ **The pipeline you are trying to create already exists.** +> +> If you continue, you will **override** the existing pipeline. +> Please change the pipeline or organisation name to create a different pipeline. +> Alternatively, provide a different output directory. +""" + + +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", + ) + + yield Markdown(dedent(pipeline_exists_warn), id="exist_warn", classes="hide") + + 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 + + # Create the new pipeline + self._create_pipeline() + self.parent.LOGGING_STATE = "pipeline created" + self.parent.push_screen("logging") + + @on(Input.Changed) + @on(Input.Submitted) + def show_exists_warn(self): + """Check if the pipeline exists on every input change or submitted. + If the pipeline exists, show warning message saying that it will be overriden.""" + outdir = "" + for text_input in self.query("TextInput"): + this_input = text_input.query_one(Input) + if text_input.field_id == "outdir": + outdir = this_input.value + if Path(outdir, self.parent.TEMPLATE_CONFIG.org + "-" + self.parent.TEMPLATE_CONFIG.name).is_dir(): + remove_hide_class(self.parent, "exist_warn") + + def on_screen_resume(self): + """Hide warn message on screen resume.""" + add_hide_class(self.parent, "exist_warn") + + @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, + is_interactive=True, + ) + create_obj.init_pipeline() + remove_hide_class(self.parent, "close_screen") diff --git a/nf_core/pipelines/create/githubexit.py b/nf_core/pipelines/create/githubexit.py new file mode 100644 index 0000000000..3dac88cc5f --- /dev/null +++ b/nf_core/pipelines/create/githubexit.py @@ -0,0 +1,48 @@ +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 + +from nf_core.utils import nfcore_logo + +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 + ``` + > 💡 Note the `--all` flag: this is needed to push all branches to the remote. +""" + + +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( + "\n" + "\n".join(nfcore_logo) + "\n", + id="logo", + ) + yield Markdown(exit_help_text_markdown) + yield Center( + Button("Close", id="close_app", variant="success"), + classes="cta", + ) diff --git a/nf_core/pipelines/create/githubrepo.py b/nf_core/pipelines/create/githubrepo.py new file mode 100644 index 0000000000..99e7b09ab8 --- /dev/null +++ b/nf_core/pipelines/create/githubrepo.py @@ -0,0 +1,253 @@ +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 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.utils import ShowLogs, TextInput, remove_hide_class + +log = logging.getLogger(__name__) + +github_org_help = """ +> ⚠️ **You can't create a repository directly in the nf-core organisation.** +> +> Please create the pipeline repo to an organisation where you have access or use your user account. +> A core-team member will be able to transfer the repo to nf-core once the development has started. + +> 💡 Your GitHub user account will be used by default if `nf-core` is given as the org name. +""" + + +class GithubRepo(Screen): + """Create a GitHub repository and push all branches.""" + + def compose(self) -> ComposeResult: + yield Header() + yield Footer() + gh_user, gh_token = self._get_github_credentials() + github_text_markdown = dedent( + """ + # Create GitHub repository + + Now that we have created a new pipeline locally, we can + create a new GitHub repository and push the code to it. + """ + ) + if gh_user: + github_text_markdown += f">\n> 💡 _Found GitHub username {'and token ' if gh_token else ''}in local [GitHub CLI](https://cli.github.com/) config_\n>\n" + yield Markdown(github_text_markdown) + with Horizontal(classes="ghrepo-cols"): + 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 [link=https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens]personal access token[/link] 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") + with Horizontal(classes="ghrepo-cols"): + yield TextInput( + "repo_org", + "Organisation name", + "The name of the organisation where the GitHub repo will be cretaed", + default=self.parent.TEMPLATE_CONFIG.org, + classes="column", + ) + yield TextInput( + "repo_name", + "Repository name", + "The name of the new GitHub repository", + default=self.parent.TEMPLATE_CONFIG.name, + classes="column", + ) + if self.parent.TEMPLATE_CONFIG.is_nfcore: + yield Markdown(dedent(github_org_help)) + with Horizontal(classes="ghrepo-cols"): + yield Switch(value=False, id="private") + with Vertical(): + yield Static("Private", classes="") + yield Static("Select to make the new GitHub repo private.", 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, token and repo name + 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 github_variables["repo_org"] != "nf-core": + try: + org = github_auth.get_organization(github_variables["repo_org"]) + log.info( + f"Repo will be created in the GitHub organisation account '{github_variables['repo_org']}'" + ) + except UnknownObjectException: + log.warn(f"Provided organisation '{github_variables['repo_org']}' not found. ") + + # Create the repo + try: + if org: + self._create_repo_and_push( + org, + github_variables["repo_name"], + pipeline_repo, + github_variables["private"], + ) + 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, + github_variables["repo_name"], + pipeline_repo, + github_variables["private"], + ) + except UserWarning as e: + log.error(f"There was an error with message: {e}") + self.parent.push_screen("github_exit") + + self.parent.LOGGING_STATE = "repo created" + self.parent.push_screen("logging") + + class RepoExists(Message): + """Custom message to indicate that the GitHub repo already exists.""" + + pass + + @on(RepoExists) + def show_github_info_button(self) -> None: + remove_hide_class(self.parent, "exit") + remove_hide_class(self.parent, "back") + + @work(thread=True, exclusive=True) + def _create_repo_and_push(self, org, repo_name, pipeline_repo, private): + """Create a GitHub repository and push all branches.""" + self.post_message(ShowLogs()) + # Check if repo already exists + try: + repo = org.get_repo(repo_name) + # Check if it has a commit history + try: + repo.get_commits().totalCount + raise UserWarning(f"GitHub repository '{repo_name}' already exists") + except GithubException: + # Repo is empty + repo_exists = True + except UserWarning as e: + # Repo already exists + log.error(e) + self.post_message(self.RepoExists()) + return + except UnknownObjectException: + # Repo doesn't exist + repo_exists = False + + # Create the repo + if not repo_exists: + repo = org.create_repo(repo_name, description=self.parent.TEMPLATE_CONFIG.description, private=private) + log.info(f"GitHub repository '{repo_name}' created successfully") + remove_hide_class(self.parent, "close_app") + + # Add the remote + try: + pipeline_repo.create_remote("origin", repo.clone_url) + except git.exc.GitCommandError: + # Remote already exists + pass + # Push all branches + 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): + try: + 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"] + except KeyError: + pass + # 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..f862dccea1 --- /dev/null +++ b/nf_core/pipelines/create/loggingscreen.py @@ -0,0 +1,48 @@ +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 + +from nf_core.pipelines.create.utils import add_hide_class +from nf_core.utils import nfcore_logo + + +class LoggingScreen(Screen): + """A screen to show the final logs.""" + + def compose(self) -> ComposeResult: + yield Header() + yield Footer() + yield Markdown( + dedent( + """ + # Logging + """ + ) + ) + yield Static( + "\n" + "\n".join(nfcore_logo) + "\n", + id="logo", + ) + yield Markdown("Creating...") + yield Center(self.parent.LOG_HANDLER.console) + yield Center( + Button("Back", id="back", variant="default", classes="hide"), + Button("Continue", id="close_screen", variant="success", classes="hide"), + Button("Continue", id="exit", variant="success", classes="hide"), + Button("Close App", id="close_app", variant="success", classes="hide"), + classes="cta", + ) + + def on_screen_resume(self): + """Hide all buttons as disabled on screen resume.""" + button_ids = ["back", "close_screen", "exit", "close_app"] + for button in self.query("Button"): + if button.id in button_ids: + add_hide_class(self.parent, button.id) + + def on_screen_suspend(self): + """Clear console on screen suspend.""" + self.parent.LOG_HANDLER.console.clear() diff --git a/nf_core/pipelines/create/nfcorepipeline.py b/nf_core/pipelines/create/nfcorepipeline.py new file mode 100644 index 0000000000..49cc1f8f86 --- /dev/null +++ b/nf_core/pipelines/create/nfcorepipeline.py @@ -0,0 +1,48 @@ +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", + ), + classes="features-container", + ) + 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..48914e8555 --- /dev/null +++ b/nf_core/pipelines/create/pipelinetype.py @@ -0,0 +1,56 @@ +from textual.app import ComposeResult +from textual.containers import Center, Grid +from textual.screen import Screen +from textual.widgets import Button, Footer, Header, Markdown + +markdown_intro = """ +# Choose pipeline type +""" + +markdown_type_nfcore = """ +## 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 +""" +markdown_type_custom = """ +## 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 = """ +## What's the difference? + +Choosing _"nf-core"_ effectively pre-selects the following template features: + +* GitHub Actions continuous-integration configuration files: + * Pipeline test runs: Small-scale (GitHub) and large-scale (AWS) + * Code formatting checks with [Prettier](https://prettier.io/) + * Auto-fix linting functionality using [@nf-core-bot](https://github.com/nf-core-bot) + * Marking old issues as stale +* Inclusion of [shared nf-core configuration profiles](https://nf-co.re/configs) +""" + + +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 Grid( + Center( + Markdown(markdown_type_nfcore), + Center(Button("nf-core", id="type_nfcore", variant="success")), + ), + Center( + Markdown(markdown_type_custom), + Center(Button("Custom", id="type_custom", variant="primary")), + ), + classes="col-2 pipeline-type-grid", + ) + 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..6006452baf --- /dev/null +++ b/nf_core/pipelines/create/utils.py @@ -0,0 +1,226 @@ +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] = True + 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 add_hide_class(app, widget_id: str) -> None: + """Add class 'hide' to a widget. Not display widget.""" + app.get_widget_by_id(widget_id).add_class("hide") + + +def remove_hide_class(app, widget_id: str) -> None: + """Remove class 'hide' to a widget. Display widget.""" + app.get_widget_by_id(widget_id).remove_class("hide") + + +## 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..1da0a3c01d --- /dev/null +++ b/nf_core/pipelines/create/welcome.py @@ -0,0 +1,37 @@ +from textual.app import ComposeResult +from textual.containers import Center +from textual.screen import Screen +from textual.widgets import Button, Footer, Header, Markdown, Static + +from nf_core.utils import nfcore_logo + +markdown = """ +# Welcome to the nf-core pipeline creation wizard + +This app will help you create a new Nextflow pipeline +from the [nf-core/tools pipeline template](https://github.com/nf-core/tools). + +The template helps anyone benefit from nf-core best practices, +and is a requirement for nf-core pipelines. + +> 💡 If you want to add a pipeline to nf-core, please +> [join on Slack](https://nf-co.re/join) and discuss your plans with the +> community as early as possible; _**ideally before you start on your pipeline!**_ +> See the [nf-core guidelines](https://nf-co.re/docs/contributing/guidelines) +> and the [#new-pipelines](https://nfcore.slack.com/channels/new-pipelines) +> Slack channel for more information. +""" + + +class WelcomeScreen(Screen): + """A welcome screen for the app.""" + + def compose(self) -> ComposeResult: + yield Header() + yield Footer() + yield Static( + "\n" + "\n".join(nfcore_logo) + "\n", + id="logo", + ) + yield Markdown(markdown) + yield Center(Button("Let's go!", id="start", variant="success"), classes="cta") diff --git a/nf_core/sync.py b/nf_core/sync.py index 5e7b198d8d..81082c24be 100644 --- a/nf_core/sync.py +++ b/nf_core/sync.py @@ -15,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__) @@ -257,7 +257,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: @@ -265,7 +265,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("'"), @@ -273,7 +273,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 5b31f48f4b..8c50f0a49f 100644 --- a/nf_core/utils.py +++ b/nf_core/utils.py @@ -36,6 +36,15 @@ log = logging.getLogger(__name__) +# ASCII nf-core logo +nfcore_logo = [ + r"[green] ,--.[grey39]/[green],-.", + r"[blue] ___ __ __ __ ___ [green]/,-._.--~\ ", + r"[blue] |\ | |__ __ / ` / \ |__) |__ [yellow] } {", + r"[blue] | \| | \__, \__/ | \ |___ [green]\`-._,-`-,", + r"[green] `._,._,'", +] + # Custom style for questionary nfcore_question_style = prompt_toolkit.styles.Style( [ diff --git a/pytest.ini b/pytest.ini index cf37159478..fcbd03fa45 100644 --- a/pytest.ini +++ b/pytest.ini @@ -2,3 +2,6 @@ testpaths = tests python_files = test_*.py + +# automatically run coroutine tests with asyncio +asyncio_mode = auto 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 6b5b3ab57d..acf30f491d 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>=2.0.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..c486ec4f8f --- /dev/null +++ b/tests/__snapshots__/test_create_app.ambr @@ -0,0 +1,3329 @@ +# 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 + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + Choose pipeline type + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +        Choose "nf-core" if:              Choose "Custom" if:         + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ● You want your pipeline to be part of● Your pipeline will never be part of  + the nf-core communitynf-core + ● You think that there's an outside ● You want full control over all + chance that it ever could be part offeatures that are included from the  + nf-coretemplate (including those that are  + mandatory for nf-core). + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + nf-core▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁Custom + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +                                  What's the difference?                                  + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Choosing "nf-core" effectively pre-selects the following template features: + + ● GitHub Actions continuous-integration configuration files: + ▪ Pipeline test runs: Small-scale (GitHub) and large-scale (AWS) + ▪ Code formatting checks with Prettier + ▪ Auto-fix linting functionality using @nf-core-bot + ▪ Marking old issues as stale + ● Inclusion of shared nf-core configuration 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 Hide help + ▁▁▁▁▁▁▁▁genomeswill be ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + configured 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 The pipeline Show help▅▅ + ▁▁▁▁▁▁▁▁testswill include ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + several GitHub  + actions for  + Continuous  + Integration (CI) + testing + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Add Github The README.md Show help + ▁▁▁▁▁▁▁▁badgesfile of the ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + pipeline will  + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + 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▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁. + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + 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 + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Now that we have created a new pipeline locally, we can create a new GitHub repository and  + push the code to it. + + + + Your GitHub usernameYour GitHub personal access token + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔for login.▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + GitHub username▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔Show + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁GitHub token▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + The name of the organisation where the The name of the new GitHub repository + GitHub repo will be cretaed▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔mypipeline + nf-core▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + ⚠️ You can't create a repository directly in the nf-core organisation. + Please create the pipeline repo to an organisation where you have access or use your user + account. A core-team member will be able to transfer the repo to nf-core once the  + development has started. + + 💡 Your GitHub user account will be used by default if nf-core is given as the org name. + + + ▔▔▔▔▔▔▔▔Private + Select to make the new GitHub repo private. + ▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + 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 + + + 💡 Note the --all flag: this is needed to push all branches to the remote. + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Close + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + +  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 The pipeline willShow help + ▁▁▁▁▁▁▁▁genomesbe configured to ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + use a copy of the + most common  + reference genome  + files from  + iGenomes + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Add Github CI The pipeline willShow help + ▁▁▁▁▁▁▁▁testsinclude several ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + GitHub actions  + for Continuous  + Integration (CI)  + testing + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Add Github badgesThe README.md Show help + ▁▁▁▁▁▁▁▁file of the ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + pipeline will  + include GitHub  + badges + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Add configurationThe pipeline willShow 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 The pipeline willShow help + ▁▁▁▁▁▁▁▁genomesbe 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 pipeline template + +                                           ,--./,-. +           ___     __   __   __   ___     /,-._.--~\  +     |\ | |__  __ /  ` /  \ |__) |__         }  { +     | \| |       \__, \__/ |  \ |___     \`-._,-`-, +                                           `._,._,' + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + Welcome to the nf-core pipeline creation wizard + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + This app will help you create a new Nextflow pipeline from the nf-core/tools pipeline  + template. + + The template helps anyone benefit from nf-core best practices, and is a requirement for  + nf-core pipelines. + + 💡 If you want to add a pipeline to nf-core, please join on Slack and discuss your plans  + with the community as early as possible; ideally before you start on your pipeline! See  + the nf-core guidelines and the #new-pipelines Slack channel for more information. + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Let's go! + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + + + + + + + + + +  D  Toggle dark mode  Q  Quit  + + + + + ''' +# --- 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/configs.py b/tests/lint/configs.py index b50a1393aa..8610910cd8 100644 --- a/tests/lint/configs.py +++ b/tests/lint/configs.py @@ -2,8 +2,8 @@ import yaml -import nf_core.create import nf_core.lint +import nf_core.pipelines.create def test_withname_in_modules_config(self): diff --git a/tests/lint/nextflow_config.py b/tests/lint/nextflow_config.py index 06af8c4fb8..4bd7959448 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/nfcore_yml.py b/tests/lint/nfcore_yml.py index 474ccd48fc..9d745a6346 100644 --- a/tests/lint/nfcore_yml.py +++ b/tests/lint/nfcore_yml.py @@ -1,8 +1,8 @@ import re from pathlib import Path -import nf_core.create import nf_core.lint +import nf_core.pipelines.create def test_nfcore_yml_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 c697d34287..059e18e92e 100644 --- a/tests/test_bump_version.py +++ b/tests/test_bump_version.py @@ -5,7 +5,7 @@ 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 5df0a32418..76d167101a 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -231,21 +231,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 @@ -253,14 +249,39 @@ 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, + template_config=None, + 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 "Partial arguments supplied." 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_create.py b/tests/test_create.py index e2672499cd..313b6f5354 100644 --- a/tests/test_create.py +++ b/tests/test_create.py @@ -3,12 +3,11 @@ 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..f01ea5b6bd --- /dev/null +++ b/tests/test_create_app.py @@ -0,0 +1,291 @@ +"""Test Pipeline Create App""" + +from nf_core.pipelines.create import PipelineCreateApp + + +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", terminal_size=(100, 50)) + + +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) + + +def test_github_question(tmpdir, 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.press("backspace") + await pilot.press("tab") + await pilot.press(*str(tmpdir)) + await pilot.click("#finish") + await pilot.app.workers.wait_for_complete() + await pilot.click("#close_screen") + + assert snap_compare("../nf_core/pipelines/create/__init__.py", terminal_size=(100, 50), run_before=run_before) + + +def test_github_details(tmpdir, 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.press("backspace") + await pilot.press("tab") + await pilot.press(*str(tmpdir)) + await pilot.click("#finish") + await pilot.app.workers.wait_for_complete() + 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) + + +def test_github_exit_message(tmpdir, 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.press("backspace") + await pilot.press("tab") + await pilot.press(*str(tmpdir)) + await pilot.click("#finish") + await pilot.app.workers.wait_for_complete() + 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 3e0f11d579..e090885bb4 100644 --- a/tests/test_download.py +++ b/tests/test_download.py @@ -12,7 +12,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 @@ -128,13 +128,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 79dbe3fb97..043055a2d5 100644 --- a/tests/test_launch.py +++ b/tests/test_launch.py @@ -8,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 @@ -65,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_lint.py b/tests/test_lint.py index b72a6bfdfa..84c54c0e5b 100644 --- a/tests/test_lint.py +++ b/tests/test_lint.py @@ -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_modules.py b/tests/test_modules.py index d3d99abadd..a122f16b69 100644 --- a/tests/test_modules.py +++ b/tests/test_modules.py @@ -10,8 +10,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, @@ -99,6 +99,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_schema.py b/tests/test_schema.py index e0921908d4..b4be7ae455 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -12,7 +12,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 +29,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 0a9224002a..59de1967e4 100644 --- a/tests/test_subworkflows.py +++ b/tests/test_subworkflows.py @@ -6,8 +6,8 @@ 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 ( @@ -63,6 +63,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 b94968cd4c..40e68dc7dc 100644 --- a/tests/test_sync.py +++ b/tests/test_sync.py @@ -11,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 @@ -25,12 +25,11 @@ 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( + self.create_obj = nf_core.pipelines.create.create.PipelineCreate( "testing", "test pipeline", "tester", outdir=self.pipeline_dir, - plain=True, default_branch=default_branch, ) self.create_obj.init_pipeline() diff --git a/tests/test_utils.py b/tests/test_utils.py index 145060450f..85f4e3c548 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -10,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 @@ -34,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