click-extra icon indicating copy to clipboard operation
click-extra copied to clipboard

Tweak callback evaluation order of eagerness options

Open kdeldycke opened this issue 3 years ago • 2 comments

Eager parameters are evaluated in the order as they were provided on the command line by the user as explained in: https://click.palletsprojects.com/en/8.0.x/advanced/#callback-evaluation-order

This means a call to:

  • --no-color --version will output a plain, uncoloured version string, while
  • --version --no-color will output a coloured string.

This is highlighted by a test at: https://github.com/kdeldycke/click-extra/blob/d589907ff7cdb198699a8be5aba86fa5ade97bef/click_extra/tests/test_colorize.py#L318-L346

It could be great to have --no-color (and its NO_COLOR env var) be respected whatever its order.

kdeldycke avatar Apr 11 '22 20:04 kdeldycke

I'm using this to workaround the env var issue:

import os
from collections.abc import Callable
from configparser import RawConfigParser
from functools import partial
from typing import TYPE_CHECKING, Any, ParamSpec, TypeVar, cast

import click
import cloup
from click_extra.colorize import (
    HelpExtraFormatter,
    color_envvars,
    default_theme,
    nocolor_theme,
)
from click_extra.commands import (
    ExtraCommand,
    ExtraGroup,
)
from click_extra.commands import (
    default_extra_params as _default_extra_params,
)

Param = ParamSpec("Param")
T = TypeVar("T")


def _wrap_impl(
    func: Callable[Param, T],
    lazy_params: Callable[[], list[click.Parameter]],
    lazy_context: Callable[[], dict[str, Any]] = dict,
    *default_args: Param.args,
    **default_kwargs: Param.kwargs,
) -> Callable[Param, T]:
    func_partial = partial(func, *default_args, **default_kwargs)

    def wrapped(*args: Param.args, **kwargs: Param.kwargs) -> T:
        if "params" not in kwargs:
            kwargs["params"] = lazy_params()
        if "context_settings" not in kwargs:
            kwargs["context_settings"] = lazy_context()

        return func_partial(*args, **kwargs)

    return wrapped


if TYPE_CHECKING:
    _wrap = partial
else:
    _wrap = _wrap_impl


def default_extra_params() -> list[click.Parameter]:
    return cast(list[click.Parameter], _default_extra_params())


def clicked_extra_context() -> dict[str, Any]:
    color: bool = True
    # Collect all colorize flags in environment variables we recognize.
    colorize_from_env: set[bool] = set()
    for var, default in color_envvars.items():
        if var in os.environ:
            # Presence of the variable in the environment without a value encodes
            # for an activation, hence the default to True.
            var_value = os.environ.get(var, "true")
            # `os.environ` is a dict whose all values are strings. Here we normalize
            # these string into booleans. If we can't, we fallback to True, in the
            # same spirit as above.
            var_boolean = RawConfigParser.BOOLEAN_STATES.get(
                var_value.lower(),
                True,
            )
            colorize_from_env.add(default ^ (not var_boolean))

    # Re-interpret the provided value against the recognized environment variables.
    if colorize_from_env:
        # The environment can only override the provided value if it comes from
        # the default value or the config file.
        color = True in colorize_from_env

    # XXX: mypy thinks click_extra.HelpFormatter (imported via star)
    #      is not the same as cloup.HelpFormatter.
    #      this is not needed for pyright.
    help_formatter = cast(cloup.HelpFormatter, HelpExtraFormatter)
    return {
        "color": color,
        "formatter_settings": help_formatter.settings(
            theme=default_theme if color else nocolor_theme,
        ),
    }


extra_command = _wrap(
    cloup.command,
    lazy_params=default_extra_params,
    lazy_context=clicked_extra_context,
    cls=ExtraCommand,
)
extra_group = _wrap(
    cloup.group,
    lazy_params=default_extra_params,
    lazy_context=clicked_extra_context,
    cls=ExtraGroup,
)

mochaaP avatar Oct 09 '25 12:10 mochaaP

I'm using this to workaround the env var issue:

@mochaaP Which env var issue are you talking about? 😅

kdeldycke avatar Oct 16 '25 13:10 kdeldycke