Warning

This document is for an old release of Galaxy. You can alternatively view this page in the latest release if it exists or view the top of the latest release's documentation.

Source code for galaxy.tool_util.lint

"""This modules contains the functions that drive the tool linting framework.

LintContext: a container for LintMessages
LintMessage: the actual message and level

The idea is to define a LintContext and to apply a linting function ``foo`` on a
``target``. The ``level`` (defined by ``LintLevel``) determines which linting
messages are shown::


    lint_ctx = LintContext(level) # level is the reporting level
    lint_ctx.lint(..., lint_func = foo, lint_target = target, ...)

The ``lint`` function essentially calls ``foo(target, self)``. Hence
the function ``foo`` must have two parameters:

1. the object to lint
2. the lint context

In ``foo`` the lint context can be used to add LintMessages to the lint context
by using its ``valid``, ``info``, ``warn``, and ``error`` functions::


    lint_foo(target, lint_ctx):
        lint_ctx.error("target is screwed")

Calling ``lint`` prints out the messages emmited by the linter
function immediately. Which messages are shown can be determined with the
``level`` argument of the ``LintContext`` constructor. If set to ``SILENT``,
no messages will be printed.

For special lint targets it might be useful to have additional information
in the lint messages. This can be achieved by subclassing ``LintMessage``.
See for instance ``XMLLintMessageLine`` which has an additional argument
``node`` in its constructor which is used to determine the line and filename in
an XML document that caused the message.

In order to use this.

- the lint context needs to be initialized with the additional parameter
  ``lint_message_class=XMLLintMessageLine``
- the additional parameter needs to be added as well to calls adding messages
  to the lint context, e.g. ``lint_ctx.error("some message", node=X)``. Note
  that the additional properties must be given as keyword arguments.
"""

import inspect
from enum import IntEnum
from typing import (
    Callable,
    List,
    Optional,
    Type,
    TypeVar,
    Union,
)

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


