3

I have a simple Typer application:

import typer

app = typer.Typer()

@app.command()
def say_hi():
    print("Hi")

@app.callback()
def main():
    pass

if __name__ == "__main__":
    app()

I would like to use Hydra for the configuration management of the app, however I am not sure how to do that without losing the ability to override the config from the CLI.

My first attempt was:

import hydra
import typer
from omegaconf import DictConfig, OmegaConf

app = typer.Typer()

@app.command()
def say_hi():
    print("Hi")

@app.callback()
@hydra.main(config_path="conf", config_name="config")
def main(cfg: DictConfig) -> None:
    print(OmegaConf.to_yaml(cfg))

if __name__ == "__main__":
    app()

But I get an error saying:

RuntimeError: Type not yet supported: <class 'omegaconf.dictconfig.DictConfig'>

If I remove the DictConfig type annotation I get an error that cfg is missing.

I saw in Hydra docs the Compose API that allows to initialize the config without decorators:

@app.callback()
def main() -> None:
    with initialize(config_path="conf", job_name="test_app"):
        cfg = compose(config_name="config")
        print(OmegaConf.to_yaml(cfg))

but It seems that I can't override config from the command line in this case as those values are not recognized by the Typer app.

Any recommendations how it can be resolved?

Jasha
  • 5,507
  • 2
  • 33
  • 44
Gabio
  • 9,126
  • 3
  • 12
  • 32

1 Answers1

2

The compose function accepts an optional list of override strings:

with initialize(config_path="conf", job_name="test_app"):
    cfg = compose(config_name="config", overrides=["db=mysql", "db.user=me"])

You will need to get a list of override strings from the command line, then pass that list to compose. Here is an example using Typer; similar patterns could work for e.g. argparse or click. (Disclaimer: I am not a Typer expert)

from typing import List, Optional

import typer
from omegaconf import OmegaConf, DictConfig

from hydra import compose, initialize

app = typer.Typer()

def my_compose(overrides: Optional[List[str]]) -> DictConfig:
    with initialize(config_path="conf", job_name="test_app"):
        return compose(config_name="config", overrides=overrides)

@app.command()
def say_hi(overrides: Optional[List[str]] = typer.Argument(None)):
    print("HI!")
    print(f"Got {overrides=}")
    cfg = my_compose(overrides)
    print("\nHydra config:")
    print(OmegaConf.to_yaml(cfg))

@app.command()
def say_bye(overrides: Optional[List[str]] = typer.Argument(None)):
    cfg = my_compose(overrides)
    ...
    print("BYE!")

if __name__ == "__main__":
    app()
$ python my_app.py say-hi +foo=bar +baz=qux
HI!
Got overrides=('+foo=bar', '+baz=qux')

Hydra config:
foo: bar
baz: qux

$ python my_app.py say-bye
BYE!
Jasha
  • 5,507
  • 2
  • 33
  • 44
  • Thanks for the thorough answer! Do you know about a way to solve it without using the Compose API? It is mentioned in the docs that `Please avoid using the Compose API in cases where @hydra.main() can be used. Doing so forfeits many of the benefits of Hydra (e.g., Tab completion, Multirun, Working directory management, Logging management and more)` – Gabio Jan 25 '22 at 08:43
  • 1
    The problem is that both Typer and `hydra.main` are controlled by the state of `sys.argv`. (meanwhile Hydra's `compose` does not interact by `sys.argv`). It sounds like in your use-case you want some of `argv` to be processed by Typer, and some of it to be processed by Hydra. Is that correct? – Jasha Jan 25 '22 at 10:47
  • See [this github comment](https://github.com/facebookresearch/hydra/issues/1964#issue-1105224246) for an example of a script that does pre-processing of `sys.argv` before calling the function decorated with `@hydra.main`. – Jasha Jan 25 '22 at 10:48
  • Thanks again! (I wish I could upvote again) – Gabio Jan 25 '22 at 11:33