Skip to content

Commit 6684cba

Browse files
andystaplesCopilot
andauthored
Add Python modernization and deprecation guardrails (#327)
* Add Python modernization and deprecation guardrails Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Remove deprecated UTC calls from executor tests Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Set the async fixture loop scope for strict warning checks Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Use the supported logger API in the Managed pagination test Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 4d82397 commit 6684cba

12 files changed

Lines changed: 157 additions & 26 deletions

File tree

‎.github/copilot-instructions.md‎

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,20 @@ Examples:
5555
- Follow PEP 8 conventions.
5656
- Use `autopep8` for Python formatting.
5757

58+
## Python Modernization
59+
60+
- Use modern Python syntax, typing, and standard-library APIs compatible with
61+
each package's minimum supported Python version.
62+
- Do not introduce APIs deprecated in any supported Python version. Nearby
63+
legacy code is not precedent for new code.
64+
- Preserve public API compatibility and runtime annotation behavior when
65+
modernizing.
66+
- Resolve deprecation warnings introduced by your changes. Any suppression
67+
must be narrowly scoped and justified.
68+
- Ruff's `UP` rules complement Flake8 and `autopep8`; they do not detect every
69+
deprecated API. Temporary rule exclusions in `pyproject.toml` track existing
70+
cleanup work, not preferred patterns for new code.
71+
5872
## Public API Compatibility
5973

6074
- Treat adding an abstract method or property to a public ABC as a breaking
@@ -91,7 +105,9 @@ priority over style.
91105
## Python Linting
92106

93107
This repository uses [flake8](https://flake8.pycqa.org/) for Python
94-
linting. Run it after making changes to verify there are no issues. Lint
108+
linting and Ruff's `UP` rules for modernization checks. Run `nox -s lint`
109+
for both checks. Ruff targets Python 3.11 for core/Managed and Python 3.13
110+
for Functions, including their tests and examples. Lint
95111
package source and its tests separately, matching CI:
96112

97113
```bash
@@ -103,6 +119,9 @@ python -m flake8 azure-functions-durable
103119
python -m flake8 tests/azure-functions-durable
104120
```
105121

122+
For a focused modernization check, run `python -m ruff check` with the changed
123+
paths. Run from the repository root to use the shared configuration.
124+
106125
## Markdown Style
107126

108127
Use GitHub-style callouts for notes, warnings, and tips in Markdown files:
@@ -172,6 +191,15 @@ python -m pytest tests/azure-functions-durable \
172191
-m "not dts and not azurite and not functions_e2e"
173192
```
174193

194+
The root pytest configuration treats `DeprecationWarning` and
195+
`PendingDeprecationWarning` as errors. Nox keeps Python 3.13 as the strict
196+
deprecation gate for every package and reports these warnings without failing
197+
on the other supported versions. Keep the full compatibility matrix; do not
198+
raise package minimum versions just to satisfy modernization suggestions.
199+
Tests deliberately exercising deprecated APIs should assert their warnings
200+
with `pytest.warns`. Known third-party exceptions must be scoped by warning
201+
category, message, and originating module where practical, with a reason.
202+
175203
Run Azure Functions Durable E2E tests through Nox, not directly from the root
176204
virtual environment. Nox creates an isolated Python 3.13 session environment,
177205
installs the local packages editable, and links it into each sample Function

‎dev-requirements.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ grpcio-tools
22
nox
33
pymarkdownlnt
44
pyright
5+
ruff

‎docs/development.md‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,51 @@ Azure Functions unit tests cover Python 3.13--3.15; use
2929
Nox fails rather than silently skipping a session when its required Python
3030
interpreter is unavailable.
3131

32+
### Modernization and deprecation checks
33+
34+
`nox -s lint` runs Flake8 and Ruff's `UP` modernization rules; `autopep8`
35+
remains the formatter. Ruff uses Python 3.11 for core/Managed code and Python
36+
3.13 for Functions code, including their respective tests and examples.
37+
Generated protobuf files are excluded, including when passed explicitly.
38+
For a focused check, run from the repository root:
39+
40+
```sh
41+
python -m ruff check durabletask/client.py
42+
```
43+
44+
The temporary typing and UTC rule exclusions in `pyproject.toml` are tracked
45+
in [#322](https://github.com/microsoft/durabletask-python/issues/322) and
46+
[#323](https://github.com/microsoft/durabletask-python/issues/323).
47+
Behavior-sensitive generator and enum suggestions have file-specific
48+
exceptions tracked in
49+
[#324](https://github.com/microsoft/durabletask-python/issues/324).
50+
Excluded rules do not reject new occurrences until their cleanup is complete;
51+
they are not recommendations for new code.
52+
53+
The root pytest configuration treats `DeprecationWarning` and
54+
`PendingDeprecationWarning` as errors and explicitly uses function-scoped event
55+
loops for async fixtures. Nox uses Python 3.13 as the representative
56+
strict deprecation gate for all three packages. On other supported versions,
57+
Nox reports these warnings without failing, preserving compatibility coverage
58+
without requiring APIs unavailable at the package's minimum version.
59+
Run the strict sessions with:
60+
61+
```sh
62+
nox -s core_tests-3.13 azuremanaged_tests-3.13 functions_unit-3.13
63+
```
64+
65+
Direct pytest runs from the repository root are strict on every interpreter.
66+
Tests intentionally exercising deprecated APIs should use `pytest.warns`.
67+
Any known third-party warning exception must be narrowly scoped by category,
68+
message, and originating module where practical, and include a reason.
69+
Two upstream Functions loose-codec warnings remain visible but non-fatal
70+
while legacy payload support is required; their removal is tracked in
71+
[#325](https://github.com/microsoft/durabletask-python/issues/325).
72+
Warnings raised in separately launched Functions workers are not governed by
73+
the parent pytest process's filters.
74+
75+
### Services and focused validation
76+
3277
Nox starts Azurite automatically for the core and Azure Functions tests. The
3378
Azure Managed tests start a disposable DTS emulator Docker container. Start
3479
Docker before running that session. Azure Functions end-to-end tests also

‎noxfile.py‎

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,16 @@ def _pytest_arguments(
137137
return tuple(posargs) if has_target else (default_target, *posargs)
138138

139139

140+
def _pytest_warning_arguments(session: nox.Session) -> tuple[str, ...]:
141+
"""Keep Python 3.13 as the strict deprecation gate across packages."""
142+
if session.python == "3.13":
143+
return ()
144+
return (
145+
"-W", "default::DeprecationWarning",
146+
"-W", "default::PendingDeprecationWarning",
147+
)
148+
149+
140150
def _start_azurite(
141151
session: nox.Session,
142152
ports: Sequence[int],
@@ -280,8 +290,8 @@ def _stop_dts_emulator(container_name: str) -> None:
280290

281291
@nox.session
282292
def lint(session: nox.Session) -> None:
283-
"""Run the Flake8 checks required by the GitHub Actions workflows."""
284-
session.install("flake8")
293+
"""Run Flake8 and Ruff modernization checks without replacing formatting."""
294+
session.install("flake8", "ruff")
285295
targets = session.posargs or (
286296
"durabletask",
287297
"durabletask-azuremanaged",
@@ -290,8 +300,10 @@ def lint(session: nox.Session) -> None:
290300
"tests/durabletask-azuremanaged",
291301
"tests/azure-functions-durable",
292302
"examples",
303+
"noxfile.py",
293304
)
294305
session.run("flake8", *targets)
306+
session.run("ruff", "check", *targets)
295307

296308

297309
@nox.session(python=["3.11"])
@@ -413,6 +425,7 @@ def core_tests(session: nox.Session) -> None:
413425
arguments = _pytest_arguments("tests/durabletask", session.posargs)
414426
session.run(
415427
"pytest",
428+
*_pytest_warning_arguments(session),
416429
*arguments,
417430
"-m",
418431
"not dts",
@@ -441,7 +454,10 @@ def azuremanaged_tests(session: nox.Session) -> None:
441454
"tests/durabletask-azuremanaged",
442455
session.posargs,
443456
)
444-
session.run("pytest", *arguments, "-m", "dts", "--verbose")
457+
session.run(
458+
"pytest", *_pytest_warning_arguments(session),
459+
*arguments, "-m", "dts", "--verbose",
460+
)
445461
finally:
446462
_stop_dts_emulator(container_name)
447463

@@ -454,7 +470,7 @@ def functions_unit(session: nox.Session) -> None:
454470
session.install("pytest")
455471
arguments = _pytest_arguments("tests/azure-functions-durable", session.posargs)
456472
session.run(
457-
"pytest", *arguments,
473+
"pytest", *_pytest_warning_arguments(session), *arguments,
458474
"-m", "not dts and not azurite and not functions_e2e",
459475
"--verbose",
460476
)
@@ -508,7 +524,7 @@ def functions_e2e(session: nox.Session) -> None:
508524
session.posargs,
509525
)
510526
session.run(
511-
"pytest", *arguments,
527+
"pytest", *_pytest_warning_arguments(session), *arguments,
512528
"-m", "functions_e2e",
513529
)
514530
finally:

‎pyproject.toml‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,42 @@ include = ["durabletask", "durabletask.*"]
5858
minversion = "6.0"
5959
testpaths = ["tests"]
6060
asyncio_mode = "auto"
61+
asyncio_default_fixture_loop_scope = "function"
62+
filterwarnings = [
63+
"error::DeprecationWarning",
64+
"error::PendingDeprecationWarning",
65+
# Legacy Functions payloads still require the upstream loose codec.
66+
# Report only these known warnings without failing; see issue #325.
67+
'default:azure\.functions df_loads was called without expected_type\.:DeprecationWarning:azure\.functions\._durable_functions$',
68+
'default:azure\.functions Durable JSON codec reconstructed a custom object via the loose-mode object_hook path\.:DeprecationWarning:azure\.functions\._durable_functions$',
69+
]
6170
markers = [
6271
"azurite: tests that require Azurite (local Azure Storage emulator)",
6372
"functions_e2e: end-to-end tests that require the Azure Functions Core Tools (func) and Azurite",
6473
]
74+
75+
[tool.ruff]
76+
target-version = "py311"
77+
extend-exclude = ["*_pb2.py", "*_pb2_grpc.py", "*_pb2.pyi", "*_pb2_grpc.pyi", ".python_packages"]
78+
force-exclude = true
79+
80+
[tool.ruff.per-file-target-version]
81+
"azure-functions-durable/**/*.py" = "py313"
82+
"tests/azure-functions-durable/**/*.py" = "py313"
83+
84+
[tool.ruff.lint]
85+
select = ["UP"]
86+
ignore = [
87+
# Typing migration requires runtime annotation compatibility review:
88+
# https://github.com/microsoft/durabletask-python/issues/322
89+
"UP006", "UP007", "UP035", "UP037", "UP040", "UP043", "UP045", "UP047",
90+
# Repository-wide UTC alias cleanup:
91+
# https://github.com/microsoft/durabletask-python/issues/323
92+
"UP017",
93+
]
94+
95+
[tool.ruff.lint.per-file-ignores]
96+
# These suggestions can change generator protocol or public enum behavior:
97+
# https://github.com/microsoft/durabletask-python/issues/324
98+
"durabletask/internal/orchestration_entity_context.py" = ["UP028"]
99+
"durabletask/scheduled/schedule_status.py" = ["UP042"]

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -932,7 +932,8 @@ def test_entity_id_pickle_round_trip():
932932
with pytest.warns(DeprecationWarning):
933933
entity_id = df.EntityId("Counter", "one")
934934

935-
restored = pickle.loads(pickle.dumps(entity_id))
935+
with pytest.warns(DeprecationWarning, match="EntityId is deprecated"):
936+
restored = pickle.loads(pickle.dumps(entity_id))
936937
assert type(restored) is df.EntityId
937938
assert isinstance(restored, df.EntityId)
938939
assert restored.name == "counter"

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -629,10 +629,11 @@ def orchestrator(ctx: task.OrchestrationContext, _):
629629
def test_activity_retry_then_fan_out_uses_distinct_task_ids():
630630
"""Regression test for Azure/azure-functions-durable-python#603."""
631631
def orchestrator(context):
632-
options = df.RetryOptions(
633-
first_retry_interval_in_milliseconds=100,
634-
max_number_of_attempts=3,
635-
)
632+
with pytest.warns(DeprecationWarning, match="RetryOptions is deprecated"):
633+
options = df.RetryOptions(
634+
first_retry_interval_in_milliseconds=100,
635+
max_number_of_attempts=3,
636+
)
636637
yield context.call_activity_with_retry("flaky", options)
637638
tasks = [context.call_activity("square", value) for value in range(13)]
638639
return (yield context.task_all(tasks))

‎tests/durabletask-azuremanaged/test_dts_batch_actions.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,8 @@ def emit(self, record):
157157
log_records.append(record)
158158

159159
handler = ListHandler()
160+
logger = logging.Logger("test.azuremanaged.client.pagination")
161+
logger.addHandler(handler)
160162

161163
with DurableTaskSchedulerWorker(host_address=endpoint, secure_channel=True,
162164
taskhub=taskhub_name, token_credential=None) as w:
@@ -165,7 +167,7 @@ def emit(self, record):
165167

166168
c = DurableTaskSchedulerClient(host_address=endpoint, secure_channel=True,
167169
taskhub=taskhub_name, token_credential=None,
168-
log_handler=handler)
170+
logger=logger)
169171

170172
# Create at least 3 orchestrations to test the limit
171173
ids = []

‎tests/durabletask/extensions/history_export/test_activities.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,7 @@ def test_blob_name_matches_dotnet_hash_scheme():
352352
assert _dotnet_o_format(last_updated) == "2026-06-04T17:09:09.4209900+00:00"
353353

354354
expected_hash = hashlib.sha256(
355-
f"{_dotnet_o_format(last_updated)}|{instance_id}".encode("utf-8")
355+
f"{_dotnet_o_format(last_updated)}|{instance_id}".encode()
356356
).hexdigest()
357357

358358
assert _blob_name_for(

‎tests/durabletask/test_batch_actions.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,14 +238,16 @@ def emit(self, record):
238238
log_records.append(record)
239239

240240
handler = ListHandler()
241+
logger = logging.Logger("test.client.pagination")
242+
logger.addHandler(handler)
241243

242244
worker = TaskHubGrpcWorker(host_address=HOST)
243245

244246
worker.add_orchestrator(empty_orchestrator)
245247
worker.start()
246248

247249
try:
248-
with TaskHubGrpcClient(host_address=HOST, log_handler=handler) as c:
250+
with TaskHubGrpcClient(host_address=HOST, logger=logger) as c:
249251
# Create at least 3 orchestrations to test the limit
250252
ids = []
251253
for i in range(3):

0 commit comments

Comments
 (0)