Skip to content

Commit efe344a

Browse files
authored
Merge branch 'dev' into add-stubs-to-create-test-yml
2 parents f60b6af + 66dbf0b commit efe344a

File tree

7 files changed

+129
-35
lines changed

7 files changed

+129
-35
lines changed

.github/workflows/lint-code.yml

+19
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,22 @@ jobs:
8585
with:
8686
isortVersion: "latest"
8787
requirementsFiles: "requirements.txt requirements-dev.txt"
88+
89+
static-type-check:
90+
runs-on: ubuntu-latest
91+
steps:
92+
- uses: actions/checkout@v2
93+
- uses: actions/setup-python@v3
94+
with:
95+
python-version: "3.11"
96+
- run: pip install mypy types-PyYAML
97+
- name: Get Python changed files
98+
id: changed-py-files
99+
uses: tj-actions/changed-files@v23
100+
with:
101+
files: |
102+
*.py
103+
**/*.py
104+
- name: Run if any of the listed files above is changed
105+
if: steps.changed-py-files.outputs.any_changed == 'true'
106+
run: mypy ${{ steps.changed-py-files.outputs.all_changed_files }} --ignore-missing-imports

nf_core/components/components_command.py

+28-18
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@
33
import os
44
import shutil
55
from pathlib import Path
6-
7-
import yaml
6+
from typing import Dict, List, Optional, Union
87

98
import nf_core.utils
109
from nf_core.modules.modules_json import ModulesJson
@@ -20,7 +19,15 @@ class ComponentCommand:
2019
Base class for the 'nf-core modules' and 'nf-core subworkflows' commands
2120
"""
2221

23-
def __init__(self, component_type, dir, remote_url=None, branch=None, no_pull=False, hide_progress=False):
22+
def __init__(
23+
self,
24+
component_type: str,
25+
dir: str,
26+
remote_url: Optional[str] = None,
27+
branch: Optional[str] = None,
28+
no_pull: bool = False,
29+
hide_progress: bool = False,
30+
) -> None:
2431
"""
2532
Initialise the ComponentClass object
2633
"""
@@ -30,14 +37,15 @@ def __init__(self, component_type, dir, remote_url=None, branch=None, no_pull=Fa
3037
self.hide_progress = hide_progress
3138
self._configure_repo_and_paths()
3239

33-
def _configure_repo_and_paths(self, nf_dir_req=True):
40+
def _configure_repo_and_paths(self, nf_dir_req: bool = True) -> None:
3441
"""
3542
Determine the repo type and set some default paths.
3643
If this is a modules repo, determine the org_path too.
3744
3845
Args:
3946
nf_dir_req (bool, optional): Whether this command requires being run in the nf-core modules repo or a nf-core pipeline repository. Defaults to True.
4047
"""
48+
4149
try:
4250
if self.dir:
4351
self.dir, self.repo_type, self.org = get_repo_info(self.dir, use_prompt=nf_dir_req)
@@ -54,7 +62,7 @@ def _configure_repo_and_paths(self, nf_dir_req=True):
5462
self.default_subworkflows_path = Path("subworkflows", self.org)
5563
self.default_subworkflows_tests_path = Path("tests", "subworkflows", self.org)
5664

57-
def get_local_components(self):
65+
def get_local_components(self) -> List[str]:
5866
"""
5967
Get the local modules/subworkflows in a pipeline
6068
"""
@@ -63,7 +71,7 @@ def get_local_components(self):
6371
str(path.relative_to(local_component_dir)) for path in local_component_dir.iterdir() if path.suffix == ".nf"
6472
]
6573

66-
def get_components_clone_modules(self):
74+
def get_components_clone_modules(self) -> List[str]:
6775
"""
6876
Get the modules/subworkflows repository available in a clone of nf-core/modules
6977
"""
@@ -77,7 +85,7 @@ def get_components_clone_modules(self):
7785
if "main.nf" in files
7886
]
7987

80-
def has_valid_directory(self):
88+
def has_valid_directory(self) -> bool:
8189
"""Check that we were given a pipeline or clone of nf-core/modules"""
8290
if self.repo_type == "modules":
8391
return True
@@ -92,14 +100,14 @@ def has_valid_directory(self):
92100
log.warning(f"Could not find a 'main.nf' or 'nextflow.config' file in '{self.dir}'")
93101
return True
94102

95-
def has_modules_file(self):
103+
def has_modules_file(self) -> None:
96104
"""Checks whether a module.json file has been created and creates one if it is missing"""
97105
modules_json_path = os.path.join(self.dir, "modules.json")
98106
if not os.path.exists(modules_json_path):
99107
log.info("Creating missing 'module.json' file.")
100108
ModulesJson(self.dir).create()
101109

102-
def clear_component_dir(self, component_name, component_dir):
110+
def clear_component_dir(self, component_name: str, component_dir: str) -> bool:
103111
"""
104112
Removes all files in the module/subworkflow directory
105113
@@ -127,7 +135,7 @@ def clear_component_dir(self, component_name, component_dir):
127135
log.error(f"Could not remove {self.component_type[:-1]} {component_name}: {e}")
128136
return False
129137

130-
def components_from_repo(self, install_dir):
138+
def components_from_repo(self, install_dir: str) -> List[str]:
131139
"""
132140
Gets the modules/subworkflows installed from a certain repository
133141
@@ -145,7 +153,9 @@ def components_from_repo(self, install_dir):
145153
str(Path(dir_path).relative_to(repo_dir)) for dir_path, _, files in os.walk(repo_dir) if "main.nf" in files
146154
]
147155

148-
def install_component_files(self, component_name, component_version, modules_repo, install_dir):
156+
def install_component_files(
157+
self, component_name: str, component_version: str, modules_repo: ModulesRepo, install_dir: str
158+
) -> bool:
149159
"""
150160
Installs a module/subworkflow into the given directory
151161
@@ -160,7 +170,7 @@ def install_component_files(self, component_name, component_version, modules_rep
160170
"""
161171
return modules_repo.install_component(component_name, install_dir, component_version, self.component_type)
162172

163-
def load_lint_config(self):
173+
def load_lint_config(self) -> None:
164174
"""Parse a pipeline lint config file.
165175
166176
Load the '.nf-core.yml' config file and extract
@@ -171,7 +181,7 @@ def load_lint_config(self):
171181
_, tools_config = nf_core.utils.load_tools_config(self.dir)
172182
self.lint_config = tools_config.get("lint", {})
173183

174-
def check_modules_structure(self):
184+
def check_modules_structure(self) -> None:
175185
"""
176186
Check that the structure of the modules directory in a pipeline is the correct one:
177187
modules/nf-core/TOOL/SUBTOOL
@@ -180,7 +190,7 @@ def check_modules_structure(self):
180190
modules/nf-core/modules/TOOL/SUBTOOL
181191
"""
182192
if self.repo_type == "pipeline":
183-
wrong_location_modules = []
193+
wrong_location_modules: List[Path] = []
184194
for directory, _, files in os.walk(Path(self.dir, "modules")):
185195
if "main.nf" in files:
186196
module_path = Path(directory).relative_to(Path(self.dir, "modules"))
@@ -201,14 +211,14 @@ def check_modules_structure(self):
201211
modules_dir = Path("modules").resolve()
202212
correct_dir = Path(modules_dir, self.modules_repo.repo_path, Path(*module.parts[2:]))
203213
wrong_dir = Path(modules_dir, module)
204-
shutil.move(wrong_dir, correct_dir)
214+
shutil.move(str(wrong_dir), str(correct_dir))
205215
log.info(f"Moved {wrong_dir} to {correct_dir}.")
206216
shutil.rmtree(Path(self.dir, "modules", self.modules_repo.repo_path, "modules"))
207217
# Regenerate modules.json file
208218
modules_json = ModulesJson(self.dir)
209219
modules_json.check_up_to_date()
210220

211-
def check_patch_paths(self, patch_path, module_name):
221+
def check_patch_paths(self, patch_path: Path, module_name: str) -> None:
212222
"""
213223
Check that paths in patch files are updated to the new modules path
214224
"""
@@ -239,7 +249,7 @@ def check_patch_paths(self, patch_path, module_name):
239249
][module_name]["patch"] = str(patch_path.relative_to(Path(self.dir).resolve()))
240250
modules_json.dump()
241251

242-
def check_if_in_include_stmts(self, component_path):
252+
def check_if_in_include_stmts(self, component_path: str) -> Dict[str, List[Dict[str, Union[int, str]]]]:
243253
"""
244254
Checks for include statements in the main.nf file of the pipeline and a list of line numbers where the component is included
245255
Args:
@@ -248,7 +258,7 @@ def check_if_in_include_stmts(self, component_path):
248258
Returns:
249259
(list): A list of dictionaries, with the workflow file and the line number where the component is included
250260
"""
251-
include_stmts = {}
261+
include_stmts: Dict[str, List[Dict[str, Union[int, str]]]] = {}
252262
if self.repo_type == "pipeline":
253263
workflow_files = Path(self.dir, "workflows").glob("*.nf")
254264
for workflow_file in workflow_files:

nf_core/components/components_utils.py

+23-17
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,33 @@
11
import logging
2-
import os
32
import re
43
from pathlib import Path
4+
from typing import List, Optional, Tuple
55

66
import questionary
77
import rich.prompt
88

99
import nf_core.utils
10+
from nf_core.modules.modules_repo import ModulesRepo
1011

1112
log = logging.getLogger(__name__)
1213

1314

14-
def get_repo_info(directory, use_prompt=True):
15+
def get_repo_info(directory: str, use_prompt: Optional[bool] = True) -> Tuple[str, Optional[str], str]:
1516
"""
1617
Determine whether this is a pipeline repository or a clone of
1718
nf-core/modules
1819
"""
20+
1921
# Verify that the pipeline dir exists
2022
if directory is None or not Path(directory).is_dir():
2123
raise UserWarning(f"Could not find directory: {directory}")
2224

2325
# Try to find the root directory
24-
base_dir = nf_core.utils.determine_base_dir(directory)
26+
base_dir: str = nf_core.utils.determine_base_dir(directory)
2527

2628
# Figure out the repository type from the .nf-core.yml config file if we can
2729
config_fn, tools_config = nf_core.utils.load_tools_config(base_dir)
28-
repo_type = tools_config.get("repository_type", None)
30+
repo_type: Optional[str] = tools_config.get("repository_type", None)
2931

3032
# If not set, prompt the user
3133
if not repo_type and use_prompt:
@@ -55,7 +57,6 @@ def get_repo_info(directory, use_prompt=True):
5557
raise UserWarning(f"Invalid repository type: '{repo_type}'")
5658

5759
# Check for org if modules repo
58-
org = None
5960
if repo_type == "pipeline":
6061
org = ""
6162
elif repo_type == "modules":
@@ -77,10 +78,12 @@ def get_repo_info(directory, use_prompt=True):
7778
raise UserWarning("Organisation path could not be established")
7879

7980
# It was set on the command line, return what we were given
80-
return [base_dir, repo_type, org]
81+
return (base_dir, repo_type, org)
8182

8283

83-
def prompt_component_version_sha(component_name, component_type, modules_repo, installed_sha=None):
84+
def prompt_component_version_sha(
85+
component_name: str, component_type: str, modules_repo: ModulesRepo, installed_sha: Optional[str] = None
86+
) -> str:
8487
"""
8588
Creates an interactive questionary prompt for selecting the module/subworkflow version
8689
Args:
@@ -107,17 +110,20 @@ def prompt_component_version_sha(component_name, component_type, modules_repo, i
107110
next_page_commits = [next(all_commits, None) for _ in range(10)]
108111
next_page_commits = [commit for commit in next_page_commits if commit is not None]
109112
if all(commit is None for commit in next_page_commits):
110-
next_page_commits = None
113+
next_page_commits = []
111114

112115
choices = []
113-
for title, sha in map(lambda commit: (commit["trunc_message"], commit["git_sha"]), commits):
114-
display_color = "fg:ansiblue" if sha != installed_sha else "fg:ansired"
115-
message = f"{title} {sha}"
116-
if installed_sha == sha:
117-
message += " (installed version)"
118-
commit_display = [(display_color, message), ("class:choice-default", "")]
119-
choices.append(questionary.Choice(title=commit_display, value=sha))
120-
if next_page_commits is not None:
116+
for commit in commits:
117+
if commit:
118+
title = commit["trunc_message"]
119+
sha = commit["git_sha"]
120+
display_color = "fg:ansiblue" if sha != installed_sha else "fg:ansired"
121+
message = f"{title} {sha}"
122+
if installed_sha == sha:
123+
message += " (installed version)"
124+
commit_display = [(display_color, message), ("class:choice-default", "")]
125+
choices.append(questionary.Choice(title=commit_display, value=sha))
126+
if next_page_commits:
121127
choices += [older_commits_choice]
122128
git_sha = questionary.select(
123129
f"Select '{component_name}' commit:", choices=choices, style=nf_core.utils.nfcore_question_style
@@ -126,7 +132,7 @@ def prompt_component_version_sha(component_name, component_type, modules_repo, i
126132
return git_sha
127133

128134

129-
def get_components_to_install(subworkflow_dir):
135+
def get_components_to_install(subworkflow_dir: str) -> Tuple[List[str], List[str]]:
130136
"""
131137
Parse the subworkflow main.nf file to retrieve all imported modules and subworkflows.
132138
"""

nf_core/lint/nextflow_config.py

+33
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,11 @@ def nextflow_config(self):
108108
109109
lint:
110110
nextflow_config: False
111+
112+
**The configuration should contain the following or the test will fail:**
113+
114+
* A ``test`` configuration profile should exist.
115+
111116
"""
112117
passed = []
113118
warned = []
@@ -312,4 +317,32 @@ def nextflow_config(self):
312317
)
313318
)
314319

320+
# Check for the availability of the "test" configuration profile by parsing nextflow.config
321+
with open(os.path.join(self.wf_path, "nextflow.config"), "r") as f:
322+
content = f.read()
323+
324+
# Remove comments
325+
cleaned_content = re.sub(r"//.*", "", content)
326+
cleaned_content = re.sub(r"/\*.*?\*/", "", content, flags=re.DOTALL)
327+
328+
match = re.search(r"\bprofiles\s*{", cleaned_content)
329+
if not match:
330+
failed.append("nextflow.config does not contain `profiles` scope, but `test` profile is required")
331+
else:
332+
# Extract profiles scope content and check for test profile
333+
start = match.end()
334+
end = start
335+
brace_count = 1
336+
while brace_count > 0 and end < len(content):
337+
if cleaned_content[end] == "{":
338+
brace_count += 1
339+
elif cleaned_content[end] == "}":
340+
brace_count -= 1
341+
end += 1
342+
profiles_content = cleaned_content[start : end - 1].strip()
343+
if re.search(r"\btest\s*{", profiles_content):
344+
passed.append("nextflow.config contains configuration profile `test`")
345+
else:
346+
failed.append("nextflow.config does not contain configuration profile `test`")
347+
315348
return {"passed": passed, "warned": warned, "failed": failed, "ignored": ignored}

