From 80bab13a855a46e167507e105c524cc35b18cc34 Mon Sep 17 00:00:00 2001 From: Matthew Anderson Date: Thu, 12 Oct 2023 12:04:46 -0500 Subject: [PATCH] feat: ini decorator (#102) * chore(docs): remove deprecated example from `ini_loader` docstring * feat: ini decorator * chore: include ini in default value tests --- docs/examples/default_config.md | 4 +- docs/index.md | 4 +- tests/test_example.py | 8 ++-- typer_config/decorators.py | 66 ++++++++++++++++++++++++++++++++- typer_config/loaders.py | 8 +++- 5 files changed, 80 insertions(+), 10 deletions(-) diff --git a/docs/examples/default_config.md b/docs/examples/default_config.md index d49a3af..17d4889 100644 --- a/docs/examples/default_config.md +++ b/docs/examples/default_config.md @@ -26,7 +26,9 @@ if __name__ == "__main__": app() ``` -1. This package also provides `use_json_config`, `use_toml_config`, and `use_dotenv_config` for those file formats. +1. This package also provides `use_json_config`, `use_toml_config`, `use_ini_config`, and `use_dotenv_config` for those file formats. + > Note that since INI requires a top-level section `use_ini_config` requires a list of strings that express the path to the section + you wish to use, e.g. `@use_ini_config(["section", "subsection", ...])`. With a config file: diff --git a/docs/index.md b/docs/index.md index f9a277e..e1ea5a3 100644 --- a/docs/index.md +++ b/docs/index.md @@ -39,8 +39,10 @@ if __name__ == "__main__": app() ``` -1. This package also provides `@use_json_config`, `@use_toml_config`, and `@use_dotenv_config` for those file formats. +1. This package also provides `use_json_config`, `use_toml_config`, `use_ini_config`, and `use_dotenv_config` for those file formats. You can also use your own loader function and the `@use_config(loader_func)` decorator. + > Note that since INI requires a top-level section `use_ini_config` requires a list of strings that express the path to the section + you wish to use, e.g. `@use_ini_config(["section", "subsection", ...])`. 2. The `app.command()` decorator registers the function object in a lookup table, so we must transform our command before registration. diff --git a/tests/test_example.py b/tests/test_example.py index 174806f..bee6f63 100644 --- a/tests/test_example.py +++ b/tests/test_example.py @@ -92,7 +92,9 @@ def main( ( str(HERE.joinpath("config.ini")), INI_CALLBACK, - functools.partial(typer_config.decorators.use_config, callback=INI_CALLBACK), + functools.partial( + typer_config.decorators.use_ini_config, section=["simple_app"] + ), ), ] @@ -173,10 +175,6 @@ def test_simple_example_decorated_default(simple_app_decorated, confs): conf, _, dec = confs - # skip ini config - if conf.endswith(".ini"): - return - _app = simple_app_decorated(dec, default_value=conf) result = RUNNER.invoke(_app, ["--help"]) diff --git a/typer_config/decorators.py b/typer_config/decorators.py index ffb0ca4..69eab02 100644 --- a/typer_config/decorators.py +++ b/typer_config/decorators.py @@ -5,7 +5,7 @@ from enum import Enum from functools import wraps from inspect import Parameter, signature -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, List, Optional from typer import Option @@ -19,6 +19,7 @@ from .dumpers import json_dumper, toml_dumper, yaml_dumper from .loaders import ( dotenv_loader, + ini_loader, json_loader, loader_transformer, toml_loader, @@ -27,6 +28,7 @@ if TYPE_CHECKING: # pragma: no cover from .__typing import ( + ConfigDict, ConfigDumper, ConfigParameterCallback, FilePath, @@ -292,6 +294,68 @@ def main(...): return use_config(callback=callback, param_name=param_name, param_help=param_help) +def use_ini_config( + section: List[str], + param_name: TyperParameterName = "config", + param_help: str = "Configuration file.", + default_value: Optional[TyperParameterValue] = None, +) -> TyperCommandDecorator: + """Decorator for using INI configuration on a typer command. + + Usage: + ```py + import typer + from typer_config.decorators import use_ini_config + + app = typer.Typer() + + @app.command() + @use_ini_config(["section", "subsection"]) + def main(...): + ... + ``` + + Args: + section (List[str]): List of nested sections to access in the INI file. + param_name (str, optional): name of config parameter. Defaults to "config". + param_help (str, optional): config parameter help string. + Defaults to "Configuration file.". + default_value (TyperParameterValue, optional): default config parameter value. + Defaults to None. + + Returns: + TyperCommandDecorator: decorator to apply to command + """ + + def _get_section(_section: List[str], config: ConfigDict) -> ConfigDict: + for sect in _section: + config = config.get(sect, {}) + + return config + + if default_value is not None: + callback = conf_callback_factory( + loader_transformer( + ini_loader, + loader_conditional=lambda param_value: param_value, + param_transformer=lambda param_value: param_value + if param_value + else default_value, + config_transformer=lambda config: _get_section(section, config), + ) + ) + else: + callback = conf_callback_factory( + loader_transformer( + ini_loader, + loader_conditional=lambda param_value: param_value, + config_transformer=lambda config: _get_section(section, config), + ) + ) + + return use_config(callback=callback, param_name=param_name, param_help=param_help) + + def dump_config(dumper: ConfigDumper, location: FilePath) -> TyperCommandDecorator: """Decorator for dumping a config file with parameters from an invocation of a typer command. diff --git a/typer_config/loaders.py b/typer_config/loaders.py index ef2609d..6050518 100644 --- a/typer_config/loaders.py +++ b/typer_config/loaders.py @@ -196,10 +196,14 @@ def ini_loader(param_value: TyperParameterValue) -> ConfigDict: Note: INI files must have sections at the top level. - You probably want to combine this with `subpath_loader`. + You probably want to combine this with `loader_transformer` + to extract the correct section. For example: ```py - ini_section_loader = subpath_loader(ini_loader, ["section"]) + ini_section_loader = loader_transformer( + ini_loader, + config_transformer=lambda config: config["section"], + ) ``` Args: