Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
79 changes: 79 additions & 0 deletions modules/nf-core/primerprospector/analyzeprimers/main.nf
Original file line number Diff line number Diff line change
@@ -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"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For multiple primers in the input primer file this will cause an error or overwrite some results. Rather all the files should be emitted in the hits output channel. If prefix renaming is desired then it needs to be performed on each of the produced hits files.

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
"""
}
89 changes: 89 additions & 0 deletions modules/nf-core/primerprospector/analyzeprimers/meta.yml
Original file line number Diff line number Diff line change
@@ -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"
77 changes: 77 additions & 0 deletions modules/nf-core/primerprospector/analyzeprimers/tests/main.nf.test
Original file line number Diff line number Diff line change
@@ -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() }
)
}
}
}
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
Loading