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.linters.inputs

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

import ast
import re
import warnings
from typing import (
    Iterator,
    Optional,
    Tuple,
    TYPE_CHECKING,
)

from packaging.version import Version

from galaxy.tool_util.lint import Linter
from galaxy.util import string_as_bool
from ._util import (
    is_datasource,
    is_valid_cheetah_placeholder,
)
from ..parser.util import _parse_name

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

lint_tool_types = ["*"]

ATTRIB_VALIDATOR_COMPATIBILITY = {
    "check": ["metadata"],
    "expression": ["substitute_value_in_message"],
    "table_name": [
        "dataset_metadata_in_data_table",
        "dataset_metadata_not_in_data_table",
        "value_in_data_table",
        "value_not_in_data_table",
    ],
    "filename": ["dataset_metadata_in_file"],
    "metadata_name": [
        "dataset_metadata_equal",
        "dataset_metadata_in_data_table",
        "dataset_metadata_not_in_data_table",
        "dataset_metadata_in_file",
        "dataset_metadata_in_range",
    ],
    "metadata_column": [
        "dataset_metadata_in_data_table",
        "dataset_metadata_not_in_data_table",
        "value_in_data_table",
        "value_not_in_data_table",
        "dataset_metadata_in_file",
    ],
    "line_startswith": ["dataset_metadata_in_file"],
    "min": ["in_range", "length", "dataset_metadata_in_range"],
    "max": ["in_range", "length", "dataset_metadata_in_range"],
    "exclude_min": ["in_range", "dataset_metadata_in_range"],
    "exclude_max": ["in_range", "dataset_metadata_in_range"],
    "split": ["dataset_metadata_in_file"],
    "skip": ["metadata"],
    "value": ["dataset_metadata_equal"],
    "value_json": ["dataset_metadata_equal"],
}

PARAMETER_VALIDATOR_TYPE_COMPATIBILITY = {
    "integer": ["in_range", "expression"],
    "float": ["in_range", "expression"],
    "data": [
        "metadata",
        "no_options",
        "unspecified_build",
        "dataset_ok_validator",
        "dataset_metadata_equal",
        "dataset_metadata_in_range",
        "dataset_metadata_in_file",
        "dataset_metadata_in_data_table",
        "dataset_metadata_not_in_data_table",
        "expression",
    ],
    "data_collection": [
        "metadata",
        "no_options",
        "unspecified_build",
        "dataset_ok_validator",
        "dataset_metadata_equal",
        "dataset_metadata_in_range",
        "dataset_metadata_in_file",
        "dataset_metadata_in_data_table",
        "dataset_metadata_not_in_data_table",
        "expression",
    ],
    "text": ["regex", "length", "empty_field", "value_in_data_table", "value_not_in_data_table", "expression"],
    "select": [
        "in_range",
        "no_options",
        "regex",
        "length",
        "empty_field",
        "value_in_data_table",
        "value_not_in_data_table",
        "expression",
    ],
    "drill_down": [
        "no_options",
        "regex",
        "length",
        "empty_field",
        "value_in_data_table",
        "value_not_in_data_table",
        "expression",
    ],
    "data_column": [
        "no_options",
        "regex",
        "length",
        "empty_field",
        "value_in_data_table",
        "value_not_in_data_table",
        "expression",
    ],
}

PARAM_TYPE_CHILD_COMBINATIONS = [
    ("./options", ["data", "select", "drill_down"]),
    ("./options/option", ["drill_down"]),
    ("./options/column", ["data", "select"]),
]

# TODO lint for valid param type - attribute combinations
# TODO check if dataset is available for filters referring other datasets
# TODO check if ref input param is present for from_dataset


