Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix issues with with_param #1236

Merged
merged 14 commits into from
Oct 25, 2024

Conversation

alicederyn
Copy link
Collaborator

Pull Request Checklist

Description of PR
Currently, the logic implementing with_param assumes that Argo Parameters exactly match script function parameters, including the Python parameter name. This does not support the experimental script_annotations and script_pydantic_io features.

This PR extracts common logic for creating an IO object from parameter/field metadata into a construct_io_from_annotation function, which copies any IO annotation it finds, sets the name if missing, and defaults to a Parameter if no annotation is found. It then uses this to generalize _get_param_items_from_source, including:

  • pulling the parameters from a Pydantic Input type instead, if present
  • respecting the name in the Parameter annotation, if present
  • ignoring output=True annotations
  • ignoring Artifact annotations

Many places need to inspect a parameter or field to construct a
Parameter or Artifact based on the name and other annotations. Extract
the single-field logic from _construct_io_from_fields to simplify many
callsites that were using get_workflow_annotation and stitching in the
name and default Parameter themselves. This will also allow us to
reliably provide more features in future, such as inferring an enum
parameter for a Literal.

Signed-off-by: Alice Purcell <[email protected]>
Add three unit tests of the working current behaviour to prevent regression.

Signed-off-by: Alice Purcell <[email protected]>
Support Parameter/Artifact annotations in _get_param_items_from_source.
If a Parameter annotation is provided, the name will override the name
of the Python parameter; additionally, if it is an output parameter, or
if there is an Artifact annotation, it will be skipped.

Signed-off-by: Alice Purcell <[email protected]>
Support Pydantic Input subclasses in _get_param_items_from_source.

Signed-off-by: Alice Purcell <[email protected]>
Add examples reproducing issues argoproj-labs#861 (using with_param with an annotated
input) and argoproj-labs#1234 (using with_param with a Pydantic Input type).

Signed-off-by: Alice Purcell <[email protected]>
@alicederyn alicederyn added semver:patch A change requiring a patch version bump type:bug A general bug labels Oct 10, 2024
Copy link

codecov bot commented Oct 10, 2024

Codecov Report

Attention: Patch coverage is 11.76471% with 45 lines in your changes missing coverage. Please review.

Project coverage is 45.8%. Comparing base (12de907) to head (9b471d2).
Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
src/hera/workflows/_meta_mixins.py 8.0% 23 Missing ⚠️
src/hera/workflows/script.py 7.1% 13 Missing ⚠️
src/hera/shared/_type_util.py 16.6% 5 Missing ⚠️
src/hera/workflows/_mixins.py 25.0% 3 Missing ⚠️
src/hera/workflows/io/_io_mixins.py 50.0% 1 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##            main   #1236     +/-   ##
=======================================
- Coverage   45.9%   45.8%   -0.2%     
=======================================
  Files         60      60             
  Lines       4096    4110     +14     
  Branches     861     863      +2     
=======================================
+ Hits        1884    1886      +2     
- Misses      2180    2192     +12     
  Partials      32      32             

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@alicederyn
Copy link
Collaborator Author

It looks like the code coverage is broken, because it's complaining about lines not being tested that definitely are.

@alicederyn alicederyn marked this pull request as ready for review October 10, 2024 08:35
If a field has a Parameter or Artifact annotation, a copy will be returned, with name added if missing.
Otherwise, a Parameter object will be constructed.
If a field has a Parameter or Artifact annotation, a copy will be returned, with missing
fields filled out based on other metadata. Otherwise, a Parameter object will be constructed.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: I changed this docstring because the core logic is now in a different function, so it will be easy to miss changes in future and bitrot. I intend to change the behaviour as part of fixing #1173, for instance, to set the enum field for Literals.

Signed-off-by: Alice Purcell <[email protected]>
Copy link
Collaborator

@jeongukjae jeongukjae left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

other things looks good

src/hera/shared/_type_util.py Outdated Show resolved Hide resolved
src/hera/workflows/_meta_mixins.py Show resolved Hide resolved
if annotation := get_workflow_annotation(annotations[field]):
# Copy so as to not modify the fields themselves
annotation_copy = annotation.copy()
annotation_copy.name = annotation.name or field
yield field, field_info, annotation_copy
else:
yield field, field_info, Parameter(name=field)
yield field, field_info, construct_io_from_annotation(field, annotations[field])
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Comment on lines 441 to +442
for name, p in inspect.signature(source).parameters.items():
annotation = get_workflow_annotation(p.annotation)
if not annotation or not annotation.output:
annotation = construct_io_from_annotation(name, p.annotation)

This comment was marked as resolved.

Co-authored-by: Ukjae Jeong <[email protected]>
Signed-off-by: Alice <[email protected]>
Copy link
Collaborator

@jeongukjae jeongukjae left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks good to me :)

Comment on lines 305 to 306
for param in non_default_parameters:
param.value = "{{" + ("item" if len(non_default_parameters) == 1 else f"item.{param.name}") + "}}"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems harder to read/understand IMO

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can't be as succinct as before because we're doing a side-effect. A couple of options spring to mind, which do you prefer?

# Just move the if/else out into a separate line
for param in non_default_parameters:
    param_label = "item" if len(non_default_parameters) == 1 else f"item.{param.name}"
    param.value = "{{" + param_label + "}}"
# Move the if/else back out to the top
if len(non_default_parameters) == 1:
    non_default_parameters[0].value = "{{item}}"
else:
    for param in non_default_parameters:
        param.value = "{{{item." + param.name + "))

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The second please! 🙏 Makes it much more obvious that we're doing a very different thing if len == 1

@@ -0,0 +1,38 @@
"""
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need to add these two examples - they are not demonstrating something unique - script annotations/pydantic IO have their own examples, and the syntax for with_param is more simply shown in the loops examples, where the syntax in the DAG construction is the same.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair, I added them to exercise the code rather than as examples per se. Is there a better place to put this kind of end-to-end YAML output test?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could do a roundtrip to_dict/from_dict like in the test_script_annotations tests

workflow_dict = workflow.to_dict()
assert workflow == Workflow.from_dict(workflow_dict)

@alicederyn alicederyn force-pushed the get.param.items.bugfixes branch 2 times, most recently from 6f32dce to 4508194 Compare October 14, 2024 13:14
@sambhav sambhav merged commit 55fcdf1 into argoproj-labs:main Oct 25, 2024
21 of 23 checks passed
@alicederyn alicederyn deleted the get.param.items.bugfixes branch October 25, 2024 14:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
semver:patch A change requiring a patch version bump type:bug A general bug
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Issue using with_param and Pydantic IO Script annotation inputs doesn't play well with with_param
4 participants