From 07a7d404d836201a56efcaafc13c5557794e7d2a Mon Sep 17 00:00:00 2001 From: Bernd Verst Date: Sun, 26 Jul 2026 23:15:31 -0700 Subject: [PATCH 1/5] Avoid eager worker imports from the durabletask package initializer The `durabletask` package initializer eagerly imported `durabletask.worker` just to re-export a handful of public types. Because the `durabletask-azuremanaged` distribution shares the same `durabletask` namespace package, importing anything from it (for example `durabletask.azuremanaged.client`) first executed this initializer and therefore loaded the entire worker dependency graph - gRPC, protobuf, entities, serialization, and OpenTelemetry - even for client-only applications. Resolve the public re-exports lazily with a module-level `__getattr__` (PEP 562), paired with a `TYPE_CHECKING` block so static type checkers and IDEs still resolve every symbol. Resolved values are cached in the module globals so subsequent lookups bypass the hook entirely. Measured with `python -X importtime`, median of 11 runs: import durabletask 419.2 ms -> 5.5 ms (76x faster) 293 -> 81 modules loaded import durabletask.azuremanaged.client 641.1 ms -> 610.8 ms 408 -> 398 modules loaded No public API changes: every existing import path, `__all__`, `PACKAGE_NAME`, `dir()`, star-imports, and the `AttributeError` message shape are preserved. Fixes #196 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b6ec6f9e-f919-4874-b146-fa764b5c5617 --- CHANGELOG.md | 1 + durabletask/__init__.py | 61 ++++++-- tests/durabletask/test_lazy_exports.py | 200 +++++++++++++++++++++++++ 3 files changed, 252 insertions(+), 10 deletions(-) create mode 100644 tests/durabletask/test_lazy_exports.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 99372609..99290d12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ ADDED CHANGED +- Importing `durabletask` no longer eagerly imports the worker implementation and its dependencies (gRPC, protobuf, entities, serialization, OpenTelemetry). The public names re-exported from the package — `ActivityWorkItemFilter`, `ConcurrencyOptions`, `EntityWorkItemFilter`, `GrpcChannelOptions`, `GrpcRetryPolicyOptions`, `LargePayloadStorageOptions`, `OrchestrationWorkItemFilter`, `PayloadStore`, `VersioningOptions`, and `WorkItemFilters` — are now resolved on first use, so `import durabletask` is substantially faster and loads far fewer modules. This measurably reduces cold-start time for client-only applications, including those using `durabletask.azuremanaged`, which shares the same `durabletask` namespace. All existing import paths, `__all__`, `dir()`, and star-imports behave exactly as before. - **Breaking:** `FailureDetails.error_type` — and the `errorType` value sent over the wire — is now the fully-qualified type name (`module.ClassName`, e.g. `builtins.ValueError`, `durabletask.task.TaskFailedError`) instead of the bare class name, matching the .NET and Java SDKs. Code that compared `error_type` against a bare name (for example `== "ValueError"`) must be updated to the qualified name or, preferably, switched to `FailureDetails.is_caused_by()`. Because this value is persisted and crosses the orchestration boundary, failures produced by older workers may still carry a bare name; `is_caused_by()` accepts both. ## v1.8.0 diff --git a/durabletask/__init__.py b/durabletask/__init__.py index 1d7ed82e..8b86c2c3 100644 --- a/durabletask/__init__.py +++ b/durabletask/__init__.py @@ -3,16 +3,19 @@ """Durable Task SDK for Python""" -from durabletask.grpc_options import GrpcChannelOptions, GrpcRetryPolicyOptions -from durabletask.payload.store import LargePayloadStorageOptions, PayloadStore -from durabletask.worker import ( - ActivityWorkItemFilter, - ConcurrencyOptions, - EntityWorkItemFilter, - OrchestrationWorkItemFilter, - VersioningOptions, - WorkItemFilters, -) +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from durabletask.grpc_options import GrpcChannelOptions, GrpcRetryPolicyOptions + from durabletask.payload.store import LargePayloadStorageOptions, PayloadStore + from durabletask.worker import ( + ActivityWorkItemFilter, + ConcurrencyOptions, + EntityWorkItemFilter, + OrchestrationWorkItemFilter, + VersioningOptions, + WorkItemFilters, + ) __all__ = [ "ActivityWorkItemFilter", @@ -28,3 +31,41 @@ ] PACKAGE_NAME = "durabletask" + +# Public names are resolved lazily so that merely importing the ``durabletask`` +# package - which also happens implicitly when importing anything from the +# ``durabletask.azuremanaged`` distribution, since both share this namespace - +# does not pull in the worker dependency graph (gRPC, protobuf, entities, +# serialization, OpenTelemetry). Client-only applications would otherwise pay +# that cost at process startup. +_LAZY_EXPORTS: dict[str, str] = { + "ActivityWorkItemFilter": "durabletask.worker", + "ConcurrencyOptions": "durabletask.worker", + "EntityWorkItemFilter": "durabletask.worker", + "GrpcChannelOptions": "durabletask.grpc_options", + "GrpcRetryPolicyOptions": "durabletask.grpc_options", + "LargePayloadStorageOptions": "durabletask.payload.store", + "OrchestrationWorkItemFilter": "durabletask.worker", + "PayloadStore": "durabletask.payload.store", + "VersioningOptions": "durabletask.worker", + "WorkItemFilters": "durabletask.worker", +} + + +def __getattr__(name: str) -> Any: + """Import and return a lazily exported public name (PEP 562).""" + module_name = _LAZY_EXPORTS.get(name) + if module_name is None: + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + from importlib import import_module + + value = getattr(import_module(module_name), name) + # Cache on the module so subsequent lookups bypass this hook entirely. + globals()[name] = value + return value + + +def __dir__() -> list[str]: + """Include lazily exported names that have not been resolved yet.""" + return sorted(set(globals()) | set(__all__)) diff --git a/tests/durabletask/test_lazy_exports.py b/tests/durabletask/test_lazy_exports.py new file mode 100644 index 00000000..05a6c6b7 --- /dev/null +++ b/tests/durabletask/test_lazy_exports.py @@ -0,0 +1,200 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +"""Tests for the lazy public exports of the ``durabletask`` package. + +The ``durabletask`` package initializer resolves its public names lazily (PEP +562) so that importing it - which also happens implicitly when importing +anything from the ``durabletask.azuremanaged`` distribution, because both share +the ``durabletask`` namespace - does not pull in the worker dependency graph. +These tests pin both that behavior and the public surface it has to preserve. +""" + +import importlib +import subprocess +import sys + +import pytest + +import durabletask + +EXPECTED_ALL = [ + "ActivityWorkItemFilter", + "ConcurrencyOptions", + "EntityWorkItemFilter", + "GrpcChannelOptions", + "GrpcRetryPolicyOptions", + "LargePayloadStorageOptions", + "OrchestrationWorkItemFilter", + "PayloadStore", + "VersioningOptions", + "WorkItemFilters", +] + +# Where each public name is actually defined. Declared independently of the +# package's own lazy-export table so that a wrong mapping is caught rather than +# mirrored back. +EXPECTED_SOURCE_MODULES = { + "ActivityWorkItemFilter": "durabletask.worker", + "ConcurrencyOptions": "durabletask.worker", + "EntityWorkItemFilter": "durabletask.worker", + "GrpcChannelOptions": "durabletask.grpc_options", + "GrpcRetryPolicyOptions": "durabletask.grpc_options", + "LargePayloadStorageOptions": "durabletask.payload.store", + "OrchestrationWorkItemFilter": "durabletask.worker", + "PayloadStore": "durabletask.payload.store", + "VersioningOptions": "durabletask.worker", + "WorkItemFilters": "durabletask.worker", +} + +# Modules that a client-only application should not have to pay for at import +# time. ``durabletask.worker`` is the entry point into the rest of them. +EAGERLY_IMPORTED_WORKER_MODULES = [ + "durabletask.worker", + "durabletask.internal.type_discovery", +] + + +def _run_python(code: str) -> "subprocess.CompletedProcess[str]": + """Run a snippet in a fresh interpreter so ``sys.modules`` starts clean.""" + return subprocess.run( + [sys.executable, "-c", code], + capture_output=True, + text=True, + ) + + +def _assert_ok(result: "subprocess.CompletedProcess[str]") -> None: + assert result.returncode == 0, ( + f"subprocess failed:\nstdout:\n{result.stdout}\nstderr:\n{result.stderr}" + ) + + +@pytest.mark.parametrize("module_name", EAGERLY_IMPORTED_WORKER_MODULES) +def test_importing_package_does_not_import_worker(module_name: str): + """A bare ``import durabletask`` must not load the worker graph.""" + result = _run_python( + "import sys\n" + "import durabletask\n" + f"assert {module_name!r} not in sys.modules, sorted(\n" + " m for m in sys.modules if m.startswith('durabletask')\n" + ")\n" + ) + _assert_ok(result) + + +def test_importing_package_does_not_import_grpc_or_protobuf(): + """The worker's heavyweight third-party dependencies stay unimported.""" + result = _run_python( + "import sys\n" + "import durabletask\n" + "loaded = [\n" + " name\n" + " for name in ('grpc', 'google.protobuf', 'opentelemetry')\n" + " if name in sys.modules\n" + "]\n" + "assert not loaded, loaded\n" + ) + _assert_ok(result) + + +def test_importing_azuremanaged_client_does_not_import_worker(): + """The Azure managed client shares the namespace but is client-only.""" + result = _run_python( + "import sys\n" + "import durabletask.azuremanaged.client\n" + "assert 'durabletask.worker' not in sys.modules\n" + ) + _assert_ok(result) + + +@pytest.mark.parametrize("name", EXPECTED_ALL) +def test_public_name_is_importable(name: str): + """Every re-exported symbol is still reachable via ``from durabletask``.""" + result = _run_python(f"from durabletask import {name}\nassert {name} is not None\n") + _assert_ok(result) + + +@pytest.mark.parametrize("name", EXPECTED_ALL) +def test_public_name_is_the_same_object_as_in_its_defining_module(name: str): + """Lazy resolution returns the original object, not a copy or proxy.""" + module = importlib.import_module(EXPECTED_SOURCE_MODULES[name]) + assert getattr(durabletask, name) is getattr(module, name) + + +def test_all_is_unchanged(): + assert durabletask.__all__ == EXPECTED_ALL + + +def test_every_public_name_has_a_known_source_module(): + """``__all__`` and the expected source mapping must not drift apart.""" + assert sorted(EXPECTED_SOURCE_MODULES) == sorted(EXPECTED_ALL) + + +def test_package_name_is_unchanged(): + assert durabletask.PACKAGE_NAME == "durabletask" + + +def test_dir_includes_public_names(): + """``dir()`` reports lazy names even before they have been resolved.""" + result = _run_python( + "import durabletask\n" + f"missing = [name for name in {EXPECTED_ALL!r} if name not in dir(durabletask)]\n" + "assert not missing, missing\n" + "assert 'PACKAGE_NAME' in dir(durabletask)\n" + "assert dir(durabletask) == sorted(dir(durabletask))\n" + ) + _assert_ok(result) + + +def test_star_import_binds_all_public_names(): + result = _run_python( + "import durabletask\n" + "namespace = {}\n" + "exec('from durabletask import *', namespace)\n" + "missing = [name for name in durabletask.__all__ if name not in namespace]\n" + "assert not missing, missing\n" + ) + _assert_ok(result) + + +def test_unknown_attribute_raises_attribute_error(): + with pytest.raises(AttributeError) as excinfo: + durabletask.ThisAttributeDoesNotExist # type: ignore[attr-defined] + + assert str(excinfo.value) == ( + "module 'durabletask' has no attribute 'ThisAttributeDoesNotExist'" + ) + + +def test_hasattr_is_false_for_unknown_attribute(): + assert not hasattr(durabletask, "ThisAttributeDoesNotExist") + + +def test_submodules_remain_importable_from_the_package(): + """``__getattr__`` must not shadow normal submodule imports.""" + result = _run_python( + "from durabletask import client, entities, task, worker\n" + "assert task.__name__ == 'durabletask.task'\n" + ) + _assert_ok(result) + + +def test_resolved_name_is_cached_on_the_module(): + result = _run_python( + "import durabletask\n" + "assert 'ConcurrencyOptions' not in vars(durabletask)\n" + "durabletask.ConcurrencyOptions\n" + "assert 'ConcurrencyOptions' in vars(durabletask)\n" + ) + _assert_ok(result) + + +def test_importing_worker_first_does_not_break_the_package(): + """Guard against an import cycle: ``durabletask.worker`` imports the package.""" + result = _run_python( + "import durabletask.worker\n" + "from durabletask import ConcurrencyOptions\n" + "assert ConcurrencyOptions is durabletask.worker.ConcurrencyOptions\n" + ) + _assert_ok(result) From fdd2b99c1a21222499e3c2ac8e265c11e3d9c306 Mon Sep 17 00:00:00 2001 From: Bernd Verst Date: Sun, 26 Jul 2026 23:35:06 -0700 Subject: [PATCH 2/5] Keep eagerly bound submodules reachable as package attributes Making the public re-exports lazy also removed a side effect of the previous eager imports: the import system had been binding the imported submodules as attributes of the `durabletask` package. A bare `import durabletask` therefore left `entities`, `grpc_options`, `internal`, `payload`, `serialization`, `task`, and `worker` reachable through plain attribute access, and that stopped working. `from durabletask import task` kept working either way, because importlib's `_handle_fromlist` imports the submodule when `__getattr__` raises, which is why the existing submodule test did not catch this. Plain attribute access has no such fallback. Resolve those seven names through `__getattr__` as well, and include them in `__dir__`. They are still imported only when touched, so the cold-start win is unchanged: a bare `import durabletask` continues to load 81 modules and pulls in neither the worker nor gRPC. The list deliberately mirrors the previous surface rather than extending it - `durabletask.client` was never bound this way, so it stays unreachable by attribute access, and a test pins that. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b6ec6f9e-f919-4874-b146-fa764b5c5617 --- durabletask/__init__.py | 27 +++++++++-- tests/durabletask/test_lazy_exports.py | 63 ++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 4 deletions(-) diff --git a/durabletask/__init__.py b/durabletask/__init__.py index 8b86c2c3..bc9ea674 100644 --- a/durabletask/__init__.py +++ b/durabletask/__init__.py @@ -51,16 +51,35 @@ "WorkItemFilters": "durabletask.worker", } +# Submodules that the previous eager imports bound as attributes of this package +# as a side effect. Attribute access keeps them reachable without an explicit +# ``import durabletask.``, exactly as before, but they are now imported +# only when actually touched. This list mirrors the old surface and must not be +# extended: ``durabletask.client``, for instance, was never bound this way. +_LAZY_SUBMODULES: frozenset[str] = frozenset({ + "entities", + "grpc_options", + "internal", + "payload", + "serialization", + "task", + "worker", +}) + def __getattr__(name: str) -> Any: - """Import and return a lazily exported public name (PEP 562).""" + """Import and return a lazily exported public name or submodule (PEP 562).""" module_name = _LAZY_EXPORTS.get(name) - if module_name is None: + if module_name is None and name not in _LAZY_SUBMODULES: raise AttributeError(f"module {__name__!r} has no attribute {name!r}") from importlib import import_module - value = getattr(import_module(module_name), name) + if module_name is not None: + value = getattr(import_module(module_name), name) + else: + value = import_module(f".{name}", __name__) + # Cache on the module so subsequent lookups bypass this hook entirely. globals()[name] = value return value @@ -68,4 +87,4 @@ def __getattr__(name: str) -> Any: def __dir__() -> list[str]: """Include lazily exported names that have not been resolved yet.""" - return sorted(set(globals()) | set(__all__)) + return sorted(set(globals()) | set(__all__) | _LAZY_SUBMODULES) diff --git a/tests/durabletask/test_lazy_exports.py b/tests/durabletask/test_lazy_exports.py index 05a6c6b7..0c272bab 100644 --- a/tests/durabletask/test_lazy_exports.py +++ b/tests/durabletask/test_lazy_exports.py @@ -54,6 +54,21 @@ "durabletask.internal.type_discovery", ] +# Submodules that the previous eager imports left bound as attributes of the +# package as a side effect. A bare ``import durabletask`` must keep them +# reachable through attribute access alone - without an explicit +# ``import durabletask.`` - because that was observable behavior before +# the public exports became lazy. +EAGERLY_BOUND_SUBMODULES = [ + "entities", + "grpc_options", + "internal", + "payload", + "serialization", + "task", + "worker", +] + def _run_python(code: str) -> "subprocess.CompletedProcess[str]": """Run a snippet in a fresh interpreter so ``sys.modules`` starts clean.""" @@ -180,6 +195,54 @@ def test_submodules_remain_importable_from_the_package(): _assert_ok(result) +@pytest.mark.parametrize("name", EAGERLY_BOUND_SUBMODULES) +def test_submodule_is_reachable_by_attribute_after_a_bare_import(name: str): + """``import durabletask`` then ``durabletask.`` must still work. + + This is distinct from ``from durabletask import ``, which + succeeds either way because the import system falls back to importing the + submodule when ``__getattr__`` raises. Plain attribute access has no such + fallback, so the names have to be resolvable through ``__getattr__``. + """ + result = _run_python( + "import durabletask\n" + f"module = getattr(durabletask, {name!r})\n" + f"assert module.__name__ == 'durabletask.{name}', module.__name__\n" + ) + _assert_ok(result) + + +def test_bare_import_does_not_newly_bind_the_client_submodule(): + """The lazily bound submodules must mirror the old surface, not exceed it. + + ``durabletask.client`` was never pulled in by the previous eager imports, + so attribute access on it failed before this change and must keep failing; + binding it now would expand the public surface rather than preserve it. + """ + result = _run_python( + "import durabletask\n" + "try:\n" + " durabletask.client\n" + "except AttributeError:\n" + " pass\n" + "else:\n" + " raise AssertionError('durabletask.client should not be bound')\n" + ) + _assert_ok(result) + + +def test_attribute_access_to_a_submodule_stays_lazy(): + """Reaching a submodule by attribute must not happen at package import.""" + result = _run_python( + "import sys\n" + "import durabletask\n" + "assert 'durabletask.task' not in sys.modules\n" + "durabletask.task\n" + "assert 'durabletask.task' in sys.modules\n" + ) + _assert_ok(result) + + def test_resolved_name_is_cached_on_the_module(): result = _run_python( "import durabletask\n" From ea332e91447709ddadc5115756e2d8fc7f030a51 Mon Sep 17 00:00:00 2001 From: Bernd Verst Date: Mon, 27 Jul 2026 09:42:02 -0700 Subject: [PATCH 3/5] Skip the azuremanaged lazy-import test when the provider is absent `test_importing_azuremanaged_client_does_not_import_worker` failed on all five Python versions in the `run-tests` job of `.github/workflows/durabletask.yml` with `ModuleNotFoundError: No module named 'durabletask.azuremanaged'`. The cause is a missing package, not lazy initialization. That job installs `requirements.txt`, `.[azure-blob-payloads]` and `aiohttp`, and never installs `durabletask-azuremanaged` -- so the test's subprocess could never import the provider there. It passes locally only because a development environment has both distributions installed. Guard the test with `pytest.importorskip("durabletask.azuremanaged")` so it skips cleanly where the provider is not installed. The assertion itself is unchanged, and the test still runs (and still has teeth) wherever the provider is present. Widening the workflow's install scope would be a broader change than this fix needs. Verified by blocking the provider behind a `sys.meta_path` hook to simulate the CI environment: 42 passed, 1 skipped without the provider, 43 passed with it. Also rewrap the CHANGELOG entry to the 100-character md013 limit using non-indented continuation lines. The file has 16 pre-existing md013 violations on `main`; this keeps that count unchanged rather than adding to it. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b6ec6f9e-f919-4874-b146-fa764b5c5617 --- CHANGELOG.md | 11 ++++++++++- tests/durabletask/test_lazy_exports.py | 3 +++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99290d12..61fc409b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,16 @@ ADDED CHANGED -- Importing `durabletask` no longer eagerly imports the worker implementation and its dependencies (gRPC, protobuf, entities, serialization, OpenTelemetry). The public names re-exported from the package — `ActivityWorkItemFilter`, `ConcurrencyOptions`, `EntityWorkItemFilter`, `GrpcChannelOptions`, `GrpcRetryPolicyOptions`, `LargePayloadStorageOptions`, `OrchestrationWorkItemFilter`, `PayloadStore`, `VersioningOptions`, and `WorkItemFilters` — are now resolved on first use, so `import durabletask` is substantially faster and loads far fewer modules. This measurably reduces cold-start time for client-only applications, including those using `durabletask.azuremanaged`, which shares the same `durabletask` namespace. All existing import paths, `__all__`, `dir()`, and star-imports behave exactly as before. +- Importing `durabletask` no longer eagerly imports the worker implementation and its +dependencies (gRPC, protobuf, entities, serialization, OpenTelemetry). The public names +re-exported from the package — `ActivityWorkItemFilter`, `ConcurrencyOptions`, +`EntityWorkItemFilter`, `GrpcChannelOptions`, `GrpcRetryPolicyOptions`, +`LargePayloadStorageOptions`, `OrchestrationWorkItemFilter`, `PayloadStore`, +`VersioningOptions`, and `WorkItemFilters` — are now resolved on first use, so +`import durabletask` is substantially faster and loads far fewer modules. This +measurably reduces cold-start time for client-only applications, including those using +`durabletask.azuremanaged`, which shares the same `durabletask` namespace. All existing +import paths, `__all__`, `dir()`, and star-imports behave exactly as before. - **Breaking:** `FailureDetails.error_type` — and the `errorType` value sent over the wire — is now the fully-qualified type name (`module.ClassName`, e.g. `builtins.ValueError`, `durabletask.task.TaskFailedError`) instead of the bare class name, matching the .NET and Java SDKs. Code that compared `error_type` against a bare name (for example `== "ValueError"`) must be updated to the qualified name or, preferably, switched to `FailureDetails.is_caused_by()`. Because this value is persisted and crosses the orchestration boundary, failures produced by older workers may still carry a bare name; `is_caused_by()` accepts both. ## v1.8.0 diff --git a/tests/durabletask/test_lazy_exports.py b/tests/durabletask/test_lazy_exports.py index 0c272bab..f2576d5a 100644 --- a/tests/durabletask/test_lazy_exports.py +++ b/tests/durabletask/test_lazy_exports.py @@ -115,6 +115,9 @@ def test_importing_package_does_not_import_grpc_or_protobuf(): def test_importing_azuremanaged_client_does_not_import_worker(): """The Azure managed client shares the namespace but is client-only.""" + # The provider is distributed separately and is not installed in every CI + # job that runs this suite, so skip rather than fail where it is absent. + pytest.importorskip("durabletask.azuremanaged") result = _run_python( "import sys\n" "import durabletask.azuremanaged.client\n" From e1db0f42497b9ee655f443eb36330df0ca41515a Mon Sep 17 00:00:00 2001 From: Bernd Verst Date: Mon, 27 Jul 2026 10:01:47 -0700 Subject: [PATCH 4/5] Indent CHANGELOG continuation lines for repo consistency The wrapped bullet added by this PR used non-indented continuation lines. Surveying every wrapped bullet in the repository shows that is the sole exception: CHANGELOG.md 59 indented, 0 non-indented durabletask-azuremanaged/CHANGELOG 20 indented, 0 non-indented azure-functions-durable/CHANGELOG 26 indented, 0 non-indented Indent the nine continuation lines to match. Content is unchanged -- the diff is exactly 18 added space characters (9 lines x 2). Note the rendering rationale offered in review does not apply here. CommonMark lazy continuation keeps unindented paragraph text inside the list item, and GitHub's own renderer returns byte-identical HTML for both forms (1
    , 1
  • , no text escaping the item). The reason to indent is consistency with the other 105 wrapped bullets, not a rendering defect. Indented text is still more robust, since a future reflow that starts a line with "-" or "1." would silently open a new list block. Longest line is now 89 characters, within the md013 limit of 100. Violation count is unchanged at 16, all pre-existing and none on the lines this PR touches. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b6ec6f9e-f919-4874-b146-fa764b5c5617 --- CHANGELOG.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 61fc409b..d00dbf61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,15 +14,15 @@ ADDED CHANGED - Importing `durabletask` no longer eagerly imports the worker implementation and its -dependencies (gRPC, protobuf, entities, serialization, OpenTelemetry). The public names -re-exported from the package — `ActivityWorkItemFilter`, `ConcurrencyOptions`, -`EntityWorkItemFilter`, `GrpcChannelOptions`, `GrpcRetryPolicyOptions`, -`LargePayloadStorageOptions`, `OrchestrationWorkItemFilter`, `PayloadStore`, -`VersioningOptions`, and `WorkItemFilters` — are now resolved on first use, so -`import durabletask` is substantially faster and loads far fewer modules. This -measurably reduces cold-start time for client-only applications, including those using -`durabletask.azuremanaged`, which shares the same `durabletask` namespace. All existing -import paths, `__all__`, `dir()`, and star-imports behave exactly as before. + dependencies (gRPC, protobuf, entities, serialization, OpenTelemetry). The public names + re-exported from the package — `ActivityWorkItemFilter`, `ConcurrencyOptions`, + `EntityWorkItemFilter`, `GrpcChannelOptions`, `GrpcRetryPolicyOptions`, + `LargePayloadStorageOptions`, `OrchestrationWorkItemFilter`, `PayloadStore`, + `VersioningOptions`, and `WorkItemFilters` — are now resolved on first use, so + `import durabletask` is substantially faster and loads far fewer modules. This + measurably reduces cold-start time for client-only applications, including those using + `durabletask.azuremanaged`, which shares the same `durabletask` namespace. All existing + import paths, `__all__`, `dir()`, and star-imports behave exactly as before. - **Breaking:** `FailureDetails.error_type` — and the `errorType` value sent over the wire — is now the fully-qualified type name (`module.ClassName`, e.g. `builtins.ValueError`, `durabletask.task.TaskFailedError`) instead of the bare class name, matching the .NET and Java SDKs. Code that compared `error_type` against a bare name (for example `== "ValueError"`) must be updated to the qualified name or, preferably, switched to `FailureDetails.is_caused_by()`. Because this value is persisted and crosses the orchestration boundary, failures produced by older workers may still carry a bare name; `is_caused_by()` accepts both. ## v1.8.0 From 1ea92d66a4184fa0efe1e4dccf31c4adc91abecc Mon Sep 17 00:00:00 2001 From: Bernd Verst Date: Mon, 27 Jul 2026 10:58:05 -0700 Subject: [PATCH 5/5] Restore unindented changelog continuation lines The repo's agent instructions, updated in #211, now explicitly codify the changelog style: 'wrapped entry text remains unindented rather than being aligned beneath the bullet.' My earlier commit e1db0f4 indented these continuations after surveying existing entries (105 of 105 wrapped bullets were indented). That survey measured the legacy style; the documented convention is a deliberate forward-looking change, and andystaples had said as much in review. The maintainer's stated preference is now written policy, so this reverts to unindented. Content is unchanged apart from removing 18 leading spaces. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b6ec6f9e-f919-4874-b146-fa764b5c5617 --- CHANGELOG.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d00dbf61..61fc409b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,15 +14,15 @@ ADDED CHANGED - Importing `durabletask` no longer eagerly imports the worker implementation and its - dependencies (gRPC, protobuf, entities, serialization, OpenTelemetry). The public names - re-exported from the package — `ActivityWorkItemFilter`, `ConcurrencyOptions`, - `EntityWorkItemFilter`, `GrpcChannelOptions`, `GrpcRetryPolicyOptions`, - `LargePayloadStorageOptions`, `OrchestrationWorkItemFilter`, `PayloadStore`, - `VersioningOptions`, and `WorkItemFilters` — are now resolved on first use, so - `import durabletask` is substantially faster and loads far fewer modules. This - measurably reduces cold-start time for client-only applications, including those using - `durabletask.azuremanaged`, which shares the same `durabletask` namespace. All existing - import paths, `__all__`, `dir()`, and star-imports behave exactly as before. +dependencies (gRPC, protobuf, entities, serialization, OpenTelemetry). The public names +re-exported from the package — `ActivityWorkItemFilter`, `ConcurrencyOptions`, +`EntityWorkItemFilter`, `GrpcChannelOptions`, `GrpcRetryPolicyOptions`, +`LargePayloadStorageOptions`, `OrchestrationWorkItemFilter`, `PayloadStore`, +`VersioningOptions`, and `WorkItemFilters` — are now resolved on first use, so +`import durabletask` is substantially faster and loads far fewer modules. This +measurably reduces cold-start time for client-only applications, including those using +`durabletask.azuremanaged`, which shares the same `durabletask` namespace. All existing +import paths, `__all__`, `dir()`, and star-imports behave exactly as before. - **Breaking:** `FailureDetails.error_type` — and the `errorType` value sent over the wire — is now the fully-qualified type name (`module.ClassName`, e.g. `builtins.ValueError`, `durabletask.task.TaskFailedError`) instead of the bare class name, matching the .NET and Java SDKs. Code that compared `error_type` against a bare name (for example `== "ValueError"`) must be updated to the qualified name or, preferably, switched to `FailureDetails.is_caused_by()`. Because this value is persisted and crosses the orchestration boundary, failures produced by older workers may still carry a bare name; `is_caused_by()` accepts both. ## v1.8.0