[docs]class InputsNum(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("./inputs") if tool_node is None: tool_node = tool_xml.getroot() num_inputs = len(tool_xml.findall("./inputs//param")) if num_inputs: lint_ctx.info(f"Found {num_inputs} input parameters.", linter=cls.name(), node=tool_node)
[docs]class InputsMissing(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("./inputs") if tool_node is None: tool_node = tool_xml.getroot() num_inputs = len(tool_xml.findall("./inputs//param")) if num_inputs == 0 and not is_datasource(tool_xml): lint_ctx.warn("Found no input parameters.", linter=cls.name(), node=tool_node)
[docs]class InputsMissingDataSource(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("./inputs") if tool_node is None: tool_node = tool_xml.getroot() num_inputs = len(tool_xml.findall("./inputs//param")) if num_inputs == 0 and is_datasource(tool_xml): lint_ctx.info("No input parameters, OK for data sources", linter=cls.name(), node=tool_node)
[docs]class InputsDatasourceTags(Linter): """ Lint that datasource tools have display and uihints tags """
[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("./inputs") if tool_node is None: tool_node = tool_xml.getroot() inputs = tool_xml.findall("./inputs//param") if is_datasource(tool_xml): # TODO only display is subtag of inputs, uihints is a separate top level tag (supporting only attrib minwidth) for datasource_tag in ("display", "uihints"): if not any(param.tag == datasource_tag for param in inputs): lint_ctx.info( f"{datasource_tag} tag usually present in data sources", linter=cls.name(), node=tool_node )
def _iter_param(tool_xml: "ElementTree") -> Iterator[Tuple["Element", str]]: for param in tool_xml.findall("./inputs//param"): if "name" not in param.attrib and "argument" not in param.attrib: continue param_name = _parse_name(param.attrib.get("name"), param.attrib.get("argument")) yield param, param_name def _iter_param_type(tool_xml: "ElementTree") -> Iterator[Tuple["Element", str, str]]: for param, param_name in _iter_param(tool_xml): if "type" not in param.attrib: continue param_type = param.attrib["type"] if param_type == "": continue yield param, param_name, param_type
[docs]class InputsName(Linter): """ Lint params define a name """
[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 param in tool_xml.findall("./inputs//param"): if "name" not in param.attrib and "argument" not in param.attrib: lint_ctx.error("Found param input with no name specified.", linter=cls.name(), node=param)
[docs]class InputsNameRedundantArgument(Linter): """ Lint params define a name """
[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 param in tool_xml.findall("./inputs//param"): if "name" in param.attrib and "argument" in param.attrib: param_name = _parse_name(param.attrib.get("name"), param.attrib.get("argument")) if param.attrib.get("name") == _parse_name(None, param.attrib.get("argument")): lint_ctx.warn( f"Param input [{param_name}] 'name' attribute is redundant if argument implies the same name.", linter=cls.name(), node=param, )
# TODO redundant with InputsNameValid
[docs]class InputsNameEmpty(Linter): """ Lint params define a non-empty name """
[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 param, param_name in _iter_param(tool_xml): if param_name.strip() == "": lint_ctx.error("Param input with empty name.", linter=cls.name(), node=param)
[docs]class InputsNameValid(Linter): """ Lint params define valid cheetah placeholder """
[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 param, param_name in _iter_param(tool_xml): if param_name != "" and not is_valid_cheetah_placeholder(param_name): lint_ctx.warn( f"Param input [{param_name}] is not a valid Cheetah placeholder.", linter=cls.name(), node=param )
def _param_path(param: "Element", param_name: str) -> str: path = [param_name] parent = param while True: parent = parent.getparent() if parent.tag == "inputs": break # parameters of the same name in different when branches are allowed # just add the value of the when branch to the path (this also allows # that the conditional select has the same name as params in the whens) if parent.tag == "when": path.append(str(parent.attrib.get("value"))) else: path.append(str(parent.attrib.get("name"))) return ".".join(reversed(path))
[docs]class InputsNameDuplicate(Linter): """ Lint params with duplicate names """
[docs] @classmethod def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"): tool_xml = getattr(tool_source, "xml_tree", None) if not tool_xml: return input_names = set() for param, param_name in _iter_param(tool_xml): # check for parameters with duplicate names path = _param_path(param, param_name) if path in input_names: lint_ctx.error( f"Tool defines multiple parameters with the same name: '{path}'", linter=cls.name(), node=param ) input_names.add(path)
[docs]class InputsNameDuplicateOutput(Linter): """ Lint params names that are also used in outputs """
[docs] @classmethod def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"): tool_xml = getattr(tool_source, "xml_tree", None) if not tool_xml: return input_names = set() for param, param_name in _iter_param(tool_xml): input_names.add(_param_path(param, param_name)) # check if there is an output with the same name as an input outputs = tool_xml.findall("./outputs/*") for output in outputs: if output.get("name") in input_names: lint_ctx.error( f'Tool defines an output with a name equal to the name of an input: \'{output.get("name")}\'', linter=cls.name(), node=output, )
[docs]class InputsTypeChildCombination(Linter): """ Lint for invalid parameter type child combinations """
[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 param, param_name, param_type in _iter_param_type(tool_xml): # lint for valid param type - child node combinations for ptcc in PARAM_TYPE_CHILD_COMBINATIONS: if param.find(ptcc[0]) is not None and param_type not in ptcc[1]: lint_ctx.error( f"Parameter [{param_name}] '{ptcc[0]}' tags are only allowed for parameters of type {ptcc[1]}", linter=cls.name(), node=param, )
[docs]class InputsDataFormat(Linter): """ Lint for data params wo format """
[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 param, param_name, param_type in _iter_param_type(tool_xml): if param_type != "data": continue if "format" not in param.attrib: lint_ctx.warn( f"Param input [{param_name}] with no format specified - 'data' format will be assumed.", linter=cls.name(), node=param, )
[docs]class InputsDataOptionsMultiple(Linter): """ Lint for data params with multiple options tags """
[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 param, param_name, param_type in _iter_param_type(tool_xml): if param_type != "data": continue options = param.findall("./options") if len(options) > 1: lint_ctx.error( f"Data parameter [{param_name}] contains multiple options elements.", linter=cls.name(), node=options[1], )
[docs]class InputsDataOptionsAttrib(Linter): """ Lint for data params with options that have invalid attributes """
[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 param, param_name, param_type in _iter_param_type(tool_xml): if param_type != "data": continue options = param.find("./options") if options is None: continue for oa in options.attrib: if oa != "options_filter_attribute": lint_ctx.error( f"Data parameter [{param_name}] uses invalid attribute: {oa}", linter=cls.name(), node=param )
[docs]class InputsDataOptionsFilterAttribFiltersType(Linter): """ Lint for valid filter types for data parameters if options set options_filter_attribute """
[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 param, param_name, param_type in _iter_param_type(tool_xml): if param_type != "data": continue options = param.find("./options") if options is None: continue for filter in param.findall("./options/filter"): if "options_filter_attribute" in options.attrib: if filter.get("type") != "data_meta": lint_ctx.error( f'Data parameter [{param_name}] for filters only type="data_meta" is allowed, found type="{filter.get("type")}"', linter=cls.name(), node=filter, )
[docs]class InputsDataOptionsFiltersType(Linter): """ Lint for valid filter types for data parameters if options do not set options_filter_attribute """
[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 param, param_name, param_type in _iter_param_type(tool_xml): if param_type != "data": continue options = param.find("./options") if options is None: continue for filter in param.findall("./options/filter"): if "options_filter_attribute" not in options.attrib: if filter.get("key") != "dbkey" or filter.get("type") != "data_meta": lint_ctx.error( f'Data parameter [{param_name}] for filters only type="data_meta" and key="dbkey" are allowed, found type="{filter.get("type")}" and key="{filter.get("key")}"', linter=cls.name(), node=filter, )
[docs]class InputsDataOptionsFiltersRef(Linter): """ Lint for set ref for filters of data parameters """
[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 param, param_name, param_type in _iter_param_type(tool_xml): if param_type != "data": continue options = param.find("./options") if options is None: continue for filter in param.findall("./options/filter"): if not filter.get("ref"): lint_ctx.error( f"Data parameter [{param_name}] filter needs to define a ref attribute", linter=cls.name(), node=filter, )
[docs]class InputsSelectDynamicOptions(Linter): """ Lint for select with deprecated dynamic_options attribute """
[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 param, param_name, param_type in _iter_param_type(tool_xml): if param_type != "select": continue dynamic_options = param.get("dynamic_options", None) if dynamic_options is not None: lint_ctx.warn( f"Select parameter [{param_name}] uses deprecated 'dynamic_options' attribute.", linter=cls.name(), node=param, )
[docs]class InputsSelectOptionsDef(Linter): """ Lint for valid ways to define select options """
[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 param, param_name, param_type in _iter_param_type(tool_xml): if param_type != "select": continue dynamic_options = param.get("dynamic_options", None) options = param.findall("./options") select_options = param.findall("./option") if param.getparent().tag != "conditional": if (dynamic_options is not None) + (len(options) > 0) + (len(select_options) > 0) != 1: lint_ctx.error( f"Select parameter [{param_name}] options have to be defined by either 'option' children elements, a 'options' element or the 'dynamic_options' attribute.", linter=cls.name(), node=param, )
[docs]class InputsSelectOptionsDefConditional(Linter): """ Lint for valid ways to define select options (for a select in a conditional) """
[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 param, param_name, param_type in _iter_param_type(tool_xml): if param_type != "select": continue dynamic_options = param.get("dynamic_options", None) options = param.findall("./options") select_options = param.findall("./option") if param.getparent().tag == "conditional": if len(select_options) == 0 or dynamic_options is not None or len(options) > 0: lint_ctx.error( f"Select parameter of a conditional [{param_name}] options have to be defined by 'option' children elements.", linter=cls.name(), node=param, )
# TODO xsd
[docs]class InputsSelectOptionValueMissing(Linter): """ Lint for select option tags without value """
[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 param, param_name, param_type in _iter_param_type(tool_xml): if param_type != "select": continue select_options = param.findall("./option") if any("value" not in option.attrib for option in select_options): lint_ctx.error( f"Select parameter [{param_name}] has option without value", linter=cls.name(), node=param )
[docs]class InputsSelectOptionDuplicateValue(Linter): """ Lint for select option with same value """
[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 param, param_name, param_type in _iter_param_type(tool_xml): if param_type != "select": continue select_options = param.findall("./option") select_options_values = list() for option in select_options: value = option.attrib.get("value", "") select_options_values.append((value, option.attrib.get("selected", "false"))) if len(set(select_options_values)) != len(select_options_values): lint_ctx.error( f"Select parameter [{param_name}] has multiple options with the same value", linter=cls.name(), node=param, )
[docs]class InputsSelectOptionDuplicateText(Linter): """ Lint for select option with same text """
[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 param, param_name, param_type in _iter_param_type(tool_xml): if param_type != "select": continue select_options = param.findall("./option") select_options_texts = list() for option in select_options: if option.text is None: text = option.attrib.get("value", "").capitalize() else: text = option.text select_options_texts.append((text, option.attrib.get("selected", "false"))) if len(set(select_options_texts)) != len(select_options_texts): lint_ctx.error( f"Select parameter [{param_name}] has multiple options with the same text content", linter=cls.name(), node=param, )
[docs]class InputsSelectOptionsMultiple(Linter): """ Lint dynamic options select for multiple options tags """
[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 param, param_name, param_type in _iter_param_type(tool_xml): if param_type != "select": continue options = param.findall("./options") if len(options) > 1: lint_ctx.error( f"Select parameter [{param_name}] contains multiple options elements.", linter=cls.name(), node=options[1], )
[docs]class InputsSelectOptionsDefinesOptions(Linter): """ Lint dynamic options select for the potential to define options """
[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 param, param_name, param_type in _iter_param_type(tool_xml): if param_type != "select": continue options = param.find("./options") if options is None: continue filter_adds_options = any( [f.get("type", None) in ["add_value", "data_meta"] for f in param.findall("./options/filter")] ) from_file = options.get("from_file", None) from_parameter = options.get("from_parameter", None) # TODO check if input param is present for from_dataset from_dataset = options.get("from_dataset", None) from_data_table = options.get("from_data_table", None) if ( from_file is None and from_parameter is None and from_dataset is None and from_data_table is None and not filter_adds_options ): lint_ctx.error( f"Select parameter [{param_name}] options tag defines no options. Use 'from_dataset', 'from_data_table', or a filter that adds values.", linter=cls.name(), node=options, )
[docs]class InputsSelectOptionsDeprecatedAttr(Linter): """ Lint dynamic options select deprecated attributes """
[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 param, param_name, param_type in _iter_param_type(tool_xml): if param_type != "select": continue options = param.find("./options") if options is None: continue for deprecated_attr in ["from_file", "from_parameter", "options_filter_attribute", "transform_lines"]: if options.get(deprecated_attr) is not None: lint_ctx.warn( f"Select parameter [{param_name}] options uses deprecated '{deprecated_attr}' attribute.", linter=cls.name(), node=options, )
[docs]class InputsSelectOptionsFromDatasetAndDatatable(Linter): """ Lint dynamic options select for from_dataset and from_data_table """
[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 param, param_name, param_type in _iter_param_type(tool_xml): if param_type != "select": continue options = param.find("./options") if options is None: continue from_dataset = options.get("from_dataset", None) from_data_table = options.get("from_data_table", None) if from_dataset is not None and from_data_table is not None: lint_ctx.error( f"Select parameter [{param_name}] options uses 'from_dataset' and 'from_data_table' attribute.", linter=cls.name(), node=options, )
[docs]class InputsSelectOptionsMetaFileKey(Linter): """ Lint dynamic options select: meta_file_key attribute can only be used with from_dataset """
[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 param, param_name, param_type in _iter_param_type(tool_xml): if param_type != "select": continue options = param.find("./options") if options is None: continue from_dataset = options.get("from_dataset", None) if options.get("meta_file_key", None) is not None and from_dataset is None: lint_ctx.error( f"Select parameter [{param_name}] 'meta_file_key' is only compatible with 'from_dataset'.", linter=cls.name(), node=options, )
[docs]class InputsBoolDistinctValues(Linter): """ Lint booleans for distinct true/false value """
[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 param, param_name, param_type in _iter_param_type(tool_xml): if param_type != "boolean": continue profile = tool_source.parse_profile() truevalue = param.attrib.get("truevalue", "true") falsevalue = param.attrib.get("falsevalue", "false") problematic_booleans_allowed = Version(profile) < Version("23.1") lint_level = lint_ctx.warn if problematic_booleans_allowed else lint_ctx.error if truevalue == falsevalue: lint_level( f"Boolean parameter [{param_name}] needs distinct 'truevalue' and 'falsevalue' values.", linter=cls.name(), node=param, )
[docs]class InputsBoolProblematic(Linter): """ Lint booleans for problematic values, i.e. truevalue being false and vice versa """
[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 param, param_name, param_type in _iter_param_type(tool_xml): if param_type != "boolean": continue profile = tool_source.parse_profile() truevalue = param.attrib.get("truevalue", "true") falsevalue = param.attrib.get("falsevalue", "false") problematic_booleans_allowed = Version(profile) < Version("23.1") lint_level = lint_ctx.warn if problematic_booleans_allowed else lint_ctx.error if truevalue.lower() == "false": lint_level( f"Boolean parameter [{param_name}] has invalid truevalue [{truevalue}].", linter=cls.name(), node=param, ) if falsevalue.lower() == "true": lint_level( f"Boolean parameter [{param_name}] has invalid falsevalue [{falsevalue}].", linter=cls.name(), node=param, )
[docs]class InputsSelectSingleCheckboxes(Linter): """ Lint selects that allow only a single selection but display as checkboxes """
[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 param, param_name, param_type in _iter_param_type(tool_xml): if param_type not in ["select", "data_column", "drill_down"]: continue multiple = string_as_bool(param.attrib.get("multiple", "false")) if param.attrib.get("display") == "checkboxes": if not multiple: lint_ctx.error( f'Select [{param_name}] `display="checkboxes"` is incompatible with `multiple="false"`, remove the `display` attribute', linter=cls.name(), node=param, )
[docs]class InputsSelectMandatoryCheckboxes(Linter): """ Lint selects that are mandatory but display as checkboxes """
[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 param, param_name, param_type in _iter_param_type(tool_xml): if param_type not in ["select", "data_column", "drill_down"]: continue multiple = string_as_bool(param.attrib.get("multiple", "false")) optional = string_as_bool(param.attrib.get("optional", multiple)) if param.attrib.get("display") == "checkboxes": if not optional: lint_ctx.error( f'Select [{param_name}] `display="checkboxes"` is incompatible with `optional="false"`, remove the `display` attribute', linter=cls.name(), node=param, )
[docs]class InputsSelectMultipleRadio(Linter): """ Lint selects that allow only multiple selections but display as radio """
[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 param, param_name, param_type in _iter_param_type(tool_xml): if param_type not in ["select", "data_column", "drill_down"]: continue multiple = string_as_bool(param.attrib.get("multiple", "false")) if param.attrib.get("display") == "radio": if multiple: lint_ctx.error( f'Select [{param_name}] display="radio" is incompatible with multiple="true"', linter=cls.name(), node=param, )
[docs]class InputsSelectOptionalRadio(Linter): """ Lint selects that are optional but display as radio """
[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 param, param_name, param_type in _iter_param_type(tool_xml): if param_type not in ["select", "data_column", "drill_down"]: continue multiple = string_as_bool(param.attrib.get("multiple", "false")) optional = string_as_bool(param.attrib.get("optional", multiple)) if param.attrib.get("display") == "radio": if optional: lint_ctx.error( f'Select [{param_name}] display="radio" is incompatible with optional="true"', linter=cls.name(), node=param, )
def _iter_param_validator(tool_xml: "ElementTree") -> Iterator[Tuple[str, str, "Element", str]]: input_params = tool_xml.findall("./inputs//param[@type]") for param in input_params: try: param_name = _parse_name(param.attrib.get("name"), param.attrib.get("argument")) except ValueError: continue param_type = param.attrib["type"] validators = param.findall("./validator[@type]") for validator in validators: vtype = validator.attrib["type"] yield (param_name, param_type, validator, vtype)
[docs]class ValidatorParamIncompatible(Linter): """ Lint for validator type - parameter type incompatibilities """
[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 param_name, param_type, validator, vtype in _iter_param_validator(tool_xml): if param_type in PARAMETER_VALIDATOR_TYPE_COMPATIBILITY: if vtype not in PARAMETER_VALIDATOR_TYPE_COMPATIBILITY[param_type]: lint_ctx.error( f"Parameter [{param_name}]: validator with an incompatible type '{vtype}'", linter=cls.name(), node=validator, )
[docs]class ValidatorAttribIncompatible(Linter): """ Lint for incompatibilities between validator type and given attributes """
[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 param_name, _, validator, vtype in _iter_param_validator(tool_xml): for attrib in ATTRIB_VALIDATOR_COMPATIBILITY: if attrib in validator.attrib and vtype not in ATTRIB_VALIDATOR_COMPATIBILITY[attrib]: lint_ctx.error( f"Parameter [{param_name}]: attribute '{attrib}' is incompatible with validator of type '{vtype}'", linter=cls.name(), node=validator, )
[docs]class ValidatorHasText(Linter): """ Lint that parameters that need text have text """
[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 param_name, _, validator, vtype in _iter_param_validator(tool_xml): if vtype in ["expression", "regex"]: if validator.text is None: lint_ctx.error( f"Parameter [{param_name}]: {vtype} validators are expected to contain text", linter=cls.name(), node=validator, )
[docs]class ValidatorHasNoText(Linter): """ Lint that parameters that need no text have no text """
[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 param_name, _, validator, vtype in _iter_param_validator(tool_xml): if vtype not in ["expression", "regex"] and validator.text is not None: lint_ctx.warn( f"Parameter [{param_name}]: '{vtype}' validators are not expected to contain text (found '{validator.text}')", linter=cls.name(), node=validator, )
[docs]class ValidatorExpression(Linter): """ Lint that checks expressions / regexp (ignoring FutureWarning) """
[docs] @classmethod def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"): tool_xml = getattr(tool_source, "xml_tree", None) if not tool_xml: return with warnings.catch_warnings(): warnings.simplefilter("error", FutureWarning) for param_name, _, validator, vtype in _iter_param_validator(tool_xml): if vtype in ["expression", "regex"] and validator.text is not None: try: if vtype == "regex": re.compile(validator.text) else: ast.parse(validator.text, mode="eval") except FutureWarning: pass except Exception as e: lint_ctx.error( f"Parameter [{param_name}]: '{validator.text}' is no valid {vtype}: {str(e)}", linter=cls.name(), node=validator, )
[docs]class ValidatorExpressionFuture(Linter): """ Lint that checks expressions / regexp FutureWarnings """
[docs] @classmethod def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"): tool_xml = getattr(tool_source, "xml_tree", None) if not tool_xml: return with warnings.catch_warnings(): warnings.simplefilter("error", FutureWarning) for param_name, _, validator, vtype in _iter_param_validator(tool_xml): if vtype in ["expression", "regex"] and validator.text is not None: try: if vtype == "regex": re.compile(validator.text) else: ast.parse(validator.text, mode="eval") except FutureWarning as e: lint_ctx.warn( f"Parameter [{param_name}]: '{validator.text}' is marked as deprecated {vtype}: {str(e)}", linter=cls.name(), node=validator, ) except Exception: pass
[docs]class ValidatorMinMax(Linter): """ Lint for min/max for validator that need it """
[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 param_name, _, validator, vtype in _iter_param_validator(tool_xml): if vtype in ["in_range", "length", "dataset_metadata_in_range"] and ( "min" not in validator.attrib and "max" not in validator.attrib ): lint_ctx.error( f"Parameter [{param_name}]: '{vtype}' validators need to define the 'min' or 'max' attribute(s)", linter=cls.name(), node=validator, )
[docs]class ValidatorDatasetMetadataEqualValue(Linter): """ Lint dataset_metadata_equal needs value or value_json """
[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 param_name, _, validator, vtype in _iter_param_validator(tool_xml): if vtype in ["dataset_metadata_equal"]: if ( not ("value" in validator.attrib or "value_json" in validator.attrib) or "metadata_name" not in validator.attrib ): lint_ctx.error( f"Parameter [{param_name}]: '{vtype}' validators need to define the 'value'/'value_json' and 'metadata_name' attributes", linter=cls.name(), node=validator, )
[docs]class ValidatorDatasetMetadataEqualValueOrJson(Linter): """ Lint dataset_metadata_equal needs either value or value_json """
[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 param_name, _, validator, vtype in _iter_param_validator(tool_xml): if vtype in ["dataset_metadata_equal"]: if "value" in validator.attrib and "value_json" in validator.attrib: lint_ctx.error( f"Parameter [{param_name}]: '{vtype}' validators must not define the 'value' and the 'value_json' attributes", linter=cls.name(), node=validator, )
[docs]class ValidatorMetadataCheckSkip(Linter): """ Lint metadata needs check or skip """
[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 param_name, _, validator, vtype in _iter_param_validator(tool_xml): if vtype in ["metadata"] and ("check" not in validator.attrib and "skip" not in validator.attrib): lint_ctx.error( f"Parameter [{param_name}]: '{vtype}' validators need to define the 'check' or 'skip' attribute(s)", linter=cls.name(), node=validator, )
[docs]class ValidatorTableName(Linter): """ Lint table_name is present if needed """
[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 param_name, _, validator, vtype in _iter_param_validator(tool_xml): if ( vtype in [ "value_in_data_table", "value_not_in_data_table", "dataset_metadata_in_data_table", "dataset_metadata_not_in_data_table", ] and "table_name" not in validator.attrib ): lint_ctx.error( f"Parameter [{param_name}]: '{vtype}' validators need to define the 'table_name' attribute", linter=cls.name(), node=validator, )
[docs]class ValidatorMetadataName(Linter): """ Lint metadata_name is present if needed """
[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 param_name, _, validator, vtype in _iter_param_validator(tool_xml): if ( vtype in [ "dataset_metadata_in_data_table", "dataset_metadata_not_in_data_table", "dataset_metadata_in_file", "dataset_metadata_in_range", ] and "metadata_name" not in validator.attrib ): lint_ctx.error( f"Parameter [{param_name}]: '{vtype}' validators need to define the 'metadata_name' attribute", linter=cls.name(), node=validator, )
def _iter_conditional(tool_xml: "ElementTree") -> Iterator[Tuple["Element", Optional[str], "Element", Optional[str]]]: conditionals = tool_xml.findall("./inputs//conditional") for conditional in conditionals: conditional_name = conditional.get("name") if conditional.get("value_from"): # Probably only the upload tool use this, no children elements continue first_param = conditional.find("param") if first_param is None: continue first_param_type = first_param.get("type") yield conditional, conditional_name, first_param, first_param_type
[docs]class ConditionalParamTypeBool(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 _, conditional_name, first_param, first_param_type in _iter_conditional(tool_xml): if first_param_type == "boolean": lint_ctx.warn( f'Conditional [{conditional_name}] first param of type="boolean" is discouraged, use a select', linter=cls.name(), node=first_param, )
[docs]class ConditionalParamType(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 _, conditional_name, first_param, first_param_type in _iter_conditional(tool_xml): if first_param_type not in ["boolean", "select"]: lint_ctx.error( f'Conditional [{conditional_name}] first param should have type="select"', linter=cls.name(), node=first_param, )
[docs]class ConditionalParamIncompatibleAttributes(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 _, conditional_name, first_param, first_param_type in _iter_conditional(tool_xml): if first_param_type not in ["boolean", "select"]: continue for incomp in ["optional", "multiple"]: if string_as_bool(first_param.get(incomp, False)): lint_ctx.warn( f'Conditional [{conditional_name}] test parameter cannot be {incomp}="true"', linter=cls.name(), node=first_param, )
[docs]class ConditionalWhenMissing(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 conditional, conditional_name, first_param, first_param_type in _iter_conditional(tool_xml): if first_param_type not in ["boolean", "select"]: continue if first_param_type == "select": options = first_param.findall("./option[@value]") option_ids = set([option.get("value") for option in options]) else: # boolean option_ids = set([first_param.get("truevalue", "true"), first_param.get("falsevalue", "false")]) whens = conditional.findall("./when[@value]") when_ids = set([w.get("value") for w in whens if w.get("value") is not None]) for option_id in option_ids - when_ids: lint_ctx.warn( f"Conditional [{conditional_name}] no <when /> block found for {first_param_type} option '{option_id}'", linter=cls.name(), node=conditional, )
[docs]class ConditionalOptionMissing(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 conditional, conditional_name, first_param, first_param_type in _iter_conditional(tool_xml): if first_param_type != "select": continue options = first_param.findall("./option[@value]") option_ids = set([option.get("value") for option in options]) whens = conditional.findall("./when[@value]") when_ids = set([w.get("value") for w in whens if w.get("value") is not None]) for when_id in when_ids - option_ids: lint_ctx.warn( f"Conditional [{conditional_name}] no <option /> found for when block '{when_id}'", linter=cls.name(), node=conditional, )
[docs]class ConditionalOptionMissingBoolean(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 conditional, conditional_name, first_param, first_param_type in _iter_conditional(tool_xml): if first_param_type != "boolean": continue option_ids = set([first_param.get("truevalue", "true"), first_param.get("falsevalue", "false")]) whens = conditional.findall("./when[@value]") when_ids = set([w.get("value") for w in whens if w.get("value")]) for when_id in when_ids - option_ids: lint_ctx.warn( f"Conditional [{conditional_name}] no truevalue/falsevalue found for when block '{when_id}'", linter=cls.name(), node=conditional, )