Skip to content
Merged
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
3 changes: 2 additions & 1 deletion hatch.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ typing-mypy = [
"mypy tests examples",
]
typing-pyrefly = [
"pyrefly check",
"pyrefly check --preset strict src/cloup",
"pyrefly check tests examples",
]
typing = [
"click-version",
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ classifiers = [
]
dependencies = [
"click >= 8.5.0, < 9.0",
'typing_extensions; python_version <= "3.10"',
'typing_extensions; python_version < "3.12"',
]
dynamic = [
"readme",
Expand Down
14 changes: 14 additions & 0 deletions src/cloup/_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"""

import inspect
import sys
from collections.abc import Callable, Iterable, Mapping, MutableMapping, Sequence
from typing import (
Any,
Expand All @@ -32,6 +33,11 @@

import click

if sys.version_info >= (3, 12):
from typing import override
else:
from typing_extensions import override

import cloup

from ._context import Context
Expand Down Expand Up @@ -90,13 +96,15 @@ def get_normalized_epilog(self) -> str:
return self.epilog or ""

# Differently from Click, this doesn't indent the epilog.
@override
def format_epilog(self, ctx: click.Context, formatter: click.HelpFormatter) -> None:
if self.epilog:
assert isinstance(formatter, cloup.HelpFormatter)
epilog = self.get_normalized_epilog()
formatter.write_paragraph()
formatter.write_epilog(epilog)

@override
def format_help_text(
self, ctx: click.Context, formatter: click.HelpFormatter
) -> None:
Expand All @@ -109,6 +117,7 @@ def format_aliases(self, ctx: click.Context, formatter: click.HelpFormatter) ->
assert isinstance(formatter, cloup.HelpFormatter)
formatter.write_aliases(self.aliases)

@override
def format_help(self, ctx: click.Context, formatter: click.HelpFormatter) -> None:
self.format_usage(ctx, formatter)
self.format_aliases(ctx, formatter)
Expand Down Expand Up @@ -180,6 +189,7 @@ def add_multiple_commands(
for cmd in commands:
self.add_command(cmd)

@override
def add_command(
self,
cmd: click.Command,
Expand All @@ -202,6 +212,7 @@ def resolve_command_name(self, ctx: click.Context, name: str) -> str | None:
return name
return self.alias2name.get(name)

@override
def resolve_command(
self, ctx: click.Context, args: list[str]
) -> tuple[str | None, click.Command | None, list[str]]:
Expand Down Expand Up @@ -248,6 +259,7 @@ def must_show_subcommand_aliases(self, ctx: click.Context) -> bool:
Group.SHOW_SUBCOMMAND_ALIASES,
)

@override
def format_subcommand_name(
self, ctx: click.Context, name: str, cmd: click.Command
) -> str:
Expand Down Expand Up @@ -320,6 +332,7 @@ def command( # Why overloading? Refer to module docstring.
**kwargs: Any,
) -> Callable[[AnyCallable], C]: ...

@override
def command(
self,
name: str | None = None,
Expand Down Expand Up @@ -409,6 +422,7 @@ def group( # Why overloading? Refer to module docstring.
**kwargs: Any,
) -> Callable[[AnyCallable], G]: ...

@override
def group(
self,
name: str | None = None,
Expand Down
7 changes: 7 additions & 0 deletions src/cloup/_context.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import sys
import warnings
from collections.abc import Callable
from functools import update_wrapper
Expand All @@ -15,6 +16,11 @@

import click

if sys.version_info >= (3, 12):
from typing import override
else:
from typing_extensions import override

import cloup
from cloup._util import coalesce, pick_non_missing
from cloup.formatting import HelpFormatter
Expand Down Expand Up @@ -173,6 +179,7 @@ def get_formatter_settings(self) -> dict[str, Any]:
**getattr(self.command, "formatter_settings", {}),
}

@override
def make_formatter(self) -> HelpFormatter:
opts = self.get_formatter_settings()
return self.formatter_class(**opts)
Expand Down
35 changes: 26 additions & 9 deletions src/cloup/_params.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
from __future__ import annotations

from typing import TYPE_CHECKING
from collections.abc import Callable
from typing import TYPE_CHECKING, Any

import click
from click.decorators import _param_memo

from .typing import F

if TYPE_CHECKING:
from ._option_groups import OptionGroup

Expand All @@ -14,7 +17,12 @@ class Option(click.Option):

group: OptionGroup | None

def __init__(self, *args, group=None, **attrs):
def __init__(
self,
*args: Any,
group: OptionGroup | None = None,
**attrs: Any,
) -> None:
super().__init__(*args, **attrs)
self.group = group

Expand All @@ -23,17 +31,26 @@ def __init__(self, *args, group=None, **attrs):
"""Alias of ``Option``."""


def argument(*param_decls, cls=None, **attrs):
def argument(
*param_decls: str,
cls: type[click.Argument] | None = None,
**attrs: Any,
) -> Callable[[F], F]:
cls = cls or click.Argument

def decorator(f):
def decorator(f: F) -> F:
_param_memo(f, cls(param_decls, **attrs))
return f

return decorator


def option(*param_decls, cls=None, group=None, **attrs):
def option(
*param_decls: str,
cls: type[click.Option] | None = None,
group: OptionGroup | None = None,
**attrs: Any,
) -> Callable[[F], F]:
"""Attach an ``Option`` to the command.
Refer to :class:`click.Option` and :class:`click.Parameter` for more info
about the accepted parameters.
Expand All @@ -43,10 +60,10 @@ def option(*param_decls, cls=None, group=None, **attrs):
"""
OptionClass = cls or Option

def decorator(f):
_param_memo(f, OptionClass(param_decls, **attrs))
new_option = f.__click_params__[-1]
new_option.group = group
def decorator(f: F) -> F:
new_option = OptionClass(param_decls, **attrs)
_param_memo(f, new_option)
setattr(new_option, "group", group)
if group and group.hidden:
new_option.hidden = True
return f
Expand Down
8 changes: 8 additions & 0 deletions src/cloup/constraints/_conditional.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@
This modules contains classes for creating conditional constraints.
"""

import sys
from collections.abc import Sequence

from click import Context, Parameter

if sys.version_info >= (3, 12):
from typing import override
else:
from typing_extensions import override

from .._util import make_repr
from ._core import Constraint
from .conditions import AllSet, IsSet, Predicate
Expand Down Expand Up @@ -60,6 +66,7 @@ def help(self, ctx: Context) -> str:
else:
return f"{then_help} if {condition}, otherwise {else_help}"

@override
def check_consistency(self, params: Sequence[Parameter]) -> None:
self._then.check_consistency(params)
if self._else:
Expand All @@ -83,6 +90,7 @@ def check_values(self, params: Sequence[Parameter], ctx: Context) -> None:
f"when {desc}, {err}", ctx=ctx, constraint=self, params=params
)

@override
def __repr__(self) -> str:
if self._else:
return make_repr(self, self._condition, then=self._then, else_=self._else)
Expand Down
21 changes: 21 additions & 0 deletions src/cloup/constraints/_core.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import abc
import sys
from collections.abc import Callable, Sequence
from typing import (
Any,
Expand All @@ -9,6 +10,11 @@

import click

if sys.version_info >= (3, 12):
from typing import override
else:
from typing_extensions import override

from cloup._util import (
FrozenSpace,
check_arg,
Expand Down Expand Up @@ -260,6 +266,7 @@ def help(self, ctx: click.Context) -> str:
for c in self.constraints
)

@override
def check_consistency(self, params: Sequence[click.Parameter]) -> None:
for c in self.constraints:
c.check_consistency(params)
Expand All @@ -277,6 +284,7 @@ def check_values(self, params: Sequence[click.Parameter], ctx: click.Context) ->
for c in self.constraints:
c.check_values(params, ctx)

@override
def __and__(self, other: Constraint) -> "And":
if isinstance(other, And):
return And(*self.constraints, *other.constraints)
Expand All @@ -299,6 +307,7 @@ def check_values(self, params: Sequence[click.Parameter], ctx: click.Context) ->
self.help(ctx), ctx=ctx, constraint=self, params=params
)

@override
def __or__(self, other: Constraint) -> "Or":
if isinstance(other, Or):
return Or(*self.constraints, *other.constraints)
Expand Down Expand Up @@ -370,6 +379,7 @@ def _get_rephrased_error(self, err: ConstraintViolated) -> str | None:
else:
return self._error(err)

@override
def check_consistency(self, params: Sequence[click.Parameter]) -> None:
try:
self.constraint.check_consistency(params)
Expand All @@ -387,6 +397,7 @@ def check_values(self, params: Sequence[click.Parameter], ctx: click.Context) ->
)
raise

