Skip to content

Commit a6ff9cb

Browse files
rewrite tool linters defined in planemo
1 parent 251776c commit a6ff9cb

File tree

9 files changed

+294
-163
lines changed

9 files changed

+294
-163
lines changed

planemo/commands/cmd_lint.py

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -20,25 +20,7 @@
2020
@options.fail_level_option()
2121
@options.skip_options()
2222
@options.recursive_option()
23-
@click.option(
24-
"--urls",
25-
is_flag=True,
26-
default=False,
27-
help="Check validity of URLs in XML files",
28-
)
29-
@click.option(
30-
"--doi",
31-
is_flag=True,
32-
default=False,
33-
help="Check validity of DOIs in XML files",
34-
)
35-
@click.option(
36-
"--conda_requirements",
37-
is_flag=True,
38-
default=False,
39-
help="Check tool requirements for availability in best practice Conda channels.",
40-
)
41-
@options.lint_biocontainers_option()
23+
@options.lint_planemo_defined_tool_linters_options()
4224
# @click.option(
4325
# "--verify",
4426
# is_flag=True,
@@ -48,6 +30,7 @@
4830
@command_function
4931
def cli(ctx: PlanemoCliContext, uris, **kwds):
5032
"""Check for common errors and best practices."""
33+
print("LINT")
5134
lint_args = build_tool_lint_args(ctx, **kwds)
5235
exit_code = lint_tools_on_path(ctx, uris, lint_args, recursive=kwds["recursive"])
5336

planemo/commands/cmd_shed_lint.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,7 @@
3030
"to allow automated creation and/or updates."
3131
),
3232
)
33-
@click.option(
34-
"--urls",
35-
is_flag=True,
36-
default=False,
37-
help="Check validity of URLs in XML files",
38-
)
39-
@options.lint_biocontainers_option()
33+
@options.lint_planemo_defined_tool_linters_options()
4034
# @click.option(
4135
# "--verify",
4236
# is_flag=True,

planemo/lint.py

Lines changed: 0 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,13 @@
66
Dict,
77
TYPE_CHECKING,
88
)
9-
from urllib.request import urlopen
109

11-
import requests
1210
from galaxy.tool_util.lint import (
1311
LintContext,
1412
Linter,
1513
)
1614

1715
from planemo.io import error
18-
from planemo.shed import find_urls_for_xml
1916
from planemo.xml import validation
2017

2118
if TYPE_CHECKING:
@@ -71,46 +68,6 @@ def handle_lint_complete(lint_ctx, lint_args, failed=False):
7168
return 1 if failed else 0
7269

7370

74-
def lint_dois(tool_xml, lint_ctx):
75-
"""Find referenced DOIs and check they have valid with https://doi.org."""
76-
dois = find_dois_for_xml(tool_xml)
77-
for publication in dois:
78-
is_doi(publication, lint_ctx)
79-
80-
81-
def find_dois_for_xml(tool_xml):
82-
dois = []
83-
for element in tool_xml.getroot().findall("citations"):
84-
for citation in list(element):
85-
if citation.tag == "citation" and citation.attrib.get("type", "") == "doi":
86-
dois.append(citation.text)
87-
return dois
88-
89-
90-
def is_doi(publication_id, lint_ctx):
91-
"""Check if dx.doi knows about the ``publication_id``."""
92-
base_url = "https://doi.org"
93-
if publication_id is None:
94-
lint_ctx.error("Empty DOI citation")
95-
return
96-
publication_id = publication_id.strip()
97-
doiless_publication_id = publication_id.split("doi:", 1)[-1]
98-
if not doiless_publication_id:
99-
lint_ctx.error("Empty DOI citation")
100-
return
101-
url = f"{base_url}/{doiless_publication_id}"
102-
r = requests.get(url)
103-
if r.status_code == 200:
104-
if publication_id != doiless_publication_id:
105-
lint_ctx.error("%s is valid, but Galaxy expects DOI without 'doi:' prefix" % publication_id)
106-
else:
107-
lint_ctx.info("%s is a valid DOI" % publication_id)
108-
elif r.status_code == 404:
109-
lint_ctx.error("%s is not a valid DOI" % publication_id)
110-
else:
111-
lint_ctx.warn("dx.doi returned unexpected status code %d" % r.status_code)
112-
113-
11471
def lint_xsd(lint_ctx, schema_path, path):
11572
"""Lint XML at specified path with supplied schema."""
11673
name = lint_ctx.object_name or os.path.basename(path)
@@ -124,55 +81,8 @@ def lint_xsd(lint_ctx, schema_path, path):
12481
lint_ctx.info("File validates against XML schema.")
12582

