Skip to content

Commit

Permalink
feat: ini decorator (#102)
Browse files Browse the repository at this point in the history
* chore(docs): remove deprecated example from `ini_loader` docstring

* feat: ini decorator

* chore: include ini in default value tests
  • Loading branch information
maxb2 authored Oct 12, 2023
1 parent 4a7e190 commit 80bab13
Show file tree
Hide file tree
Showing 5 changed files with 80 additions and 10 deletions.
4 changes: 3 additions & 1 deletion docs/examples/default_config.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
4 changes: 3 additions & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
8 changes: 3 additions & 5 deletions tests/test_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
),
),
]

Expand Down Expand Up @@ -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"])
Expand Down
66 changes: 65 additions & 1 deletion typer_config/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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,
Expand All @@ -27,6 +28,7 @@

if TYPE_CHECKING: # pragma: no cover
from .__typing import (
ConfigDict,
ConfigDumper,
ConfigParameterCallback,
FilePath,
Expand Down Expand Up @@ -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.
Expand Down
8 changes: 6 additions & 2 deletions typer_config/loaders.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down

0 comments on commit 80bab13

Please sign in to comment.