@override
def __repr__(self) -> str:
return make_one_line_repr(self, help=self._help)

Expand All @@ -412,6 +423,7 @@ def __init__(self, constraint: Constraint, **attrs: Any):
def help(self, ctx: click.Context) -> str:
return self._constraint.help(ctx)

@override
def check_consistency(self, params: Sequence[click.Parameter]) -> None:
try:
self._constraint.check_consistency(params)
Expand All @@ -421,6 +433,7 @@ def check_consistency(self, params: Sequence[click.Parameter]) -> None:
def check_values(self, params: Sequence[click.Parameter], ctx: click.Context) -> None:
self._constraint.check_values(params, ctx)

@override
def __repr__(self) -> str:
return make_repr(self, **self._attrs)

Expand Down Expand Up @@ -462,6 +475,7 @@ def __init__(self, n: int):
def help(self, ctx: click.Context) -> str:
return f"at least {self.min_num_params} required"

@override
def check_consistency(self, params: Sequence[click.Parameter]) -> None:
n = self.min_num_params
if len(params) < n:
Expand All @@ -483,6 +497,7 @@ def check_values(self, params: Sequence[click.Parameter], ctx: click.Context) ->
params=params,
)

@override
def __repr__(self) -> str:
return make_repr(self, self.min_num_params)

