Skip to content

Decorators

The four main decorators are the public surface of pyclif. They wrap Click objects with framework features: automatic configuration, global option propagation, Rich logging, and standardized response handling.

app_group

Entry point decorator. Creates the root CLI group with all framework features enabled.

pyclif.app_group(**kwargs)

Decorator for the main CLI application entry point.

Enables all automatic features (config, logging, version, etc.) by default. Options like --verbosity will be propagated to all subcommands.

All keyword arguments map to GroupConfig fields or are forwarded to Click. Notable options:

  • handle_response (bool): intercept and print Response objects automatically.
  • timer (bool): inject --time/--no-time. Prints elapsed time in rich/table/raw; injects execution_time and execution_time_str into Response.data in json/yaml.
  • output_format_default (str): default for --output-format (json, yaml, table, rich, raw, text).

Parameters:

Name Type Description Default
**kwargs Any

GroupConfig fields or Click group arguments.

{}

Returns:

Type Description
Callable[[Callable[..., Any]], Group]

A decorator that wraps the function as a pyclif CLI group.

Source code in src/pyclif/core/decorators.py
def app_group(**kwargs: Any) -> Callable[[Callable[..., Any]], click_extra.Group]:
    """Decorator for the main CLI application entry point.

    Enables all automatic features (config, logging, version, etc.) by default.
    Options like --verbosity will be propagated to all subcommands.

    All keyword arguments map to `GroupConfig` fields or are forwarded to Click.
    Notable options:

    - `handle_response` (bool): intercept and print `Response` objects automatically.
    - `timer` (bool): inject `--time/--no-time`. Prints elapsed time in rich/table/raw;
      injects `execution_time` and `execution_time_str` into `Response.data` in json/yaml.
    - `output_format_default` (str): default for `--output-format`
      (json, yaml, table, rich, raw, text).

    Args:
        **kwargs: GroupConfig fields or Click group arguments.

    Returns:
        A decorator that wraps the function as a pyclif CLI group.
    """
    config_fields = {f.name for f in fields(GroupConfig)}
    config_kwargs = {k: v for k, v in kwargs.items() if k in config_fields}
    click_kwargs = {k: v for k, v in kwargs.items() if k not in config_fields}

    # Create config with App defaults
    config = GroupConfig(
        add_config_option=config_kwargs.pop("add_config_option", True),
        add_verbosity_option=config_kwargs.pop("add_verbosity_option", True),
        add_log_file_option=config_kwargs.pop("add_log_file_option", True),
        add_version_option=config_kwargs.pop("add_version_option", True),
        add_output_format_option=config_kwargs.pop("add_output_format_option", True),
        **config_kwargs,
    )

    return GroupDecorator(config, click_kwargs)

group

Creates a subgroup that inherits global options from its parent.

pyclif.group(**kwargs)

Decorator for CLI subgroups.

Creates a standard group without global application options by default.

Source code in src/pyclif/core/decorators.py
def group(**kwargs: Any) -> Callable[[Callable[..., Any]], click_extra.Group]:
    """Decorator for CLI subgroups.

    Creates a standard group without global application options by default.
    """
    config_fields = {f.name for f in fields(GroupConfig)}
    config_kwargs = {k: v for k, v in kwargs.items() if k in config_fields}
    click_kwargs = {k: v for k, v in kwargs.items() if k not in config_fields}

    # Create config with Sub-group defaults (mostly False from the dataclass)
    config = GroupConfig(**config_kwargs)

    return GroupDecorator(config, click_kwargs)

command

Creates a CLI command. Use inside a group or app_group.

pyclif.command(name=None, handle_response=False, **kwargs)

Create a Click command with optional automatic response handling.

When handle_response=True, any Response returned by the command function is automatically printed using the output format resolved from ctx.meta (--output-format option). This is equivalent to manually applying the returns_response decorator.

Parameters:

Name Type Description Default
name str | None

Name of the command.

None
handle_response bool

If True, wrap the function with returns_response.

False
**kwargs Any

Additional arguments passed to click_extra.command().

{}

Returns:

Type Description
Callable[[_F], Command]

Decorated function as a Click command.

Source code in src/pyclif/core/decorators.py
def command(
    name: str | None = None,
    handle_response: bool = False,
    **kwargs: Any,
) -> Callable[[_F], click_extra.Command]:
    """Create a Click command with optional automatic response handling.

    When handle_response=True, any Response returned by the command function
    is automatically printed using the output format resolved from ctx.meta
    (--output-format option). This is equivalent to manually applying the
    returns_response decorator.

    Args:
        name: Name of the command.
        handle_response: If True, wrap the function with returns_response.
        **kwargs: Additional arguments passed to click_extra.command().

    Returns:
        Decorated function as a Click command.
    """
    command_decorator = rich_command_decorator

    if not handle_response:
        decorate = command_decorator(name=name, **kwargs) if name else command_decorator(**kwargs)
        return cast(Callable[[_F], click_extra.Command], decorate)

    def decorator(f: _F) -> click_extra.Command:
        """Decorator for Click commands with automatic response handling"""
        wrapped = returns_response(f)
        deco = command_decorator(name=name, **kwargs) if name else command_decorator(**kwargs)
        cmd = deco(wrapped)
        return cast(click_extra.Command, cmd)

    return decorator

