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

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

import ast
import re
import warnings
from copy import deepcopy
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 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, )
FILTER_REQUIRED_ATTRIBUTES = { "data_meta": ["type", "ref", "key"], # column needs special treatment "param_value": ["type", "ref", "column"], "static_value": ["type", "column", "value"], "regexp": ["type", "column", "value"], "unique_value": ["type", "column"], "multiple_splitter": ["type", "column"], "attribute_value_splitter": ["type", "column"], "add_value": ["type", "value"], "remove_value": ["type"], # this is handled separately in InputsOptionsRemoveValueFilterRequiredAttributes "sort_by": ["type", "column"], "data_table": ["type", "column", "table_name", "data_table_column"], }
[docs]class InputsOptionsFiltersRequiredAttributes(Linter): """ check required attributes of filters """
[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): options = param.find("./options") if options is None: continue for filter in param.findall("./options/filter"): filter_type = filter.get("type", None) if filter_type is None or filter_type not in FILTER_ALLOWED_ATTRIBUTES: continue for attrib in FILTER_REQUIRED_ATTRIBUTES[filter_type]: if attrib not in filter.attrib: lint_ctx.error( f"Select parameter [{param_name}] '{filter_type}' filter misses required attribute '{attrib}'", node=filter, )
[docs]class InputsOptionsRemoveValueFilterRequiredAttributes(Linter): """ check required attributes of remove_value filter """
[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): options = param.find("./options") if options is None: continue for filter in param.findall("./options/filter"): filter_type = filter.get("type", None) # check for required attributes for filter (remove_value needs a bit more logic here) if filter_type != "remove_value": continue if not ( ( "value" in filter.attrib and "ref" not in filter.attrib and "meta_ref" not in filter.attrib and "key" not in filter.attrib ) or ( "value" not in filter.attrib and "ref" in filter.attrib and "meta_ref" not in filter.attrib and "key" not in filter.attrib ) or ( "value" not in filter.attrib and "ref" not in filter.attrib and "meta_ref" in filter.attrib and "key" in filter.attrib ) ): lint_ctx.error( f"Select parameter [{param_name}] '{filter_type}'' filter needs either the 'value'; 'ref'; or 'meta' and 'key' attribute(s)", node=filter, )
FILTER_ALLOWED_ATTRIBUTES = deepcopy(FILTER_REQUIRED_ATTRIBUTES) FILTER_ALLOWED_ATTRIBUTES["static_value"].append("keep") FILTER_ALLOWED_ATTRIBUTES["regexp"].append("keep") FILTER_ALLOWED_ATTRIBUTES["data_meta"].extend(["column", "multiple", "separator"]) FILTER_ALLOWED_ATTRIBUTES["param_value"].extend(["keep", "ref_attribute"]) FILTER_ALLOWED_ATTRIBUTES["multiple_splitter"].append("separator") FILTER_ALLOWED_ATTRIBUTES["attribute_value_splitter"].extend(["pair_separator", "name_val_separator"]) FILTER_ALLOWED_ATTRIBUTES["add_value"].extend(["name", "index"]) FILTER_ALLOWED_ATTRIBUTES["remove_value"].extend(["value", "ref", "meta_ref", "key"]) FILTER_ALLOWED_ATTRIBUTES["data_table"].append("keep")
[docs]class InputsOptionsFiltersAllowedAttributes(Linter): """ check allowed attributes of filters """
[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): options = param.find("./options") if options is None: continue for filter in param.findall("./options/filter"): filter_type = filter.get("type", None) if filter_type is None or filter_type not in FILTER_ALLOWED_ATTRIBUTES: continue for attrib in filter.attrib: if attrib not in FILTER_ALLOWED_ATTRIBUTES[filter_type]: lint_ctx.warn( f"Select parameter [{param_name}] '{filter_type}' filter specifies unnecessary attribute '{attrib}'", node=filter, )
[docs]class InputsOptionsRegexFilterExpression(Linter): """ Check the regular expression of regexp filters """
[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): options = param.find("./options") if options is None: continue for filter in param.findall("./options/filter"): filter_type = filter.get("type", None) if filter_type == "regexp" and "value" in filter.attrib: try: re.compile(filter.attrib["value"]) except re.error as re_error: lint_ctx.error( f"Select parameter [{param_name}] '{filter_type}'' filter 'value' is not a valid regular expression ({re_error})'", node=filter, )
[docs]class InputsOptionsFiltersCheckReferences(Linter): """ Check the references used in filters """
[docs] @classmethod def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"): tool_xml = getattr(tool_source, "xml_tree", None) if not tool_xml: return # get the set of param names param_names = {param_name for _, param_name in _iter_param(tool_xml)} for param, param_name in _iter_param(tool_xml): options = param.find("./options") if options is None: continue for filter in param.findall("./options/filter"): filter_type = filter.get("type", None) if filter_type is not None: # check for references to other inputs # TODO: currently ref and metaref seem only to work for top level params, # once this is fixed the linter needs to be extended, e.g. `f.attrib[ref_attrib].split('|')[-1]` for ref_attrib in ["meta_ref", "ref"]: if ref_attrib in filter.attrib and filter.attrib[ref_attrib] not in param_names: lint_ctx.error( f"Select parameter [{param_name}] '{filter_type}'' filter attribute '{ref_attrib}' refers to non existing parameter '{filter.attrib[ref_attrib]}'", 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 = [] 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 = [] 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) from_url = options.get("from_url", None) if ( from_file is None and from_parameter is None and from_dataset is None and from_data_table is None and from_url 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 = {option.get("value") for option in options} else: # boolean option_ids = {first_param.get("truevalue", "true"), first_param.get("falsevalue", "false")} whens = conditional.findall("./when[@value]") when_ids = {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 = {option.get("value") for option in options} whens = conditional.findall("./when[@value]") when_ids = {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 = {first_param.get("truevalue", "true"), first_param.get("falsevalue", "false")} whens = conditional.findall("./when[@value]") when_ids = {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, )