Skip to content

Add change supporting unit testing #537

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 29 commits into from
May 28, 2025
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
0e33ce3
Add change supporting unit testing
andystaples Apr 3, 2025
90500a2
Add support for durable client functions
andystaples Apr 3, 2025
ed470ce
Naming
andystaples Apr 18, 2025
69bd129
Add test samples to fan_in_fan_out app
andystaples Apr 23, 2025
a1282eb
Linting fixes
andystaples Apr 23, 2025
5adfb63
Merge branch 'dev' into andystaples/add-unit-testing-change
andystaples Apr 23, 2025
11cad72
Linting fixes 2
andystaples Apr 23, 2025
6d8c330
Merge branch 'andystaples/add-unit-testing-change' of https://github.…
andystaples Apr 23, 2025
2ef009f
Linter fixes 3
andystaples Apr 23, 2025
4584c90
Probable test issue fix
andystaples Apr 29, 2025
8f09b5a
Exclude samples from pytest github workflow
andystaples Apr 29, 2025
7902b65
Add testing matrix for samples
andystaples May 2, 2025
d2f8d1e
Pipeline fix
andystaples May 2, 2025
55886db
Build extension into tests
andystaples May 2, 2025
a2ca265
Merge branch 'dev' into andystaples/add-unit-testing-change
andystaples May 2, 2025
a9dd0e0
Add tests to other projects
andystaples May 2, 2025
7e10d5e
Tweak script
andystaples May 2, 2025
545fbcc
Update tests from PR feedback
andystaples May 5, 2025
c5f3540
Fix tests
andystaples May 5, 2025
f2db1cd
Fix tests
andystaples May 5, 2025
3a2f94b
Fix tests
andystaples May 5, 2025
0cb7871
PR feedback
andystaples May 7, 2025
81b67f5
Expose OrchestratorGeneratorWrapper in SDK (#548)
andystaples May 13, 2025
02ab139
Linting fixes
andystaples May 13, 2025
739a9e8
Test fix
andystaples May 13, 2025
b489e05
Linting fix
andystaples May 13, 2025
2343381
More linting and test fixes
andystaples May 13, 2025
850b2d0
Linting again
andystaples May 13, 2025
9ab1c17
Delete azure-functions-durable-python.sln
andystaples May 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions azure/durable_functions/decorators/durable_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,17 @@ async def df_client_middleware(*args, **kwargs):
# Invoke user code with rich DF Client binding
return await user_code(*args, **kwargs)

# Todo: This feels awkward - however, there are two reasons that I can't naively implement
# this in the same way as entities and orchestrators:
# 1. We intentionally wrap this exported signature with @wraps, to preserve the original
# signature of the user code. This means that we can't just assign a new object to the
# fb._function._func, as that would overwrite the original signature.
# 2. I have not yet fully tested the behavior of overriding __call__ on an object with an
# async method.
# Here we lose type hinting and auto-documentation - not great. Need to find a better way
# to do this.
df_client_middleware.client_function = fb._function._func

user_code_with_rich_client = df_client_middleware
fb._function._func = user_code_with_rich_client

Expand Down
48 changes: 36 additions & 12 deletions azure/durable_functions/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,45 @@
from datetime import datetime
from typing import Callable, Any, List, Dict

import azure.functions as func

class InternalEntityException(Exception):
"""Framework-internal Exception class (for internal use only)."""

pass

class EntityHandler(Callable):
"""Durable Entity Handler.
A callable class that wraps the user defined entity function for execution by the Python worker
and also allows access to the original method for unit testing
"""

def __init__(self, func: Callable[[DurableEntityContext], None]):
"""
Create a new entity handler for the user defined entity function.

Parameters
----------
func: Callable[[DurableEntityContext], None]
The user defined entity function.
"""
self.entity_function = func

def __call__(self, context: func.EntityContext) -> str:
"""Handle the execution of the user defined entity function.
Parameters
----------
context : func.EntityContext
The DF entity context"""
# It is not clear when the context JSON would be found
# inside a "body"-key, but this pattern matches the
# orchestrator implementation, so we keep it for safety.
context_body = getattr(context, "body", None)
if context_body is None:
context_body = context
ctx, batch = DurableEntityContext.from_json(context_body)
return Entity(self.entity_function).handle(ctx, batch)


class Entity:
"""Durable Entity Class.
Expand Down Expand Up @@ -92,19 +125,10 @@ def create(cls, fn: Callable[[DurableEntityContext], None]) -> Callable[[Any], s

Returns
-------
Callable[[Any], str]
Handle function of the newly created entity client
EntityHandler
Entity Handler callable for the newly created entity client
"""
def handle(context) -> str:
# It is not clear when the context JSON would be found
# inside a "body"-key, but this pattern matches the
# orchestrator implementation, so we keep it for safety.
context_body = getattr(context, "body", None)
if context_body is None:
context_body = context
ctx, batch = DurableEntityContext.from_json(context_body)
return Entity(fn).handle(ctx, batch)
return handle
return EntityHandler(fn)

def _elapsed_milliseconds_since(self, start_time: datetime) -> int:
"""Calculate the elapsed time, in milliseconds, from the start_time to the present.
Expand Down
42 changes: 32 additions & 10 deletions azure/durable_functions/orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,35 @@
import azure.functions as func


class OrchestrationHandler(Callable):
"""Durable Orchestration Handler.
A callable class that wraps the user defined generator function for execution by the Python worker
and also allows access to the original method for unit testing
"""

def __init__(self, func: Callable[[DurableOrchestrationContext], Generator[Any, Any, Any]]):
"""
Create a new orchestrator handler for the user defined orchestrator function.

Parameters
----------
func: Callable[[DurableOrchestrationContext], Generator[Any, Any, Any]]
The user defined orchestrator function.
"""
self.orchestrator_function = func

def __call__(self, context: func.OrchestrationContext) -> str:
"""Handle the execution of the user defined orchestrator function.
Parameters
----------
context : func.OrchestrationContext
The DF orchestration context"""
context_body = getattr(context, "body", None)
if context_body is None:
context_body = context
return Orchestrator(self.orchestrator_function).handle(DurableOrchestrationContext.from_json(context_body))


class Orchestrator:
"""Durable Orchestration Class.

Expand Down Expand Up @@ -58,14 +87,7 @@ def create(cls, fn: Callable[[DurableOrchestrationContext], Generator[Any, Any,

Returns
-------
Callable[[Any], str]
Handle function of the newly created orchestration client
OrchestrationHandler
Orchestration handler callable class for the newly created orchestration client
"""

def handle(context: func.OrchestrationContext) -> str:
context_body = getattr(context, "body", None)
if context_body is None:
context_body = context
return Orchestrator(fn).handle(DurableOrchestrationContext.from_json(context_body))

return handle
return OrchestrationHandler(fn)
Loading