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 25 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
14 changes: 11 additions & 3 deletions planemo/commands/cmd_workflow_lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@

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.workflow_lint import (
build_wf_lint_args,
lint_workflow_artifacts_on_paths,
)


@click.command("workflow_lint")
Expand All @@ -14,11 +16,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):
"""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
134 changes: 129 additions & 5 deletions planemo/workflow_lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
output_labels,
required_input_labels,
)
from planemo.lint import build_lint_args
from planemo.runnable import (
cases,
for_path,
Expand All @@ -59,6 +60,12 @@ class WorkflowLintContext(LintContext):
training_topic = None


def build_wf_lint_args(ctx, **kwds):
lint_args = build_lint_args(ctx, **kwds)
lint_args["iwc_grade"] = str(kwds.get("iwc", False))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
lint_args["iwc_grade"] = str(kwds.get("iwc", False))
lint_args["iwc_grade"] = kwds.get("iwc", False)

We should just leave this a boolean.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I keep it as boolean then I get a type violation as lint_args is supposed to be a dict of str -> str

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See

ctx: "PlanemoCliContext", paths: Iterable[str], lint_args: Dict[str, Union[str, List[str]]]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or I change it to : lint_args: Dict[str, Union[str, List[str], bool]] ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dict[str, Any] is probably enough, I'll push a commit to your branch.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, if not I can commit it.

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 @@ -138,9 +145,22 @@ def lint_workflow_artifacts_on_paths(
def _lint_workflow_artifacts_on_path(
lint_context: WorkflowLintContext, path: str, lint_args: Dict[str, Union[str, List[str]]]
) -> None:
iwc_grade = lint_args["iwc_grade"] == "True"
if iwc_grade:
lldelisle marked this conversation as resolved.
Show resolved Hide resolved
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 iwc_grade:
lldelisle marked this conversation as resolved.
Show resolved Hide resolved
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 +172,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 iwc_grade:
lldelisle marked this conversation as resolved.
Show resolved Hide resolved
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 +416,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 +430,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 +455,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 +470,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 +584,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}.")
11 changes: 11 additions & 0 deletions tests/data/wf_repos/basic_wf_iwc_invalid_version/.dockstore.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
version: 1.2
workflows:
- name: main
subclass: Galaxy
publish: true
primaryDescriptorPath: /Super-simple-workflow.ga
testParameterFiles:
- /Super-simple-workflow-tests.yml
authors:
- name: Lucille Delisle
orcid: 0000-0002-1964-4960
5 changes: 5 additions & 0 deletions tests/data/wf_repos/basic_wf_iwc_invalid_version/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Changelog

## [0.1] 2024-06-17

First release.
3 changes: 3 additions & 0 deletions tests/data/wf_repos/basic_wf_iwc_invalid_version/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Super simple workflow

This is a super simple workflow which generates a file with x lines with 'hello'
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
- doc: Test outline for Super-simple-workflow
job:
n_rows: 5
outputs:
outfile:
asserts:
has_n_lines:
n: 5
has_line:
line: "hello"
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
{
"a_galaxy_workflow": "true",
"annotation": "This workflow generates a file with x lines with 'hello'",
"comments": [],
"creator": [
{
"class": "Person",
"identifier": "https://orcid.org/0000-0002-1964-4960",
"name": "Lucille Delisle"
}
],
"format-version": "0.1",
"license": "MIT",
"release": "0.2",
"name": "Super simple workflow",
"report": {
"markdown": "\n# Workflow Execution Report\n\n## Workflow Inputs\n```galaxy\ninvocation_inputs()\n```\n\n## Workflow Outputs\n```galaxy\ninvocation_outputs()\n```\n\n## Workflow\n```galaxy\nworkflow_display()\n```\n"
},
"steps": {
"0": {
"annotation": "Number of rows to generate",
"content_id": null,
"errors": null,
"id": 0,
"input_connections": {},
"inputs": [
{
"description": "Number of rows to generate",
"name": "n_rows"
}
],
"label": "n_rows",
"name": "Input parameter",
"outputs": [],
"position": {
"left": 0,
"top": 0
},
"tool_id": null,
"tool_state": "{\"parameter_type\": \"integer\", \"optional\": false}",
"tool_version": null,
"type": "parameter_input",
"uuid": "2ee42c13-83f5-4ac4-b35d-74294edf7dea",
"when": null
},
"1": {
"annotation": "this creates a file with a given number of lines",
"content_id": "toolshed.g2.bx.psu.edu/repos/bgruening/text_processing/tp_text_file_with_recurring_lines/9.3+galaxy1",
"errors": null,
"id": 1,
"input_connections": {
"token_set_0|repeat_select|times": {
"id": 0,
"output_name": "output"
}
},
"inputs": [],
"label": "create file",
"name": "Create text file",
"outputs": [
{
"name": "outfile",
"type": "txt"
}
],
"position": {
"left": 236,
"top": 11
},
"post_job_actions": {},
"tool_id": "toolshed.g2.bx.psu.edu/repos/bgruening/text_processing/tp_text_file_with_recurring_lines/9.3+galaxy1",
"tool_shed_repository": {
"changeset_revision": "fbf99087e067",
"name": "text_processing",
"owner": "bgruening",
"tool_shed": "toolshed.g2.bx.psu.edu"
},
"tool_state": "{\"token_set\": [{\"__index__\": 0, \"line\": \"hello\", \"repeat_select\": {\"repeat_select_opts\": \"user\", \"__current_case__\": 0, \"times\": {\"__class__\": \"ConnectedValue\"}}}], \"__page__\": null, \"__rerun_remap_job_id__\": null}",
"tool_version": "9.3+galaxy1",
"type": "tool",
"uuid": "a86ff433-8dce-417c-baf0-bc106a93ee48",
"when": null,
"workflow_outputs": [
{
"label": "outfile",
"output_name": "outfile",
"uuid": "34572088-8ad8-4661-81a2-6dfe2d83a0fe"
}
]
}
},
"tags": [],
"uuid": "72c87042-27a8-4bcc-92af-ca74704e6161",
"version": 3
}
Loading
Loading