diff --git a/lib/galaxy/tool_util/parameters/factory.py b/lib/galaxy/tool_util/parameters/factory.py index e55ac884287..b183589dc4e 100644 --- a/lib/galaxy/tool_util/parameters/factory.py +++ b/lib/galaxy/tool_util/parameters/factory.py @@ -8,6 +8,8 @@ from typing import ( Union, ) +from typing_extensions import TypedDict + from galaxy.tool_util.parser.interface import ( InputSource, PageSource, @@ -76,6 +78,23 @@ def get_color_value(input_source: InputSource) -> str: return input_source.get("value", "#000000") +class _CommonParamKwargs(TypedDict, total=False): + label: str + help: str + + +def _common_param_kwargs(input_source: InputSource) -> _CommonParamKwargs: + """Extract common metadata (label, help) from InputSource for parameter models.""" + kwargs = _CommonParamKwargs() + label = input_source.parse_label() + if label: + kwargs["label"] = label + help_text = input_source.parse_help() + if help_text: + kwargs["help"] = help_text + return kwargs + + def _from_input_source_galaxy(input_source: InputSource, profile: float) -> ToolParameterT: input_type = input_source.parse_input_type() if input_type == "param": @@ -110,6 +129,7 @@ def _from_input_source_galaxy(input_source: InputSource, profile: float) -> Tool min=min_int, max=max_int, validators=int_validators, + **_common_param_kwargs(input_source), ) elif param_type == "boolean": nullable = input_source.parse_optional() @@ -119,6 +139,7 @@ def _from_input_source_galaxy(input_source: InputSource, profile: float) -> Tool name=input_source.parse_name(), optional=nullable, value=value, + **_common_param_kwargs(input_source), ) elif param_type == "text": optional, optionality_inferred = text_input_is_optional(input_source) @@ -131,6 +152,7 @@ def _from_input_source_galaxy(input_source: InputSource, profile: float) -> Tool optional=optional, validators=text_validators, value=default_value, + **_common_param_kwargs(input_source), ) elif param_type == "float": optional = input_source.parse_optional() @@ -162,6 +184,7 @@ def _from_input_source_galaxy(input_source: InputSource, profile: float) -> Tool min=min_float, max=max_float, validators=float_validators, + **_common_param_kwargs(input_source), ) elif param_type == "hidden": optional = input_source.parse_optional() @@ -173,6 +196,7 @@ def _from_input_source_galaxy(input_source: InputSource, profile: float) -> Tool optional=optional, value=value, validators=hidden_validators, + **_common_param_kwargs(input_source), ) elif param_type == "color": optional = input_source.parse_optional() @@ -181,11 +205,13 @@ def _from_input_source_galaxy(input_source: InputSource, profile: float) -> Tool name=input_source.parse_name(), optional=optional, value=get_color_value(input_source), + **_common_param_kwargs(input_source), ) elif param_type == "rules": return RulesParameterModel( type="rules", name=input_source.parse_name(), + **_common_param_kwargs(input_source), ) elif param_type == "data": optional = input_source.parse_optional() @@ -195,6 +221,7 @@ def _from_input_source_galaxy(input_source: InputSource, profile: float) -> Tool name=input_source.parse_name(), optional=optional, multiple=multiple, + **_common_param_kwargs(input_source), ) elif param_type == "data_collection": optional = input_source.parse_optional() @@ -205,6 +232,7 @@ def _from_input_source_galaxy(input_source: InputSource, profile: float) -> Tool name=input_source.parse_name(), optional=optional, value=default_value, + **_common_param_kwargs(input_source), ) elif param_type == "select": # Function... example in devteam cummeRbund. @@ -232,6 +260,7 @@ def _from_input_source_galaxy(input_source: InputSource, profile: float) -> Tool options=options, multiple=multiple, validators=select_validators, + **_common_param_kwargs(input_source), ) elif param_type == "drill_down": multiple = input_source.get_bool("multiple", False) @@ -246,6 +275,7 @@ def _from_input_source_galaxy(input_source: InputSource, profile: float) -> Tool multiple=multiple, hierarchy=hierarchy, options=static_options, + **_common_param_kwargs(input_source), ) elif param_type == "data_column": multiple = input_source.get_bool("multiple", False) @@ -269,6 +299,7 @@ def _from_input_source_galaxy(input_source: InputSource, profile: float) -> Tool multiple=multiple, optional=optional, value=value, + **_common_param_kwargs(input_source), ) elif param_type == "group_tag": multiple = input_source.get_bool("multiple", False) @@ -278,11 +309,13 @@ def _from_input_source_galaxy(input_source: InputSource, profile: float) -> Tool name=input_source.parse_name(), optional=optional, multiple=multiple, + **_common_param_kwargs(input_source), ) elif param_type == "baseurl": return BaseUrlParameterModel( type="baseurl", name=input_source.parse_name(), + **_common_param_kwargs(input_source), ) elif param_type == "genomebuild": optional = input_source.parse_optional() @@ -292,6 +325,7 @@ def _from_input_source_galaxy(input_source: InputSource, profile: float) -> Tool name=input_source.parse_name(), optional=optional, multiple=multiple, + **_common_param_kwargs(input_source), ) elif param_type == "directory_uri": directory_uri_validators: List[TextCompatiableValidators] = _text_validators(input_source) @@ -299,6 +333,7 @@ def _from_input_source_galaxy(input_source: InputSource, profile: float) -> Tool type="directory", name=input_source.parse_name(), validators=directory_uri_validators, + **_common_param_kwargs(input_source), ) else: raise UnknownParameterTypeError(f"Unknown Galaxy parameter type {param_type}") @@ -336,6 +371,7 @@ def _from_input_source_galaxy(input_source: InputSource, profile: float) -> Tool name=input_source.parse_name(), test_parameter=test_parameter, whens=whens, + **_common_param_kwargs(input_source), ) elif input_type == "repeat": name = input_source.get("name") @@ -353,6 +389,7 @@ def _from_input_source_galaxy(input_source: InputSource, profile: float) -> Tool parameters=instance_tool_parameter_models, min=min, max=max, + **_common_param_kwargs(input_source), ) elif input_type == "section": name = input_source.get("name") @@ -362,6 +399,7 @@ def _from_input_source_galaxy(input_source: InputSource, profile: float) -> Tool type="section", name=name, parameters=instance_tool_parameter_models, + **_common_param_kwargs(input_source), ) else: raise Exception( diff --git a/lib/galaxy/tool_util_models/parameters.py b/lib/galaxy/tool_util_models/parameters.py index 054c3d5235d..3b97d6aacf1 100644 --- a/lib/galaxy/tool_util_models/parameters.py +++ b/lib/galaxy/tool_util_models/parameters.py @@ -158,6 +158,10 @@ class ParamModel(Protocol): # input value MUST be specified. ... + def field_kwargs(self) -> Dict[str, Any]: + """Return kwargs for pydantic Field() including json_schema_extra metadata.""" + ... + def safe_field_name(name: str) -> str: if name.startswith("_"): @@ -165,6 +169,10 @@ def safe_field_name(name: str) -> str: return name +def _label_value_dicts(options: List[Any]) -> List[Dict[str, Any]]: + return [{"label": o.label, "value": o.value, "selected": o.selected} for o in options] + + def dynamic_model_information_from_py_type( param_model: ParamModel, py_type: Type, requires_value: Optional[bool] = None, validators=None ): @@ -177,9 +185,10 @@ def dynamic_model_information_from_py_type( if not py_type_is_optional and not requires_value: validators["not_null"] = field_validator(name)(Validators.validate_not_none) + field_kwargs = param_model.field_kwargs() return DynamicModelInformation( name, - (py_type, Field(initialize, alias=param_model.name if param_model.name != name else None)), + (py_type, Field(initialize, alias=param_model.name if param_model.name != name else None, **field_kwargs)), validators, ) @@ -198,6 +207,10 @@ class BaseToolParameterModelDefinition(ToolSourceBaseModel): def pydantic_template(self, state_representation: StateRepresentationT) -> DynamicModelInformation: """Return info needed to build Pydantic model at runtime for validation.""" + def field_kwargs(self) -> Dict[str, Any]: + """Return kwargs for pydantic Field() including json_schema_extra metadata.""" + return {"json_schema_extra": {"gx_type": self.parameter_type}} + class BaseGalaxyToolParameterModelDefinition(BaseToolParameterModelDefinition): hidden: bool = False @@ -219,6 +232,20 @@ class BaseGalaxyToolParameterModelDefinition(BaseToolParameterModelDefinition): is_dynamic: bool = False optional: Annotated[bool, Field(description="If `false`, parameter must have a value.")] = False + def field_kwargs(self) -> Dict[str, Any]: + kwargs: Dict[str, Any] = {} + if self.label: + kwargs["title"] = self.label + description_parts = [] + if self.help: + description_parts.append(self.help) + if self.argument: + description_parts.append(f"({self.argument})") + if description_parts: + kwargs["description"] = " ".join(description_parts) + kwargs["json_schema_extra"] = {"gx_type": self.parameter_type} + return kwargs + class LabelValue(BaseModel): label: str @@ -279,6 +306,14 @@ class TextParameterModel(BaseGalaxyToolParameterModelDefinition): default_options: List[LabelValue] = [] validators: List[TextCompatiableValidators] = [] + def field_kwargs(self) -> Dict[str, Any]: + kwargs = super().field_kwargs() + extra = kwargs["json_schema_extra"] + extra["gx_area"] = self.area + if self.default_options: + extra["gx_default_options"] = _label_value_dicts(self.default_options) + return kwargs + @property def py_type(self) -> Type: return optional_if_needed(StrictStr, self.optional) @@ -318,6 +353,15 @@ class IntegerParameterModel(BaseGalaxyToolParameterModelDefinition): max: Optional[int] = None validators: List[NumberCompatiableValidators] = [] + def field_kwargs(self) -> Dict[str, Any]: + kwargs = super().field_kwargs() + extra = kwargs["json_schema_extra"] + if self.min is not None: + extra["gx_min"] = self.min + if self.max is not None: + extra["gx_max"] = self.max + return kwargs + @property def py_type(self) -> Type: return optional_if_needed(StrictInt, self.optional) @@ -350,6 +394,15 @@ class FloatParameterModel(BaseGalaxyToolParameterModelDefinition): max: Optional[float] = None validators: List[NumberCompatiableValidators] = [] + def field_kwargs(self) -> Dict[str, Any]: + kwargs = super().field_kwargs() + extra = kwargs["json_schema_extra"] + if self.min is not None: + extra["gx_min"] = self.min + if self.max is not None: + extra["gx_max"] = self.max + return kwargs + @property def py_type(self) -> Type: return optional_if_needed(union_type([StrictInt, StrictFloat]), self.optional) @@ -919,6 +972,17 @@ class DataParameterModel(BaseGalaxyToolParameterModelDefinition): min: Optional[int] = None max: Optional[int] = None + def field_kwargs(self) -> Dict[str, Any]: + kwargs = super().field_kwargs() + extra = kwargs["json_schema_extra"] + extra["gx_extensions"] = self.extensions + extra["gx_multiple"] = self.multiple + if self.min is not None: + extra["gx_min"] = self.min + if self.max is not None: + extra["gx_max"] = self.max + return kwargs + @property def py_type(self) -> Type: base_model: Type @@ -1111,6 +1175,11 @@ class DataCollectionParameterModel(BaseGalaxyToolParameterModelDefinition): extensions: List[str] = ["data"] value: Optional[Dict[str, Any]] + def field_kwargs(self) -> Dict[str, Any]: + kwargs = super().field_kwargs() + kwargs["json_schema_extra"]["gx_extensions"] = self.extensions + return kwargs + @property def py_type(self) -> Type: return optional_if_needed(DataCollectionRequestOrCollectionUri, self.optional) @@ -1294,9 +1363,10 @@ class ColorParameterModel(BaseGalaxyToolParameterModelDefinition): validators = {"color_format": field_validator(self.name)(ColorParameterModel.validate_color_str_if_value)} else: validators = {"color_format": field_validator(self.name)(ColorParameterModel.validate_color_str)} + field_kwargs = self.field_kwargs() return DynamicModelInformation( self.name, - (py_type, initialize), + (py_type, Field(initialize, **field_kwargs)), validators, ) @@ -1392,6 +1462,14 @@ class SelectParameterModel(BaseGalaxyToolParameterModelDefinition): multiple: bool = False validators: List[SelectCompatiableValidators] = [] + def field_kwargs(self) -> Dict[str, Any]: + kwargs = super().field_kwargs() + extra = kwargs["json_schema_extra"] + if self.options is not None and self.options: + extra["gx_options"] = _label_value_dicts(self.options) + extra["gx_multiple"] = self.multiple + return kwargs + @staticmethod def split_str(cls, data: Any) -> Any: if isinstance(data, str): @@ -1498,6 +1576,11 @@ class GenomeBuildParameterModel(BaseGalaxyToolParameterModelDefinition): type: Literal["genomebuild"] multiple: bool + def field_kwargs(self) -> Dict[str, Any]: + kwargs = super().field_kwargs() + kwargs["json_schema_extra"]["gx_multiple"] = self.multiple + return kwargs + @property def py_type(self) -> Type: py_type: Type = StrictStr @@ -1552,6 +1635,11 @@ class DrillDownParameterModel(BaseGalaxyToolParameterModelDefinition): multiple: bool hierarchy: DrillDownHierarchyT + def field_kwargs(self) -> Dict[str, Any]: + kwargs = super().field_kwargs() + kwargs["json_schema_extra"]["gx_multiple"] = self.multiple + return kwargs + @property def py_type(self) -> Type: if self.options is not None: @@ -1645,6 +1733,11 @@ class DataColumnParameterModel(BaseGalaxyToolParameterModelDefinition): multiple: bool value: Optional[Union[int, List[int]]] = None + def field_kwargs(self) -> Dict[str, Any]: + kwargs = super().field_kwargs() + kwargs["json_schema_extra"]["gx_multiple"] = self.multiple + return kwargs + @staticmethod def split_str(cls, data: Any) -> Any: if isinstance(data, str): @@ -1695,6 +1788,11 @@ class GroupTagParameterModel(BaseGalaxyToolParameterModelDefinition): type: Literal["group_tag"] multiple: bool + def field_kwargs(self) -> Dict[str, Any]: + kwargs = super().field_kwargs() + kwargs["json_schema_extra"]["gx_multiple"] = self.multiple + return kwargs + @property def py_type(self) -> Type: py_type: Type = StrictStr @@ -1757,6 +1855,18 @@ class ConditionalParameterModel(BaseGalaxyToolParameterModelDefinition): test_parameter: Union[BooleanParameterModel, SelectParameterModel] whens: List[ConditionalWhen] + def field_kwargs(self) -> Dict[str, Any]: + kwargs = super().field_kwargs() + extra = kwargs["json_schema_extra"] + test_param = self.test_parameter + if isinstance(test_param, SelectParameterModel) and test_param.options: + extra["gx_options"] = _label_value_dicts(test_param.options) + if test_param.label: + extra["gx_test_label"] = test_param.label + if test_param.help: + extra["gx_test_help"] = test_param.help + return kwargs + def pydantic_template(self, state_representation: StateRepresentationT) -> DynamicModelInformation: is_boolean = isinstance(self.test_parameter, BooleanParameterModel) test_param_name = self.test_parameter.name @@ -1846,9 +1956,10 @@ class ConditionalParameterModel(BaseGalaxyToolParameterModelDefinition): else: initialize_cond = None + field_kwargs = self.field_kwargs() return DynamicModelInformation( self.name, - (py_type, initialize_cond), + (py_type, Field(initialize_cond, **field_kwargs)), {}, ) @@ -1864,6 +1975,15 @@ class RepeatParameterModel(BaseGalaxyToolParameterModelDefinition): min: Optional[int] = None max: Optional[int] = None + def field_kwargs(self) -> Dict[str, Any]: + kwargs = super().field_kwargs() + extra = kwargs["json_schema_extra"] + if self.min is not None: + extra["gx_min"] = self.min + if self.max is not None: + extra["gx_max"] = self.max + return kwargs + def pydantic_template(self, state_representation: StateRepresentationT) -> DynamicModelInformation: # Maybe validators for min and max... instance_class: Type[BaseModel] = create_field_model( @@ -1887,9 +2007,10 @@ class RepeatParameterModel(BaseGalaxyToolParameterModelDefinition): class RepeatType(RootModel): root: List[instance_class] = Field(initialize_repeat, min_length=min_length, max_length=max_length) # type: ignore[valid-type] + field_kwargs = self.field_kwargs() return DynamicModelInformation( self.name, - (RepeatType, initialize_repeat), + (RepeatType, Field(initialize_repeat, **field_kwargs)), {}, ) @@ -1921,9 +2042,10 @@ class SectionParameterModel(BaseGalaxyToolParameterModelDefinition): initialize_section = ... else: initialize_section = None + field_kwargs = self.field_kwargs() return DynamicModelInformation( self.name, - (instance_class, initialize_section), + (instance_class, Field(initialize_section, **field_kwargs)), {}, )