22# Licensed under the MIT License.
33
44from functools import wraps
5+ from typing import Any , Callable , Optional , Union
56
6- from durabletask import task
7-
8- from typing import Callable , Optional
9- from typing import Union
107from azure .functions import FunctionRegister , TriggerApi , BindingApi , AuthLevel
8+ from azure .functions .decorators .function_app import FunctionBuilder
9+
10+ from durabletask import task
1111
1212from .metadata import OrchestrationTrigger , ActivityTrigger , EntityTrigger , \
1313 DurableClient
@@ -40,9 +40,14 @@ def __init__(self,
4040 DFApp
4141 New instance of a Durable Functions app
4242 """
43- super ().__init__ (auth_level = http_auth_level )
44-
45- def _configure_orchestrator_callable (self , wrap ) -> Callable :
43+ # The next-in-MRO base (``DecoratorApi.__init__``) is declared with
44+ # untyped ``*args``/``**kwargs``, so pyright cannot see this call's type.
45+ super ().__init__ (auth_level = http_auth_level ) # pyright: ignore[reportUnknownMemberType]
46+
47+ def _configure_orchestrator_callable (
48+ self ,
49+ wrap : Callable [[Callable [..., Any ]], FunctionBuilder ]
50+ ) -> Callable [[task .Orchestrator [Any , Any ]], FunctionBuilder ]:
4651 """Obtain decorator to construct an Orchestrator class from a user-defined Function.
4752
4853 Parameters
@@ -56,7 +61,7 @@ def _configure_orchestrator_callable(self, wrap) -> Callable:
5661 The function to construct an Orchestrator class from the user-defined Function,
5762 wrapped by the next decorator in the sequence.
5863 """
59- def decorator (orchestrator_func : task .Orchestrator ) :
64+ def decorator (orchestrator_func : task .Orchestrator [ Any , Any ]) -> FunctionBuilder :
6065 # Construct an orchestrator based on the end-user code
6166
6267 handle = Orchestrator .create (orchestrator_func )
@@ -67,7 +72,10 @@ def decorator(orchestrator_func: task.Orchestrator):
6772
6873 return decorator
6974
70- def _configure_entity_callable (self , wrap ) -> Callable :
75+ def _configure_entity_callable (
76+ self ,
77+ wrap : Callable [[Callable [..., Any ]], FunctionBuilder ]
78+ ) -> Callable [[task .Entity [Any , Any ]], FunctionBuilder ]:
7179 """Obtain decorator to construct an Entity class from a user-defined Function.
7280
7381 Parameters
@@ -81,34 +89,44 @@ def _configure_entity_callable(self, wrap) -> Callable:
8189 The function to construct an Entity class from the user-defined Function,
8290 wrapped by the next decorator in the sequence.
8391 """
84- def decorator (entity_func : task .Entity ) :
92+ def decorator (entity_func : task .Entity [ Any , Any ]) -> FunctionBuilder :
8593 # Construct an orchestrator based on the end-user code
8694
8795 # TODO: Because this handle method is the one actually exposed to the Functions SDK decorator,
8896 # the parameter name will always be "context" here, even if the user specified a different name.
8997 # We need to find a way to allow custom context names (like "ctx").
90- def handle (context ) -> str :
91- return DurableFunctionsWorker ()._execute_entity_batch (entity_func , context )
98+ def handle (context : Any ) -> str :
99+ return DurableFunctionsWorker ().execute_entity_batch_request (entity_func , context )
92100
93- handle .entity_function = entity_func # type : ignore
101+ handle .entity_function = entity_func # pyright : ignore[reportFunctionMemberAccess]
94102
95103 # invoke next decorator, with the Entity as input
96104 handle .__name__ = entity_func .__name__
97105 return wrap (handle )
98106
99107 return decorator
100108
101- def _add_rich_client (self , fb , parameter_name , client_constructor ):
109+ def _add_rich_client (
110+ self ,
111+ fb : FunctionBuilder ,
112+ parameter_name : str ,
113+ client_constructor : Callable [[Any ], Any ]
114+ ) -> None :
102115 # Obtain user-code and force type annotation on the client-binding parameter to be `str`.
103116 # This ensures a passing type-check of that specific parameter,
104117 # circumventing a limitation of the worker in type-checking rich DF Client objects.
105118 # TODO: Once rich-binding type checking is possible, remove the annotation change.
106- user_code = fb ._function ._func
119+ # ``FunctionBuilder._function`` and ``Function._func`` are private to
120+ # azure-functions with no public accessor for mutating the wrapped
121+ # user function. Holding it as ``Any`` keeps the single private-access
122+ # waiver here rather than spreading it across each ``._func`` use.
123+ function_obj : Any = fb ._function # pyright: ignore[reportPrivateUsage]
124+ user_code = function_obj ._func
107125 user_code .__annotations__ [parameter_name ] = str
108126
109127 # `wraps` This ensures we re-export the same method-signature as the decorated method
110128 @wraps (user_code )
111- async def df_client_middleware (* args , ** kwargs ) :
129+ async def df_client_middleware (* args : Any , ** kwargs : Any ) -> Any :
112130
113131 # Obtain JSON-string currently passed as DF Client,
114132 # construct rich object from it,
@@ -121,13 +139,30 @@ async def df_client_middleware(*args, **kwargs):
121139 return await user_code (* args , ** kwargs )
122140
123141 # TODO: Is there a better way to support retrieving the unwrapped user code?
124- df_client_middleware .client_function = fb . _function . _func # type : ignore
142+ df_client_middleware .client_function = function_obj . _func # pyright : ignore[reportAttributeAccessIssue]
125143
126- user_code_with_rich_client = df_client_middleware
127- fb ._function ._func = user_code_with_rich_client
144+ function_obj ._func = df_client_middleware
145+
146+ def _build_function (
147+ self ,
148+ wrap : Callable [[FunctionBuilder ], FunctionBuilder ]
149+ ) -> Callable [[Callable [..., Any ]], FunctionBuilder ]:
150+ """Typed equivalent of the base ``_configure_function_builder``.
151+
152+ The inherited method is untyped, which would otherwise propagate
153+ ``Unknown`` types through every decorator below. This mirrors its
154+ behaviour exactly using the typed protected members it relies on.
155+ """
156+ def decorator (func : Callable [..., Any ]) -> FunctionBuilder :
157+ fb = self ._validate_type (func )
158+ self ._function_builders .append (fb )
159+ return wrap (fb )
160+
161+ return decorator
128162
129163 def orchestration_trigger (self , context_name : str ,
130- orchestration : Optional [str ] = None ):
164+ orchestration : Optional [str ] = None
165+ ) -> Callable [[task .Orchestrator [Any , Any ]], FunctionBuilder ]:
131166 """Register an Orchestrator Function.
132167
133168 Parameters
@@ -139,10 +174,10 @@ def orchestration_trigger(self, context_name: str,
139174 The value is None by default, in which case the name of the method is used.
140175 """
141176 @self ._configure_orchestrator_callable
142- @self ._configure_function_builder
143- def wrap (fb ) :
177+ @self ._build_function
178+ def wrap (fb : FunctionBuilder ) -> FunctionBuilder :
144179
145- def decorator ():
180+ def decorator () -> FunctionBuilder :
146181 fb .add_trigger (
147182 trigger = OrchestrationTrigger (name = context_name ,
148183 orchestration = orchestration ))
@@ -153,7 +188,8 @@ def decorator():
153188 return wrap
154189
155190 def activity_trigger (self , input_name : str ,
156- activity : Optional [str ] = None ):
191+ activity : Optional [str ] = None
192+ ) -> Callable [[Callable [..., Any ]], FunctionBuilder ]:
157193 """Register an Activity Function.
158194
159195 Parameters
@@ -164,9 +200,9 @@ def activity_trigger(self, input_name: str,
164200 Name of Activity Function.
165201 The value is None by default, in which case the name of the method is used.
166202 """
167- @self ._configure_function_builder
168- def wrap (fb ) :
169- def decorator ():
203+ @self ._build_function
204+ def wrap (fb : FunctionBuilder ) -> FunctionBuilder :
205+ def decorator () -> FunctionBuilder :
170206 fb .add_trigger (
171207 trigger = ActivityTrigger (name = input_name ,
172208 activity = activity ))
@@ -178,7 +214,8 @@ def decorator():
178214
179215 def entity_trigger (self ,
180216 context_name : str ,
181- entity_name : Optional [str ] = None ):
217+ entity_name : Optional [str ] = None
218+ ) -> Callable [[task .Entity [Any , Any ]], FunctionBuilder ]:
182219 """Register an Entity Function.
183220
184221 Parameters
@@ -190,9 +227,9 @@ def entity_trigger(self,
190227 The value is None by default, in which case the name of the method is used.
191228 """
192229 @self ._configure_entity_callable
193- @self ._configure_function_builder
194- def wrap (fb ) :
195- def decorator ():
230+ @self ._build_function
231+ def wrap (fb : FunctionBuilder ) -> FunctionBuilder :
232+ def decorator () -> FunctionBuilder :
196233 fb .add_trigger (
197234 trigger = EntityTrigger (name = context_name ,
198235 entity_name = entity_name ))
@@ -206,7 +243,7 @@ def durable_client_input(self,
206243 client_name : str ,
207244 task_hub : Optional [str ] = None ,
208245 connection_name : Optional [str ] = None
209- ):
246+ ) -> Callable [[ Callable [..., Any ]], FunctionBuilder ] :
210247 """Register a Durable-client Function.
211248
212249 Parameters
@@ -225,9 +262,9 @@ def durable_client_input(self,
225262 account connection string for the function app is used.
226263 """
227264
228- @self ._configure_function_builder
229- def wrap (fb ) :
230- def decorator ():
265+ @self ._build_function
266+ def wrap (fb : FunctionBuilder ) -> FunctionBuilder :
267+ def decorator () -> FunctionBuilder :
231268 fb .add_binding (
232269 binding = DurableClient (name = client_name ,
233270 task_hub = task_hub ,
0 commit comments