Merge branch 'release_24.2' into dev

This commit is contained in:
Nicola Soranzo
2025-01-07 22:27:15 +00:00
17 changed files with 184 additions and 81 deletions
@@ -70,8 +70,8 @@ function executeActivity(activity: Activity) {
</script>
<template>
<div class="activity-settings rounded no-highlight">
<div v-if="foundActivities" class="activity-settings-content">
<div>
<div v-if="foundActivities">
<button
v-for="activity in filteredActivities"
:key="activity.id"
@@ -122,7 +122,7 @@ function executeActivity(activity: Activity) {
</div>
</button>
</div>
<div v-else class="activity-settings-content">
<div v-else>
<b-alert v-localize class="py-1 px-2" show> No matching activities found. </b-alert>
</div>
</div>
@@ -131,16 +131,6 @@ function executeActivity(activity: Activity) {
<style lang="scss">
@import "theme/blue.scss";
.activity-settings {
overflow-y: hidden;
display: flex;
flex-direction: column;
}
.activity-settings-content {
overflow-y: auto;
}
.activity-settings-item {
background: none;
border: none;
@@ -148,6 +138,7 @@ function executeActivity(activity: Activity) {
transition: none;
width: 100%;
}
.activity-settings-item:hover {
background: $gray-200;
}
@@ -104,11 +104,11 @@ onMounted(() => {
<h3>Create Visualization</h3>
<DelayedInput :delay="100" placeholder="Search visualizations" @change="query = $event" />
</template>
<div class="overflow-y mt-2">
<div>
<LoadingSpan v-if="isLoading" message="Loading visualizations" />
<div v-else-if="filteredPlugins.length > 0">
<div v-for="plugin in filteredPlugins" :key="plugin.name">
<button :data-plugin-name="plugin.name" @click="selectVisualization(plugin)">
<button class="plugin-item" :data-plugin-name="plugin.name" @click="selectVisualization(plugin)">
<div class="d-flex">
<div class="plugin-thumbnail mr-2">
<img v-if="plugin.logo" alt="visualization" :src="absPath(plugin.logo)" />
@@ -143,6 +143,20 @@ onMounted(() => {
</template>
<style lang="scss">
@import "theme/blue.scss";
.plugin-item {
background: none;
border: none;
text-align: left;
transition: none;
width: 100%;
}
.plugin-item:hover {
background: $gray-200;
}
.plugin-thumbnail {
img {
width: 2rem;
+6 -5
View File
@@ -8103,7 +8103,7 @@ class WorkflowStep(Base, RepresentById, UsesCreateAndUpdateTime):
tool_errors: Mapped[Optional[bytes]] = mapped_column(JSONType)
position: Mapped[Optional[bytes]] = mapped_column(MutableJSONType)
config: Mapped[Optional[bytes]] = mapped_column(JSONType)
order_index: Mapped[Optional[int]]
order_index: Mapped[int]
when_expression: Mapped[Optional[bytes]] = mapped_column(JSONType)
uuid: Mapped[Optional[Union[UUID, str]]] = mapped_column(UUIDType)
label: Mapped[Optional[str]] = mapped_column(Unicode(255))
@@ -8201,17 +8201,18 @@ class WorkflowStep(Base, RepresentById, UsesCreateAndUpdateTime):
# Ensure input_connections has already been set.
# Make connection information available on each step by input name.
inputs_by_name = {}
inputs_by_name: Dict[str, Any] = {}
for step_input in self.inputs:
input_name = step_input.name
assert input_name not in inputs_by_name
inputs_by_name[input_name] = step_input
self._inputs_by_name = inputs_by_name
return inputs_by_name
@property
def inputs_by_name(self):
if self._inputs_by_name is None:
self.setup_inputs_by_name()
return self.setup_inputs_by_name()
return self._inputs_by_name
def get_input(self, input_name):
@@ -8690,7 +8691,7 @@ class WorkflowInvocation(Base, UsesCreateAndUpdateTime, Dictifiable, Serializabl
state: Mapped[Optional[str]] = mapped_column(TrimmedString(64), index=True)
scheduler: Mapped[Optional[str]] = mapped_column(TrimmedString(255), index=True)
handler: Mapped[Optional[str]] = mapped_column(TrimmedString(255), index=True)
uuid: Mapped[Optional[Union[UUID, str]]] = mapped_column(UUIDType())
uuid: Mapped[Optional[Union[UUID]]] = mapped_column(UUIDType())
history_id: Mapped[Optional[int]] = mapped_column(ForeignKey("history.id"), index=True)
history = relationship("History", back_populates="workflow_invocations")
@@ -9415,7 +9416,7 @@ class WorkflowInvocationStep(Base, Dictifiable, Serializable):
)
action: Mapped[Optional[bytes]] = mapped_column(MutableJSONType)
workflow_step = relationship("WorkflowStep")
workflow_step: Mapped[WorkflowStep] = relationship("WorkflowStep")
job: Mapped[Optional["Job"]] = relationship(back_populates="workflow_invocation_step", uselist=False)
implicit_collection_jobs = relationship("ImplicitCollectionJobs", uselist=False)
output_dataset_collections = relationship(
+2
View File
@@ -358,6 +358,7 @@ class WorkflowRunCrateProfileBuilder:
def _add_step_parameter_fp(self, step: WorkflowInvocationStep, crate: ROCrate):
param_id = step.workflow_step.label
assert step.workflow_step.tool_inputs
param_type = step.workflow_step.tool_inputs["parameter_type"]
return crate.add(
ContextEntity(
@@ -375,6 +376,7 @@ class WorkflowRunCrateProfileBuilder:
def _add_step_tool_pv(self, step: WorkflowInvocationStep, tool_input: str, crate: ROCrate):
param_id = tool_input
assert step.workflow_step.tool_inputs
return crate.add(
ContextEntity(
crate,
+1 -1
View File
@@ -48,7 +48,7 @@ IS_OS_X = sys.platform == "darwin"
VERSIONED_ENV_DIR_NAME = re.compile(r"__(.*)@(.*)")
UNVERSIONED_ENV_DIR_NAME = re.compile(r"__(.*)@_uv_")
USE_PATH_EXEC_DEFAULT = False
CONDA_PACKAGE_SPECS = ("conda>=23.7.0", "conda-libmamba-solver", "'pyopenssl>=22.1.0'")
CONDA_PACKAGE_SPECS = ("conda>=23.7.0", "conda-libmamba-solver", "pyopenssl>=22.1.0")
CONDA_BUILD_SPECS = ("conda-build>=3.22.0",)
USE_LOCAL_DEFAULT = False
+25 -2
View File
@@ -31,6 +31,7 @@ from .grouping import (
)
from .workflow_utils import (
is_runtime_value,
NO_REPLACEMENT,
runtime_to_json,
)
from .wrapped import flat_to_nested_state
@@ -174,14 +175,36 @@ def visit_input_values(
if input.name not in input_values:
args["error"] = f"No value found for '{args.get('prefixed_label')}'."
new_value = callback(**args)
# is this good enough ? feels very ugh
if new_value == [no_replacement_value]:
# Single unspecified value in multiple="true" input with a single null input, pretend it's a singular value
new_value = no_replacement_value
if isinstance(new_value, list):
# Maybe mixed input, I guess tool defaults don't really make sense here ?
# Would e.g. be default dataset in multiple="true" input, you wouldn't expect the default to be inserted
# if other inputs are connected and provided.
new_value = [item if not item == no_replacement_value else None for item in new_value]
if no_replacement_value is REPLACE_ON_TRUTHY:
replace = bool(new_value)
else:
replace = new_value != no_replacement_value
if replace:
input_values[input.name] = new_value
elif replace_optional_connections and is_runtime_value(value) and hasattr(input, "value"):
input_values[input.name] = input.value
elif replace_optional_connections:
# Only used in workflow context
has_default = hasattr(input, "value")
if new_value is value is NO_REPLACEMENT or is_runtime_value(value):
# NO_REPLACEMENT means value was connected but left unspecified
if has_default:
# Use default if we have one
input_values[input.name] = input.value
else:
# Should fail if input is not optional and does not have default value
# Effectively however depends on parameter implementation.
# We might want to raise an exception here, instead of depending on a tool parameter value error.
input_values[input.name] = None
def get_current_case(input, input_values):
test_parameter = input.test_param
+11 -3
View File
@@ -49,7 +49,10 @@ from galaxy.tool_util.parser.util import (
ParameterParseException,
text_input_is_optional,
)
from galaxy.tools.parameters.workflow_utils import workflow_building_modes
from galaxy.tools.parameters.workflow_utils import (
NO_REPLACEMENT,
workflow_building_modes,
)
from galaxy.util import (
sanitize_param,
string_as_bool,
@@ -247,6 +250,8 @@ class ToolParameter(UsesDictVisibleKeys):
def value_to_basic(self, value, app, use_security=False):
if is_runtime_value(value):
return runtime_to_json(value)
elif value == NO_REPLACEMENT:
return {"__class__": "NoReplacement"}
return self.to_json(value, app, use_security)
def value_from_basic(self, value, app, ignore_errors=False):
@@ -255,8 +260,11 @@ class ToolParameter(UsesDictVisibleKeys):
if isinstance(self, HiddenToolParameter):
raise ParameterValueError(message_suffix="Runtime Parameter not valid", parameter_name=self.name)
return runtime_to_object(value)
elif isinstance(value, MutableMapping) and value.get("__class__") == "UnvalidatedValue":
return value["value"]
elif isinstance(value, MutableMapping):
if value.get("__class__") == "UnvalidatedValue":
return value["value"]
elif value.get("__class__") == "NoReplacement":
return NO_REPLACEMENT
# Delegate to the 'to_python' method
try:
return self.to_python(value, app)
@@ -1,6 +1,15 @@
from collections.abc import MutableMapping
class NoReplacement:
def __str__(self):
return "NO_REPLACEMENT singleton"
NO_REPLACEMENT = NoReplacement()
class workflow_building_modes:
DISABLED = False
ENABLED = True
+31 -25
View File
@@ -95,6 +95,8 @@ from galaxy.tools.parameters.populate_model import populate_model
from galaxy.tools.parameters.workflow_utils import (
ConnectedValue,
is_runtime_value,
NO_REPLACEMENT,
NoReplacement,
runtime_to_json,
workflow_building_modes,
)
@@ -129,14 +131,6 @@ RUNTIME_POST_JOB_ACTIONS_KEY = "__POST_JOB_ACTIONS__"
POSSIBLE_PARAMETER_TYPES: Tuple[INPUT_PARAMETER_TYPES] = get_args(INPUT_PARAMETER_TYPES)
class NoReplacement:
def __str__(self):
return "NO_REPLACEMENT singleton"
NO_REPLACEMENT = NoReplacement()
class ConditionalStepWhen(BooleanToolParameter):
pass
@@ -416,14 +410,11 @@ class WorkflowModule:
"""
return {}
def compute_runtime_state(self, trans, step=None, step_updates=None):
def compute_runtime_state(self, trans, step=None, step_updates=None, replace_default_values=False):
"""Determine the runtime state (potentially different from self.state
which describes configuration state). This (again unlike self.state) is
currently always a `DefaultToolState` object.
If `step` is not `None`, it will be used to search for default values
defined in workflow input steps.
If `step_updates` is `None`, this is likely for rendering the run form
for instance and no runtime properties are available and state must be
solely determined by the default runtime state described by the step.
@@ -432,6 +423,8 @@ class WorkflowModule:
supplied by the workflow runner.
"""
state = self.get_runtime_state()
if replace_default_values and step:
state.inputs = step.state.inputs
step_errors = {}
if step is not None:
@@ -441,8 +434,11 @@ class WorkflowModule:
if step_input is None:
return NO_REPLACEMENT
if step_input.default_value_set:
return step_input.default_value
if replace_default_values and step_input.default_value_set:
input_value = step_input.default_value
if isinstance(input, BaseDataToolParameter):
input_value = raw_to_galaxy(trans.app, trans.history, input_value)
return input_value
return NO_REPLACEMENT
@@ -469,7 +465,7 @@ class WorkflowModule:
return state, step_errors
def encode_runtime_state(self, step, runtime_state):
def encode_runtime_state(self, step, runtime_state: DefaultToolState):
"""Takes the computed runtime state and serializes it during run request creation."""
return runtime_state.encode(Bunch(inputs=self.get_runtime_inputs(step)), self.trans.app)
@@ -954,7 +950,7 @@ class InputModule(WorkflowModule):
def get_runtime_state(self):
state = DefaultToolState()
state.inputs = dict(input=None)
state.inputs = dict(input=NO_REPLACEMENT)
return state
def get_all_inputs(self, data_only=False, connectable_only=False):
@@ -966,7 +962,7 @@ class InputModule(WorkflowModule):
invocation = invocation_step.workflow_invocation
step = invocation_step.workflow_step
input_value = step.state.inputs["input"]
if input_value is None:
if input_value is NO_REPLACEMENT:
default_value = step.get_input_default_value(NO_REPLACEMENT)
if default_value is not NO_REPLACEMENT:
input_value = raw_to_galaxy(trans.app, trans.history, default_value)
@@ -993,7 +989,7 @@ class InputModule(WorkflowModule):
# everything should come in from the API and this can be eliminated.
if not invocation.has_input_for_step(step.id):
content = next(iter(step_outputs.values()))
if content:
if content and content is not NO_REPLACEMENT:
invocation.add_input(content, step.id)
progress.set_outputs_for_input(invocation_step, step_outputs)
return None
@@ -1582,7 +1578,7 @@ class InputParameterModule(WorkflowModule):
def get_runtime_state(self):
state = DefaultToolState()
state.inputs = dict(input=None)
state.inputs = dict(input=NO_REPLACEMENT)
return state
def get_all_outputs(self, data_only=False):
@@ -1609,7 +1605,7 @@ class InputParameterModule(WorkflowModule):
input_value = progress.inputs_by_step_id[step.id]
else:
input_value = step.state.inputs["input"]
if input_value is None:
if input_value is NO_REPLACEMENT:
default_value = step.get_input_default_value(NO_REPLACEMENT)
# TODO: look at parameter type and infer if value should be a dictionary
# instead. Guessing only field parameter types in CWL branch would have
@@ -2233,13 +2229,14 @@ class ToolModule(WorkflowModule):
def get_runtime_inputs(self, step, connections: Optional[Iterable[WorkflowStepConnection]] = None):
return self.get_inputs()
def compute_runtime_state(self, trans, step=None, step_updates=None):
def compute_runtime_state(self, trans, step=None, step_updates=None, replace_default_values=False):
# Warning: This method destructively modifies existing step state.
if self.tool:
step_errors = {}
state = self.state
self.runtime_post_job_actions = {}
state, step_errors = super().compute_runtime_state(trans, step, step_updates)
state, step_errors = super().compute_runtime_state(
trans, step, step_updates, replace_default_values=replace_default_values
)
if step_updates:
self.runtime_post_job_actions = step_updates.get(RUNTIME_POST_JOB_ACTIONS_KEY, {})
step_metadata_runtime_state = self.__step_meta_runtime_state()
@@ -2266,7 +2263,11 @@ class ToolModule(WorkflowModule):
)
def execute(
self, trans, progress: "WorkflowProgress", invocation_step, use_cached_job: bool = False
self,
trans,
progress: "WorkflowProgress",
invocation_step: "WorkflowInvocationStep",
use_cached_job: bool = False,
) -> Optional[bool]:
invocation = invocation_step.workflow_invocation
step = invocation_step.workflow_step
@@ -2275,7 +2276,12 @@ class ToolModule(WorkflowModule):
# TODO: why do we even create an invocation, seems like something we could check on submit?
message = f"Specified tool [{tool.id}] in step {step.order_index + 1} is not workflow-compatible."
raise exceptions.MessageException(message)
self.state, _ = self.compute_runtime_state(
trans, step, step_updates=progress.param_map.get(step.id), replace_default_values=True
)
step.state = self.state
tool_state = step.state
assert tool_state is not None
tool_inputs = tool.inputs.copy()
# Not strictly needed - but keep Tool state clean by stripping runtime
# metadata parameters from it.
@@ -2404,7 +2410,7 @@ class ToolModule(WorkflowModule):
mapping_params=mapping_params,
history=invocation.history,
collection_info=collection_info,
workflow_invocation_uuid=invocation.uuid.hex,
workflow_invocation_uuid=invocation.uuid.hex if invocation.uuid else None,
invocation_step=invocation_step,
max_num_jobs=max_num_jobs,
validate_outputs=validate_outputs,
+2 -4
View File
@@ -9,6 +9,7 @@ from galaxy.tools.parameters import visit_input_values
from galaxy.tools.parameters.basic import contains_workflow_parameter
from galaxy.tools.parameters.workflow_utils import (
ConnectedValue,
NO_REPLACEMENT,
runtime_to_json,
)
from .schema import (
@@ -41,10 +42,7 @@ from .schema import (
UpgradeSubworkflowAction,
UpgradeToolAction,
)
from ..modules import (
InputParameterModule,
NO_REPLACEMENT,
)
from ..modules import InputParameterModule
log = logging.getLogger(__name__)
+26 -22
View File
@@ -1,5 +1,6 @@
import logging
import uuid
from collections.abc import MutableMapping
from typing import (
Any,
Dict,
@@ -37,6 +38,11 @@ from galaxy.schema.invocation import (
WarningReason,
)
from galaxy.tools.parameters.basic import raw_to_galaxy
from galaxy.tools.parameters.workflow_utils import (
is_runtime_value,
NO_REPLACEMENT,
NoReplacement,
)
from galaxy.tools.parameters.wrapped import nested_key_to_path
from galaxy.util import ExecutionTimer
from galaxy.workflow import modules
@@ -334,6 +340,7 @@ class WorkflowInvoker:
)
def _invoke_step(self, invocation_step: WorkflowInvocationStep) -> Optional[bool]:
assert invocation_step.workflow_step.module
incomplete_or_none = invocation_step.workflow_step.module.execute(
self.trans,
self.progress,
@@ -432,11 +439,11 @@ class WorkflowProgress:
def replacement_for_input(self, trans, step: "WorkflowStep", input_dict: Dict[str, Any]):
replacement: Union[
modules.NoReplacement,
NoReplacement,
model.DatasetCollectionInstance,
List[model.DatasetCollectionInstance],
HistoryItem,
] = modules.NO_REPLACEMENT
] = NO_REPLACEMENT
prefixed_name = input_dict["name"]
multiple = input_dict["multiple"]
is_data = input_dict["input_type"] in ["dataset", "dataset_collection"]
@@ -456,15 +463,18 @@ class WorkflowProgress:
replacement = temp
else:
replacement = self.replacement_for_connection(connection[0], is_data=is_data)
elif step.state and (state_input := get_path(step.state.inputs, nested_key_to_path(prefixed_name), None)):
elif (
step.state
and (state_input := get_path(step.state.inputs, nested_key_to_path(prefixed_name), None))
and not is_runtime_value(state_input)
):
# workflow submitted with step parameters populates state directly
# via populate_module_and_state
replacement = state_input
else:
for step_input in step.inputs:
if step_input.name == prefixed_name and step_input.default_value_set:
if is_data:
replacement = raw_to_galaxy(trans.app, trans.history, step_input.default_value)
elif (step_input := step.inputs_by_name.get(prefixed_name)) and step_input.default_value_set:
replacement = step_input.default_value
if is_data:
replacement = raw_to_galaxy(trans.app, trans.history, step_input.default_value)
return replacement
def replacement_for_connection(self, connection: "WorkflowStepConnection", is_data: bool = True):
@@ -494,6 +504,8 @@ class WorkflowProgress:
dependent_workflow_step_id=output_step_id,
)
)
if isinstance(replacement, MutableMapping) and replacement.get("__class__") == "NoReplacement":
return NO_REPLACEMENT
if isinstance(replacement, model.HistoryDatasetCollectionAssociation):
if not replacement.collection.populated:
if not replacement.waiting_for_elements:
@@ -574,19 +586,8 @@ class WorkflowProgress:
if self.inputs_by_step_id:
step_id = step.id
if step_id not in self.inputs_by_step_id and "output" not in outputs:
default_value = step.get_input_default_value(modules.NO_REPLACEMENT)
if default_value is not modules.NO_REPLACEMENT:
outputs["output"] = default_value
else:
log.error(f"{step.log_str()} not found in inputs_step_id {self.inputs_by_step_id}")
raise modules.FailWorkflowEvaluation(
why=InvocationFailureOutputNotFound(
reason=FailureReason.output_not_found,
workflow_step_id=invocation_step.workflow_step_id,
output_name="output",
dependent_workflow_step_id=invocation_step.workflow_step_id,
)
)
default_value = step.get_input_default_value(NO_REPLACEMENT)
outputs["output"] = default_value
elif step_id in self.inputs_by_step_id:
if self.inputs_by_step_id[step_id] is not None or "output" not in outputs:
outputs["output"] = self.inputs_by_step_id[step_id]
@@ -620,7 +621,7 @@ class WorkflowProgress:
# Add this non-data, non workflow-output output to the workflow outputs.
# This is required for recovering the output in the next scheduling iteration,
# and should be replaced with a WorkflowInvocationStepOutputValue ASAP.
if not workflow_outputs_by_name.get(output_name) and not output_object == modules.NO_REPLACEMENT:
if not workflow_outputs_by_name.get(output_name) and output_object is not NO_REPLACEMENT:
workflow_output = model.WorkflowOutput(step, output_name=output_name)
step.workflow_outputs.append(workflow_output)
for workflow_output in step.workflow_outputs:
@@ -645,6 +646,8 @@ class WorkflowProgress:
)
def _record_workflow_output(self, step: "WorkflowStep", workflow_output: "WorkflowOutput", output: Any) -> None:
if output is NO_REPLACEMENT:
output = {"__class__": "NoReplacement"}
self.workflow_invocation.add_output(workflow_output, step, output)
def mark_step_outputs_delayed(self, step: "WorkflowStep", why: Optional[str] = None) -> None:
@@ -737,6 +740,7 @@ class WorkflowProgress:
return raw_to_galaxy(self.module_injector.trans.app, self.module_injector.trans.history, value)
def _recover_mapping(self, step_invocation: WorkflowInvocationStep) -> None:
assert step_invocation.workflow_step.module
try:
step_invocation.workflow_step.module.recover_mapping(step_invocation, self)
except modules.DelayedWorkflowEvaluation as de:
+3 -1
View File
@@ -30,6 +30,7 @@ from galaxy.model.base import (
from galaxy.tool_util.parameters import DataRequestUri
from galaxy.tools.parameters.basic import ParameterValueError
from galaxy.tools.parameters.meta import expand_workflow_inputs
from galaxy.tools.parameters.workflow_utils import NO_REPLACEMENT
from galaxy.workflow.modules import WorkflowModuleInjector
from galaxy.workflow.resources import get_resource_mapper_function
@@ -599,7 +600,8 @@ def workflow_run_config_to_request(
type=param_types.REPLACEMENT_PARAMETERS,
)
for step_id, content in run_config.inputs.items():
workflow_invocation.add_input(content, step_id)
if content is not NO_REPLACEMENT:
workflow_invocation.add_input(content, step_id)
for step_id, param_dict in run_config.param_map.items():
add_parameter(
name=str(step_id),
@@ -8,7 +8,8 @@
- that: has_text
text: "1"
- doc: |
Test that null is replaced with default value (follows https://www.commonwl.org/v1.2/Workflow.html#WorkflowInputParameter)
Test that explicit null is not replaced and fails
expect_failure: true
job:
required_int_with_default:
type: raw
@@ -8,7 +8,8 @@
- that: has_text
text: "1"
- doc: |
Test that null is replaced with default value (follows https://www.commonwl.org/v1.2/Workflow.html#WorkflowInputParameter)
Test that explicit null is not replaced and fails
expect_failure: true
job:
optional_int_with_default:
type: raw
@@ -0,0 +1,12 @@
- doc: |
Test that scheduling succeeds even if step needs rescheduling
job:
optional_text:
type: raw
value: null
outputs:
out:
class: File
asserts:
- that: has_text
text: "null"
@@ -0,0 +1,30 @@
class: GalaxyWorkflow
inputs:
optional_text:
type: text
optional: true
when:
type: boolean
default: true
outputs:
out:
outputSource: gx_text_optional/inputs_json
steps:
pick_value:
tool_id: pick_value
tool_state:
style_cond:
pick_style: first
type_cond:
param_type: boolean
pick_from:
- value: true
gx_text_optional:
tool_id: gx_text_optional
# the when expression requires pick_value to execute, which recapitulates https://github.com/galaxyproject/galaxy/issues/19328
when: $(inputs.when)
in:
parameter:
source: optional_text
when:
source: pick_value/boolean_param
+2 -1
View File
@@ -14,6 +14,7 @@ import pytest
from galaxy import model
from galaxy.managers.workflows import WorkflowContentsManager
from galaxy.tools.parameters.workflow_utils import NO_REPLACEMENT
from galaxy.util import bunch
from galaxy.workflow import modules
from .workflow_support import (
@@ -57,7 +58,7 @@ def test_data_input_compute_runtime_state_default():
state, errors = module.compute_runtime_state(module.trans, module.test_step)
assert not errors
assert "input" in state.inputs
assert state.inputs["input"] is None
assert state.inputs["input"] is NO_REPLACEMENT
def test_data_input_compute_runtime_state_args():