Skip to content

Commit ad953fa

Browse files
committed
Fix default seeding
1 parent d0b84d2 commit ad953fa

4 files changed

Lines changed: 80 additions & 0 deletions

File tree

lib/galaxy/tool_util/parameters/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
landing_encode,
5353
MappedCollectionInput,
5454
RequestInternalToWorkflowStateError,
55+
seed_repeat_defaults,
5556
strictify,
5657
to_workflow_step_state,
5758
)
@@ -188,6 +189,7 @@
188189
"landing_decode",
189190
"landing_encode",
190191
"dereference",
192+
"seed_repeat_defaults",
191193
"strictify",
192194
"to_workflow_step_state",
193195
"from_workflow_execution_state",

lib/galaxy/tool_util/parameters/convert.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -639,6 +639,44 @@ def _initialize_repeat_state(parameter: RepeatParameterModel, tool_state: dict[s
639639
return repeat_instances
640640

641641

642+
def seed_repeat_defaults(tool_state: dict[str, Any], input_models: ToolParameterBundle) -> None:
643+
"""Seed ``<repeat default="N">`` instances for repeats OMITTED from ``tool_state``, in place.
644+
645+
The async execution path's legacy meta-parameter visitor rewrites every omitted repeat to
646+
``[]`` before ``fill_static_defaults`` runs, masking the omission so a repeat whose ``default``
647+
exceeds its ``min`` never gets seeded. Seeding here - while the omission is still visible -
648+
mirrors the legacy ``Repeat.get_initial_value``. Only absent repeats are seeded, so an
649+
explicit ``[]`` still means zero; descent is limited to containers already present in the
650+
state (fill_static_defaults handles omitted ones and their ``min`` padding).
651+
"""
652+
_seed_repeat_defaults(tool_state, input_models.parameters)
653+
654+
655+
def _seed_repeat_defaults(tool_state: dict[str, Any], parameters: Sequence[ToolParameterT]) -> None:
656+
for parameter in parameters:
657+
if isinstance(parameter, RepeatParameterModel):
658+
if parameter.name not in tool_state and not parameter.default:
659+
continue
660+
for instance_state in _initialize_repeat_state(parameter, tool_state):
661+
_seed_repeat_defaults(instance_state, parameter.parameters)
662+
elif isinstance(parameter, SectionParameterModel):
663+
section_state = tool_state.get(parameter.name)
664+
if isinstance(section_state, dict):
665+
_seed_repeat_defaults(section_state, parameter.parameters)
666+
elif isinstance(parameter, ConditionalParameterModel):
667+
conditional_state = tool_state.get(parameter.name)
668+
if not isinstance(conditional_state, dict):
669+
continue
670+
test_parameter_name = parameter.test_parameter.name
671+
explicit_test_value = conditional_state.get(test_parameter_name)
672+
test_value = validate_explicit_conditional_test_value(test_parameter_name, explicit_test_value)
673+
try:
674+
when = _select_which_when(parameter, test_value, conditional_state)
675+
except Exception:
676+
continue
677+
_seed_repeat_defaults(conditional_state, when.parameters)
678+
679+
642680
def _select_which_when(
643681
conditional: ConditionalParameterModel, test_value: DiscriminatorType | None, conditional_state: dict[str, Any]
644682
) -> ConditionalWhen:

lib/galaxy/tools/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@
9393
input_models_for_pages,
9494
JobInternalToolState,
9595
RequestInternalDereferencedToolState,
96+
seed_repeat_defaults,
9697
)
9798
from galaxy.tool_util.parser import (
9899
get_tool_source,
@@ -2164,6 +2165,14 @@ def expand_incoming_async(
21642165

21652166
set_dataset_matcher_factory(request_context, self)
21662167

2168+
# Seed <repeat default="N"> instances while omitted repeats are still visible, before
2169+
# expand_meta_parameters_async rewrites every omitted repeat to [] (which would mask the
2170+
# default from fill_static_defaults for a repeat whose default exceeds its min).
2171+
if self.parameters is not None:
2172+
seed_repeat_defaults(
2173+
tool_request_internal_state.input_state, ToolParameterBundleModel(parameters=self.parameters)
2174+
)
2175+
21672176
expanded_incomings: list[ToolStateJobInstanceExpansionT]
21682177
job_tool_states: list[ToolStateJobInstanceT]
21692178
collection_info: MatchingCollections | None

test/unit/tool_util/test_parameter_convert.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
RequestInternalDereferencedToolState,
2020
RequestInternalToolState,
2121
RequestToolState,
22+
seed_repeat_defaults,
2223
strictify,
2324
)
2425
from galaxy.tool_util.parser.util import parse_profile_version
@@ -452,6 +453,36 @@ def _strict_async_decode_and_dereference(
452453
return dereference(request_internal_state, bundle, _fake_dereference, _fake_collection_deference)
453454

454455

456+
def test_seed_repeat_defaults():
457+
# <repeat min="0" default="2">: the async execution path materializes an omitted repeat to []
458+
# before fill_static_defaults runs, so the default must be seeded earlier - while the omission
459+
# is still visible. Regression for the bigscape async failure (default="1" with no min => an
460+
# empty --cutoffs argument => argparse exit 2).
461+
bundle = input_models_for_tool_source(tool_source_for("parameters/gx_repeat_boolean_default"))
462+
463+
# Omitted -> seeded with `default` instances.
464+
omitted: dict[str, Any] = {}
465+
seed_repeat_defaults(omitted, bundle)
466+
assert omitted["parameter"] == [{}, {}]
467+
468+
# Explicit [] still means zero (the omitted-vs-empty distinction is preserved).
469+
explicit_empty: dict[str, Any] = {"parameter": []}
470+
seed_repeat_defaults(explicit_empty, bundle)
471+
assert explicit_empty["parameter"] == []
472+
473+
# Explicit instances are left untouched.
474+
explicit_one: dict[str, Any] = {"parameter": [{"boolean_parameter": True}]}
475+
seed_repeat_defaults(explicit_one, bundle)
476+
assert explicit_one["parameter"] == [{"boolean_parameter": True}]
477+
478+
# Seeded instances then get their params filled by fill_static_defaults.
479+
seeded = {}
480+
seed_repeat_defaults(seeded, bundle)
481+
profile = parse_profile_version(tool_source_for("parameters/gx_repeat_boolean_default"))
482+
filled = fill_static_defaults(seeded, bundle, profile, partial=True)
483+
assert filled["parameter"] == [{"boolean_parameter": False}, {"boolean_parameter": False}]
484+
485+
455486
def fill_state_for(tool_state: dict[str, Any], tool_path: str, partial: bool = False) -> dict[str, Any]:
456487
tool_source = tool_source_for(tool_path)
457488
bundle = input_models_for_tool_source(tool_source)

0 commit comments

Comments
 (0)