Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/fixed/wobbling-teacup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Decoding a dataclass with `field(init=False)` fields no longer raises `TypeError`; those fields are left out of the constructor call and their serialized values are restored afterwards, including on frozen dataclasses.
17 changes: 15 additions & 2 deletions temporalio/converter/_payload_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -996,24 +996,37 @@ def value_to_type(
fields = dataclasses.fields(hint)
field_hints = get_type_hints(hint)
field_values = {}
non_init_field_values = {}
for field in fields:
field_value = value.get(field.name, dataclasses.MISSING)
# We do not check whether field is required here. Rather, we let the
# attempted instantiation of the dataclass raise if a field is
# missing
if field_value is not dataclasses.MISSING:
# Convert init=False fields too, so a mismatched type still
# rejects this dataclass when trying union members
try:
field_values[field.name] = value_to_type(
converted = value_to_type(
field_hints[field.name], field_value, custom_converters
)
except Exception as err:
raise TypeError(
f"Failed converting field {field.name} on dataclass {hint}"
) from err
if field.init:
field_values[field.name] = converted
else:
non_init_field_values[field.name] = converted
# Simply instantiate the dataclass. This will fail as expected when
# missing required fields.
# TODO(cretz): Want way to convert snake case to camel case?
return hint(**field_values)
obj = hint(**field_values)
# init=False fields cannot be passed to __init__ but are serialized, so
# restore them afterwards. object.__setattr__ also works on frozen
# dataclasses.
for name, field_value in non_init_field_values.items():
object.__setattr__(obj, name, field_value)
return obj

# Pydantic model instance
# Pydantic users should use Pydantic v2 with
Expand Down
28 changes: 28 additions & 0 deletions tests/test_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,24 @@ class NestedDataClass:
qux: UUID | None = None


@dataclass
class InitFalseDataClass:
foo: str
bar: str = dataclasses.field(default="default-bar", init=False)


@dataclass(frozen=True)
class FrozenInitFalseDataClass:
foo: str
bar: str = dataclasses.field(default="default-bar", init=False)


@dataclass
class InitFalseIntDataClass:
foo: str
bar: int = dataclasses.field(default=0, init=False)


class MyTypedDict(TypedDict):
foo: str
bar: MyDataClass
Expand Down Expand Up @@ -633,6 +651,16 @@ def fail(hint: Any, value: Any) -> None:
fail(NestedDataClass, {})
# Additional dataclass fields is ok
ok(NestedDataClass, {"foo": "bar", "unknownfield": "baz"}, NestedDataClass("bar"))
# Dataclass with init=False fields restores their values
ok(InitFalseDataClass, InitFalseDataClass("hello"))
init_false = InitFalseDataClass("hello")
init_false.bar = "custom-bar"
ok(InitFalseDataClass, init_false)
frozen_init_false = FrozenInitFalseDataClass("hello")
object.__setattr__(frozen_init_false, "bar", "custom-bar")
ok(FrozenInitFalseDataClass, frozen_init_false)
# init=False field types still decide which union member matches
ok(InitFalseIntDataClass | InitFalseDataClass, init_false)

# Optional/Union
ok(int | None, 5)
Expand Down
Loading