diff --git a/modules/nf-core/primerprospector/analyzeprimers/environment.yml b/modules/nf-core/primerprospector/analyzeprimers/environment.yml new file mode 100644 index 00000000000..fd12537c4f9 --- /dev/null +++ b/modules/nf-core/primerprospector/analyzeprimers/environment.yml @@ -0,0 +1,7 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/environment-schema.json +channels: + - conda-forge + - bioconda +dependencies: + - bioconda::primerprospector=1.0.1 diff --git a/modules/nf-core/primerprospector/analyzeprimers/main.nf b/modules/nf-core/primerprospector/analyzeprimers/main.nf new file mode 100644 index 00000000000..59cf1a8dca2 --- /dev/null +++ b/modules/nf-core/primerprospector/analyzeprimers/main.nf @@ -0,0 +1,79 @@ +process PRIMERPROSPECTOR_ANALYZEPRIMERS { + tag "$meta.id" + label 'process_single' + + conda "${moduleDir}/environment.yml" + container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? + 'https://depot.galaxyproject.org/singularity/primerprospector:1.0.1--py27_0' : + 'quay.io/biocontainers/primerprospector:1.0.1--py27_0' }" + + input: + tuple val(meta), path(fasta), path(primers) + + output: + tuple val(meta), path("${prefix}_hits.txt"), emit: hits + tuple val(meta), path("${prefix}.ps") , emit: plots + tuple val("${task.process}"), val('primerprospector'), eval("analyze_primers.py --version 2>&1 | grep -Eo '[0-9]+(\\.[0-9]+)+' | tail -n 1"), topic: versions, emit: versions_primerprospector + + when: + task.ext.when == null || task.ext.when + + script: + def args = task.ext.args ?: '' + prefix = task.ext.prefix ?: "${meta.id}" + def fasta_arg = fasta instanceof List ? fasta.join(':') : fasta + def primer_arg = primers ? "-P \"${primers}\"" : '' + def arg_tokens = args.tokenize() + if (arg_tokens.any { arg -> arg == '-f' || arg.startsWith('--fasta_seqs') || arg == '-P' || arg.startsWith('--primers_filepath') || arg == '-o' || arg.startsWith('--output_dir') }) { + error "'-f/--fasta_seqs', '-P/--primers_filepath' and '-o/--output_dir' are reserved by this module. Use input files for fasta/primers and task.ext.prefix to control output file prefixes." + } + if (!primers && !(arg_tokens.any { arg -> arg == '-p' || arg.startsWith('--primer_name') } && arg_tokens.any { arg -> arg == '-s' || arg.startsWith('--primer_sequence') })) { + error "Provide a primers file in the input tuple, or specify a single primer with '-p/--primer_name' and '-s/--primer_sequence' in task.ext.args." + } + """ + # Bug: Primer Prospector 1.0.1 passes numpy floats to range(), which requires integer arguments, while plotting. + # This shim avoids a Python 2 TypeError that otherwise stops .ps output generation. + cat <<'PY' > analyze_primers_compat.py + import __builtin__ + import os + import sys + _range = __builtin__.range + def _int_range(*args): + return _range(*[int(arg) for arg in args]) + __builtin__.range = _int_range + for path_dir in os.environ.get('PATH', '').split(os.pathsep): + script_path = os.path.join(path_dir, 'analyze_primers.py') + if os.path.exists(script_path): + sys.argv[0] = script_path + execfile(script_path) + break + else: + raise RuntimeError('Could not find analyze_primers.py on PATH') + PY + + python analyze_primers_compat.py \\ + $args \\ + -f "${fasta_arg}" \\ + ${primer_arg} + + mv *_hits.txt "${prefix}_hits.txt" + mv *.ps "${prefix}.ps" + """ + + stub: + def args = task.ext.args ?: '' + prefix = task.ext.prefix ?: "${meta.id}" + def arg_tokens = args.tokenize() + if (arg_tokens.any { arg -> arg == '-f' || arg.startsWith('--fasta_seqs') || arg == '-P' || arg.startsWith('--primers_filepath') || arg == '-o' || arg.startsWith('--output_dir') }) { + error "'-f/--fasta_seqs', '-P/--primers_filepath' and '-o/--output_dir' are reserved by this module. Use input files for fasta/primers and task.ext.prefix to control output file prefixes." + } + if (!primers && !(arg_tokens.any { arg -> arg == '-p' || arg.startsWith('--primer_name') } && arg_tokens.any { arg -> arg == '-s' || arg.startsWith('--primer_sequence') })) { + error "Provide a primers file in the input tuple, or specify a single primer with '-p/--primer_name' and '-s/--primer_sequence' in task.ext.args." + } + """ + echo "$args" + + touch ${prefix}_hits.txt + touch ${prefix}.ps + """ +} diff --git a/modules/nf-core/primerprospector/analyzeprimers/meta.yml b/modules/nf-core/primerprospector/analyzeprimers/meta.yml new file mode 100644 index 00000000000..bb412eec114 --- /dev/null +++ b/modules/nf-core/primerprospector/analyzeprimers/meta.yml @@ -0,0 +1,89 @@ +name: "primerprospector_analyzeprimers" +description: Score PCR primers for binding to target sequences +keywords: + - primer design + - pcr + - fasta + - sequence analysis + - primer scoring +tools: + - "primerprospector": + description: "Primer Prospector is a pipeline of programs to design and analyze PCR primers." + homepage: "https://pprospector.sourceforge.net/" + documentation: "https://pprospector.sourceforge.net/scripts/analyze_primers.html" + tool_dev_url: "https://sourceforge.net/p/pprospector/code/" + doi: "10.1093/bioinformatics/btr087" + licence: + - "GPL" + args_id: "$args" + identifier: "" +input: + - - meta: + type: map + description: | + Groovy Map containing sample information. Mandatory. + e.g. `[ id:'sample1' ]` + - fasta: + type: file + description: Target FASTA file or files to score primers against. Mandatory. + pattern: "*.{fasta,fa,fna}" + ontologies: + - edam: "http://edamontology.org/format_1929" # FASTA + - primers: + type: file + description: | + Tab-delimited primers file containing primer names and sequences. Optional + when primer name and sequence are supplied with task.ext.args. + pattern: "*.{txt,tsv}" + ontologies: + - edam: "http://edamontology.org/format_3475" # TSV +output: + hits: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'sample1' ]` + - ${prefix}_hits.txt: + type: file + description: Primer hit details, mismatches, gaps, positions, and weighted scores. + pattern: "*_hits.txt" + ontologies: + - edam: "http://edamontology.org/format_3752" # CSV + plots: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'sample1' ]` + - ${prefix}.ps: + type: file + description: PostScript summary plots of primer mismatch and weighted score information. + pattern: "*.ps" + ontologies: + - edam: "http://edamontology.org/format_2330" # Textual format + versions_primerprospector: + - - ${task.process}: + type: string + description: The name of the process + - primerprospector: + type: string + description: The name of the tool + - analyze_primers.py --version 2>&1 | grep -Eo '[0-9]+(\.[0-9]+)+' | tail -n 1: + type: eval + description: The expression to obtain the version of the tool +topics: + versions: + - - ${task.process}: + type: string + description: The name of the process + - primerprospector: + type: string + description: The name of the tool + - analyze_primers.py --version 2>&1 | grep -Eo '[0-9]+(\.[0-9]+)+' | tail -n 1: + type: eval + description: The expression to obtain the version of the tool +authors: + - "@vagkaratzas" +maintainers: + - "@vagkaratzas" diff --git a/modules/nf-core/primerprospector/analyzeprimers/tests/main.nf.test b/modules/nf-core/primerprospector/analyzeprimers/tests/main.nf.test new file mode 100644 index 00000000000..7be5cfc6ad2 --- /dev/null +++ b/modules/nf-core/primerprospector/analyzeprimers/tests/main.nf.test @@ -0,0 +1,77 @@ +nextflow_process { + + name "Test Process PRIMERPROSPECTOR_ANALYZEPRIMERS" + script "../main.nf" + process "PRIMERPROSPECTOR_ANALYZEPRIMERS" + + tag "modules" + tag "modules_nfcore" + tag "primerprospector" + tag "primerprospector/analyzeprimers" + + test("synthetic - fasta primers - hits plots") { + + when { + process { + """ + def fasta = channel.of( + '>seq1', + 'AAACCCGGGTTTAAACCCGGGTTT', + '>seq2', + 'GGGTTTAAACCCGGGTTTAAA' + ).collectFile(name: 'test.fasta', newLine: true, sort: false) + def primers = channel.of( + 'testf\\tAAACCCGGGTTT' + ).collectFile(name: 'primers.txt', newLine: true, sort: false) + + input[0] = fasta.combine(primers).map { fasta_file, primers_file -> + [[ id:'test' ], fasta_file, primers_file] + } + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot( + process.out.hits, + path(process.out.plots[0][1]).exists(), + process.out.findAll { key, val -> key.startsWith("versions") } + ).match() } + ) + } + } + + test("synthetic - fasta primers - hits plots - stub") { + + options "-stub" + + when { + process { + """ + def fasta = channel.of( + '>seq1', + 'AAACCCGGGTTTAAACCCGGGTTT', + '>seq2', + 'GGGTTTAAACCCGGGTTTAAA' + ).collectFile(name: 'test.fasta', newLine: true, sort: false) + def primers = channel.of( + 'testf\\tAAACCCGGGTTT' + ).collectFile(name: 'primers.txt', newLine: true, sort: false) + + input[0] = fasta.combine(primers).map { fasta_file, primers_file -> + [[ id:'test' ], fasta_file, primers_file] + } + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(sanitizeOutput(process.out)).match() } + ) + } + } +} diff --git a/modules/nf-core/primerprospector/analyzeprimers/tests/main.nf.test.snap b/modules/nf-core/primerprospector/analyzeprimers/tests/main.nf.test.snap new file mode 100644 index 00000000000..2094eaf51c3 --- /dev/null +++ b/modules/nf-core/primerprospector/analyzeprimers/tests/main.nf.test.snap @@ -0,0 +1,63 @@ +{ + "synthetic - fasta primers - hits plots - stub": { + "content": [ + { + "hits": [ + [ + { + "id": "test" + }, + "test_hits.txt:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "plots": [ + [ + { + "id": "test" + }, + "test.ps:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "versions_primerprospector": [ + [ + "PRIMERPROSPECTOR_ANALYZEPRIMERS", + "primerprospector", + "1.0.1" + ] + ] + } + ], + "timestamp": "2026-05-12T11:08:06.296032028", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.0" + } + }, + "synthetic - fasta primers - hits plots": { + "content": [ + [ + [ + { + "id": "test" + }, + "test_hits.txt:md5,5ad64787f36ad9ecea34d8bcd912d164" + ] + ], + true, + { + "versions_primerprospector": [ + [ + "PRIMERPROSPECTOR_ANALYZEPRIMERS", + "primerprospector", + "1.0.1" + ] + ] + } + ], + "timestamp": "2026-05-12T11:07:58.777832191", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.0" + } + } +} \ No newline at end of file