Expand All @@ -497,6 +512,7 @@ def __init__(self, n: int):
def help(self, ctx: click.Context) -> str:
return f"at most {self.max_num_params} accepted"

@override
def check_consistency(self, params: Sequence[click.Parameter]) -> None:
num_required_params = len(get_required_params(params))
if num_required_params > self.max_num_params:
Expand All @@ -515,6 +531,7 @@ def check_values(self, params: Sequence[click.Parameter], ctx: click.Context) ->
params=params,
)

@override
def __repr__(self) -> str:
return make_repr(self, self.max_num_params)

Expand All @@ -528,9 +545,11 @@ def __init__(self, n: int):
super().__init__(RequireAtLeast(n) & AcceptAtMost(n))
self.num_params = n

@override
def help(self, ctx: click.Context) -> str:
return f"exactly {self.num_params} required"

@override
def check_values(self, params: Sequence[click.Parameter], ctx: click.Context) -> None:
n = self.num_params
given_params = get_params_whose_value_is_set(params, ctx.params)
Expand All @@ -542,6 +561,7 @@ def check_values(self, params: Sequence[click.Parameter], ctx: click.Context) ->
) + format_param_list(params)
raise ConstraintViolated(reason, ctx=ctx, constraint=self, params=params)

@override
def __repr__(self) -> str:
return make_repr(self, self.num_params)

Expand All @@ -561,6 +581,7 @@ def __init__(self, min: int, max: int): # noqa
self.min_num_params = min
self.max_num_params = max

@override
def help(self, ctx: click.Context) -> str:
return (
f"at least {self.min_num_params} required, "
Expand Down
3 changes: 2 additions & 1 deletion src/cloup/constraints/_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ def _constraint_memo(
f: Any, constr: Union[BoundConstraintSpec, "BoundConstraint"]
) -> None:
if not hasattr(f, "__cloup_constraints__"):
f.__cloup_constraints__ = []
constraints: list[BoundConstraintSpec | BoundConstraint] = []
f.__cloup_constraints__ = constraints
f.__cloup_constraints__.append(constr)


Expand Down
Loading
Loading