Skip to content

Commit bcdeba1

Browse files
authored
feat(cli): allow disabling automated parameter detection in renku run (#3548)
1 parent 020434a commit bcdeba1

File tree

6 files changed

+155
-53
lines changed

6 files changed

+155
-53
lines changed

renku/core/workflow/plan_factory.py

Lines changed: 72 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ def __init__(
7171
working_dir: Optional[Union[Path, str]] = None,
7272
no_input_detection: bool = False,
7373
no_output_detection: bool = False,
74+
no_parameter_detection: bool = False,
7475
success_codes: Optional[List[int]] = None,
7576
stdin: Optional[str] = None,
7677
stdout: Optional[str] = None,
@@ -80,6 +81,7 @@ def __init__(
8081

8182
self.no_input_detection = no_input_detection
8283
self.no_output_detection = no_output_detection
84+
self.no_parameter_detection = no_parameter_detection
8385

8486
if not command_line:
8587
raise errors.UsageError("Command line can not be empty. Please specify a command to execute.")
@@ -392,23 +394,34 @@ def add_command_input(
392394

393395
mapped_stream = self.get_stream_mapping_for_value(default_value)
394396

395-
self.inputs.append(
396-
CommandInput(
397-
id=CommandInput.generate_id(
398-
plan_id=self.plan_id,
399-
position=position,
400-
postfix=mapped_stream.stream_type if mapped_stream else postfix,
401-
),
402-
default_value=default_value,
403-
prefix=prefix,
397+
inp_param = CommandInput(
398+
id=CommandInput.generate_id(
399+
plan_id=self.plan_id,
404400
position=position,
405-
mapped_to=mapped_stream,
406-
encoding_format=encoding_format,
407-
postfix=postfix,
408-
name=name,
409-
)
401+
postfix=mapped_stream.stream_type if mapped_stream else postfix,
402+
),
403+
default_value=default_value,
404+
prefix=prefix,
405+
position=position,
406+
mapped_to=mapped_stream,
407+
encoding_format=encoding_format,
408+
postfix=postfix,
409+
name=name,
410410
)
411411

412+
existing_parameter = next((p for p in self.inputs if p.name == inp_param.name), None)
413+
414+
if existing_parameter is not None and existing_parameter.default_value == inp_param.default_value:
415+
existing_parameter.update_from(inp_param)
416+
elif existing_parameter is not None:
417+
# duplicate with different values!
418+
raise errors.ParameterError(
419+
f"Duplicate input '{inp_param.name}' found with differing values ('{inp_param.default_value}'"
420+
f" vs. '{existing_parameter.default_value}')"
421+
)
422+
else:
423+
self.inputs.append(inp_param)
424+
412425
def add_command_output(
413426
self,
414427
default_value: Any,
@@ -447,20 +460,31 @@ def add_command_output(
447460
postfix=mapped_stream.stream_type if mapped_stream else postfix,
448461
)
449462

450-
self.outputs.append(
451-
CommandOutput(
452-
id=id,
453-
default_value=default_value,
454-
prefix=prefix,
455-
position=position,
456-
mapped_to=mapped_stream,
457-
encoding_format=encoding_format,
458-
postfix=postfix,
459-
create_folder=create_folder,
460-
name=name,
461-
)
463+
out_param = CommandOutput(
464+
id=id,
465+
default_value=default_value,
466+
prefix=prefix,
467+
position=position,
468+
mapped_to=mapped_stream,
469+
encoding_format=encoding_format,
470+
postfix=postfix,
471+
create_folder=create_folder,
472+
name=name,
462473
)
463474

475+
existing_parameter = next((p for p in self.outputs if p.name == out_param.name), None)
476+
477+
if existing_parameter is not None and existing_parameter.default_value == out_param.default_value:
478+
existing_parameter.update_from(out_param)
479+
elif existing_parameter is not None:
480+
# duplicate with different values!
481+
raise errors.ParameterError(
482+
f"Duplicate output '{out_param.name}' found with differing values ('{out_param.default_value}'"
483+
f" vs. '{existing_parameter.default_value}')"
484+
)
485+
else:
486+
self.outputs.append(out_param)
487+
464488
def add_command_output_from_input(self, input: CommandInput, name):
465489
"""Create a CommandOutput from an input."""
466490
self.inputs.remove(input)
@@ -496,16 +520,30 @@ def add_command_parameter(
496520
name: Optional[str] = None,
497521
):
498522
"""Create a CommandParameter."""
499-
self.parameters.append(
500-
CommandParameter(
501-
id=CommandParameter.generate_id(plan_id=self.plan_id, position=position),
502-
default_value=default_value,
503-
prefix=prefix,
504-
position=position,
505-
name=name,
506-
)
523+
if self.no_parameter_detection and all(default_value != v for v, _ in self.explicit_parameters):
524+
return
525+
526+
parameter = CommandParameter(
527+
id=CommandParameter.generate_id(plan_id=self.plan_id, position=position),
528+
default_value=default_value,
529+
prefix=prefix,
530+
position=position,
531+
name=name,
507532
)
508533

534+
existing_parameter = next((p for p in self.parameters if p.name == parameter.name), None)
535+
536+
if existing_parameter is not None and existing_parameter.default_value == parameter.default_value:
537+
existing_parameter.update_from(parameter)
538+
elif existing_parameter is not None:
539+
# duplicate with different values!
540+
raise errors.ParameterError(
541+
f"Duplicate parameter '{parameter.name}' found with differing values ('{parameter.default_value}'"
542+
f" vs. '{existing_parameter.default_value}')"
543+
)
544+
else:
545+
self.parameters.append(parameter)
546+
509547
def add_explicit_inputs(self):
510548
"""Add explicit inputs ."""
511549
input_paths = [input.default_value for input in self.inputs]

renku/core/workflow/run.py

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -167,18 +167,19 @@ def get_valid_parameter_name(name: str) -> str:
167167
@inject.autoparams("activity_gateway", "plan_gateway")
168168
@validate_arguments(config=dict(arbitrary_types_allowed=True))
169169
def run_command_line(
170-
name,
171-
description,
172-
keyword,
173-
explicit_inputs,
174-
explicit_outputs,
175-
explicit_parameters,
176-
no_output,
177-
no_input_detection,
178-
no_output_detection,
179-
success_codes,
180-
command_line,
181-
creators,
170+
name: Optional[str],
171+
description: Optional[str],
172+
keyword: Optional[List[str]],
173+
explicit_inputs: List[str],
174+
explicit_outputs: List[str],
175+
explicit_parameters: List[str],
176+
no_output: bool,
177+
no_input_detection: bool,
178+
no_output_detection: bool,
179+
no_parameter_detection: bool,
180+
success_codes: List[int],
181+
command_line: List[str],
182+
creators: Optional[List[Person]],
182183
activity_gateway: IActivityGateway,
183184
plan_gateway: IPlanGateway,
184185
) -> PlanViewModel:
@@ -262,19 +263,20 @@ def parse_explicit_definition(entries, type):
262263

263264
return result
264265

265-
explicit_inputs = parse_explicit_definition(explicit_inputs, "input")
266-
explicit_outputs = parse_explicit_definition(explicit_outputs, "output")
267-
explicit_parameters = parse_explicit_definition(explicit_parameters, "param")
266+
explicit_inputs_parsed = parse_explicit_definition(explicit_inputs, "input")
267+
explicit_outputs_parsed = parse_explicit_definition(explicit_outputs, "output")
268+
explicit_parameters_parsed = parse_explicit_definition(explicit_parameters, "param")
268269

269270
factory = PlanFactory(
270271
command_line=command_line,
271-
explicit_inputs=explicit_inputs,
272-
explicit_outputs=explicit_outputs,
273-
explicit_parameters=explicit_parameters,
272+
explicit_inputs=explicit_inputs_parsed,
273+
explicit_outputs=explicit_outputs_parsed,
274+
explicit_parameters=explicit_parameters_parsed,
274275
directory=os.getcwd(),
275276
working_dir=working_dir,
276277
no_input_detection=no_input_detection,
277278
no_output_detection=no_output_detection,
279+
no_parameter_detection=no_parameter_detection,
278280
success_codes=success_codes,
279281
**{name: os.path.relpath(path, working_dir) for name, path in mapped_std.items()},
280282
)

renku/domain_model/workflow/parameter.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,17 @@ def to_argv(self, quote_string: bool = True) -> List[Any]:
159159

160160
return [str(value)]
161161

162+
def _update_from(self, other: "CommandParameterBase"):
163+
"""Update this parameter with values from another parameter, if applicable."""
164+
if other.prefix is not None:
165+
self.prefix = other.prefix
166+
if other.position is not None:
167+
self.position = other.position
168+
if other.description:
169+
self.description = other.description
170+
if other.name_set_by_user:
171+
self.name_set_by_user = other.name_set_by_user
172+
162173
@property
163174
def actual_value(self):
164175
"""Get the actual value to be used for execution."""
@@ -242,6 +253,10 @@ def derive(self, plan_id: str) -> "CommandParameter":
242253
parameter.id = CommandParameter.generate_id(plan_id=plan_id, position=self.position, postfix=self.postfix)
243254
return parameter
244255

256+
def update_from(self, other: "CommandParameter"):
257+
"""Update this output with values from another output, if applicable."""
258+
super()._update_from(other)
259+
245260

246261
class CommandInput(CommandParameterBase):
247262
"""An input to a command."""
@@ -317,6 +332,16 @@ def derive(self, plan_id: str) -> "CommandInput":
317332
parameter.id = CommandInput.generate_id(plan_id=plan_id, position=self.position, postfix=self.postfix)
318333
return parameter
319334

335+
def update_from(self, other: "CommandInput"):
336+
"""Update this input with values from another input, if applicable."""
337+
super()._update_from(other)
338+
339+
if other.encoding_format:
340+
self.encoding_format = other.encoding_format
341+
342+
if other.mapped_to is not None:
343+
self.mapped_to = other.mapped_to
344+
320345

321346
class HiddenInput(CommandInput):
322347
"""An input to a command that is added by Renku and should be hidden from users."""
@@ -391,6 +416,19 @@ def is_equal_to(self, other) -> bool:
391416

392417
return super().is_equal_to(other)
393418

419+
def update_from(self, other: "CommandOutput"):
420+
"""Update this output with values from another output, if applicable."""
421+
super()._update_from(other)
422+
423+
if other.encoding_format:
424+
self.encoding_format = other.encoding_format
425+
426+
if other.mapped_to is not None:
427+
self.mapped_to = other.mapped_to
428+
429+
if other.create_folder:
430+
self.create_folder = other.create_folder
431+
394432
@staticmethod
395433
def _get_equality_attributes() -> List[str]:
396434
"""Return a list of attributes values that determine if instances are equal."""

renku/ui/cli/run.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,15 @@
132132
This only affects files and directories; command options and flags are
133133
still treated as inputs.
134134
135+
.. topic:: Disabling parameter detection (``--no-parameter-detection``)
136+
137+
Inputs that aren't files or directories are automatically detected as
138+
parameters in ``renku run``. You can disable this feature by passing the
139+
``--no-parameter-detection`` flag, which completely ignores them on the
140+
workflow. You can still manually specify parameters using ``--param``
141+
arguments mentioned above or using the ``renku.api.Parameter`` class in
142+
Python code.
143+
135144
.. note:: ``renku run`` prints the generated plan after execution if you pass
136145
``--verbose`` to it. You can check the generated plan to verify that the
137146
execution was done as you intended. The plan will always be printed to
@@ -498,6 +507,7 @@
498507
@click.option("--no-output", is_flag=True, default=False, help="Allow command without output files.")
499508
@click.option("--no-input-detection", is_flag=True, default=False, help="Disable auto-detection of inputs.")
500509
@click.option("--no-output-detection", is_flag=True, default=False, help="Disable auto-detection of outputs.")
510+
@click.option("--no-parameter-detection", is_flag=True, default=False, help="Disable auto-detection of parameters.")
501511
@click.option(
502512
"--success-code",
503513
"success_codes",
@@ -537,6 +547,7 @@ def run(
537547
no_output,
538548
no_input_detection,
539549
no_output_detection,
550+
no_parameter_detection,
540551
success_codes,
541552
isolation,
542553
file,
@@ -587,6 +598,7 @@ def is_workflow_file() -> bool:
587598
or no_output
588599
or no_input_detection
589600
or no_output_detection
601+
or no_parameter_detection
590602
or success_codes
591603
or isolation
592604
or creators
@@ -653,6 +665,7 @@ def is_workflow_file() -> bool:
653665
no_output=no_output,
654666
no_input_detection=no_input_detection,
655667
no_output_detection=no_output_detection,
668+
no_parameter_detection=no_parameter_detection,
656669
success_codes=success_codes,
657670
command_line=command_line,
658671
creators=creators,

tests/api/test_parameter.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,8 @@ def test_parameters(project):
121121

122122
assert (42, "42", 42.42) == (p1.value, p2.value, p3.value)
123123

124+
_ = Parameter("parameter_3", 42.42)
125+
124126
data = read_indirect_parameters(project.path)
125127

126128
assert {"parameter-1", "param-2", "parameter_3"} == set(data.keys())

tests/cli/test_output_option.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,12 +279,21 @@ def test_no_output_and_disabled_detection(renku_cli):
279279
def test_disabled_detection(renku_cli):
280280
"""Test disabled auto-detection of inputs and outputs."""
281281
exit_code, activity = renku_cli(
282-
"run", "--no-input-detection", "--no-output-detection", "--output", "README.md", "touch", "some-files"
282+
"run",
283+
"--no-input-detection",
284+
"--no-output-detection",
285+
"--no-parameter-detection",
286+
"--output",
287+
"README.md",
288+
"touch",
289+
"some-files",
290+
"-f",
283291
)
284292

285293
assert 0 == exit_code
286294
plan = activity.association.plan
287295
assert 0 == len(plan.inputs)
296+
assert 0 == len(plan.parameters)
288297
assert 1 == len(plan.outputs)
289298
assert "README.md" == str(plan.outputs[0].default_value)
290299

0 commit comments

Comments
 (0)