Skip to content

Ensure compatibility with click v8.2. #676

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ repos:
- id: nbstripout
exclude: (docs)
- repo: https://github.com/crate-ci/typos
rev: v1
rev: v1.32.0
hooks:
- id: typos
exclude: (\.ipynb)
Expand Down
4 changes: 2 additions & 2 deletions src/_pytask/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

from __future__ import annotations

import importlib.metadata
from typing import Any

import click
from packaging.version import parse as parse_version

from _pytask.click import ColoredGroup
from _pytask.pluginmanager import storage
Expand All @@ -16,7 +16,7 @@
}


if parse_version(click.__version__) >= parse_version("8"): # pragma: no cover
if importlib.metadata.version("click") >= "8": # pragma: no cover
_VERSION_OPTION_KWARGS = {"package_name": "pytask"}
else: # pragma: no cover
_VERSION_OPTION_KWARGS = {}
Expand Down
79 changes: 52 additions & 27 deletions src/_pytask/click.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,24 @@

from __future__ import annotations

import importlib.metadata
import inspect
from enum import Enum
from gettext import gettext as _
from gettext import ngettext
from typing import TYPE_CHECKING
from typing import Any
from typing import ClassVar
from typing import TypeVar

import click
from click import Choice
from click import Command
from click import Context
from click import Parameter
from click.parser import split_opt
from click_default_group import DefaultGroup
from rich.highlighter import RegexHighlighter
from rich.padding import Padding
from rich.panel import Panel
from rich.table import Table
from rich.text import Text
Expand All @@ -27,40 +29,57 @@
from _pytask.console import create_panel_title

if TYPE_CHECKING:
from collections.abc import Iterable
from collections.abc import Sequence


__all__ = ["ColoredCommand", "ColoredGroup", "EnumChoice"]


class EnumChoice(Choice):
"""An enum-based choice type.
if importlib.metadata.version("click") < "8.2":
from click.parser import split_opt

The implementation is copied from https://github.com/pallets/click/pull/2210 and
related discussion can be found in https://github.com/pallets/click/issues/605.
class EnumChoice(Choice): # type: ignore[type-arg]
"""An enum-based choice type.

In contrast to using :class:`click.Choice`, using this type ensures that the error
message does not show the enum members.
The implementation is copied from https://github.com/pallets/click/pull/2210 and
related discussion can be found in https://github.com/pallets/click/issues/605.

In contrast to the proposed implementation in the PR, this implementation does not
use the members than rather the values of the enum.
In contrast to using :class:`click.Choice`, using this type ensures that the
error message does not show the enum members.

"""
In contrast to the proposed implementation in the PR, this implementation does
not use the members than rather the values of the enum.

def __init__(self, enum_type: type[Enum], case_sensitive: bool = True) -> None:
super().__init__(
choices=[element.value for element in enum_type],
case_sensitive=case_sensitive,
)
self.enum_type = enum_type
"""

def __init__(self, enum_type: type[Enum], case_sensitive: bool = True) -> None:
super().__init__(
choices=[element.value for element in enum_type],
case_sensitive=case_sensitive,
)
self.enum_type = enum_type

def convert(
self, value: Any, param: Parameter | None, ctx: Context | None
) -> Any:
if isinstance(value, Enum):
value = value.value
value = super().convert(value=value, param=param, ctx=ctx)
if value is None:
return None
return self.enum_type(value)

else:
from click.parser import _split_opt as split_opt

ParamTypeValue = TypeVar("ParamTypeValue")

def convert(self, value: Any, param: Parameter | None, ctx: Context | None) -> Any:
if isinstance(value, Enum):
value = value.value
value = super().convert(value=value, param=param, ctx=ctx)
if value is None:
return None
return self.enum_type(value)
class EnumChoice(Choice): # type: ignore[no-redef, type-arg]
def __init__(
self, choices: Iterable[ParamTypeValue], case_sensitive: bool = False
) -> None:
super().__init__(choices=choices, case_sensitive=case_sensitive)


class _OptionHighlighter(RegexHighlighter):
Expand Down Expand Up @@ -119,7 +138,10 @@ def format_help(
_print_options(self, ctx)

console.print(
"[bold #FF0000]♥[/] [#f2f2f2]https://pytask-dev.readthedocs.io[/]",
Padding(
"[bold #FF0000]♥[/] [#f2f2f2]https://pytask-dev.readthedocs.io[/]",
(0, 3, 0, 0),
),
justify="right",
)

Expand Down Expand Up @@ -197,7 +219,10 @@ def format_help(
_print_options(self, ctx)

console.print(
"[bold #FF0000]♥[/] [#f2f2f2]https://pytask-dev.readthedocs.io[/]",
Padding(
"[bold #FF0000]♥[/] [#f2f2f2]https://pytask-dev.readthedocs.io[/]",
(0, 3, 0, 0),
),
justify="right",
)

Expand Down Expand Up @@ -235,7 +260,7 @@ def _print_options(group_or_command: Command | DefaultGroup, ctx: Context) -> No
if param.metavar:
opt2 += Text(f" {param.metavar}", style="metavar")
elif isinstance(param.type, click.Choice):
choices = "[" + "|".join(param.type.choices) + "]"
choices = "[" + "|".join([str(c.value) for c in param.type.choices]) + "]"
opt2 += Text(f" {choices}", style="metavar", overflow="fold")

help_text = _format_help_text(param, ctx)
Expand Down Expand Up @@ -313,7 +338,7 @@ def _format_help_text( # noqa: C901, PLR0912, PLR0915
elif param.is_bool_flag and param.secondary_opts: # type: ignore[attr-defined]
# For boolean flags that have distinct True/False opts,
# use the opt without prefix instead of the value.
default_string = split_opt(
default_string = split_opt( # type: ignore[operator]
(param.opts if param.default else param.secondary_opts)[0]
)[1]
elif (
Expand Down
Loading