Skip to content

Commit

Permalink
Integrate static validators into Pydantic models...
Browse files Browse the repository at this point in the history
  • Loading branch information
jmchilton committed Oct 3, 2024
1 parent f9216e6 commit e7af9ad
Show file tree
Hide file tree
Showing 7 changed files with 162 additions and 1 deletion.
11 changes: 11 additions & 0 deletions lib/galaxy/tool_util/parameters/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"""

from typing import (
Any,
cast,
List,
Optional,
Expand All @@ -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,
)
Expand Down Expand Up @@ -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])]
26 changes: 25 additions & 1 deletion lib/galaxy/tool_util/parameters/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -42,10 +47,12 @@
HiddenParameterModel,
IntegerParameterModel,
LabelValue,
NumberCompatiableValidators,
RepeatParameterModel,
RulesParameterModel,
SectionParameterModel,
SelectParameterModel,
TextCompatiableValidators,
TextParameterModel,
ToolParameterBundle,
ToolParameterBundleModel,
Expand Down Expand Up @@ -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)
Expand All @@ -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()
Expand All @@ -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()
Expand Down
46 changes: 46 additions & 0 deletions lib/galaxy/tool_util/parameters/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
Field,
field_validator,
HttpUrl,
PlainValidator,
RootModel,
StrictBool,
StrictFloat,
Expand All @@ -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,
Expand Down Expand Up @@ -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:
Expand All @@ -196,19 +226,28 @@ 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
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:
Expand All @@ -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
Expand All @@ -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:
Expand All @@ -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
Expand Down
15 changes: 15 additions & 0 deletions test/functional/tools/parameters/gx_int_validation_range.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<tool id="gx_int_validation_range" name="gx_int_validation_range" version="1.0.0">
<command><![CDATA[
echo '$parameter' >> '$output'
]]></command>
<inputs>
<param name="parameter" value="1" type="integer">
<validator type="in_range" min="0" max="10" exclude_min="false" exclude_max="true" />
</param>
</inputs>
<outputs>
<data name="output" format="txt" />
</outputs>
<tests>
</tests>
</tool>
20 changes: 20 additions & 0 deletions test/functional/tools/parameters/gx_text_length_validation.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<tool id="gx_text_length_validation" name="gx_text_length_validation" version="1.0.0">
<command><![CDATA[
echo '$parameter' >> '$output';
cat '$inputs' >> $inputs_json;
]]></command>
<configfiles>
<inputs name="inputs" filename="inputs.json" />
</configfiles>
<inputs>
<param name="parameter" type="text">
<validator type="length" min="2" max="10" />
</param>
</inputs>
<outputs>
<data name="output" format="txt" />
<data name="inputs_json" format="json" />
</outputs>
<tests>
</tests>
</tool>
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<tool id="gx_text_length_validation" name="gx_text_length_validation" version="1.0.0">
<command><![CDATA[
echo '$parameter' >> '$output';
cat '$inputs' >> $inputs_json;
]]></command>
<configfiles>
<inputs name="inputs" filename="inputs.json" />
</configfiles>
<inputs>
<param name="parameter" type="text">
<validator type="length" min="2" max="10" negate="true" />
</param>
</inputs>
<outputs>
<data name="output" format="txt" />
<data name="inputs_json" format="json" />
</outputs>
<tests>
</tests>
</tool>
25 changes: 25 additions & 0 deletions test/unit/tool_util/parameter_specification.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down

0 comments on commit e7af9ad

Please sign in to comment.