option

Extends Click options with environment variable binding and optional global propagation.

pyclif.option(*param_decls, is_global=False, show_envvar=True, store_in_meta=False, **kwargs)

Create a Click option with global propagation support.

Ensures a consistent environment variable display and allows options to be marked as global to be available on all subcommands.

Parameters:

Name Type Description Default
*param_decls str

Parameter declarations for the option.

()
is_global bool

If True, the option is propagated to all subcommands.

False
show_envvar bool

Show environment variables in the help output.

True
store_in_meta bool

If True, stores the option value in ctx.meta automatically.

False
**kwargs Any

Additional arguments passed to click_extra.option().

{}

Returns:

Type Description
Callable[[Callable], Callable]

Option decorator function.

Source code in src/pyclif/core/decorators.py
def option(
    *param_decls: str,
    is_global: bool = False,
    show_envvar: bool = True,
    store_in_meta: bool = False,
    **kwargs: Any,
) -> Callable[[Callable], Callable]:
    """Create a Click option with global propagation support.

    Ensures a consistent environment variable display and allows options
    to be marked as global to be available on all subcommands.

    Args:
        *param_decls: Parameter declarations for the option.
        is_global: If True, the option is propagated to all subcommands.
        show_envvar: Show environment variables in the help output.
        store_in_meta: If True, stores the option value in ctx.meta automatically.
        **kwargs: Additional arguments passed to click_extra.option().

    Returns:
        Option decorator function.
    """
    cls = kwargs.get("cls", PyclifOption)
    kwargs["cls"] = cls
    kwargs["is_global"] = is_global
    kwargs.setdefault("show_envvar", show_envvar)

    # Delegate to the Mixin if the class supports it
    if isinstance(cls, type) and issubclass(cls, StoreInMetaMixin):
        kwargs["store_in_meta"] = store_in_meta
    elif store_in_meta:
        # Fallback for external classes (like PyclifVerbosityOption) that don't use StoreInMetaMixin
        kwargs["callback"] = get_meta_storing_callback(kwargs.get("callback"))
        kwargs.setdefault("expose_value", False)

    return click_extra.option(*param_decls, **kwargs)

output_filter_option

Adds --output-format to a command (JSON, YAML, Table, Rich, Raw).

pyclif.output_filter_option(*param_decls, show_envvar=True, **kwargs)

Add an output filter option to a command.

When combined with --output-format raw, json, or yaml, this option lets users extract a value from the Response data using a dotted key path.

The selected path is stored in ctx.meta['pyclif.output_filter'] and is automatically picked up by returns_response.

Numeric path segments are treated as list indices. Resolution order: data["data"] first, then top-level response fields.

Example:

@app.command()
@output_filter_option()
@returns_response
@click.pass_context
def articles(ctx):
    return Response(
        success=True,
        message="2 articles",
        data={"results": [{"id": 1, "title": "Hello"}, {"id": 2}]},
    )

# myapp articles -o raw -f results.0.title  -> Hello
# myapp articles -o json -f results.0       -> {"id": 1, "title": "Hello"}
# myapp articles -o raw -f message          -> 2 articles

Parameters:

Name Type Description Default
*param_decls str

Parameter declarations (default: --output-filter, -f).

()
show_envvar bool

Show environment variables in the help output.

True
**kwargs Any

Additional arguments passed to the option decorator.

{}

Returns:

Type Description
Callable[[Callable], Callable]

The decorated function.

Source code in src/pyclif/core/decorators.py
def output_filter_option(
    *param_decls: str,
    show_envvar: bool = True,
    **kwargs: Any,
) -> Callable[[Callable], Callable]:
    """Add an output filter option to a command.

    When combined with --output-format raw, json, or yaml, this option lets
    users extract a value from the Response data using a dotted key path.

    The selected path is stored in ctx.meta['pyclif.output_filter'] and is
    automatically picked up by returns_response.

    Numeric path segments are treated as list indices. Resolution order:
    data["data"] first, then top-level response fields.

    Example:

        @app.command()
        @output_filter_option()
        @returns_response
        @click.pass_context
        def articles(ctx):
            return Response(
                success=True,
                message="2 articles",
                data={"results": [{"id": 1, "title": "Hello"}, {"id": 2}]},
            )

        # myapp articles -o raw -f results.0.title  -> Hello
        # myapp articles -o json -f results.0       -> {"id": 1, "title": "Hello"}
        # myapp articles -o raw -f message          -> 2 articles

    Args:
        *param_decls: Parameter declarations (default: --output-filter, -f).
        show_envvar: Show environment variables in the help output.
        **kwargs: Additional arguments passed to the option decorator.

    Returns:
        The decorated function.
    """
    if not param_decls:
        param_decls = ("--output-filter", "-f")

    kwargs.setdefault("help", "Dotted path to extract from the response (raw, json, yaml).")
    # noinspection PyArgumentEqualDefault
    kwargs.setdefault("default", None)
    kwargs.setdefault("store_in_meta", True)

    return option(*param_decls, show_envvar=show_envvar, **kwargs)

