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

Feature/python typing/components command.py #2223

Merged
Merged
Show file tree
Hide file tree
Changes from 37 commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
8295f77
my repo test
NovakApis Mar 28, 2023
b803580
Merge branch 'dev' into feature/python-typing/components_command.py
NovakApis Mar 28, 2023
68513c0
black + isort
NovakApis Mar 28, 2023
203a04b
first test
NovakApis Mar 28, 2023
2b044f8
python-typing/components_command.py
NovakApis Mar 28, 2023
31b474d
add mypy ci check
fabianegli Mar 28, 2023
47b9779
made some variable types depend on function return value
NovakApis Mar 29, 2023
ef93b96
updated __init__ keyword arguments
NovakApis Mar 29, 2023
5262237
Merge branch 'dev' into feature/python-typing/components_command.py
fabianegli Mar 29, 2023
4afe175
make mypy ignore missing imports
fabianegli Mar 29, 2023
a679153
mypy configuration edit
NovakApis Mar 29, 2023
a1119f6
use mypy without pre-built action
fabianegli Apr 3, 2023
ba848af
fix typo in mypy target directory
fabianegli Apr 3, 2023
2003c5e
install missing type annotations for mypy
fabianegli Apr 3, 2023
a503c16
install types non-interactive and with a target
fabianegli Apr 3, 2023
3d978de
install mypy check requirements explicitly
fabianegli Apr 3, 2023
e3348df
install tools dependencies for mypy checks
fabianegli Apr 3, 2023
325f043
allow redefinition in mypy checks
fabianegli Apr 3, 2023
0b25bfa
ignore redefinitions in mypy checks
fabianegli Apr 3, 2023
3c75ee8
use tool prefix for mypy section
fabianegli Apr 3, 2023
7aa26f5
make disable error code a string
fabianegli Apr 3, 2023
d7b2c04
Merge branch 'dev' into feature/python-typing/components_command.py
kedhammar Oct 16, 2023
e91f64e
Update nf_core/components/components_command.py
kedhammar Oct 16, 2023
3b75418
Update nf_core/components/components_command.py
kedhammar Oct 16, 2023
594f62f
Update nf_core/components/components_command.py
kedhammar Oct 16, 2023
64cef2f
Update nf_core/components/components_command.py
kedhammar Oct 16, 2023
9291733
Update nf_core/components/components_command.py
kedhammar Oct 16, 2023
c394848
Update nf_core/components/components_command.py
kedhammar Oct 16, 2023
f31c933
Update nf_core/components/components_command.py
kedhammar Oct 16, 2023
c5c8cda
Try adding mypy to pre-commit config
kedhammar Oct 16, 2023
681eb5e
Fix invalid pre-commit config
kedhammar Oct 16, 2023
fc1be87
Revert pre-commit change for now
kedhammar Oct 16, 2023
315438d
Try changing CI to only run mypy on changed files
kedhammar Oct 16, 2023
dcaa124
Differentiate dir arg (optional) from dir path
kedhammar Oct 16, 2023
9db5fd5
Declare type within func
kedhammar Oct 16, 2023
d9972b6
Try installing MyPy stubs
kedhammar Oct 17, 2023
6d8156f
Specify missing stubs
kedhammar Oct 17, 2023
62cb1b9
try passing mypy
kedhammar Oct 17, 2023
95eb4b3
prettier
kedhammar Oct 17, 2023
bf60f5f
bump python version
kedhammar Oct 17, 2023
dba49b3
bugfix
kedhammar Oct 17, 2023
73e2e72
try reverting variable differentiation and instead adapt initial typi…
kedhammar Oct 17, 2023
2c234bd
Don't combine typing square bracket syntax with non-subscriptable abc…
kedhammar Oct 17, 2023
78b078c
Try further simplifying typing to prevent pytest suite from trying to…
kedhammar Oct 17, 2023
fbeafa4
extend last commit to this file
kedhammar Oct 17, 2023
b91c88e
Supplement typing and remove superfluous code
kedhammar Oct 17, 2023
6b4f9ff
isort
kedhammar Oct 17, 2023
d5c11b5
supplement typing, 2 errors left
kedhammar Oct 17, 2023
3d81425
1 error left
kedhammar Oct 17, 2023
5c88dbf
Try fixing typing error by clarifying list-of-dicts parsing
kedhammar Oct 17, 2023
a7b26b3
Fix mypy complaints across entire repo
kedhammar Oct 17, 2023
ce13018
add stubs
kedhammar Oct 17, 2023
12a8adf
isort
kedhammar Oct 17, 2023
c97ef6e
try reading custom config in GHA
kedhammar Oct 17, 2023
ee920c8
Revert last 4 commits
kedhammar Oct 17, 2023
989f1b7
Update .github/workflows/lint-code.yml
kedhammar Oct 17, 2023
56b6a08
Update .github/workflows/lint-code.yml
kedhammar Oct 17, 2023
b518c1c
Update nf_core/components/components_command.py
kedhammar Oct 17, 2023
4e71fb8
Update nf_core/components/components_utils.py
kedhammar Oct 17, 2023
7edf84f
Update nf_core/components/components_utils.py
kedhammar Oct 17, 2023
ddf4fda
import missing typing obj
kedhammar Oct 17, 2023
e88d721
black
kedhammar Oct 17, 2023
9171fa0
revert minor tweak to prevent breaking
kedhammar Oct 17, 2023
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
20 changes: 20 additions & 0 deletions .github/workflows/lint-code.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,23 @@ jobs:
with:
isortVersion: "latest"
requirementsFiles: "requirements.txt requirements-dev.txt"

