10
10
import json
11
11
import logging
12
12
import os
13
- import PyInquirer
13
+ import prompt_toolkit
14
+ import questionary
14
15
import re
15
16
import subprocess
16
17
import textwrap
20
21
21
22
log = logging .getLogger (__name__ )
22
23
23
- #
24
- # NOTE: When PyInquirer 1.0.3 is released we can capture keyboard interruptions
25
- # in a nicer way # with the raise_keyboard_interrupt=True argument in the PyInquirer.prompt() calls
26
- # It also allows list selections to have a default set.
27
- #
28
- # Until then we have workarounds:
29
- # * Default list item is moved to the top of the list
30
- # * We manually raise a KeyboardInterrupt if we get None back from a question
31
- #
24
+ # Custom style for questionary
25
+ nfcore_question_style = prompt_toolkit .styles .Style (
26
+ [
27
+ ("qmark" , "fg:ansiblue bold" ), # token in front of the question
28
+ ("question" , "bold" ), # question text
29
+ ("answer" , "fg:ansigreen nobold" ), # submitted answer text behind the question
30
+ ("pointer" , "fg:ansiyellow bold" ), # pointer used in select and checkbox prompts
31
+ ("highlighted" , "fg:ansiblue bold" ), # pointed-at choice in select and checkbox prompts
32
+ ("selected" , "fg:ansigreen noreverse" ), # style for a selected item of a checkbox
33
+ ("separator" , "fg:ansiblack" ), # separator in lists
34
+ ("instruction" , "" ), # user instructions for select, rawselect, checkbox
35
+ ("text" , "" ), # plain text
36
+ ("disabled" , "fg:gray italic" ), # disabled choices for select and checkbox prompts
37
+ ]
38
+ )
32
39
33
40
34
41
class Launch (object ):
@@ -256,11 +263,9 @@ def prompt_web_gui(self):
256
263
"name" : "use_web_gui" ,
257
264
"message" : "Choose launch method" ,
258
265
"choices" : ["Web based" , "Command line" ],
266
+ "default" : "Web based" ,
259
267
}
260
- answer = PyInquirer .prompt ([question ])
261
- # TODO: use raise_keyboard_interrupt=True when PyInquirer 1.0.3 is released
262
- if answer == {}:
263
- raise KeyboardInterrupt
268
+ answer = questionary .unsafe_prompt ([question ], style = nfcore_question_style )
264
269
return answer ["use_web_gui" ] == "Web based"
265
270
266
271
def launch_web_gui (self ):
@@ -347,14 +352,14 @@ def sanitise_web_response(self):
347
352
The web builder returns everything as strings.
348
353
Use the functions defined in the cli wizard to convert to the correct types.
349
354
"""
350
- # Collect pyinquirer objects for each defined input_param
351
- pyinquirer_objects = {}
355
+ # Collect questionary objects for each defined input_param
356
+ questionary_objects = {}
352
357
for param_id , param_obj in self .schema_obj .schema .get ("properties" , {}).items ():
353
- pyinquirer_objects [param_id ] = self .single_param_to_pyinquirer (param_id , param_obj , print_help = False )
358
+ questionary_objects [param_id ] = self .single_param_to_questionary (param_id , param_obj , print_help = False )
354
359
355
360
for d_key , definition in self .schema_obj .schema .get ("definitions" , {}).items ():
356
361
for param_id , param_obj in definition .get ("properties" , {}).items ():
357
- pyinquirer_objects [param_id ] = self .single_param_to_pyinquirer (param_id , param_obj , print_help = False )
362
+ questionary_objects [param_id ] = self .single_param_to_questionary (param_id , param_obj , print_help = False )
358
363
359
364
# Go through input params and sanitise
360
365
for params in [self .nxf_flags , self .schema_obj .input_params ]:
@@ -364,7 +369,7 @@ def sanitise_web_response(self):
364
369
del params [param_id ]
365
370
continue
366
371
# Run filter function on value
367
- filter_func = pyinquirer_objects .get (param_id , {}).get ("filter" )
372
+ filter_func = questionary_objects .get (param_id , {}).get ("filter" )
368
373
if filter_func is not None :
369
374
params [param_id ] = filter_func (params [param_id ])
370
375
@@ -396,19 +401,13 @@ def prompt_param(self, param_id, param_obj, is_required, answers):
396
401
"""Prompt for a single parameter"""
397
402
398
403
# Print the question
399
- question = self .single_param_to_pyinquirer (param_id , param_obj , answers )
400
- answer = PyInquirer .prompt ([question ])
401
- # TODO: use raise_keyboard_interrupt=True when PyInquirer 1.0.3 is released
402
- if answer == {}:
403
- raise KeyboardInterrupt
404
+ question = self .single_param_to_questionary (param_id , param_obj , answers )
405
+ answer = questionary .unsafe_prompt ([question ], style = nfcore_question_style )
404
406
405
407
# If required and got an empty reponse, ask again
406
408
while type (answer [param_id ]) is str and answer [param_id ].strip () == "" and is_required :
407
409
log .error ("'–-{}' is required" .format (param_id ))
408
- answer = PyInquirer .prompt ([question ])
409
- # TODO: use raise_keyboard_interrupt=True when PyInquirer 1.0.3 is released
410
- if answer == {}:
411
- raise KeyboardInterrupt
410
+ answer = questionary .unsafe_prompt ([question ], style = nfcore_question_style )
412
411
413
412
# Don't return empty answers
414
413
if answer [param_id ] == "" :
@@ -426,37 +425,39 @@ def prompt_group(self, group_id, group_obj):
426
425
Returns:
427
426
Dict of param_id:val answers
428
427
"""
429
- question = {
430
- "type" : "list" ,
431
- "name" : group_id ,
432
- "message" : group_obj .get ("title" , group_id ),
433
- "choices" : ["Continue >>" , PyInquirer .Separator ()],
434
- }
435
-
436
- for param_id , param in group_obj ["properties" ].items ():
437
- if not param .get ("hidden" , False ) or self .show_hidden :
438
- question ["choices" ].append (param_id )
439
-
440
- # Skip if all questions hidden
441
- if len (question ["choices" ]) == 2 :
442
- return {}
443
-
444
428
while_break = False
445
429
answers = {}
446
430
while not while_break :
431
+ question = {
432
+ "type" : "list" ,
433
+ "name" : group_id ,
434
+ "message" : group_obj .get ("title" , group_id ),
435
+ "choices" : ["Continue >>" , questionary .Separator ()],
436
+ }
437
+
438
+ for param_id , param in group_obj ["properties" ].items ():
439
+ if not param .get ("hidden" , False ) or self .show_hidden :
440
+ q_title = param_id
441
+ if param_id in answers :
442
+ q_title += " [{}]" .format (answers [param_id ])
443
+ elif "default" in param :
444
+ q_title += " [{}]" .format (param ["default" ])
445
+ question ["choices" ].append (questionary .Choice (title = q_title , value = param_id ))
446
+
447
+ # Skip if all questions hidden
448
+ if len (question ["choices" ]) == 2 :
449
+ return {}
450
+
447
451
self .print_param_header (group_id , group_obj )
448
- answer = PyInquirer .prompt ([question ])
449
- # TODO: use raise_keyboard_interrupt=True when PyInquirer 1.0.3 is released
450
- if answer == {}:
451
- raise KeyboardInterrupt
452
+ answer = questionary .unsafe_prompt ([question ], style = nfcore_question_style )
452
453
if answer [group_id ] == "Continue >>" :
453
454
while_break = True
454
455
# Check if there are any required parameters that don't have answers
455
456
for p_required in group_obj .get ("required" , []):
456
457
req_default = self .schema_obj .input_params .get (p_required , "" )
457
458
req_answer = answers .get (p_required , "" )
458
459
if req_default == "" and req_answer == "" :
459
- log .error ("'{}' is required." .format (p_required ))
460
+ log .error ("'-- {}' is required." .format (p_required ))
460
461
while_break = False
461
462
else :
462
463
param_id = answer [group_id ]
@@ -465,8 +466,8 @@ def prompt_group(self, group_id, group_obj):
465
466
466
467
return answers
467
468
468
- def single_param_to_pyinquirer (self , param_id , param_obj , answers = None , print_help = True ):
469
- """Convert a JSONSchema param to a PyInquirer question
469
+ def single_param_to_questionary (self , param_id , param_obj , answers = None , print_help = True ):
470
+ """Convert a JSONSchema param to a Questionary question
470
471
471
472
Args:
472
473
param_id: Parameter ID (string)
@@ -475,7 +476,7 @@ def single_param_to_pyinquirer(self, param_id, param_obj, answers=None, print_he
475
476
print_help: If description and help_text should be printed (bool)
476
477
477
478
Returns:
478
- Single PyInquirer dict, to be appended to questions list
479
+ Single Questionary dict, to be appended to questions list
479
480
"""
480
481
if answers is None :
481
482
answers = {}
@@ -530,7 +531,11 @@ def validate_number(val):
530
531
try :
531
532
if val .strip () == "" :
532
533
return True
533
- float (val )
534
+ fval = float (val )
535
+ if "minimum" in param_obj and fval < float (param_obj ["minimum" ]):
536
+ return "Must be greater than or equal to {}" .format (param_obj ["minimum" ])
537
+ if "maximum" in param_obj and fval > float (param_obj ["maximum" ]):
538
+ return "Must be less than or equal to {}" .format (param_obj ["maximum" ])
534
539
except ValueError :
535
540
return "Must be a number"
536
541
else :
@@ -568,46 +573,11 @@ def filter_integer(val):
568
573
569
574
question ["filter" ] = filter_integer
570
575
571
- if param_obj .get ("type" ) == "range" :
572
- # Validate range type
573
- def validate_range (val ):
574
- try :
575
- if val .strip () == "" :
576
- return True
577
- fval = float (val )
578
- if "minimum" in param_obj and fval < float (param_obj ["minimum" ]):
579
- return "Must be greater than or equal to {}" .format (param_obj ["minimum" ])
580
- if "maximum" in param_obj and fval > float (param_obj ["maximum" ]):
581
- return "Must be less than or equal to {}" .format (param_obj ["maximum" ])
582
- return True
583
- except ValueError :
584
- return "Must be a number"
585
-
586
- question ["validate" ] = validate_range
587
-
588
- # Filter returned value
589
- def filter_range (val ):
590
- if val .strip () == "" :
591
- return ""
592
- return float (val )
593
-
594
- question ["filter" ] = filter_range
595
-
596
576
if "enum" in param_obj :
597
577
# Use a selection list instead of free text input
598
578
question ["type" ] = "list"
599
579
question ["choices" ] = param_obj ["enum" ]
600
580
601
- # Validate enum from schema
602
- def validate_enum (val ):
603
- if val == "" :
604
- return True
605
- if val in param_obj ["enum" ]:
606
- return True
607
- return "Must be one of: {}" .format (", " .join (param_obj ["enum" ]))
608
-
609
- question ["validate" ] = validate_enum
610
-
611
581
# Validate pattern from schema
612
582
if "pattern" in param_obj :
613
583
@@ -620,21 +590,6 @@ def validate_pattern(val):
620
590
621
591
question ["validate" ] = validate_pattern
622
592
623
- # WORKAROUND - PyInquirer <1.0.3 cannot have a default position in a list
624
- # For now, move the default option to the top.
625
- # TODO: Delete this code when PyInquirer >=1.0.3 is released.
626
- if question ["type" ] == "list" and "default" in question :
627
- try :
628
- question ["choices" ].remove (question ["default" ])
629
- question ["choices" ].insert (0 , question ["default" ])
630
- except ValueError :
631
- log .warning (
632
- "Default value `{}` not found in list of choices: {}" .format (
633
- question ["default" ], ", " .join (question ["choices" ])
634
- )
635
- )
636
- ### End of workaround code
637
-
638
593
return question
639
594
640
595
def print_param_header (self , param_id , param_obj ):
0 commit comments