[docs]class LintLevel(IntEnum): SILENT = 5 ERROR = 4 WARN = 3 INFO = 2 VALID = 1 ALL = 0
[docs]class LintMessage: """ a message from the linter """
[docs] def __init__(self, level: str, message: str, **kwargs): self.level = level self.message = message
def __eq__(self, other) -> bool: """ add equal operator to easy lookup of a message in a List[LintMessage] which is useful in tests. If the other object is a string, it is loosely checked if the string is contained in the message. """ if isinstance(other, str): return other in self.message if isinstance(other, LintMessage): return self.message == other.message return False def __str__(self) -> str: return f".. {self.level.upper()}: {self.message}" def __repr__(self) -> str: return f"LintMessage({self.message})"
[docs]class XMLLintMessageLine(LintMessage):
[docs] def __init__(self, level: str, message: str, node: Optional[etree.Element] = None): super().__init__(level, message) self.line = None if node is not None: self.line = node.sourceline
def __str__(self) -> str: rval = super().__str__() if self.line is not None: rval += " (" rval += str(self.line) rval += ")" return rval
[docs]class XMLLintMessageXPath(LintMessage):
[docs] def __init__(self, level: str, message: str, node: Optional[etree.Element] = None): super().__init__(level, message) self.xpath = None if node is not None: tool_xml = node.getroottree() self.xpath = tool_xml.getpath(node)
def __str__(self) -> str: rval = super().__str__() if self.xpath is not None: rval += f" [{self.xpath}]" return rval
LintTargetType = TypeVar("LintTargetType") # TODO: Nothing inherently tool-y about LintContext and in fact # it is reused for repositories in planemo. Therefore, it should probably # be moved to galaxy.util.lint.
[docs]class LintContext: skip_types: List[str] level: LintLevel lint_message_class: Type[LintMessage] object_name: Optional[str] message_list: List[LintMessage]
[docs] def __init__( self, level: Union[LintLevel, str], lint_message_class: Type[LintMessage] = LintMessage, skip_types: Optional[List[str]] = None, object_name: Optional[str] = None, ): self.skip_types = skip_types or [] if isinstance(level, str): self.level = LintLevel[level.upper()] else: self.level = level self.lint_message_class = lint_message_class self.object_name = object_name self.message_list = []
@property def found_errors(self) -> bool: return len(self.error_messages) > 0 @property def found_warns(self) -> bool: return len(self.warn_messages) > 0
[docs] def lint(self, name: str, lint_func: Callable[[LintTargetType, "LintContext"], None], lint_target: LintTargetType): name = name[len("lint_") :] if name in self.skip_types: return if self.level < LintLevel.SILENT: # this is a relict from the past where the lint context # was reset when called with a new lint_func, as workaround # we save the message list, apply the lint_func (which then # adds to the message_list) and restore the message list # at the end (+ append the new messages) tmp_message_list = list(self.message_list) self.message_list = [] # call linter 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
@property def valid_messages(self) -> List[LintMessage]: return [x for x in self.message_list if x.level == "check"] @property def info_messages(self) -> List[LintMessage]: return [x for x in self.message_list if x.level == "info"] @property def warn_messages(self) -> List[LintMessage]: return [x for x in self.message_list if x.level == "warning"] @property 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: if args: message = message % args self.message_list.append(self.lint_message_class(level=level, message=message, **kwargs))
[docs] def valid(self, message: str, *args, **kwargs) -> None: self.__handle_message("check", message, *args, **kwargs)
[docs] def info(self, message: str, *args, **kwargs) -> None: self.__handle_message("info", message, *args, **kwargs)
[docs] def error(self, message: str, *args, **kwargs) -> None: self.__handle_message("error", message, *args, **kwargs)
[docs] def warn(self, message: str, *args, **kwargs) -> None: self.__handle_message("warning", message, *args, **kwargs)
[docs] def failed(self, fail_level: Union[LintLevel, str]) -> bool: if isinstance(fail_level, str): fail_level = LintLevel[fail_level.upper()] found_warns = self.found_warns found_errors = self.found_errors if fail_level == LintLevel.WARN: lint_fail = found_warns or found_errors elif fail_level >= LintLevel.ERROR: lint_fail = found_errors return lint_fail
[docs]def lint_tool_source( tool_source, level=LintLevel.ALL, fail_level=LintLevel.WARN, extra_modules=None, skip_types=None, name=None ) -> bool: """ apply all (applicable) linters from the linters submodule and the ones in extramodules immediately print linter messages (wrt level) and return if linting failed (wrt fail_level) """ extra_modules = extra_modules or [] skip_types = skip_types or [] lint_context = LintContext(level=level, skip_types=skip_types, object_name=name) lint_tool_source_with(lint_context, tool_source, extra_modules) return not lint_context.failed(fail_level)
[docs]def get_lint_context_for_tool_source(tool_source, extra_modules=None, skip_types=None, name=None) -> LintContext: """ this is the silent variant of lint_tool_source it returns the LintContext from which all linter messages and the status can be obtained """ extra_modules = extra_modules or [] skip_types = skip_types or [] lint_context = LintContext(level=LintLevel.SILENT, skip_types=skip_types, object_name=name) lint_tool_source_with(lint_context, tool_source, extra_modules) return lint_context
[docs]def lint_xml( tool_xml, level=LintLevel.ALL, fail_level=LintLevel.WARN, lint_message_class=LintMessage, extra_modules=None, skip_types=None, name=None, ) -> bool: """ lint an xml tool """ extra_modules = extra_modules or [] skip_types = skip_types or [] lint_context = LintContext( level=level, lint_message_class=lint_message_class, skip_types=skip_types, object_name=name ) lint_xml_with(lint_context, tool_xml, extra_modules) return not lint_context.failed(fail_level)
[docs]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) 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): continue for name, value in inspect.getmembers(module): if callable(value) and name.startswith("lint_"): # Look at the first argument to the linter to decide # if we should lint the XML description or the abstract # tool parser object. first_arg = inspect.getfullargspec(value).args[0] if first_arg == "tool_xml": if tool_xml is None: # XML linter and non-XML tool, skip for now continue else: lint_context.lint(name, value, tool_xml) else: lint_context.lint(name, value, tool_source) return lint_context
[docs]def lint_xml_with(lint_context, tool_xml, extra_modules=None) -> LintContext: extra_modules = extra_modules or [] tool_source = get_tool_source(xml_tree=tool_xml) return lint_tool_source_with(lint_context, tool_source, extra_modules=extra_modules)