Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Swapped pyinquirer with questionary #799

Merged
merged 18 commits into from
Dec 2, 2020
Merged
Show file tree
Hide file tree
Changes from 4 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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## v1.13dev

* swapped PyInquirer with questionary for command line questions in launch.py
KevinMenden marked this conversation as resolved.
Show resolved Hide resolved

### Tools helper code

### Template
Expand Down
51 changes: 20 additions & 31 deletions nf_core/launch.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import json
import logging
import os
import PyInquirer
import questionary
import re
import subprocess
import textwrap
Expand All @@ -20,16 +20,6 @@

log = logging.getLogger(__name__)

#
# NOTE: When PyInquirer 1.0.3 is released we can capture keyboard interruptions
# in a nicer way # with the raise_keyboard_interrupt=True argument in the PyInquirer.prompt() calls
# It also allows list selections to have a default set.
#
# Until then we have workarounds:
# * Default list item is moved to the top of the list
# * We manually raise a KeyboardInterrupt if we get None back from a question
#


class Launch(object):
""" Class to hold config option to launch a pipeline """
Expand Down Expand Up @@ -257,8 +247,8 @@ def prompt_web_gui(self):
"message": "Choose launch method",
"choices": ["Web based", "Command line"],
}
answer = PyInquirer.prompt([question])
# TODO: use raise_keyboard_interrupt=True when PyInquirer 1.0.3 is released
answer = questionary.prompt([question])
# Raise keyboard interrupt
if answer == {}:
raise KeyboardInterrupt
return answer["use_web_gui"] == "Web based"
Expand Down Expand Up @@ -347,14 +337,14 @@ def sanitise_web_response(self):
The web builder returns everything as strings.
Use the functions defined in the cli wizard to convert to the correct types.
"""
# Collect pyinquirer objects for each defined input_param
pyinquirer_objects = {}
# Collect questionary objects for each defined input_param
questionare_objects = {}
KevinMenden marked this conversation as resolved.
Show resolved Hide resolved
for param_id, param_obj in self.schema_obj.schema.get("properties", {}).items():
pyinquirer_objects[param_id] = self.single_param_to_pyinquirer(param_id, param_obj, print_help=False)
questionare_objects[param_id] = self.single_param_to_questionary(param_id, param_obj, print_help=False)

for d_key, definition in self.schema_obj.schema.get("definitions", {}).items():
for param_id, param_obj in definition.get("properties", {}).items():
pyinquirer_objects[param_id] = self.single_param_to_pyinquirer(param_id, param_obj, print_help=False)
questionare_objects[param_id] = self.single_param_to_questionary(param_id, param_obj, print_help=False)

# Go through input params and sanitise
for params in [self.nxf_flags, self.schema_obj.input_params]:
Expand All @@ -364,7 +354,7 @@ def sanitise_web_response(self):
del params[param_id]
continue
# Run filter function on value
filter_func = pyinquirer_objects.get(param_id, {}).get("filter")
filter_func = questionare_objects.get(param_id, {}).get("filter")
if filter_func is not None:
params[param_id] = filter_func(params[param_id])

Expand Down Expand Up @@ -396,17 +386,17 @@ def prompt_param(self, param_id, param_obj, is_required, answers):
"""Prompt for a single parameter"""

# Print the question
question = self.single_param_to_pyinquirer(param_id, param_obj, answers)
answer = PyInquirer.prompt([question])
# TODO: use raise_keyboard_interrupt=True when PyInquirer 1.0.3 is released
question = self.single_param_to_questionary(param_id, param_obj, answers)
answer = questionary.prompt([question])
# Raise keyboard interrupg
KevinMenden marked this conversation as resolved.
Show resolved Hide resolved
if answer == {}:
raise KeyboardInterrupt

# If required and got an empty reponse, ask again
while type(answer[param_id]) is str and answer[param_id].strip() == "" and is_required:
log.error("'–-{}' is required".format(param_id))
answer = PyInquirer.prompt([question])
# TODO: use raise_keyboard_interrupt=True when PyInquirer 1.0.3 is released
answer = questionary.prompt([question])
# Raise keyboard interrupt
if answer == {}:
raise KeyboardInterrupt

Expand All @@ -430,7 +420,7 @@ def prompt_group(self, group_id, group_obj):
"type": "list",
"name": group_id,
"message": group_obj.get("title", group_id),
"choices": ["Continue >>", PyInquirer.Separator()],
"choices": ["Continue >>", questionary.Separator()],
}

for param_id, param in group_obj["properties"].items():
Expand All @@ -445,8 +435,8 @@ def prompt_group(self, group_id, group_obj):
answers = {}
while not while_break:
self.print_param_header(group_id, group_obj)
answer = PyInquirer.prompt([question])
# TODO: use raise_keyboard_interrupt=True when PyInquirer 1.0.3 is released
answer = questionary.prompt([question])
# Raise keyboard interrupg
KevinMenden marked this conversation as resolved.
Show resolved Hide resolved
if answer == {}:
raise KeyboardInterrupt
if answer[group_id] == "Continue >>":
Expand All @@ -465,8 +455,8 @@ def prompt_group(self, group_id, group_obj):

return answers

def single_param_to_pyinquirer(self, param_id, param_obj, answers=None, print_help=True):
"""Convert a JSONSchema param to a PyInquirer question
def single_param_to_questionary(self, param_id, param_obj, answers=None, print_help=True):
"""Convert a JSONSchema param to a Questionary question

Args:
param_id: Parameter ID (string)
Expand All @@ -475,7 +465,7 @@ def single_param_to_pyinquirer(self, param_id, param_obj, answers=None, print_he
print_help: If description and help_text should be printed (bool)

Returns:
Single PyInquirer dict, to be appended to questions list
Single Questionary dict, to be appended to questions list
"""
if answers is None:
answers = {}
Expand Down Expand Up @@ -620,9 +610,8 @@ def validate_pattern(val):

