Skip to content

Decorators

The four main decorators are the public surface of pyclifer. 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.

Key parameters:

  • context_factory — callable that receives all context=True option values as keyword arguments and returns the ctx.obj instance. Enables declarative context construction without a manual ctx.obj = assignment in the group callback.
  • context_options_panel — label for the help section that lists context=True options in subcommand --help output. Defaults to "Context Options (anywhere-passable)". Set to any string to customise the heading.
  • auto_envvar_prefix — uppercase prefix for automatic environment variable binding. With auto_envvar_prefix="MYAPP", --database-url reads MYAPP_DATABASE_URL.
  • rich_help_configRichHelpConfiguration instance (or dict) forwarded to rich-click. Lets you customise colours, column widths, and panel styles in --help output.
  • use_rich_help — set to False to disable rich-click rendering and use plain Click help. Defaults to True.

pyclifer.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 pyclifer CLI group.

Source code in src/pyclifer/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 pyclifer CLI group.
    """
    config_kwargs, click_kwargs = _split_group_kwargs(kwargs)

    # 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),
        handle_response=config_kwargs.pop("handle_response", True),
        **config_kwargs,
    )

    return GroupDecorator(config, click_kwargs)

group

Creates a subgroup that inherits global options from its parent.

pyclifer.group(**kwargs)

Decorator for CLI subgroups.

Creates a standard group without global application options by default.

Source code in src/pyclifer/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_kwargs, click_kwargs = _split_group_kwargs(kwargs)

    # 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.

Key parameter:

  • handle_response=False — when True, wraps the command with returns_response so any Response returned by the function is printed automatically. Equivalent to stacking @returns_response manually.

pyclifer.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/pyclifer/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/context propagation.

  • is_global=True — propagates this option to all subcommands (see GlobalOptionsMixin).
  • context=True — marks this option as a context option: its value feeds context_factory and is accepted at any position in the command chain (see Anywhere-passable options). By default it also appears in subcommand --help under the Context Options panel.
  • show_in_subcommand_help=True — when context=True, controls whether the option is shown in subcommand help. Set to False to hide it from subcommand help while keeping the anywhere-passable behaviour intact.
  • store_in_meta=False — when True, stores the option value in ctx.meta automatically (key: "pyclifer.<option-name>"). The option is not exposed as a function parameter. Used internally by pagination_options for --page and --limit.

pyclifer.option(*param_decls, is_global=False, context=False, show_envvar=True, store_in_meta=False, show_in_subcommand_help=True, **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
context bool

If True, the option feeds ctx.obj construction and is accepted at any position in the command chain.

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
show_in_subcommand_help bool

If True, display-only copy of the option appears in subcommand help when context=True.

True
**kwargs Any

Additional arguments passed to click_extra.option().

{}

Returns:

Type Description
Callable[[Callable], Callable]

Option decorator function.

Source code in src/pyclifer/core/decorators.py
def option(
    *param_decls: str,
    is_global: bool = False,
    context: bool = False,
    show_envvar: bool = True,
    store_in_meta: bool = False,
    show_in_subcommand_help: bool = True,
    **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.
        context: If True, the option feeds ctx.obj construction and is accepted
            at any position in the command chain.
        show_envvar: Show environment variables in the help output.
        store_in_meta: If True, stores the option value in ctx.meta automatically.
        show_in_subcommand_help: If True, display-only copy of the option appears
            in subcommand help when context=True.
        **kwargs: Additional arguments passed to click_extra.option().

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

    # Only forward context to classes that declare the attribute (PycliferOption subclasses).
    if isinstance(cls, type) and issubclass(cls, PycliferOption):
        kwargs["context"] = context
        kwargs["show_in_subcommand_help"] = show_in_subcommand_help

    # 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 PycliferVerbosityOption) 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).

pyclifer.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['pyclifer.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/pyclifer/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['pyclifer.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 for all commands under @app_group (on by default). Use handle_response=False on the group or individual commands to opt out.

pyclifer.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['pyclifer.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/pyclifer/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['pyclifer.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"""
        # Lazy import to avoid circular dependency at module load time
        from .output.responses import Response as _Response

        root = _get_root_context()
        meta = root.meta if root is not None else {}

        try:
            result = f(*args, **kwargs)
        except Exception as e:
            log_level = meta.get("pyclifer.unhandled_exception_log_level", "error")
            output_format = meta.get("pyclifer.output_format", "table")
            structured = output_format in ("json", "yaml")
            use_exc_info = not structured or _log.isEnabledFor(logging.DEBUG)
            msg = (
                "Unhandled exception in command '%s'"
                if use_exc_info
                else "Unhandled exception in command '%s': %s"
            )
            _log.log(
                getattr(logging, log_level.upper(), logging.ERROR),
                msg,
                f.__name__,
                *(() if use_exc_info else (str(e),)),
                exc_info=use_exc_info,
            )
            result = _Response(success=False, message=str(e), error_code=ExitCode.ERROR)
        _log.debug(
            "returns_response: command '%s' returned %s",
            f.__name__,
            type(result).__name__,
        )
        if isinstance(result, _Response):
            from pyclifer.core.context import BaseContext

            ctx = click_extra.get_current_context(silent=True)

            # 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()
            output_format = meta.get("pyclifer.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()),
            )
            output_filter = meta.get("pyclifer.output_filter")
            options = {"filter_value": output_filter} if output_filter else {}
            output_ctx.print_result_based_on_format(result, options=options)
            if not result.success and result.error_code and ctx is not None:
                ctx.exit(result.error_code)
        else:
            _log.debug(
                "returns_response: result is not a Response instance — skipping output dispatch"
            )
        return result

    return wrapper

pagination_options

Injects --page and --limit options into a command. Values are stored in ctx.meta["pyclifer.page"] and ctx.meta["pyclifer.limit"] via store_in_meta.

pyclifer.pagination_options(default_limit=20, max_limit=100)

Inject --page and --limit options into a command.

Options are stored in ctx.meta under the keys 'pyclifer.page' and 'pyclifer.limit' via store_in_meta.

Parameters:

Name Type Description Default
default_limit int

Default number of results per page.

20
max_limit int

Maximum allowed value for --limit (enforced via IntRange).

100

Returns:

Type Description
Callable[[_F], _F]

A decorator that adds --page and --limit to the decorated function.

Source code in src/pyclifer/core/decorators.py
def pagination_options(
    default_limit: int = 20,
    max_limit: int = 100,
) -> Callable[[_F], _F]:
    """Inject --page and --limit options into a command.

    Options are stored in ctx.meta under the keys 'pyclifer.page' and
    'pyclifer.limit' via store_in_meta.

    Args:
        default_limit: Default number of results per page.
        max_limit: Maximum allowed value for --limit (enforced via IntRange).

    Returns:
        A decorator that adds --page and --limit to the decorated function.
    """

    def decorator(f: _F) -> _F:
        """Add --page and --limit options to the command."""
        f = option(
            "--limit",
            "-l",
            default=default_limit,
            type=click_extra.IntRange(1, max_limit),
            help=f"Results per page (max {max_limit}).",
            store_in_meta=True,
        )(f)
        f = option(
            "--page",
            "-p",
            default=1,
            type=click_extra.IntRange(min=1),
            help="Page number (1-indexed).",
            store_in_meta=True,
        )(f)
        return f

    return decorator