From c632756bf3ca1ee845f3c83e243d742028ec6981 Mon Sep 17 00:00:00 2001 From: Alex Mazzeo Date: Mon, 27 Jul 2026 14:23:34 -0700 Subject: [PATCH 1/2] Add type_hint to TransferTypeConverter.from_transfer_type to allow inspection of type args during conversion --- temporalio/converter/_payload_converter.py | 13 +++- tests/test_converter.py | 71 ++++++++++++++++++++++ 2 files changed, 81 insertions(+), 3 deletions(-) diff --git a/temporalio/converter/_payload_converter.py b/temporalio/converter/_payload_converter.py index f10b6a4e0..a8bc35e28 100644 --- a/temporalio/converter/_payload_converter.py +++ b/temporalio/converter/_payload_converter.py @@ -81,9 +81,14 @@ def to_transfer_type(self, value: ValueT) -> TransferTypeT: raise NotImplementedError @abstractmethod - def from_transfer_type(self, value: TransferTypeT) -> ValueT: + def from_transfer_type( + self, value: TransferTypeT, type_hint: type[ValueT] + ) -> ValueT: """Convert a transfer type value to its user-facing value. + ``type_hint`` is the requested user-facing type, including concrete + generic arguments. + .. warning:: This API is experimental and subject to change. """ @@ -638,8 +643,10 @@ def from_payloads( payloads, typing.cast("list[type]", inner_type_hints) ) return [ - converter.from_transfer_type(value) if converter is not None else value - for value, converter in zip(values, converters) + converter.from_transfer_type(value, type_hint) + if converter is not None + else value + for value, converter, type_hint in zip(values, converters, type_hints) ] def with_context(self, context: SerializationContext) -> Self: diff --git a/tests/test_converter.py b/tests/test_converter.py index b5e10c518..7c4b60a9b 100644 --- a/tests/test_converter.py +++ b/tests/test_converter.py @@ -14,11 +14,15 @@ from typing import ( Any, Dict, # type:ignore[reportDeprecated] + Generic, Literal, NewType, + TypeVar, + cast, get_args, get_type_hints, ) +import typing from uuid import UUID, uuid4 import nexusrpc @@ -278,6 +282,7 @@ def to_transfer_type( def from_transfer_type( self, value: temporalio.api.common.v1.WorkflowExecution, + type_hint: type[TemporalTransferTypeValue], ) -> TemporalTransferTypeValue: return TemporalTransferTypeValue(value=value.workflow_id) @@ -305,6 +310,7 @@ def to_transfer_type( def from_transfer_type( self, value: temporalio.api.common.v1.WorkflowExecution, + type_hint: type[TemporalTransferTypeValueWithoutHint], ) -> TemporalTransferTypeValueWithoutHint: return TemporalTransferTypeValueWithoutHint(value=value.workflow_id) @@ -315,6 +321,47 @@ class TemporalTransferTypeValueWithoutHint: value: str +T = TypeVar("T") + + +@dataclass +class TemporalTransferTypeGenericValue(Generic[T]): + value: T + + +class TemporalTransferTypeGenericValueConverter( + TransferTypeConverter[ + TemporalTransferTypeGenericValue[T], + temporalio.api.common.v1.WorkflowExecution, + ] +): + transfer_type = temporalio.api.common.v1.WorkflowExecution + + def to_transfer_type( + self, value: TemporalTransferTypeGenericValue[T] + ) -> temporalio.api.common.v1.WorkflowExecution: + return temporalio.api.common.v1.WorkflowExecution( + workflow_id=str(value.value), + run_id="run-id", + ) + + def from_transfer_type( + self, + value: temporalio.api.common.v1.WorkflowExecution, + type_hint: type[TemporalTransferTypeGenericValue[T]], + ) -> TemporalTransferTypeGenericValue[T]: + converted_value: str | int = value.workflow_id + if typing.get_args(type_hint)[0] is int: + converted_value = int(converted_value) + return TemporalTransferTypeGenericValue(value=cast(T, converted_value)) + + +# Register after both classes are defined so the generic type can be resolved. +transfer_type_convertible(TemporalTransferTypeGenericValueConverter)( + TemporalTransferTypeGenericValue +) + + class CustomDefaultPayloadConverter(DefaultPayloadConverter): pass @@ -358,6 +405,30 @@ def test_temporal_transfer_type_payload_converter_without_transfer_type_hint(): ) +@pytest.mark.parametrize( + ("value", "type_hint"), + [ + ( + TemporalTransferTypeGenericValue("workflow-id"), + TemporalTransferTypeGenericValue[str], + ), + ( + TemporalTransferTypeGenericValue(123), + TemporalTransferTypeGenericValue[int], + ), + ], +) +def test_temporal_transfer_type_payload_converter_with_generic_value( + value: TemporalTransferTypeGenericValue[T], + type_hint: type[TemporalTransferTypeGenericValue[T]], +): + converter = DataConverter.default.payload_converter + + payload = converter.to_payload(value) + + assert converter.from_payload(payload, type_hint) == value + + def test_transfer_type_convertible_rejects_existing_converter(): with pytest.raises(TypeError, match="already has a transfer type converter"): transfer_type_convertible(TemporalTransferTypeValueConverter)( From edbc3db835129433653539769246f44a91dbc9a2 Mon Sep 17 00:00:00 2001 From: Alex Mazzeo Date: Mon, 27 Jul 2026 14:29:21 -0700 Subject: [PATCH 2/2] Run formatter --- tests/test_converter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_converter.py b/tests/test_converter.py index 7c4b60a9b..f1a056c5f 100644 --- a/tests/test_converter.py +++ b/tests/test_converter.py @@ -6,6 +6,7 @@ import logging import sys import traceback +import typing from collections import deque from collections.abc import Iterable, Mapping, MutableMapping, Sequence from dataclasses import dataclass @@ -22,7 +23,6 @@ get_args, get_type_hints, ) -import typing from uuid import UUID, uuid4 import nexusrpc