diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/click/__init__.py | 2 | ||||
-rw-r--r-- | src/click/core.py | 1075 | ||||
-rw-r--r-- | src/click/exceptions.py | 8 | ||||
-rw-r--r-- | src/click/parser.py | 485 | ||||
-rw-r--r-- | src/click/shell_completion.py | 16 | ||||
-rw-r--r-- | src/click/testing.py | 6 |
6 files changed, 624 insertions, 968 deletions
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, |