@@ -162,6 +162,15 @@ class CommandLineToolFactory(object):
162
162
if isinstance (cmd , (list , tuple )) else shlex .split (cmd ),
163
163
)
164
164
165
+ explicit_inputs = attr .ib (
166
+ default = [],
167
+ converter = lambda paths : [Path (path ).resolve () for path in paths ]
168
+ )
169
+ explicit_outputs = attr .ib (
170
+ default = [],
171
+ converter = lambda paths : [Path (path ).resolve () for path in paths ]
172
+ )
173
+
165
174
directory = attr .ib (
166
175
default = '.' ,
167
176
converter = lambda path : Path (path ).resolve (),
@@ -183,7 +192,7 @@ class CommandLineToolFactory(object):
183
192
successCodes = attr .ib (default = attr .Factory (list )) # list(int)
184
193
185
194
def __attrs_post_init__ (self ):
186
- """Derive basic informations ."""
195
+ """Derive basic information ."""
187
196
self .baseCommand , detect = self .split_command_and_args ()
188
197
self .arguments = []
189
198
self .inputs = []
@@ -218,6 +227,10 @@ def __attrs_post_init__(self):
218
227
else :
219
228
self .inputs .append (input_ )
220
229
230
+ if self .explicit_inputs :
231
+ for input in self .find_explicit_inputs ():
232
+ self .inputs .append (input )
233
+
221
234
def generate_tool (self ):
222
235
"""Return an instance of command line tool."""
223
236
return CommandLineTool (
@@ -232,24 +245,11 @@ def generate_tool(self):
232
245
)
233
246
234
247
@contextmanager
235
- def watch (self , client , no_output = False , outputs = None ):
248
+ def watch (self , client , no_output = False ):
236
249
"""Watch a Renku repository for changes to detect outputs."""
237
250
tool = self .generate_tool ()
238
251
repo = client .repo
239
252
240
- if outputs :
241
- directories = [
242
- output for output in outputs if Path (output ).is_dir ()
243
- ]
244
-
245
- client .repo .git .rm (
246
- * outputs , r = True , force = True , ignore_unmatch = True
247
- )
248
- client .repo .index .commit ('renku: automatic removal of outputs' )
249
-
250
- for directory in directories :
251
- Path (directory ).mkdir (parents = True , exist_ok = True )
252
-
253
253
# NOTE consider to use git index instead
254
254
existing_directories = {
255
255
str (p .relative_to (client .path ))
@@ -261,6 +261,10 @@ def watch(self, client, no_output=False, outputs=None):
261
261
if repo :
262
262
# List of all output paths.
263
263
paths = []
264
+
265
+ inputs = {input .id : input for input in self .inputs }
266
+ outputs = list (tool .outputs )
267
+
264
268
# Keep track of unmodified output files.
265
269
unmodified = set ()
266
270
@@ -277,9 +281,6 @@ def watch(self, client, no_output=False, outputs=None):
277
281
from renku .cli ._graph import _safe_path
278
282
candidates = {path for path in candidates if _safe_path (path )}
279
283
280
- inputs = {input .id : input for input in self .inputs }
281
- outputs = list (tool .outputs )
282
-
283
284
for output , input , path in self .guess_outputs (candidates ):
284
285
outputs .append (output )
285
286
paths .append (path )
@@ -292,27 +293,48 @@ def watch(self, client, no_output=False, outputs=None):
292
293
293
294
for stream_name in ('stdout' , 'stderr' ):
294
295
stream = getattr (self , stream_name )
295
- if stream and stream not in candidates :
296
+ if (
297
+ stream and stream not in candidates and
298
+ Path (stream ).resolve () not in self .explicit_outputs
299
+ ):
296
300
unmodified .add (stream )
297
301
elif stream :
298
302
paths .append (stream )
299
303
304
+ if self .explicit_outputs :
305
+ last_output_id = len (outputs )
306
+
307
+ for output , input , path in self .find_explicit_outputs (
308
+ last_output_id
309
+ ):
310
+ outputs .append (output )
311
+ paths .append (path )
312
+
313
+ if input is not None :
314
+ if input .id not in inputs : # pragma: no cover
315
+ raise RuntimeError ('Inconsistent input name.' )
316
+
317
+ inputs [input .id ] = input
318
+
300
319
if unmodified :
301
320
raise errors .UnmodifiedOutputs (repo , unmodified )
302
321
303
322
if not no_output and not paths :
304
323
raise errors .OutputsNotFound (repo , inputs .values ())
305
324
325
+ if client .has_external_storage :
326
+ client .track_paths_in_storage (* paths )
327
+
306
328
tool .inputs = list (inputs .values ())
307
329
tool .outputs = outputs
308
330
309
- client .track_paths_in_storage (* paths )
310
-
311
331
# Requirement detection can be done anytime.
312
332
from .process_requirements import InitialWorkDirRequirement , \
313
333
InlineJavascriptRequirement
314
334
initial_work_dir_requirement = InitialWorkDirRequirement .from_tool (
315
- tool , existing_directories = existing_directories
335
+ tool ,
336
+ existing_directories = existing_directories ,
337
+ working_dir = self .working_dir
316
338
)
317
339
if initial_work_dir_requirement :
318
340
tool .requirements .extend ([
@@ -521,25 +543,28 @@ def guess_outputs(self, paths):
521
543
str (input_path / path )
522
544
for path in tree .get (input_path , default = [])
523
545
}
524
- content = {
525
- str (path )
526
- for path in input_path .rglob ('*' )
527
- if not path .is_dir () and path .name != '.gitkeep'
528
- }
529
- extra_paths = content - subpaths
530
- if extra_paths :
531
- raise errors .InvalidOutputPath (
532
- 'The output directory "{0}" is not empty. \n \n '
533
- 'Delete existing files before running the command:'
534
- '\n (use "git rm <file>..." to remove them first)'
535
- '\n \n ' .format (input_path ) + '\n ' .join (
536
- '\t ' + click .style (path , fg = 'yellow' )
537
- for path in extra_paths
538
- ) + '\n \n '
539
- 'Once you have removed files that should be used '
540
- 'as outputs,\n '
541
- 'you can safely rerun the previous command.'
542
- )
546
+ if input_path .resolve () not in self .explicit_outputs :
547
+ content = {
548
+ str (path )
549
+ for path in input_path .rglob ('*' )
550
+ if not path .is_dir () and path .name != '.gitkeep'
551
+ }
552
+ extra_paths = content - subpaths
553
+ if extra_paths :
554
+ raise errors .InvalidOutputPath (
555
+ 'The output directory "{0}" is not empty. \n \n '
556
+ 'Delete existing files before running the '
557
+ 'command:'
558
+ '\n (use "git rm <file>..." to remove them '
559
+ 'first)'
560
+ '\n \n ' .format (input_path ) + '\n ' .join (
561
+ '\t ' + click .style (path , fg = 'yellow' )
562
+ for path in extra_paths
563
+ ) + '\n \n '
564
+ 'Once you have removed files that should be used '
565
+ 'as outputs,\n '
566
+ 'you can safely rerun the previous command.'
567
+ )
543
568
544
569
# Remove files from the input directory
545
570
paths = [path for path in paths if path not in subpaths ]
@@ -611,3 +636,83 @@ def guess_outputs(self, paths):
611
636
outputBinding = dict (glob = glob , ),
612
637
), None , glob
613
638
)
639
+
640
+ def find_explicit_inputs (self ):
641
+ """Yield explicit inputs and command line input bindings if any."""
642
+ input_paths = [
643
+ input .default .path
644
+ for input in self .inputs if input .type in PATH_OBJECTS
645
+ ]
646
+ input_id = len (self .inputs ) + len (self .arguments )
647
+
648
+ for explicit_input in self .explicit_inputs :
649
+ if explicit_input in input_paths :
650
+ continue
651
+
652
+ try :
653
+ explicit_input .relative_to (self .working_dir )
654
+ except ValueError :
655
+ raise errors .InvalidInputPath (
656
+ 'The input file or directory is not in the repository.'
657
+ '\n \n \t ' + click .style (str (explicit_input ), fg = 'yellow' ) +
658
+ '\n \n '
659
+ )
660
+ if self .file_candidate (explicit_input ) is None :
661
+ raise errors .InvalidInputPath (
662
+ 'The input file or directory does not exist.'
663
+ '\n \n \t ' + click .style (str (explicit_input ), fg = 'yellow' ) +
664
+ '\n \n '
665
+ )
666
+ input_id += 1
667
+ default , type , _ = self .guess_type (explicit_input )
668
+ # Explicit inputs are either File or Directory
669
+ assert type in PATH_OBJECTS
670
+ # The inputBinging is None because these inputs won't
671
+ # appear on command-line
672
+ yield CommandInputParameter (
673
+ id = 'input_{0}' .format (input_id ),
674
+ type = type ,
675
+ default = default ,
676
+ inputBinding = None
677
+ )
678
+
679
+ def find_explicit_outputs (self , starting_output_id ):
680
+ """Yield explicit output and changed command input parameter."""
681
+ inputs = {
682
+ str (i .default .path .relative_to (self .working_dir )): i
683
+ for i in self .inputs if i .type in PATH_OBJECTS
684
+ }
685
+ output_id = starting_output_id
686
+
687
+ for path in self .explicit_outputs :
688
+ if self .file_candidate (path ) is None :
689
+ raise errors .InvalidOutputPath (
690
+ 'The output file or directory does not exist.'
691
+ '\n \n \t ' + click .style (str (path ), fg = 'yellow' ) + '\n \n '
692
+ )
693
+
694
+ output_path = str (path .relative_to (self .working_dir ))
695
+ type = 'Directory' if path .is_dir () else 'File'
696
+ if output_path in inputs :
697
+ # change input type to note that it is also an output
698
+ input = inputs [output_path ]
699
+ input = attr .evolve (input , type = 'string' , default = output_path )
700
+ yield (
701
+ CommandOutputParameter (
702
+ id = 'output_{0}' .format (output_id ),
703
+ type = type ,
704
+ outputBinding = dict (
705
+ glob = '$(inputs.{0})' .format (input .id )
706
+ )
707
+ ), input , output_path
708
+ )
709
+ else :
710
+ yield (
711
+ CommandOutputParameter (
712
+ id = 'output_{0}' .format (output_id ),
713
+ type = type ,
714
+ outputBinding = dict (glob = str (output_path ))
715
+ ), None , output_path
716
+ )
717
+
718
+ output_id += 1
0 commit comments