Skip to content

Commit 7e7d525

Browse files
authored
Add ability to define templates library at module level (#2332)
Signed-off-by: Manuele Simi <[email protected]>
1 parent bc769b1 commit 7e7d525

File tree

9 files changed

+175
-17
lines changed

9 files changed

+175
-17
lines changed

.github/workflows/build.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,9 @@ jobs:
102102
AZURE_STORAGE_ACCOUNT_KEY: ${{ secrets.AZURE_STORAGE_ACCOUNT_KEY }}
103103
AZURE_BATCH_ACCOUNT_KEY: ${{ secrets.AZURE_BATCH_ACCOUNT_KEY }}
104104

105-
- name: Publish
105+
- name: Publish tests report
106106
if: failure()
107107
uses: actions/upload-artifact@v2
108108
with:
109-
name: test-reports
110-
path: "*build/reports/tests/test"
109+
name: test-reports-jdk-${{ matrix.java_version }}
110+
path: "**/build/reports/tests/test"

docs/dsl2.rst

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,7 @@ Multiple inclusions
354354
-------------------
355355

356356
A Nextflow script allows the inclusion of any number of modules. When multiple
357-
components need to be included from the some module script, the component names can be
357+
components need to be included from the same module script, the component names can be
358358
specified in the same inclusion using the curly brackets notation as shown below::
359359

360360
include { foo; bar } from './some/module'
@@ -447,6 +447,66 @@ The above snippet prints::
447447
Finally the include option ``params`` allows the specification of one or more parameters without
448448
inheriting any value from the external environment.
449449

450+
.. _module-templates:
451+
452+
Module templates
453+
-----------------
454+
The module script can be defined in an external :ref:`template <process-template>` file. With DSL2 the template file
455+
can be placed under the ``templates`` directory where the module script is located.
456+
457+
For example, let's suppose to have a project L with a module script defining 2 processes (P1 and P2) and both use templates.
458+
The template files can be made available under the local ``templates`` directory::
459+
460+
Project L
461+
|-myModules.nf
462+
|-templates
463+
|-P1-template.sh
464+
|-P2-template.sh
465+
466+
Then, we have a second project A with a workflow that includes P1 and P2::
467+
468+
Pipeline A
469+
|-main.nf
470+
471+
Finally, we have a third project B with a workflow that includes again P1 and P2::
472+
473+
Pipeline B
474+
|-main.nf
475+
476+
With the possibility to keep the template files inside the project L, A and B can use the modules defined in L without any changes.
477+
A future prject C would do the same, just cloning L (if not available on the system) and including its module script.
478+
479+
Beside promoting sharing modules across pipelines, there are several advantages in keeping the module template under the script path::
480+
1 - module components are *self-contained*
481+
2 - module components can be tested independently from the pipeline(s) importing them
482+
3 - it is possible to create libraries of module components
483+
484+
Ultimately, having multiple template locations allows a more structured organization within the same project. If a project
485+
has several module components, and all them use templates, the project could group module scripts and their templates as needed. For example::
486+
487+
baseDir
488+
|-main.nf
489+
|-Phase0-Modules
490+
|-mymodules1.nf
491+
|-mymodules2.nf
492+
|-templates
493+
|-P1-template.sh
494+
|-P2-template.sh
495+
|-Phase1-Modules
496+
|-mymodules3.nf
497+
|-mymodules4.nf
498+
|-templates
499+
|-P3-template.sh
500+
|-P4-template.sh
501+
|-Phase2-Modules
502+
|-mymodules5.nf
503+
|-mymodules6.nf
504+
|-templates
505+
|-P5-template.sh
506+
|-P6-template.sh
507+
|-P7-template.sh
508+
509+
450510
Channel forking
451511
===============
452512

docs/process.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,11 @@ as shown below::
235235
Nextflow looks for the ``my_script.sh`` template file in the directory ``templates`` that must exist in the same folder
236236
where the Nextflow script file is located (any other location can be provided by using an absolute template path).
237237

238+
.. note::
239+
When using :ref:`DSL2 <dsl2-page>` Nextflow looks for the specified file name also in the ``templates`` directory
240+
located in the same folder where the module script is placed. See :ref:`module templates <module-templates>`.
241+
242+
238243
The template script can contain any piece of code that can be executed by the underlying system. For example::
239244

240245
#!/bin/bash

modules/nextflow/src/main/groovy/nextflow/datasource/SraExplorer.groovy

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,14 @@ class SraExplorer {
109109
return target
110110
}
111111

112-
protected Map getEnv() { System.getenv() }
112+
protected Map env() {
113+
return System.getenv()
114+
}
115+
116+
protected Map config() {
117+
final session = Global.session as Session
118+
return session.getConfig()
119+
}
113120

114121
protected Path getCacheFolder() {
115122
if( cacheFolder )
@@ -120,10 +127,9 @@ class SraExplorer {
120127
}
121128

122129
protected String getConfigApiKey() {
123-
def session = Global.session as Session
124-
def result = session ?.config ?. navigate('ncbi.apiKey')
130+
def result = config().navigate('ncbi.apiKey')
125131
if( !result )
126-
result = getEnv().get('NCBI_API_KEY')
132+
result = env().get('NCBI_API_KEY')
127133
if( !result )
128134
log.warn1("Define the NCBI_API_KEY env variable to use NCBI search service -- Read more https://ncbiinsights.ncbi.nlm.nih.gov/2017/11/02/new-api-keys-for-the-e-utilities/")
129135
return result

modules/nextflow/src/main/groovy/nextflow/processor/TaskContext.groovy

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,12 @@
1717

1818
package nextflow.processor
1919

20+
import nextflow.NF
21+
import nextflow.script.ScriptMeta
22+
2023
import java.nio.file.Path
2124
import java.nio.file.Paths
25+
import java.nio.file.Files
2226
import java.util.concurrent.atomic.AtomicBoolean
2327

2428
import com.esotericsoftware.kryo.io.Input
@@ -303,14 +307,22 @@ class TaskContext implements Map<String,Object>, Cloneable {
303307
if( !path )
304308
throw new ProcessException("Process `$name` missing template name")
305309

306-
if( !(path instanceof Path) )
310+
if( path !instanceof Path )
307311
path = Paths.get(path.toString())
308312

309313
// if the path is already absolute just return it
310314
if( path.isAbsolute() )
311315
return path
312316

313-
// otherwise make
317+
// make from the module dir
318+
def module = NF.isDsl2Final() ? ScriptMeta.get(this.script)?.getModuleDir() : null
319+
if( module ) {
320+
def target = module.resolve('templates').resolve(path)
321+
if (Files.exists(target))
322+
return target
323+
}
324+
325+
// otherwise make from the base dir
314326
def base = Global.session.baseDir
315327
if( base )
316328
return base.resolve('templates').resolve(path)

modules/nextflow/src/main/groovy/nextflow/script/BaseScript.groovy

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ abstract class BaseScript extends Script implements ExecutionContext {
100100
binding.setVariable( 'workflow', session.workflowMetadata )
101101
binding.setVariable( 'nextflow', NextflowMeta.instance )
102102
binding.setVariable('launchDir', Paths.get('./').toRealPath())
103-
binding.setVariable('moduleDir', meta.scriptPath?.parent )
103+
binding.setVariable('moduleDir', meta.moduleDir )
104104
}
105105

106106
protected process( String name, Closure<BodyDef> body ) {

modules/nextflow/src/main/groovy/nextflow/script/ScriptMeta.groovy

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ class ScriptMeta {
4747

4848
static ScriptMeta get(BaseScript script) {
4949
if( !script ) throw new IllegalStateException("Missing current script context")
50-
REGISTRY.get(script)
50+
return REGISTRY.get(script)
5151
}
5252

5353
static Set<String> allProcessNames() {
@@ -71,7 +71,7 @@ class ScriptMeta {
7171
/** the script {@link Class} object */
7272
private Class<? extends BaseScript> clazz
7373

74-
/** The location path from there the script has been loaded */
74+
/** The location path from where the script has been loaded */
7575
private Path scriptPath
7676

7777
/** The list of function, procs and workflow defined in this script */
@@ -87,6 +87,8 @@ class ScriptMeta {
8787

8888
Path getScriptPath() { scriptPath }
8989

90+
Path getModuleDir () { scriptPath?.parent }
91+
9092
String getScriptName() { clazz.getName() }
9193

9294
boolean isModule() { module }

modules/nextflow/src/test/groovy/nextflow/datasource/SraExplorerTest.groovy

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,8 @@ class SraExplorerTest extends Specification {
197197
when:
198198
def result = slurper.getConfigApiKey()
199199
then:
200-
1 * slurper.getEnv() >> [NCBI_API_KEY: '1bc']
200+
1 * slurper.config() >> [:]
201+
1 * slurper.env() >> [NCBI_API_KEY: '1bc']
201202
then:
202203
result == '1bc'
203204
}

modules/nextflow/src/test/groovy/nextflow/processor/TaskContextTest.groovy

Lines changed: 75 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,21 @@
1717

1818
package nextflow.processor
1919

20+
import java.nio.file.Files
2021
import java.nio.file.Paths
2122

23+
import groovy.runtime.metaclass.DelegatingPlugin
24+
import groovy.runtime.metaclass.NextflowDelegatingMetaClass
2225
import groovy.transform.InheritConstructors
26+
import nextflow.Global
27+
import nextflow.NF
28+
import nextflow.NextflowMeta
29+
import nextflow.Session
2330
import nextflow.script.BaseScript
31+
import nextflow.script.BodyDef
2432
import nextflow.script.ProcessConfig
2533
import nextflow.script.ScriptBinding
26-
import nextflow.script.BodyDef
34+
import nextflow.script.ScriptMeta
2735
import nextflow.util.BlankSeparatedList
2836
import nextflow.util.Duration
2937
import nextflow.util.MemoryUnit
@@ -34,6 +42,10 @@ import spock.lang.Specification
3442
*/
3543
class TaskContextTest extends Specification {
3644

45+
def setupSpec() {
46+
NF.init()
47+
}
48+
3749
def 'should save and read TaskContext object' () {
3850

3951
setup:
@@ -76,8 +88,8 @@ class TaskContextTest extends Specification {
7688

7789
setup:
7890
def bind = new ScriptBinding([x:1, y:2])
79-
def script = new MockScript(bind)
80-
91+
def script = Mock(BaseScript) { getBinding() >> bind }
92+
and:
8193
def local = [p:3, q:4, path: Paths.get('some/path')]
8294
def delegate = new TaskContext( script, local, 'hola' )
8395

@@ -139,6 +151,66 @@ class TaskContextTest extends Specification {
139151

140152
}
141153

154+
155+
def 'should resolve absolute paths as template paths' () {
156+
given:
157+
def temp = Files.createTempDirectory('test')
158+
and:
159+
Global.session = Mock(Session) { getBaseDir() >> temp }
160+
and:
161+
def holder = [:]
162+
def script = Mock(BaseScript)
163+
TaskContext context = Spy(TaskContext, constructorArgs: [script, holder, 'proc_1'])
164+
165+
when:
166+
// an absolute path is specified
167+
def absolutePath = Paths.get('/some/template.txt')
168+
def result = context.template(absolutePath)
169+
then:
170+
// the path is returned
171+
result == absolutePath
172+
173+
when:
174+
// an absolute string path is specified
175+
result = context.template('/some/template.txt')
176+
then:
177+
// the path is returned
178+
result == absolutePath
179+
180+
when:
181+
// when a rel file path is matched against the project
182+
// template dir, it's returned as the template file
183+
temp.resolve('templates').mkdirs()
184+
temp.resolve('templates/foo.txt').text = 'echo hello'
185+
and:
186+
result = context.template('foo.txt')
187+
then:
188+
result == temp.resolve('templates/foo.txt')
189+
190+
when:
191+
// it's a DSL2 module
192+
NextflowMeta.instance.enableDsl2()
193+
NextflowDelegatingMetaClass.plugin = Mock(DelegatingPlugin) { operatorNames() >> new HashSet<String>() }
194+
def meta = ScriptMeta.register(script)
195+
meta.setScriptPath(temp.resolve('modules/my-module/main.nf'))
196+
and:
197+
// the module has a nested `templates` directory
198+
temp.resolve('modules/my-module/templates').mkdirs()
199+
temp.resolve('modules/my-module/templates/foo.txt').text = 'echo Hola'
200+
and:
201+
// the template is a relative path
202+
result = context.template('foo.txt')
203+
then:
204+
// the path is resolved against the module templates
205+
result == temp.resolve('modules/my-module/templates/foo.txt')
206+
207+
cleanup:
208+
NextflowDelegatingMetaClass.plugin = null
209+
NextflowMeta.instance.disableDsl2()
210+
temp?.deleteDir()
211+
}
212+
213+
142214
}
143215

144216
@InheritConstructors

0 commit comments

Comments
 (0)