Skip to content

Commit c9ea2fb

Browse files
committed
Cleanup pyright errors
1 parent 0505a0d commit c9ea2fb

10 files changed

Lines changed: 201 additions & 111 deletions

File tree

.github/workflows/typecheck.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ on:
77
tags:
88
- "v*"
99
- "azuremanaged-v*"
10+
- "azurefunctions-v*"
1011
pull_request:
1112
branches:
1213
- "main"
@@ -36,3 +37,25 @@ jobs:
3637
3738
- name: Run pyright (strict, Python 3.10)
3839
run: pyright
40+
41+
pyright-azurefunctions:
42+
runs-on: ubuntu-latest
43+
steps:
44+
- name: Checkout repository
45+
uses: actions/checkout@v4
46+
47+
- name: Set up Python 3.13 (lowest supported by azure-functions-durable)
48+
uses: actions/setup-python@v5
49+
with:
50+
python-version: "3.13"
51+
52+
- name: Install packages and dependencies
53+
run: |
54+
python -m pip install --upgrade pip
55+
pip install -r requirements.txt
56+
pip install -e ".[azure-blob-payloads,opentelemetry]"
57+
pip install -e ./azure-functions-durable
58+
pip install pyright
59+
60+
- name: Run pyright (strict, Python 3.13)
61+
run: pyright -p azure-functions-durable/pyrightconfig.json

azure-functions-durable/azure/durable_functions/__init__.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
# Copyright (c) Microsoft Corporation.
22
# Licensed under the MIT License.
33

4-
# This import ensures that the replacement of the global JSON encoder/decoder
5-
# happens as soon as the durabletask.azurefunctions package is imported.
6-
from .internal import functions_json as _
7-
4+
from .internal.functions_json import install_custom_serialization
85
from .decorators.durable_app import Blueprint, DFApp
96
from .client import DurableFunctionsClient
107
from .orchestrator import Orchestrator
118

9+
# Ensure the durabletask JSON encoder/decoder is replaced as soon as the
10+
# durable_functions package is imported.
11+
install_custom_serialization()
12+
1213
# IMPORTANT: DO NOT REMOVE. `azure-functions` relies on the presence and value of this variable
1314
# for version detection
1415
version = "2.x"

azure-functions-durable/azure/durable_functions/client.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class DurableFunctionsClient(TaskHubGrpcClient):
1818
"""A gRPC client passed to Durable Functions durable client bindings.
1919
2020
Connects to the Durable Functions runtime using gRPC and provides methods
21-
for creating and managing Durable orchestrations, interacting with Durable entities,
21+
for creating and managing Durable orchestrations, interacting with Durable entities,
2222
and creating HTTP management payloads and check status responses for use with Durable Functions invocations.
2323
"""
2424
taskHubName: str
@@ -44,7 +44,7 @@ def __init__(self, client_as_string: str):
4444
json.JSONDecodeError: If the provided string is not valid JSON.
4545
"""
4646
self._parse_client_configuration(client_as_string)
47-
if self.httpBaseUrl is None:
47+
if not self.httpBaseUrl:
4848
# This happens when the extension has not been configured for gRPC yet. For some reason, instead of
4949
# the client returning with null rpcBaseUrl and httpBaseUrl, it returns rpcBaseUrl with the http url.
5050
self.configure_extension_for_grpc()
@@ -87,7 +87,7 @@ def configure_extension_for_grpc(self) -> None:
8787
8888
Makes an HTTP request to the extension's management endpoint to enable gRPC.
8989
"""
90-
90+
9191
# Make an HTTP request to the extension to configure gRPC
9292
configure_base_url = self.httpBaseUrl
9393
if not configure_base_url:
@@ -103,7 +103,7 @@ def configure_extension_for_grpc(self) -> None:
103103
response = requests.get(url, params=params)
104104
if response.status_code != 200:
105105
raise Exception(f"Failed to configure gRPC for Durable Functions extension. Status code: {response.status_code}, Response: {response.text}")
106-
106+
107107
# Parse the response to update client configuration - it's double-encoded so we need to load it twice
108108
self._parse_client_configuration(json.loads(response.text))
109109

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

Lines changed: 72 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22
# Licensed under the MIT License.
33

44
from 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
107
from azure.functions import FunctionRegister, TriggerApi, BindingApi, AuthLevel
8+
from azure.functions.decorators.function_app import FunctionBuilder
9+
10+
from durabletask import task
1111

1212
from .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,

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ def get_binding_name() -> str:
2828
def __init__(self,
2929
name: str,
3030
orchestration: Optional[str] = None,
31-
durable_requires_grpc=True,
31+
durable_requires_grpc: bool = True,
3232
) -> None:
3333
self.orchestration = orchestration
3434
self.durable_requires_grpc = durable_requires_grpc
@@ -55,7 +55,7 @@ def get_binding_name() -> str:
5555
def __init__(self,
5656
name: str,
5757
activity: Optional[str] = None,
58-
durable_requires_grpc=True,
58+
durable_requires_grpc: bool = True,
5959
) -> None:
6060
self.activity = activity
6161
self.durable_requires_grpc = durable_requires_grpc
@@ -82,7 +82,7 @@ def get_binding_name() -> str:
8282
def __init__(self,
8383
name: str,
8484
entity_name: Optional[str] = None,
85-
durable_requires_grpc=True,
85+
durable_requires_grpc: bool = True,
8686
) -> None:
8787
self.entity_name = entity_name
8888
self.durable_requires_grpc = durable_requires_grpc
@@ -110,7 +110,7 @@ def __init__(self,
110110
name: str,
111111
task_hub: Optional[str] = None,
112112
connection_name: Optional[str] = None,
113-
durable_requires_grpc=True,
113+
durable_requires_grpc: bool = True,
114114
) -> None:
115115
self.task_hub = task_hub
116116
self.connection_name = connection_name

0 commit comments

Comments
 (0)