maxb2 / typer-config

Utilities for working with configuration files in typer CLIs.
https://maxb2.github.io/typer-config/
MIT License
24 stars 1 forks source link

Add example for location of config file #158

Open ssbarnea opened 8 months ago

ssbarnea commented 8 months ago

Most tools are able to detect existing config file in current directory, its parents or user home directory. Currently I seen no support in typer-config for having a dynamic detection of the config file.

Even worse, when passing the default_value="mytool.yml" to it, it will fail if the config file is not present.

How can we make it not fail when a config file does not exist and also to document the default config filename?

--config                    TEXT  Configuration file.
maxb2 commented 8 months ago

Sorry for the late reply. I've been trying to fix this issue for several days.

Let's start with the easy stuff.

Searching Parent Directories

There is not explicit support for searching parent directories, but I designed this library to be easy to extend. You can define your own file loader with that behavior and plug it in to the rest of the typer-config machinery. Here is an example:

from pathlib import Path
from typing import Any, Dict, Union

import typer
from typing_extensions import Annotated

from typer_config import conf_callback_factory
from typer_config.decorators import use_config
from typer_config.loaders import yaml_loader

def search_path_parents(filename: Union[str, Path]) -> Path:
    path = Path(filename).absolute()

    for _dir in path.parents:
        _path = _dir.joinpath(path.name)
        if _path.exists():
            return _path

    raise FileNotFoundError(
        f"Could not find {path.name} in {path.parent} or any of its parents."
    )

def parent_loader(param_value: str) -> Dict[str, Any]:
    param_value = search_path_parents(param_value)
    conf = yaml_loader(param_value)
    return conf

### You can define the same loader using the loader_transformer combinator:
#
# from typer_config.loaders import loader_transformer

# parent_loader = loader_transformer(
#     yaml_loader,
#     param_transformer=search_path_parents,
# )

parent_callback = conf_callback_factory(parent_loader)

app = typer.Typer()

@app.command()
@use_config(parent_callback)
def main(
    arg1: str,
    opt1: Annotated[str, typer.Option()],
    opt2: Annotated[str, typer.Option()] = "hello",
):
    typer.echo(f"{opt1} {opt2} {arg1}")

if __name__ == "__main__":
    app()

I could add this functionality in the default decorators, e.g. @use_yaml_config(search_parents=True). I think this particular behavior would be okay to add, but I don't want the decorators to become a soup of feature flags with every other feature suggestion. Everybody has their own opinion on default behavior.

Missing file crashes

I did not have a test for the case to ensure you can get to --help when you have a default value that is a not an existing file (it's surprisingly hard to test all the edge cases of this library 😅), so this bug slipped through. I spent several days poking around the typer/click internals to see how I can fix this. Since click parses the parameters in a certain order (and I must use is_eager=True), it's impossible to tell if --help has been provided while parsing --config. So, I can't distinguish between the cli and cli --help invocations since the default value is passed to --config in both cases. I see two options:

  1. Emit a warning when the config file isn't found rather than raising a FileNotFoundError. This will let the execution continue and most likely fail on a missing argument. For example:

    /Users/maxb2/Programs/typer-config/typer_config/callbacks.py:61: UserWarning: Could not load config from config.yml.
      warn(f"Could not load config from {param_value}.")
    Usage: app.py [OPTIONS] ARG1
    Try 'app.py --help' for help.
    
    Error: Missing argument 'ARG1'.

    I could see this being confusing to the end user if they don't notice the message at the top. Plus, you will no longer get the nice error message that typer provides when you give invalid input to --config.

  2. Fork/extend click to adjust the order in which parameters are parsed so that --help always comes before --config. This would be a lot of work and a maintenance nightmare. I don't have enough free time to do that.

I think option 1 is a decent compromise (I can even make the error handling configurable for those that want to raise errors rather than just warn).

Let me know what you think! I'll try to get an alpha release out soon if you would like to test it and give feedback.

kierun commented 8 months ago

My $0.02 would be to emit a warning if there is no configuration file. If we can catch the UserWarning, then we can either ignore it (and use whatever the defaults are) or create a configuration file based on some template.

Please let me know if you want me to test anything…