From 51fc2df37a28ac9a52b53e7b97a8211563320d99 Mon Sep 17 00:00:00 2001 From: Phil Ewels Date: Thu, 14 Jan 2021 16:30:29 +0100 Subject: [PATCH 1/6] Launch - rich formatting for prompts, show required. * Use ansi colours for the default values and filled values in the group select view of params (nf-core/tools#816) * Add to group select prompt a warning that a value is required, if not yet filled and no default --- nf_core/launch.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/nf_core/launch.py b/nf_core/launch.py index 97231f8827..c940496898 100644 --- a/nf_core/launch.py +++ b/nf_core/launch.py @@ -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"), ] ) @@ -437,11 +440,16 @@ def prompt_group(self, group_id, group_obj): 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 = [("", 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 From 29108ff11655bc8eb0498f1f8c749ce5fb87e584 Mon Sep 17 00:00:00 2001 From: Phil Ewels Date: Thu, 14 Jan 2021 17:39:25 +0100 Subject: [PATCH 2/6] Launch: Refine rich text output for cli prompts --- nf_core/launch.py | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/nf_core/launch.py b/nf_core/launch.py index c940496898..51a5c619da 100644 --- a/nf_core/launch.py +++ b/nf_core/launch.py @@ -430,33 +430,48 @@ def prompt_group(self, group_id, group_obj): """ while_break = False answers = {} + first_ask = True + error_msgs = [] while not while_break: 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 = [] + for param_id, param in group_obj["properties"].items(): if not param.get("hidden", False) or self.show_hidden: - q_title = [("", 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]))) + 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.append(("class:choice-default", " [{}]".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)")) + 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) + if first_ask: + self.print_param_header(group_id, group_obj) + first_ask = False answer = questionary.unsafe_prompt([question], style=nfcore_question_style) if answer[group_id] == "Continue >>": while_break = True @@ -465,7 +480,7 @@ 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] @@ -605,14 +620,14 @@ def print_param_header(self, param_id, 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") + console.print("(Use arrow keys)", style="italic", highlight=False) def strip_default_params(self): """ Strip parameters if they have not changed from the default """ From 48b81e0fe2b568b597a73b1397448d8289f35598 Mon Sep 17 00:00:00 2001 From: Phil Ewels Date: Thu, 14 Jan 2021 18:47:16 +0100 Subject: [PATCH 3/6] Launch: No duplication for parameter heading. Fix order of code for group heading. --- nf_core/launch.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/nf_core/launch.py b/nf_core/launch.py index 51a5c619da..f14a5739a7 100644 --- a/nf_core/launch.py +++ b/nf_core/launch.py @@ -430,9 +430,12 @@ def prompt_group(self, group_id, group_obj): """ while_break = False answers = {} - first_ask = True 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, @@ -469,9 +472,6 @@ def prompt_group(self, group_id, group_obj): if len(question["choices"]) == 2: return {} - if first_ask: - self.print_param_header(group_id, group_obj) - first_ask = False answer = questionary.unsafe_prompt([question], style=nfcore_question_style) if answer[group_id] == "Continue >>": while_break = True @@ -504,7 +504,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: @@ -615,7 +615,7 @@ 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()) @@ -627,7 +627,8 @@ def print_param_header(self, param_id, param_obj): if "help_text" in param_obj: help_md = Markdown(param_obj["help_text"].strip()) console.print(help_md, style="dim") - console.print("(Use arrow keys)", style="italic", highlight=False) + 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 """ From ee7e9717e158dc5e695145717e80a3d8ef2baff0 Mon Sep 17 00:00:00 2001 From: Phil Ewels Date: Thu, 14 Jan 2021 19:00:01 +0100 Subject: [PATCH 4/6] Launch cli: Allow answers to be deleted --- nf_core/launch.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/nf_core/launch.py b/nf_core/launch.py index f14a5739a7..12b1c059f0 100644 --- a/nf_core/launch.py +++ b/nf_core/launch.py @@ -388,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(): @@ -412,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): """ @@ -485,7 +493,7 @@ def prompt_group(self, group_id, group_obj): 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 From 42b97da6e711d1de1a8799ad30d67098be14b8b3 Mon Sep 17 00:00:00 2001 From: Phil Ewels Date: Thu, 14 Jan 2021 19:03:16 +0100 Subject: [PATCH 5/6] Changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7bcb86e5ab..1790cf66b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)] From 32a17902cb1f92e01c8212e2d30ff71bcad27d60 Mon Sep 17 00:00:00 2001 From: Phil Ewels Date: Thu, 14 Jan 2021 19:07:22 +0100 Subject: [PATCH 6/6] Update pytests --- tests/test_launch.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_launch.py b/tests/test_launch.py index ac3575b407..a0acebee24 100644 --- a/tests/test_launch.py +++ b/tests/test_launch.py @@ -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): @@ -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))