12683

127-
def lint_urls(root, lint_ctx):
128-
"""Find referenced URLs and verify they are valid."""
129-
urls, docs = find_urls_for_xml(root)
130-
131-
# This is from Google Chome on macOS, current at time of writing:
132-
BROWSER_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 11_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36"
133-
134-
def validate_url(url, lint_ctx, user_agent=None):
135-
is_valid = True
136-
if url.startswith("http://") or url.startswith("https://"):
137-
if user_agent:
138-
headers = {"User-Agent": user_agent, "Accept": "*/*"}
139-
else:
140-
headers = None
141-
r = None
142-
try:
143-
r = requests.get(url, headers=headers, stream=True)
144-
r.raise_for_status()
145-
next(r.iter_content(1000))
146-
except Exception as e:
147-
if r is not None and r.status_code == 429:
148-
# too many requests
149-
pass
150-
if r is not None and r.status_code in [403, 503] and "cloudflare" in r.text:
151-
# CloudFlare protection block
152-
pass
153-
else:
154-
is_valid = False
155-
lint_ctx.error(f"Error '{e}' accessing {url}")
156-
else:
157-
try:
158-
with urlopen(url) as handle:
159-
handle.read(100)
160-
except Exception as e:
161-
is_valid = False
162-
lint_ctx.error(f"Error '{e}' accessing {url}")
163-
if is_valid:
164-
lint_ctx.info("URL OK %s" % url)
165-
166-
for url in urls:
167-
validate_url(url, lint_ctx)
168-
for url in docs:
169-
validate_url(url, lint_ctx, BROWSER_USER_AGENT)
170-
171-
17284
__all__ = (
17385
"build_lint_args",
17486
"handle_lint_complete",
175-
"lint_dois",
176-
"lint_urls",
17787
"lint_xsd",
17888
)

planemo/linters/biocontainer_registered.py

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
"""Ensure best-practice biocontainer registered for this tool."""
22

3+
from typing import TYPE_CHECKING
4+
35
from galaxy.tool_util.deps.container_resolvers.mulled import targets_to_mulled_name
4-
from galaxy.tool_util.deps.mulled.util import build_target
6+
from galaxy.tool_util.deps.mulled.mulled_build_tool import requirements_to_mulled_targets
7+
from galaxy.tool_util.lint import Linter
58

6-
from planemo.conda import tool_source_conda_targets
9+
if TYPE_CHECKING:
10+
from galaxy.tool_util.lint import LintContext
11+
from galaxy.tool_util.parser.interface import ToolSource
712

813
MESSAGE_WARN_NO_REQUIREMENTS = "No valid package requirement tags found to infer BioContainer from."
914
MESSAGE_WARN_NO_CONTAINER = "Failed to find a BioContainer registered for these requirements."
@@ -12,18 +17,24 @@
1217
lint_tool_types = ["*"]
1318

1419

15-
def lint_biocontainer_registered(tool_source, lint_ctx):
16-
conda_targets = tool_source_conda_targets(tool_source)
17-
if not conda_targets:
18-
lint_ctx.warn(MESSAGE_WARN_NO_REQUIREMENTS)
19-
return
20-
21-
mulled_targets = [build_target(c.package, c.version) for c in conda_targets]
22-
name = mulled_container_name("biocontainers", mulled_targets)
23-
if name:
24-
lint_ctx.info(MESSAGE_INFO_FOUND_BIOCONTAINER % name)
25-
else:
26-
lint_ctx.warn(MESSAGE_WARN_NO_CONTAINER)
20+
class BiocontainerValid(Linter):
21+
@classmethod
22+
def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
23+
requirements, *_ = tool_source.parse_requirements_and_containers()
24+
targets = requirements_to_mulled_targets(requirements)
25+
name = mulled_container_name("biocontainers", targets)
26+
if name:
27+
lint_ctx.info(MESSAGE_INFO_FOUND_BIOCONTAINER % name, linter=cls.name(), node=requirements)
28+
29+
30+
class BiocontainerMissing(Linter):
31+
@classmethod
32+
def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
33+
requirements, *_ = tool_source.parse_requirements_and_containers()
34+
targets = requirements_to_mulled_targets(requirements)
35+
name = mulled_container_name("biocontainers", targets)
36+
if not name:
37+
lint_ctx.warn(MESSAGE_WARN_NO_CONTAINER, linter=cls.name(), node=requirements)
2738

