From d50ed247a34c4cccf7d2f3fecce39bc12f3f7b72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Skar=C5=BCy=C5=84ski?= Date: Wed, 22 Oct 2025 18:35:09 +0200 Subject: [PATCH 01/10] refactor: use classmethod in Client.connect factory method --- temporalio/client.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/temporalio/client.py b/temporalio/client.py index 71f43d6d9..770a51392 100644 --- a/temporalio/client.py +++ b/temporalio/client.py @@ -112,8 +112,9 @@ class Client: Clients do not work across forks since runtimes do not work across forks. """ - @staticmethod + @classmethod async def connect( + cls, target_host: str, *, namespace: str = "default", @@ -133,7 +134,7 @@ async def connect( runtime: Optional[temporalio.runtime.Runtime] = None, http_connect_proxy_config: Optional[HttpConnectProxyConfig] = None, header_codec_behavior: HeaderCodecBehavior = HeaderCodecBehavior.NO_CODEC, - ) -> Client: + ) -> Self: """Connect to a Temporal server. Args: @@ -209,7 +210,7 @@ def make_lambda(plugin, next): service_client = await next_function(connect_config) - return Client( + return cls( service_client, namespace=namespace, data_converter=data_converter, From ffcd3a65b73c54400ad9b6821afcd5e1df6c0fa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Skar=C5=BCy=C5=84ski?= Date: Wed, 22 Oct 2025 18:39:37 +0200 Subject: [PATCH 02/10] refactor: use classmethod in Runtime.default factory method --- temporalio/runtime.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/temporalio/runtime.py b/temporalio/runtime.py index 84b683941..180b38ba8 100644 --- a/temporalio/runtime.py +++ b/temporalio/runtime.py @@ -37,8 +37,8 @@ class Runtime: Runtimes do not work across forks. """ - @staticmethod - def default() -> Runtime: + @classmethod + def default(cls) -> Self: """Get the default runtime, creating if not already created. If the default runtime needs to be different, it should be done with @@ -49,7 +49,7 @@ def default() -> Runtime: """ global _default_runtime if not _default_runtime: - _default_runtime = Runtime(telemetry=TelemetryConfig()) + _default_runtime = cls(telemetry=TelemetryConfig()) return _default_runtime @staticmethod From fd15b5c8b0008fabd2b259ecb2ed82ac991d0b38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Skar=C5=BCy=C5=84ski?= Date: Wed, 22 Oct 2025 18:45:25 +0200 Subject: [PATCH 03/10] refactor: use classmethod in WorkerTuner factory methods --- temporalio/worker/_tuning.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/temporalio/worker/_tuning.py b/temporalio/worker/_tuning.py index cfe95aac4..0e2f56dce 100644 --- a/temporalio/worker/_tuning.py +++ b/temporalio/worker/_tuning.py @@ -7,7 +7,7 @@ from datetime import timedelta from typing import Any, Callable, Literal, Optional, Protocol, Union, runtime_checkable -from typing_extensions import TypeAlias +from typing_extensions import Self, TypeAlias import temporalio.bridge.worker from temporalio.common import WorkerDeploymentVersion @@ -310,8 +310,9 @@ def _to_bridge_slot_supplier( class WorkerTuner(ABC): """WorkerTuners allow for the dynamic customization of some aspects of worker configuration""" - @staticmethod + @classmethod def create_resource_based( + cls, *, target_memory_usage: float, target_cpu_usage: float, @@ -319,7 +320,7 @@ def create_resource_based( activity_config: Optional[ResourceBasedSlotConfig] = None, local_activity_config: Optional[ResourceBasedSlotConfig] = None, nexus_config: Optional[ResourceBasedSlotConfig] = None, - ) -> WorkerTuner: + ) -> Self: """Create a resource-based tuner with the provided options.""" resource_cfg = ResourceBasedTunerConfig(target_memory_usage, target_cpu_usage) wf = ResourceBasedSlotSupplier( @@ -341,14 +342,15 @@ def create_resource_based( nexus, ) - @staticmethod + @classmethod def create_fixed( + cls, *, workflow_slots: Optional[int] = None, activity_slots: Optional[int] = None, local_activity_slots: Optional[int] = None, nexus_slots: Optional[int] = None, - ) -> WorkerTuner: + ) -> Self: """Create a fixed-size tuner with the provided number of slots. Any unspecified slot numbers will default to 100. @@ -362,14 +364,15 @@ def create_fixed( FixedSizeSlotSupplier(nexus_slots if nexus_slots else 100), ) - @staticmethod + @classmethod def create_composite( + cls, *, workflow_supplier: SlotSupplier, activity_supplier: SlotSupplier, local_activity_supplier: SlotSupplier, nexus_supplier: SlotSupplier, - ) -> WorkerTuner: + ) -> Self: """Create a tuner composed of the provided slot suppliers.""" return _CompositeTuner( workflow_supplier, From 20e5856814e7a82e75fb1b3e888ada87af23a1b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Skar=C5=BCy=C5=84ski?= Date: Wed, 22 Oct 2025 18:49:19 +0200 Subject: [PATCH 04/10] refactor: use classmethod in WorkflowEnvironment.start_local factory method --- temporalio/testing/_workflow.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/temporalio/testing/_workflow.py b/temporalio/testing/_workflow.py index 7c74f5cc6..7ca8f6363 100644 --- a/temporalio/testing/_workflow.py +++ b/temporalio/testing/_workflow.py @@ -19,6 +19,8 @@ cast, ) +from typing_extensions import Self + import google.protobuf.empty_pb2 import temporalio.api.testservice.v1 @@ -73,8 +75,9 @@ def from_client(client: temporalio.client.Client) -> WorkflowEnvironment: _client_with_interceptors(client, _AssertionErrorInterceptor()) ) - @staticmethod + @classmethod async def start_local( + cls, *, namespace: str = "default", data_converter: temporalio.converter.DataConverter = temporalio.converter.DataConverter.default, @@ -100,7 +103,7 @@ async def start_local( dev_server_download_version: str = "default", dev_server_extra_args: Sequence[str] = [], dev_server_download_ttl: Optional[timedelta] = None, - ) -> WorkflowEnvironment: + ) -> Self: """Start a full Temporal server locally, downloading if necessary. This environment is good for testing full server capabilities, but does From 686eff86ebf686a3da0c6634cd29f8157a376d91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Skar=C5=BCy=C5=84ski?= Date: Wed, 22 Oct 2025 18:53:10 +0200 Subject: [PATCH 05/10] refactor: use classmethod in SandboxMatcher.nested_child factory method --- temporalio/worker/workflow_sandbox/_restrictions.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/temporalio/worker/workflow_sandbox/_restrictions.py b/temporalio/worker/workflow_sandbox/_restrictions.py index baad22fcb..ae7609708 100644 --- a/temporalio/worker/workflow_sandbox/_restrictions.py +++ b/temporalio/worker/workflow_sandbox/_restrictions.py @@ -34,6 +34,8 @@ cast, ) +from typing_extensions import Self + try: import pydantic import pydantic_core @@ -182,8 +184,8 @@ class SandboxMatcher: instances. """ - @staticmethod - def nested_child(path: Sequence[str], child: SandboxMatcher) -> SandboxMatcher: + @classmethod + def nested_child(cls, path: Sequence[str], child: SandboxMatcher) -> Self: """Create a matcher where the given child is put at the given path. Args: @@ -195,7 +197,7 @@ def nested_child(path: Sequence[str], child: SandboxMatcher) -> SandboxMatcher: """ ret = child for key in reversed(path): - ret = SandboxMatcher(children={key: ret}) + ret = cls(children={key: ret}) return ret access: Set[str] = frozenset() # type: ignore From fdb8293814f13d2a5e821a0cf31457e540dd5903 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Skar=C5=BCy=C5=84ski?= Date: Wed, 22 Oct 2025 18:56:59 +0200 Subject: [PATCH 06/10] refactor: use classmethod in configurations factory methods --- temporalio/envconfig.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/temporalio/envconfig.py b/temporalio/envconfig.py index 0dc14be65..6f016dc75 100644 --- a/temporalio/envconfig.py +++ b/temporalio/envconfig.py @@ -10,7 +10,7 @@ from pathlib import Path from typing import Any, Dict, Literal, Mapping, Optional, Union, cast -from typing_extensions import TypeAlias, TypedDict +from typing_extensions import Self, TypeAlias, TypedDict import temporalio.service from temporalio.bridge.temporal_sdk_bridge import envconfig as _bridge_envconfig @@ -148,12 +148,12 @@ def to_connect_tls_config(self) -> Union[bool, temporalio.service.TLSConfig]: client_private_key=_read_source(self.client_private_key), ) - @staticmethod - def from_dict(d: Optional[ClientConfigTLSDict]) -> Optional[ClientConfigTLS]: + @classmethod + def from_dict(cls, d: Optional[ClientConfigTLSDict]) -> Optional[Self]: """Create a ClientConfigTLS from a dictionary.""" if not d: return None - return ClientConfigTLS( + return cls( disabled=d.get("disabled"), server_name=d.get("server_name"), # Note: Bridge uses snake_case, but TOML uses kebab-case which is @@ -202,10 +202,10 @@ class ClientConfigProfile: grpc_meta: Mapping[str, str] = field(default_factory=dict) """gRPC metadata.""" - @staticmethod - def from_dict(d: ClientConfigProfileDict) -> ClientConfigProfile: + @classmethod + def from_dict(cls, d: ClientConfigProfileDict) -> Self: """Create a ClientConfigProfile from a dictionary.""" - return ClientConfigProfile( + return cls( address=d.get("address"), namespace=d.get("namespace"), api_key=d.get("api_key"), @@ -318,14 +318,15 @@ def to_dict(self) -> Mapping[str, ClientConfigProfileDict]: """Convert to a dictionary that can be used for TOML serialization.""" return {k: v.to_dict() for k, v in self.profiles.items()} - @staticmethod + @classmethod def from_dict( + cls, d: Mapping[str, Mapping[str, Any]], - ) -> ClientConfig: + ) -> Self: """Create a ClientConfig from a dictionary.""" # We must cast the inner dictionary because the source is often a plain # Mapping[str, Any] from the bridge or other sources. - return ClientConfig( + return cls( profiles={ k: ClientConfigProfile.from_dict(cast(ClientConfigProfileDict, v)) for k, v in d.items() From 6c7850135de7fab407efea071a510edba3f44aa6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Skar=C5=BCy=C5=84ski?= Date: Wed, 22 Oct 2025 19:10:55 +0200 Subject: [PATCH 07/10] refactor: use classmethod in the remaining WorkflowEnvironment factory methods --- temporalio/testing/_workflow.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/temporalio/testing/_workflow.py b/temporalio/testing/_workflow.py index 7ca8f6363..d0ae8d2b5 100644 --- a/temporalio/testing/_workflow.py +++ b/temporalio/testing/_workflow.py @@ -56,8 +56,8 @@ class WorkflowEnvironment: to have ``assert`` failures fail the workflow with the assertion error. """ - @staticmethod - def from_client(client: temporalio.client.Client) -> WorkflowEnvironment: + @classmethod + def from_client(cls, client: temporalio.client.Client) -> Self: """Create a workflow environment from the given client. :py:attr:`supports_time_skipping` will always return ``False`` for this @@ -71,7 +71,7 @@ def from_client(client: temporalio.client.Client) -> WorkflowEnvironment: The workflow environment that runs against the given client. """ # Add the assertion interceptor - return WorkflowEnvironment( + return cls( _client_with_interceptors(client, _AssertionErrorInterceptor()) ) @@ -237,8 +237,9 @@ async def start_local( ) raise - @staticmethod + @classmethod async def start_time_skipping( + cls, *, data_converter: temporalio.converter.DataConverter = temporalio.converter.DataConverter.default, interceptors: Sequence[temporalio.client.Interceptor] = [], @@ -256,7 +257,7 @@ async def start_time_skipping( test_server_download_version: str = "default", test_server_extra_args: Sequence[str] = [], test_server_download_ttl: Optional[timedelta] = None, - ) -> WorkflowEnvironment: + ) -> Self: """Start a time skipping workflow environment. By default, this environment will automatically skip to the next events @@ -360,7 +361,8 @@ async def start_time_skipping( def __init__(self, client: temporalio.client.Client) -> None: """Create a workflow environment from a client. - Most users would use a static method instead. + Most users would use a factory methods instead. + """ self._client = client From ce117c89432a2d3bb756767e801576a20534fc29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Skar=C5=BCy=C5=84ski?= Date: Wed, 22 Oct 2025 19:12:20 +0200 Subject: [PATCH 08/10] chore(dev): apply fixers on new changes --- temporalio/testing/_workflow.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/temporalio/testing/_workflow.py b/temporalio/testing/_workflow.py index d0ae8d2b5..47ee4ac37 100644 --- a/temporalio/testing/_workflow.py +++ b/temporalio/testing/_workflow.py @@ -19,9 +19,8 @@ cast, ) -from typing_extensions import Self - import google.protobuf.empty_pb2 +from typing_extensions import Self import temporalio.api.testservice.v1 import temporalio.bridge.testing @@ -71,9 +70,7 @@ def from_client(cls, client: temporalio.client.Client) -> Self: The workflow environment that runs against the given client. """ # Add the assertion interceptor - return cls( - _client_with_interceptors(client, _AssertionErrorInterceptor()) - ) + return cls(_client_with_interceptors(client, _AssertionErrorInterceptor())) @classmethod async def start_local( From 6e73132bd17491b51165d9dd3dea8ed47564f225 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Skar=C5=BCy=C5=84ski?= Date: Wed, 22 Oct 2025 19:18:32 +0200 Subject: [PATCH 09/10] fix: resolve typing issues with Self return type --- temporalio/runtime.py | 2 +- temporalio/testing/_workflow.py | 4 ++-- temporalio/worker/_tuning.py | 6 +++--- temporalio/worker/workflow_sandbox/_restrictions.py | 8 ++++---- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/temporalio/runtime.py b/temporalio/runtime.py index 180b38ba8..64fa12192 100644 --- a/temporalio/runtime.py +++ b/temporalio/runtime.py @@ -38,7 +38,7 @@ class Runtime: """ @classmethod - def default(cls) -> Self: + def default(cls) -> Runtime: """Get the default runtime, creating if not already created. If the default runtime needs to be different, it should be done with diff --git a/temporalio/testing/_workflow.py b/temporalio/testing/_workflow.py index 47ee4ac37..8e054e56b 100644 --- a/temporalio/testing/_workflow.py +++ b/temporalio/testing/_workflow.py @@ -100,7 +100,7 @@ async def start_local( dev_server_download_version: str = "default", dev_server_extra_args: Sequence[str] = [], dev_server_download_ttl: Optional[timedelta] = None, - ) -> Self: + ) -> WorkflowEnvironment: """Start a full Temporal server locally, downloading if necessary. This environment is good for testing full server capabilities, but does @@ -254,7 +254,7 @@ async def start_time_skipping( test_server_download_version: str = "default", test_server_extra_args: Sequence[str] = [], test_server_download_ttl: Optional[timedelta] = None, - ) -> Self: + ) -> WorkflowEnvironment: """Start a time skipping workflow environment. By default, this environment will automatically skip to the next events diff --git a/temporalio/worker/_tuning.py b/temporalio/worker/_tuning.py index 0e2f56dce..84abd87ac 100644 --- a/temporalio/worker/_tuning.py +++ b/temporalio/worker/_tuning.py @@ -320,7 +320,7 @@ def create_resource_based( activity_config: Optional[ResourceBasedSlotConfig] = None, local_activity_config: Optional[ResourceBasedSlotConfig] = None, nexus_config: Optional[ResourceBasedSlotConfig] = None, - ) -> Self: + ) -> WorkerTuner: """Create a resource-based tuner with the provided options.""" resource_cfg = ResourceBasedTunerConfig(target_memory_usage, target_cpu_usage) wf = ResourceBasedSlotSupplier( @@ -350,7 +350,7 @@ def create_fixed( activity_slots: Optional[int] = None, local_activity_slots: Optional[int] = None, nexus_slots: Optional[int] = None, - ) -> Self: + ) -> WorkerTuner: """Create a fixed-size tuner with the provided number of slots. Any unspecified slot numbers will default to 100. @@ -372,7 +372,7 @@ def create_composite( activity_supplier: SlotSupplier, local_activity_supplier: SlotSupplier, nexus_supplier: SlotSupplier, - ) -> Self: + ) -> WorkerTuner: """Create a tuner composed of the provided slot suppliers.""" return _CompositeTuner( workflow_supplier, diff --git a/temporalio/worker/workflow_sandbox/_restrictions.py b/temporalio/worker/workflow_sandbox/_restrictions.py index ae7609708..69050c476 100644 --- a/temporalio/worker/workflow_sandbox/_restrictions.py +++ b/temporalio/worker/workflow_sandbox/_restrictions.py @@ -185,7 +185,7 @@ class SandboxMatcher: """ @classmethod - def nested_child(cls, path: Sequence[str], child: SandboxMatcher) -> Self: + def nested_child(cls, path: Sequence[str], child: SandboxMatcher) -> SandboxMatcher: """Create a matcher where the given child is put at the given path. Args: @@ -202,7 +202,7 @@ def nested_child(cls, path: Sequence[str], child: SandboxMatcher) -> Self: access: Set[str] = frozenset() # type: ignore """Immutable set of names to match access. - + This is often only used for pass through checks and not member restrictions. If this is used for member restrictions, even importing/accessing the value will fail as opposed to :py:attr:`use` which is for when it is used. @@ -212,7 +212,7 @@ def nested_child(cls, path: Sequence[str], child: SandboxMatcher) -> Self: use: Set[str] = frozenset() # type: ignore """Immutable set of names to match use. - + This is best used for member restrictions on functions/classes because the restriction will not apply to referencing/importing the item, just when it is used. @@ -248,7 +248,7 @@ def nested_child(cls, path: Sequence[str], child: SandboxMatcher) -> Self: exclude: Set[str] = frozenset() # type: ignore """Immutable set of names to exclude. - + These override anything that may have been matched elsewhere. """ From 879f995e2694e49d1459a310047b2e3413a91878 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Skar=C5=BCy=C5=84ski?= Date: Wed, 22 Oct 2025 19:22:01 +0200 Subject: [PATCH 10/10] fix: use default_factory for MappingProxyType instances defaults --- tests/nexus/test_handler.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/nexus/test_handler.py b/tests/nexus/test_handler.py index c805a967c..1f3420da3 100644 --- a/tests/nexus/test_handler.py +++ b/tests/nexus/test_handler.py @@ -20,7 +20,7 @@ import uuid from collections.abc import Mapping from concurrent.futures.thread import ThreadPoolExecutor -from dataclasses import dataclass +from dataclasses import dataclass, field from types import MappingProxyType from typing import Any, Callable, Optional, Union @@ -313,7 +313,9 @@ async def non_serializable_output( class SuccessfulResponse: status_code: int body_json: Optional[Union[dict[str, Any], Callable[[dict[str, Any]], bool]]] = None - headers: Mapping[str, str] = SUCCESSFUL_RESPONSE_HEADERS + headers: Mapping[str, str] = field( + default_factory=lambda: SUCCESSFUL_RESPONSE_HEADERS + ) @dataclass @@ -325,7 +327,9 @@ class UnsuccessfulResponse: # Expected value of inverse of non_retryable attribute of exception. retryable_exception: bool = True body_json: Optional[Callable[[dict[str, Any]], bool]] = None - headers: Mapping[str, str] = UNSUCCESSFUL_RESPONSE_HEADERS + headers: Mapping[str, str] = field( + default_factory=lambda: UNSUCCESSFUL_RESPONSE_HEADERS + ) class _TestCase: