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

Split linters in separate classes #17081

Merged
merged 34 commits into from
Feb 16, 2024
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
8eb0ef0
split stdio linters in separate classes
bernt-matthias Nov 24, 2023
0fbe617
remove linter code and fixes
bernt-matthias Nov 25, 2023
8d43947
split general and citations linter
bernt-matthias Nov 25, 2023
dd2a388
split command linter
bernt-matthias Nov 26, 2023
7b3d38c
make xml_order a Linter
bernt-matthias Nov 26, 2023
350a439
split help linters
bernt-matthias Nov 26, 2023
bf340a5
started with test linters
bernt-matthias Dec 1, 2023
3bccffc
split test linters
bernt-matthias Dec 4, 2023
5ae6594
split output linters
bernt-matthias Dec 4, 2023
d357ad0
split input linters
bernt-matthias Dec 5, 2023
ca364d1
add lxml based xml schema check
bernt-matthias Dec 7, 2023
5ef5651
do not print linter info
bernt-matthias Dec 7, 2023
7817fe1
run xsd linter in all linter unit tests
bernt-matthias Dec 7, 2023
c3a93e5
some fixes for xsd
bernt-matthias Dec 7, 2023
aac06ac
Fix unit test assertion
mvdbeek Dec 8, 2023
1a25ef6
skip validation of upload tool
bernt-matthias Dec 8, 2023
3d09584
remove fix method from linters
bernt-matthias Dec 8, 2023
212acf3
do not lint with ABC
bernt-matthias Dec 13, 2023
7e0b77c
some fixes
bernt-matthias Dec 13, 2023
6da7809
fix: allow options/column in data parameters
bernt-matthias Dec 13, 2023
c74f066
remove InputsSelectOptionTextMissing
bernt-matthias Dec 13, 2023
12f69be
allow macro
bernt-matthias Dec 14, 2023
584464e
add linter to lint context output
bernt-matthias Dec 15, 2023
0bb8c72
split cwl linters
bernt-matthias Dec 15, 2023
62adedb
allow options element in tool
bernt-matthias Dec 17, 2023
555fe5f
try source_path() instead of _source_path
bernt-matthias Dec 17, 2023
ccced2f
add function to list available linters
bernt-matthias Jan 15, 2024
3a98a3a
implement listing of linters
bernt-matthias Jan 17, 2024
2428b9d
Merge branch 'dev' into topic/linter-overhaul
bernt-matthias Feb 15, 2024
46ff3f2
use types-lxml instead of lxml-stubs
bernt-matthias Feb 15, 2024
368f3e8
Revert "use types-lxml instead of lxml-stubs"
bernt-matthias Feb 15, 2024
ed8eb6e
Add type ignore
bernt-matthias Feb 15, 2024
a0b25b5
new lines
bernt-matthias Feb 15, 2024
101a8a9
Merge branch 'topic/linter-overhaul' of https://github.com/bernt-matt…
bernt-matthias Feb 15, 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
4 changes: 3 additions & 1 deletion .ci/validate_test_tools.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,7 @@ xsd_path="lib/galaxy/tools/xsd/galaxy.xsd"
xmllint --noout "$xsd_path"

