Skip to content

Commit 8c68650

Browse files
authored
Merge pull request #806 from nf-core/dev
Dev > Master for release
2 parents b67fd2a + 9c6cca5 commit 8c68650

File tree

17 files changed

+273
-159
lines changed

17 files changed

+273
-159
lines changed

.github/markdownlint.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,13 @@ default: true
33
line-length: false
44
no-duplicate-header:
55
siblings_only: true
6+
no-inline-html:
7+
allowed_elements:
8+
- img
9+
- p
10+
- kbd
11+
- details
12+
- summary
613
# tools only - the {{ jinja variables }} break URLs and cause this to error
714
no-bare-urls: false
815
# tools only - suppresses error messages for usage of $ in main README

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,3 +112,5 @@ ENV/
112112

113113
# Jetbrains IDEs
114114
.idea
115+
pip-wheel-metadata
116+
.vscode

CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,23 @@
11
# nf-core/tools: Changelog
22

3+
## [v1.12.1 - Silver Dolphin](https://github.com/nf-core/tools/releases/tag/1.12.1) - [2020-12-03]
4+
5+
### Template
6+
7+
* Finished switch from `$baseDir` to `$projectDir` in `iGenomes.conf` and `main.nf`
8+
* Main fix is for `smail_fields` which was a bug introduced in the previous release. Sorry about that!
9+
* Ported a number of small content tweaks from nf-core/eager to the template [[#786](https://github.com/nf-core/tools/issues/786)]
10+
* Better contributing documentation, more placeholders in documentation files, more relaxed markdownlint exceptions for certain HTML tags, more content for the PR and issue templates.
11+
12+
### Tools helper code
13+
14+
* Pipeline schema: make parameters of type `range` to `number`. [[#738](https://github.com/nf-core/tools/issues/738)]
15+
* Respect `$NXF_HOME` when looking for pipelines with `nf-core list` [[#798](https://github.com/nf-core/tools/issues/798)]
16+
* Swapped PyInquirer with questionary for command line questions in `launch.py` [[#726](https://github.com/nf-core/tools/issues/726)]
17+
* This should fix conda installation issues that some people had been hitting
18+
* The change also allows other improvements to the UI
19+
* Fix linting crash when a file deleted but not yet staged in git [[#796](https://github.com/nf-core/tools/issues/796)]
20+
321
## [v1.12 - Mercury Weasel](https://github.com/nf-core/tools/releases/tag/1.12) - [2020-11-19]
422

523
### Tools helper code

nf_core/launch.py

Lines changed: 57 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
import json
1111
import logging
1212
import os
13-
import PyInquirer
13+
import prompt_toolkit
14+
import questionary
1415
import re
1516
import subprocess
1617
import textwrap
@@ -20,15 +21,21 @@
2021

2122
log = logging.getLogger(__name__)
2223

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+
)
3239

3340

3441
class Launch(object):
@@ -256,11 +263,9 @@ def prompt_web_gui(self):
256263
"name": "use_web_gui",
257264
"message": "Choose launch method",
258265
"choices": ["Web based", "Command line"],
266+
"default": "Web based",
259267
}
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)
264269
return answer["use_web_gui"] == "Web based"
265270

266271
def launch_web_gui(self):
@@ -347,14 +352,14 @@ def sanitise_web_response(self):
347352
The web builder returns everything as strings.
348353
Use the functions defined in the cli wizard to convert to the correct types.
349354
"""
350-
# Collect pyinquirer objects for each defined input_param
351-
pyinquirer_objects = {}
355+
# Collect questionary objects for each defined input_param
356+
questionary_objects = {}
352357
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)
354359

355360
for d_key, definition in self.schema_obj.schema.get("definitions", {}).items():
356361
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)
358363

359364
# Go through input params and sanitise
360365
for params in [self.nxf_flags, self.schema_obj.input_params]:
@@ -364,7 +369,7 @@ def sanitise_web_response(self):
364369
del params[param_id]
365370
continue
366371
# 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")
368373
if filter_func is not None:
369374
params[param_id] = filter_func(params[param_id])
370375

@@ -396,19 +401,13 @@ def prompt_param(self, param_id, param_obj, is_required, answers):
396401
"""Prompt for a single parameter"""
397402

398403
# 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)
404406

405407
# If required and got an empty reponse, ask again
406408
while type(answer[param_id]) is str and answer[param_id].strip() == "" and is_required:
407409
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)
412411

413412
# Don't return empty answers
414413
if answer[param_id] == "":
@@ -426,37 +425,39 @@ def prompt_group(self, group_id, group_obj):
426425
Returns:
427426
Dict of param_id:val answers
428427
"""
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-
444428
while_break = False
445429
answers = {}
446430
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+
447451
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)
452453
if answer[group_id] == "Continue >>":
453454
while_break = True
454455
# Check if there are any required parameters that don't have answers
455456
for p_required in group_obj.get("required", []):
456457
req_default = self.schema_obj.input_params.get(p_required, "")
457458
req_answer = answers.get(p_required, "")
458459
if req_default == "" and req_answer == "":
459-
log.error("'{}' is required.".format(p_required))
460+
log.error("'--{}' is required.".format(p_required))
460461
while_break = False
461462
else:
462463
param_id = answer[group_id]
@@ -465,8 +466,8 @@ def prompt_group(self, group_id, group_obj):
465466

466467
return answers
467468

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
470471
471472
Args:
472473
param_id: Parameter ID (string)
@@ -475,7 +476,7 @@ def single_param_to_pyinquirer(self, param_id, param_obj, answers=None, print_he
475476
print_help: If description and help_text should be printed (bool)
476477
477478
Returns:
478-
Single PyInquirer dict, to be appended to questions list
479+
Single Questionary dict, to be appended to questions list
479480
"""
480481
if answers is None:
481482
answers = {}
@@ -530,7 +531,11 @@ def validate_number(val):
530531
try:
531532
if val.strip() == "":
532533
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"])
534539
except ValueError:
535540
return "Must be a number"
536541
else:
@@ -568,46 +573,11 @@ def filter_integer(val):
568573

569574
question["filter"] = filter_integer
570575

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-
596576
if "enum" in param_obj:
597577
# Use a selection list instead of free text input
598578
question["type"] = "list"
599579
question["choices"] = param_obj["enum"]
600580

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-
611581
# Validate pattern from schema
612582
if "pattern" in param_obj:
613583

@@ -620,21 +590,6 @@ def validate_pattern(val):
620590

621591
question["validate"] = validate_pattern
622592

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-
638593
return question
639594

640595
def print_param_header(self, param_id, param_obj):

nf_core/lint.py

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ def lint_pipeline(self, release_mode=False):
239239
log.debug("Running lint test: {}".format(fun_name))
240240
getattr(self, fun_name)()
241241
if len(self.failed) > 0:
242-
log.error("Found test failures in `{}`, halting lint run.".format(fun_name))
242+
log.critical("Found test failures in `{}`, halting lint run.".format(fun_name))
243243
break
244244

245245
def check_files_exist(self):
@@ -1241,17 +1241,25 @@ def check_cookiecutter_strings(self):
12411241
num_files = 0
12421242
for fn in list_of_files:
12431243
num_files += 1
1244-
with io.open(fn, "r", encoding="latin1") as fh:
1245-
lnum = 0
1246-
for l in fh:
1247-
lnum += 1
1248-
cc_matches = re.findall(r"{{\s*cookiecutter[^}]*}}", l)
1249-
if len(cc_matches) > 0:
1250-
for cc_match in cc_matches:
1251-
self.failed.append(
1252-
(13, "Found a cookiecutter template string in `{}` L{}: {}".format(fn, lnum, cc_match))
1253-
)
1254-
num_matches += 1
1244+
try:
1245+
with io.open(fn, "r", encoding="latin1") as fh:
1246+
lnum = 0
1247+
for l in fh:
1248+
lnum += 1
1249+
cc_matches = re.findall(r"{{\s*cookiecutter[^}]*}}", l)
1250+
if len(cc_matches) > 0:
1251+
for cc_match in cc_matches:
1252+
self.failed.append(
1253+
(
1254+
13,
1255+
"Found a cookiecutter template string in `{}` L{}: {}".format(
1256+
fn, lnum, cc_match
1257+
),
1258+
)
1259+
)
1260+
num_matches += 1
1261+
except FileNotFoundError as e:
1262+
log.warn("`git ls-files` returned '{}' but could not open it!".format(fn))
12551263
if num_matches == 0:
12561264
self.passed.append((13, "Did not find any cookiecutter template strings ({} files)".format(num_files)))
12571265

nf_core/list.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,8 @@ def get_local_nf_workflows(self):
128128
# Try to guess the local cache directory (much faster than calling nextflow)
129129
if len(os.environ.get("NXF_ASSETS", "")) > 0:
130130
nextflow_wfdir = os.environ.get("NXF_ASSETS")
131+
elif len(os.environ.get("NXF_HOME", "")) > 0:
132+
nextflow_wfdir = os.path.join(os.environ.get("NXF_HOME"), "assets")
131133
else:
132134
nextflow_wfdir = os.path.join(os.getenv("HOME"), ".nextflow", "assets")
133135
if os.path.isdir(nextflow_wfdir):
@@ -348,6 +350,8 @@ def get_local_nf_workflow_details(self):
348350
# Try to guess the local cache directory
349351
if len(os.environ.get("NXF_ASSETS", "")) > 0:
350352
nf_wfdir = os.path.join(os.environ.get("NXF_ASSETS"), self.full_name)
353+
elif len(os.environ.get("NXF_HOME", "")) > 0:
354+
nf_wfdir = os.path.join(os.environ.get("NXF_HOME"), "assets")
351355
else:
352356
nf_wfdir = os.path.join(os.getenv("HOME"), ".nextflow", "assets", self.full_name)
353357
if os.path.isdir(nf_wfdir):

0 commit comments

Comments
 (0)