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

Increase worflow linting #1463

Merged
merged 29 commits into from
Jul 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
64c684c
increase worflow linting
lldelisle Jun 29, 2024
af6758e
linting
lldelisle Jun 29, 2024
1d61ca1
restore build_lint_args
lldelisle Jun 29, 2024
dd9b1bb
fix cases with no authors in dockstore.yml
lldelisle Jun 29, 2024
f7401d8
fix DOCKSTORE_REGISTRY_CONF_VERSION
lldelisle Jun 29, 2024
d4ae50f
change name from required to recommanded
lldelisle Jun 29, 2024
619cae9
linting
lldelisle Jun 29, 2024
eb86e3b
fix forgotten consequence of name from required to recommanded
lldelisle Jun 29, 2024
5516443
put iwc_grade in lint_args as suggested by @bernt-matthias
lldelisle Jul 1, 2024
8639d1d
remove release/version from WorkflowLintContext as suggested by @matt…
lldelisle Jul 1, 2024
5c19e56
fix type of lint_args
lldelisle Jul 1, 2024
302c504
remove iwc_grade from WorkflowLintContext and add lint_dockstore_best…
lldelisle Jul 1, 2024
1310eb7
new build_wf_lint_args to match what is happening with tools
lldelisle Jul 1, 2024
906c5ca
fix error on CHANGELOG get version
lldelisle Jul 1, 2024
39480a8
add tests
lldelisle Jul 1, 2024
7bb3a50
restore comma
lldelisle Jul 1, 2024
db8f24c
test --iwc on file
lldelisle Jul 1, 2024
22112e8
remove path from asserts output
lldelisle Jul 1, 2024
147296a
run black
lldelisle Jul 1, 2024
f5af20f
run isort
lldelisle Jul 1, 2024
6e8f0ce
fix strange output when no --skip
lldelisle Jul 1, 2024
f748c11
fix final test
lldelisle Jul 1, 2024
0d8dbd6
change strategy for iwc on files
lldelisle Jul 1, 2024
f88024c
Merge remote-tracking branch 'upstream/master' into lint_workflow
lldelisle Jul 1, 2024
64f9076
add comma
lldelisle Jul 1, 2024
bafecff
Add type annotation
nsoranzo Jul 2, 2024
b1eedf4
Apply suggestions from @bernt-matthias
lldelisle Jul 2, 2024
ecdcf0f
fix lint_args typing to different places
lldelisle Jul 2, 2024
2f7c139
remove unused Union
lldelisle Jul 2, 2024
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
7 changes: 5 additions & 2 deletions planemo/commands/cmd_lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
import click

from planemo import options
from planemo.cli import command_function
from planemo.cli import (
command_function,
PlanemoCliContext,
)
from planemo.tool_lint import (
build_tool_lint_args,
lint_tools_on_path,
Expand Down Expand Up @@ -43,7 +46,7 @@
# default=False,
# )
@command_function
def cli(ctx, uris, **kwds):
def cli(ctx: PlanemoCliContext, uris, **kwds):
"""Check for common errors and best practices."""
lint_args = build_tool_lint_args(ctx, **kwds)
exit_code = lint_tools_on_path(ctx, uris, lint_args, recursive=kwds["recursive"])
Expand Down
7 changes: 5 additions & 2 deletions planemo/commands/cmd_shed_lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@
shed,
shed_lint,
)
from planemo.cli import command_function
from planemo.cli import (
command_function,
PlanemoCliContext,
)


