Skip to content

Commit 9083237

Browse files
committed
Initial extract from cmd2 main project
1 parent 50f1202 commit 9083237

File tree

8 files changed

+766
-36
lines changed

8 files changed

+766
-36
lines changed

.gitignore

+2-36
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,13 @@ wheels/
2525
.installed.cfg
2626
*.egg
2727

28-
# PyInstaller
29-
# Usually these files are written by a python script from a template
30-
# before PyInstaller builds the exe, so as to inject date/other infos into it.
31-
*.manifest
32-
*.spec
33-
3428
# Installer logs
3529
pip-log.txt
3630
pip-delete-this-directory.txt
3731

3832
# Unit test / coverage reports
33+
.tox
34+
.pytest_cache
3935
htmlcov/
4036
.tox/
4137
.coverage
@@ -50,35 +46,12 @@ coverage.xml
5046
*.mo
5147
*.pot
5248

53-
# Django stuff:
54-
*.log
55-
local_settings.py
56-
57-
# Flask stuff:
58-
instance/
59-
.webassets-cache
60-
61-
# Scrapy stuff:
62-
.scrapy
63-
6449
# Sphinx documentation
6550
docs/_build/
6651

67-
# PyBuilder
68-
target/
69-
70-
# Jupyter Notebook
71-
.ipynb_checkpoints
72-
7352
# pyenv
7453
.python-version
7554

76-
# celery beat schedule file
77-
celerybeat-schedule
78-
79-
# SageMath parsed files
80-
*.sage.py
81-
8255
# dotenv
8356
.env
8457

@@ -87,15 +60,8 @@ celerybeat-schedule
8760
venv/
8861
ENV/
8962

90-
# Spyder project settings
91-
.spyderproject
92-
.spyproject
93-
9463
# Rope project settings
9564
.ropeproject
9665

97-
# mkdocs documentation
98-
/site
99-
10066
# mypy
10167
.mypy_cache/

CHANGELOG.md

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Changelog
2+
All notable changes to this project will be documented in this file.
3+
4+
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
5+
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
6+
7+
## Unreleased
8+
### Added
9+
- Initial extract from cmd2 codebase
10+

README.md

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# cmd2-submenu
2+
This project provides a submenu system for cmd2
3+
4+
## Installing
5+
To install the plugin, do:
6+
```
7+
$ pip install cmd2-submenu
8+
```
9+
10+
## How to use
11+

cmd2_submenu/__init__.py

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#
2+
# coding=utf-8
3+
4+
from .submenu import AddSubmenu

cmd2_submenu/submenu.py

