Skip to content

Commit e4ab9f3

Browse files
authored
Support properties in command component (Azure#27000)
* support properties * remove properties if not specified * remove properties in post dump
1 parent 9bfdf2e commit e4ab9f3

File tree

6 files changed

+486
-2
lines changed

6 files changed

+486
-2
lines changed

sdk/ml/azure-ai-ml/azure/ai/ml/_schema/component/command_component.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from copy import deepcopy
88

99
import yaml
10-
from marshmallow import INCLUDE, fields, post_load
10+
from marshmallow import INCLUDE, fields, post_load, post_dump
1111

1212
from azure.ai.ml._schema.assets.asset import AnonymousAssetSchema
1313
from azure.ai.ml._schema.component.component import ComponentSchema
@@ -48,6 +48,14 @@ class Meta:
4848
]
4949
),
5050
)
51+
properties = fields.Dict(keys=fields.Str(), values=fields.Raw())
52+
53+
@post_dump
54+
def remove_unnecessary_fields(self, component_schema_dict, **kwargs):
55+
# remove empty properties to keep the component spec unchanged
56+
if not component_schema_dict.get("properties"):
57+
component_schema_dict.pop("properties", None)
58+
return component_schema_dict
5159

5260

5361
class RestCommandComponentSchema(CommandComponentSchema):

sdk/ml/azure-ai-ml/azure/ai/ml/entities/_component/command_component.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ class CommandComponent(Component, ParameterizedCommand):
5454
:type instance_count: int
5555
:param is_deterministic: Whether the command component is deterministic.
5656
:type is_deterministic: bool
57+
:param properties: Properties of the component. Contents inside will pass through to backend as a dictionary.
58+
:type properties: dict
59+
5760
:raises ~azure.ai.ml.exceptions.ValidationException: Raised if CommandComponent cannot be successfully validated.
5861
Details will be provided in the error message.
5962
"""
@@ -75,6 +78,7 @@ def __init__(
7578
outputs: Dict = None,
7679
instance_count: int = None, # promoted property from resources.instance_count
7780
is_deterministic: bool = True,
81+
properties: Dict = None,
7882
**kwargs,
7983
):
8084
# validate init params are valid type
@@ -98,6 +102,7 @@ def __init__(
98102
inputs=inputs,
99103
outputs=outputs,
100104
is_deterministic=is_deterministic,
105+
properties=properties,
101106
**kwargs,
102107
)
103108

sdk/ml/azure-ai-ml/tests/component/e2etests/test_component.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77

88
import pydash
99
import pytest
10-
from test_utilities.utils import _PYTEST_TIMEOUT_METHOD
1110

1211
from azure.ai.ml import MLClient, MpiDistribution, load_component, load_environment
1312
from azure.ai.ml._restclient.v2022_05_01.models import ComponentContainerData, ListViewType
@@ -876,3 +875,43 @@ def test_component_with_default_label(
876875

877876
node = default_component()
878877
assert node._to_rest_object()["componentId"] == default_component.id
878+
879+
def test_command_component_with_properties_e2e_flow(self, client: MLClient, randstr: Callable[[str], str]) -> None:
880+
command_component = load_component(
881+
source="./tests/test_configs/components/helloworld_component_with_properties.yml",
882+
)
883+
expected_dict = {
884+
'$schema': 'https://azuremlschemas.azureedge.net/development/commandComponent.schema.json',
885+
'_source': 'YAML.COMPONENT',
886+
'command': 'echo Hello World & echo $[[${{inputs.component_in_number}}]] & '
887+
'echo ${{inputs.component_in_path}} & echo '
888+
'${{outputs.component_out_path}} > '
889+
'${{outputs.component_out_path}}/component_in_number',
890+
'description': 'This is the basic command component',
891+
'display_name': 'CommandComponentBasic',
892+
'inputs': {'component_in_number': {'default': '10.99',
893+
'description': 'A number',
894+
'optional': True,
895+
'type': 'number'},
896+
'component_in_path': {'description': 'A path',
897+
'type': 'uri_folder'}},
898+
'is_deterministic': True,
899+
'outputs': {'component_out_path': {'type': 'uri_folder'}},
900+
'properties': {'azureml.pipelines.dynamic': 'true'},
901+
'tags': {'owner': 'sdkteam', 'tag': 'tagvalue'},
902+
'type': 'command',
903+
}
904+
omit_fields = ["name", "creation_context", "id", "code", "environment", "version"]
905+
rest_component = pydash.omit(
906+
command_component._to_rest_object().as_dict()["properties"]["component_spec"],
907+
omit_fields,
908+
)
909+
910+
assert rest_component == expected_dict
911+
912+
from_rest_component = client.components.create_or_update(command_component, is_anonymous=True)
913+
914+
previous_dict = pydash.omit(command_component._to_dict(), omit_fields)
915+
current_dict = pydash.omit(from_rest_component._to_dict(), omit_fields)
916+
# TODO(2037030): verify when backend ready
917+
# assert previous_dict == current_dict

sdk/ml/azure-ai-ml/tests/component/unittests/test_component_schema.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,14 @@ def test_anonymous_component_same_name(self, mock_machinelearning_client: MLClie
294294
component_hash2 = component_entity2._get_anonymous_hash()
295295
assert component_hash1 != component_hash2
296296

297+
def test_command_component_with_properties(self):
298+
test_path = "./tests/test_configs/components/helloworld_component_with_properties.yml"
299+
component_entity = load_component(source=test_path)
300+
assert component_entity.properties == {"azureml.pipelines.dynamic": "true"}
301+
302+
validation_result = component_entity._validate()
303+
assert validation_result.passed is True
304+
297305

298306
@pytest.mark.timeout(_COMPONENT_TIMEOUT_SECOND)
299307
@pytest.mark.unittest

0 commit comments

Comments
 (0)