Skip to content

Commit

Permalink
Merge pull request #2 from FragileTech/api
Browse files Browse the repository at this point in the history
Add hydra.main to API. Add flogging config to API
  • Loading branch information
Guillemdb authored Sep 15, 2024
2 parents 9329000 + 6f291ae commit a6ceb23
Show file tree
Hide file tree
Showing 4 changed files with 209 additions and 35 deletions.
17 changes: 9 additions & 8 deletions requirements-dev.lock
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,13 @@ greenlet==3.1.0 ; (python_full_version < '3.13' and platform_machine == 'AMD64')
# via sqlalchemy
hydra-core==1.3.2
# via hydraclick
hypothesis==6.112.0
hypothesis==6.112.1
# via hydraclick
idna==3.8
idna==3.9
# via requests
imagesize==1.4.1
# via sphinx
importlib-metadata==8.4.0
importlib-metadata==8.5.0
# via jupyter-cache
# via myst-nb
iniconfig==2.0.0
Expand Down Expand Up @@ -159,7 +159,7 @@ parso==0.8.4
# via jedi
pexpect==4.9.0 ; sys_platform != 'emscripten' and sys_platform != 'win32'
# via ipython
platformdirs==4.3.2
platformdirs==4.3.3
# via jupyter-core
pluggy==1.5.0
# via pytest
Expand Down Expand Up @@ -223,12 +223,13 @@ requests==2.32.3
rpds-py==0.20.0
# via jsonschema
# via referencing
ruff==0.6.4
ruff==0.6.5
ruyaml==0.91.0
setuptools==74.1.2
# via hydraclick
# via ruyaml
# via sphinx-togglebutton
# via sphinxcontrib-bibtex
six==1.16.0
# via asttokens
# via pybtex
Expand Down Expand Up @@ -258,7 +259,7 @@ sphinx-rtd-theme==0.5.1
sphinx-togglebutton==0.3.2
sphinxcontrib-applehelp==2.0.0
# via sphinx
sphinxcontrib-bibtex==2.6.2
sphinxcontrib-bibtex==2.6.3
sphinxcontrib-devhelp==2.0.0
# via sphinx
sphinxcontrib-htmlhelp==2.1.0
Expand Down Expand Up @@ -306,13 +307,13 @@ typing-extensions==4.12.2
# via sqlalchemy
uc-micro-py==1.0.3
# via linkify-it-py
urllib3==2.2.2
urllib3==2.2.3
# via requests
wcwidth==0.2.13
# via prompt-toolkit
wheel==0.44.0
# via sphinx-togglebutton
xxhash==3.5.0
# via flogging
zipp==3.20.1
zipp==3.20.2
# via importlib-metadata
2 changes: 1 addition & 1 deletion requirements.lock
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ frozendict==2.4.4
# via hydraclick
hydra-core==1.3.2
# via hydraclick
hypothesis==6.112.0
hypothesis==6.112.1
# via hydraclick
iniconfig==2.0.0
# via pytest
Expand Down
218 changes: 196 additions & 22 deletions src/hydraclick/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import hydra
from omegaconf import DictConfig, OmegaConf
from unittest.mock import patch
import flogging


