diff --git a/requirements-dev.lock b/requirements-dev.lock index 1c0bc95..a5831f5 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -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 @@ -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 @@ -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 @@ -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 @@ -306,7 +307,7 @@ 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 @@ -314,5 +315,5 @@ wheel==0.44.0 # via sphinx-togglebutton xxhash==3.5.0 # via flogging -zipp==3.20.1 +zipp==3.20.2 # via importlib-metadata diff --git a/requirements.lock b/requirements.lock index e532e9f..0d8ca0d 100644 --- a/requirements.lock +++ b/requirements.lock @@ -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 diff --git a/src/hydraclick/core.py b/src/hydraclick/core.py index 3b61b54..06d8780 100644 --- a/src/hydraclick/core.py +++ b/src/hydraclick/core.py @@ -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 ( @@ -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, @@ -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) @@ -110,13 +117,43 @@ 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)]): @@ -124,13 +161,89 @@ def _run_hydra_function(loaded_config: DictConfig): 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 @@ -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( @@ -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 diff --git a/src/hydraclick/options.py b/src/hydraclick/options.py index e46cf73..643408b 100644 --- a/src/hydraclick/options.py +++ b/src/hydraclick/options.py @@ -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()." @@ -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, @@ -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(