Skip to content

Commit 6dc3abe

Browse files
committed
Infer Functions durable client type
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 76f2163a-0773-4889-b627-1bd4e5c7bea0
1 parent 8c5a626 commit 6dc3abe

4 files changed

Lines changed: 23 additions & 30 deletions

File tree

azure-functions-durable/CHANGELOG.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
ADDED
1111

12-
- Added `SyncDurableFunctionsClient` and
13-
`DFApp.durable_client_input_sync()` for synchronous durable-client functions.
14-
Both synchronous and asynchronous Functions durable clients can now use the
15-
scheduled-tasks and history-export client APIs without an async-to-sync bridge.
12+
- Added `SyncDurableFunctionsClient`. `DFApp.durable_client_input()` now injects
13+
the synchronous client into synchronous functions and the asynchronous client
14+
into coroutine functions. Both clients support scheduled-task and history-export
15+
APIs without an async-to-sync bridge.
1616

1717
## 2.0.0b1
1818

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

Lines changed: 12 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -152,12 +152,12 @@ def configure_history_export(self, writer: Any) -> None:
152152
# whatever worker runs them). ``durable_client_input`` is applied as the
153153
# outer decorator over ``activity_trigger`` so the built function carries
154154
# both bindings.
155-
self.durable_client_input(client_name="client", sync=True)(
155+
self.durable_client_input(client_name="client")(
156156
self.activity_trigger(
157157
input_name="input",
158158
activity=LIST_TERMINAL_INSTANCES_ACTIVITY)(
159159
list_terminal_instances_client_bound))
160-
self.durable_client_input(client_name="client", sync=True)(
160+
self.durable_client_input(client_name="client")(
161161
self.activity_trigger(
162162
input_name="input",
163163
activity=EXPORT_INSTANCE_HISTORY_ACTIVITY)(
@@ -359,11 +359,13 @@ def durable_client_input(self,
359359
client_name: str,
360360
task_hub: Optional[str] = None,
361361
connection_name: Optional[str] = None,
362-
*,
363-
sync: bool = False,
364362
) -> Callable[[Callable[..., Any]], FunctionBuilder]:
365363
"""Register a Durable-client Function.
366364
365+
Coroutine functions receive an asynchronous
366+
:class:`DurableFunctionsClient`; synchronous functions receive a cached
367+
:class:`SyncDurableFunctionsClient`.
368+
367369
Parameters
368370
----------
369371
client_name: str
@@ -378,17 +380,14 @@ def durable_client_input(self,
378380
The storage account represented by this connection string must be the same one
379381
used by the target orchestrator functions. If not specified, the default storage
380382
account connection string for the function app is used.
381-
sync: bool
382-
When ``True``, inject a :class:`SyncDurableFunctionsClient`. The
383-
default injects the asynchronous :class:`DurableFunctionsClient`.
384383
"""
385384

386385
@self._build_function
387386
def wrap(fb: FunctionBuilder) -> FunctionBuilder:
388387
def decorator() -> FunctionBuilder:
389388
# The converter returns the host configuration string. The
390-
# function wrapper below constructs and closes the requested
391-
# synchronous or asynchronous rich client per invocation.
389+
# function wrapper below constructs the rich client matching
390+
# the decorated function's synchronous or asynchronous type.
392391
fb.add_binding(
393392
binding=DurableClient(name=client_name,
394393
task_hub=task_hub,
@@ -407,6 +406,7 @@ def attach_client_function(user_fn: Callable[..., Any]) -> FunctionBuilder:
407406
function = (user_fn._function._func # pyright: ignore[reportPrivateUsage]
408407
if isinstance(user_fn, FunctionBuilder) else user_fn)
409408
signature = inspect.signature(function)
409+
is_async_function = inspect.iscoroutinefunction(function)
410410

411411
def bind_client(
412412
args: tuple[Any, ...],
@@ -421,8 +421,8 @@ def bind_client(
421421
if not isinstance(raw_client, str):
422422
raise TypeError(
423423
f"durable client binding '{client_name}' did not provide its configuration")
424-
client = (SyncDurableFunctionsClient.get_cached(raw_client) if sync
425-
else DurableFunctionsClient(raw_client))
424+
client = (DurableFunctionsClient(raw_client) if is_async_function
425+
else SyncDurableFunctionsClient.get_cached(raw_client))
426426
bound.arguments[client_name] = client
427427
return bound, client
428428

@@ -439,7 +439,7 @@ def set_client_metadata(client_bound: Callable[..., Any]) -> None:
439439
client_bound.__annotations__ = annotations
440440
setattr(client_bound, "client_function", function)
441441

442-
if inspect.iscoroutinefunction(function):
442+
if is_async_function:
443443
@wraps(function)
444444
async def async_client_bound(*args: Any, **kwargs: Any) -> Any:
445445
bound, client = bind_client(args, kwargs)
@@ -469,16 +469,6 @@ def sync_client_bound(*args: Any, **kwargs: Any) -> Any:
469469

470470
return attach_client_function
471471

472-
def durable_client_input_sync(
473-
self,
474-
client_name: str,
475-
task_hub: Optional[str] = None,
476-
connection_name: Optional[str] = None,
477-
) -> Callable[[Callable[..., Any]], FunctionBuilder]:
478-
"""Register a durable-client binding that injects a synchronous client."""
479-
return self.durable_client_input(
480-
client_name, task_hub, connection_name, sync=True)
481-
482472

483473
class DFApp(Blueprint, FunctionRegister):
484474
"""Durable Functions (DF) app.

durabletask/scheduled/client.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,10 @@ async def list_schedules(
269269
results: list[ScheduleDescription] = []
270270
for metadata in await self._client.get_all_entities(query):
271271
state = metadata.get_typed_state(ScheduleState)
272-
if state is not None and state.schedule_configuration is not None and \
273-
ScheduledTaskClient.matches_filter(state, schedule_query):
272+
if (
273+
state is not None
274+
and state.schedule_configuration is not None
275+
and ScheduledTaskClient.matches_filter(state, schedule_query)
276+
):
274277
results.append(state.to_description())
275278
return results

tests/azure-functions-durable/test_decorator_compat.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,15 +135,15 @@ async def starter(client):
135135
assert await fb._function._func(client="{}") == "DurableFunctionsClient"
136136

137137

138-
def test_durable_client_input_sync_injects_sync_client():
138+
def test_durable_client_input_injects_sync_client_for_sync_function():
139139
app = df.DFApp()
140140
clients = []
141141

142142
def starter(client):
143143
clients.append(client)
144144
return type(client).__name__
145145

146-
fb = app.durable_client_input_sync(client_name="client")(starter)
146+
fb = app.durable_client_input(client_name="client")(starter)
147147
assert not inspect.iscoroutinefunction(fb._function._func)
148148
assert fb._function._func(client="{}") == "SyncDurableFunctionsClient"
149149
assert fb._function._func(client="{}") == "SyncDurableFunctionsClient"

0 commit comments

Comments
 (0)