diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml index 010f1524..83eca624 100644 --- a/.github/workflows/typecheck.yml +++ b/.github/workflows/typecheck.yml @@ -53,3 +53,6 @@ jobs: - name: Run pyright (strict, Python 3.13) run: nox -s typecheck_functions + + - name: Check packaged types (strict, Python 3.13) + run: nox -s typecheck_functions_package diff --git a/azure-functions-durable/CHANGELOG.md b/azure-functions-durable/CHANGELOG.md index e9c022e6..fea8d4dc 100644 --- a/azure-functions-durable/CHANGELOG.md +++ b/azure-functions-durable/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ADDED +- Added PEP 561 type information so strict type checkers recognize +`azure.durable_functions` as a typed package. - Added `azure.durable_functions.testing.execute_entity()` for unit testing function-style and class-based entities without a Functions host or Durable Task backend. The helper returns the operation result, resulting state, and typed diff --git a/azure-functions-durable/azure/durable_functions/py.typed b/azure-functions-durable/azure/durable_functions/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/azure-functions-durable/pyproject.toml b/azure-functions-durable/pyproject.toml index fb675b4c..62698a04 100644 --- a/azure-functions-durable/pyproject.toml +++ b/azure-functions-durable/pyproject.toml @@ -39,5 +39,8 @@ changelog = "https://github.com/microsoft/durabletask-python/blob/main/azure-fun [tool.setuptools.packages.find] include = ["azure.durable_functions", "azure.durable_functions.*"] +[tool.setuptools.package-data] +"azure.durable_functions" = ["py.typed"] + [tool.pytest.ini_options] minversion = "6.0" diff --git a/noxfile.py b/noxfile.py index 075c71cc..99a3e1e5 100644 --- a/noxfile.py +++ b/noxfile.py @@ -12,6 +12,7 @@ nox -s lint nox -s typecheck_core + nox -s typecheck_functions_package nox -s core_tests-3.10 nox -s azuremanaged_tests-3.10 nox -s functions_unit-3.13 @@ -27,8 +28,10 @@ import shutil import socket import subprocess +import tarfile import time import uuid +import zipfile from pathlib import Path from typing import Sequence @@ -323,6 +326,79 @@ def typecheck_functions(session: nox.Session) -> None: ) +@nox.session(python=["3.13"]) +def typecheck_functions_package(session: nox.Session) -> None: + """Check the installed Functions and core wheels with strict Pyright.""" + session.install("build", "pyright") + temp_dir = Path(session.create_tmp()) + artifact_dir = temp_dir / "functions-dist" + core_artifact_dir = temp_dir / "core-dist" + if artifact_dir.exists(): + shutil.rmtree(artifact_dir) + if core_artifact_dir.exists(): + shutil.rmtree(core_artifact_dir) + session.run( + "python", + "-m", + "build", + "--outdir", + str(artifact_dir), + str(AZURE_FUNCTIONS_DURABLE), + ) + session.run( + "python", + "-m", + "build", + "--wheel", + "--outdir", + str(core_artifact_dir), + str(REPO_ROOT), + ) + + wheels = list(artifact_dir.glob("azure_functions_durable-*.whl")) + sdists = list(artifact_dir.glob("azure_functions_durable-*.tar.gz")) + core_wheels = list(core_artifact_dir.glob("durabletask-*.whl")) + if len(wheels) != 1 or len(sdists) != 1 or len(core_wheels) != 1: + session.error( + "Expected one provider wheel, provider source distribution, " + "and core wheel") + wheel = wheels[0] + sdist = sdists[0] + core_wheel = core_wheels[0] + marker = "azure/durable_functions/py.typed" + with zipfile.ZipFile(wheel) as archive: + if marker not in archive.namelist(): + session.error(f"{marker} is missing from {wheel.name}") + with tarfile.open(sdist, "r:gz") as archive: + if not any(name.endswith(f"/{marker}") for name in archive.getnames()): + session.error(f"{marker} is missing from {sdist.name}") + + session.install(str(core_wheel), str(wheel)) + session.run( + "python", + "-m", + "pip", + "install", + "--force-reinstall", + "--no-deps", + str(core_wheel), + str(wheel), + ) + python_path = session.run( + "python", + "-c", + "import sys; print(sys.executable)", + silent=True, + ).strip() + session.run( + "pyright", + "--pythonpath", + python_path, + "-p", + "tests/azure-functions-durable/typing/pyrightconfig.json", + ) + + @nox.session(python=PYTHON_VERSIONS) def core_tests(session: nox.Session) -> None: """Run core SDK tests against an automatically started Blob Azurite.""" @@ -459,6 +535,7 @@ def ci(session: nox.Session) -> None: session.notify("lint", ()) session.notify("typecheck_core", ()) session.notify("typecheck_functions", ()) + session.notify("typecheck_functions_package", ()) session.notify(f"core_tests-{python_version}", ()) session.notify(f"azuremanaged_tests-{python_version}", ()) session.notify("functions_unit-3.13", ()) diff --git a/tests/azure-functions-durable/typing/import_package.py b/tests/azure-functions-durable/typing/import_package.py new file mode 100644 index 00000000..fe91818c --- /dev/null +++ b/tests/azure-functions-durable/typing/import_package.py @@ -0,0 +1,21 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +from datetime import timedelta + +import azure.durable_functions as df + + +def create_app() -> df.DFApp: + return df.DFApp() + + +def create_retry_policy() -> df.RetryPolicy: + return df.RetryPolicy( + first_retry_interval=timedelta(seconds=1), + max_number_of_attempts=3, + ) + + +def activity_is_complete(context: df.DurableOrchestrationContext) -> bool: + return context.call_activity("activity").is_complete diff --git a/tests/azure-functions-durable/typing/pyrightconfig.json b/tests/azure-functions-durable/typing/pyrightconfig.json new file mode 100644 index 00000000..68700dda --- /dev/null +++ b/tests/azure-functions-durable/typing/pyrightconfig.json @@ -0,0 +1,7 @@ +{ + "include": [ + "import_package.py" + ], + "pythonVersion": "3.13", + "typeCheckingMode": "strict" +}