Skip to content

Commit ccfdf0f

Browse files
committed
Extract AddSubmenu() into it’s own module
1 parent 297f820 commit ccfdf0f

File tree

5 files changed

+271
-267
lines changed

5 files changed

+271
-267
lines changed

cmd2/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
#
22
# -*- coding: utf-8 -*-
3-
from .cmd2 import __version__, Cmd, AddSubmenu, CmdResult, categorize
3+
from .cmd2 import __version__, Cmd, CmdResult, categorize
44
from .cmd2 import with_argument_list, with_argparser, with_argparser_and_unknown_args, with_category

cmd2/cmd2.py

Lines changed: 0 additions & 261 deletions
Original file line numberDiff line numberDiff line change
@@ -312,267 +312,6 @@ class EmptyStatement(Exception):
312312
pass
313313

314314

315-
def _pop_readline_history(clear_history: bool=True) -> List[str]:
316-
"""Returns a copy of readline's history and optionally clears it (default)"""
317-
# noinspection PyArgumentList
318-
history = [
319-
readline.get_history_item(i)
320-
for i in range(1, 1 + readline.get_current_history_length())
321-
]
322-
if clear_history:
323-
readline.clear_history()
324-
return history
325-
326-
327-
def _push_readline_history(history, clear_history=True):
328-
"""Restores readline's history and optionally clears it first (default)"""
329-
if clear_history:
330-
readline.clear_history()
331-
for line in history:
332-
readline.add_history(line)
333-
334-
335-
def _complete_from_cmd(cmd_obj, text, line, begidx, endidx):
336-
"""Complete as though the user was typing inside cmd's cmdloop()"""
337-
from itertools import takewhile
338-
command_subcommand_params = line.split(None, 3)
339-
340-
if len(command_subcommand_params) < (3 if text else 2):
341-
n = len(command_subcommand_params[0])
342-
n += sum(1 for _ in takewhile(str.isspace, line[n:]))
343-
return cmd_obj.completenames(text, line[n:], begidx - n, endidx - n)
344-
345-
command, subcommand = command_subcommand_params[:2]
346-
n = len(command) + sum(1 for _ in takewhile(str.isspace, line))
347-
cfun = getattr(cmd_obj, 'complete_' + subcommand, cmd_obj.complete)
348-
return cfun(text, line[n:], begidx - n, endidx - n)
349-
350-
351-
class AddSubmenu(object):
352-
"""Conveniently add a submenu (Cmd-like class) to a Cmd
353-
354-
e.g. given "class SubMenu(Cmd): ..." then
355-
356-
@AddSubmenu(SubMenu(), 'sub')
357-
class MyCmd(cmd.Cmd):
358-
....
359-
360-
will have the following effects:
361-
1. 'sub' will interactively enter the cmdloop of a SubMenu instance
362-
2. 'sub cmd args' will call do_cmd(args) in a SubMenu instance
363-
3. 'sub ... [TAB]' will have the same behavior as [TAB] in a SubMenu cmdloop
364-
i.e., autocompletion works the way you think it should
365-
4. 'help sub [cmd]' will print SubMenu's help (calls its do_help())
366-
"""
367-
368-
class _Nonexistent(object):
369-
"""
370-
Used to mark missing attributes.
371-
Disable __dict__ creation since this class does nothing
372-
"""
373-
__slots__ = () #
374-
375-
def __init__(self,
376-
submenu,
377-
command,
378-
aliases=(),
379-
reformat_prompt="{super_prompt}>> {sub_prompt}",
380-
shared_attributes=None,
381-
require_predefined_shares=True,
382-
create_subclass=False,
383-
preserve_shares=False,
384-
persistent_history_file=None
385-
):
386-
"""Set up the class decorator
387-
388-
submenu (Cmd): Instance of something cmd.Cmd-like
389-
390-
command (str): The command the user types to access the SubMenu instance
391-
392-
aliases (iterable): More commands that will behave like "command"
393-
394-
reformat_prompt (str): Format str or None to disable
395-
if it's a string, it should contain one or more of:
396-
{super_prompt}: The current cmd's prompt
397-
{command}: The command in the current cmd with which it was called
398-
{sub_prompt}: The subordinate cmd's original prompt
399-
the default is "{super_prompt}{command} {sub_prompt}"
400-
401-
shared_attributes (dict): dict of the form {'subordinate_attr': 'parent_attr'}
402-
the attributes are copied to the submenu at the last moment; the submenu's
403-
attributes are backed up before this and restored afterward
404-
405-
require_predefined_shares: The shared attributes above must be independently
406-
defined in the subordinate Cmd (default: True)
407-
408-
create_subclass: put the modifications in a subclass rather than modifying
409-
the existing class (default: False)
410-
"""
411-
self.submenu = submenu
412-
self.command = command
413-
self.aliases = aliases
414-
if persistent_history_file:
415-
self.persistent_history_file = os.path.expanduser(persistent_history_file)
416-
else:
417-
self.persistent_history_file = None
418-
419-
if reformat_prompt is not None and not isinstance(reformat_prompt, str):
420-
raise ValueError("reformat_prompt should be either a format string or None")
421-
self.reformat_prompt = reformat_prompt
422-
423-
self.shared_attributes = {} if shared_attributes is None else shared_attributes
424-
if require_predefined_shares:
425-
for attr in self.shared_attributes.keys():
426-
if not hasattr(submenu, attr):
427-
raise AttributeError("The shared attribute '{attr}' is not defined in {cmd}. Either define {attr} "
428-
"in {cmd} or set require_predefined_shares=False."
429-
.format(cmd=submenu.__class__.__name__, attr=attr))
430-
431-
self.create_subclass = create_subclass
432-
self.preserve_shares = preserve_shares
433-
434-
def _get_original_attributes(self):
435-
return {
436-
attr: getattr(self.submenu, attr, AddSubmenu._Nonexistent)
437-
for attr in self.shared_attributes.keys()
438-
}
439-
440-
def _copy_in_shared_attrs(self, parent_cmd):
441-
for sub_attr, par_attr in self.shared_attributes.items():
442-
setattr(self.submenu, sub_attr, getattr(parent_cmd, par_attr))
443-
444-
def _copy_out_shared_attrs(self, parent_cmd, original_attributes):
445-
if self.preserve_shares:
446-
for sub_attr, par_attr in self.shared_attributes.items():
447-
setattr(parent_cmd, par_attr, getattr(self.submenu, sub_attr))
448-
else:
449-
for attr, value in original_attributes.items():
450-
if attr is not AddSubmenu._Nonexistent:
451-
setattr(self.submenu, attr, value)
452-
else:
453-
delattr(self.submenu, attr)
454-
455-
def __call__(self, cmd_obj):
456-
"""Creates a subclass of Cmd wherein the given submenu can be accessed via the given command"""
457-
def enter_submenu(parent_cmd, statement):
458-
"""
459-
This function will be bound to do_<submenu> and will change the scope of the CLI to that of the
460-
submenu.
461-
"""
462-
submenu = self.submenu
463-
original_attributes = self._get_original_attributes()
464-
history = _pop_readline_history()
465-
466-
if self.persistent_history_file:
467-
try:
468-
readline.read_history_file(self.persistent_history_file)
469-
except FileNotFoundError:
470-
pass
471-
472-
try:
473-
# copy over any shared attributes
474-
self._copy_in_shared_attrs(parent_cmd)
475-
476-
if statement.args:
477-
# Remove the menu argument and execute the command in the submenu
478-
submenu.onecmd_plus_hooks(statement.args)
479-
else:
480-
if self.reformat_prompt is not None:
481-
prompt = submenu.prompt
482-
submenu.prompt = self.reformat_prompt.format(
483-
super_prompt=parent_cmd.prompt,
484-
command=self.command,
485-
sub_prompt=prompt,
486-
)
487-
submenu.cmdloop()
488-
if self.reformat_prompt is not None:
489-
# noinspection PyUnboundLocalVariable
490-
self.submenu.prompt = prompt
491-
finally:
492-
# copy back original attributes
493-
self._copy_out_shared_attrs(parent_cmd, original_attributes)
494-
495-
# write submenu history
496-
if self.persistent_history_file:
497-
readline.write_history_file(self.persistent_history_file)
498-
# reset main app history before exit
499-
_push_readline_history(history)
500-
501-
def complete_submenu(_self, text, line, begidx, endidx):
502-
"""
503-
This function will be bound to complete_<submenu> and will perform the complete commands of the submenu.
504-
"""
505-
submenu = self.submenu
506-
original_attributes = self._get_original_attributes()
507-
try:
508-
# copy over any shared attributes
509-
self._copy_in_shared_attrs(_self)
510-
511-
# Reset the submenu's tab completion parameters
512-
submenu.allow_appended_space = True
513-
submenu.allow_closing_quote = True
514-
submenu.display_matches = []
515-
516-
return _complete_from_cmd(submenu, text, line, begidx, endidx)
517-
finally:
518-
# copy back original attributes
519-
self._copy_out_shared_attrs(_self, original_attributes)
520-
521-
# Pass the submenu's tab completion parameters back up to the menu that called complete()
522-
import copy
523-
_self.allow_appended_space = submenu.allow_appended_space
524-
_self.allow_closing_quote = submenu.allow_closing_quote
525-
_self.display_matches = copy.copy(submenu.display_matches)
526-
527-
original_do_help = cmd_obj.do_help
528-
original_complete_help = cmd_obj.complete_help
529-
530-
def help_submenu(_self, line):
531-
"""
532-
This function will be bound to help_<submenu> and will call the help commands of the submenu.
533-
"""
534-
tokens = line.split(None, 1)
535-
if tokens and (tokens[0] == self.command or tokens[0] in self.aliases):
536-
self.submenu.do_help(tokens[1] if len(tokens) == 2 else '')
537-
else:
538-
original_do_help(_self, line)
539-
540-
def _complete_submenu_help(_self, text, line, begidx, endidx):
541-
"""autocomplete to match help_submenu()'s behavior"""
542-
tokens = line.split(None, 1)
543-
if len(tokens) == 2 and (
544-
not (not tokens[1].startswith(self.command) and not any(
545-
tokens[1].startswith(alias) for alias in self.aliases))
546-
):
547-
return self.submenu.complete_help(
548-
text,
549-
tokens[1],
550-
begidx - line.index(tokens[1]),
551-
endidx - line.index(tokens[1]),
552-
)
553-
else:
554-
return original_complete_help(_self, text, line, begidx, endidx)
555-
556-
if self.create_subclass:
557-
class _Cmd(cmd_obj):
558-
do_help = help_submenu
559-
complete_help = _complete_submenu_help
560-
else:
561-
_Cmd = cmd_obj
562-
_Cmd.do_help = help_submenu
563-
_Cmd.complete_help = _complete_submenu_help
564-
565-
# Create bindings in the parent command to the submenus commands.
566-
setattr(_Cmd, 'do_' + self.command, enter_submenu)
567-
setattr(_Cmd, 'complete_' + self.command, complete_submenu)
568-
569-
# Create additional bindings for aliases
570-
for _alias in self.aliases:
571-
setattr(_Cmd, 'do_' + _alias, enter_submenu)
572-
setattr(_Cmd, 'complete_' + _alias, complete_submenu)
573-
return _Cmd
574-
575-
576315
class Cmd(cmd.Cmd):
577316
"""An easy but powerful framework for writing line-oriented command interpreters.
578317

0 commit comments

Comments
 (0)