pyproject.toml

+5
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,8 @@ norecursedirs = [ ".*", "build", "dist", "*.egg", "data", "__pycache__", ".githu
2020
profile = "black"
2121
known_first_party = ["nf_core"]
2222
multi_line_output = 3
23+
24+
[tool.mypy]
25+
ignore_missing_imports = true
26+
follow_imports = "skip"
27+
disable_error_code = "no-redef"

tests/lint/nextflow_config.py

+20
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import os
2+
import re
3+
14
import nf_core.create
25
import nf_core.lint
36

@@ -33,3 +36,20 @@ def test_nextflow_config_dev_in_release_mode_failed(self):
3336
result = lint_obj.nextflow_config()
3437
assert len(result["failed"]) > 0
3538
assert len(result["warned"]) == 0
39+
40+
41+
def test_nextflow_config_missing_test_profile_failed(self):
42+
"""Test failure if config file does not contain `test` profile."""
43+
new_pipeline = self._make_pipeline_copy()
44+
# Change the name of the test profile so there is no such profile
45+
nf_conf_file = os.path.join(new_pipeline, "nextflow.config")
46+
with open(nf_conf_file, "r") as f:
47+
content = f.read()
48+
fail_content = re.sub(r"\btest\b", "testfail", content)
49+
with open(nf_conf_file, "w") as f:
50+
f.write(fail_content)
51+
lint_obj = nf_core.lint.PipelineLint(new_pipeline)
52+
lint_obj._load_pipeline_config()
53+
result = lint_obj.nextflow_config()
54+
assert len(result["failed"]) > 0
55+
assert len(result["warned"]) == 0

tests/test_lint.py

+1
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ def test_sphinx_md_files(self):
213213
test_nextflow_config_bad_name_fail,
214214
test_nextflow_config_dev_in_release_mode_failed,
215215
test_nextflow_config_example_pass,
216+
test_nextflow_config_missing_test_profile_failed,
216217
)
217218
from .lint.version_consistency import test_version_consistency
218219

0 commit comments

Comments
 (0)