Skip to content

Commit 1a70b90

Browse files
committed
Merge branch 'master' into speedup_import
# Conflicts: # cmd2/cmd2.py # tests/test_completion.py # tests/test_submenu.py
2 parents b1516f4 + 5d64ebe commit 1a70b90

40 files changed

+614
-625
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
* Changes
2020
* ``strip_ansi()`` and ``strip_quotes()`` functions have moved to new utils module
2121
* Several constants moved to new constants module
22+
* Submenu support has been moved to a new [cmd2-submenu](https://github.com/python-cmd2/cmd2-submenu) plugin. If you use submenus, you will need to update your dependencies and modify your imports.
2223
* Deletions (potentially breaking changes)
2324
* Deleted all ``optparse`` code which had previously been deprecated in release 0.8.0
2425
* The ``options`` decorator no longer exists
@@ -28,6 +29,7 @@
2829
* Deleted ``cmd_with_subs_completer``, ``get_subcommands``, and ``get_subcommand_completer``
2930
* Replaced by default AutoCompleter implementation for all commands using argparse
3031
* Deleted support for old method of calling application commands with ``cmd()`` and ``self``
32+
* ``cmd2.redirector`` is no longer supported. Output redirection can only be done with '>' or '>>'
3133
* Python 2 no longer supported
3234
* ``cmd2`` now supports Python 3.4+
3335
* Known Issues

cmd2/argcomplete_bridge.py

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,17 @@
66
import argcomplete
77
except ImportError: # pragma: no cover
88
# not installed, skip the rest of the file
9-
pass
10-
9+
DEFAULT_COMPLETER = None
1110
else:
1211
# argcomplete is installed
1312

13+
# Newer versions of argcomplete have FilesCompleter at top level, older versions only have it under completers
14+
try:
15+
DEFAULT_COMPLETER = argcomplete.FilesCompleter()
16+
except AttributeError:
17+
DEFAULT_COMPLETER = argcomplete.completers.FilesCompleter()
18+
19+
from cmd2.argparse_completer import ACTION_ARG_CHOICES, ACTION_SUPPRESS_HINT
1420
from contextlib import redirect_stdout
1521
import copy
1622
from io import StringIO
@@ -102,7 +108,7 @@ class CompletionFinder(argcomplete.CompletionFinder):
102108

103109
def __call__(self, argument_parser, completer=None, always_complete_options=True, exit_method=os._exit, output_stream=None,
104110
exclude=None, validator=None, print_suppressed=False, append_space=None,
105-
default_completer=argcomplete.FilesCompleter()):
111+
default_completer=DEFAULT_COMPLETER):
106112
"""
107113
:param argument_parser: The argument parser to autocomplete on
108114
:type argument_parser: :class:`argparse.ArgumentParser`
@@ -140,9 +146,14 @@ def __call__(self, argument_parser, completer=None, always_complete_options=True
140146
added to argcomplete.safe_actions, if their values are wanted in the ``parsed_args`` completer argument, or
141147
their execution is otherwise desirable.
142148
"""
143-
self.__init__(argument_parser, always_complete_options=always_complete_options, exclude=exclude,
144-
validator=validator, print_suppressed=print_suppressed, append_space=append_space,
145-
default_completer=default_completer)
149+
# Older versions of argcomplete have fewer keyword arguments
150+
if sys.version_info >= (3, 5):
151+
self.__init__(argument_parser, always_complete_options=always_complete_options, exclude=exclude,
152+
validator=validator, print_suppressed=print_suppressed, append_space=append_space,
153+
default_completer=default_completer)
154+
else:
155+
self.__init__(argument_parser, always_complete_options=always_complete_options, exclude=exclude,
156+
validator=validator, print_suppressed=print_suppressed)
146157

