Warning

This document is for an in-development version 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.linters.outputs

"""This module contains a linting functions for tool outputs."""

from typing import TYPE_CHECKING

from packaging.version import Version

from galaxy.tool_util.lint import Linter
from ._util import is_valid_cheetah_placeholder
from ..parser.output_collection_def import NAMED_PATTERNS

if TYPE_CHECKING:
    from galaxy.tool_util.lint import LintContext
    from galaxy.tool_util.parser import ToolSource
    from galaxy.util.etree import (
        Element,
        ElementTree,
    )


[docs]class OutputsMissing(Linter):
[docs] @classmethod def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"): tool_xml = getattr(tool_source, "xml_tree", None) if not tool_xml: return tool_node = tool_xml.find("./outputs") if tool_node is None: tool_node = tool_xml.getroot() if len(tool_xml.findall("./outputs")) == 0: lint_ctx.warn( "Tool contains no outputs section, most tools should produce outputs.", linter=cls.name(), node=tool_node, )
[docs]class OutputsOutput(Linter):
[docs] @classmethod def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"): tool_xml = getattr(tool_source, "xml_tree", None) if not tool_xml: return output = tool_xml.find("./outputs/output") if output is not None: lint_ctx.warn( "Avoid the use of 'output' and replace by 'data' or 'collection'", linter=cls.name(), node=output )
[docs]class OutputsNameInvalidCheetah(Linter):
[docs] @classmethod def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"): tool_xml = getattr(tool_source, "xml_tree", None) if not tool_xml: return for output in tool_xml.findall("./outputs/data[@name]") + tool_xml.findall("./outputs/collection[@name]"): if not is_valid_cheetah_placeholder(output.attrib["name"]): lint_ctx.warn( f'Tool output name [{output.attrib["name"]}] is not a valid Cheetah placeholder.', linter=cls.name(), node=output, )
[docs]class OutputsNameDuplicated(Linter):
[docs] @classmethod def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"): tool_xml = getattr(tool_source, "xml_tree", None) if not tool_xml: return names = set() for output in tool_xml.findall("./outputs/data[@name]") + tool_xml.findall("./outputs/collection[@name]"): name = output.attrib["name"] if name in names: lint_ctx.error(f"Tool output [{name}] has duplicated name", linter=cls.name(), node=output) names.add(name)
[docs]class OutputsLabelDuplicatedFilter(Linter):
[docs] @classmethod def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"): tool_xml = getattr(tool_source, "xml_tree", None) if not tool_xml: return labels = set() for output in tool_xml.findall("./outputs/data") + tool_xml.findall("./outputs/collection"): name = output.attrib.get("name", "") label = output.attrib.get("label", "${tool.name} on ${on_string}") if label in labels and output.find(".//filter") is not None: lint_ctx.warn( f"Tool output [{name}] uses duplicated label '{label}', double check if filters imply disjoint cases", linter=cls.name(), node=output, ) labels.add(label)
[docs]class OutputsLabelDuplicatedNoFilter(Linter):
[docs] @classmethod def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"): tool_xml = getattr(tool_source, "xml_tree", None) if not tool_xml: return labels = set() for output in tool_xml.findall("./outputs/data[@name]") + tool_xml.findall("./outputs/collection[@name]"): name = output.attrib.get("name", "") label = output.attrib.get("label", "${tool.name} on ${on_string}") if label in labels and output.find(".//filter") is None: lint_ctx.warn(f"Tool output [{name}] uses duplicated label '{label}'", linter=cls.name(), node=output) labels.add(label)
[docs]class OutputsCollectionType(Linter):
[docs] @classmethod def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"): tool_xml = getattr(tool_source, "xml_tree", None) if not tool_xml: return for output in tool_xml.findall("./outputs/collection"): if "type" not in output.attrib: lint_ctx.warn("Collection output with undefined 'type' found.", linter=cls.name(), node=output)
[docs]class OutputsNumber(Linter):
[docs] @classmethod def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"): tool_xml = getattr(tool_source, "xml_tree", None) if not tool_xml: return outputs = tool_xml.findall("./outputs") if len(outputs) == 0: return num_outputs = len(outputs[0].findall("./data")) + len(outputs[0].findall("./collection")) lint_ctx.info(f"{num_outputs} outputs found.", linter=cls.name(), node=outputs[0])
[docs]class OutputsFormatInput(Linter):
[docs] @classmethod def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"): def _report(output: "Element"): message = f"Using format='input' on {output.tag} is deprecated. Use the format_source attribute." if Version(str(profile)) <= Version("16.01"): lint_ctx.warn(message, linter=cls.name(), node=output) else: lint_ctx.error(message, linter=cls.name(), node=output) profile = tool_source.parse_profile() tool_xml = getattr(tool_source, "xml_tree", None) if not tool_xml: return for output in tool_xml.findall("./outputs/data") + tool_xml.findall("./outputs/collection"): fmt = output.attrib.get("format") if fmt == "input": _report(output) for sub in output: fmt = sub.attrib.get("format", sub.attrib.get("ext")) if fmt == "input": _report(output)
[docs]class OutputsFormat(Linter):
[docs] @classmethod def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"): tool_xml = getattr(tool_source, "xml_tree", None) if not tool_xml: return for output in tool_xml.findall("./outputs/data") + tool_xml.findall("./outputs/collection"): format_set = False if _check_format(output): format_set = True if output.tag == "data": if "auto_format" in output.attrib and output.attrib["auto_format"]: format_set = True elif output.tag == "collection": if "structured_like" in output.attrib and "inherit_format" in output.attrib: format_set = True for sub in output: if _check_pattern(sub) or _has_tool_provided_metadata(tool_xml): format_set = True elif _check_format(sub): format_set = True if not format_set: lint_ctx.warn( f"Tool {output.tag} output {output.attrib.get('name', 'with missing name')} doesn't define an output format.", linter=cls.name(), node=output, )
[docs]class OutputsFormatSourceIncomp(Linter):
[docs] @classmethod def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"): def _check_and_report(node): if "format_source" in node.attrib and ("ext" in node.attrib or "format" in node.attrib): lint_ctx.warn( f"Tool {node.tag} output '{node.attrib.get('name', 'with missing name')}' should use either format_source or format/ext", linter=cls.name(), node=node, ) tool_xml = getattr(tool_source, "xml_tree", None) if not tool_xml: return for output in tool_xml.findall("./outputs/data") + tool_xml.findall("./outputs/collection"): _check_and_report(output) for sub in output: _check_and_report(sub)
def _check_format(node): """ check if format/ext/format_source attribute is set in a given node issue a warning if the value is input return true (node defines format/ext) / false (else) """ if "format_source" in node.attrib: return True if node.find(".//action[@type='format']") is not None: return True # if allowed (e.g. for discover_datasets), ext takes precedence over format fmt = node.attrib.get("format", node.attrib.get("ext")) return fmt is not None def _check_pattern(node): """ check if - pattern attribute is set and defines the extension or - from_tool_provided_metadata is true """ if node.tag != "discover_datasets": return False if "pattern" not in node.attrib: return False pattern = node.attrib["pattern"] regex_pattern = NAMED_PATTERNS.get(pattern, pattern) # TODO error on wrong pattern or non-regexp if "(?P<ext>" in regex_pattern: return True def _has_tool_provided_metadata(tool_xml: "ElementTree") -> bool: outputs = tool_xml.find("./outputs") if outputs is not None: if "provided_metadata_file" in outputs.attrib or "provided_metadata_style" in outputs.attrib: return True command = tool_xml.find("./command") if command is not None: if "galaxy.json" in command.text: return True config = tool_xml.find("./configfiles/configfile[@filename='galaxy.json']") if config is not None: return True return False