fix(sdk.v2): Fix regression on optional inputs (#6905)

* Fix regression on optional inputs

* add release note
This commit is contained in:
Chen Sun 2021-11-15 10:43:43 -08:00 committed by GitHub
parent 8f809c56b1
commit 9ee4534aef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 118 additions and 119 deletions

View File

@ -23,6 +23,7 @@
* Fix importer ignoring reimport setting, and switch to Protobuf.Value for import uri [\#6827](https://github.com/kubeflow/pipelines/pull/6827)
* Fix display name support for groups [\#6832](https://github.com/kubeflow/pipelines/pull/6832)
* Remove redundant check in set_gpu_limit [\#6866](https://github.com/kubeflow/pipelines/pull/6866)
* Fix regression on optional inputs [\#6905](https://github.com/kubeflow/pipelines/pull/6905)
## Documentation Updates

View File

@ -143,11 +143,11 @@ class CompilerCliTests(unittest.TestCase):
def test_pipeline_with_loops(self):
self._test_compile_py_to_json('pipeline_with_loops')
# TODO: re-enable the test, fix optional input support
# TODO: re-enable the test, fix regression on loop
# def test_pipeline_with_nested_loops(self):
# self._test_compile_py_to_json('pipeline_with_nested_loops')
# TODO: re-enable the test, fix optional input support
# TODO: re-enable the test, fix regression on loop
# def test_pipeline_with_loops_and_conditions(self):
# self._test_compile_py_to_json('pipeline_with_loops_and_conditions')
@ -176,9 +176,8 @@ class CompilerCliTests(unittest.TestCase):
# def test_pipeline_with_env(self):
# self._test_compile_py_to_json('pipeline_with_env')
# TODO: re-enable the test, fix optional input support
# def test_v2_component_with_optional_inputs(self):
# self._test_compile_py_to_json('v2_component_with_optional_inputs')
def test_v2_component_with_optional_inputs(self):
self._test_compile_py_to_json('v2_component_with_optional_inputs')
def test_pipeline_with_gcpc_types(self):
self._test_compile_py_to_json('pipeline_with_gcpc_types')

View File

@ -75,9 +75,6 @@
},
"message": {
"parameterType": "STRING"
},
"num_steps": {
"parameterType": "NUMBER_INTEGER"
}
}
},
@ -223,11 +220,6 @@
"outputParameterKey": "output_parameter_path",
"producerTask": "preprocess"
}
},
"num_steps": {
"runtimeValue": {
"constant": 100.0
}
}
}
},

View File