147158
if "_ARGCOMPLETE" not in os.environ:
148159
# not an argument completion invocation
@@ -235,11 +246,22 @@ def __call__(self, argument_parser, completer=None, always_complete_options=True
235246
if comp_type == 63: # type is 63 for second tab press
236247
print(outstr.rstrip(), file=argcomplete.debug_stream, end='')
237248

238-
output_stream.write(ifs.join([ifs, ' ']).encode(argcomplete.sys_encoding))
249+
if completions is not None:
250+
output_stream.write(ifs.join([ifs, ' ']).encode(argcomplete.sys_encoding))
251+
else:
252+
output_stream.write(ifs.join([]).encode(argcomplete.sys_encoding))
239253
else:
240254
# if completions is None we assume we don't know how to handle it so let bash
241255
# go forward with normal filesystem completion
242256
output_stream.write(ifs.join([]).encode(argcomplete.sys_encoding))
243257
output_stream.flush()
244258
argcomplete.debug_stream.flush()
245259
exit_method(0)
260+
261+
262+
def bash_complete(action, show_hint: bool=True):
263+
"""Helper function to configure an argparse action to fall back to bash completion"""
264+
def complete_none(*args, **kwargs):
265+
return None
266+
setattr(action, ACTION_SUPPRESS_HINT, not show_hint)
267+
setattr(action, ACTION_ARG_CHOICES, (complete_none,))

cmd2/argparse_completer.py

Lines changed: 57 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ def my_completer(text: str, line: str, begidx: int, endidx:int, extra_param: str
7575
# attribute that can optionally added to an argparse argument (called an Action) to
7676
# define the completion choices for the argument. You may provide a Collection or a Function.
7777
ACTION_ARG_CHOICES = 'arg_choices'
78+
ACTION_SUPPRESS_HINT = 'suppress_hint'
7879

7980

8081
class _RangeAction(object):
@@ -261,6 +262,7 @@ def __init__(self,
261262
sub_completers[subcmd] = AutoCompleter(action.choices[subcmd], subcmd_start,
262263
arg_choices=subcmd_args,
263264
subcmd_args_lookup=subcmd_lookup,
265+
tab_for_arg_help=tab_for_arg_help,
264266
cmd2_app=cmd2_app)
265267
sub_commands.append(subcmd)
266268
self._positional_completers[action.dest] = sub_completers
@@ -472,23 +474,43 @@ def _complete_for_arg(self, action: argparse.Action,
472474
if action.dest in self._arg_choices:
473475
arg_choices = self._arg_choices[action.dest]
474476

475-
if isinstance(arg_choices, tuple) and len(arg_choices) > 0 and callable(arg_choices[0]):
476-
completer = arg_choices[0]
477+
# if arg_choices is a tuple
478+
# Let's see if it's a custom completion function. If it is, return what it provides
479+
# To do this, we make sure the first element is either a callable
480+
# or it's the name of a callable in the application
481+
if isinstance(arg_choices, tuple) and len(arg_choices) > 0 and \
482+
(callable(arg_choices[0]) or
483+
(isinstance(arg_choices[0], str) and hasattr(self._cmd2_app, arg_choices[0]) and
484+
callable(getattr(self._cmd2_app, arg_choices[0]))
485+
)
486+
):
487+
488+
if callable(arg_choices[0]):
489+
completer = arg_choices[0]
490+
elif isinstance(arg_choices[0], str) and callable(getattr(self._cmd2_app, arg_choices[0])):
491+
completer = getattr(self._cmd2_app, arg_choices[0])
492+
493+
# extract the positional and keyword arguments from the tuple
477494
list_args = None
478495
kw_args = None
479496
for index in range(1, len(arg_choices)):
480497
if isinstance(arg_choices[index], list) or isinstance(arg_choices[index], tuple):
481498
list_args = arg_choices[index]
482499
elif isinstance(arg_choices[index], dict):
483500
kw_args = arg_choices[index]
484-
if list_args is not None and kw_args is not None:
485-
return completer(text, line, begidx, endidx, *list_args, **kw_args)
486-
elif list_args is not None:
487-
return completer(text, line, begidx, endidx, *list_args)
488-
elif kw_args is not None:
489-
return completer(text, line, begidx, endidx, **kw_args)
490-
else:
491-
return completer(text, line, begidx, endidx)
501+
try:
502+
# call the provided function differently depending on the provided positional and keyword arguments
503+
if list_args is not None and kw_args is not None:
504+
return completer(text, line, begidx, endidx, *list_args, **kw_args)
505+
elif list_args is not None:
506+
return completer(text, line, begidx, endidx, *list_args)
507+
elif kw_args is not None:
508+
return completer(text, line, begidx, endidx, **kw_args)
509+
else:
510+
return completer(text, line, begidx, endidx)
511+
except TypeError:
512+
# assume this is due to an incorrect function signature, return nothing.
513+
return []
492514
else:
493515
return AutoCompleter.basic_complete(text, line, begidx, endidx,
494516
self._resolve_choices_for_arg(action, used_values))
@@ -499,6 +521,16 @@ def _resolve_choices_for_arg(self, action: argparse.Action, used_values=()) -> L
499521
if action.dest in self._arg_choices:
500522
args = self._arg_choices[action.dest]
501523

524+
# is the argument a string? If so, see if we can find an attribute in the
525+
# application matching the string.
526+
if isinstance(args, str):
527+
try:
528+
args = getattr(self._cmd2_app, args)
529+
except AttributeError:
530+
# Couldn't find anything matching the name
531+
return []
532+
533+
# is the provided argument a callable. If so, call it
502534
if callable(args):
503535
try:
504536
if self._cmd2_app is not None:
@@ -525,8 +557,19 @@ def _resolve_choices_for_arg(self, action: argparse.Action, used_values=()) -> L
525557
return []
526558

527559
def _print_action_help(self, action: argparse.Action) -> None:
560+
# is parameter hinting disabled globally?
528561
if not self._tab_for_arg_help:
529562
return
563+
564+
# is parameter hinting disabled for this parameter?
565+
try:
566+
suppress_hint = getattr(action, ACTION_SUPPRESS_HINT)
567+
except AttributeError:
568+
pass
569+
else:
570+
if suppress_hint:
571+
return
572+
530573
if action.option_strings:
531574
flags = ', '.join(action.option_strings)
532575
param = ''
@@ -535,7 +578,10 @@ def _print_action_help(self, action: argparse.Action) -> None:
535578

536579
prefix = '{}{}'.format(flags, param)
537580
else:
538-
prefix = '{}'.format(str(action.dest).upper())
581+
if action.dest != SUPPRESS:
582+
prefix = '{}'.format(str(action.dest).upper())
583+
else:
584+
prefix = ''
539585

540586
prefix = ' {0: <{width}} '.format(prefix, width=20)
541587
pref_len = len(prefix)

0 commit comments

Comments
 (0)