From e890152779d498ca9a09653f7b026e39c09ba62b Mon Sep 17 00:00:00 2001 From: "Ekaterina Legacheva (AKVELON INC)" Date: Tue, 29 Sep 2026 12:07:06 -0700 Subject: [PATCH 1/3] Deprecate Workspace(storage=...) in favor of workspace-linked storage Emit a DeprecationWarning when storage= is supplied, pointing users to configure workspace-linked storage instead. Removal is planned for azure-quantum 4.0.0. Transitional behavior (explicit-storage container URI generation) is unchanged. Workspace.from_connection_string(...) is unaffected and does not warn. --- azure-quantum/azure/quantum/workspace.py | 44 +++++++---- azure-quantum/tests/test_workspace.py | 93 ++++++++++++++++++++---- 2 files changed, 110 insertions(+), 27 deletions(-) diff --git a/azure-quantum/azure/quantum/workspace.py b/azure-quantum/azure/quantum/workspace.py index 0ad43032..5ea1e6f9 100644 --- a/azure-quantum/azure/quantum/workspace.py +++ b/azure-quantum/azure/quantum/workspace.py @@ -10,6 +10,7 @@ from __future__ import annotations from datetime import datetime import logging +import warnings from urllib.parse import quote from typing import ( Any, @@ -71,12 +72,12 @@ class Workspace: 2. specify a valid subscription ID, resource group, and workspace name; or 3. specify a valid workspace name. - You can also use a connection string to specify the connection parameters - to an Azure Quantum Workspace by calling + You can also use an Azure Quantum workspace authentication connection + string to specify the connection parameters by calling :obj:`~ Workspace.from_connection_string() `. - If the Azure Quantum workspace does not have linked storage, the caller - must also pass a valid Azure storage account connection string. + Azure Quantum workspaces require linked storage. Configure linked storage + on the workspace instead of passing a storage account connection string. :param subscription_id: The Azure subscription ID. @@ -91,9 +92,9 @@ class Workspace: Ignored if resource_id is specified. :param storage: - The Azure storage account connection string. - Required only if the specified Azure Quantum - workspace does not have linked storage. + Deprecated. The Azure storage account connection string. + Configure workspace-linked storage instead. This parameter will be + removed in azure-quantum 4.0.0. :param resource_id: The resource ID of the Azure Quantum workspace. @@ -120,6 +121,10 @@ class Workspace: _QUANTUM_ENDPOINT_PARAM = '_quantum_endpoint' _WORKSPACE_KIND_PARAM = '_workspace_kind' _MGMT_CLIENT_PARAM = '_mgmt_client' + _STORAGE_DEPRECATION_MESSAGE = ( + "Workspace(storage=...) is deprecated and will be removed in " + "azure-quantum 4.0.0. Configure workspace-linked storage instead." + ) def __init__( self, @@ -133,6 +138,13 @@ def __init__( user_agent: Optional[str] = None, **kwargs: Any, ) -> None: + if storage is not None: + warnings.warn( + Workspace._STORAGE_DEPRECATION_MESSAGE, + DeprecationWarning, + stacklevel=2, + ) + # Extract internal params before passing kwargs to WorkspaceConnectionParams # Param to track whether the workspace was created from a connection string from_connection_string = kwargs.pop(Workspace._FROM_CONNECTION_STRING_PARAM, False) @@ -254,12 +266,16 @@ def credential(self) -> Any: return self._connection_params.credential @property - def storage(self) -> str: + def storage(self) -> Optional[str]: """ - Returns the Azure Storage account name associated with the Quantum Workspace. + Returns the explicitly configured Azure storage account connection string. - :return: Azure Storage account name. - :rtype: str + This property is retained for compatibility with the deprecated + ``Workspace(storage=...)`` parameter. Configure workspace-linked storage + instead. + + :return: Azure storage account connection string, or ``None`` if one was not configured. + :rtype: typing.Optional[str] """ return self._storage @@ -312,10 +328,12 @@ def append_user_agent(self, value: str) -> None: @classmethod def from_connection_string(cls, connection_string: str, **kwargs) -> Workspace: """ - Creates a new Azure Quantum Workspace client from a connection string. + Creates a new Azure Quantum Workspace client from an Azure Quantum + workspace authentication connection string. :param connection_string: - A valid connection string, usually obtained from the + A valid Azure Quantum workspace authentication connection string, + distinct from a storage account connection string and usually obtained from the `Quantum Workspace -> Operations -> Access Keys` blade in the Azure Portal. :return: New Azure Quantum Workspace client. diff --git a/azure-quantum/tests/test_workspace.py b/azure-quantum/tests/test_workspace.py index 4fc0151f..7da33d7a 100644 --- a/azure-quantum/tests/test_workspace.py +++ b/azure-quantum/tests/test_workspace.py @@ -5,10 +5,13 @@ import pytest import os +import warnings from unittest import mock from azure.quantum.job.job import Job from azure.quantum._client.models import JobDetails from azure.quantum import Priority +from azure.quantum import workspace as workspace_module +from azure.quantum.workspace import Workspace from azure.quantum._constants import EnvironmentVariables, ConnectionConstants from azure.core.credentials import AzureKeyCredential from azure.core.pipeline.policies import AzureKeyCredentialPolicy @@ -47,6 +50,8 @@ quantum_endpoint=ConnectionConstants.GET_QUANTUM_PRODUCTION_ENDPOINT_v2(LOCATION) ) +STORAGE_DEPRECATION_WARNING = Workspace._STORAGE_DEPRECATION_MESSAGE + def test_create_workspace_instance_valid(): def assert_all_required_params(ws: WorkspaceMock): @@ -63,12 +68,13 @@ def assert_all_required_params(ws: WorkspaceMock): ) assert_all_required_params(ws) - ws = WorkspaceMock( - subscription_id=SUBSCRIPTION_ID, - resource_group=RESOURCE_GROUP, - name=WORKSPACE, - storage=STORAGE, - ) + with pytest.warns(DeprecationWarning): + ws = WorkspaceMock( + subscription_id=SUBSCRIPTION_ID, + resource_group=RESOURCE_GROUP, + name=WORKSPACE, + storage=STORAGE, + ) assert_all_required_params(ws) assert ws.storage == STORAGE @@ -77,10 +83,11 @@ def assert_all_required_params(ws: WorkspaceMock): ) assert_all_required_params(ws) - ws = WorkspaceMock( - resource_id=SIMPLE_RESOURCE_ID, - storage=STORAGE, - ) + with pytest.warns(DeprecationWarning): + ws = WorkspaceMock( + resource_id=SIMPLE_RESOURCE_ID, + storage=STORAGE, + ) assert_all_required_params(ws) assert ws.storage == STORAGE @@ -89,10 +96,11 @@ def assert_all_required_params(ws: WorkspaceMock): ) assert_all_required_params(ws) - ws = WorkspaceMock( - name=WORKSPACE, - storage=STORAGE, - ) + with pytest.warns(DeprecationWarning): + ws = WorkspaceMock( + name=WORKSPACE, + storage=STORAGE, + ) assert_all_required_params(ws) assert ws.storage == STORAGE @@ -129,6 +137,43 @@ def assert_all_required_params(ws: WorkspaceMock): assert_all_required_params(ws) +def test_workspace_storage_parameter_is_deprecated(): + with pytest.warns(DeprecationWarning) as warning_info: + workspace = WorkspaceMock( + subscription_id=SUBSCRIPTION_ID, + resource_group=RESOURCE_GROUP, + name=WORKSPACE, + storage=STORAGE, + ) + + assert str(warning_info[0].message) == STORAGE_DEPRECATION_WARNING + # stacklevel=2 must attribute the warning to the caller, not to workspace.py itself. + assert warning_info[0].filename != workspace_module.__file__ + assert workspace.storage == STORAGE + + +@pytest.mark.parametrize("workspace_kwargs", [{}, {"storage": None}]) +def test_workspace_without_storage_does_not_warn(workspace_kwargs): + with warnings.catch_warnings(): + warnings.simplefilter("error", DeprecationWarning) + workspace = WorkspaceMock( + subscription_id=SUBSCRIPTION_ID, + resource_group=RESOURCE_GROUP, + name=WORKSPACE, + **workspace_kwargs, + ) + + assert workspace.storage is None + + +def test_workspace_from_connection_string_does_not_warn_about_storage(): + with mock.patch.dict(os.environ, clear=True), warnings.catch_warnings(): + warnings.simplefilter("error", DeprecationWarning) + workspace = WorkspaceMock.from_connection_string(SIMPLE_CONNECTION_STRING) + + assert workspace.name == WORKSPACE + + def test_create_workspace_locations(): # Location name should be normalized _mgmt_client = MockWorkspaceMgmtClient() @@ -714,3 +759,23 @@ def test_get_container_uri_uses_linked_storage_sas_when_storage_none(): assert isinstance(uri, str) assert "https://example.com/" in uri assert "sas-token" in uri + + +def test_get_container_uri_uses_explicit_storage_connection_string(): + with pytest.warns(DeprecationWarning): + ws = WorkspaceMock( + subscription_id=SUBSCRIPTION_ID, + resource_group=RESOURCE_GROUP, + name=WORKSPACE, + storage=STORAGE, + ) + + with mock.patch( + "azure.quantum.workspace.get_container_uri", + return_value="https://example.com/container?sas-token", + ) as mock_get_container_uri: + # WorkspaceMock overrides get_container_uri for offline tests; call the real implementation directly. + uri = Workspace.get_container_uri(ws, job_id="job-123") + + mock_get_container_uri.assert_called_once_with(STORAGE, "job-job-123") + assert uri == "https://example.com/container?sas-token" From 52b361b43bb24c8e39327d59ce9a97736fada074 Mon Sep 17 00:00:00 2001 From: "Ekaterina Legacheva (AKVELON INC)" Date: Tue, 29 Sep 2026 12:25:30 -0700 Subject: [PATCH 2/3] Use literal expected text for storage deprecation warning test Compare against a literal string instead of Workspace._STORAGE_DEPRECATION_MESSAGE so the assertion can actually detect a regression in the warning wording (addresses PR #774 review comment). --- azure-quantum/tests/test_workspace.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/azure-quantum/tests/test_workspace.py b/azure-quantum/tests/test_workspace.py index 7da33d7a..ff9e7663 100644 --- a/azure-quantum/tests/test_workspace.py +++ b/azure-quantum/tests/test_workspace.py @@ -50,7 +50,10 @@ quantum_endpoint=ConnectionConstants.GET_QUANTUM_PRODUCTION_ENDPOINT_v2(LOCATION) ) -STORAGE_DEPRECATION_WARNING = Workspace._STORAGE_DEPRECATION_MESSAGE +STORAGE_DEPRECATION_WARNING = ( + "Workspace(storage=...) is deprecated and will be removed in " + "azure-quantum 4.0.0. Configure workspace-linked storage instead." +) def test_create_workspace_instance_valid(): From c9b71bdd923f01622d8bc615c8b4217dc951e8e0 Mon Sep 17 00:00:00 2001 From: "Ekaterina Legacheva (AKVELON INC)" Date: Tue, 29 Sep 2026 13:23:19 -0700 Subject: [PATCH 3/3] Verify exact caller for storage deprecation warning Assert both the source file and line of WorkspaceMock.__init__'s super call so the test precisely verifies stacklevel=2 attribution. --- azure-quantum/tests/test_workspace.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/azure-quantum/tests/test_workspace.py b/azure-quantum/tests/test_workspace.py index ff9e7663..cff17f44 100644 --- a/azure-quantum/tests/test_workspace.py +++ b/azure-quantum/tests/test_workspace.py @@ -4,13 +4,13 @@ ## import pytest +import inspect import os import warnings from unittest import mock from azure.quantum.job.job import Job from azure.quantum._client.models import JobDetails from azure.quantum import Priority -from azure.quantum import workspace as workspace_module from azure.quantum.workspace import Workspace from azure.quantum._constants import EnvironmentVariables, ConnectionConstants from azure.core.credentials import AzureKeyCredential @@ -150,8 +150,13 @@ def test_workspace_storage_parameter_is_deprecated(): ) assert str(warning_info[0].message) == STORAGE_DEPRECATION_WARNING - # stacklevel=2 must attribute the warning to the caller, not to workspace.py itself. - assert warning_info[0].filename != workspace_module.__file__ + source_lines, start_line = inspect.getsourcelines(WorkspaceMock.__init__) + caller_offset = next( + index for index, line in enumerate(source_lines) + if "super().__init__" in line + ) + assert warning_info[0].filename == inspect.getsourcefile(WorkspaceMock.__init__) + assert warning_info[0].lineno == start_line + caller_offset assert workspace.storage == STORAGE