diff --git a/lib/galaxy/tool_util/parameters/_types.py b/lib/galaxy/tool_util/parameters/_types.py
index 2b97ee16c200..9bb94d8c97c0 100644
--- a/lib/galaxy/tool_util/parameters/_types.py
+++ b/lib/galaxy/tool_util/parameters/_types.py
@@ -6,6 +6,7 @@
"""
from typing import (
+ Any,
cast,
List,
Optional,
@@ -15,6 +16,7 @@
# https://stackoverflow.com/questions/56832881/check-if-a-field-is-typing-optional
from typing_extensions import (
+ Annotated,
get_args,
get_origin,
)
@@ -46,3 +48,12 @@ def cast_as_type(arg) -> Type:
def is_optional(field) -> bool:
return get_origin(field) is Union and type(None) in get_args(field)
+
+
+def expand_annotation(field: Type, new_annotations: List[Any]) -> Type:
+ is_annotation = get_origin(field) is Annotated
+ if is_annotation:
+ args = get_args(field) # noqa: F841
+ return Annotated[tuple([args[0], *args[1:], *new_annotations])]
+ else:
+ return Annotated[tuple([field, *new_annotations])]
diff --git a/lib/galaxy/tool_util/parameters/factory.py b/lib/galaxy/tool_util/parameters/factory.py
index e200b67aeb0e..ff67726ef35d 100644
--- a/lib/galaxy/tool_util/parameters/factory.py
+++ b/lib/galaxy/tool_util/parameters/factory.py
@@ -14,6 +14,11 @@
PagesSource,
ToolSource,
)
+from galaxy.tool_util.parser.parameter_validators import (
+ InRangeParameterValidatorModel,
+ LengthParameterValidatorModel,
+ static_validators,
+)
from galaxy.tool_util.parser.util import parse_profile_version
from galaxy.util import string_as_bool
from .models import (
@@ -42,10 +47,12 @@
HiddenParameterModel,
IntegerParameterModel,
LabelValue,
+ NumberCompatiableValidators,
RepeatParameterModel,
RulesParameterModel,
SectionParameterModel,
SelectParameterModel,
+ TextCompatiableValidators,
TextParameterModel,
ToolParameterBundle,
ToolParameterBundleModel,
@@ -82,7 +89,12 @@ def _from_input_source_galaxy(input_source: InputSource, profile: float) -> Tool
int_value = None
else:
raise ParameterDefinitionError()
- return IntegerParameterModel(name=input_source.parse_name(), optional=optional, value=int_value)
+ static_validator_models = static_validators(input_source.parse_validators())
+ validators: List[NumberCompatiableValidators] = []
+ for static_validator in static_validator_models:
+ if static_validator.type == "in_range":
+ validators.append(cast(InRangeParameterValidatorModel, static_validator))
+ return IntegerParameterModel(name=input_source.parse_name(), optional=optional, value=int_value, validators=validators)
elif param_type == "boolean":
nullable = input_source.parse_optional()
value = input_source.get_bool_or_none("checked", None if nullable else False)
@@ -93,9 +105,15 @@ def _from_input_source_galaxy(input_source: InputSource, profile: float) -> Tool
)
elif param_type == "text":
optional = input_source.parse_optional()
+ static_validator_models = static_validators(input_source.parse_validators())
+ validators: List[TextCompatiableValidators] = []
+ for static_validator in static_validator_models:
+ if static_validator.type == "length":
+ validators.append(cast(LengthParameterValidatorModel, static_validator))
return TextParameterModel(
name=input_source.parse_name(),
optional=optional,
+ validators=validators,
)
elif param_type == "float":
optional = input_source.parse_optional()
@@ -107,10 +125,16 @@ def _from_input_source_galaxy(input_source: InputSource, profile: float) -> Tool
float_value = None
else:
raise ParameterDefinitionError()
+ static_validator_models = static_validators(input_source.parse_validators())
+ validators: List[NumberCompatiableValidators] = []
+ for static_validator in static_validator_models:
+ if static_validator.type == "in_range":
+ validators.append(cast(InRangeParameterValidatorModel, static_validator))
return FloatParameterModel(
name=input_source.parse_name(),
optional=optional,
value=float_value,
+ validators=validators,
)
elif param_type == "hidden":
optional = input_source.parse_optional()
diff --git a/lib/galaxy/tool_util/parameters/models.py b/lib/galaxy/tool_util/parameters/models.py
index 7a70c4d5c245..54dd2c4f16d3 100644
--- a/lib/galaxy/tool_util/parameters/models.py
+++ b/lib/galaxy/tool_util/parameters/models.py
@@ -24,6 +24,7 @@
Field,
field_validator,
HttpUrl,
+ PlainValidator,
RootModel,
StrictBool,
StrictFloat,
@@ -44,8 +45,14 @@
JsonTestCollectionDefDict,
JsonTestDatasetDefDict,
)
+from galaxy.tool_util.parser.parameter_validators import (
+ InRangeParameterValidatorModel,
+ LengthParameterValidatorModel,
+ StaticValidatorModel,
+)
from ._types import (
cast_as_type,
+ expand_annotation,
is_optional as is_python_type_optional,
list_type,
optional,
@@ -179,11 +186,34 @@ class LabelValue(BaseModel):
selected: bool
+TextCompatiableValidators = Union[
+ LengthParameterValidatorModel,
+]
+
+
+def pydantic_validator_for(validator_model: StaticValidatorModel):
+
+ def validator(v: Any) -> Any:
+ validator_model.validate(v)
+ return v
+
+ return PlainValidator(validator)
+
+
+# actual typing will require generics and a typevar I think...
+def static_tool_validators_to_pydantic(static_tool_param_validators: List[StaticValidatorModel]):
+ pydantic_validators = []
+ for static_validator in static_tool_param_validators:
+ pydantic_validators.append(pydantic_validator_for(static_validator))
+ return pydantic_validators
+
+
class TextParameterModel(BaseGalaxyToolParameterModelDefinition):
parameter_type: Literal["gx_text"] = "gx_text"
area: bool = False
default_value: Optional[str] = Field(default=None, alias="value")
default_options: List[LabelValue] = []
+ validators: List[TextCompatiableValidators] = []
@property
def py_type(self) -> Type:
@@ -196,6 +226,9 @@ def pydantic_template(self, state_representation: StateRepresentationT) -> Dynam
requires_value = self.request_requires_value
if state_representation == "job_internal":
requires_value = True
+ validators = static_tool_validators_to_pydantic(self.validators)
+ if validators:
+ py_type = expand_annotation(py_type, validators)
return dynamic_model_information_from_py_type(self, py_type, requires_value=requires_value)
@property
@@ -203,12 +236,18 @@ def request_requires_value(self) -> bool:
return False
+NumberCompatiableValidators = Union[
+ InRangeParameterValidatorModel,
+]
+
+
class IntegerParameterModel(BaseGalaxyToolParameterModelDefinition):
parameter_type: Literal["gx_integer"] = "gx_integer"
optional: bool
value: Optional[int] = None
min: Optional[int] = None
max: Optional[int] = None
+ validators: List[NumberCompatiableValidators] = None
@property
def py_type(self) -> Type:
@@ -223,6 +262,9 @@ def pydantic_template(self, state_representation: StateRepresentationT) -> Dynam
requires_value = True
elif _is_landing_request(state_representation):
requires_value = False
+ validators = static_tool_validators_to_pydantic(self.validators)
+ if validators:
+ py_type = expand_annotation(py_type, validators)
return dynamic_model_information_from_py_type(self, py_type, requires_value=requires_value)
@property
@@ -235,6 +277,7 @@ class FloatParameterModel(BaseGalaxyToolParameterModelDefinition):
value: Optional[float] = None
min: Optional[float] = None
max: Optional[float] = None
+ validators: List[NumberCompatiableValidators] = None
@property
def py_type(self) -> Type:
@@ -244,6 +287,9 @@ def pydantic_template(self, state_representation: StateRepresentationT) -> Dynam
py_type = self.py_type
if state_representation == "workflow_step_linked":
py_type = allow_connected_value(py_type)
+ validators = static_tool_validators_to_pydantic(self.validators)
+ if validators:
+ py_type = expand_annotation(py_type, validators)
return dynamic_model_information_from_py_type(self, py_type)
@property
diff --git a/test/functional/tools/parameters/gx_int_validation_range.xml b/test/functional/tools/parameters/gx_int_validation_range.xml
new file mode 100644
index 000000000000..5f7862497abf
--- /dev/null
+++ b/test/functional/tools/parameters/gx_int_validation_range.xml
@@ -0,0 +1,15 @@
+
+ > '$output'
+ ]]>
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/functional/tools/parameters/gx_text_length_validation.xml b/test/functional/tools/parameters/gx_text_length_validation.xml
new file mode 100644
index 000000000000..4d5434aa6c58
--- /dev/null
+++ b/test/functional/tools/parameters/gx_text_length_validation.xml
@@ -0,0 +1,20 @@
+
+ > '$output';
+cat '$inputs' >> $inputs_json;
+ ]]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/functional/tools/parameters/gx_text_length_validation_negate.xml b/test/functional/tools/parameters/gx_text_length_validation_negate.xml
new file mode 100644
index 000000000000..4b0b835fe562
--- /dev/null
+++ b/test/functional/tools/parameters/gx_text_length_validation_negate.xml
@@ -0,0 +1,20 @@
+
+ > '$output';
+cat '$inputs' >> $inputs_json;
+ ]]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/unit/tool_util/parameter_specification.yml b/test/unit/tool_util/parameter_specification.yml
index a46de9e0ec83..31975ac5ba25 100644
--- a/test/unit/tool_util/parameter_specification.yml
+++ b/test/unit/tool_util/parameter_specification.yml
@@ -191,6 +191,16 @@ gx_int_required: &gx_int_required
gx_int_required_via_empty_string:
<<: *gx_int_required
+
+gx_int_validation_range:
+ request_valid:
+ - parameter: 1
+ - parameter: 9
+ request_invalid:
+ - parameter: -1
+ - parameter: 11
+ - parameter: 10
+
gx_text:
request_valid: &gx_text_request_valid
- parameter: moocow
@@ -294,6 +304,21 @@ gx_text_optional:
- parameter: {}
- parameter: { "moo": "cow" }
+gx_text_length_validation:
+ request_valid:
+ - parameter: "mytext"
+ - parameter: "mytext123"
+ request_invalid:
+ - parameter: "s" # too short
+ - parameter: "mytext1231231231sd" # too long
+
+gx_text_length_validation_negate:
+ request_valid:
+ - parameter: "m"
+ - parameter: "mytext123mocowdowees"
+ request_invalid:
+ - parameter: "goldilocks"
+
gx_select:
request_valid:
- parameter: "--ex1"