test_tools_path='test/functional/tools'
tool_files_list=$(ls "$test_tools_path"/*.xml | grep -v '_conf.xml$')
# test all test tools except upload.xml which uses a non-standard conditional
# (without param) which does not survive xsd validation
tool_files_list=$(ls "$test_tools_path"/*.xml | grep -v '_conf.xml$' | grep -v upload.xml)
sh scripts/validate_tools.sh $tool_files_list
109 changes: 70 additions & 39 deletions lib/galaxy/tool_util/lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,22 +45,31 @@
"""

import inspect
from abc import (
ABC,
abstractmethod,
)
from enum import IntEnum
from typing import (
Callable,
List,
Optional,
Type,
TYPE_CHECKING,
TypeVar,
Union,
)

import galaxy.tool_util.linters
from galaxy.tool_util.parser import get_tool_source
from galaxy.util import (
Element,
submodules,
)

if TYPE_CHECKING:
from galaxy.tool_util.parser.interface import ToolSource


class LintLevel(IntEnum):
SILENT = 5
Expand All @@ -71,14 +80,44 @@ class LintLevel(IntEnum):
ALL = 0


class Linter(ABC):
"""
a linter. needs to define a lint method and the code property.
optionally a fix method can be given
"""

@classmethod
@abstractmethod
def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
"""
should add at most one message to the lint context
"""
pass

@classmethod
def name(cls) -> str:
"""
get the linter name
"""
return cls.__name__

@classmethod
def list_listers(cls) -> List[str]:
"""
list the names of all linter derived from Linter
"""
return [s.__name__ for s in cls.__subclasses__()]


class LintMessage:
"""
a message from the linter
"""

def __init__(self, level: str, message: str, **kwargs):
def __init__(self, level: str, message: str, linter: Optional[str] = None, **kwargs):
self.level = level
self.message = message
self.linter = linter

def __eq__(self, other) -> bool:
"""
Expand All @@ -95,15 +134,19 @@ def __eq__(self, other) -> bool:
return False

def __str__(self) -> str:
return f".. {self.level.upper()}: {self.message}"
if self.linter:
linter = f" ({self.linter})"
else:
linter = ""
return f".. {self.level.upper()}{linter}: {self.message}"

def __repr__(self) -> str:
return f"LintMessage({self.message})"


class XMLLintMessageLine(LintMessage):
def __init__(self, level: str, message: str, node: Optional[Element] = None):
super().__init__(level, message)
def __init__(self, level: str, message: str, linter: Optional[str] = None, node: Optional[Element] = None):
super().__init__(level, message, linter)
self.line = None
if node is not None:
self.line = node.sourceline
Expand All @@ -118,8 +161,8 @@ def __str__(self) -> str:


class XMLLintMessageXPath(LintMessage):
def __init__(self, level: str, message: str, node: Optional[Element] = None):
super().__init__(level, message)
def __init__(self, level: str, message: str, linter: Optional[str] = None, node: Optional[Element] = None):
super().__init__(level, message, linter)
self.xpath = None
if node is not None:
tool_xml = node.getroottree()
Expand Down Expand Up @@ -170,7 +213,8 @@ def found_warns(self) -> bool:
return len(self.warn_messages) > 0

def lint(self, name: str, lint_func: Callable[[LintTargetType, "LintContext"], None], lint_target: LintTargetType):
name = name[len("lint_") :]
if name.startswith("lint_"):
name = name[len("lint_") :]
if name in self.skip_types:
return

Expand All @@ -187,37 +231,18 @@ def lint(self, name: str, lint_func: Callable[[LintTargetType, "LintContext"], N
lint_func(lint_target, self)

if self.level < LintLevel.SILENT:
# TODO: colorful emoji if in click CLI.
if self.error_messages:
status = "FAIL"
elif self.warn_messages:
status = "WARNING"
else:
status = "CHECK"

def print_linter_info(printed_linter_info):
if printed_linter_info:
return True
print(f"Applying linter {name}... {status}")
return True

plf = False
for message in self.error_messages:
plf = print_linter_info(plf)
print(f"{message}")

if self.level <= LintLevel.WARN:
for message in self.warn_messages:
plf = print_linter_info(plf)
print(f"{message}")

if self.level <= LintLevel.INFO:
for message in self.info_messages:
plf = print_linter_info(plf)
print(f"{message}")
if self.level <= LintLevel.VALID:
for message in self.valid_messages:
plf = print_linter_info(plf)
print(f"{message}")
self.message_list = tmp_message_list + self.message_list

Expand All @@ -237,22 +262,22 @@ def warn_messages(self) -> List[LintMessage]:
def error_messages(self) -> List[LintMessage]:
return [x for x in self.message_list if x.level == "error"]

def __handle_message(self, level: str, message: str, *args, **kwargs) -> None:
def __handle_message(self, level: str, message: str, linter: Optional[str] = None, *args, **kwargs) -> None:
if args:
message = message % args
self.message_list.append(self.lint_message_class(level=level, message=message, **kwargs))
self.message_list.append(self.lint_message_class(level=level, message=message, linter=linter, **kwargs))

def valid(self, message: str, *args, **kwargs) -> None:
self.__handle_message("check", message, *args, **kwargs)
def valid(self, message: str, linter: Optional[str] = None, *args, **kwargs) -> None:
self.__handle_message("check", message, linter, *args, **kwargs)

def info(self, message: str, *args, **kwargs) -> None:
self.__handle_message("info", message, *args, **kwargs)
def info(self, message: str, linter: Optional[str] = None, *args, **kwargs) -> None:
self.__handle_message("info", message, linter, *args, **kwargs)

def error(self, message: str, *args, **kwargs) -> None:
self.__handle_message("error", message, *args, **kwargs)
def error(self, message: str, linter: Optional[str] = None, *args, **kwargs) -> None:
self.__handle_message("error", message, linter, *args, **kwargs)

def warn(self, message: str, *args, **kwargs) -> None:
self.__handle_message("warning", message, *args, **kwargs)
def warn(self, message: str, linter: Optional[str] = None, *args, **kwargs) -> None:
self.__handle_message("warning", message, linter, *args, **kwargs)

def failed(self, fail_level: Union[LintLevel, str]) -> bool:
if isinstance(fail_level, str):
Expand Down Expand Up @@ -319,12 +344,16 @@ def lint_xml(

def lint_tool_source_with(lint_context, tool_source, extra_modules=None) -> LintContext:
extra_modules = extra_modules or []
import galaxy.tool_util.linters

tool_xml = getattr(tool_source, "xml_tree", None)
tool_type = tool_source.parse_tool_type() or "default"
linter_modules = submodules.import_submodules(galaxy.tool_util.linters)
linter_modules.extend(extra_modules)
return lint_tool_source_with_modules(lint_context, tool_source, linter_modules)


def lint_tool_source_with_modules(lint_context: LintContext, tool_source, linter_modules) -> LintContext:
tool_xml = getattr(tool_source, "xml_tree", None)
tool_type = tool_source.parse_tool_type() or "default"

for module in linter_modules:
lint_tool_types = getattr(module, "lint_tool_types", ["default", "manage_data"])
if not ("*" in lint_tool_types or tool_type in lint_tool_types):
Expand All @@ -344,6 +373,8 @@ def lint_tool_source_with(lint_context, tool_source, extra_modules=None) -> Lint
lint_context.lint(name, value, tool_xml)
else:
lint_context.lint(name, value, tool_source)
bernt-matthias marked this conversation as resolved.
Show resolved Hide resolved
elif inspect.isclass(value) and issubclass(value, Linter) and not inspect.isabstract(value):
lint_context.lint(name, value.lint, tool_source)
return lint_context


Expand Down
102 changes: 66 additions & 36 deletions lib/galaxy/tool_util/linters/citations.py
Original file line number Diff line number Diff line change
@@ -1,42 +1,72 @@
"""This module contains a citation lint function.
"""This module contains citation linters.

Citations describe references that should be used when consumers
of the tool publish results.
"""
from typing import TYPE_CHECKING

from galaxy.tool_util.lint import Linter

def lint_citations(tool_xml, lint_ctx):
"""Ensure tool contains at least one valid citation."""
root = tool_xml.find("./citations")
if root is None:
root = tool_xml.getroot()

citations = tool_xml.findall("citations")
if len(citations) > 1:
lint_ctx.error("More than one citation section found, behavior undefined.", node=citations[1])
return

if len(citations) == 0:
lint_ctx.warn("No citations found, consider adding citations to your tool.", node=root)
return

valid_citations = 0
for citation in citations[0]:
if citation.tag != "citation":
lint_ctx.warn(
f"Unknown tag discovered in citations block [{citation.tag}], will be ignored.", node=citation
)
continue
citation_type = citation.attrib.get("type")
if citation_type not in ("bibtex", "doi"):
lint_ctx.warn(f"Unknown citation type discovered [{citation_type}], will be ignored.", node=citation)
continue
if citation.text is None or not citation.text.strip():
lint_ctx.error(f"Empty {citation_type} citation.", node=citation)
continue
valid_citations += 1

if valid_citations > 0:
lint_ctx.valid(f"Found {valid_citations} likely valid citations.", node=root)
else:
lint_ctx.warn("Found no valid citations.", node=root)
if TYPE_CHECKING:
from galaxy.tool_util.lint import LintContext
from galaxy.tool_util.parser.interface import ToolSource


class CitationsMissing(Linter):
@classmethod
def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
tool_xml = getattr(tool_source, "xml_tree", None)
if not tool_xml:
return
root = tool_xml.find("./citations")
if root is None:
root = tool_xml.getroot()
citations = tool_xml.findall("citations")
if len(citations) == 0:
lint_ctx.warn("No citations found, consider adding citations to your tool.", linter=cls.name(), node=root)


class CitationsNoText(Linter):
@classmethod
def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
tool_xml = getattr(tool_source, "xml_tree", None)
if not tool_xml:
return
citations = tool_xml.find("citations")
if citations is None:
return
for citation in citations:
citation_type = citation.attrib.get("type")
if citation_type in ["doi", "bibtex"] and (citation.text is None or not citation.text.strip()):
lint_ctx.error(f"Empty {citation_type} citation.", linter=cls.name(), node=citation)


class CitationsFound(Linter):
@classmethod
def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
tool_xml = getattr(tool_source, "xml_tree", None)
if not tool_xml:
return
root = tool_xml.find("./citations")
if root is None:
root = tool_xml.getroot()
citations = tool_xml.find("citations")

if citations is not None and len(citations) > 0:
lint_ctx.valid(f"Found {len(citations)} citations.", linter=cls.name(), node=root)


class CitationsNoValid(Linter):
@classmethod
def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
tool_xml = getattr(tool_source, "xml_tree", None)
if not tool_xml:
return
root = tool_xml.find("./citations")
if root is None:
root = tool_xml.getroot()
citations = tool_xml.findall("citations")
if len(citations) != 1:
return
if len(citations[0]) == 0:
lint_ctx.warn("Found no valid citations.", linter=cls.name(), node=root)
Loading
Loading