@click.command("shed_lint")
Expand Down Expand Up @@ -41,7 +44,7 @@
# default=False,
# )
@command_function
def cli(ctx, paths, **kwds):
def cli(ctx: PlanemoCliContext, paths, **kwds):
"""Check Tool Shed repository for common issues.

With the ``--tools`` flag, this command lints actual Galaxy tools
Expand Down
21 changes: 16 additions & 5 deletions planemo/commands/cmd_workflow_lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,14 @@
import click

from planemo import options
from planemo.cli import command_function
from planemo.lint import build_lint_args
from planemo.workflow_lint import lint_workflow_artifacts_on_paths
from planemo.cli import (
command_function,
PlanemoCliContext,
)
from planemo.workflow_lint import (
build_wf_lint_args,
lint_workflow_artifacts_on_paths,
)


@click.command("workflow_lint")
Expand All @@ -14,11 +19,17 @@
@options.report_xunit()
@options.fail_level_option()
@options.skip_option()
@click.option(
"--iwc",
is_flag=True,
default=False,
help="Check workflows directory with the standards of iwc",
)
@command_function
def cli(ctx, paths, **kwds):
def cli(ctx: PlanemoCliContext, paths, **kwds):
"""Check workflows for syntax errors and best practices."""
# Unlike tools, lets just make this recursive by default.
lint_args = build_lint_args(ctx, **kwds)
lint_args = build_wf_lint_args(ctx, **kwds)
exit_code = lint_workflow_artifacts_on_paths(
ctx,
paths,
Expand Down
12 changes: 10 additions & 2 deletions planemo/lint.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
"""Utilities to help linting various targets."""

import os
from typing import (
Any,
Dict,
TYPE_CHECKING,
)
from urllib.request import urlopen

import requests
Expand All @@ -13,8 +18,11 @@
from planemo.shed import find_urls_for_xml
from planemo.xml import validation

if TYPE_CHECKING:
from planemo.cli import PlanemoCliContext


def build_lint_args(ctx, **kwds):
def build_lint_args(ctx: "PlanemoCliContext", **kwds) -> Dict[str, Any]:
"""Handle common report, error, and skip linting arguments."""
report_level = kwds.get("report_level", "all")
fail_level = kwds.get("fail_level", "warn")
Expand All @@ -39,7 +47,7 @@ def build_lint_args(ctx, **kwds):
if len(invalid_skip_types):
error(f"Unknown linter type(s) {invalid_skip_types} in list of linters to be skipped. Known linters {linters}")

lint_args = dict(
lint_args: Dict[str, Any] = dict(
level=report_level,
fail_level=fail_level,
skip_types=skip_types,
Expand Down
6 changes: 5 additions & 1 deletion planemo/shed_lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import os
import xml.etree.ElementTree as ET
from typing import TYPE_CHECKING

import yaml
from galaxy.tool_util.lint import lint_tool_source_with
Expand Down Expand Up @@ -31,6 +32,9 @@
from planemo.tools import yield_tool_sources
from planemo.xml import XSDS_PATH

if TYPE_CHECKING:
from planemo.cli import PlanemoCliContext

TOOL_DEPENDENCIES_XSD = os.path.join(XSDS_PATH, "tool_dependencies.xsd")
REPO_DEPENDENCIES_XSD = os.path.join(XSDS_PATH, "repository_dependencies.xsd")

Expand All @@ -50,7 +54,7 @@
]


def lint_repository(ctx, realized_repository, **kwds):
def lint_repository(ctx: "PlanemoCliContext", realized_repository, **kwds):
"""Lint a realized shed repository.

See :mod:`planemo.shed` for details on constructing a realized
Expand Down
10 changes: 9 additions & 1 deletion planemo/tool_lint.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
from os.path import basename
from typing import (
Any,
Dict,
TYPE_CHECKING,
)

from galaxy.tool_util.lint import lint_tool_source

Expand All @@ -22,10 +27,13 @@
yield_tool_sources_on_paths,
)

if TYPE_CHECKING:
from planemo.cli import PlanemoCliContext

LINTING_TOOL_MESSAGE = "Linting tool %s"


def build_tool_lint_args(ctx, **kwds):
def build_tool_lint_args(ctx: "PlanemoCliContext", **kwds) -> Dict[str, Any]:
lint_args = build_lint_args(ctx, **kwds)
extra_modules = _lint_extra_modules(**kwds)
lint_args["extra_modules"] = extra_modules
Expand Down
142 changes: 130 additions & 12 deletions planemo/workflow_lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
Optional,
Tuple,
TYPE_CHECKING,
Union,
)