@ -1,82 +1,77 @@
{
"pipelineSpec": {
"components": {
"comp-component-op": {
"executorLabel": "exec-component-op",
"inputDefinitions": {
"parameters": {
"input1": {
"parameterType": "STRING"
},
"input2": {
"parameterType": "STRING"
}
"components": {
"comp-component-op": {
"executorLabel": "exec-component-op",
"inputDefinitions": {
"parameters": {
"input1": {
"parameterType": "STRING"
},
"input2": {
"parameterType": "STRING"
}
}
}
},
"deploymentSpec": {
"executors": {
"exec-component-op": {
"container": {
"args": [
"--executor_input",
"{{$}}",
"--function_to_execute",
"component_op"
],
"command": [
"sh",
"-c",
"\nif ! [ -x \"$(command -v pip)\" ]; then\n python3 -m ensurepip || python3 -m ensurepip --user || apt-get install python3-pip\nfi\n\nPIP_DISABLE_PIP_VERSION_CHECK=1 python3 -m pip install --quiet --no-warn-script-location 'kfp==1.8.6' && \"$0\" \"$@\"\n",
"sh",
"-ec",
"program_path=$(mktemp -d)\nprintf \"%s\" \"$0\" > \"$program_path/ephemeral_component.py\"\npython3 -m kfp.v2.components.executor_main --component_module_path \"$program_path/ephemeral_component.py\" \"$@\"\n",
"\nimport kfp\nfrom kfp.v2 import dsl\nfrom kfp.v2.dsl import *\nfrom typing import *\n\ndef component_op(\n input1: str = 'default value',\n input2: Optional[str] = None,\n input3: Optional[str] = None,\n):\n print(f'input1: {input1}, type: {type(input1)}')\n print(f'input2: {input2}, type: {type(input2)}')\n print(f'input3: {input3}, type: {type(input3)}')\n\n"
],
"image": "python:3.7"
}
}
},
"deploymentSpec": {
"executors": {
"exec-component-op": {
"container": {
"args": [
"--executor_input",
"{{$}}",
"--function_to_execute",
"component_op"
],
"command": [
"sh",
"-c",
"\nif ! [ -x \"$(command -v pip)\" ]; then\n python3 -m ensurepip || python3 -m ensurepip --user || apt-get install python3-pip\nfi\n\nPIP_DISABLE_PIP_VERSION_CHECK=1 python3 -m pip install --quiet --no-warn-script-location 'kfp==1.8.9' && \"$0\" \"$@\"\n",
"sh",
"-ec",
"program_path=$(mktemp -d)\nprintf \"%s\" \"$0\" > \"$program_path/ephemeral_component.py\"\npython3 -m kfp.v2.components.executor_main --component_module_path \"$program_path/ephemeral_component.py\" \"$@\"\n",
"\nimport kfp\nfrom kfp.v2 import dsl\nfrom kfp.v2.dsl import *\nfrom typing import *\n\ndef component_op(\n input1: str = 'default value',\n input2: Optional[str] = None,\n input3: Optional[str] = None,\n):\n print(f'input1: {input1}, type: {type(input1)}')\n print(f'input2: {input2}, type: {type(input2)}')\n print(f'input3: {input3}, type: {type(input3)}')\n\n"
],
"image": "python:3.7"
}
}
},
"pipelineInfo": {
"name": "v2-component-optional-input"
},
"root": {
"dag": {
"tasks": {
"component-op": {
"cachingOptions": {
"enableCache": true
},
"componentRef": {
"name": "comp-component-op"
},
"inputs": {
"parameters": {
"input1": {
"runtimeValue": {
"constant": "Hello"
}
},
"input2": {
"runtimeValue": {
"constant": "World"
}
}
},
"pipelineInfo": {
"name": "v2-component-optional-input"
},
"root": {
"dag": {
"tasks": {
"component-op": {
"cachingOptions": {
"enableCache": true
},
"componentRef": {
"name": "comp-component-op"
},
"inputs": {
"parameters": {
"input1": {
"runtimeValue": {
"constant": "Hello"
}
},
"input2": {
"runtimeValue": {
"constant": "World"
}
}
},
"taskInfo": {
"name": "component-op"
}
},
"taskInfo": {
"name": "component-op"
}
}
}
},
"schemaVersion": "2.1.0",
"sdkVersion": "kfp-1.8.6"
}
},
"runtimeConfig": {
"gcsOutputDirectory": "dummy_root"
}
"schemaVersion": "2.1.0",
"sdkVersion": "kfp-1.8.9"
}

View File

@ -62,23 +62,18 @@ class BaseComponent(metaclass=abc.ABCMeta):
f'{self.name}() got multiple values for argument "{k}".')
task_inputs[k] = v
# Fill in default value if there was no user provided value
for input_name, input_spec in (self.component_spec.inputs or
{}).items():
if input_spec.default is not None and input_name not in task_inputs:
task_inputs[input_name] = input_spec.default
missing_arguments = [
input_name for input_name in (self.component_spec.inputs or {})
if input_name not in task_inputs
input_name for input_name, input_spec in (
self.component_spec.inputs or {}).items()
if input_name not in task_inputs and not input_spec.optional
]
if missing_arguments:
argument_or_arguments = 'argument' if len(
missing_arguments) == 1 else 'arguments'
arguments = ','.join(missing_arguments)
arguments = ', '.join(missing_arguments)
raise TypeError(
f'{self.name}() missing {len(missing_arguments)} required positional '
f'{self.name}() missing {len(missing_arguments)} required '
f'{argument_or_arguments}: {arguments}.')
return pipeline_task.create_pipeline_task(

View File

@ -43,9 +43,14 @@ component_op = TestComponent(
],
),
inputs={
'input1': structures.InputSpec(type='String'),
'input2': structures.InputSpec(type='Integer'),
'input3': structures.InputSpec(type='Float', default=3.14),
'input1':
structures.InputSpec(type='String'),
'input2':
structures.InputSpec(type='Integer'),
'input3':
structures.InputSpec(type='Float', default=3.14),
'input4':
structures.InputSpec(type='Optional[Float]', default=None),
},
outputs={
'output1': structures.OutputSpec(name='output1', type='String'),
@ -59,7 +64,7 @@ class BaseComponentTest(unittest.TestCase):
def test_instantiate_component_with_keyword_arguments(
self, mock_create_pipeline_task):
component_op(input1='hello', input2=100, input3=1.23)
component_op(input1='hello', input2=100, input3=1.23, input4=3.21)
mock_create_pipeline_task.assert_called_once_with(
component_spec=component_op.component_spec,
@ -67,6 +72,7 @@ class BaseComponentTest(unittest.TestCase):
'input1': 'hello',
'input2': 100,
'input3': 1.23,
'input4': 3.21,
})
@patch.object(pipeline_task, 'create_pipeline_task', autospec=True)
@ -80,33 +86,31 @@ class BaseComponentTest(unittest.TestCase):
arguments={
'input1': 'hello',
'input2': 100,
'input3': '3.14',
})
def test_instantiate_component_with_positional_arugment(self):
with self.assertRaisesRegex(
TypeError,
'Components must be instantiated using keyword arguments. Positional '
'parameters are not allowed \(found 3 such parameters for component '
'"component_1"\).'):
'Components must be instantiated using keyword arguments.'
' Positional parameters are not allowed \(found 3 such'
' parameters for component "component_1"\).'):
component_op('abc', 1, 2.3)
def test_instantiate_component_with_unexpected_keyword_arugment(self):
with self.assertRaisesRegex(
TypeError,
'component_1\(\) got an unexpected keyword argument "input4".'):
component_op(input1='abc', input2=1, input3=2.3, input4='extra')
'component_1\(\) got an unexpected keyword argument "input0".'):
component_op(input1='abc', input2=1, input3=2.3, input0='extra')
def test_instantiate_component_with_missing_arugments(self):
with self.assertRaisesRegex(
TypeError,
'component_1\(\) missing 1 required positional argument: input1.'
):
'component_1\(\) missing 1 required argument: input1.'):
component_op(input2=1)
with self.assertRaisesRegex(
TypeError,
'component_1\(\) missing 2 required positional arguments: input1,input2.'
'component_1\(\) missing 2 required arguments: input1, input2.'
):
component_op()

View File

@ -226,19 +226,18 @@ def extract_component_interface(func: Callable) -> structures.ComponentSpec:
else:
io_name = _maybe_make_unique(io_name, input_names)
input_names.add(io_name)
input_spec = structures.InputSpec(
type=type_struct, description=doc_dict.get(parameter.name))
if parameter.default is not inspect.Parameter.empty:
# input_spec.optional = True
if parameter.default is not None:
outer_type_name = list(type_struct.keys())[0] if isinstance(
type_struct, dict) else type_struct
try:
input_spec.default = parameter.default
except Exception as ex:
warnings.warn(
'Could not serialize the default value of the'
' parameter "{}". {}'.format(parameter.name, ex))
input_spec = structures.InputSpec(
type=type_struct,
description=doc_dict.get(parameter.name),
default=parameter.default,
)
else:
input_spec = structures.InputSpec(
type=type_struct,
description=doc_dict.get(parameter.name),
)
inputs[io_name] = input_spec
#Analyzing the return type annotations.

View File

@ -38,11 +38,23 @@ class InputSpec(BaseModel):
type: The type of the input.
default: Optional; the default value for the input.
description: Optional: the user description of the input.
optional: Wether the input is optional. An input is optional when it has
an explicit default value.
"""
# TODO(ji-yaqi): Add logic to cast default value into the specified type.
type: str
default: Optional[Union[str, int, float, bool, dict, list]] = None
description: Optional[str] = None
_optional: bool = pydantic.PrivateAttr()
def __init__(self, **data):
super().__init__(**data)
# An input is optional if a default value is explicitly specified.
self._optional = 'default' in data
@property
def optional(self) -> bool:
return self._optional
class OutputSpec(BaseModel):
@ -578,7 +590,7 @@ class ComponentSpec(BaseModel):
"""
with open(output_file, 'a') as output_file:
json_component = self.json(
exclude_none=True, exclude_unset=True, by_alias=True)
exclude_none=False, exclude_unset=True, by_alias=True)
yaml_file = yaml.safe_dump(
json.loads(json_component),
default_flow_style=None,

View File

@ -43,7 +43,7 @@ V1_YAML_IF_PLACEHOLDER = textwrap.dedent("""\
V2_YAML_IF_PLACEHOLDER = textwrap.dedent("""\
name: component_if
inputs:
optional_input_1: {type: String}
optional_input_1: {type: String, default: null}
implementation:
container:
image: alpine
@ -75,7 +75,9 @@ V2_COMPONENT_SPEC_IF_PLACEHOLDER = structures.ComponentSpec(
'default',
]))
])),
inputs={'optional_input_1': structures.InputSpec(type='String')},
inputs={
'optional_input_1': structures.InputSpec(type='String', default=None)
},
)
V1_YAML_CONCAT_PLACEHOLDER = textwrap.dedent("""\