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 -%}
[](https://github.com/{{ name }}/actions/workflows/ci.yml)
[](https://github.com/{{ name }}/actions/workflows/linting.yml){% endif -%}
-{% if branded -%}[](https://nf-co.re/{{ short_name }}/results){% endif -%}
+{% if is_nfcore -%}[](https://nf-co.re/{{ short_name }}/results){% endif -%}
{%- if github_badges -%}
[](https://doi.org/10.5281/zenodo.XXXXXXX)
[](https://www.nf-test.com)
@@ -23,10 +23,10 @@
[](https://cloud.seqera.io/launch?pipeline=https://github.com/{{ name }})
{% endif -%}
-{%- if branded -%}[](https://nfcore.slack.com/channels/{{ short_name }}){% endif -%}
-{%- if branded -%}[](https://twitter.com/nf_core){% endif -%}
-{%- if branded -%}[](https://mstdn.science/@nf_core){% endif -%}
-{%- if branded -%}[](https://www.youtube.com/c/nf-core)
+{%- if is_nfcore -%}[](https://nfcore.slack.com/channels/{{ short_name }}){% endif -%}
+{%- if is_nfcore -%}[](https://twitter.com/nf_core){% endif -%}
+{%- if is_nfcore -%}[](https://mstdn.science/@nf_core){% endif -%}
+{%- if is_nfcore -%}[](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
+ '''
+
+
+ '''
+# ---
+# name: test_basic_details_nfcore
+ '''
+
+
+ '''
+# ---
+# name: test_choose_type
+ '''
+
+
+ '''
+# ---
+# name: test_customisation_help
+ '''
+
+
+ '''
+# ---
+# name: test_final_details
+ '''
+
+
+ '''
+# ---
+# name: test_github_details
+ '''
+
+
+ '''
+# ---
+# name: test_github_exit_message
+ '''
+
+
+ '''
+# ---
+# name: test_github_question
+ '''
+
+
+ '''
+# ---
+# name: test_type_custom
+ '''
+
+
+ '''
+# ---
+# name: test_type_nfcore
+ '''
+
+
+ '''
+# ---
+# name: test_type_nfcore_validation
+ '''
+
+
+ '''
+# ---
+# name: test_welcome
+ '''
+
+
+ '''
+# ---
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 15c1550e72..aaf8330800 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