+271
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
#
2+
# coding=utf-8
3+
import copy
4+
import readline
5+
from typing import List
6+
7+
from cmd2.rl_utils import rl_type, RlType
8+
9+
10+
class AddSubmenu(object):
11+
"""Conveniently add a submenu (Cmd-like class) to a Cmd
12+
13+
e.g. given "class SubMenu(Cmd): ..." then
14+
15+
@AddSubmenu(SubMenu(), 'sub')
16+
class MyCmd(cmd.Cmd):
17+
....
18+
19+
will have the following effects:
20+
1. 'sub' will interactively enter the cmdloop of a SubMenu instance
21+
2. 'sub cmd args' will call do_cmd(args) in a SubMenu instance
22+
3. 'sub ... [TAB]' will have the same behavior as [TAB] in a SubMenu cmdloop
23+
i.e., autocompletion works the way you think it should
24+
4. 'help sub [cmd]' will print SubMenu's help (calls its do_help())
25+
"""
26+
27+
class _Nonexistent(object):
28+
"""
29+
Used to mark missing attributes.
30+
Disable __dict__ creation since this class does nothing
31+
"""
32+
__slots__ = () #
33+
34+
def __init__(self,
35+
submenu,
36+
command,
37+
aliases=(),
38+
reformat_prompt="{super_prompt}>> {sub_prompt}",
39+
shared_attributes=None,
40+
require_predefined_shares=True,
41+
create_subclass=False,
42+
preserve_shares=False,
43+
persistent_history_file=None
44+
):
45+
"""Set up the class decorator
46+
47+
submenu (Cmd): Instance of something cmd.Cmd-like
48+
49+
command (str): The command the user types to access the SubMenu instance
50+
51+
aliases (iterable): More commands that will behave like "command"
52+
53+
reformat_prompt (str): Format str or None to disable
54+
if it's a string, it should contain one or more of:
55+
{super_prompt}: The current cmd's prompt
56+
{command}: The command in the current cmd with which it was called
57+
{sub_prompt}: The subordinate cmd's original prompt
58+
the default is "{super_prompt}{command} {sub_prompt}"
59+
60+
shared_attributes (dict): dict of the form {'subordinate_attr': 'parent_attr'}
61+
the attributes are copied to the submenu at the last moment; the submenu's
62+
attributes are backed up before this and restored afterward
63+
64+
require_predefined_shares: The shared attributes above must be independently
65+
defined in the subordinate Cmd (default: True)
66+
67+
create_subclass: put the modifications in a subclass rather than modifying
68+
the existing class (default: False)
69+
"""
70+
self.submenu = submenu
71+
self.command = command
72+
self.aliases = aliases
73+
if persistent_history_file:
74+
self.persistent_history_file = os.path.expanduser(persistent_history_file)
75+
else:
76+
self.persistent_history_file = None
77+
78+
if reformat_prompt is not None and not isinstance(reformat_prompt, str):
79+
raise ValueError("reformat_prompt should be either a format string or None")
80+
self.reformat_prompt = reformat_prompt
81+
82+
self.shared_attributes = {} if shared_attributes is None else shared_attributes
83+
if require_predefined_shares:
84+
for attr in self.shared_attributes.keys():
85+
if not hasattr(submenu, attr):
86+
raise AttributeError("The shared attribute '{attr}' is not defined in {cmd}. Either define {attr} "
87+
"in {cmd} or set require_predefined_shares=False."
88+
.format(cmd=submenu.__class__.__name__, attr=attr))
89+
90+
self.create_subclass = create_subclass
91+
self.preserve_shares = preserve_shares
92+
93+
def _get_original_attributes(self):
94+
return {
95+
attr: getattr(self.submenu, attr, AddSubmenu._Nonexistent)
96+
for attr in self.shared_attributes.keys()
97+
}
98+
99+
def _copy_in_shared_attrs(self, parent_cmd):
100+
for sub_attr, par_attr in self.shared_attributes.items():
101+
setattr(self.submenu, sub_attr, getattr(parent_cmd, par_attr))
102+
103+
def _copy_out_shared_attrs(self, parent_cmd, original_attributes):
104+
if self.preserve_shares:
105+
for sub_attr, par_attr in self.shared_attributes.items():
106+
setattr(parent_cmd, par_attr, getattr(self.submenu, sub_attr))
107+
else:
108+
for attr, value in original_attributes.items():
109+
if attr is not AddSubmenu._Nonexistent:
110+
setattr(self.submenu, attr, value)
111+
else:
112+
delattr(self.submenu, attr)
113+
114+
def __call__(self, cmd_obj):
115+
"""Creates a subclass of Cmd wherein the given submenu can be accessed via the given command"""
116+
def enter_submenu(parent_cmd, statement):
117+
"""
118+
This function will be bound to do_<submenu> and will change the scope of the CLI to that of the
119+
submenu.
120+
"""
121+
submenu = self.submenu
122+
original_attributes = self._get_original_attributes()
123+
history = _pop_readline_history()
124+
125+
if self.persistent_history_file and rl_type != RlType.NONE:
126+
try:
127+
readline.read_history_file(self.persistent_history_file)
128+
except FileNotFoundError:
129+
pass
130+
131+
try:
132+
# copy over any shared attributes
133+
self._copy_in_shared_attrs(parent_cmd)
134+
135+
if statement.args:
136+
# Remove the menu argument and execute the command in the submenu
137+
submenu.onecmd_plus_hooks(statement.args)
138+
else:
139+
if self.reformat_prompt is not None:
140+
prompt = submenu.prompt
141+
submenu.prompt = self.reformat_prompt.format(
142+
super_prompt=parent_cmd.prompt,
143+
command=self.command,
144+
sub_prompt=prompt,
145+
)
146+
submenu.cmdloop()
147+
if self.reformat_prompt is not None:
148+
# noinspection PyUnboundLocalVariable
149+
self.submenu.prompt = prompt
150+
finally:
151+
# copy back original attributes
152+
self._copy_out_shared_attrs(parent_cmd, original_attributes)
153+
154+
# write submenu history
155+
if self.persistent_history_file and rl_type != RlType.NONE:
156+
readline.write_history_file(self.persistent_history_file)
157+
# reset main app history before exit
158+
_push_readline_history(history)
159+
160+
def complete_submenu(_self, text, line, begidx, endidx):
161+
"""
162+
This function will be bound to complete_<submenu> and will perform the complete commands of the submenu.
163+
"""
164+
submenu = self.submenu
165+
original_attributes = self._get_original_attributes()
166+
try:
167+
# copy over any shared attributes
168+
self._copy_in_shared_attrs(_self)
169+
170+
# Reset the submenu's tab completion parameters
171+
submenu.allow_appended_space = True
172+
submenu.allow_closing_quote = True
173+
submenu.display_matches = []
174+
175+
return _complete_from_cmd(submenu, text, line, begidx, endidx)
176+
finally:
177+
# copy back original attributes
178+
self._copy_out_shared_attrs(_self, original_attributes)
179+
180+
# Pass the submenu's tab completion parameters back up to the menu that called complete()
181+
_self.allow_appended_space = submenu.allow_appended_space
182+
_self.allow_closing_quote = submenu.allow_closing_quote
183+
_self.display_matches = copy.copy(submenu.display_matches)
184+
185+
original_do_help = cmd_obj.do_help
186+
original_complete_help = cmd_obj.complete_help
187+
188+
def help_submenu(_self, line):
189+
"""
190+
This function will be bound to help_<submenu> and will call the help commands of the submenu.
191+
"""
192+
tokens = line.split(None, 1)
193+
if tokens and (tokens[0] == self.command or tokens[0] in self.aliases):
194+
self.submenu.do_help(tokens[1] if len(tokens) == 2 else '')
195+
else:
196+
original_do_help(_self, line)
197+
198+
def _complete_submenu_help(_self, text, line, begidx, endidx):
199+
"""autocomplete to match help_submenu()'s behavior"""
200+
tokens = line.split(None, 1)
201+
if len(tokens) == 2 and (
202+
not (not tokens[1].startswith(self.command) and not any(
203+
tokens[1].startswith(alias) for alias in self.aliases))
204+
):
205+
return self.submenu.complete_help(
206+
text,
207+
tokens[1],
208+
begidx - line.index(tokens[1]),
209+
endidx - line.index(tokens[1]),
210+
)
211+
else:
212+
return original_complete_help(_self, text, line, begidx, endidx)
213+
214+
if self.create_subclass:
215+
class _Cmd(cmd_obj):
216+
do_help = help_submenu
217+
complete_help = _complete_submenu_help
218+
else:
219+
_Cmd = cmd_obj
220+
_Cmd.do_help = help_submenu
221+
_Cmd.complete_help = _complete_submenu_help
222+
223+
# Create bindings in the parent command to the submenus commands.
224+
setattr(_Cmd, 'do_' + self.command, enter_submenu)
225+
setattr(_Cmd, 'complete_' + self.command, complete_submenu)
226+
227+
# Create additional bindings for aliases
228+
for _alias in self.aliases:
229+
setattr(_Cmd, 'do_' + _alias, enter_submenu)
230+
setattr(_Cmd, 'complete_' + _alias, complete_submenu)
231+
return _Cmd
232+
233+
234+
def _pop_readline_history(clear_history: bool=True) -> List[str]:
235+
"""Returns a copy of readline's history and optionally clears it (default)"""
236+
# noinspection PyArgumentList
237+
if rl_type == RlType.NONE:
238+
return []
239+
240+
history = [
241+
readline.get_history_item(i)
242+
for i in range(1, 1 + readline.get_current_history_length())
243+
]
244+
if clear_history:
245+
readline.clear_history()
246+
return history
247+
248+
249+
def _push_readline_history(history, clear_history=True):
250+
"""Restores readline's history and optionally clears it first (default)"""
251+
if rl_type != RlType.NONE:
252+
if clear_history:
253+
readline.clear_history()
254+
for line in history:
255+
readline.add_history(line)
256+
257+
258+
def _complete_from_cmd(cmd_obj, text, line, begidx, endidx):
259+
"""Complete as though the user was typing inside cmd's cmdloop()"""
260+
from itertools import takewhile
261+
command_subcommand_params = line.split(None, 3)
262+
263+
if len(command_subcommand_params) < (3 if text else 2):
264+
n = len(command_subcommand_params[0])
265+
n += sum(1 for _ in takewhile(str.isspace, line[n:]))
266+
return cmd_obj.completenames(text, line[n:], begidx - n, endidx - n)
267+
268+
command, subcommand = command_subcommand_params[:2]
269+
n = len(command) + sum(1 for _ in takewhile(str.isspace, line))
270+
cfun = getattr(cmd_obj, 'complete_' + subcommand, cmd_obj.complete)
271+
return cfun(text, line[n:], begidx - n, endidx - n)

0 commit comments

Comments
 (0)