Search code examples
pythonsingletonpython-dataclasses

how to elegant use python dataclass singleton for config?


for some reason, I use the toml config instead of the .py config.

then, the singleton nightmare comes, continuously come up with not init, or RecursionError

is there an elegant way to use a python dataclass in project config?

@dataclasses.dataclass(frozen=True)
class Config:
    _instance: typing.Optional[typing.Self] = dataclasses.field(init=False, repr=False)

    Targets: list[Target]

    FFmpeg: typing.Optional[FFmpeg]

    Whisper: typing.Optional[Whisper]

    Translate: typing.Optional[Translate]

    Srt: typing.Optional[Srt]

    Log: typing.Optional[Log]

    @classmethod
    def init_config(cls) -> typing.Self:
        if cls._instance is None:
            config = pathlib.Path().absolute().joinpath("config.toml")
            with open(config, "rb") as f:
                data = tomllib.load(f)
            log_config = Log(level=logging.DEBUG, count=0, size=0)
            targets: list[Target] = list()
            srt_config = Srt(overwrite=data['srt']['overwrite'], bilingual=data['srt']['bilingual'])
            cls._instance = Config(Targets=targets,
                                   FFmpeg=ffmpeg_config,
                                   Whisper=whisper_config,
                                   Translate=translate_config,
                                   Srt=srt_config,
                                   Log=log_config)
            return cls._instance


CONFIG: typing.Optional[Config] = Config.init_config()

or some other errors like below:

    if cls._instance is None:
       ^^^^^^^^^^^^^
AttributeError: type object 'Config' has no attribute '_instance'

Solution

  • You can use functools.cache.

    @dataclasses.dataclass(frozen=True)
    class Config:
        Targets: list[Target]
        FFmpeg: typing.Optional[FFmpeg]
        Whisper: typing.Optional[Whisper]
        Translate: typing.Optional[Translate]
        Srt: typing.Optional[Srt]
        Log: typing.Optional[Log]
    
    @functools.cache
    def config():
        config_filename = pathlib.Path().absolute().joinpath("config.toml")
        with open(config_filename, "rb") as f:
            data = tomllib.load(f)
        log_config = Log(level=logging.DEBUG, count=0, size=0)
        targets: list[Target] = list()
        srt_config = Srt(overwrite=data['srt']['overwrite'], bilingual=data['srt']['bilingual'])
        config_ = Config(Targets=targets,
                         FFmpeg=ffmpeg_config,
                         Whisper=whisper_config,
                         Translate=translate_config,
                         Srt=srt_config,
                         Log=log_config)
        return config
    

    An addition I like it the possibility to clear the cache to generate the config again. If this is useful usecase for you, consider to never store the returned config anywhere, "just" call config().FFmpeg for example. The function call overhead is negligeable.