2839

2940
def mulled_container_name(namespace, targets):
Lines changed: 52 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,62 @@
11
"""Ensure requirements are matched in best practice conda channels."""
22

3+
from typing import TYPE_CHECKING
4+
5+
from galaxy.tool_util.deps.conda_util import requirement_to_conda_targets
6+
from galaxy.tool_util.lint import Linter
7+
38
from planemo.conda import (
49
BEST_PRACTICE_CHANNELS,
510
best_practice_search,
6-
tool_source_conda_targets,
711
)
812

13+
if TYPE_CHECKING:
14+
from galaxy.tool_util.lint import LintContext
15+
from galaxy.tool_util.parser.interface import ToolSource
16+
917
lint_tool_types = ["*"]
1018

1119

12-
def lint_requirements_in_conda(tool_source, lint_ctx):
13-
"""Check requirements of tool source against best practice Conda channels."""
14-
conda_targets = tool_source_conda_targets(tool_source)
15-
if not conda_targets:
16-
lint_ctx.warn("No valid package requirement tags found to check against Conda.")
17-
return
18-
19-
for conda_target in conda_targets:
20-
(best_hit, exact) = best_practice_search(conda_target)
21-
conda_target_str = conda_target.package
22-
if conda_target.version:
23-
conda_target_str += "@%s" % (conda_target.version)
24-
if best_hit and exact:
25-
template = "Requirement [%s] matches target in best practice Conda channel [%s]."
26-
message = template % (conda_target_str, best_hit.get("channel"))
27-
lint_ctx.info(message)
28-
elif best_hit:
29-
template = (
30-
"Requirement [%s] doesn't exactly match available version [%s] in best practice Conda channel [%s]."
31-
)
32-
message = template % (conda_target_str, best_hit["version"], best_hit.get("channel"))
33-
lint_ctx.warn(message)
34-
else:
35-
template = "Requirement [%s] doesn't match any recipe in a best practice conda channel [%s]."
36-
message = template % (conda_target_str, BEST_PRACTICE_CHANNELS)
37-
lint_ctx.warn(message)
20+
class CondaRequirementValid(Linter):
21+
@classmethod
22+
def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
23+
for conda_target, requirement in _requirements_conda_targets(tool_source):
24+
(best_hit, exact) = best_practice_search(conda_target)
25+
conda_target_str = conda_target.package
26+
if conda_target.version:
27+
conda_target_str += "@%s" % (conda_target.version)
28+
if best_hit and exact:
29+
message = f"Requirement [{conda_target_str}] matches target in best practice Conda channel [{best_hit.get('channel')}]."
30+
lint_ctx.info(message, linter=cls.name(), node=requirement)
31+
32+
33+
class CondaRequirementInexact(Linter):
34+
@classmethod
35+
def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
36+
for conda_target, requirement in _requirements_conda_targets(tool_source):
37+
(best_hit, exact) = best_practice_search(conda_target)
38+
conda_target_str = conda_target.package
39+
if conda_target.version:
40+
conda_target_str += "@%s" % (conda_target.version)
41+
if best_hit and not exact:
42+
message = f"Requirement [{conda_target_str}] doesn't exactly match available version [{best_hit['version']}] in best practice Conda channel [{best_hit.get('channel')}]."
43+
lint_ctx.warn(message, linter=cls.name(), node=requirement)
44+
45+
46+
class CondaRequirementMissing(Linter):
47+
@classmethod
48+
def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
49+
for conda_target, requirement in _requirements_conda_targets(tool_source):
50+
(best_hit, exact) = best_practice_search(conda_target)
51+
conda_target_str = conda_target.package
52+
if conda_target.version:
53+
conda_target_str += "@%s" % (conda_target.version)
54+
if best_hit and not exact:
55+
message = f"Requirement [{conda_target_str}] doesn't match any recipe in a best practice conda channel ['{BEST_PRACTICE_CHANNELS}']."
56+
lint_ctx.warn(message, linter=cls.name(), node=requirement)
57+
58+
59+
def _requirements_conda_targets(tool_source):
60+
requirements, *_ = tool_source.parse_requirements_and_containers()
61+
for requirement in requirements:
62+
yield requirement_to_conda_targets(requirement), requirement

0 commit comments

Comments
 (0)