Skip to content
Closed
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
Expand Up @@ -105,6 +105,7 @@ import nextflow.util.CacheHelper
import nextflow.util.Escape
import nextflow.util.LockManager
import nextflow.util.LoggerHelper
import nextflow.util.NullPath
import nextflow.util.TestOnly
import org.codehaus.groovy.control.CompilerConfiguration
import org.codehaus.groovy.control.customizers.ASTTransformationCustomizer
Expand Down Expand Up @@ -1595,6 +1596,10 @@ class TaskProcessor {
def path = param.glob ? splitter.strip(filePattern) : filePattern
def file = workDir.resolve(path)
def exists = checkFileExists(file, param.followLinks)
if( !exists && param.isNullable() ) {
file = new NullPath(path)
exists = true
}
if( exists )
result = [file]
else
Expand All @@ -1612,7 +1617,7 @@ class TaskProcessor {
}
}

if( !param.isValidArity(allFiles.size()) )
if( !param.isValidArity(allFiles) )
throw new IllegalArityException("Incorrect number of output files for process `${safeTaskName(task)}` -- expected ${param.arity}, found ${allFiles.size()}")

task.setOutput( param, allFiles.size()==1 && param.isSingle() ? allFiles[0] : allFiles )
Expand Down Expand Up @@ -1816,7 +1821,10 @@ class TaskProcessor {
return new FileHolder(source, result)
}

