For automation purposes, or to simplify the creation of a __post_init__() method which, in this case, sets the default value for a dataclass field based on the value passed in to __init__() for another field, what you can do is define (and use) a metaclass such as below which will auto-generate a __post_init__() method for the class.
Note that in below example, the metaclass relies on a metadata dict object that you pass in as a dataclasses.field() argument for a field. In the below case I'm passing in 'default_from' as the key in the metadata, but really this can be any other name which conveys the intent a bit more clearly.
It's admittedly not very pretty to look at, but in any case here's how I'd define the metaclass helper function, named default_meta in this case:
from __future__ import annotations
# noinspection PyProtectedMember
from dataclasses import Field, _create_fn
from typing import Any
def default_meta(name: str, bases: tuple[type, ...], cls_dict: dict[str, Any]):
"""Metaclass which takes the same arguments as builtin `type`"""
post_init_lines = []
# This metaclass runs before the `@dataclass` decorator, so we don't have
# access to `dataclasses.fields`, unfortunately. Instead, we can determine
# the valid attributes that have a :class:`dataclasses.Field` value ourselves.
for attr, attr_val in cls_dict.items():
# skip the attribute if it doesn't match what we're looking for
if not isinstance(attr_val, Field):
continue
default_from = attr_val.metadata.get('default_from')
if not default_from:
continue
# set the default for this field based on the value passed in to
# __init__() for the specified dataclass field.
post_init_lines.append(f'if not self.{attr}:')
post_init_lines.append(f' self.{attr} = self.{default_from}')
# Set the __post_init__() method on the class
#
# Bonus or rather a "TODO": consider checking if a __post_init__() method
# is a already defined for the class.
cls_dict['__post_init__'] = _create_fn(
'__post_init__',
('self', ),
post_init_lines
)
# create and return the new class
return type(name, bases, cls_dict)
And you'd use (the above metaclass) like this:
from __future__ import annotations
# TODO update the module name here
from other_module import default_meta
from dataclasses import dataclass, field
@dataclass
class FunSig(metaclass=default_meta):
name: str
result_name: str = field(default=None, metadata={'default_from': 'name'})
# usage
print(FunSig('test', result_name='setting an explicit value!'))
print(FunSig('hello world!'))
Output:
FunSig(name='test', result_name='setting an explicit value!')
FunSig(name='hello world!', result_name='hello world!')
If you wanted to fine-tune it or abstract away the implementation a bit more, I don't see
why cleaner syntax such as below shouldn't work either:
@dataclass
class FunSig(metaclass=default_meta):
name: str
result_name: str = DefaultFrom(field='name')
Side note: I definitely think it would be cool it dataclasses adds support for this scenario in the future.