returns_response

Decorator that intercepts a Response return value and dispatches it to the formatter. Applied automatically when handle_response=True on @app_group.

pyclif.returns_response(f)

Decorator that intercepts a Response return value and prints it automatically.

When the decorated command function returns a Response instance, this decorator reads the output format stored in ctx.meta['pyclif.output_format'] (set by the --output-format option) and dispatches printing via BaseContext. Non-Response return values are left untouched.

Example:

@app.command()
@returns_response
@option("--name", default="world")
@click.pass_context
def hello(ctx, name):
    return Response(success=True, message=f"Hello {name}", data={"name": name})

Parameters:

Name Type Description Default
f Callable

The command function to wrap.

required

Returns:

Type Description
Callable

The wrapped function.

Source code in src/pyclif/core/decorators.py
def returns_response(f: Callable) -> Callable:
    """Decorator that intercepts a Response return value and prints it automatically.

    When the decorated command function returns a Response instance, this decorator
    reads the output format stored in ctx.meta['pyclif.output_format'] (set by
    the --output-format option) and dispatches printing via BaseContext.
    Non-Response return values are left untouched.

    Example:

        @app.command()
        @returns_response
        @option("--name", default="world")
        @click.pass_context
        def hello(ctx, name):
            return Response(success=True, message=f"Hello {name}", data={"name": name})

    Args:
        f: The command function to wrap.

    Returns:
        The wrapped function.
    """

    @functools.wraps(f)
    def wrapper(*args: Any, **kwargs: Any) -> Any:
        """Wrapper for returning a Response object based on command output"""
        import logging
        from time import perf_counter

        from .output.responses import Response as _Response

        _log = logging.getLogger(__name__)
        try:
            result = f(*args, **kwargs)
        except Exception as e:
            ctx = click_extra.get_current_context(silent=True)
            root = ctx
            if root is not None:
                while (parent := root.parent) is not None:
                    root = parent
            meta = root.meta if root is not None else {}
            log_level = meta.get("pyclif.unhandled_exception_log_level", "error")
            _log.log(
                getattr(logging, log_level.upper(), logging.ERROR),
                "Unhandled exception in command '%s'",
                f.__name__,
                exc_info=True,
            )
            result = _Response(success=False, message=str(e), error_code=1)
        _log.debug(
            "returns_response: command '%s' returned %s",
            f.__name__,
            type(result).__name__,
        )
        if isinstance(result, _get_response_class()):
            from pyclif.core.context import BaseContext

            # --output-format is set on the root group context.
            # Walk up the context chain to find the root where the user's
            # explicit value (or the app-level default) is stored.
            ctx = click_extra.get_current_context(silent=True)
            root = ctx
            if root is not None:
                while (parent := root.parent) is not None:
                    root = parent

            # Use the actual context object (ctx.obj) if it is a BaseContext
            # subclass so that custom overrides (e.g., print_result_based_on_format)
            # are respected.  Fall back to a fresh BaseContext when ctx.obj is
            # absent or of an unrelated type.
            obj = ctx.obj if ctx is not None else None
            output_ctx = obj if isinstance(obj, BaseContext) else BaseContext()
            meta = root.meta if root is not None else {}
            output_format = meta.get("pyclif.output_format", "table")

            # Inject execution time into structured output when timer is active.
            start_time = meta.get("click_extra.start_time")
            if (
                start_time is not None
                and output_format in ("json", "yaml")
                and isinstance(result.data, dict)
            ):
                # noinspection PyTypeChecker
                elapsed = perf_counter() - start_time
                result.data["execution_time"] = round(elapsed, 3)
                result.data["execution_time_str"] = f"{elapsed:.3f}s"
            output_ctx.output_format = output_format
            _log.debug(
                "returns_response: ctx.obj type=%s, using output_ctx type=%s, "
                "output_format=%r, meta keys=%s",
                type(obj).__name__,
                type(output_ctx).__name__,
                output_format,
                list(meta.keys()),
            )
            options: dict[str, Any] = {}
            output_filter = meta.get("pyclif.output_filter")
            if output_filter:
                options["filter_value"] = output_filter
            output_ctx.print_result_based_on_format(result, options=options)
        else:
            _log.debug(
                "returns_response: result is not a Response instance — skipping output dispatch"
            )
        return result

    return wrapper