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
5 changes: 5 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,8 @@ updates:
interval: "weekly"
reviewers:
- dskrypa
open-pull-requests-limit: 1
groups:
all-deps:
patterns:
- "*"
33 changes: 27 additions & 6 deletions docs/_src/parameters.rst
Original file line number Diff line number Diff line change
Expand Up @@ -147,12 +147,21 @@ more about how attribute names are used to automatically generate ``--long`` opt
**Unique Option initialization parameters:**

:choices: A container that holds the specific values that users must pick from. By default, any value is allowed.
:nargs: The number of values that are expected/required when this parameter is specified. Defaults to ``+``
when ``action='append'``, and to ``1`` otherwise. See :ref:`parameters:Parameters:nargs` for more info.
:action: The action to take on individual parsed values. Supported actions include ``store`` and ``append``.
Defaults to ``store`` when ``nargs=1`` (the default if neither action nor nargs are specified), and to ``append``
otherwise. A single value will be stored when ``action='store'``, and a list of values will be stored when
``action='append'``.
:nargs: The number of values that are expected/required when this parameter is specified. Defaults to ``+`` when
``action='append'`` or ``action='append_default'``, and to ``1`` otherwise. See :ref:`parameters:Parameters:nargs`
for more info.
:action: The action to take on individual parsed values. Supported actions include ``store``, ``append``, and
``append_default``. Defaults to ``store`` when ``nargs=1`` (the default if neither action nor nargs are specified),
and to ``append`` otherwise. A single value will be stored when ``action='store'``, and a list of values will be
stored when ``action='append'`` or ``action='append_default'``. The difference between the two append actions is
that using ``append_default`` will result in the default value(s) being included at the beginning of the list of
values.

.. version-changed:: 2026-07-04

The ``append`` action was changed to exclude default value(s). To restore the behavior from previous versions,
explicitly specify ``action='append_default'``.

