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

Better UI for command line launch #829

Merged
merged 6 commits into from
Jan 15, 2021
Merged
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## v1.13dev

### Tools helper code

* Fixed some bugs in the command line interface for `nf-core launch` and improved formatting [[#829](https://github.com/nf-core/tools/pull/829)]

### Linting

* Added schema validation of GitHub action workflows to lint function [[#795](https://github.com/nf-core/tools/issues/795)]
Expand Down
64 changes: 48 additions & 16 deletions nf_core/launch.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@
("instruction", ""), # user instructions for select, rawselect, checkbox
("text", ""), # plain text
("disabled", "fg:gray italic"), # disabled choices for select and checkbox prompts
("choice-default", "fg:ansiblack"),
("choice-default-changed", "fg:ansiyellow"),
("choice-required", "fg:ansired"),
]
)

Expand Down Expand Up @@ -385,7 +388,7 @@ def prompt_schema(self):
for param_id, param_obj in self.schema_obj.schema.get("properties", {}).items():
if not param_obj.get("hidden", False) or self.show_hidden:
is_required = param_id in self.schema_obj.schema.get("required", [])
answers.update(self.prompt_param(param_id, param_obj, is_required, answers))
answers = self.prompt_param(param_id, param_obj, is_required, answers)

# Split answers into core nextflow options and params
for key, answer in answers.items():
Expand All @@ -409,10 +412,18 @@ def prompt_param(self, param_id, param_obj, is_required, answers):
log.error("'–-{}' is required".format(param_id))
answer = questionary.unsafe_prompt([question], style=nfcore_question_style)

# Don't return empty answers
# Ignore if empty
if answer[param_id] == "":
return {}
return answer
answer = {}

# Previously entered something but this time we deleted it
if param_id not in answer and param_id in answers:
answers.pop(param_id)
# Everything else (first time answer no response or normal response)
else:
answers.update(answer)

return answers

def prompt_group(self, group_id, group_obj):
"""
Expand All @@ -427,28 +438,48 @@ def prompt_group(self, group_id, group_obj):
"""
while_break = False
answers = {}
error_msgs = []
while not while_break:

if len(error_msgs) == 0:
self.print_param_header(group_id, group_obj, True)

question = {
"type": "list",
"name": group_id,
"message": group_obj.get("title", group_id),
"qmark": "",
"message": "",
"instruction": " ",
"choices": ["Continue >>", questionary.Separator()],
}

# Show error messages if we have any
for msg in error_msgs:
question["choices"].append(
questionary.Choice(
[("bg:ansiblack fg:ansired bold", " error "), ("fg:ansired", f" - {msg}")], disabled=True
)
)
error_msgs = []
Comment on lines +456 to +463
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't that make more sense after line 491, where you check for errors?
Or does it require a 'fresh' question object?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, the logic is a bit funny here. If there are errors then the question is asked again. So in lines 491 we collect error messages, then the loop fires again. Here we are printing the errors if there are any from the last time we went around and then clearing them once shown (so that they don't persist forever).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I get it, makes sense like that :D thanks!


for param_id, param in group_obj["properties"].items():
if not param.get("hidden", False) or self.show_hidden:
q_title = param_id
if param_id in answers:
q_title += " [{}]".format(answers[param_id])
q_title = [("", "{} ".format(param_id))]
# If already filled in, show value
if param_id in answers and answers.get(param_id) != param.get("default"):
q_title.append(("class:choice-default-changed", "[{}]".format(answers[param_id])))
# If the schema has a default, show default
elif "default" in param:
q_title += " [{}]".format(param["default"])
q_title.append(("class:choice-default", "[{}]".format(param["default"])))
# Show that it's required if not filled in and no default
elif param_id in group_obj.get("required", []):
q_title.append(("class:choice-required", "(required)"))
question["choices"].append(questionary.Choice(title=q_title, value=param_id))

# Skip if all questions hidden
if len(question["choices"]) == 2:
return {}

self.print_param_header(group_id, group_obj)
answer = questionary.unsafe_prompt([question], style=nfcore_question_style)
if answer[group_id] == "Continue >>":
while_break = True
Expand All @@ -457,12 +488,12 @@ def prompt_group(self, group_id, group_obj):
req_default = self.schema_obj.input_params.get(p_required, "")
req_answer = answers.get(p_required, "")
if req_default == "" and req_answer == "":
log.error("'--{}' is required.".format(p_required))
error_msgs.append(f"`{p_required}` is required")
while_break = False
else:
param_id = answer[group_id]
is_required = param_id in group_obj.get("required", [])
answers.update(self.prompt_param(param_id, group_obj["properties"][param_id], is_required, answers))
answers = self.prompt_param(param_id, group_obj["properties"][param_id], is_required, answers)

return answers

Expand All @@ -481,7 +512,7 @@ def single_param_to_questionary(self, param_id, param_obj, answers=None, print_h
if answers is None:
answers = {}

question = {"type": "input", "name": param_id, "message": param_id}
question = {"type": "input", "name": param_id, "message": ""}

# Print the name, description & help text
if print_help:
Expand Down Expand Up @@ -592,19 +623,20 @@ def validate_pattern(val):

return question

def print_param_header(self, param_id, param_obj):
def print_param_header(self, param_id, param_obj, is_group=False):
if "description" not in param_obj and "help_text" not in param_obj:
return
console = Console(force_terminal=nf_core.utils.rich_force_colors())
console.print("\n")
console.print(param_obj.get("title", param_id), style="bold")
console.print("[bold blue]?[/] [bold on black] {} [/]".format(param_obj.get("title", param_id)))
if "description" in param_obj:
md = Markdown(param_obj["description"])
console.print(md)
if "help_text" in param_obj:
help_md = Markdown(param_obj["help_text"].strip())
console.print(help_md, style="dim")
console.print("\n")
if is_group:
console.print("(Use arrow keys)", style="italic", highlight=False)

def strip_default_params(self):
""" Strip parameters if they have not changed from the default """
Expand Down
4 changes: 2 additions & 2 deletions tests/test_launch.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ def test_ob_to_questionary_string(self):
"default": "data/*{1,2}.fastq.gz",
}
result = self.launcher.single_param_to_questionary("input", sc_obj)
assert result == {"type": "input", "name": "input", "message": "input", "default": "data/*{1,2}.fastq.gz"}
assert result == {"type": "input", "name": "input", "message": "", "default": "data/*{1,2}.fastq.gz"}

@mock.patch("questionary.unsafe_prompt", side_effect=[{"use_web_gui": "Web based"}])
def test_prompt_web_gui_true(self, mock_prompt):
Expand Down Expand Up @@ -207,7 +207,7 @@ def test_ob_to_questionary_bool(self):
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"
assert result["message"] == ""
assert result["choices"] == ["True", "False"]
assert result["default"] == "True"
print(type(True))
Expand Down