question["validate"] = validate_pattern

# WORKAROUND - PyInquirer <1.0.3 cannot have a default position in a list
# WORKAROUND - Questionary cannot have a default position in a list
# For now, move the default option to the top.
# TODO: Delete this code when PyInquirer >=1.0.3 is released.
if question["type"] == "list" and "default" in question:
try:
question["choices"].remove(question["default"])
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"GitPython",
"jinja2",
"jsonschema",
"PyInquirer==1.0.2",
"questionary",
ewels marked this conversation as resolved.
Show resolved Hide resolved
"pyyaml",
"requests",
"requests_cache",
Expand Down
32 changes: 16 additions & 16 deletions tests/test_launch.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,21 +93,21 @@ def test_nf_merge_schema(self):
assert self.launcher.schema_obj.schema["allOf"][0] == {"$ref": "#/definitions/coreNextflow"}
assert "-resume" in self.launcher.schema_obj.schema["definitions"]["coreNextflow"]["properties"]

def test_ob_to_pyinquirer_string(self):
def test_ob_to_questionary_string(self):
""" Check converting a python dict to a pyenquirer format - simple strings """
sc_obj = {
"type": "string",
"default": "data/*{1,2}.fastq.gz",
}
result = self.launcher.single_param_to_pyinquirer("input", sc_obj)
result = self.launcher.single_param_to_questionary("input", sc_obj)
assert result == {"type": "input", "name": "input", "message": "input", "default": "data/*{1,2}.fastq.gz"}

@mock.patch("PyInquirer.prompt", side_effect=[{"use_web_gui": "Web based"}])
@mock.patch("questionary.prompt", side_effect=[{"use_web_gui": "Web based"}])
def test_prompt_web_gui_true(self, mock_prompt):
""" Check the prompt to launch the web schema or use the cli """
assert self.launcher.prompt_web_gui() == True

@mock.patch("PyInquirer.prompt", side_effect=[{"use_web_gui": "Command line"}])
@mock.patch("questionary.prompt", side_effect=[{"use_web_gui": "Command line"}])
def test_prompt_web_gui_false(self, mock_prompt):
""" Check the prompt to launch the web schema or use the cli """
assert self.launcher.prompt_web_gui() == False
Expand Down Expand Up @@ -198,13 +198,13 @@ def test_sanitise_web_response(self):
assert self.launcher.schema_obj.input_params["single_end"] == True
assert self.launcher.schema_obj.input_params["max_cpus"] == 12