:allow_leading_dash: Whether string values may begin with a dash (``-``). By default, if a value begins with a dash,
it is only accepted if it appears to be a negative numeric value. Use ``True`` / ``always`` /
``AllowLeadingDash.ALWAYS`` to allow any value that begins with a dash (as long as it is not an option string for an
Expand Down Expand Up @@ -388,6 +397,18 @@ The generic :class:`.Positional` parameter that accepts arbitrary values or list
allows 0 values to have the same effect as making the Parameter not required (the ``required`` option is not
supported for Positional Parameters). Only the last Positional in a given :class:`.Command` may allow a
variable / unbound number of arguments.
:action: The action to take on individual parsed values. Supported actions include ``store``, ``append``, and
``append_default``. Defaults to ``store`` when ``nargs=1`` (the default if neither action nor nargs are specified),
and to ``append`` otherwise. A single value will be stored when ``action='store'``, and a list of values will be
stored when ``action='append'`` or ``action='append_default'``. The difference between the two append actions is
that using ``append_default`` will result in the default value(s) being included at the beginning of the list of
values.

.. version-changed:: 2026-07-04

The ``append`` action was changed to exclude default value(s). To restore the behavior from previous versions,
explicitly specify ``action='append_default'``.

:default: Only supported when ``action='store'`` and 0 values are allowed by the specified ``nargs``. Defaults
to ``None`` under those conditions.
:choices: A container that holds the specific values that users must pick from. By default, any value is allowed.
Expand Down
4 changes: 3 additions & 1 deletion lib/cli_command_parser/nargs.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ def __contains__(self, num: int) -> bool:
"""See :meth:`.satisfied`"""
return self.satisfied(num)

def __eq__(self, other) -> bool:
def matches(self, other: Any) -> bool:
match other:
case Nargs():
return self._eq_nargs(other)
Expand All @@ -139,6 +139,8 @@ def __eq__(self, other) -> bool:
case _:
return NotImplemented

__eq__ = matches

def _eq_nargs(self, other: Nargs) -> bool:
if not self._has_upper_bound:
return other.max is self.max and self.min == other.min
Expand Down
86 changes: 25 additions & 61 deletions lib/cli_command_parser/parameters/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,21 @@

from abc import ABC, abstractmethod
from enum import Enum
from typing import TYPE_CHECKING, ClassVar, Generic, NoReturn, TypeVar, Union
from typing import TYPE_CHECKING, ClassVar, Generic, NoReturn, TypeVar

from ..context import ctx
from ..exceptions import BadArgument, InvalidChoice, MissingArgument, ParamConflict, ParamUsageError, TooManyArguments
from ..inputs import InputType
from ..nargs import Nargs
from ..utils import _NotSet, camel_to_snake_case
from ..utils import _NotSet, _NotSetType, camel_to_snake_case

if TYPE_CHECKING:
from ..commands import Command
from ..typing import Bool, OptStr
from .base import BaseFlag, Parameter

Found = int | NoReturn

__all__ = [
'ParamAction',
'Store',
Expand All @@ -46,19 +48,21 @@ def __str__(self) -> str:

P = TypeVar('P', bound='Parameter')
F = TypeVar('F', bound='BaseFlag')
Found = Union[int, NoReturn]


class ParamAction(ABC, Generic[P]):
__slots__ = ('param',)
name: str
# param: P
default = _NotSet
default: None | _NotSetType = _NotSet
accepts_values: bool = False
accepts_consts: bool = False

def __init_subclass__(
cls, default=_PANotSet, accepts_values: bool | None = None, accepts_consts: bool | None = None, **kwargs
cls,
default: None | _PANotSetType = _PANotSet,
accepts_values: bool | None = None,
accepts_consts: bool | None = None,
**kwargs,
):
super().__init_subclass__(**kwargs)
cls.name = camel_to_snake_case(cls.__name__)
Expand Down Expand Up @@ -104,13 +108,6 @@ def add_value(self, value: str, *, combo: bool = False, joined: bool = False, en
def add_env_value(self, value: str, env_var: str) -> Found:
return self.add_value(value, env_var=env_var)

# Note: Not used yet
# def add_values(self, values: Sequence[str], *, combo: bool = False) -> Found:
# added = 0
# for value in values:
# added += self.add_value(value, combo=combo)
# return added

def add_const(self, *, opt: OptStr = None, combo: bool = False) -> Found: # noqa
ctx.record_action(self.param)
raise MissingArgument(self.param)
Expand All @@ -135,14 +132,6 @@ def would_accept_all(self, values: list[str], combo: bool = False) -> bool:
else:
return valid_values and len(values) in self.param.nargs

# Note: Not used yet
# def _prep_and_validate(self, values: Sequence[str], combo: bool) -> Iterator[T_co]:
# prepare_value, validate = self.param.prepare_value, self.param.validate
# for value in values:
# value = prepare_value(value, combo)
# validate(value)
# yield value

# endregion

# region Backtracking
Expand Down Expand Up @@ -198,22 +187,13 @@ def set_value(self, value):
def append_value(self, value):
parsed = ctx.get_parsed_value(self.param)
if parsed is _NotSet:
parsed = self.get_default()
parsed = []
ctx.set_parsed_value(self.param, parsed)
elif self.param.nargs.max_reached(parsed):
raise TooManyArguments(self.param, f'already found {len(parsed)} values')

parsed.append(value)

# Note: Not used yet
# def extend_values(self, values: Iterable[T_co]):
# parsed = ctx.get_parsed_value(self.param)
# if parsed is _NotSet:
# parsed = self.get_default()
# ctx.set_parsed_value(self.param, parsed)
#
# parsed.extend(values)


class _ConstAction(ParamAction[F], ABC):
__slots__ = ()
Expand All @@ -239,14 +219,6 @@ def append_const(self, const):

parsed.append(const)

# def extend_consts(self, consts):
# parsed = ctx.get_parsed_value(self.param)
# if parsed is _NotSet:
# parsed = self.get_default()
# ctx.set_parsed_value(self.param, parsed)
#
# parsed.extend(consts)

def add_env_value(self, value: str, env_var: str) -> Found:
const, use_value = self.param.get_env_const(value, env_var)
# The const may only be _NotSet once StoreValueOrConst / AppendValueOrConst are put into use
Expand Down Expand Up @@ -280,17 +252,6 @@ def add_value(self, value: str, *, combo: bool = False, joined: Bool = False, en
self.set_value(value)
return 1

# Note: Not used yet
# def add_values(self, values: Sequence[str], *, combo: bool = False) -> Found:
# ctx.record_action(self.param)
# if not values:
# raise MissingArgument(self.param)
# elif (val_count := len(values)) not in self.param.nargs:
# raise BadArgument(self.param, f'expected nargs={self.param.nargs} values but found {val_count}')
#
# self.set_value([value for value in self._prep_and_validate(values, combo)])
# return val_count

# endregion

# region Parsing
Expand All @@ -316,17 +277,6 @@ def add_value(self, value: str, *, combo: bool = False, joined: Bool = False, en
self.append_value(value)
return 1

# Note: Not used yet
# def add_values(self, values: Sequence[str], *, combo: bool = False) -> Found:
# ctx.record_action(self.param)
# if not values:
# raise MissingArgument(self.param)
# elif (val_count := len(values)) not in (nargs := self.param.nargs):
# raise BadArgument(self.param, f'expected {nargs=} values but found {val_count}')
#
# self.extend_values(value for value in self._prep_and_validate(values, combo))
# return val_count

# endregion

# region Parsing
Expand Down Expand Up @@ -398,6 +348,20 @@ def finalize_value(self, value):
# endregion


class AppendDefault(Append):
__slots__ = ()

def append_value(self, value):
parsed = ctx.get_parsed_value(self.param)
if parsed is _NotSet:
parsed = self.get_default()
ctx.set_parsed_value(self.param, parsed)
elif self.param.nargs.max_reached(parsed):
raise TooManyArguments(self.param, f'already found {len(parsed)} values')

parsed.append(value)


class BasicConstAction(_ConstAction, ABC, accepts_consts=True):
__slots__ = ()
default_nargs = Nargs(0)
Expand Down
68 changes: 13 additions & 55 deletions lib/cli_command_parser/parameters/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@


class Param(Generic[T]):
"""
Primarily used for type checking. Generally, explicit annotations for parameters are not necessary. In some cases,
such as when indicating that a common parameter will exist with different definitions in one or more subclasses,
then the use of this class for the annotations may be useful.
"""

__slots__ = ()

if TYPE_CHECKING:
Expand Down Expand Up @@ -626,56 +632,6 @@ class - it is not meant to be used directly.
strict_env: Bool
use_env_value: Bool

# region Init Overloads

if TYPE_CHECKING:

@overload
def __init__(
self: BaseOption[T, _NotSetType],
*option_strs: str,
action: str,
help: OptStr = None, # noqa
hide: Bool = False,
metavar: OptStr = None,
name: OptStr = None,
name_mode: OptionNameMode | OptStr | _NotSetType = _NotSet,
required: Literal[True],
default: _NotSetType = _NotSet,
default_cb: None = None,
cb_with_cmd: Bool = False,
show_default: Bool = None,
strict_default: Bool = False,
env_var: OptStrs = None,
strict_env: bool = True,
use_env_value: Bool = None,
show_env_var: Bool = None,
): ...

@overload
def __init__(
self: BaseOption[T, D],
*option_strs: str,
action: str,
help: OptStr = None, # noqa
hide: Bool = False,
metavar: OptStr = None,
name: OptStr = None,
name_mode: OptionNameMode | OptStr | _NotSetType = _NotSet,
required: Bool = False,
default: D | _NotSetType = _NotSet,
default_cb: DefaultFunc[D] | None = None,
cb_with_cmd: Bool = False,
show_default: Bool = None,
strict_default: Bool = False,
env_var: OptStrs = None,
strict_env: bool = True,
use_env_value: Bool = None,
show_env_var: Bool = None,
): ...

# endregion

def __init__(
self,
*option_strs: str,
Expand Down Expand Up @@ -741,13 +697,15 @@ def __init__(self, default: AllowLeadingDash = AllowLeadingDash.NUMERIC):
def __set_name__(self, owner, name: str):
self.name = name

@overload
def __get__(self, instance: None, owner: Any) -> AllowLeadingDashProperty: ...
if TYPE_CHECKING:

@overload
def __get__(self, instance: AnyParam, owner: Any) -> AllowLeadingDash: ...
@overload
def __get__(self, instance: Literal[None], owner: Any = None) -> Self: ...

@overload
def __get__(self, instance: AnyParam, owner: Any = None) -> AllowLeadingDash: ...

def __get__(self, instance: AnyParam | None, owner: Any) -> AllowLeadingDash | AllowLeadingDashProperty:
def __get__(self, instance: AnyParam | None, owner: Any = None) -> AllowLeadingDash | Self:
if instance is None:
return self
return instance.__dict__.get(self.name, self.default)
Expand Down
Loading