protected Path normalizeToPath( obj ) {
protected Path normalizeToPath( obj, boolean nullable=false ) {
if( obj instanceof NullPath && !nullable )
throw new ProcessUnrecoverableException("Path value cannot be null")

if( obj instanceof Path )
return obj

Expand All @@ -1839,7 +1847,7 @@ class TaskProcessor {
throw new ProcessUnrecoverableException("Not a valid path value: '$str'")
}

protected List<FileHolder> normalizeInputToFiles( Object obj, int count, boolean coerceToPath, FilePorter.Batch batch ) {
protected List<FileHolder> normalizeInputToFiles( Object obj, int count, boolean coerceToPath, boolean nullable, FilePorter.Batch batch ) {

Collection allItems = obj instanceof Collection ? obj : [obj]
def len = allItems.size()
Expand All @@ -1849,7 +1857,7 @@ class TaskProcessor {
for( def item : allItems ) {

if( item instanceof Path || coerceToPath ) {
def path = normalizeToPath(item)
def path = normalizeToPath(item, nullable)
def target = executor.isForeignFile(path) ? batch.addToForeign(path) : path
def holder = new FileHolder(target)
files << holder
Expand Down Expand Up @@ -2063,11 +2071,15 @@ class TaskProcessor {
final param = entry.getKey()
final val = entry.getValue()
final fileParam = param as FileInParam
final normalized = normalizeInputToFiles(val, count, fileParam.isPathQualifier(), batch)
final normalized = normalizeInputToFiles(val, count, fileParam.isPathQualifier(), param.isNullable(), batch)
final resolved = expandWildcards( fileParam.getFilePattern(ctx), normalized )

if( !param.isValidArity(resolved.size()) )
throw new IllegalArityException("Incorrect number of input files for process `${safeTaskName(task)}` -- expected ${param.arity}, found ${resolved.size()}")
if( !param.isValidArity(resolved) ) {
final msg = param.isNullable()
? "expected a nullable file (0..1) but a list was provided"
: "expected ${param.arity}, found ${resolved.size()}"
throw new IllegalArityException("Incorrect number of input files for process `${safeTaskName(task)}` -- ${msg}")
}

ctx.put( param.name, singleItemOrList(resolved, param.isSingle(), task.type) )
count += resolved.size()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,35 @@ trait ArityParam {
throw new IllegalArityException("Path arity should be a number (e.g. '1') or a range (e.g. '1..*')")
}

/**
* Determine whether a null file is allowed.
*/
boolean isNullable() {
return arity && arity.min == 0 && arity.max == 1
}

/**
* Determine whether a single output file should be unwrapped.
*/
boolean isSingle() {
return !arity || arity.max == 1
}

boolean isValidArity(int size) {
return !arity || arity.contains(size)
/**
* Determine whether a collection of files has valid arity.
*
* If the param is nullable, there should be exactly one file (either
* a real file or a null file)
*
* @param files
*/
boolean isValidArity(Collection files) {
if( !arity )
return true

return isNullable()
? files.size() == 1
: arity.contains(files.size())
}

@EqualsAndHashCode
Expand All @@ -72,12 +92,10 @@ trait ArityParam {
int max

Range(int min, int max) {
if( min<0 )
throw new IllegalArityException("Path arity min value must be greater or equals to 0")
if( max<1 )
throw new IllegalArityException("Path arity max value must be greater or equals to 1")
if( min==0 && max==1 )
throw new IllegalArityException("Path arity 0..1 is not allowed")
if( min < 0 )
throw new IllegalArityException("Path arity min value must be at least 0")
if( max < 1 )
throw new IllegalArityException("Path arity max value must be at least 1")
this.min = min
this.max = max
}
Expand Down
41 changes: 41 additions & 0 deletions modules/nextflow/src/main/groovy/nextflow/util/NullPath.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright 2013-2023, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package nextflow.util

import java.nio.file.Path
import java.nio.file.Paths

import groovy.transform.EqualsAndHashCode
import groovy.transform.PackageScope
import groovy.util.logging.Slf4j

/**
*
* @author Ben Sherman <[email protected]>
*/
@EqualsAndHashCode
@Slf4j
class NullPath implements Path {

@PackageScope
@Delegate
Path delegate

NullPath(String path) {
delegate = Paths.get(path)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -839,7 +839,7 @@ class TaskProcessorTest extends Specification {
def proc = new TaskProcessor(); proc.executor = executor

when:
def result = proc.normalizeInputToFiles(PATH.toString(), 0, true, batch)
def result = proc.normalizeInputToFiles(PATH.toString(), 0, true, false, batch)
then:
1 * executor.isForeignFile(PATH) >> false
0 * batch.addToForeign(PATH) >> null
Expand Down Expand Up @@ -1043,7 +1043,7 @@ class TaskProcessorTest extends Specification {

where:
FILE_NAME | FILE_VALUE | ARITY | ERROR
'file.txt' | [] | '0' | 'Path arity max value must be greater or equals to 1'
'file.txt' | [] | '0' | 'Path arity max value must be at least 1'
'file.txt' | [] | '1' | 'Incorrect number of input files for process `foo` -- expected 1, found 0'
'f*' | [] | '1..*' | 'Incorrect number of input files for process `foo` -- expected 1..*, found 0'
'f*' | '/some/file.txt' | '2..*' | 'Incorrect number of input files for process `foo` -- expected 2..*, found 1'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,15 @@ class ArityParamTest extends Specification {
then:
param.arity.min == MIN
param.arity.max == MAX
param.isNullable() == NULLABLE
param.isSingle() == SINGLE

where:
VALUE | SINGLE | MIN | MAX
'1' | true | 1 | 1
'1..*' | false | 1 | Integer.MAX_VALUE
'0..*' | false | 0 | Integer.MAX_VALUE
VALUE | NULLABLE | SINGLE | MIN | MAX
'1' | false | true | 1 | 1
'0..1' | true | true | 0 | 1
'1..*' | false | false | 1 | Integer.MAX_VALUE
'0..*' | false | false | 0 | Integer.MAX_VALUE
}

@Unroll
Expand All @@ -58,6 +60,7 @@ class ArityParamTest extends Specification {
where:
MIN | MAX | TWO | STRING
1 | 1 | false | '1'
0 | 1 | false | '0..1'
1 | Integer.MAX_VALUE | true | '1..*'
}

Expand Down
18 changes: 18 additions & 0 deletions tests/checks/process-arity-fails.nf/.checks
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
set +e

#
# run normal mode
#
echo ''
$NXF_RUN
[[ $? == 0 ]] && exit 1


#
# RESUME mode
#
echo ''
$NXF_RUN -resume
[[ $? == 0 ]] && exit 1

exit 0
19 changes: 19 additions & 0 deletions tests/process-arity-fails.nf
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/usr/bin/env nextflow

process foo {
output:
path('output.txt', arity: '0..1')
script:
true
}

process bar {
input:
path(file)
script:
true
}

workflow {
foo | bar
}
6 changes: 6 additions & 0 deletions tests/process-arity.nf
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ process foo {
path('one.txt', arity: '1')
path('pair_*.txt', arity: '2')
path('many_*.txt', arity: '1..*')
path('opt_one.txt', arity: '0..1')
path('opt_many_*.txt', arity: '0..*')
script:
"""
echo 'one' > one.txt
Expand All @@ -21,11 +23,15 @@ process bar {
path('one.txt', arity: '1')
path('pair_*.txt', arity: '2')
path('many_*.txt', arity: '1..*')
path('opt_one.txt', arity: '0..1')
path('opt_many_*.txt', arity: '0..*')
script:
"""
cat one.txt
cat pair_*.txt
cat many_*.txt
cat opt_one.txt || true
cat opt_many_*.txt || true
"""
}

Expand Down