static-type-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v3
with:
python-version: '3.11'
kedhammar marked this conversation as resolved.
Show resolved Hide resolved
- run: pip install mypy
- run: pip install types-PyYAML
- name: Get Python changed files
id: changed-py-files
uses: tj-actions/changed-files@v23
with:
files: |
*.py
**/*.py
- name: Run if any of the listed files above is changed
if: steps.changed-py-files.outputs.any_changed == 'true'
run: mypy ${{ steps.changed-py-files.outputs.all_changed_files }} --ignore-missing-imports
1 change: 1 addition & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ repos:
rev: "v2.7.1"
hooks:
- id: prettier

104 changes: 58 additions & 46 deletions nf_core/components/components_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import os
import shutil
from pathlib import Path
from typing import Dict, List, Optional, Tuple, Union

import yaml

Expand All @@ -20,17 +21,25 @@ class ComponentCommand:
Base class for the 'nf-core modules' and 'nf-core subworkflows' commands
"""

def __init__(self, component_type, dir, remote_url=None, branch=None, no_pull=False, hide_progress=False):
def __init__(
self,
component_type: str,
dir_arg: Optional[Union[str, os.PathLike[str]]],
remote_url: Optional[str] = None,
branch: Optional[str] = None,
no_pull: bool = False,
hide_progress: bool = False,
) -> None:
"""
Initialise the ComponentClass object
"""
self.component_type = component_type
self.dir = dir
self.modules_repo = ModulesRepo(remote_url, branch, no_pull, hide_progress)
self.dir_arg = dir_arg
self.modules_repo: ModulesRepo = ModulesRepo(remote_url, branch, no_pull, hide_progress)
self.hide_progress = hide_progress
self._configure_repo_and_paths()

def _configure_repo_and_paths(self, nf_dir_req=True):
def _configure_repo_and_paths(self, nf_dir_req: Optional[bool] = True) -> None:
kedhammar marked this conversation as resolved.
Show resolved Hide resolved
"""
Determine the repo type and set some default paths.
If this is a modules repo, determine the org_path too.
Expand All @@ -39,8 +48,9 @@ def _configure_repo_and_paths(self, nf_dir_req=True):
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.
"""
try:
if self.dir:
self.dir, self.repo_type, self.org = get_repo_info(self.dir, use_prompt=nf_dir_req)
if self.dir_arg:
self.dir_path: os.PathLike[str]
self.dir_path, self.repo_type, self.org = get_repo_info(self.dir_path, use_prompt=nf_dir_req)
else:
self.repo_type = None
self.org = ""
Expand All @@ -49,57 +59,57 @@ def _configure_repo_and_paths(self, nf_dir_req=True):
raise
self.repo_type = None
self.org = ""
self.default_modules_path = Path("modules", self.org)
self.default_tests_path = Path("tests", "modules", self.org)
self.default_subworkflows_path = Path("subworkflows", self.org)
self.default_subworkflows_tests_path = Path("tests", "subworkflows", self.org)
self.default_modules_path: Path = Path("modules", self.org)
self.default_tests_path: Path = Path("tests", "modules", self.org)
self.default_subworkflows_path: Path = Path("subworkflows", self.org)
self.default_subworkflows_tests_path: Path = Path("tests", "subworkflows", self.org)

def get_local_components(self):
def get_local_components(self) -> List[str]:
"""
Get the local modules/subworkflows in a pipeline
"""
local_component_dir = Path(self.dir, self.component_type, "local")
local_component_dir = Path(self.dir_path, self.component_type, "local")
return [
str(path.relative_to(local_component_dir)) for path in local_component_dir.iterdir() if path.suffix == ".nf"
]

def get_components_clone_modules(self):
def get_components_clone_modules(self) -> List[str]:
"""
Get the modules/subworkflows repository available in a clone of nf-core/modules
"""
if self.component_type == "modules":
component_base_path = Path(self.dir, self.default_modules_path)
component_base_path = Path(self.dir_path, self.default_modules_path)
elif self.component_type == "subworkflows":
component_base_path = Path(self.dir, self.default_subworkflows_path)
component_base_path = Path(self.dir_path, self.default_subworkflows_path)
return [
str(Path(dir).relative_to(component_base_path))
for dir, _, files in os.walk(component_base_path)
if "main.nf" in files
]

def has_valid_directory(self):
def has_valid_directory(self) -> bool:
"""Check that we were given a pipeline or clone of nf-core/modules"""
if self.repo_type == "modules":
return True
if self.dir is None or not os.path.exists(self.dir):
log.error(f"Could not find directory: {self.dir}")
if self.dir_path is None or not os.path.exists(self.dir_path):
log.error(f"Could not find directory: {self.dir_path}")
return False
main_nf = os.path.join(self.dir, "main.nf")
nf_config = os.path.join(self.dir, "nextflow.config")
main_nf = os.path.join(self.dir_path, "main.nf")
nf_config = os.path.join(self.dir_path, "nextflow.config")
if not os.path.exists(main_nf) and not os.path.exists(nf_config):
if Path(self.dir).resolve().parts[-1].startswith("nf-core"):
raise UserWarning(f"Could not find a 'main.nf' or 'nextflow.config' file in '{self.dir}'")
log.warning(f"Could not find a 'main.nf' or 'nextflow.config' file in '{self.dir}'")
if Path(self.dir_path).resolve().parts[-1].startswith("nf-core"):
raise UserWarning(f"Could not find a 'main.nf' or 'nextflow.config' file in '{self.dir_path}'")
log.warning(f"Could not find a 'main.nf' or 'nextflow.config' file in '{self.dir_path}'")
return True

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

def clear_component_dir(self, component_name, component_dir):
def clear_component_dir(self, component_name: str, component_dir: str) -> bool:
"""
Removes all files in the module/subworkflow directory

Expand All @@ -112,7 +122,7 @@ def clear_component_dir(self, component_name, component_dir):
try:
shutil.rmtree(component_dir)
# remove all empty directories
for dir_path, dir_names, filenames in os.walk(self.dir, topdown=False):
for dir_path, dir_names, filenames in os.walk(self.dir_path, topdown=False):
if not dir_names and not filenames:
try:
os.rmdir(dir_path)
Expand All @@ -127,7 +137,7 @@ def clear_component_dir(self, component_name, component_dir):
log.error(f"Could not remove {self.component_type[:-1]} {component_name}: {e}")
return False

def components_from_repo(self, install_dir):
def components_from_repo(self, install_dir: str) -> List[str]:
"""
Gets the modules/subworkflows installed from a certain repository

Expand All @@ -137,15 +147,17 @@ def components_from_repo(self, install_dir):
Returns:
[str]: The names of the modules/subworkflows
"""
repo_dir = Path(self.dir, self.component_type, install_dir)
repo_dir = Path(self.dir_path, self.component_type, install_dir)
if not repo_dir.exists():
raise LookupError(f"Nothing installed from {install_dir} in pipeline")

return [
str(Path(dir_path).relative_to(repo_dir)) for dir_path, _, files in os.walk(repo_dir) if "main.nf" in files
]

def install_component_files(self, component_name, component_version, modules_repo, install_dir):
def install_component_files(
self, component_name: str, component_version: str, modules_repo: ModulesRepo, install_dir: str
) -> bool:
"""
Installs a module/subworkflow into the given directory

Expand All @@ -160,18 +172,18 @@ def install_component_files(self, component_name, component_version, modules_rep
"""
return modules_repo.install_component(component_name, install_dir, component_version, self.component_type)

def load_lint_config(self):
def load_lint_config(self) -> None:
"""Parse a pipeline lint config file.

Load the '.nf-core.yml' config file and extract
the lint config from it

Add parsed config to the `self.lint_config` class attribute.
"""
_, tools_config = nf_core.utils.load_tools_config(self.dir)
_, tools_config = nf_core.utils.load_tools_config(self.dir_path)
self.lint_config = tools_config.get("lint", {})

def check_modules_structure(self):
def check_modules_structure(self) -> None:
"""
Check that the structure of the modules directory in a pipeline is the correct one:
modules/nf-core/TOOL/SUBTOOL
Expand All @@ -180,10 +192,10 @@ def check_modules_structure(self):
modules/nf-core/modules/TOOL/SUBTOOL
"""
if self.repo_type == "pipeline":
wrong_location_modules = []
for directory, _, files in os.walk(Path(self.dir, "modules")):
wrong_location_modules: List[Path] = []
for directory, _, files in os.walk(Path(self.dir_path, "modules")):
if "main.nf" in files:
module_path = Path(directory).relative_to(Path(self.dir, "modules"))
module_path = Path(directory).relative_to(Path(self.dir_path, "modules"))
parts = module_path.parts
# Check that there are modules installed directly under the 'modules' directory
if parts[1] == "modules":
Expand All @@ -201,14 +213,14 @@ def check_modules_structure(self):
modules_dir = Path("modules").resolve()
correct_dir = Path(modules_dir, self.modules_repo.repo_path, Path(*module.parts[2:]))
wrong_dir = Path(modules_dir, module)
shutil.move(wrong_dir, correct_dir)
shutil.move(str(wrong_dir), str(correct_dir))
log.info(f"Moved {wrong_dir} to {correct_dir}.")
shutil.rmtree(Path(self.dir, "modules", self.modules_repo.repo_path, "modules"))
shutil.rmtree(Path(self.dir_path, "modules", self.modules_repo.repo_path, "modules"))
# Regenerate modules.json file
modules_json = ModulesJson(self.dir)
modules_json = ModulesJson(self.dir_path)
modules_json.check_up_to_date()

def check_patch_paths(self, patch_path, module_name):
def check_patch_paths(self, patch_path: Path, module_name: str) -> None:
"""
Check that paths in patch files are updated to the new modules path
"""
Expand All @@ -231,15 +243,15 @@ def check_patch_paths(self, patch_path, module_name):
for line in lines:
fh.write(line)
# Update path in modules.json if the file is in the correct format
modules_json = ModulesJson(self.dir)
modules_json = ModulesJson(self.dir_path)
modules_json.load()
if modules_json.has_git_url_and_modules():
modules_json.modules_json["repos"][self.modules_repo.remote_url]["modules"][
self.modules_repo.repo_path
][module_name]["patch"] = str(patch_path.relative_to(Path(self.dir).resolve()))
][module_name]["patch"] = str(patch_path.relative_to(Path(self.dir_path).resolve()))
modules_json.dump()

def check_if_in_include_stmts(self, component_path):
def check_if_in_include_stmts(self, component_path: str) -> Dict[str, List[Dict[str, Union[int, str]]]]:
"""
Checks for include statements in the main.nf file of the pipeline and a list of line numbers where the component is included
Args:
Expand All @@ -248,9 +260,9 @@ def check_if_in_include_stmts(self, component_path):
Returns:
(list): A list of dictionaries, with the workflow file and the line number where the component is included
"""
include_stmts = {}
include_stmts: Dict[str, List[Dict[str, Union[int, str]]]] = {}
if self.repo_type == "pipeline":
workflow_files = Path(self.dir, "workflows").glob("*.nf")
workflow_files = Path(self.dir_path, "workflows").glob("*.nf")
for workflow_file in workflow_files:
with open(workflow_file, "r") as fh:
# Check if component path is in the file using mmap
Expand Down
9 changes: 6 additions & 3 deletions nf_core/components/components_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import os
import re
from pathlib import Path
from typing import Optional, Tuple, Union

import questionary
import rich.prompt
Expand All @@ -11,7 +12,9 @@
log = logging.getLogger(__name__)


def get_repo_info(directory, use_prompt=True):
def get_repo_info(
directory: Union[str, Path], use_prompt=True
) -> Tuple[Union[str, Path], Optional[str], Optional[str]]:
"""
Determine whether this is a pipeline repository or a clone of
nf-core/modules
Expand All @@ -21,7 +24,7 @@ def get_repo_info(directory, use_prompt=True):
raise UserWarning(f"Could not find directory: {directory}")

# Try to find the root directory
base_dir = nf_core.utils.determine_base_dir(directory)
base_dir: Path = nf_core.utils.determine_base_dir(directory)

# Figure out the repository type from the .nf-core.yml config file if we can
config_fn, tools_config = nf_core.utils.load_tools_config(base_dir)
Expand Down Expand Up @@ -77,7 +80,7 @@ def get_repo_info(directory, use_prompt=True):
raise UserWarning("Organisation path could not be established")

# It was set on the command line, return what we were given
return [base_dir, repo_type, org]
return (base_dir, repo_type, org)


def prompt_component_version_sha(component_name, component_type, modules_repo, installed_sha=None):
Expand Down
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,8 @@ norecursedirs = [ ".*", "build", "dist", "*.egg", "data", "__pycache__", ".githu
profile = "black"
known_first_party = ["nf_core"]
multi_line_output = 3

[tool.mypy]
ignore_missing_imports = true
follow_imports = "skip"
fabianegli marked this conversation as resolved.
Show resolved Hide resolved
disable_error_code = "no-redef"
Loading