def test_ob_to_pyinquirer_bool(self):
def test_ob_to_questionary_bool(self):
""" Check converting a python dict to a pyenquirer format - booleans """
sc_obj = {
"type": "boolean",
"default": "True",
}
result = self.launcher.single_param_to_pyinquirer("single_end", sc_obj)
result = self.launcher.single_param_to_questionary("single_end", sc_obj)
assert result["type"] == "list"
assert result["name"] == "single_end"
assert result["message"] == "single_end"
Expand All @@ -218,10 +218,10 @@ def test_ob_to_pyinquirer_bool(self):
assert result["filter"]("false") == False
assert result["filter"](False) == False

def test_ob_to_pyinquirer_number(self):
def test_ob_to_questionary_number(self):
""" Check converting a python dict to a pyenquirer format - with enum """
sc_obj = {"type": "number", "default": 0.1}
result = self.launcher.single_param_to_pyinquirer("min_reps_consensus", sc_obj)
result = self.launcher.single_param_to_questionary("min_reps_consensus", sc_obj)
assert result["type"] == "input"
assert result["default"] == "0.1"
assert result["validate"]("123") is True
Expand All @@ -232,10 +232,10 @@ def test_ob_to_pyinquirer_number(self):
assert result["filter"]("123.456") == float(123.456)
assert result["filter"]("") == ""

def test_ob_to_pyinquirer_integer(self):
def test_ob_to_questionary_integer(self):
""" Check converting a python dict to a pyenquirer format - with enum """
sc_obj = {"type": "integer", "default": 1}
result = self.launcher.single_param_to_pyinquirer("broad_cutoff", sc_obj)
result = self.launcher.single_param_to_questionary("broad_cutoff", sc_obj)
assert result["type"] == "input"
assert result["default"] == "1"
assert result["validate"]("123") is True
Expand All @@ -246,10 +246,10 @@ def test_ob_to_pyinquirer_integer(self):
assert result["filter"]("123") == int(123)
assert result["filter"]("") == ""

def test_ob_to_pyinquirer_range(self):
def test_ob_to_questionary_range(self):
""" Check converting a python dict to a pyenquirer format - with enum """
sc_obj = {"type": "range", "minimum": "10", "maximum": "20", "default": 15}
result = self.launcher.single_param_to_pyinquirer("broad_cutoff", sc_obj)
result = self.launcher.single_param_to_questionary("broad_cutoff", sc_obj)
assert result["type"] == "input"
assert result["default"] == "15"
assert result["validate"]("20") is True
Expand All @@ -260,21 +260,21 @@ def test_ob_to_pyinquirer_range(self):
assert result["filter"]("20") == float(20)
assert result["filter"]("") == ""

def test_ob_to_pyinquirer_enum(self):
def test_ob_to_questionary_enum(self):
""" Check converting a python dict to a pyenquirer format - with enum """
sc_obj = {"type": "string", "default": "copy", "enum": ["symlink", "rellink"]}
result = self.launcher.single_param_to_pyinquirer("publish_dir_mode", sc_obj)
result = self.launcher.single_param_to_questionary("publish_dir_mode", sc_obj)
assert result["type"] == "list"
assert result["default"] == "copy"
assert result["choices"] == ["symlink", "rellink"]
assert result["validate"]("symlink") is True
assert result["validate"]("") is True
assert result["validate"]("not_allowed") == "Must be one of: symlink, rellink"

def test_ob_to_pyinquirer_pattern(self):
def test_ob_to_questionary_pattern(self):
""" Check converting a python dict to a pyenquirer format - with pattern """
sc_obj = {"type": "string", "pattern": "^([a-zA-Z0-9_\\-\\.]+)@([a-zA-Z0-9_\\-\\.]+)\\.([a-zA-Z]{2,5})$"}
result = self.launcher.single_param_to_pyinquirer("email", sc_obj)
result = self.launcher.single_param_to_questionary("email", sc_obj)
assert result["type"] == "input"
assert result["validate"]("[email protected]") is True
assert result["validate"]("") is True
Expand Down