import requests
Expand All @@ -39,6 +38,7 @@
output_labels,
required_input_labels,
)
from planemo.lint import build_lint_args
from planemo.runnable import (
cases,
for_path,
Expand All @@ -59,6 +59,12 @@ class WorkflowLintContext(LintContext):
training_topic = None


def build_wf_lint_args(ctx: "PlanemoCliContext", **kwds) -> Dict[str, Any]:
lint_args = build_lint_args(ctx, **kwds)
lint_args["iwc_grade"] = kwds.get("iwc", False)
return lint_args


def generate_dockstore_yaml(directory: str, publish: bool = True) -> str:
workflows = []
all_workflow_paths = list(find_workflow_descriptions(directory))
Expand Down Expand Up @@ -121,9 +127,7 @@ def generate_dockstore_yaml(directory: str, publish: bool = True) -> str:
return contents


def lint_workflow_artifacts_on_paths(
ctx: "PlanemoCliContext", paths: Iterable[str], lint_args: Dict[str, Union[str, List[str]]]
) -> int:
def lint_workflow_artifacts_on_paths(ctx: "PlanemoCliContext", paths: Iterable[str], lint_args: Dict[str, Any]) -> int:
report_level = lint_args["level"]
lint_context = WorkflowLintContext(report_level, skip_types=lint_args["skip_types"])
for path in paths:
Expand All @@ -135,12 +139,22 @@ def lint_workflow_artifacts_on_paths(
return EXIT_CODE_OK


def _lint_workflow_artifacts_on_path(
lint_context: WorkflowLintContext, path: str, lint_args: Dict[str, Union[str, List[str]]]
) -> None:
def _lint_workflow_artifacts_on_path(lint_context: WorkflowLintContext, path: str, lint_args: Dict[str, Any]) -> None:
if lint_args["iwc_grade"]:
if not os.path.isdir(path):
lldelisle marked this conversation as resolved.
Show resolved Hide resolved
path = os.path.dirname(path)
lint_context.lint("lint_required_files", _lint_required_files_workflow_dir, path)
lint_context.lint("lint_changelog", _lint_changelog_version, path)

for potential_workflow_artifact_path in find_potential_workflow_files(path):
if os.path.basename(potential_workflow_artifact_path) == DOCKSTORE_REGISTRY_CONF:
lint_context.lint("lint_dockstore", _lint_dockstore_config, potential_workflow_artifact_path)
if lint_args["iwc_grade"]:
lint_context.lint(
"lint_dockstore_best_practices",
_lint_dockstore_config_best_practices,
potential_workflow_artifact_path,
)

elif looks_like_a_workflow(potential_workflow_artifact_path):

Expand All @@ -152,6 +166,8 @@ def structure(path, lint_context):
lint_func(lint_context, workflow_dict, path=path)

lint_context.lint("lint_structure", structure, potential_workflow_artifact_path)
if lint_args["iwc_grade"]:
lint_context.lint("lint_release", _lint_release, potential_workflow_artifact_path)
lint_context.lint("lint_best_practices", _lint_best_practices, potential_workflow_artifact_path)
lint_context.lint("lint_tests", _lint_tsts, potential_workflow_artifact_path)
lint_context.lint("lint_tool_ids", _lint_tool_ids, potential_workflow_artifact_path)
Expand Down Expand Up @@ -394,6 +410,11 @@ def _lint_dockstore_config(path: str, lint_context: WorkflowLintContext) -> None
lint_context.error("Invalid YAML contents found in %s" % DOCKSTORE_REGISTRY_CONF)
return

if "version" not in dockstore_yaml:
lint_context.error("Invalid YAML contents found in %s, no version defined" % DOCKSTORE_REGISTRY_CONF)
if str(dockstore_yaml.get("version")) != DOCKSTORE_REGISTRY_CONF_VERSION:
lint_context.error("Invalid YAML version found in %s." % DOCKSTORE_REGISTRY_CONF)

if "workflows" not in dockstore_yaml:
lint_context.error("Invalid YAML contents found in %s, no workflows defined" % DOCKSTORE_REGISTRY_CONF)
return
Expand All @@ -403,8 +424,16 @@ def _lint_dockstore_config(path: str, lint_context: WorkflowLintContext) -> None
lint_context.error("Invalid YAML contents found in %s, workflows not a list" % DOCKSTORE_REGISTRY_CONF)
return

if len(workflow_entries) == 0:
lint_context.error("No workflow specified in the .dockstore.yml.")

workflow_names_in_dockstore = []
for workflow_entry in workflow_entries:
_lint_dockstore_workflow_entry(lint_context, os.path.dirname(path), workflow_entry)
workflow_name = workflow_entry.get("name", "")
if workflow_name in workflow_names_in_dockstore:
lint_context.error(f"{DOCKSTORE_REGISTRY_CONF} has multiple workflow entries with the same name")
workflow_names_in_dockstore.append(workflow_name)


def _lint_dockstore_workflow_entry(
Expand All @@ -420,15 +449,13 @@ def _lint_dockstore_workflow_entry(
lint_context.error(f"{DOCKSTORE_REGISTRY_CONF} workflow entry missing required key {required_key}")
found_errors = True

for recommended_key in ["testParameterFiles"]:
if recommended_key not in workflow_entry:
lint_context.warn(f"{DOCKSTORE_REGISTRY_CONF} workflow entry missing recommended key {recommended_key}")

if found_errors:
# Don't do the rest of the validation for a broken file.
return

# TODO: validate subclass
if workflow_entry.get("subclass") != "Galaxy":
lint_context.error(f"{DOCKSTORE_REGISTRY_CONF} workflow entry subclass must be 'Galaxy'.")

descriptor_path = workflow_entry["primaryDescriptorPath"]
test_files = workflow_entry.get("testParameterFiles", [])

Expand All @@ -437,6 +464,20 @@ def _lint_dockstore_workflow_entry(
if not os.path.exists(referenced_path):
lint_context.error(f"{DOCKSTORE_REGISTRY_CONF} workflow entry references absent file {referenced_file}")

# Check there is no space in name:
workflow_name = workflow_entry.get("name", "")
# Check the name has no space
if " " in workflow_name:
lint_context.error(
"Dockstore does not accept workflow names with space.",
f"Change '{workflow_name}' in {DOCKSTORE_REGISTRY_CONF}.",
)

# Check there is not mailto
for author in workflow_entry.get("authors", []):
if author.get("email", "").startswith("mailto:"):
lint_context.error("email field of the .dockstore.yml must not " "contain 'mailto:'")


def looks_like_a_workflow(path: str) -> bool:
"""Return boolean indicating if this path looks like a workflow."""
Expand Down Expand Up @@ -537,3 +578,80 @@ def _lint_tool_ids_steps(lint_context: WorkflowLintContext, wf_dict: Dict, ts: T
if not failed:
lint_context.valid("All tool ids appear to be valid.")
return None


def _lint_required_files_workflow_dir(path: str, lint_context: WorkflowLintContext) -> None:
# Check all required files are present
required_files = ["README.md", "CHANGELOG.md", ".dockstore.yml"]
for required_file in required_files:
if not os.path.exists(os.path.join(path, required_file)):
lint_context.error(f"The file {required_file} is" " missing but required.")


def _get_changelog_version(path: str) -> str:
# Get the version from the CHANGELOG.md
version = ""
if not os.path.exists(os.path.join(path, "CHANGELOG.md")):
return version
with open(os.path.join(path, "CHANGELOG.md"), "r") as f:
for line in f:
if line.startswith("## ["):
version = line.split("]")[0].replace("## [", "")
break
return version


def _lint_changelog_version(path: str, lint_context: WorkflowLintContext) -> None:
# Check the version can be get from the CHANGELOG.md
if not os.path.exists(os.path.join(path, "CHANGELOG.md")):
return
if _get_changelog_version(path) == "":
lint_context.error(
"No version found in CHANGELOG. "
"The version should be in a line that starts like "
"'## [version number]'"
)


def _lint_release(path, lint_context):
with open(path) as f:
workflow_dict = ordered_load(f)
if "release" not in workflow_dict:
lint_context.error(f"The workflow {path} has no release")
else:
# Try to get the version from the CHANGELOG:
version = _get_changelog_version(os.path.dirname(path))
if version != "" and workflow_dict.get("release") != version:
lint_context.error(f"The release of workflow {path} does not match " "the version in the CHANGELOG.")


def _lint_dockstore_config_best_practices(path: str, lint_context: WorkflowLintContext) -> None:
dockstore_yaml = None
try:
with open(path) as f:
dockstore_yaml = yaml.safe_load(f)
except Exception:
return

if not isinstance(dockstore_yaml, dict):
return

workflow_entries = dockstore_yaml.get("workflows")
if not isinstance(workflow_entries, list):
return

for workflow_entry in workflow_entries:
_lint_dockstore_workflow_entry_best_practices(lint_context, os.path.dirname(path), workflow_entry)


def _lint_dockstore_workflow_entry_best_practices(
lint_context: WorkflowLintContext, directory: str, workflow_entry: Dict[str, Any]
) -> None:
for recommended_key in ["testParameterFiles", "name"]:
if recommended_key not in workflow_entry:
lint_context.error(f"{DOCKSTORE_REGISTRY_CONF} workflow entry missing recommended key {recommended_key}")

workflow_name = workflow_entry.get("name", "")
# Check there is at least one author
if len(workflow_entry.get("authors", [])) == 0:
lint_context.error(f"Workflow {workflow_name} have no 'authors' in the {DOCKSTORE_REGISTRY_CONF}.")
Loading
Loading