Skip to content

Commit bad91c1

Browse files
committed
Preserve activity source directories through Functions wrappers
1 parent f1aeb63 commit bad91c1

7 files changed

Lines changed: 48 additions & 2 deletions

File tree

‎azure-functions-durable/CHANGELOG.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ User orchestrators remain generators, and activities retain their synchronous
2121
or asynchronous calling convention. Synchronous functions retain synchronous
2222
client bindings, and invocation logging context is preserved.
2323
Worker payload-size and storage errors retain their original error details.
24+
Activity wrappers preserve the application's source directory for
25+
`context.function_directory`, including when payload storage is not configured.
2426

2527
## v2.0.0rc1
2628

‎azure-functions-durable/azure/durable_functions/decorators/durable_app.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
builtin_http_poll_orchestrator,
2222
)
2323
from ..internal.compat.activity import wrap_activity, wrap_activity_payloads
24-
from ..internal.invocation import wrap_invocation
24+
from ..internal.invocation import preserve_function_source, wrap_invocation
2525
from ..worker import DurableFunctionsWorker
2626

2727

@@ -454,6 +454,7 @@ def set_client_metadata(client_bound: Callable[..., Any]) -> None:
454454
annotations[client_name] = str
455455
client_bound.__annotations__ = annotations
456456
setattr(client_bound, "client_function", function)
457+
preserve_function_source(client_bound, function)
457458

458459
if is_async_function:
459460
@wraps(function)

‎azure-functions-durable/azure/durable_functions/internal/compat/activity.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
from azure.functions._durable_functions import df_loads
3434

3535
from ..converters import ActivityTriggerConverter
36+
from ..invocation import preserve_function_source
3637
from ..payloads import (
3738
ActivityPayload,
3839
deexternalize_payload,
@@ -174,6 +175,7 @@ def wrap_activity(fn: Callable[..., Any], input_name: str) -> Callable[..., Any]
174175
if ret_ann is not inspect.Parameter.empty:
175176
annotations["return"] = ret_ann
176177
adapter.__annotations__ = annotations
178+
preserve_function_source(adapter, fn)
177179
return adapter
178180

179181

@@ -222,4 +224,5 @@ def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
222224
wrapper = sync_wrapper
223225

224226
setattr(wrapper, "__signature__", signature)
227+
preserve_function_source(wrapper, fn)
225228
return wrapper

‎azure-functions-durable/azure/durable_functions/internal/invocation.py‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from concurrent.futures import ThreadPoolExecutor
1212
from contextvars import ContextVar, copy_context
1313
from functools import lru_cache, wraps
14+
from types import FunctionType
1415
from typing import Any, Callable, ParamSpec, TypeVar
1516

1617
import azure.functions as func
@@ -20,6 +21,12 @@
2021
_Result = TypeVar("_Result")
2122

2223

24+
def preserve_function_source(wrapper: Callable[..., Any], original: Callable[..., Any]) -> None:
25+
"""Preserve the filename used by the Functions worker to index the app directory."""
26+
if isinstance(wrapper, FunctionType) and isinstance(original, FunctionType):
27+
wrapper.__code__ = wrapper.__code__.replace(co_filename=original.__code__.co_filename)
28+
29+
2330
@lru_cache(maxsize=1)
2431
def _fallback_executor() -> ThreadPoolExecutor:
2532
setting = os.environ.get("PYTHON_THREADPOOL_THREAD_COUNT")

‎tests/azure-functions-durable/e2e/apps/dtask_style/large_payloads.py‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"""Blob-backed payload round trips through the real Functions bindings."""
55

66
import json
7+
from pathlib import Path
78
from typing import Any
89

910
import azure.functions as func
@@ -19,6 +20,7 @@
1920
def payload_echo(payload: dict, client: df.SyncDurableFunctionsClient, context: func.Context) -> dict:
2021
assert isinstance(client, df.SyncDurableFunctionsClient)
2122
assert context.thread_local_storage.invocation_id == context.invocation_id
23+
assert json.loads((Path(context.function_directory) / "host.json").read_text())["version"] == "2.0"
2224
return {"data": payload["data"], "stages": [*payload["stages"], "activity"]}
2325

2426

@@ -32,8 +34,9 @@ def payload_roundtrip(ctx: task.OrchestrationContext, payload: dict[str, Any]):
3234

3335
@bp.activity_trigger(input_name="payload")
3436
@bp.durable_client_input(client_name="client")
35-
async def payload_echo_async(payload: dict, client: df.DurableFunctionsClient) -> dict:
37+
async def payload_echo_async(payload: dict, client: df.DurableFunctionsClient, context: func.Context) -> dict:
3638
assert isinstance(client, df.DurableFunctionsClient)
39+
assert json.loads((Path(context.function_directory) / "host.json").read_text())["version"] == "2.0"
3740
return {"data": payload["data"], "stages": [*payload["stages"], "activity"]}
3841

3942

‎tests/azure-functions-durable/test_activity_adapter_compat.py‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,34 @@ def act(x):
3232
assert wrap_activity(act, "x") is act
3333

3434

35+
@pytest.mark.parametrize("native", [False, True])
36+
@pytest.mark.parametrize("user_async", [False, True])
37+
async def test_activity_wrappers_preserve_indexed_source_directory(monkeypatch, native, user_async):
38+
monkeypatch.setattr(payloads, "_payload_store", None)
39+
40+
def activity(payload):
41+
return payload
42+
43+
async def async_activity(payload):
44+
return payload
45+
46+
def native_activity(context, payload):
47+
return payload
48+
49+
async def async_native_activity(context, payload):
50+
return payload
51+
52+
original = (async_native_activity if user_async else native_activity) if native else (
53+
async_activity if user_async else activity)
54+
adapted = wrap_activity(original, "payload")
55+
wrapped = wrap_activity_payloads(adapted, "payload")
56+
assert inspect.getfile(adapted) == inspect.getfile(original)
57+
assert inspect.getfile(wrapped) == inspect.getfile(original)
58+
assert inspect.iscoroutinefunction(wrapped) == user_async
59+
result = await wrapped("value") if user_async else wrapped("value")
60+
assert result == "value"
61+
62+
3563
def test_two_param_activity_is_adapted_to_single_input():
3664
def act(ctx, payload):
3765
return (ctx, payload)

‎tests/azure-functions-durable/test_decorator_compat.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,9 @@ async def async_activity(payload, client, context):
278278
client_decorator = app.durable_client_input(client_name="client")
279279
registered = (client_decorator(activity_decorator(function)) if client_outer
280280
else activity_decorator(client_decorator(function)))
281+
original_filename = inspect.getfile(function)
281282
function = registered.build().get_user_function()
283+
assert inspect.getfile(function) == original_filename
282284
assert inspect.iscoroutinefunction(function) == user_async
283285
result = function(payload="result", client="{}", context=invocation)
284286
assert (await result if user_async else result) == "result"

0 commit comments

Comments
 (0)