from hydraclick.display_config import display_config
from hydraclick.options import (
Expand All @@ -28,6 +28,13 @@

_logger = logging.getLogger(__name__)

try:
import flogging

FLOGGING_AVAILABLE = True
except ImportError:
FLOGGING_AVAILABLE = False


def wrap_kwargs_and_config(
function: Callable,
Expand Down Expand Up @@ -98,7 +105,7 @@ def build_hydra_args(
if config_dir:
hydra_args_.extend(("--config-dir", f"{config_dir}"))
if shell_completion:
hydra_args_.append("--shell-completion")
hydra_args_.extend(("--shell-completion", f"{shell_completion}"))
_logger.debug(f"Hydra args after composition: {hydra_args}")
return (*hydra_args_, *hydra_args)

Expand All @@ -110,27 +117,133 @@ def get_default_dir() -> str:
return str(curr_dir / "config") if (curr_dir / "config").exists() else str(curr_dir)


def run_hydra(function: Callable, hydra_args: tuple[str, ...]) -> None:
"""Run a function as a hydra app."""
def run_hydra(
function: Callable,
hydra_args: tuple[str, ...],
config_path: str | None = None,
config_name: str | None = "config",
version_base: str | None = None,
use_flogging: bool = True,
**flogging_kwargs: Any,
) -> Any:
"""Run a function as a Hydra app.
Args:
function (Callable): The function to be executed as a Hydra command. This function \
should accept a `DictConfig` object as its argument.
hydra_args (tuple[str, ...]): The arguments to pass to the Hydra command.
config_path (str | None, optional): The path to the configuration directory. If not \
specified, the default directory is used, which is the current working \
directory/config. Defaults to None.
config_name (str | None, optional): The name of the configuration file \
(without the `.yaml` or `.yml` extension). Defaults to "config".
version_base (str | None, optional): The base version of the configuration. \
Defaults to None.
use_flogging (bool, optional): Whether to use the `flogging` library for \
structured logging. Defaults to True.
**flogging_kwargs (Any, optional): Additional keyword arguments to pass to \
the `flogging.setup` function.
Returns:
Then return value of the function.
@hydra.main(config_path=get_default_dir(), config_name="config", version_base=None)
"""

@hydra.main(config_path=config_path, config_name=config_name, version_base=version_base)
@functools.wraps(function)
def _run_hydra_function(loaded_config: DictConfig):
flogging.setup(allow_trailing_dot=True)
if use_flogging:
flogging.setup(**flogging_kwargs)
return function(loaded_config)

with patch("sys.argv", [sys.argv[0], *list(hydra_args)]):
return _run_hydra_function()


def command_api(
function: Callable[[DictConfig], Any],
run_mode="config",
preprocess_config=None,
print_config=True,
resolve=True,
function: Callable[[DictConfig | dict[str, Any]], Any],
config_path: str | Path | None = None,
config_name: str | None = "config",
version_base: str | None = None,
run_mode: str = "config",
preprocess_config: Callable[[DictConfig], DictConfig] | None = None,
print_config: bool = True,
resolve: bool = True,
use_flogging: bool = True,
**flogging_kwargs: Any,
) -> Callable:
"""Implement using click the hydra CLI API."""
"""Integrate Hydra's configuration management capabilities with a Click-based CLI.
Args:
function (Callable[[DictConfig], Any]): The function to be executed as a Hydra command. \
This function should accept a `DictConfig` object as its argument.
config_path (str | Path | None, optional): The path to the configuration directory. \
If not specified, the default directory is used.
config_name (str | None, optional): The name of the configuration file \
(without the `.yaml` or `.yml` extension). Defaults to `"config"`.
version_base (str | None, optional): The base version of the configuration. \
Defaults to `None`.
run_mode (str, optional): The mode in which to run the function. Can be `"config"` \
or `"kwargs"`. Defaults to `"config"`.
preprocess_config (Callable[[DictConfig], DictConfig] | None, optional): A function \
to preprocess the configuration before passing it to the main function. \
Defaults to `None`.
print_config (bool, optional): Whether to print the configuration before \
running the function. Defaults to `True`.
resolve (bool, optional): Whether to resolve the configuration before running the \
function. Defaults to `True`.
use_flogging (bool, optional): Whether to use the `flogging` library for structured \
logging. Defaults to `True`.
**flogging_kwargs (Any, optional): Additional keyword arguments to pass to the \
`flogging.setup` function.
Returns:
Callable: A Click-compatible command function that can be used as a CLI command.
Example:
```python
from omegaconf import DictConfig
def my_function(config: DictConfig):
print(config.pretty())
click_command = command_api(
function=my_function,
config_path="path/to/config",
config_name="my_config",
version_base="1.0",
run_mode="config",
preprocess_config=None,
print_config=True,
resolve=True,
use_flogging=True,
allow_trailing_dot=True
)
```
In this example, `my_function` is wrapped by `command_api` to create a Click-compatible \
command. The configuration is loaded from the specified `config_path` and `config_name`, \
and the function is executed with the resolved configuration.
Notes:
- The `command_api` function uses several Hydra and Click decorators to provide \
a rich CLI experience.
- If `use_flogging` is enabled but the `flogging` library is not available, \
a warning is logged, and `flogging` is disabled.
- The `preprocess_config` function, if provided, allows for custom preprocessing of the \
configuration before it is passed to the main function.
"""
config_path = get_default_dir() if config_path is None else str(config_path)
if config_name is not None:
config_name = str(config_name).replace(".yaml", "").replace(".yml", "")
if use_flogging and not FLOGGING_AVAILABLE:
_logger.warning(
"Flogging is not available. Run `pip install flogging` to use the structured logging."
)
use_flogging = False
if not flogging_kwargs:
flogging_kwargs = {"allow_trailing_dot": True}

@hydra_args_argument
@hydra_help_option
Expand All @@ -155,13 +268,22 @@ def click_compatible(
info: bool,
run: bool,
multirun: bool,
config_path: str,
config_name: str,
config_path_: str,
config_name_: str,
config_dir: str,
shell_completion: bool,
hydra_args: tuple[str, ...] | None = None,
):
nonlocal print_config, run_mode, preprocess_config, resolve
nonlocal \
print_config, \
run_mode, \
preprocess_config, \
resolve, \
config_path, \
config_name, \
version_base, \
use_flogging, \
flogging_kwargs
if show_config:
print_config = False
true_func = wrap_kwargs_and_config(
Expand All @@ -176,32 +298,84 @@ def click_compatible(
info,
run,
multirun,
config_path,
config_name,
config_path_,
config_name_,
config_dir,
shell_completion,
hydra_args,
)
return run_hydra(true_func, hydra_args)
return run_hydra(
true_func,
hydra_args=hydra_args,
config_path=config_path,
config_name=config_name,
version_base=version_base,
use_flogging=use_flogging,
**flogging_kwargs,
)

return click_compatible


def hydra_command(
run_mode: str = "config", # "config" | "kwargs"
print_config: bool = True,
config_path: str | Path | None = None,
config_name: str | None = "config",
version_base: str | None = None,
run_mode: str = "config",
preprocess_config: Callable[[DictConfig], DictConfig] | None = None,
print_config: bool = True,
resolve: bool = True,
):
"""Wrap a function so it can run as a hydra command."""
use_flogging: bool = True,
**flogging_kwargs: Any,
) -> Callable:
"""Integrate Hydra's configuration management capabilities with a Click-based CLI.
Args:
config_path (str | Path | None, optional): The path to the configuration directory. \
If not specified, the default directory is used.
config_name (str | None, optional): The name of the configuration file \
(without the `.yaml` or `.yml` extension). Defaults to `"config"`.
version_base (str | None, optional): The base version of the configuration. \
Defaults to `None`.
run_mode (str, optional): The mode in which to run the function. Can be `"config"` \
or `"kwargs"`. Defaults to `"config"`.
preprocess_config (Callable[[DictConfig], DictConfig] | None, optional): A function to \
preprocess the configuration before passing it to the main function. \
Defaults to `None`.
print_config (bool, optional): Whether to print the configuration before \
running the function. Defaults to `True`.
resolve (bool, optional): Whether to resolve the configuration before running \
the function. Defaults to `True`.
use_flogging (bool, optional): Whether to use the `flogging` library for structured \
logging. Defaults to `True`.
**flogging_kwargs (Any, optional): Additional keyword arguments to pass to the \
`flogging.setup` function.
Returns:
Callable: A Click-compatible command function that can be used as a CLI command.
Notes:
- The `command_api` function uses several Hydra and Click decorators to provide \
a rich CLI experience.
- If `use_flogging` is enabled but the `flogging` library is not available, \
a warning is logged, and `flogging` is disabled.
- The `preprocess_config` function, if provided, allows for custom preprocessing of the \
configuration before it is passed to the main function.
"""

def decorator(function: Callable):
return command_api(
function,
config_path=config_path,
config_name=config_name,
version_base=version_base,
use_flogging=use_flogging,
run_mode=run_mode,
print_config=print_config,
preprocess_config=preprocess_config,
resolve=resolve,
**flogging_kwargs,
)

return decorator
7 changes: 3 additions & 4 deletions src/hydraclick/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@
config_path_option = click.option(
"--config-path",
"-cp",
"config_path",
"config_path_",
help=(
"Overrides the config_path specified in hydra.main(). "
"The config_path is absolute or relative to the Python file declaring @hydra.main()."
Expand All @@ -96,7 +96,7 @@
config_name_option = click.option(
"--config-name",
"-cn",
"config_name",
"config_name_",
help="Overrides the config_name specified in hydra.main()",
default=None,
type=click.STRING,
Expand Down Expand Up @@ -126,8 +126,7 @@
"-sc",
"shell_completion",
help="Install or Uninstall shell tab completion",
is_flag=True,
default=False,
default=None,
)
# Unused stuff
parallel_option = click.option(
Expand Down

0 comments on commit a6ceb23

Please sign in to comment.