Skip to content

Commit c4fc0bf

Browse files
committed
feat: support setting path for node output directly
1 parent 8d20078 commit c4fc0bf

File tree

3 files changed

+58
-7
lines changed

3 files changed

+58
-7
lines changed

sdk/ml/azure-ai-ml/azure/ai/ml/entities/_job/pipeline/_io.py

+9-5
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,8 @@ def path(self) -> str:
155155

156156
@path.setter
157157
def path(self, path):
158+
# For un-configured input/output, we build a default data entry for them.
159+
self._build_default_data()
158160
if hasattr(self._data, "path"):
159161
self._data.path = path
160162
else:
@@ -361,7 +363,9 @@ def is_control(self) -> str:
361363
def _build_default_data(self):
362364
"""Build default data when output not configured."""
363365
if self._data is None:
364-
self._data = Output()
366+
# _meta will be None when node._component is not a Component object
367+
# so we just leave the type inference work to backend
368+
self._data = Output(type=None)
365369

366370
def _build_data(self, data, key=None):
367371
"""Build output data according to assigned input, eg: node.outputs.key = data"""
@@ -593,15 +597,13 @@ def _validate_inputs(cls, inputs):
593597

594598
def __getattr__(self, name: K) -> V:
595599
if name not in self:
596-
# pylint: disable=unnecessary-comprehension
597-
raise UnexpectedAttributeError(keyword=name, keywords=[key for key in self])
600+
raise UnexpectedAttributeError(keyword=name, keywords=list(self))
598601
return super().__getitem__(name)
599602

600603
def __getitem__(self, item: K) -> V:
601604
# We raise this exception instead of KeyError
602605
if item not in self:
603-
# pylint: disable=unnecessary-comprehension
604-
raise UnexpectedKeywordError(func_name="ParameterGroup", keyword=item, keywords=[key for key in self])
606+
raise UnexpectedKeywordError(func_name="ParameterGroup", keyword=item, keywords=list(self))
605607
return super().__getitem__(item)
606608

607609
# For Jupyter Notebook auto-completion
@@ -649,6 +651,8 @@ def __setattr__(self, key: str, value: Union[Data, Output]):
649651
if isinstance(value, Output):
650652
mode = value.mode
651653
value = Output(type=value.type, path=value.path, mode=mode)
654+
if key not in self:
655+
raise UnexpectedAttributeError(keyword=key, keywords=list(self))
652656
original_output = self.__getattr__(key) # Note that an exception will be raised if the keyword is invalid.
653657
original_output._data = original_output._build_data(value)
654658

sdk/ml/azure-ai-ml/tests/internal/unittests/test_pipeline_job.py

+13
Original file line numberDiff line numberDiff line change
@@ -527,3 +527,16 @@ def pipeline_func():
527527
if key.startswith("data_"):
528528
expected_inputs[key] = {"job_input_type": "mltable", "uri": "azureml:scope_tsv:1"}
529529
assert rest_obj.properties.jobs["node"]["inputs"] == expected_inputs
530+
531+
def test_pipeline_with_setting_node_output_directly(self) -> None:
532+
component_dir = Path(__file__).parent.parent.parent / "test_configs" / "internal" / "command-component"
533+
copy_func = load_component(component_dir / "command-linux/copy/component.yaml")
534+
535+
copy_file = copy_func(
536+
input_dir=None,
537+
file_names=None,
538+
)
539+
540+
copy_file.outputs.output_dir.path = "path_on_datastore"
541+
assert copy_file.outputs.output_dir.path == "path_on_datastore"
542+
assert copy_file.outputs.output_dir.type == "path"

sdk/ml/azure-ai-ml/tests/pipeline_job/unittests/test_pipeline_job_entity.py

+36-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from pytest_mock import MockFixture
99
from test_utilities.utils import verify_entity_load_and_dump
1010

11-
from azure.ai.ml import MLClient, load_job
11+
from azure.ai.ml import MLClient, load_job, Output
1212
from azure.ai.ml._restclient.v2022_02_01_preview.models import JobBaseData as FebRestJob
1313
from azure.ai.ml._restclient.v2022_10_01_preview.models import JobBase as RestJob
1414
from azure.ai.ml._schema.automl import AutoMLRegressionSchema
@@ -26,7 +26,7 @@
2626
from azure.ai.ml.entities._job.automl.nlp import TextClassificationJob, TextClassificationMultilabelJob, TextNerJob
2727
from azure.ai.ml.entities._job.automl.tabular import ClassificationJob, ForecastingJob, RegressionJob
2828
from azure.ai.ml.entities._job.pipeline._io import PipelineInput, _GroupAttrDict
29-
from azure.ai.ml.exceptions import ValidationException
29+
from azure.ai.ml.exceptions import ValidationException, UnexpectedAttributeError
3030

3131
from .._util import _PIPELINE_JOB_TIMEOUT_SECOND
3232

@@ -1454,3 +1454,37 @@ def test_comment_in_pipeline(self) -> None:
14541454
rest_pipeline_dict = pipeline_job._to_rest_object().as_dict()["properties"]
14551455
assert pipeline_dict["jobs"]["hello_world_component"]["comment"] == "arbitrary string"
14561456
assert rest_pipeline_dict["jobs"]["hello_world_component"]["comment"] == "arbitrary string"
1457+
1458+
def test_pipeline_node_default_output(self):
1459+
test_path = "./tests/test_configs/pipeline_jobs/helloworld_pipeline_job_with_component_output.yml"
1460+
pipeline: PipelineJob = load_job(source=test_path)
1461+
1462+
test_output_path = "azureml://datastores/workspaceblobstore/paths/azureml/ps_copy_component/outputs/output_dir"
1463+
1464+
# pipeline level output
1465+
pipeline_output = pipeline.outputs["job_out_path_2"]
1466+
assert pipeline_output.mode == "upload"
1467+
1468+
# node level output
1469+
pipeline.jobs["hello_world_component_1"].outputs["component_out_path_1"].path = test_output_path
1470+
1471+
# normal output from component
1472+
node_output = pipeline.jobs["hello_world_component_1"].outputs["component_out_path_1"]
1473+
assert node_output.path == test_output_path
1474+
assert node_output.mode == "mount"
1475+
1476+
# data-binding-expression
1477+
node_output = pipeline.jobs["merge_component_outputs"].outputs["component_out_path_1"]
1478+
with pytest.raises(ValidationException, match="<class '.*'> does not support setting path."):
1479+
node_output.path = test_output_path
1480+
1481+
# non-existent output
1482+
with pytest.raises(
1483+
UnexpectedAttributeError,
1484+
match="Got an unexpected attribute 'component_out_path_non', "
1485+
"valid attributes: 'component_out_path_1', "
1486+
"'component_out_path_2', 'component_out_path_3'."
1487+
):
1488+
pipeline.jobs["hello_world_component_1"].outputs["component_out_path_non"] = Output(
1489+
path=test_output_path, mode="upload"
1490+
)

0 commit comments

Comments
 (0)