Skip to content

Commit 981878b

Browse files
committed
PR Feedback addressed.
1 parent 84528fb commit 981878b

File tree

3 files changed

+143
-53
lines changed

3 files changed

+143
-53
lines changed

README.md

+46
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,52 @@ my_data_converter = dataclasses.replace(
446446

447447
Now `IPv4Address` can be used in type hints including collections, optionals, etc.
448448

449+
When the `JSONPlainPayloadConverter` is used a class can implement `to_temporal_json` and `from_temporal_json` methods to
450+
support custom conversion logic. Custom conversion of generic classes is supported.
451+
These methods should have the following signatures:
452+
453+
```
454+
class MyClass:
455+
...
456+
```
457+
`from_temporal_json` be either classmethod:
458+
```
459+
@classmethod
460+
def from_temporal_json(cls, json: Any) -> MyClass:
461+
...
462+
```
463+
or static method:
464+
```
465+
@staticmethod
466+
def from_temporal_json(json: Any) -> MyClass:
467+
...
468+
```
469+
`to_temporal_json` is always an instance method:
470+
```
471+
def to_temporal_json(self) -> Any:
472+
...
473+
```
474+
The to_json should return the same Python JSON types produced by JSONEncoder:
475+
```
476+
+-------------------+---------------+
477+
| Python | JSON |
478+
+===================+===============+
479+
| dict | object |
480+
+-------------------+---------------+
481+
| list, tuple | array |
482+
+-------------------+---------------+
483+
| str | string |
484+
+-------------------+---------------+
485+
| int, float | number |
486+
+-------------------+---------------+
487+
| True | true |
488+
+-------------------+---------------+
489+
| False | false |
490+
+-------------------+---------------+
491+
| None | null |
492+
+-------------------+---------------+
493+
```
494+
449495
### Workers
450496

451497
Workers host workflows and/or activities. Here's how to run a worker:

temporalio/converter.py

+46-43
Original file line numberDiff line numberDiff line change
@@ -489,43 +489,6 @@ class AdvancedJSONEncoder(json.JSONEncoder):
489489
490490
This encoder supports dataclasses and all iterables as lists.
491491
492-
A class can implement to_json and from_json methods to support custom conversion logic.
493-
Custom conversion of generic classes is supported.
494-
These methods should have the following signatures:
495-
496-
.. code-block:: python
497-
498-
class MyClass:
499-
...
500-
501-
@classmethod
502-
def from_json(cls, json: Any) -> MyClass:
503-
...
504-
505-
def to_json(self) -> Any:
506-
...
507-
508-
The to_json should return the same Python JSON types produced by JSONEncoder:
509-
510-
+-------------------+---------------+
511-
| Python | JSON |
512-
+===================+===============+
513-
| dict | object |
514-
+-------------------+---------------+
515-
| list, tuple | array |
516-
+-------------------+---------------+
517-
| str | string |
518-
+-------------------+---------------+
519-
| int, float | number |
520-
+-------------------+---------------+
521-
| True | true |
522-
+-------------------+---------------+
523-
| False | false |
524-
+-------------------+---------------+
525-
| None | null |
526-
+-------------------+---------------+
527-
528-
529492
It also uses Pydantic v1's "dict" methods if available on the object,
530493
but this is deprecated. Pydantic users should upgrade to v2 and use
531494
temporalio.contrib.pydantic.pydantic_data_converter.
@@ -538,11 +501,11 @@ def default(self, o: Any) -> Any:
538501
"""
539502
# Custom encoding and decoding through to_json and from_json
540503
# to_json should be an instance method with only self argument
541-
to_json = "to_json"
504+
to_json = "to_temporal_json"
542505
if hasattr(o, to_json):
543506
attr = getattr(o, to_json)
544507
if not callable(attr):
545-
raise TypeError(f"Type {o.__class__}: to_json must be a method")
508+
raise TypeError(f"Type {o.__class__}: {to_json} must be a method")
546509
return attr()
547510

548511
# Dataclass support
@@ -570,6 +533,44 @@ class JSONPlainPayloadConverter(EncodingPayloadConverter):
570533
571534
For decoding, this uses type hints to attempt to rebuild the type from the
572535
type hint.
536+
537+
A class can implement to_json and from_temporal_json methods to support custom conversion logic.
538+
Custom conversion of generic classes is supported.
539+
These methods should have the following signatures:
540+
541+
.. code-block:: python
542+
543+
class MyClass:
544+
...
545+
546+
@classmethod
547+
def from_temporal_json(cls, json: Any) -> MyClass:
548+
...
549+
550+
def to_temporal_json(self) -> Any:
551+
...
552+
553+
The to_json should return the same Python JSON types produced by JSONEncoder:
554+
555+
+-------------------+---------------+
556+
| Python | JSON |
557+
+===================+===============+
558+
| dict | object |
559+
+-------------------+---------------+
560+
| list, tuple | array |
561+
+-------------------+---------------+
562+
| str | string |
563+
+-------------------+---------------+
564+
| int, float | number |
565+
+-------------------+---------------+
566+
| True | true |
567+
+-------------------+---------------+
568+
| False | false |
569+
+-------------------+---------------+
570+
| None | null |
571+
+-------------------+---------------+
572+
573+
573574
"""
574575

575576
_encoder: Optional[Type[json.JSONEncoder]]
@@ -1476,12 +1477,14 @@ def value_to_type(
14761477
return value
14771478

14781479
# Has from_json class method (must have to_json as well)
1479-
from_json = "from_json"
1480+
from_json = "from_temporal_json"
14801481
if hasattr(hint, from_json):
14811482
attr = getattr(hint, from_json)
1482-
attr_cls = getattr(attr, "__self__")
1483-
if not callable(attr) or not attr_cls == origin:
1484-
raise TypeError(f"Type {hint}: temporal_from_json must be a class method")
1483+
attr_cls = getattr(attr, "__self__", None)
1484+
if not callable(attr) or (attr_cls is not None and attr_cls is not origin):
1485+
raise TypeError(
1486+
f"Type {hint}: {from_json} must be a staticmethod or classmethod"
1487+
)
14851488
return attr(value)
14861489

14871490
is_union = origin is Union

tests/worker/test_workflow.py

+51-10
Original file line numberDiff line numberDiff line change
@@ -2256,10 +2256,32 @@ def __init__(self, field1: str):
22562256
self.field2 = "foo"
22572257

22582258
@classmethod
2259-
def from_json(cls, json_obj: Any) -> MyGenericClass:
2259+
def from_temporal_json(cls, json_obj: Any) -> MyGenericClass:
22602260
return MyGenericClass(str(json_obj) + "_from_json")
22612261

2262-
def to_json(self) -> Any:
2262+
def to_temporal_json(self) -> Any:
2263+
return self.field1 + "_to_json"
2264+
2265+
def assert_expected(self, value: str) -> None:
2266+
# Part of the assertion is that this is the right type, which is
2267+
# confirmed just by calling the method. We also check the field.
2268+
assert str(self.field1) == value
2269+
2270+
2271+
class MyGenericClassWithStatic(typing.Generic[T]):
2272+
"""
2273+
Demonstrates custom conversion and that it works even with generic classes.
2274+
"""
2275+
2276+
def __init__(self, field1: str):
2277+
self.field1 = field1
2278+
self.field2 = "foo"
2279+
2280+
@staticmethod
2281+
def from_temporal_json(json_obj: Any) -> MyGenericClass:
2282+
return MyGenericClass(str(json_obj) + "_from_json")
2283+
2284+
def to_temporal_json(self) -> Any:
22632285
return self.field1 + "_to_json"
22642286

22652287
def assert_expected(self, value: str) -> None:
@@ -2281,6 +2303,13 @@ async def generic_class_typed_activity(
22812303
return param
22822304

22832305

2306+
@activity.defn
2307+
async def generic_class_typed_activity_with_static(
2308+
param: MyGenericClassWithStatic[str],
2309+
) -> MyGenericClassWithStatic[str]:
2310+
return param
2311+
2312+
22842313
@runtime_checkable
22852314
@workflow.defn(name="DataClassTypedWorkflow")
22862315
class DataClassTypedWorkflowProto(Protocol):
@@ -2347,13 +2376,13 @@ async def run(self, param: MyDataClass) -> MyDataClass:
23472376
generic_param.assert_expected(
23482377
"some_value2_to_json_from_json_to_json_from_json"
23492378
)
2350-
generic_param = MyGenericClass[str]("some_value2")
2351-
generic_param = await workflow.execute_local_activity(
2352-
generic_class_typed_activity,
2353-
generic_param,
2379+
generic_param_s = MyGenericClassWithStatic[str]("some_value2")
2380+
generic_param_s = await workflow.execute_local_activity(
2381+
generic_class_typed_activity_with_static,
2382+
generic_param_s,
23542383
start_to_close_timeout=timedelta(seconds=30),
23552384
)
2356-
generic_param.assert_expected(
2385+
generic_param_s.assert_expected(
23572386
"some_value2_to_json_from_json_to_json_from_json"
23582387
)
23592388
child_handle = await workflow.start_child_workflow(
@@ -2400,7 +2429,11 @@ async def test_workflow_dataclass_typed(client: Client, env: WorkflowEnvironment
24002429
async with new_worker(
24012430
client,
24022431
DataClassTypedWorkflow,
2403-
activities=[data_class_typed_activity, generic_class_typed_activity],
2432+
activities=[
2433+
data_class_typed_activity,
2434+
generic_class_typed_activity,
2435+
generic_class_typed_activity_with_static,
2436+
],
24042437
) as worker:
24052438
val = MyDataClass(field1="some value")
24062439
handle = await client.start_workflow(
@@ -2427,7 +2460,11 @@ async def test_workflow_separate_protocol(client: Client):
24272460
async with new_worker(
24282461
client,
24292462
DataClassTypedWorkflow,
2430-
activities=[data_class_typed_activity, generic_class_typed_activity],
2463+
activities=[
2464+
data_class_typed_activity,
2465+
generic_class_typed_activity,
2466+
generic_class_typed_activity_with_static,
2467+
],
24312468
) as worker:
24322469
assert isinstance(DataClassTypedWorkflow(), DataClassTypedWorkflowProto)
24332470
val = MyDataClass(field1="some value")
@@ -2451,7 +2488,11 @@ async def test_workflow_separate_abstract(client: Client):
24512488
async with new_worker(
24522489
client,
24532490
DataClassTypedWorkflow,
2454-
activities=[data_class_typed_activity, generic_class_typed_activity],
2491+
activities=[
2492+
data_class_typed_activity,
2493+
generic_class_typed_activity,
2494+
generic_class_typed_activity_with_static,
2495+
],
24552496
) as worker:
24562497
assert issubclass(DataClassTypedWorkflow, DataClassTypedWorkflowAbstract)
24572498
val = MyDataClass(field1="some value")

0 commit comments

Comments
 (0)