diff --git a/sdk/ml/azure-ai-ml/azure/ai/ml/_schema/core/fields.py b/sdk/ml/azure-ai-ml/azure/ai/ml/_schema/core/fields.py index fd7956b85bb0..5986ab7d8f73 100644 --- a/sdk/ml/azure-ai-ml/azure/ai/ml/_schema/core/fields.py +++ b/sdk/ml/azure-ai-ml/azure/ai/ml/_schema/core/fields.py @@ -45,6 +45,7 @@ module_logger = logging.getLogger(__name__) T = typing.TypeVar("T") +NO_SUCH_FILE_OR_DIRECTORY_ERROR = "No such file or directory" class StringTransformedEnum(Field): @@ -369,7 +370,10 @@ def _deserialize(self, value, attr, data, **kwargs): if not path.is_absolute(): path = base_path / path path.resolve() - data = load_file(path) + try: + data = load_file(path) + except (FileNotFoundError, OSError) as e: + raise ValidationError(f"{NO_SUCH_FILE_OR_DIRECTORY_ERROR}: {path}") from e return data raise ValidationError(f"Not supporting non file for {attr}") @@ -451,6 +455,16 @@ def union_fields(self): def insert_union_field(self, field): self._union_fields.insert(0, field) + @staticmethod + def _contains_file_not_found_error(error_message): + if isinstance(error_message, str): + return NO_SUCH_FILE_OR_DIRECTORY_ERROR in error_message + if isinstance(error_message, dict): + return any(UnionField._contains_file_not_found_error(value) for value in error_message.values()) + if isinstance(error_message, list): + return any(UnionField._contains_file_not_found_error(value) for value in error_message) + return False + # This sets the parent for the schema and also handles nesting. def _bind_to_schema(self, field_name, schema): super()._bind_to_schema(field_name, schema) @@ -484,7 +498,10 @@ def _deserialize(self, value, attr, data, **kwargs): try: return schema.deserialize(value, attr, data, **kwargs) except ValidationError as e: - errors.append(e.normalized_messages()) + normalized_messages = e.normalized_messages() + if self._contains_file_not_found_error(normalized_messages): + raise ValidationError(normalized_messages, field_name=attr) from e + errors.append(normalized_messages) except ValidationException as e: # ValidationException is explicitly raised in project code so usually easy to locate with error message errors.append([str(e)]) diff --git a/sdk/ml/azure-ai-ml/tests/schedule/unittests/test_schedule_entity.py b/sdk/ml/azure-ai-ml/tests/schedule/unittests/test_schedule_entity.py index a4ea794ace51..01b04b2f811c 100644 --- a/sdk/ml/azure-ai-ml/tests/schedule/unittests/test_schedule_entity.py +++ b/sdk/ml/azure-ai-ml/tests/schedule/unittests/test_schedule_entity.py @@ -1,8 +1,10 @@ from datetime import datetime import pytest +from marshmallow import ValidationError from test_utilities.utils import verify_entity_load_and_dump +from azure.ai.ml._schema.core.fields import NO_SUCH_FILE_OR_DIRECTORY_ERROR from azure.ai.ml.constants import TimeZone from azure.ai.ml.entities import CronTrigger, JobSchedule, PipelineJob, RecurrencePattern, RecurrenceTrigger from azure.ai.ml.entities._load_functions import load_job, load_schedule @@ -189,3 +191,15 @@ def test_schedule_create_out_of_box_monitoring_job(self): test_path = "./tests/test_configs/schedule/out_of_box_monitoring.yaml" schedule = load_schedule(test_path) assert "genai_app_monitoring", schedule.name + + def test_load_schedule_with_nonexistent_pipeline_file_reference(self): + test_path = "./tests/test_configs/schedule/hello_cron_schedule_with_file_reference.yml" + params_override = [{"create_job.job": "../pipeline_jobs/not_exists_pipeline.yml"}] + + with pytest.raises(ValidationError) as e: + load_schedule(test_path, params_override=params_override) + + message = str(e.value) + assert NO_SUCH_FILE_OR_DIRECTORY_ERROR in message + assert message.count(NO_SUCH_FILE_OR_DIRECTORY_ERROR) == 1 + assert "Not supporting non file for create_job" not in message