summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Lord <davidism@gmail.com>2022-04-28 11:05:06 -0700
committerDavid Lord <davidism@gmail.com>2022-04-28 11:05:06 -0700
commit501dec6e4324f289eee598a77865a6de95b62727 (patch)
treec62c4007aa83f9623f9cdc99be2097a5aef3cd4a
parent0aec1168ac591e159baf6f61026d6ae322c53aaf (diff)
downloadclick-parser-rewrite.tar.gz
-rw-r--r--setup.cfg2
-rw-r--r--src/click/__init__.py2
-rw-r--r--src/click/core.py1075
-rw-r--r--src/click/exceptions.py8
-rw-r--r--src/click/parser.py485
-rw-r--r--src/click/shell_completion.py16
-rw-r--r--src/click/testing.py6
-rw-r--r--tests/test_arguments.py2
-rw-r--r--tests/test_basic.py2
-rw-r--r--tests/test_commands.py2
10 files changed, 628 insertions, 972 deletions
diff --git a/setup.cfg b/setup.cfg
index 592e1a1..64b88cb 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -39,7 +39,7 @@ where = src
[tool:pytest]
testpaths = tests
filterwarnings =
- error
+ #error
[coverage:run]
branch = True
diff --git a/src/click/__init__.py b/src/click/__init__.py
index a6e9799..e07c326 100644
--- a/src/click/__init__.py
+++ b/src/click/__init__.py
@@ -5,7 +5,6 @@ around a simple API that does not come with too much magic and is
composable.
"""
from .core import Argument as Argument
-from .core import BaseCommand as BaseCommand
from .core import Command as Command
from .core import CommandCollection as CommandCollection
from .core import Context as Context
@@ -36,7 +35,6 @@ from .exceptions import UsageError as UsageError
from .formatting import HelpFormatter as HelpFormatter
from .formatting import wrap_text as wrap_text
from .globals import get_current_context as get_current_context
-from .parser import OptionParser as OptionParser
from .termui import clear as clear
from .termui import confirm as confirm
from .termui import echo_via_pager as echo_via_pager
diff --git a/src/click/core.py b/src/click/core.py
index 5abfb0f..2f9f254 100644
--- a/src/click/core.py
+++ b/src/click/core.py
@@ -1,31 +1,36 @@
import enum
import errno
import inspect
+import itertools
import os
import sys
import typing as t
from collections import abc
+from collections import defaultdict
from contextlib import contextmanager
from contextlib import ExitStack
from functools import partial
from functools import update_wrapper
+from gettext import gettext
from gettext import gettext as _
from gettext import ngettext
from itertools import repeat
from . import types
from .exceptions import Abort
+from .exceptions import BadArgumentUsage
+from .exceptions import BadOptionUsage
from .exceptions import BadParameter
from .exceptions import ClickException
from .exceptions import Exit
from .exceptions import MissingParameter
+from .exceptions import NoArgsIsHelpError
+from .exceptions import NoSuchOption
from .exceptions import UsageError
from .formatting import HelpFormatter
from .formatting import join_options
from .globals import pop_context
from .globals import push_context
-from .parser import _flag_needs_value
-from .parser import OptionParser
from .parser import split_opt
from .termui import confirm
from .termui import prompt
@@ -34,7 +39,6 @@ from .utils import _detect_program_name
from .utils import _expand_args
from .utils import echo
from .utils import make_default_short_help
-from .utils import make_str
from .utils import PacifyFlushWrapper
if t.TYPE_CHECKING:
@@ -287,11 +291,6 @@ class Context:
self.params: t.Dict[str, t.Any] = {}
#: the leftover arguments.
self.args: t.List[str] = []
- #: protected arguments. These are arguments that are prepended
- #: to `args` when certain parsing scenarios are encountered but
- #: must be never propagated to another arguments. This is used
- #: to implement nested parsing.
- self.protected_args: t.List[str] = []
#: the collected prefixes of the command's options.
self._opt_prefixes: t.Set[str] = set(parent._opt_prefixes) if parent else set()
@@ -808,26 +807,52 @@ class Context:
return self._parameter_source.get(name)
-class BaseCommand:
- """The base command implements the minimal API contract of commands.
- Most code will never use this as it does not implement a lot of useful
- functionality but it can act as the direct subclass of alternative
- parsing methods that do not depend on the Click parser.
+# Sentinel value that indicates an option was passed as a flag without a
+# value but is not a flag option. Option.consume_value uses this to
+# prompt or use the flag_value.
+_flag_needs_value = object()
- For instance, this can be used to bridge Click and other systems like
- argparse or docopt.
- Because base commands do not implement a lot of the API that other
- parts of Click take for granted, they are not supported for all
- operations. For instance, they cannot be used with the decorators
- usually and they have no built-in callback system.
-
- .. versionchanged:: 2.0
- Added the `context_settings` parameter.
+class Command:
+ """Commands are the basic building block of command line interfaces in
+ Click. A basic command handles command line parsing and might dispatch
+ more parsing to commands nested below it.
:param name: the name of the command to use unless a group overrides it.
:param context_settings: an optional dictionary with defaults that are
passed to the context object.
+ :param callback: the callback to invoke. This is optional.
+ :param params: the parameters to register with this command. This can
+ be either :class:`Option` or :class:`Argument` objects.
+ :param help: the help string to use for this command.
+ :param epilog: like the help string but it's printed at the end of the
+ help page after everything else.
+ :param short_help: the short help to use for this command. This is
+ shown on the command listing of the parent command.
+ :param add_help_option: by default each command registers a ``--help``
+ option. This can be disabled by this parameter.
+ :param no_args_is_help: this controls what happens if no arguments are
+ provided. This option is disabled by default.
+ If enabled this will add ``--help`` as argument
+ if no arguments are passed
+ :param hidden: hide this command from help outputs.
+
+ :param deprecated: issues a message indicating that
+ the command is deprecated.
+
+ .. versionchanged:: 8.1
+ ``help``, ``epilog``, and ``short_help`` are stored unprocessed,
+ all formatting is done when outputting help text, not at init,
+ and is done even if not using the ``@command`` decorator.
+
+ .. versionchanged:: 8.0
+ Added a ``repr`` showing the command name.
+
+ .. versionchanged:: 7.1
+ Added the ``no_args_is_help`` parameter.
+
+ .. versionchanged:: 2.0
+ Added the ``context_settings`` parameter.
"""
#: The context class to create with :meth:`make_context`.
@@ -845,6 +870,16 @@ class BaseCommand:
self,
name: t.Optional[str],
context_settings: t.Optional[t.Dict[str, t.Any]] = None,
+ callback: t.Optional[t.Callable[..., t.Any]] = None,
+ params: t.Optional[t.List["Parameter"]] = None,
+ help: t.Optional[str] = None,
+ epilog: t.Optional[str] = None,
+ short_help: t.Optional[str] = None,
+ options_metavar: t.Optional[str] = "[OPTIONS]",
+ add_help_option: bool = True,
+ no_args_is_help: bool = False,
+ hidden: bool = False,
+ deprecated: bool = False,
) -> None:
#: the name the command thinks it has. Upon registering a command
#: on a :class:`Group` the group will default the command name
@@ -857,29 +892,178 @@ class BaseCommand:
#: an optional dictionary with defaults passed to the context.
self.context_settings: t.Dict[str, t.Any] = context_settings
+ #: the callback to execute when the command fires. This might be
+ #: `None` in which case nothing happens.
+ self.callback = callback
+ #: the list of parameters for this command in the order they
+ #: should show up in the help page and execute. Eager parameters
+ #: will automatically be handled before non eager ones.
+ self.params: t.List["Parameter"] = params or []
+ self.help = help
+ self.epilog = epilog
+ self.options_metavar = options_metavar
+ self.short_help = short_help
+ self.add_help_option = add_help_option
+ self.no_args_is_help = no_args_is_help
+ self.hidden = hidden
+ self.deprecated = deprecated
def to_info_dict(self, ctx: Context) -> t.Dict[str, t.Any]:
- """Gather information that could be useful for a tool generating
- user-facing documentation. This traverses the entire structure
- below this command.
+ return {
+ "name": self.name,
+ "params": [param.to_info_dict() for param in self.get_params(ctx)],
+ "help": self.help,
+ "epilog": self.epilog,
+ "short_help": self.short_help,
+ "hidden": self.hidden,
+ "deprecated": self.deprecated,
+ }
- Use :meth:`click.Context.to_info_dict` to traverse the entire
- CLI structure.
+ def __repr__(self) -> str:
+ return f"<{self.__class__.__name__} {self.name}>"
- :param ctx: A :class:`Context` representing this command.
+ def get_usage(self, ctx: Context) -> str:
+ """Formats the usage line into a string and returns it.
- .. versionadded:: 8.0
+ Calls :meth:`format_usage` internally.
"""
- return {"name": self.name}
+ formatter = ctx.make_formatter()
+ self.format_usage(ctx, formatter)
+ return formatter.getvalue().rstrip("\n")
- def __repr__(self) -> str:
- return f"<{self.__class__.__name__} {self.name}>"
+ def get_params(self, ctx: Context) -> t.List["Parameter"]:
+ rv = self.params
+ help_option = self.get_help_option(ctx)
- def get_usage(self, ctx: Context) -> str:
- raise NotImplementedError("Base commands cannot get usage")
+ if help_option is not None:
+ rv = [*rv, help_option]
+
+ return rv
+
+ def format_usage(self, ctx: Context, formatter: HelpFormatter) -> None:
+ """Writes the usage line into the formatter.
+
+ This is a low-level method called by :meth:`get_usage`.
+ """
+ pieces = self.collect_usage_pieces(ctx)
+ formatter.write_usage(ctx.command_path, " ".join(pieces))
+
+ def collect_usage_pieces(self, ctx: Context) -> t.List[str]:
+ """Returns all the pieces that go into the usage line and returns
+ it as a list of strings.
+ """
+ rv = [self.options_metavar] if self.options_metavar else []
+
+ for param in self.get_params(ctx):
+ rv.extend(param.get_usage_pieces(ctx))
+
+ return rv
+
+ def get_help_option_names(self, ctx: Context) -> t.List[str]:
+ """Returns the names for the help option."""
+ all_names = set(ctx.help_option_names)
+ for param in self.params:
+ all_names.difference_update(param.opts)
+ all_names.difference_update(param.secondary_opts)
+ return list(all_names)
+
+ def get_help_option(self, ctx: Context) -> t.Optional["Option"]:
+ """Returns the help option object."""
+ help_options = self.get_help_option_names(ctx)
+
+ if not help_options or not self.add_help_option:
+ return None
+
+ def show_help(ctx: Context, param: "Parameter", value: str) -> None:
+ if value and not ctx.resilient_parsing:
+ echo(ctx.get_help(), color=ctx.color)
+ ctx.exit()
+
+ return Option(
+ help_options,
+ is_flag=True,
+ is_eager=True,
+ expose_value=False,
+ callback=show_help,
+ help=_("Show this message and exit."),
+ )
def get_help(self, ctx: Context) -> str:
- raise NotImplementedError("Base commands cannot get help")
+ """Formats the help into a string and returns it.
+
+ Calls :meth:`format_help` internally.
+ """
+ formatter = ctx.make_formatter()
+ self.format_help(ctx, formatter)
+ return formatter.getvalue().rstrip("\n")
+
+ def get_short_help_str(self, limit: int = 45) -> str:
+ """Gets short help for the command or makes it by shortening the
+ long help string.
+ """
+ if self.short_help:
+ text = inspect.cleandoc(self.short_help)
+ elif self.help:
+ text = make_default_short_help(self.help, limit)
+ else:
+ text = ""
+
+ if self.deprecated:
+ text = _("(Deprecated) {text}").format(text=text)
+
+ return text.strip()
+
+ def format_help(self, ctx: Context, formatter: HelpFormatter) -> None:
+ """Writes the help into the formatter if it exists.
+
+ This is a low-level method called by :meth:`get_help`.
+
+ This calls the following methods:
+
+ - :meth:`format_usage`
+ - :meth:`format_help_text`
+ - :meth:`format_options`
+ - :meth:`format_epilog`
+ """
+ self.format_usage(ctx, formatter)
+ self.format_help_text(ctx, formatter)
+ self.format_options(ctx, formatter)
+ self.format_epilog(ctx, formatter)
+
+ def format_help_text(self, ctx: Context, formatter: HelpFormatter) -> None:
+ """Writes the help text to the formatter if it exists."""
+ text = self.help if self.help is not None else ""
+
+ if self.deprecated:
+ text = _("(Deprecated) {text}").format(text=text)
+
+ if text:
+ text = inspect.cleandoc(text).partition("\f")[0]
+ formatter.write_paragraph()
+
+ with formatter.indentation():
+ formatter.write_text(text)
+
+ def format_options(self, ctx: Context, formatter: HelpFormatter) -> None:
+ """Writes all the options into the formatter if they exist."""
+ opts = []
+ for param in self.get_params(ctx):
+ rv = param.get_help_record(ctx)
+ if rv is not None:
+ opts.append(rv)
+
+ if opts:
+ with formatter.section(_("Options")):
+ formatter.write_dl(opts)
+
+ def format_epilog(self, ctx: Context, formatter: HelpFormatter) -> None:
+ """Writes the epilog into the formatter if it exists."""
+ if self.epilog:
+ epilog = inspect.cleandoc(self.epilog)
+ formatter.write_paragraph()
+
+ with formatter.indentation():
+ formatter.write_text(epilog)
def make_context(
self,
@@ -912,30 +1096,347 @@ class BaseCommand:
if key not in extra:
extra[key] = value
- ctx = self.context_class(
- self, info_name=info_name, parent=parent, **extra # type: ignore
- )
+ ctx = self.context_class(self, info_name=info_name, parent=parent, **extra)
with ctx.scope(cleanup=False):
self.parse_args(ctx, args)
+
return ctx
def parse_args(self, ctx: Context, args: t.List[str]) -> t.List[str]:
- """Given a context and a list of arguments this creates the parser
- and parses the arguments, then modifies the context as necessary.
- This is automatically invoked by :meth:`make_context`.
- """
- raise NotImplementedError("Base commands do not know how to parse arguments.")
+ if not args and self.no_args_is_help and not ctx.resilient_parsing:
+ raise NoArgsIsHelpError(ctx)
+
+ if ctx.token_normalize_func is not None:
+
+ def normalize(name: str) -> str:
+ if name[0] == name[1]:
+ prefix = name[:2]
+ name = name[2:]
+ else:
+ prefix = name[0]
+ name = name[1:]
+
+ return f"{prefix}{ctx.token_normalize_func(name)}"
+
+ else:
+ normalize = None
+
+ params = self.get_params(ctx)
+ # Map each short and long option flag to their option objects, and collect the
+ # order of positional arguments. The one and two character prefixes used by
+ # options are collected to check if a token looks like an option.
+ opt_prefixes: t.Set[str] = set()
+ short_opts: t.Dict[str, Option] = {}
+ long_opts: t.Dict[str, Option] = {}
+ secondary_opts: t.Dict[Option, t.Set[str]] = {}
+ arg_params: t.List[Argument] = []
+
+ for param in params:
+ if isinstance(param, Option):
+ for name in itertools.chain(param.opts, param.secondary_opts):
+ if normalize is not None:
+ name = normalize(name)
+
+ if name[0] == name[1]:
+ # long prefix
+ opt_prefixes.add(name[:2])
+ long_opts[name] = param
+ else:
+ # short prefix
+ opt_prefixes.add(name[0])
+
+ if len(name) == 2:
+ # -a is a short opt
+ short_opts[name] = param
+ else:
+ # -ab is a long opt with a short prefix
+ long_opts[name] = param
+
+ # Record the normalized secondary names for each option,
+ # for easier comparison during parsing.
+ if normalize is not None:
+ secondary_opts[param] = {
+ normalize(name) for name in param.secondary_opts
+ }
+ else:
+ secondary_opts[param] = set(param.secondary_opts)
+
+ elif isinstance(param, Argument):
+ arg_params.append(param)
+
+ # Map parameter names to collected values.
+ values: t.Dict[str, t.List[t.Any]] = defaultdict(list)
+ # Track what order parameters were seen.
+ param_order: t.List[Parameter] = []
+ # After mapping arguments to values, any extra values will still be here.
+ rest: t.List[str] = []
+ # Track what the parser should do with the current token.
+ lf_any = 0
+ lf_value_or_opt = 1
+ lf_value = 2
+ looking_for = lf_any
+ # Tracks the current option that needs a value.
+ looking_opt: t.Optional[Option] = None
+ looking_opt_name: t.Optional[str] = None
+ # Treat tokens as a stack, with the first token at the top.
+ tokens = list(reversed(args))
+
+ while tokens:
+ token = tokens.pop()
+
+ if looking_for is lf_any:
+ if token == "--":
+ # All tokens after -- are considered arguments.
+ rest.extend(reversed(tokens))
+ tokens.clear()
+ elif token != "-" and ( # stdin/out file value
+ token[:1] in opt_prefixes or token[:2] in opt_prefixes
+ ):
+ # looks like an option
+ original_name, long_sep, value = token.partition("=")
+
+ if normalize is not None:
+ name = normalize(original_name)
+ else:
+ name = original_name
+
+ if name in long_opts:
+ # any prefix, matching long opt
+ opt = long_opts[name]
+ param_order.append(opt)
+
+ if long_sep:
+ # key=value, only valid if the option is not a flag
+ if opt.is_flag:
+ message = gettext(
+ "Option '{name}' does not take a value."
+ ).format(name=original_name)
+ raise BadOptionUsage(original_name, message, ctx=ctx)
+
+ tokens.append(value)
+ looking_opt = opt
+ looking_opt_name = original_name
+ looking_for = lf_value
+ elif opt.is_flag:
+ # no attached value, and no value needed
+ if name in secondary_opts[opt]:
+ values[opt.name].append(not opt.flag_value)
+ else:
+ values[opt.name].append(opt.flag_value)
+ else:
+ # no attached value, and a value may be needed
+ looking_opt = opt
+ looking_opt_name = original_name
+
+ if opt._flag_needs_value:
+ looking_for = lf_value_or_opt
+ else:
+ looking_for = lf_value
+ elif token[:2] in opt_prefixes:
+ # long prefix, no matching long opt
+ if ctx.ignore_unknown_options:
+ rest.append(token)
+ else:
+ from difflib import get_close_matches
+
+ possibilities = get_close_matches(token, long_opts)
+ raise NoSuchOption(
+ token, possibilities=possibilities, ctx=ctx
+ )
+ else:
+ # short prefix, try short opts
+ prefix = token[0]
+ chars = token[1:]
+ unmatched = []
+
+ for i, c in enumerate(chars, 1):
+ original_name = f"{prefix}{c}"
+
+ if normalize is not None:
+ name = normalize(original_name)
+ else:
+ name = original_name
+
+ if name in short_opts:
+ opt = short_opts[name]
+ param_order.append(opt)
+
+ if opt.is_flag:
+ # Record the flag, then continue trying short opts.
+ if name in secondary_opts[opt]:
+ values[opt.name].append(not opt.flag_value)
+ else:
+ values[opt.name].append(opt.flag_value)
+ else:
+ # Not a flag, stop trying short options and begin
+ # looking for values.
+ value = chars[i:]
+
+ if value:
+ # Use any remaining chars as a value.
+ tokens.append(value)
+
+ looking_opt = opt
+ looking_opt_name = original_name
+ looking_for = lf_value
+ break
+ else:
+ # no matching short opt
+ if ctx.ignore_unknown_options:
+ unmatched.append(c)
+ else:
+ raise NoSuchOption(name, ctx=ctx)
+
+ if unmatched:
+ # If unknown options are allowed, add any unused chars back
+ # as a single name with the same prefix.
+ rest.append(f"{prefix}{''.join(unmatched)}")
+ else:
+ # an argument
+ rest.append(token)
+
+ if not ctx.allow_interspersed_args:
+ # If interspersed isn't allowed, all remaining
+ # tokens are considered arguments.
+ rest.extend(reversed(tokens))
+ tokens.clear()
+ elif looking_for is lf_value_or_opt:
+ # The current opt optionally takes a value. Look at the
+ # token then put it back.
+ tokens.append(token)
+
+ if token in short_opts or token in long_opts:
+ # Next token is an opt, mark the current opt as
+ # needing a value, handled during processing.
+ values[looking_opt.name].append(_flag_needs_value)
+ looking_opt = looking_opt_name = None
+ looking_for = lf_any
+ else:
+ # Next token will be used as a value.
+ looking_for = lf_value
+ elif looking_for is lf_value:
+ # The current opt requires at least one value.
+ if looking_opt.nargs == 1:
+ # Exactly one value required
+ values[looking_opt.name].append(token)
+ else:
+ # More than one value required
+ need_n = looking_opt.nargs - 1
+
+ if len(tokens) < need_n:
+ message = ngettext(
+ "Option '{name}' requires {nargs} values but 1 was given.",
+ "Option '{name}' requires {nargs} values"
+ " but {len} were given.",
+ len(tokens) + 1,
+ ).format(
+ name=looking_opt_name,
+ nargs=looking_opt.nargs,
+ len=len(tokens) + 1,
+ )
+ raise BadOptionUsage(looking_opt_name, message, ctx=ctx)
+
+ values[looking_opt.name].append([token, *tokens[-need_n:]])
+ tokens = tokens[:-need_n]
+
+ looking_opt = looking_opt_name = None
+ looking_for = lf_any
+
+ if looking_for is lf_value_or_opt:
+ # No more tokens, mark the current op to get a value later.
+ values[looking_opt.name].append(_flag_needs_value)
+ elif looking_for is lf_value:
+ # No more tokens, but the current opt still required a value.
+ message = ngettext(
+ "Option '{name}' requires a value.",
+ "Option '{name}' requires {nargs} values.",
+ looking_opt.nargs,
+ ).format(name=looking_opt_name, nargs=looking_opt.nargs)
+ raise BadOptionUsage(looking_opt_name, message, ctx=ctx)
+
+ # Treat args as a stack, with the first at the top.
+ arg_params.reverse()
+
+ while arg_params:
+ param = arg_params.pop()
+
+ if param.nargs == -1:
+ if not arg_params:
+ buffer = rest.copy()
+ rest.clear()
+ else:
+ need_n = -sum(p.nargs for p in arg_params if p.nargs > 0)
+ buffer = rest[:need_n]
+ rest = rest[need_n:]
+
+ if param.required and len(buffer) == 0 and not ctx.resilient_parsing:
+ raise MissingParameter(ctx=ctx, param=param)
+
+ if len(buffer) > 0:
+ # Don't record an empty list, so the value can come
+ # from an env var or default.
+ values[param.name].append(buffer)
+ elif param.nargs == 1:
+ if not rest:
+ if param.required and not ctx.resilient_parsing:
+ raise MissingParameter(ctx=ctx, param=param)
+ else:
+ # Don't record a missing value, so it can come from
+ # and env var or default.
+ values[param.name].append(rest[0])
+ rest = rest[1:]
+ else:
+ if len(rest) < param.nargs:
+ if (param.required or len(rest) > 0) and not ctx.resilient_parsing:
+ message = ngettext(
+ "Argument '{name}' requires {nargs}"
+ " values but 1 was given.",
+ "Argument '{name}' requires {nargs}"
+ " values but {len} were given.",
+ len(rest),
+ ).format(
+ name=param.make_metavar(), nargs=param.nargs, len=len(rest)
+ )
+ raise BadArgumentUsage(message, ctx=ctx)
+ else:
+ values[param.name].append(rest[: param.nargs])
+ rest = rest[param.nargs :]
+
+ param_order.extend(reversed(arg_params))
+
+ for param in iter_params_for_processing(param_order, params):
+ _, rest = param.handle_parse_result(ctx, values, rest)
+
+ if rest and not ctx.allow_extra_args and not ctx.resilient_parsing:
+ message = ngettext(
+ "Got unexpected extra argument ({args})",
+ "Got unexpected extra arguments ({args})",
+ len(rest),
+ ).format(args=" ".join(rest))
+ raise BadArgumentUsage(message, ctx=ctx)
+
+ # TODO track opt_prefixes
+ ctx._opt_prefixes.update(opt_prefixes)
+ ctx.args[:] = rest
+ return rest
def invoke(self, ctx: Context) -> t.Any:
- """Given a context, this invokes the command. The default
- implementation is raising a not implemented error.
+ """Given a context, this invokes the attached callback (if it exists)
+ in the right way.
"""
- raise NotImplementedError("Base commands are not invokable by default")
+ if self.deprecated:
+ message = _(
+ "DeprecationWarning: The command {name!r} is deprecated."
+ ).format(name=self.name)
+ echo(style(message, fg="red"), err=True)
+
+ if self.callback is not None:
+ return ctx.invoke(self.callback, **ctx.params)
def shell_complete(self, ctx: Context, incomplete: str) -> t.List["CompletionItem"]:
"""Return a list of completions for the incomplete value. Looks
- at the names of chained multi-commands.
+ at the names of options and chained multi-commands.
Any command could be part of a chained multi-command, so sibling
commands are valid at any point during command completion. Other
@@ -950,6 +1451,25 @@ class BaseCommand:
results: t.List["CompletionItem"] = []
+ if incomplete and not incomplete[0].isalnum():
+ for param in self.get_params(ctx):
+ if (
+ not isinstance(param, Option)
+ or param.hidden
+ or (
+ not param.multiple
+ and ctx.get_parameter_source(param.name) # type: ignore
+ is ParameterSource.COMMANDLINE
+ )
+ ):
+ continue
+
+ results.extend(
+ CompletionItem(name, help=param.help)
+ for name in [*param.opts, *param.secondary_opts]
+ if name.startswith(incomplete)
+ )
+
while ctx.parent is not None:
ctx = ctx.parent
@@ -957,7 +1477,7 @@ class BaseCommand:
results.extend(
CompletionItem(name, help=command.get_short_help_str())
for name, command in _complete_visible_commands(ctx, incomplete)
- if name not in ctx.protected_args
+ if name not in ctx.args
)
return results
@@ -1130,315 +1650,6 @@ class BaseCommand:
return self.main(*args, **kwargs)
-class Command(BaseCommand):
- """Commands are the basic building block of command line interfaces in
- Click. A basic command handles command line parsing and might dispatch
- more parsing to commands nested below it.
-
- :param name: the name of the command to use unless a group overrides it.
- :param context_settings: an optional dictionary with defaults that are
- passed to the context object.
- :param callback: the callback to invoke. This is optional.
- :param params: the parameters to register with this command. This can
- be either :class:`Option` or :class:`Argument` objects.
- :param help: the help string to use for this command.
- :param epilog: like the help string but it's printed at the end of the
- help page after everything else.
- :param short_help: the short help to use for this command. This is
- shown on the command listing of the parent command.
- :param add_help_option: by default each command registers a ``--help``
- option. This can be disabled by this parameter.
- :param no_args_is_help: this controls what happens if no arguments are
- provided. This option is disabled by default.
- If enabled this will add ``--help`` as argument
- if no arguments are passed
- :param hidden: hide this command from help outputs.
-
- :param deprecated: issues a message indicating that
- the command is deprecated.
-
- .. versionchanged:: 8.1
- ``help``, ``epilog``, and ``short_help`` are stored unprocessed,
- all formatting is done when outputting help text, not at init,
- and is done even if not using the ``@command`` decorator.
-
- .. versionchanged:: 8.0
- Added a ``repr`` showing the command name.
-
- .. versionchanged:: 7.1
- Added the ``no_args_is_help`` parameter.
-
- .. versionchanged:: 2.0
- Added the ``context_settings`` parameter.
- """
-
- def __init__(
- self,
- name: t.Optional[str],
- context_settings: t.Optional[t.Dict[str, t.Any]] = None,
- callback: t.Optional[t.Callable[..., t.Any]] = None,
- params: t.Optional[t.List["Parameter"]] = None,
- help: t.Optional[str] = None,
- epilog: t.Optional[str] = None,
- short_help: t.Optional[str] = None,
- options_metavar: t.Optional[str] = "[OPTIONS]",
- add_help_option: bool = True,
- no_args_is_help: bool = False,
- hidden: bool = False,
- deprecated: bool = False,
- ) -> None:
- super().__init__(name, context_settings)
- #: the callback to execute when the command fires. This might be
- #: `None` in which case nothing happens.
- self.callback = callback
- #: the list of parameters for this command in the order they
- #: should show up in the help page and execute. Eager parameters
- #: will automatically be handled before non eager ones.
- self.params: t.List["Parameter"] = params or []
- self.help = help
- self.epilog = epilog
- self.options_metavar = options_metavar
- self.short_help = short_help
- self.add_help_option = add_help_option
- self.no_args_is_help = no_args_is_help
- self.hidden = hidden
- self.deprecated = deprecated
-
- def to_info_dict(self, ctx: Context) -> t.Dict[str, t.Any]:
- info_dict = super().to_info_dict(ctx)
- info_dict.update(
- params=[param.to_info_dict() for param in self.get_params(ctx)],
- help=self.help,
- epilog=self.epilog,
- short_help=self.short_help,
- hidden=self.hidden,
- deprecated=self.deprecated,
- )
- return info_dict
-
- def get_usage(self, ctx: Context) -> str:
- """Formats the usage line into a string and returns it.
-
- Calls :meth:`format_usage` internally.
- """
- formatter = ctx.make_formatter()
- self.format_usage(ctx, formatter)
- return formatter.getvalue().rstrip("\n")
-
- def get_params(self, ctx: Context) -> t.List["Parameter"]:
- rv = self.params
- help_option = self.get_help_option(ctx)
-
- if help_option is not None:
- rv = [*rv, help_option]
-
- return rv
-
- def format_usage(self, ctx: Context, formatter: HelpFormatter) -> None:
- """Writes the usage line into the formatter.
-
- This is a low-level method called by :meth:`get_usage`.
- """
- pieces = self.collect_usage_pieces(ctx)
- formatter.write_usage(ctx.command_path, " ".join(pieces))
-
- def collect_usage_pieces(self, ctx: Context) -> t.List[str]:
- """Returns all the pieces that go into the usage line and returns
- it as a list of strings.
- """
- rv = [self.options_metavar] if self.options_metavar else []
-
- for param in self.get_params(ctx):
- rv.extend(param.get_usage_pieces(ctx))
-
- return rv
-
- def get_help_option_names(self, ctx: Context) -> t.List[str]:
- """Returns the names for the help option."""
- all_names = set(ctx.help_option_names)
- for param in self.params:
- all_names.difference_update(param.opts)
- all_names.difference_update(param.secondary_opts)
- return list(all_names)
-
- def get_help_option(self, ctx: Context) -> t.Optional["Option"]:
- """Returns the help option object."""
- help_options = self.get_help_option_names(ctx)
-
- if not help_options or not self.add_help_option:
- return None
-
- def show_help(ctx: Context, param: "Parameter", value: str) -> None:
- if value and not ctx.resilient_parsing:
- echo(ctx.get_help(), color=ctx.color)
- ctx.exit()
-
- return Option(
- help_options,
- is_flag=True,
- is_eager=True,
- expose_value=False,
- callback=show_help,
- help=_("Show this message and exit."),
- )
-
- def make_parser(self, ctx: Context) -> OptionParser:
- """Creates the underlying option parser for this command."""
- parser = OptionParser(ctx)
- for param in self.get_params(ctx):
- param.add_to_parser(parser, ctx)
- return parser
-
- def get_help(self, ctx: Context) -> str:
- """Formats the help into a string and returns it.
-
- Calls :meth:`format_help` internally.
- """
- formatter = ctx.make_formatter()
- self.format_help(ctx, formatter)
- return formatter.getvalue().rstrip("\n")
-
- def get_short_help_str(self, limit: int = 45) -> str:
- """Gets short help for the command or makes it by shortening the
- long help string.
- """
- if self.short_help:
- text = inspect.cleandoc(self.short_help)
- elif self.help:
- text = make_default_short_help(self.help, limit)
- else:
- text = ""
-
- if self.deprecated:
- text = _("(Deprecated) {text}").format(text=text)
-
- return text.strip()
-
- def format_help(self, ctx: Context, formatter: HelpFormatter) -> None:
- """Writes the help into the formatter if it exists.
-
- This is a low-level method called by :meth:`get_help`.
-
- This calls the following methods:
-
- - :meth:`format_usage`
- - :meth:`format_help_text`
- - :meth:`format_options`
- - :meth:`format_epilog`
- """
- self.format_usage(ctx, formatter)
- self.format_help_text(ctx, formatter)
- self.format_options(ctx, formatter)
- self.format_epilog(ctx, formatter)
-
- def format_help_text(self, ctx: Context, formatter: HelpFormatter) -> None:
- """Writes the help text to the formatter if it exists."""
- text = self.help if self.help is not None else ""
-
- if self.deprecated:
- text = _("(Deprecated) {text}").format(text=text)
-
- if text:
- text = inspect.cleandoc(text).partition("\f")[0]
- formatter.write_paragraph()
-
- with formatter.indentation():
- formatter.write_text(text)
-
- def format_options(self, ctx: Context, formatter: HelpFormatter) -> None:
- """Writes all the options into the formatter if they exist."""
- opts = []
- for param in self.get_params(ctx):
- rv = param.get_help_record(ctx)
- if rv is not None:
- opts.append(rv)
-
- if opts:
- with formatter.section(_("Options")):
- formatter.write_dl(opts)
-
- def format_epilog(self, ctx: Context, formatter: HelpFormatter) -> None:
- """Writes the epilog into the formatter if it exists."""
- if self.epilog:
- epilog = inspect.cleandoc(self.epilog)
- formatter.write_paragraph()
-
- with formatter.indentation():
- formatter.write_text(epilog)
-
- def parse_args(self, ctx: Context, args: t.List[str]) -> t.List[str]:
- if not args and self.no_args_is_help and not ctx.resilient_parsing:
- echo(ctx.get_help(), color=ctx.color)
- ctx.exit()
-
- parser = self.make_parser(ctx)
- opts, args, param_order = parser.parse_args(args=args)
-
- for param in iter_params_for_processing(param_order, self.get_params(ctx)):
- value, args = param.handle_parse_result(ctx, opts, args)
-
- if args and not ctx.allow_extra_args and not ctx.resilient_parsing:
- ctx.fail(
- ngettext(
- "Got unexpected extra argument ({args})",
- "Got unexpected extra arguments ({args})",
- len(args),
- ).format(args=" ".join(map(str, args)))
- )
-
- ctx.args = args
- ctx._opt_prefixes.update(parser._opt_prefixes)
- return args
-
- def invoke(self, ctx: Context) -> t.Any:
- """Given a context, this invokes the attached callback (if it exists)
- in the right way.
- """
- if self.deprecated:
- message = _(
- "DeprecationWarning: The command {name!r} is deprecated."
- ).format(name=self.name)
- echo(style(message, fg="red"), err=True)
-
- if self.callback is not None:
- return ctx.invoke(self.callback, **ctx.params)
-
- def shell_complete(self, ctx: Context, incomplete: str) -> t.List["CompletionItem"]:
- """Return a list of completions for the incomplete value. Looks
- at the names of options and chained multi-commands.
-
- :param ctx: Invocation context for this command.
- :param incomplete: Value being completed. May be empty.
-
- .. versionadded:: 8.0
- """
- from click.shell_completion import CompletionItem
-
- results: t.List["CompletionItem"] = []
-
- if incomplete and not incomplete[0].isalnum():
- for param in self.get_params(ctx):
- if (
- not isinstance(param, Option)
- or param.hidden
- or (
- not param.multiple
- and ctx.get_parameter_source(param.name) # type: ignore
- is ParameterSource.COMMANDLINE
- )
- ):
- continue
-
- results.extend(
- CompletionItem(name, help=param.help)
- for name in [*param.opts, *param.secondary_opts]
- if name.startswith(incomplete)
- )
-
- results.extend(super().shell_complete(ctx, incomplete))
- return results
-
-
class MultiCommand(Command):
"""A multi command is the basic implementation of a command that
dispatches to subcommands. The most common version is the
@@ -1605,28 +1816,14 @@ class MultiCommand(Command):
with formatter.section(_("Commands")):
formatter.write_dl(rows)
- def parse_args(self, ctx: Context, args: t.List[str]) -> t.List[str]:
- if not args and self.no_args_is_help and not ctx.resilient_parsing:
- echo(ctx.get_help(), color=ctx.color)
- ctx.exit()
-
- rest = super().parse_args(ctx, args)
-
- if self.chain:
- ctx.protected_args = rest
- ctx.args = []
- elif rest:
- ctx.protected_args, ctx.args = rest[:1], rest[1:]
-
- return ctx.args
-
def invoke(self, ctx: Context) -> t.Any:
def _process_result(value: t.Any) -> t.Any:
if self._result_callback is not None:
value = ctx.invoke(self._result_callback, value, **ctx.params)
+
return value
- if not ctx.protected_args:
+ if not ctx.args:
if self.invoke_without_command:
# No subcommand was invoked, so the result callback is
# invoked with the group return value for regular
@@ -1636,84 +1833,61 @@ class MultiCommand(Command):
return _process_result([] if self.chain else rv)
ctx.fail(_("Missing command."))
- # Fetch args back out
- args = [*ctx.protected_args, *ctx.args]
- ctx.args = []
- ctx.protected_args = []
-
- # If we're not in chain mode, we only allow the invocation of a
- # single command but we also inform the current context about the
- # name of the command to invoke.
if not self.chain:
- # Make sure the context is entered so we do not clean up
- # resources until the result processor has worked.
with ctx:
- cmd_name, cmd, args = self.resolve_command(ctx, args)
+ name, cmd, args = self.resolve_command(ctx, ctx.args)
assert cmd is not None
- ctx.invoked_subcommand = cmd_name
+ ctx.invoked_subcommand = name
super().invoke(ctx)
- sub_ctx = cmd.make_context(cmd_name, args, parent=ctx)
- with sub_ctx:
+
+ with cmd.make_context(name, args, parent=ctx) as sub_ctx:
return _process_result(sub_ctx.command.invoke(sub_ctx))
- # In chain mode we create the contexts step by step, but after the
- # base command has been invoked. Because at that point we do not
- # know the subcommands yet, the invoked subcommand attribute is
- # set to ``*`` to inform the command that subcommands are executed
- # but nothing else.
with ctx:
- ctx.invoked_subcommand = "*" if args else None
+ ctx.invoked_subcommand = "*"
super().invoke(ctx)
-
- # Otherwise we make every single context and invoke them in a
- # chain. In that case the return value to the result processor
- # is the list of all invoked subcommand's results.
+ args = ctx.args
contexts = []
+ rv = []
+
while args:
- cmd_name, cmd, args = self.resolve_command(ctx, args)
+ name, cmd, args = self.resolve_command(ctx, args)
assert cmd is not None
sub_ctx = cmd.make_context(
- cmd_name,
+ name,
args,
parent=ctx,
allow_extra_args=True,
allow_interspersed_args=False,
)
contexts.append(sub_ctx)
- args, sub_ctx.args = sub_ctx.args, []
+ args[:] = sub_ctx.args
+ sub_ctx.args.clear()
- rv = []
for sub_ctx in contexts:
with sub_ctx:
rv.append(sub_ctx.command.invoke(sub_ctx))
+
return _process_result(rv)
def resolve_command(
self, ctx: Context, args: t.List[str]
) -> t.Tuple[t.Optional[str], t.Optional[Command], t.List[str]]:
- cmd_name = make_str(args[0])
- original_cmd_name = cmd_name
-
- # Get the command
- cmd = self.get_command(ctx, cmd_name)
+ name = original_name = args[0]
+ cmd = self.get_command(ctx, name)
- # If we can't find the command but there is a normalization
- # function available, we try with that one.
+ # If there's no exact match, try matching the normalized name.
if cmd is None and ctx.token_normalize_func is not None:
- cmd_name = ctx.token_normalize_func(cmd_name)
- cmd = self.get_command(ctx, cmd_name)
-
- # If we don't find the command we want to show an error message
- # to the user that it was not provided. However, there is
- # something else we should do: if the first argument looks like
- # an option we want to kick off parsing again for arguments to
- # resolve things like --help which now should go to the main
- # place.
- if cmd is None and not ctx.resilient_parsing:
- if split_opt(cmd_name)[0]:
- self.parse_args(ctx, ctx.args)
- ctx.fail(_("No such command {name!r}.").format(name=original_cmd_name))
- return cmd_name if cmd else None, cmd, args[1:]
+ name = ctx.token_normalize_func(name)
+ cmd = self.get_command(ctx, name)
+
+ if cmd is None:
+ if ctx.resilient_parsing:
+ return None, None, args[1:]
+
+ ctx.fail(_("No such command {name!r}.").format(name=original_name))
+
+ return name, cmd, args[1:]
def get_command(self, ctx: Context, cmd_name: str) -> t.Optional[Command]:
"""Given a context and a command name, this returns a
@@ -2230,15 +2404,16 @@ class Parameter:
return value
- def add_to_parser(self, parser: OptionParser, ctx: Context) -> None:
- raise NotImplementedError()
-
def consume_value(
- self, ctx: Context, opts: t.Mapping[str, t.Any]
+ self, ctx: Context, opts: t.Mapping[str, t.List[t.Any]]
) -> t.Tuple[t.Any, ParameterSource]:
- value = opts.get(self.name) # type: ignore
+ value = opts.get(self.name)
source = ParameterSource.COMMANDLINE
+ if value is not None and not self.multiple:
+ # Use only the last occurrence of the option if multiple isn't enabled.
+ value = value[-1]
+
if value is None:
value = self.value_from_envvar(ctx)
source = ParameterSource.ENVIRONMENT
@@ -2286,6 +2461,8 @@ class Parameter:
value = tuple(check_iter(value))
if len(value) != self.nargs:
+ # This should only happen when passing in args manually, the parser
+ # should ensure nargs when parsing the command line.
raise BadParameter(
ngettext(
"Takes {nargs} values but 1 was given.",
@@ -2648,45 +2825,6 @@ class Option(Parameter):
return name, opts, secondary_opts
- def add_to_parser(self, parser: OptionParser, ctx: Context) -> None:
- if self.multiple:
- action = "append"
- elif self.count:
- action = "count"
- else:
- action = "store"
-
- if self.is_flag:
- action = f"{action}_const"
-
- if self.is_bool_flag and self.secondary_opts:
- parser.add_option(
- obj=self, opts=self.opts, dest=self.name, action=action, const=True
- )
- parser.add_option(
- obj=self,
- opts=self.secondary_opts,
- dest=self.name,
- action=action,
- const=False,
- )
- else:
- parser.add_option(
- obj=self,
- opts=self.opts,
- dest=self.name,
- action=action,
- const=self.flag_value,
- )
- else:
- parser.add_option(
- obj=self,
- opts=self.opts,
- dest=self.name,
- action=action,
- nargs=self.nargs,
- )
-
def get_help_record(self, ctx: Context) -> t.Optional[t.Tuple[str, str]]:
if self.hidden:
return None
@@ -2885,7 +3023,7 @@ class Option(Parameter):
return rv
def consume_value(
- self, ctx: Context, opts: t.Mapping[str, "Parameter"]
+ self, ctx: Context, opts: t.Mapping[str, t.List[t.Any]]
) -> t.Tuple[t.Any, ParameterSource]:
value, source = super().consume_value(ctx, opts)
@@ -2993,6 +3131,3 @@ class Argument(Parameter):
def get_error_hint(self, ctx: Context) -> str:
return f"'{self.make_metavar()}'"
-
- def add_to_parser(self, parser: OptionParser, ctx: Context) -> None:
- parser.add_argument(dest=self.name, nargs=self.nargs, obj=self)
diff --git a/src/click/exceptions.py b/src/click/exceptions.py
index 9e20b3e..dfa4fa3 100644
--- a/src/click/exceptions.py
+++ b/src/click/exceptions.py
@@ -253,6 +253,14 @@ class BadArgumentUsage(UsageError):
"""
+class NoArgsIsHelpError(UsageError):
+ def __init__(self, ctx):
+ super().__init__(ctx.get_help(), ctx=ctx)
+
+ def show(self, file=None):
+ echo(self.format_message(), file=file, err=True, color=self.ctx.color)
+
+
class FileError(ClickException):
"""Raised if a file cannot be opened."""
diff --git a/src/click/parser.py b/src/click/parser.py
index 2d5a2ed..11a285b 100644
--- a/src/click/parser.py
+++ b/src/click/parser.py
@@ -1,109 +1,4 @@
-"""
-This module started out as largely a copy paste from the stdlib's
-optparse module with the features removed that we do not need from
-optparse because we implement them in Click on a higher level (for
-instance type handling, help formatting and a lot more).
-
-The plan is to remove more and more from here over time.
-
-The reason this is a different module and not optparse from the stdlib
-is that there are differences in 2.x and 3.x about the error messages
-generated and optparse in the stdlib uses gettext for no good reason
-and might cause us issues.
-
-Click uses parts of optparse written by Gregory P. Ward and maintained
-by the Python Software Foundation. This is limited to code in parser.py.
-
-Copyright 2001-2006 Gregory P. Ward. All rights reserved.
-Copyright 2002-2006 Python Software Foundation. All rights reserved.
-"""
-# This code uses parts of optparse written by Gregory P. Ward and
-# maintained by the Python Software Foundation.
-# Copyright 2001-2006 Gregory P. Ward
-# Copyright 2002-2006 Python Software Foundation
import typing as t
-from collections import deque
-from gettext import gettext as _
-from gettext import ngettext
-
-from .exceptions import BadArgumentUsage
-from .exceptions import BadOptionUsage
-from .exceptions import NoSuchOption
-from .exceptions import UsageError
-
-if t.TYPE_CHECKING:
- import typing_extensions as te
- from .core import Argument as CoreArgument
- from .core import Context
- from .core import Option as CoreOption
- from .core import Parameter as CoreParameter
-
-V = t.TypeVar("V")
-
-# Sentinel value that indicates an option was passed as a flag without a
-# value but is not a flag option. Option.consume_value uses this to
-# prompt or use the flag_value.
-_flag_needs_value = object()
-
-
-def _unpack_args(
- args: t.Sequence[str], nargs_spec: t.Sequence[int]
-) -> t.Tuple[t.Sequence[t.Union[str, t.Sequence[t.Optional[str]], None]], t.List[str]]:
- """Given an iterable of arguments and an iterable of nargs specifications,
- it returns a tuple with all the unpacked arguments at the first index
- and all remaining arguments as the second.
-
- The nargs specification is the number of arguments that should be consumed
- or `-1` to indicate that this position should eat up all the remainders.
-
- Missing items are filled with `None`.
- """
- args = deque(args)
- nargs_spec = deque(nargs_spec)
- rv: t.List[t.Union[str, t.Tuple[t.Optional[str], ...], None]] = []
- spos: t.Optional[int] = None
-
- def _fetch(c: "te.Deque[V]") -> t.Optional[V]:
- try:
- if spos is None:
- return c.popleft()
- else:
- return c.pop()
- except IndexError:
- return None
-
- while nargs_spec:
- nargs = _fetch(nargs_spec)
-
- if nargs is None:
- continue
-
- if nargs == 1:
- rv.append(_fetch(args))
- elif nargs > 1:
- x = [_fetch(args) for _ in range(nargs)]
-
- # If we're reversed, we're pulling in the arguments in reverse,
- # so we need to turn them around.
- if spos is not None:
- x.reverse()
-
- rv.append(tuple(x))
- elif nargs < 0:
- if spos is not None:
- raise TypeError("Cannot have two nargs < 0")
-
- spos = len(rv)
- rv.append(None)
-
- # spos is the position of the wildcard (star). If it's not `None`,
- # we fill it with the remainder.
- if spos is not None:
- rv[spos] = tuple(args)
- args = []
- rv[spos + 1 :] = reversed(rv[spos + 1 :])
-
- return tuple(rv), list(args)
def split_opt(opt: str) -> t.Tuple[str, str]:
@@ -115,13 +10,6 @@ def split_opt(opt: str) -> t.Tuple[str, str]:
return first, opt[1:]
-def normalize_opt(opt: str, ctx: t.Optional["Context"]) -> str:
- if ctx is None or ctx.token_normalize_func is None:
- return opt
- prefix, opt = split_opt(opt)
- return f"{prefix}{ctx.token_normalize_func(opt)}"
-
-
def split_arg_string(string: str) -> t.List[str]:
"""Split an argument string as with :func:`shlex.split`, but don't
fail if the string is incomplete. Ignores a missing closing quote or
@@ -154,376 +42,3 @@ def split_arg_string(string: str) -> t.List[str]:
out.append(lex.token)
return out
-
-
-class Option:
- def __init__(
- self,
- obj: "CoreOption",
- opts: t.Sequence[str],
- dest: t.Optional[str],
- action: t.Optional[str] = None,
- nargs: int = 1,
- const: t.Optional[t.Any] = None,
- ):
- self._short_opts = []
- self._long_opts = []
- self.prefixes = set()
-
- for opt in opts:
- prefix, value = split_opt(opt)
- if not prefix:
- raise ValueError(f"Invalid start character for option ({opt})")
- self.prefixes.add(prefix[0])
- if len(prefix) == 1 and len(value) == 1:
- self._short_opts.append(opt)
- else:
- self._long_opts.append(opt)
- self.prefixes.add(prefix)
-
- if action is None:
- action = "store"
-
- self.dest = dest
- self.action = action
- self.nargs = nargs
- self.const = const
- self.obj = obj
-
- @property
- def takes_value(self) -> bool:
- return self.action in ("store", "append")
-
- def process(self, value: str, state: "ParsingState") -> None:
- if self.action == "store":
- state.opts[self.dest] = value # type: ignore
- elif self.action == "store_const":
- state.opts[self.dest] = self.const # type: ignore
- elif self.action == "append":
- state.opts.setdefault(self.dest, []).append(value) # type: ignore
- elif self.action == "append_const":
- state.opts.setdefault(self.dest, []).append(self.const) # type: ignore
- elif self.action == "count":
- state.opts[self.dest] = state.opts.get(self.dest, 0) + 1 # type: ignore
- else:
- raise ValueError(f"unknown action '{self.action}'")
- state.order.append(self.obj)
-
-
-class Argument:
- def __init__(self, obj: "CoreArgument", dest: t.Optional[str], nargs: int = 1):
- self.dest = dest
- self.nargs = nargs
- self.obj = obj
-
- def process(
- self,
- value: t.Union[t.Optional[str], t.Sequence[t.Optional[str]]],
- state: "ParsingState",
- ) -> None:
- if self.nargs > 1:
- assert value is not None
- holes = sum(1 for x in value if x is None)
- if holes == len(value):
- value = None
- elif holes != 0:
- raise BadArgumentUsage(
- _("Argument {name!r} takes {nargs} values.").format(
- name=self.dest, nargs=self.nargs
- )
- )
-
- if self.nargs == -1 and self.obj.envvar is not None and value == ():
- # Replace empty tuple with None so that a value from the
- # environment may be tried.
- value = None
-
- state.opts[self.dest] = value # type: ignore
- state.order.append(self.obj)
-
-
-class ParsingState:
- def __init__(self, rargs: t.List[str]) -> None:
- self.opts: t.Dict[str, t.Any] = {}
- self.largs: t.List[str] = []
- self.rargs = rargs
- self.order: t.List["CoreParameter"] = []
-
-
-class OptionParser:
- """The option parser is an internal class that is ultimately used to
- parse options and arguments. It's modelled after optparse and brings
- a similar but vastly simplified API. It should generally not be used
- directly as the high level Click classes wrap it for you.
-
- It's not nearly as extensible as optparse or argparse as it does not
- implement features that are implemented on a higher level (such as
- types or defaults).
-
- :param ctx: optionally the :class:`~click.Context` where this parser
- should go with.
- """
-
- def __init__(self, ctx: t.Optional["Context"] = None) -> None:
- #: The :class:`~click.Context` for this parser. This might be
- #: `None` for some advanced use cases.
- self.ctx = ctx
- #: This controls how the parser deals with interspersed arguments.
- #: If this is set to `False`, the parser will stop on the first
- #: non-option. Click uses this to implement nested subcommands
- #: safely.
- self.allow_interspersed_args = True
- #: This tells the parser how to deal with unknown options. By
- #: default it will error out (which is sensible), but there is a
- #: second mode where it will ignore it and continue processing
- #: after shifting all the unknown options into the resulting args.
- self.ignore_unknown_options = False
-
- if ctx is not None:
- self.allow_interspersed_args = ctx.allow_interspersed_args
- self.ignore_unknown_options = ctx.ignore_unknown_options
-
- self._short_opt: t.Dict[str, Option] = {}
- self._long_opt: t.Dict[str, Option] = {}
- self._opt_prefixes = {"-", "--"}
- self._args: t.List[Argument] = []
-
- def add_option(
- self,
- obj: "CoreOption",
- opts: t.Sequence[str],
- dest: t.Optional[str],
- action: t.Optional[str] = None,
- nargs: int = 1,
- const: t.Optional[t.Any] = None,
- ) -> None:
- """Adds a new option named `dest` to the parser. The destination
- is not inferred (unlike with optparse) and needs to be explicitly
- provided. Action can be any of ``store``, ``store_const``,
- ``append``, ``append_const`` or ``count``.
-
- The `obj` can be used to identify the option in the order list
- that is returned from the parser.
- """
- opts = [normalize_opt(opt, self.ctx) for opt in opts]
- option = Option(obj, opts, dest, action=action, nargs=nargs, const=const)
- self._opt_prefixes.update(option.prefixes)
- for opt in option._short_opts:
- self._short_opt[opt] = option
- for opt in option._long_opts:
- self._long_opt[opt] = option
-
- def add_argument(
- self, obj: "CoreArgument", dest: t.Optional[str], nargs: int = 1
- ) -> None:
- """Adds a positional argument named `dest` to the parser.
-
- The `obj` can be used to identify the option in the order list
- that is returned from the parser.
- """
- self._args.append(Argument(obj, dest=dest, nargs=nargs))
-
- def parse_args(
- self, args: t.List[str]
- ) -> t.Tuple[t.Dict[str, t.Any], t.List[str], t.List["CoreParameter"]]:
- """Parses positional arguments and returns ``(values, args, order)``
- for the parsed options and arguments as well as the leftover
- arguments if there are any. The order is a list of objects as they
- appear on the command line. If arguments appear multiple times they
- will be memorized multiple times as well.
- """
- state = ParsingState(args)
- try:
- self._process_args_for_options(state)
- self._process_args_for_args(state)
- except UsageError:
- if self.ctx is None or not self.ctx.resilient_parsing:
- raise
- return state.opts, state.largs, state.order
-
- def _process_args_for_args(self, state: ParsingState) -> None:
- pargs, args = _unpack_args(
- state.largs + state.rargs, [x.nargs for x in self._args]
- )
-
- for idx, arg in enumerate(self._args):
- arg.process(pargs[idx], state)
-
- state.largs = args
- state.rargs = []
-
- def _process_args_for_options(self, state: ParsingState) -> None:
- while state.rargs:
- arg = state.rargs.pop(0)
- arglen = len(arg)
- # Double dashes always handled explicitly regardless of what
- # prefixes are valid.
- if arg == "--":
- return
- elif arg[:1] in self._opt_prefixes and arglen > 1:
- self._process_opts(arg, state)
- elif self.allow_interspersed_args:
- state.largs.append(arg)
- else:
- state.rargs.insert(0, arg)
- return
-
- # Say this is the original argument list:
- # [arg0, arg1, ..., arg(i-1), arg(i), arg(i+1), ..., arg(N-1)]
- # ^
- # (we are about to process arg(i)).
- #
- # Then rargs is [arg(i), ..., arg(N-1)] and largs is a *subset* of
- # [arg0, ..., arg(i-1)] (any options and their arguments will have
- # been removed from largs).
- #
- # The while loop will usually consume 1 or more arguments per pass.
- # If it consumes 1 (eg. arg is an option that takes no arguments),
- # then after _process_arg() is done the situation is:
- #
- # largs = subset of [arg0, ..., arg(i)]
- # rargs = [arg(i+1), ..., arg(N-1)]
- #
- # If allow_interspersed_args is false, largs will always be
- # *empty* -- still a subset of [arg0, ..., arg(i-1)], but
- # not a very interesting subset!
-
- def _match_long_opt(
- self, opt: str, explicit_value: t.Optional[str], state: ParsingState
- ) -> None:
- if opt not in self._long_opt:
- from difflib import get_close_matches
-
- possibilities = get_close_matches(opt, self._long_opt)
- raise NoSuchOption(opt, possibilities=possibilities, ctx=self.ctx)
-
- option = self._long_opt[opt]
- if option.takes_value:
- # At this point it's safe to modify rargs by injecting the
- # explicit value, because no exception is raised in this
- # branch. This means that the inserted value will be fully
- # consumed.
- if explicit_value is not None:
- state.rargs.insert(0, explicit_value)
-
- value = self._get_value_from_state(opt, option, state)
-
- elif explicit_value is not None:
- raise BadOptionUsage(
- opt, _("Option {name!r} does not take a value.").format(name=opt)
- )
-
- else:
- value = None
-
- option.process(value, state)
-
- def _match_short_opt(self, arg: str, state: ParsingState) -> None:
- stop = False
- i = 1
- prefix = arg[0]
- unknown_options = []
-
- for ch in arg[1:]:
- opt = normalize_opt(f"{prefix}{ch}", self.ctx)
- option = self._short_opt.get(opt)
- i += 1
-
- if not option:
- if self.ignore_unknown_options:
- unknown_options.append(ch)
- continue
- raise NoSuchOption(opt, ctx=self.ctx)
- if option.takes_value:
- # Any characters left in arg? Pretend they're the
- # next arg, and stop consuming characters of arg.
- if i < len(arg):
- state.rargs.insert(0, arg[i:])
- stop = True
-
- value = self._get_value_from_state(opt, option, state)
-
- else:
- value = None
-
- option.process(value, state)
-
- if stop:
- break
-
- # If we got any unknown options we re-combinate the string of the
- # remaining options and re-attach the prefix, then report that
- # to the state as new larg. This way there is basic combinatorics
- # that can be achieved while still ignoring unknown arguments.
- if self.ignore_unknown_options and unknown_options:
- state.largs.append(f"{prefix}{''.join(unknown_options)}")
-
- def _get_value_from_state(
- self, option_name: str, option: Option, state: ParsingState
- ) -> t.Any:
- nargs = option.nargs
-
- if len(state.rargs) < nargs:
- if option.obj._flag_needs_value:
- # Option allows omitting the value.
- value = _flag_needs_value
- else:
- raise BadOptionUsage(
- option_name,
- ngettext(
- "Option {name!r} requires an argument.",
- "Option {name!r} requires {nargs} arguments.",
- nargs,
- ).format(name=option_name, nargs=nargs),
- )
- elif nargs == 1:
- next_rarg = state.rargs[0]
-
- if (
- option.obj._flag_needs_value
- and isinstance(next_rarg, str)
- and next_rarg[:1] in self._opt_prefixes
- and len(next_rarg) > 1
- ):
- # The next arg looks like the start of an option, don't
- # use it as the value if omitting the value is allowed.
- value = _flag_needs_value
- else:
- value = state.rargs.pop(0)
- else:
- value = tuple(state.rargs[:nargs])
- del state.rargs[:nargs]
-
- return value
-
- def _process_opts(self, arg: str, state: ParsingState) -> None:
- explicit_value = None
- # Long option handling happens in two parts. The first part is
- # supporting explicitly attached values. In any case, we will try
- # to long match the option first.
- if "=" in arg:
- long_opt, explicit_value = arg.split("=", 1)
- else:
- long_opt = arg
- norm_long_opt = normalize_opt(long_opt, self.ctx)
-
- # At this point we will match the (assumed) long option through
- # the long option matching code. Note that this allows options
- # like "-foo" to be matched as long options.
- try:
- self._match_long_opt(norm_long_opt, explicit_value, state)
- except NoSuchOption:
- # At this point the long option matching failed, and we need
- # to try with short options. However there is a special rule
- # which says, that if we have a two character options prefix
- # (applies to "--foo" for instance), we do not dispatch to the
- # short option code and will instead raise the no option
- # error.
- if arg[:2] not in self._opt_prefixes:
- self._match_short_opt(arg, state)
- return
-
- if not self.ignore_unknown_options:
- raise
-
- state.largs.append(arg)
diff --git a/src/click/shell_completion.py b/src/click/shell_completion.py
index c17a8e6..5149b38 100644
--- a/src/click/shell_completion.py
+++ b/src/click/shell_completion.py
@@ -4,7 +4,7 @@ import typing as t
from gettext import gettext as _
from .core import Argument
-from .core import BaseCommand
+from .core import Command
from .core import Context
from .core import MultiCommand
from .core import Option
@@ -15,7 +15,7 @@ from .utils import echo
def shell_complete(
- cli: BaseCommand,
+ cli: Command,
ctx_args: t.Dict[str, t.Any],
prog_name: str,
complete_var: str,
@@ -213,7 +213,7 @@ class ShellComplete:
def __init__(
self,
- cli: BaseCommand,
+ cli: Command,
ctx_args: t.Dict[str, t.Any],
prog_name: str,
complete_var: str,
@@ -482,7 +482,7 @@ def _is_incomplete_option(ctx: Context, args: t.List[str], param: Parameter) ->
def _resolve_context(
- cli: BaseCommand, ctx_args: t.Dict[str, t.Any], prog_name: str, args: t.List[str]
+ cli: Command, ctx_args: t.Dict[str, t.Any], prog_name: str, args: t.List[str]
) -> Context:
"""Produce the context hierarchy starting with the command and
traversing the complete arguments. This only follows the commands,
@@ -494,7 +494,7 @@ def _resolve_context(
"""
ctx_args["resilient_parsing"] = True
ctx = cli.make_context(prog_name, args.copy(), **ctx_args)
- args = ctx.protected_args + ctx.args
+ args = ctx.args
while args:
command = ctx.command
@@ -507,7 +507,7 @@ def _resolve_context(
return ctx
ctx = cmd.make_context(name, args, parent=ctx, resilient_parsing=True)
- args = ctx.protected_args + ctx.args
+ args = ctx.args
else:
while args:
name, cmd, args = command.resolve_command(ctx, args)
@@ -526,7 +526,7 @@ def _resolve_context(
args = sub_ctx.args
ctx = sub_ctx
- args = [*sub_ctx.protected_args, *sub_ctx.args]
+ args = sub_ctx.args
else:
break
@@ -535,7 +535,7 @@ def _resolve_context(
def _resolve_incomplete(
ctx: Context, args: t.List[str], incomplete: str
-) -> t.Tuple[t.Union[BaseCommand, Parameter], str]:
+) -> t.Tuple[t.Union[Command, Parameter], str]:
"""Find the Click object that will handle the completion of the
incomplete value. Return the object and the incomplete value.
diff --git a/src/click/testing.py b/src/click/testing.py
index e395c2e..a68f604 100644
--- a/src/click/testing.py
+++ b/src/click/testing.py
@@ -14,7 +14,7 @@ from . import utils
from ._compat import _find_binary_reader
if t.TYPE_CHECKING:
- from .core import BaseCommand
+ from .core import Command
class EchoingStdin:
@@ -187,7 +187,7 @@ class CliRunner:
self.echo_stdin = echo_stdin
self.mix_stderr = mix_stderr
- def get_default_prog_name(self, cli: "BaseCommand") -> str:
+ def get_default_prog_name(self, cli: "Command") -> str:
"""Given a command object it will return the default program name
for it. The default is the `name` attribute or ``"root"`` if not
set.
@@ -348,7 +348,7 @@ class CliRunner:
def invoke(
self,
- cli: "BaseCommand",
+ cli: "Command",
args: t.Optional[t.Union[str, t.Sequence[str]]] = None,
input: t.Optional[t.Union[str, bytes, t.IO]] = None,
env: t.Optional[t.Mapping[str, t.Optional[str]]] = None,
diff --git a/tests/test_arguments.py b/tests/test_arguments.py
index 735df4b..899cc7c 100644
--- a/tests/test_arguments.py
+++ b/tests/test_arguments.py
@@ -315,7 +315,7 @@ def test_defaults_for_nargs(runner):
result = runner.invoke(cmd, ["3"])
assert result.exception is not None
- assert "Argument 'a' takes 2 values." in result.output
+ assert "Argument '[A]...' requires 2 values but 1 was given." in result.output
def test_multiple_param_decls_not_allowed(runner):
diff --git a/tests/test_basic.py b/tests/test_basic.py
index d68b962..1a1adc3 100644
--- a/tests/test_basic.py
+++ b/tests/test_basic.py
@@ -108,7 +108,7 @@ def test_group_from_list(runner):
[
([], "S:[no value]"),
(["--s=42"], "S:[42]"),
- (["--s"], "Error: Option '--s' requires an argument."),
+ (["--s"], "Error: Option '--s' requires a value."),
(["--s="], "S:[]"),
(["--s=\N{SNOWMAN}"], "S:[\N{SNOWMAN}]"),
],
diff --git a/tests/test_commands.py b/tests/test_commands.py
index 3a0d4b9..179d6c1 100644
--- a/tests/test_commands.py
+++ b/tests/test_commands.py
@@ -166,7 +166,7 @@ def test_base_command(runner):
def cli():
pass
- class OptParseCommand(click.BaseCommand):
+ class OptParseCommand(click.Command):
def __init__(self, name, parser, callback):
super().__init__(name)
self.parser = parser