Skip to content

Commit 78e05f7

Browse files
saanikaguptamicrosoftSaanika Gupta
authored andcommitted
Enable python 3.14 support for azure-ai-ml (#44072)
* Initial changes to enable python 3.14 support - adjusted requirements * Fix test_equality - Python 3.14 no longer allows NotImplemented to be used in boolean contexts * Unskip test in test_data_utils - Python 3.13+ enforces stricter context manager protocol for mocked objects, updated the mocks to fix it * Fix DistillationJob.__eq__ logic and unskip test in test_distillation_conversion - Python 3.13+ no longer allows NotImplemented to be used in boolean contexts * Replace help() with inspect.signature() in test_dsl_group - making the test more robust and independent of python version * Fix Python 3.14 test failures in test_init_finalize_job by accessing class attributes directly instead of through self in closures * Update skipif condition in test_persistent_locals - bytecode implementation is till 3.12, after that it uses profiler at runtime, hence need not test bytecode for 3.13 onwards * Update README * Update CHANGELOG * Add tests for dev requirements * Add test to cover src code changes in distillation_job --------- Co-authored-by: Saanika Gupta <[email protected]>
1 parent 5c02f1b commit 78e05f7

File tree

12 files changed

+167
-99
lines changed

12 files changed

+167
-99
lines changed

sdk/ml/azure-ai-ml/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
### Other Changes
1010

1111
- Ensuring that azureml-dataprep-rslex is only installed for PyPy below 3.10 and CPython below 3.13.
12+
- Adding support for Python 3.14.
1213

1314
## 1.30.0 (2025-10-29)
1415

sdk/ml/azure-ai-ml/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ We are excited to introduce the GA of Azure Machine Learning Python SDK v2. The
1010
| [Samples][ml_samples]
1111

1212

13-
This package has been tested with Python 3.8, 3.9, 3.10, 3.11, 3.12 and 3.13.
13+
This package has been tested with Python 3.8, 3.9, 3.10, 3.11, 3.12, 3.13 and 3.14.
1414

1515
For a more complete set of Azure libraries, see https://aka.ms/azsdk/python/all
1616

sdk/ml/azure-ai-ml/azure/ai/ml/entities/_job/distillation/distillation_job.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -517,9 +517,19 @@ def __eq__(self, other: object) -> bool:
517517
"""
518518
if not isinstance(other, DistillationJob):
519519
return False
520+
parent_eq = super().__eq__(other)
521+
522+
if parent_eq is NotImplemented:
523+
# Parent doesn't implement comparison, we'll continue comparison on our end
524+
pass
525+
# Adding this case for future
526+
# currently the parent doesn't implement __eq__ so we will always get NotImplemented only
527+
elif not parent_eq:
528+
# Parent says objects are not equal
529+
return False
530+
520531
return (
521-
super().__eq__(other)
522-
and self.data_generation_type == other.data_generation_type
532+
self.data_generation_type == other.data_generation_type
523533
and self.data_generation_task_type == other.data_generation_task_type
524534
and self.teacher_model_endpoint_connection.name == other.teacher_model_endpoint_connection.name
525535
and self.student_model == other.student_model

sdk/ml/azure-ai-ml/dev_requirements.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ pytest-mock
1212
pytest
1313
pydash
1414
azure-mgmt-msi
15-
pywin32==306 ; sys_platform == 'win32'
15+
pywin32==311 ; sys_platform == 'win32'
1616
docker;platform.python_implementation!="PyPy"
1717
numpy;platform.python_implementation!="PyPy"
18-
scikit-image;platform.python_implementation!="PyPy"
18+
scikit-image;platform.python_implementation!="PyPy" and python_version < "3.14"
1919
mldesigner
2020
azure-mgmt-resourcegraph<9.0.0,>=2.0.0
2121
azure-mgmt-resource<23.0.0,>=3.0.0

sdk/ml/azure-ai-ml/setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
"Programming Language :: Python :: 3.11",
5454
"Programming Language :: Python :: 3.12",
5555
"Programming Language :: Python :: 3.13",
56+
"Programming Language :: Python :: 3.14",
5657
"License :: OSI Approved :: MIT License",
5758
],
5859
zip_safe=False,

sdk/ml/azure-ai-ml/tests/batch_online_common/unittests/test_endpoint_entity.py

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,23 @@
1-
import pytest
2-
import yaml
3-
import json
41
import copy
2+
import json
53
import sys
4+
5+
import pytest
6+
import yaml
67
from test_utilities.utils import verify_entity_load_and_dump
7-
from azure.ai.ml._restclient.v2022_02_01_preview.models import (
8-
OnlineEndpointData,
9-
EndpointAuthKeys as RestEndpointAuthKeys,
10-
EndpointAuthToken as RestEndpointAuthToken,
11-
)
12-
from azure.ai.ml._restclient.v2023_10_01.models import BatchEndpoint as BatchEndpointData
8+
139
from azure.ai.ml import load_batch_endpoint, load_online_endpoint
10+
from azure.ai.ml._restclient.v2022_02_01_preview.models import EndpointAuthKeys as RestEndpointAuthKeys
11+
from azure.ai.ml._restclient.v2022_02_01_preview.models import EndpointAuthToken as RestEndpointAuthToken
12+
from azure.ai.ml._restclient.v2022_02_01_preview.models import OnlineEndpointData
13+
from azure.ai.ml._restclient.v2023_10_01.models import BatchEndpoint as BatchEndpointData
1414
from azure.ai.ml.entities import (
1515
BatchEndpoint,
16-
ManagedOnlineEndpoint,
17-
KubernetesOnlineEndpoint,
18-
OnlineEndpoint,
1916
EndpointAuthKeys,
2017
EndpointAuthToken,
18+
KubernetesOnlineEndpoint,
19+
ManagedOnlineEndpoint,
20+
OnlineEndpoint,
2121
)
2222
from azure.ai.ml.exceptions import ValidationException
2323

@@ -332,15 +332,12 @@ def test_dump(self) -> None:
332332
assert online_endpoint_dict["identity"]["type"] == online_endpoint.identity.type
333333
assert online_endpoint_dict["traffic"] == online_endpoint.traffic
334334

335-
@pytest.mark.skipif(
336-
condition=sys.version_info >= (3, 13), reason="historical implementation doesn't support Python 3.13+"
337-
)
338335
def test_equality(self) -> None:
339336
online_endpoint = load_online_endpoint(TestManagedOnlineEndpoint.ONLINE_ENDPOINT)
340337
batch_online_endpoint = load_batch_endpoint(TestManagedOnlineEndpoint.BATCH_ENDPOINT_WITH_BLUE)
341338

342-
assert online_endpoint.__eq__(None)
343-
assert online_endpoint.__eq__(batch_online_endpoint)
339+
assert online_endpoint.__eq__(None) is NotImplemented
340+
assert online_endpoint.__eq__(batch_online_endpoint) is NotImplemented
344341

345342
other_online_endpoint = copy.deepcopy(online_endpoint)
346343
assert online_endpoint == other_online_endpoint

sdk/ml/azure-ai-ml/tests/dataset/unittests/test_data_utils.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1+
import sys
12
from collections import OrderedDict
23
from pathlib import Path
34
from unittest.mock import Mock, patch
4-
import sys
55

66
import pytest
77

@@ -97,10 +97,6 @@ def test_read_mltable_metadata_contents(
9797
read_local_mltable_metadata_contents(path=mltable_folder / "should-fail")
9898
assert "No such file or directory" in str(ex)
9999

100-
@pytest.mark.skipif(
101-
sys.version_info >= (3, 13),
102-
reason="Failing in spacific use case of TemporaryDirectory in Python 3.13 in test case only, skipping the test for now.",
103-
)
104100
@patch("azure.ai.ml._utils._data_utils.get_datastore_info")
105101
@patch("azure.ai.ml._utils._data_utils.get_storage_client")
106102
def test_read_remote_mltable_metadata_contents(
@@ -126,7 +122,9 @@ def test_read_remote_mltable_metadata_contents(
126122
tmp_metadata_file.write_text(file_contents)
127123

128124
# remote azureml accessible
129-
with patch("azure.ai.ml._utils._data_utils.TemporaryDirectory", return_value=mltable_folder):
125+
with patch("azure.ai.ml._utils._data_utils.TemporaryDirectory") as mock_tmp:
126+
mock_tmp.return_value.__enter__.return_value = str(mltable_folder)
127+
mock_tmp.return_value.__exit__.return_value = None
130128
contents = read_remote_mltable_metadata_contents(
131129
datastore_operations=mock_datastore_operations,
132130
base_uri="azureml://datastores/mydatastore/paths/images/dogs",

sdk/ml/azure-ai-ml/tests/distillation_job/unittests/test_distillation_conversion.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
import pytest
21
import sys
2+
from unittest.mock import patch
3+
4+
import pytest
35

46
from azure.ai.ml._restclient.v2024_01_01_preview.models import MLFlowModelJobInput, UriFileJobInput
57
from azure.ai.ml.constants import DataGenerationTaskType, DataGenerationType
@@ -9,15 +11,31 @@
911
from azure.ai.ml.entities._job.distillation.endpoint_request_settings import EndpointRequestSettings
1012
from azure.ai.ml.entities._job.distillation.prompt_settings import PromptSettings
1113
from azure.ai.ml.entities._job.distillation.teacher_model_settings import TeacherModelSettings
14+
from azure.ai.ml.entities._job.job import Job
1215
from azure.ai.ml.entities._job.resource_configuration import ResourceConfiguration
1316
from azure.ai.ml.entities._workspace.connections.connection_subtypes import ServerlessConnection
1417
from azure.ai.ml.entities._workspace.connections.workspace_connection import WorkspaceConnection
1518

1619

17-
@pytest.mark.skipif(
18-
condition=sys.version_info >= (3, 13), reason="historical implementation doesn't support Python 3.13+"
19-
)
2020
class TestDistillationJobConversion:
21+
22+
def test_distillation_job_eq_type_check_and_parent_false(self):
23+
"""Test __eq__ edge cases for Python 3.14 compatibility."""
24+
distillation_job = DistillationJob(
25+
data_generation_type=DataGenerationType.DATA_GENERATION,
26+
data_generation_task_type=DataGenerationTaskType.NLI,
27+
teacher_model_endpoint_connection=ServerlessConnection(
28+
name="llama-teacher", endpoint="http://bar.com", api_key="TESTKEY"
29+
),
30+
student_model=Input(type=AssetTypes.MLFLOW_MODEL, path="azureml://foo/bar"),
31+
name="test-job",
32+
)
33+
34+
# mock parent __eq__ returning False
35+
# As parent doesn't currently implement __eq__ and defaults to NotImplemented which gives error in boolean context
36+
with patch.object(Job, "__eq__", return_value=False):
37+
assert distillation_job != distillation_job
38+
2139
@pytest.mark.parametrize(
2240
"data_generation_task_type",
2341
[

sdk/ml/azure-ai-ml/tests/dsl/unittests/test_dsl_group.py

Lines changed: 43 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import inspect
12
import sys
23
from enum import Enum as PyEnum
34
from io import StringIO
@@ -71,9 +72,6 @@ def test_restore_flattened_inputs(self) -> None:
7172
assert isinstance(result.ab, _GroupAttrDict)
7273
assert isinstance(result.ab.c, PipelineInput)
7374

74-
@pytest.mark.skipif(
75-
condition=sys.version_info >= (3, 13), reason="historical implementation doesn't support Python 3.13+"
76-
)
7775
def test_auto_generated_functions(self) -> None:
7876
class EnumOps(PyEnum):
7977
Option1 = "Option1"
@@ -89,15 +87,27 @@ class MixedGroup:
8987

9088
# __init__ func test
9189
assert hasattr(MixedGroup, "__init__") is True
92-
original_out = sys.stdout
93-
sys.stdout = stdout_str_IO = StringIO()
94-
help(MixedGroup.__init__)
95-
assert (
96-
"__init__(self,*,int_param:int=None,str_param:str=None,enum_param:str=None,"
97-
"str_default_param:str='test',optional_int_param:int=5)->None"
98-
in remove_extra_character(stdout_str_IO.getvalue())
99-
)
100-
sys.stdout = original_out
90+
sig = inspect.signature(MixedGroup.__init__)
91+
params = sig.parameters
92+
93+
# Verify all expected parameters exist
94+
assert "self" in params
95+
assert "int_param" in params
96+
assert "str_param" in params
97+
assert "enum_param" in params
98+
assert "str_default_param" in params
99+
assert "optional_int_param" in params
100+
101+
# Verify default values
102+
assert params["int_param"].default is None
103+
assert params["str_param"].default is None
104+
assert params["enum_param"].default is None
105+
assert params["str_default_param"].default == "test"
106+
assert params["optional_int_param"].default == 5
107+
108+
# Verify keyword-only parameters (after *)
109+
assert params["int_param"].kind == inspect.Parameter.KEYWORD_ONLY
110+
assert params["str_param"].kind == inspect.Parameter.KEYWORD_ONLY
101111

102112
# __repr__ func test
103113
var = MixedGroup(
@@ -415,9 +425,6 @@ def my_pipeline(my_inputs: PortInputs):
415425

416426
assert "Only primitive types can be used as input of group, got uri_file" in str(e.value)
417427

418-
@pytest.mark.skipif(
419-
condition=sys.version_info >= (3, 13), reason="historical implementation doesn't support Python 3.13+"
420-
)
421428
def test_group_defaults_with_outputs(self):
422429
@group
423430
class MixedGroup:
@@ -428,21 +435,28 @@ class MixedGroup:
428435
optional_int_param: Input(type="integer", optional=True) = 5
429436
output_folder: Output(type="uri_folder")
430437

438+
# __init__ func test
431439
assert hasattr(MixedGroup, "__init__") is True
432-
original_out = sys.stdout
433-
sys.stdout = stdout_str_IO = StringIO()
434-
help(MixedGroup.__init__)
435-
assert (
436-
"__init__(self,*,"
437-
"int_param:int=None,"
438-
"str_default_param:str='test',"
439-
"str_param:str=None,"
440-
"input_folder:{'type':'uri_folder'}=None,"
441-
"optional_int_param:int=5,"
442-
"output_folder:{'type':'uri_folder'}=None)"
443-
"->None" in remove_extra_character(stdout_str_IO.getvalue())
444-
)
445-
sys.stdout = original_out
440+
sig = inspect.signature(MixedGroup.__init__)
441+
params = sig.parameters
442+
443+
# Verify all expected parameters exist
444+
assert "int_param" in params
445+
assert "str_default_param" in params
446+
assert "str_param" in params
447+
assert "input_folder" in params
448+
assert "optional_int_param" in params
449+
assert "output_folder" in params
450+
451+
# Verify default values
452+
assert params["int_param"].default is None
453+
assert params["str_default_param"].default == "test"
454+
assert params["str_param"].default is None
455+
assert params["optional_int_param"].default == 5
456+
# input_folder and output_folder should have None as default
457+
assert params["input_folder"].default is None
458+
assert params["output_folder"].default is None
459+
446460
# __repr__ func test
447461
var = MixedGroup(
448462
int_param=1, str_param="test-str", input_folder=Input(path="input"), output_folder=Output(path="output")

0 commit comments

Comments
 (0)