From 1fbfcd21b78c1fba8c364eb6c4e26c7148274014 Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Fri, 5 Dec 2025 11:26:15 -0600 Subject: [PATCH 1/6] change decorators & converters based on durable package --- azure/functions/__init__.py | 4 + .../functions/decorators/durable_functions.py | 62 +++++ azure/functions/decorators/function_app.py | 18 +- azure/functions/durable_functions.py | 234 +++++++++++++++++- 4 files changed, 301 insertions(+), 17 deletions(-) create mode 100644 azure/functions/decorators/durable_functions.py diff --git a/azure/functions/__init__.py b/azure/functions/__init__.py index 99ebc0f9..feb69f70 100644 --- a/azure/functions/__init__.py +++ b/azure/functions/__init__.py @@ -12,6 +12,7 @@ AsgiFunctionApp, WsgiFunctionApp, ExternalHttpFunctionApp, BlobSource, McpPropertyType) from ._durable_functions import OrchestrationContext, EntityContext +from .durable_functions import register_durable_converters from .decorators.function_app import (FunctionRegister, TriggerApi, BindingApi, SettingsApi) from .extension import (ExtensionMeta, FunctionExtensionException, @@ -43,6 +44,9 @@ from . import mysql # NoQA +register_durable_converters() + + __all__ = ( # Functions 'get_binding_registry', diff --git a/azure/functions/decorators/durable_functions.py b/azure/functions/decorators/durable_functions.py new file mode 100644 index 00000000..dae2611e --- /dev/null +++ b/azure/functions/decorators/durable_functions.py @@ -0,0 +1,62 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import logging + +_logger = logging.getLogger('azure.functions.AsgiMiddleware') + +df = None + + +def get_durable_package(): + """Determines which Durable SDK is being used. + + If the `azure-functions-durable` package is installed, we + log a warning that this legacy package + is deprecated. + + If both the legacy and current packages are installed, + we log a warning and prefer the current package. + + If neither package is installed, we raise an exception. + """ + _logger.info("Attempting to import Durable Functions package.") + using_legacy = False + using_durable_task = False + global df + if df: + _logger.info("Durable Functions package already loaded. DF: %s", df) + return df + + try: + import azure.durable_functions as durable_functions + using_legacy = True + _logger.warning("`azure-functions-durable` is deprecated. " \ + "Please migrate to the new `durabletask-azurefunctions` package. " \ + "See for more details.") + except ImportError: + _logger.info("`azure-functions-durable` package not found.") + pass + try: + import durabletask.azurefunctions as durable_functions + using_durable_task = True + except ImportError: + _logger.info("`durabletask-azurefunctions` package not found.") + pass + + if using_durable_task and using_legacy: + # Both packages are installed; prefer `durabletask-azurefunctions`. + _logger.warning("Both `azure-functions-durable` and " \ + "`durabletask-azurefunctions` packages are installed. " \ + "The `durabletask-azurefunctions` package will be used.") + + if not using_durable_task and not using_legacy: + error_message = \ + "Attempted to use a Durable Functions decorator, " \ + "but the `durabletask-azurefunctions` SDK package could not be " \ + "found. Please install `durabletask-azurefunctions` to use " \ + "Durable Functions." + raise Exception(error_message) + + df = durable_functions + + return durable_functions diff --git a/azure/functions/decorators/function_app.py b/azure/functions/decorators/function_app.py index 16482aa3..569cfa00 100644 --- a/azure/functions/decorators/function_app.py +++ b/azure/functions/decorators/function_app.py @@ -24,6 +24,7 @@ DaprBindingTrigger, DaprInvokeOutput, DaprPublishOutput, \ DaprSecretInput, DaprServiceInvocationTrigger, DaprStateInput, \ DaprStateOutput, DaprTopicTrigger +from azure.functions.decorators.durable_functions import get_durable_package from azure.functions.decorators.eventgrid import EventGridTrigger, \ EventGridOutput from azure.functions.decorators.eventhub import EventHubTrigger, EventHubOutput @@ -57,6 +58,7 @@ from azure.functions.decorators.mysql import MySqlInput, MySqlOutput, \ MySqlTrigger +_logger = logging.getLogger('azure.functions.AsgiMiddleware') class Function(object): """ @@ -347,17 +349,11 @@ def _get_durable_blueprint(self): """Attempt to import the Durable Functions SDK from which DF decorators are implemented. """ - try: - import azure.durable_functions as df - df_bp = df.Blueprint() - return df_bp - except ImportError: - error_message = \ - "Attempted to use a Durable Functions decorator, " \ - "but the `azure-functions-durable` SDK package could not be " \ - "found. Please install `azure-functions-durable` to use " \ - "Durable Functions." - raise Exception(error_message) + _logger.info("Getting Durable Functions blueprint.") + df = get_durable_package() + df_bp = df.Blueprint() + return df_bp + @property def app_script_file(self) -> str: diff --git a/azure/functions/durable_functions.py b/azure/functions/durable_functions.py index 0ad861ca..4c298a9b 100644 --- a/azure/functions/durable_functions.py +++ b/azure/functions/durable_functions.py @@ -5,13 +5,17 @@ import json from azure.functions import _durable_functions +from azure.functions.decorators.durable_functions import get_durable_package from . import meta +import logging +_logger = logging.getLogger('azure.functions.AsgiMiddleware') -# Durable Function Orchestration Trigger -class OrchestrationTriggerConverter(meta.InConverter, +# ---------------- Legacy Durable Functions Converters ---------------- # +# Legacy Durable Function Orchestration Trigger +class LegacyOrchestrationTriggerConverter(meta.InConverter, meta.OutConverter, - binding='orchestrationTrigger', + binding=None, trigger=True): @classmethod def check_input_type_annotation(cls, pytype): @@ -39,9 +43,196 @@ def has_implicit_output(cls) -> bool: return True +# Legacy Durable Function Entity Trigger +class LegacyEnitityTriggerConverter(meta.InConverter, + meta.OutConverter, + binding=None, + trigger=True): + @classmethod + def check_input_type_annotation(cls, pytype): + return issubclass(pytype, _durable_functions.EntityContext) + + @classmethod + def check_output_type_annotation(cls, pytype): + # Implicit output should accept any return type + return True + + @classmethod + def decode(cls, + data: meta.Datum, *, + trigger_metadata) -> _durable_functions.EntityContext: + return _durable_functions.EntityContext(data.value) + + @classmethod + def encode(cls, obj: typing.Any, *, + expected_type: typing.Optional[type]) -> meta.Datum: + # Durable function context should be a json + return meta.Datum(type='json', value=obj) + + @classmethod + def has_implicit_output(cls) -> bool: + return True + + +# Legacy Durable Function Activity Trigger +class LegacyActivityTriggerConverter(meta.InConverter, + meta.OutConverter, + binding=None, + trigger=True): + @classmethod + def check_input_type_annotation(cls, pytype): + # Activity Trigger's arguments should accept any types + return True + + @classmethod + def check_output_type_annotation(cls, pytype): + # The activity trigger should accept any JSON serializable types + return True + + @classmethod + def decode(cls, + data: meta.Datum, *, + trigger_metadata) -> typing.Any: + data_type = data.type + + # Durable functions extension always returns a string of json + # See durable functions library's call_activity_task docs + if data_type in ['string', 'json']: + try: + callback = _durable_functions._deserialize_custom_object + result = json.loads(data.value, object_hook=callback) + except json.JSONDecodeError: + # String failover if the content is not json serializable + result = data.value + except Exception as e: + raise ValueError( + 'activity trigger input must be a string or a ' + f'valid json serializable ({data.value})') from e + else: + raise NotImplementedError( + f'unsupported activity trigger payload type: {data_type}') + + return result + + @classmethod + def encode(cls, obj: typing.Any, *, + expected_type: typing.Optional[type]) -> meta.Datum: + try: + callback = _durable_functions._serialize_custom_object + result = json.dumps(obj, default=callback) + except TypeError as e: + raise ValueError( + f'activity trigger output must be json serializable ({obj})') from e + + return meta.Datum(type='json', value=result) + + @classmethod + def has_implicit_output(cls) -> bool: + return True + + +# Legacy Durable Functions Durable Client Bindings +class LegacyDurableClientConverter(meta.InConverter, + meta.OutConverter, + binding=None): + @classmethod + def has_implicit_output(cls) -> bool: + return False + + @classmethod + def has_trigger_support(cls) -> bool: + return False + + @classmethod + def check_input_type_annotation(cls, pytype: type) -> bool: + return issubclass(pytype, (str, bytes)) + + @classmethod + def check_output_type_annotation(cls, pytype: type) -> bool: + return issubclass(pytype, (str, bytes, bytearray)) + + @classmethod + def encode(cls, obj: typing.Any, *, + expected_type: typing.Optional[type]) -> meta.Datum: + if isinstance(obj, str): + return meta.Datum(type='string', value=obj) + + elif isinstance(obj, (bytes, bytearray)): + return meta.Datum(type='bytes', value=bytes(obj)) + elif obj is None: + return meta.Datum(type=None, value=obj) + elif isinstance(obj, dict): + return meta.Datum(type='dict', value=obj) + elif isinstance(obj, list): + return meta.Datum(type='list', value=obj) + elif isinstance(obj, bool): + return meta.Datum(type='bool', value=obj) + elif isinstance(obj, int): + return meta.Datum(type='int', value=obj) + elif isinstance(obj, float): + return meta.Datum(type='double', value=obj) + else: + raise NotImplementedError + + @classmethod + def decode(cls, data: meta.Datum, *, trigger_metadata) -> typing.Any: + if data is None: + return None + data_type = data.type + + if data_type == 'string': + result = data.value + elif data_type == 'bytes': + result = data.value + elif data_type == 'json': + result = data.value + elif data_type is None: + result = None + else: + raise ValueError( + 'unexpected type of data received for the "generic" binding ', + repr(data_type) + ) + + return result + + +# ---------------- Durable Task Durable Functions Converters ---------------- # +# Durable Function Orchestration Trigger +class OrchestrationTriggerConverter(meta.InConverter, + meta.OutConverter, + binding=None, + trigger=True): + @classmethod + def check_input_type_annotation(cls, pytype): + return issubclass(pytype, _durable_functions.OrchestrationContext) + + @classmethod + def check_output_type_annotation(cls, pytype): + # Implicit output should accept any return type + return True + + @classmethod + def decode(cls, + data: meta.Datum, *, + trigger_metadata) -> _durable_functions.OrchestrationContext: + return _durable_functions.OrchestrationContext(data.value) + + @classmethod + def encode(cls, obj: typing.Any, *, + expected_type: typing.Optional[type]) -> meta.Datum: + # Durable function context should be a string + return meta.Datum(type='string', value=obj) + + @classmethod + def has_implicit_output(cls) -> bool: + return True + + +# Durable Function Entity Trigger class EnitityTriggerConverter(meta.InConverter, meta.OutConverter, - binding='entityTrigger', + binding=None, trigger=True): @classmethod def check_input_type_annotation(cls, pytype): @@ -72,7 +263,7 @@ def has_implicit_output(cls) -> bool: # Durable Function Activity Trigger class ActivityTriggerConverter(meta.InConverter, meta.OutConverter, - binding='activityTrigger', + binding=None, trigger=True): @classmethod def check_input_type_annotation(cls, pytype): @@ -129,7 +320,7 @@ def has_implicit_output(cls) -> bool: # Durable Functions Durable Client Bindings class DurableClientConverter(meta.InConverter, meta.OutConverter, - binding='durableClient'): + binding=None): @classmethod def has_implicit_output(cls) -> bool: return False @@ -190,3 +381,34 @@ def decode(cls, data: meta.Datum, *, trigger_metadata) -> typing.Any: ) return result + + +def register_durable_converters(): + _logger.info("Registering Durable Functions converters based on ") + pkg = get_durable_package() + if pkg is None: + # Durable library not installed → do nothing + return + + _logger.info("Durable Functions package loaded: %s", pkg.__name__) + _logger.info("Current bindings before registration: %s", meta._ConverterMeta._bindings) + # Clear existing bindings if they exist + meta._ConverterMeta._bindings.pop("orchestrationTrigger", None) + meta._ConverterMeta._bindings.pop("entityTrigger", None) + meta._ConverterMeta._bindings.pop("activityTrigger", None) + meta._ConverterMeta._bindings.pop("durableClient", None) + + if pkg.__name__ == "azure.durable_functions": + _logger.info("Registering Legacy Durable Functions converters.") + meta._ConverterMeta._bindings["orchestrationTrigger"] = LegacyOrchestrationTriggerConverter + meta._ConverterMeta._bindings["entityTrigger"] = LegacyEnitityTriggerConverter + meta._ConverterMeta._bindings["activityTrigger"] = LegacyActivityTriggerConverter + meta._ConverterMeta._bindings["durableClient"] = LegacyDurableClientConverter + else: + _logger.info("Registering Durable Task Durable Functions converters.") + meta._ConverterMeta._bindings["orchestrationTrigger"] = OrchestrationTriggerConverter + meta._ConverterMeta._bindings["entityTrigger"] = EnitityTriggerConverter + meta._ConverterMeta._bindings["activityTrigger"] = ActivityTriggerConverter + meta._ConverterMeta._bindings["durableClient"] = DurableClientConverter + _logger.info("Durable Functions converters registered.") + _logger.info("Current bindings after registration: %s", meta._ConverterMeta._bindings) From d4f0cb8a32b89acdd758592596f7770369e14bea Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Fri, 5 Dec 2025 11:40:07 -0600 Subject: [PATCH 2/6] return None --- azure/functions/decorators/durable_functions.py | 9 ++------- azure/functions/decorators/function_app.py | 12 ++++++++++-- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/azure/functions/decorators/durable_functions.py b/azure/functions/decorators/durable_functions.py index dae2611e..1777244c 100644 --- a/azure/functions/decorators/durable_functions.py +++ b/azure/functions/decorators/durable_functions.py @@ -17,7 +17,7 @@ def get_durable_package(): If both the legacy and current packages are installed, we log a warning and prefer the current package. - If neither package is installed, we raise an exception. + If neither package is installed, we return None. """ _logger.info("Attempting to import Durable Functions package.") using_legacy = False @@ -50,12 +50,7 @@ def get_durable_package(): "The `durabletask-azurefunctions` package will be used.") if not using_durable_task and not using_legacy: - error_message = \ - "Attempted to use a Durable Functions decorator, " \ - "but the `durabletask-azurefunctions` SDK package could not be " \ - "found. Please install `durabletask-azurefunctions` to use " \ - "Durable Functions." - raise Exception(error_message) + return None df = durable_functions diff --git a/azure/functions/decorators/function_app.py b/azure/functions/decorators/function_app.py index 569cfa00..ea16354e 100644 --- a/azure/functions/decorators/function_app.py +++ b/azure/functions/decorators/function_app.py @@ -351,8 +351,16 @@ def _get_durable_blueprint(self): """ _logger.info("Getting Durable Functions blueprint.") df = get_durable_package() - df_bp = df.Blueprint() - return df_bp + if df: + df_bp = df.Blueprint() + return df_bp + else: + error_message = \ + "Attempted to use a Durable Functions decorator, " \ + "but the `durabletask-azurefunctions` SDK package could not be " \ + "found. Please install `durabletask-azurefunctions` to use " \ + "Durable Functions." + raise Exception(error_message) @property From 7f3269819864b026e421a423bc13cc65d366ba06 Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Fri, 5 Dec 2025 15:57:03 -0600 Subject: [PATCH 3/6] update logs + remove extra line --- azure/functions/decorators/durable_functions.py | 8 +++++--- azure/functions/decorators/function_app.py | 1 - azure/functions/durable_functions.py | 5 +++++ 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/azure/functions/decorators/durable_functions.py b/azure/functions/decorators/durable_functions.py index 1777244c..1e29a381 100644 --- a/azure/functions/decorators/durable_functions.py +++ b/azure/functions/decorators/durable_functions.py @@ -30,8 +30,7 @@ def get_durable_package(): try: import azure.durable_functions as durable_functions using_legacy = True - _logger.warning("`azure-functions-durable` is deprecated. " \ - "Please migrate to the new `durabletask-azurefunctions` package. " \ + _logger.warning("`durabletask-azurefunctions` is available now! " \ "See for more details.") except ImportError: _logger.info("`azure-functions-durable` package not found.") @@ -47,7 +46,10 @@ def get_durable_package(): # Both packages are installed; prefer `durabletask-azurefunctions`. _logger.warning("Both `azure-functions-durable` and " \ "`durabletask-azurefunctions` packages are installed. " \ - "The `durabletask-azurefunctions` package will be used.") + "This may lead to unexpected behavior. Please resolve this " \ + "conflict by removing one of these packages from the Python " \ + "environment. Decorators from `durabletask-azurefunctions` will " \ + "be used.") if not using_durable_task and not using_legacy: return None diff --git a/azure/functions/decorators/function_app.py b/azure/functions/decorators/function_app.py index ea16354e..e5ddb07e 100644 --- a/azure/functions/decorators/function_app.py +++ b/azure/functions/decorators/function_app.py @@ -362,7 +362,6 @@ def _get_durable_blueprint(self): "Durable Functions." raise Exception(error_message) - @property def app_script_file(self) -> str: """Name of function app script file in which all the functions diff --git a/azure/functions/durable_functions.py b/azure/functions/durable_functions.py index 4c298a9b..fac82bf2 100644 --- a/azure/functions/durable_functions.py +++ b/azure/functions/durable_functions.py @@ -331,6 +331,11 @@ def has_trigger_support(cls) -> bool: @classmethod def check_input_type_annotation(cls, pytype: type) -> bool: + try: + import azure.durable_functions as adf + return issubclass(pytype, (str, bytes, adf.DurableFunctionsClient)) + except ImportError: + pass return issubclass(pytype, (str, bytes)) @classmethod From 6a7822167129cf9e7a901b45017f4aed5f480ebf Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Mon, 8 Dec 2025 15:59:04 -0600 Subject: [PATCH 4/6] durable client converter changes --- azure/functions/durable_functions.py | 33 +++++----------------------- 1 file changed, 6 insertions(+), 27 deletions(-) diff --git a/azure/functions/durable_functions.py b/azure/functions/durable_functions.py index fac82bf2..314be47e 100644 --- a/azure/functions/durable_functions.py +++ b/azure/functions/durable_functions.py @@ -252,8 +252,8 @@ def decode(cls, @classmethod def encode(cls, obj: typing.Any, *, expected_type: typing.Optional[type]) -> meta.Datum: - # Durable function context should be a json - return meta.Datum(type='json', value=obj) + # Durable function context should be a string + return meta.Datum(type='string', value=obj) @classmethod def has_implicit_output(cls) -> bool: @@ -331,12 +331,8 @@ def has_trigger_support(cls) -> bool: @classmethod def check_input_type_annotation(cls, pytype: type) -> bool: - try: - import azure.durable_functions as adf - return issubclass(pytype, (str, bytes, adf.DurableFunctionsClient)) - except ImportError: - pass - return issubclass(pytype, (str, bytes)) + import durabletask.azurefunctions as adf + return issubclass(pytype, (str, bytes, adf.DurableFunctionsClient)) @classmethod def check_output_type_annotation(cls, pytype: type) -> bool: @@ -367,25 +363,8 @@ def encode(cls, obj: typing.Any, *, @classmethod def decode(cls, data: meta.Datum, *, trigger_metadata) -> typing.Any: - if data is None: - return None - data_type = data.type - - if data_type == 'string': - result = data.value - elif data_type == 'bytes': - result = data.value - elif data_type == 'json': - result = data.value - elif data_type is None: - result = None - else: - raise ValueError( - 'unexpected type of data received for the "generic" binding ', - repr(data_type) - ) - - return result + from durabletask.azurefunctions.client import DurableFunctionsClient + return DurableFunctionsClient(data.value) def register_durable_converters(): From 3fe8ff19403c88b7085d7fa33e398fc2215482c8 Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Tue, 9 Dec 2025 11:39:10 -0600 Subject: [PATCH 5/6] lint + use DF logger --- azure/functions/__init__.py | 2 +- .../functions/decorators/durable_functions.py | 22 +++++++------- azure/functions/decorators/function_app.py | 3 +- azure/functions/durable_functions.py | 29 +++++++++++-------- 4 files changed, 31 insertions(+), 25 deletions(-) diff --git a/azure/functions/__init__.py b/azure/functions/__init__.py index feb69f70..208793a9 100644 --- a/azure/functions/__init__.py +++ b/azure/functions/__init__.py @@ -110,4 +110,4 @@ 'McpPropertyType' ) -__version__ = '1.25.0b2' +__version__ = '1.25.0b3dev1' diff --git a/azure/functions/decorators/durable_functions.py b/azure/functions/decorators/durable_functions.py index 1e29a381..dc0002cc 100644 --- a/azure/functions/decorators/durable_functions.py +++ b/azure/functions/decorators/durable_functions.py @@ -17,7 +17,7 @@ def get_durable_package(): If both the legacy and current packages are installed, we log a warning and prefer the current package. - If neither package is installed, we return None. + If neither package is installed, we return None. """ _logger.info("Attempting to import Durable Functions package.") using_legacy = False @@ -30,13 +30,13 @@ def get_durable_package(): try: import azure.durable_functions as durable_functions using_legacy = True - _logger.warning("`durabletask-azurefunctions` is available now! " \ - "See for more details.") + _logger.warning("`durabletask-azurefunctions` is available now! " + "See for more details.") except ImportError: _logger.info("`azure-functions-durable` package not found.") pass try: - import durabletask.azurefunctions as durable_functions + import durabletask.azurefunctions as durable_functions # noqa using_durable_task = True except ImportError: _logger.info("`durabletask-azurefunctions` package not found.") @@ -44,13 +44,13 @@ def get_durable_package(): if using_durable_task and using_legacy: # Both packages are installed; prefer `durabletask-azurefunctions`. - _logger.warning("Both `azure-functions-durable` and " \ - "`durabletask-azurefunctions` packages are installed. " \ - "This may lead to unexpected behavior. Please resolve this " \ - "conflict by removing one of these packages from the Python " \ - "environment. Decorators from `durabletask-azurefunctions` will " \ - "be used.") - + _logger.warning("Both `azure-functions-durable` and " + "`durabletask-azurefunctions` packages are installed. " + "This may lead to unexpected behavior. Please resolve this " + "conflict by removing one of these packages from the Python " + "environment. Decorators from `durabletask-azurefunctions` will " + "be used.") + if not using_durable_task and not using_legacy: return None diff --git a/azure/functions/decorators/function_app.py b/azure/functions/decorators/function_app.py index e5ddb07e..9f7fef36 100644 --- a/azure/functions/decorators/function_app.py +++ b/azure/functions/decorators/function_app.py @@ -58,7 +58,8 @@ from azure.functions.decorators.mysql import MySqlInput, MySqlOutput, \ MySqlTrigger -_logger = logging.getLogger('azure.functions.AsgiMiddleware') +_logger = logging.getLogger('azure.functions.DurableFunctions') + class Function(object): """ diff --git a/azure/functions/durable_functions.py b/azure/functions/durable_functions.py index 314be47e..4bedfc38 100644 --- a/azure/functions/durable_functions.py +++ b/azure/functions/durable_functions.py @@ -9,14 +9,15 @@ from . import meta import logging -_logger = logging.getLogger('azure.functions.AsgiMiddleware') +_logger = logging.getLogger('azure.functions.DurableFunctions') + # ---------------- Legacy Durable Functions Converters ---------------- # # Legacy Durable Function Orchestration Trigger class LegacyOrchestrationTriggerConverter(meta.InConverter, - meta.OutConverter, - binding=None, - trigger=True): + meta.OutConverter, + binding=None, + trigger=True): @classmethod def check_input_type_annotation(cls, pytype): return issubclass(pytype, _durable_functions.OrchestrationContext) @@ -45,9 +46,9 @@ def has_implicit_output(cls) -> bool: # Legacy Durable Function Entity Trigger class LegacyEnitityTriggerConverter(meta.InConverter, - meta.OutConverter, - binding=None, - trigger=True): + meta.OutConverter, + binding=None, + trigger=True): @classmethod def check_input_type_annotation(cls, pytype): return issubclass(pytype, _durable_functions.EntityContext) @@ -76,9 +77,9 @@ def has_implicit_output(cls) -> bool: # Legacy Durable Function Activity Trigger class LegacyActivityTriggerConverter(meta.InConverter, - meta.OutConverter, - binding=None, - trigger=True): + meta.OutConverter, + binding=None, + trigger=True): @classmethod def check_input_type_annotation(cls, pytype): # Activity Trigger's arguments should accept any types @@ -133,8 +134,8 @@ def has_implicit_output(cls) -> bool: # Legacy Durable Functions Durable Client Bindings class LegacyDurableClientConverter(meta.InConverter, - meta.OutConverter, - binding=None): + meta.OutConverter, + binding=None): @classmethod def has_implicit_output(cls) -> bool: return False @@ -368,6 +369,10 @@ def decode(cls, data: meta.Datum, *, trigger_metadata) -> typing.Any: def register_durable_converters(): + """ + Registers the appropriate Durable Functions converters based on the + installed Durable Functions package. + """ _logger.info("Registering Durable Functions converters based on ") pkg = get_durable_package() if pkg is None: From dcbaeb27afe7de0e3ddad0763d707acc88064253 Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Tue, 9 Dec 2025 15:06:52 -0600 Subject: [PATCH 6/6] fix durable tests --- azure/functions/__init__.py | 2 +- .../functions/decorators/durable_functions.py | 2 +- tests/test_durable_functions.py | 76 +++++++++---------- 3 files changed, 40 insertions(+), 40 deletions(-) diff --git a/azure/functions/__init__.py b/azure/functions/__init__.py index 208793a9..fedb7b7e 100644 --- a/azure/functions/__init__.py +++ b/azure/functions/__init__.py @@ -110,4 +110,4 @@ 'McpPropertyType' ) -__version__ = '1.25.0b3dev1' +__version__ = '1.25.0b3.dev1' diff --git a/azure/functions/decorators/durable_functions.py b/azure/functions/decorators/durable_functions.py index dc0002cc..aec99461 100644 --- a/azure/functions/decorators/durable_functions.py +++ b/azure/functions/decorators/durable_functions.py @@ -2,7 +2,7 @@ # Licensed under the MIT License. import logging -_logger = logging.getLogger('azure.functions.AsgiMiddleware') +_logger = logging.getLogger('azure.functions.DurableFunctions') df = None diff --git a/tests/test_durable_functions.py b/tests/test_durable_functions.py index 043b373d..1f319d61 100644 --- a/tests/test_durable_functions.py +++ b/tests/test_durable_functions.py @@ -5,10 +5,10 @@ import json from azure.functions.durable_functions import ( - OrchestrationTriggerConverter, - EnitityTriggerConverter, - ActivityTriggerConverter, - DurableClientConverter + LegacyOrchestrationTriggerConverter, + LegacyEnitityTriggerConverter, + LegacyActivityTriggerConverter, + LegacyDurableClientConverter ) from azure.functions._durable_functions import ( OrchestrationContext, @@ -17,7 +17,7 @@ from azure.functions.meta import Datum CONTEXT_CLASSES = [OrchestrationContext, EntityContext] -CONVERTERS = [OrchestrationTriggerConverter, EnitityTriggerConverter] +CONVERTERS = [LegacyOrchestrationTriggerConverter, LegacyEnitityTriggerConverter] class TestDurableFunctions(unittest.TestCase): @@ -127,7 +127,7 @@ def test_activity_trigger_inputs(self): ] for datum in data: - decoded = ActivityTriggerConverter.decode( + decoded = LegacyActivityTriggerConverter.decode( data=datum['input'], trigger_metadata=None) self.assertEqual(decoded, datum['expected_value']) @@ -160,7 +160,7 @@ def test_activity_trigger_encode(self): ] for datum in data: - encoded = ActivityTriggerConverter.encode( + encoded = LegacyActivityTriggerConverter.encode( obj=datum['output'], expected_type=type(datum['output'])) self.assertEqual(encoded, datum['expected_value']) @@ -173,7 +173,7 @@ def __init__(self): data = NonEncodable() try: - ActivityTriggerConverter.encode(data, expected_type=None) + LegacyActivityTriggerConverter.encode(data, expected_type=None) except ValueError as e: self.assertIsNotNone(e.__cause__) self.assertIsInstance(e.__cause__, TypeError) @@ -217,7 +217,7 @@ def test_activity_trigger_decode(self): ] for datum in data: - decoded = ActivityTriggerConverter.decode( + decoded = LegacyActivityTriggerConverter.decode( data=datum['input'], trigger_metadata=None) self.assertEqual(decoded, datum['expected_value']) @@ -226,7 +226,7 @@ def test_activity_trigger_decode_failure_exception_has_cause(self): data = Datum('{"value": "bar"}', 'json') try: - ActivityTriggerConverter.decode( + LegacyActivityTriggerConverter.decode( data=data, trigger_metadata=None) except ValueError as e: @@ -235,17 +235,17 @@ def test_activity_trigger_decode_failure_exception_has_cause(self): def test_activity_trigger_has_implicit_return(self): self.assertTrue( - ActivityTriggerConverter.has_implicit_output() + LegacyActivityTriggerConverter.has_implicit_output() ) def test_durable_client_no_implicit_return(self): self.assertFalse( - DurableClientConverter.has_implicit_output() + LegacyDurableClientConverter.has_implicit_output() ) def test_enitity_trigger_check_output_type_annotation(self): self.assertTrue( - EnitityTriggerConverter.check_output_type_annotation(pytype=None) + LegacyEnitityTriggerConverter.check_output_type_annotation(pytype=None) ) def test_activity_trigger_converter_decode_no_implementation_exception( @@ -254,7 +254,7 @@ def test_activity_trigger_converter_decode_no_implementation_exception( datum = Datum(value=b"dummy", type="bytes") # when try: - ActivityTriggerConverter.decode(data=datum, trigger_metadata=None) + LegacyActivityTriggerConverter.decode(data=datum, trigger_metadata=None) except NotImplementedError: is_exception_raised = True @@ -265,82 +265,82 @@ def test_enitity_trigger_converter_encode(self): data = '{"dummy_key": "dummy_value"}' - result = EnitityTriggerConverter.encode( + result = LegacyEnitityTriggerConverter.encode( obj=data, expected_type=None) self.assertEqual(result.type, "json") self.assertEqual(result.python_value, {'dummy_key': 'dummy_value'}) def test_durable_client_converter_has_trigger_support(self): - self.assertFalse(DurableClientConverter.has_trigger_support()) + self.assertFalse(LegacyDurableClientConverter.has_trigger_support()) def test_durable_client_converter_check_input_type_annotation(self): - self.assertTrue(DurableClientConverter.check_input_type_annotation(str)) - self.assertTrue(DurableClientConverter.check_input_type_annotation(bytes)) - self.assertFalse(DurableClientConverter.check_input_type_annotation(int)) + self.assertTrue(LegacyDurableClientConverter.check_input_type_annotation(str)) + self.assertTrue(LegacyDurableClientConverter.check_input_type_annotation(bytes)) + self.assertFalse(LegacyDurableClientConverter.check_input_type_annotation(int)) def test_durable_client_converter_check_output_type_annotation(self): - self.assertTrue(DurableClientConverter.check_output_type_annotation(str)) - self.assertTrue(DurableClientConverter.check_output_type_annotation(bytes)) - self.assertTrue(DurableClientConverter.check_output_type_annotation(bytearray)) - self.assertFalse(DurableClientConverter.check_output_type_annotation(int)) + self.assertTrue(LegacyDurableClientConverter.check_output_type_annotation(str)) + self.assertTrue(LegacyDurableClientConverter.check_output_type_annotation(bytes)) + self.assertTrue(LegacyDurableClientConverter.check_output_type_annotation(bytearray)) + self.assertFalse(LegacyDurableClientConverter.check_output_type_annotation(int)) def test_durable_client_converter_encode(self): - datum = DurableClientConverter.encode(obj="hello", expected_type=str) + datum = LegacyDurableClientConverter.encode(obj="hello", expected_type=str) self.assertEqual(datum.type, "string") self.assertEqual(datum.value, "hello") - datum = DurableClientConverter.encode(obj=b"data", expected_type=bytes) + datum = LegacyDurableClientConverter.encode(obj=b"data", expected_type=bytes) self.assertEqual(datum.type, "bytes") self.assertEqual(datum.value, b"data") - datum = DurableClientConverter.encode(obj=None, expected_type=None) + datum = LegacyDurableClientConverter.encode(obj=None, expected_type=None) self.assertIsNone(datum.type) self.assertIsNone(datum.value) - datum = DurableClientConverter.encode(obj={"a": 1}, expected_type=dict) + datum = LegacyDurableClientConverter.encode(obj={"a": 1}, expected_type=dict) self.assertEqual(datum.type, "dict") self.assertEqual(datum.value, {"a": 1}) - datum = DurableClientConverter.encode(obj=[1, 2], expected_type=list) + datum = LegacyDurableClientConverter.encode(obj=[1, 2], expected_type=list) self.assertEqual(datum.type, "list") self.assertEqual(datum.value, [1, 2]) - datum = DurableClientConverter.encode(obj=42, expected_type=int) + datum = LegacyDurableClientConverter.encode(obj=42, expected_type=int) self.assertEqual(datum.type, "int") self.assertEqual(datum.value, 42) - datum = DurableClientConverter.encode(obj=3.14, expected_type=float) + datum = LegacyDurableClientConverter.encode(obj=3.14, expected_type=float) self.assertEqual(datum.type, "double") self.assertEqual(datum.value, 3.14) - datum = DurableClientConverter.encode(obj=True, expected_type=bool) + datum = LegacyDurableClientConverter.encode(obj=True, expected_type=bool) self.assertEqual(datum.type, "bool") self.assertTrue(datum.value) with self.assertRaises(NotImplementedError): - DurableClientConverter.encode(obj=set([1, 2]), expected_type=set) + LegacyDurableClientConverter.encode(obj=set([1, 2]), expected_type=set) def test_durable_client_converter_decode(self): data = Datum(type="string", value="abc") - result = DurableClientConverter.decode(data=data, trigger_metadata=None) + result = LegacyDurableClientConverter.decode(data=data, trigger_metadata=None) self.assertEqual(result, "abc") data = Datum(type="bytes", value=b"123") - result = DurableClientConverter.decode(data=data, trigger_metadata=None) + result = LegacyDurableClientConverter.decode(data=data, trigger_metadata=None) self.assertEqual(result, b"123") data = Datum(type="json", value={"key": "val"}) - result = DurableClientConverter.decode(data=data, trigger_metadata=None) + result = LegacyDurableClientConverter.decode(data=data, trigger_metadata=None) self.assertEqual(result, {"key": "val"}) data = Datum(type=None, value=None) - result = DurableClientConverter.decode(data=data, trigger_metadata=None) + result = LegacyDurableClientConverter.decode(data=data, trigger_metadata=None) self.assertIsNone(result) - result = DurableClientConverter.decode(data=None, trigger_metadata=None) + result = LegacyDurableClientConverter.decode(data=None, trigger_metadata=None) self.assertIsNone(result) data = Datum(type="weird", value="???") with self.assertRaises(ValueError): - DurableClientConverter.decode(data=data, trigger_metadata=None) + LegacyDurableClientConverter.decode(data=data, trigger_metadata=None)