-
Notifications
You must be signed in to change notification settings - Fork 1
/
post 06.txt
252 lines (206 loc) · 12.3 KB
/
post 06.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
Now that we've seen a command based CLI that is versatile and customizable, let's make a similar menu based CLI. (The code for this post is in menu.py on the GitHub repository) Here's the docstring to give you and overview of where we're going:
[python]
import string
import sys
class Menu(object):
"""
A simple framework for writing command line menus. (object)
There is no reason to instantiate Menu itself. It should be used as a
parent class for a menu system that you define yourself.
Class Attributes:
intro: Text displayed at the beginning of the menu loop. (str)
prompt: Text displayed when getting user choices. (str)
Attributes:
choice_queue: Automatic commands yet to be proccessed. (list of str)
lastchoice: The last choice made by the user. (str)
methods: The mapping of menu choices to methods. (dict of str:bound method)
status: The status of the menu system, if any. (str)
stdin_save: Storage for when stdin is redirected. (file)
stdout_save: Storage for when stdout is redirected. (file)
text: The text of the menu. (str)
Methods:
emptyline: Handle blank choices. (bool)
menuloop: Repeatedly display a menu, get a choice, and process tit. (None)
onechoice: Act on a single menu choice. (bool)
postchoice: Common processing after the choice is proccessed. (bool)
postloop: Processing done after the menu loop ends. (None)
prechoice: Process the choice before acting on it. (str)
preloop: Processing done before starting the menu loop. (None)
set_menu: Set up the menu text and dictionary. (None)
sort_menu: Sort the lines of the menu text. (None)
unrecognized: Handle choices not in the menu. (bool)
Overridden Methods:
__init__
"""
[/python]
Class attributes are similar, although none of the ones for the help system. That's because we're not going to have a help system. I didn't see how one would work into a traditional menu system.
The instance attributes have some familiar if changed choices. The choice_queue and lastchoice attributes are equivalent to the cmdqueue and lastcmd attributes of Cmd. The stdin_save and stdout_save attributes should not be messed with. See the section below on __init__ to see how to redirect input and output.
The new instance attributes are methods, status, and text. The status attribute is text that is displayed after the menu when asking for input. It is only displayed if it is not empty, so it can just be ignored if you want. The methods attribute is a mapping of menu choices to the methods to call, and the text attribute is the text of the menu. Both are calculated once during initialization.
The methods mostly correspond to the Cmd methods. The only new methods are set_menu and sort, which are used during initialization to set up the menu text and choices. In the Cmd class, methods starting with do_ defined what commands could be handled. In the Menu class, methods starting with menu_ define what menu options there are. The set_menu method extracts all of the information from the menu_ methods to create text to display for the menu, and a mapping of menu choices to methods.
[python]
# Text displayed at the beginning of the menu loop.
intro = ''
# Text displayed when getting user choices.
prompt = 'Please enter your selection: '
[/python]
The intro attribute is blank to start, as with Cmd. The prompt defaults to just a generic menu prompt.
[python]
def __init__(self, stdin=None, stdout=None):
"""
Initialize the file interface for the menu system. (None)
Parameters:
sort_key: The key parameter when sorting menu choices. (callable)
stdin: The input file for the menu interface. (file)
stdout: The output file for the menu interface. (file)
"""
# Save the stdin before redirecting.
self.stdin_save = sys.__stdin__
if stdin is not None:
sys.stdin = stdin
# Save the stdout before redirecting.
self.stdout_save = sys.__stdout__
if stdout is not None:
sys.stdout = stdout
# Set up the menu.
self.set_menu()
# Set up the tracking attributes.
self.lastchoice = ''
self.status = ''
self.choice_queue = []
[/python]
The __init__ method sets the file redirection. I do it differently than in Cmd, and actually fully redirect sys.stdin and sys.stdout. That way you don't need to worry about using self.stdin and self.stdout, and can just write everything with inputs and prints. The set_menu method is called to set up the menu, and the tracking attributes are all given blank values.
[python]
def emptyline(self):
"""Handle blank choices. (bool)"""
# Do the last choice over again, if there is one.
if self.lastchoice:
return self.onechoice(self.lastchoice)
else:
return False
[/python]
As in Cmd, the emptyline method handles blank user input. And as in Cmd, it does the last valid choice, if one has been stored.
[python]
def menuloop(self, intro=None):
"""
Repeatedly display a menu, get a choice, and process that choice. (None)
If the intro parameter is None, the intro attribute of the class is used
instead.
Parameters:
intro: The text to display before the loop begins. (str or None)
"""
# User defined processing before the loop starts.
self.preloop()
# Display any introductory text.
if intro is not None:
self.intro = intro
if self.intro:
print(self.intro)
# Loop through the menu choices.
while True:
# Process any queued tasks first.
if self.choice_queue:
choice = self.choice_queue.pop(0)
else:
# Display the menu, with any status.
print(self.text)
print()
if self.status:
print('Status:', self.status)
print()
self.status = ''
# Get the user's choice.
choice = input(self.prompt).strip()
# Process the choice.
choice = self.prechoice(choice)
stop = self.onechoice(choice)
stop = self.postchoice(stop, choice)
# Check for loop termination.
if stop:
break
# Clean up after the menu loop.
self.postloop()
sys.stdin = self.stdin_save
sys.stdout = self.stdout_save
[/python]
The menuloop method, corresponding to Cmd's cmdloop method, is the meat of the Menu class. It starts by running preloop, which contains any set up required for a specific sub-class of Menu. Then it displays the introductory text, just as in Cmd: any intro given to menuloop as a paramter takes priority, the intro class is used as a second priority, and if they're both blank nothing is displayed.
Then we get to the actual processing loop. If there are any choices in the choice_queue attribute, those are taken first. Otherwise, the menu (self.text) is displayed, then the status attribute is displayed if there is one, and then a simple input gets the user's choice.
Once the choice is obtained, it is processed in three steps. First, the prechoice method is run, allowing for changes to the choice. Then the onechoice method is run, to actually process the choice. This returns a stop value, which if True will end the menu loop. Then the postchoice method is run, allowing for changes to the stop value.
Finally, the stop value is checked, and the loop either continues or ends.
After the loop ends, the postloop method is run for any finally processing or messages, and stdin/stdout are reset to their saved values.
[python]
def onechoice(self, choice):
"""
Act on a single menu choice. (bool)
Parameters:
choice: The user's menu choice. (str)
"""
if not choice:
stop = self.emptyline()
elif choice.lower() in self.methods:
stop = self.methods[choice.lower()]()
self.lastchoice = choice
else:
stop = self.unrecognized(choice)
return stop
[/python]
The onechoice method does the determination of which method handles a given user choice. If the choice is empty, it uses the emptyline method. If it recognizes the choice, it sends it to the appropriate method. 'Recognize' in this context means that it can find the choice in the methods dictionary that the set_up method created. Finally, if it has a non-empty choice that it doesn't recognize, it sends it to the unrecognized method.
[python]
def postchoice(self, stop, choice):
"""
Common processing after the choice is proccessed. (bool)
Parameters:
stop: Flag for stopping the menu loop. (bool)
choice: The user's choice. (str)
"""
return stop
def postloop(self):
"""Processing done after the menu loop ends. (None)"""
pass
def prechoice(self, choice):
"""
Process the choice before acting on it. (str)
Parameters:
choice: The original user's choice. (str)
"""
return choice
def preloop(self):
"""Processing done before starting the menu loop. (None)"""
pass
[/python]
The post- and pre- methods for Menu are analogous to the ones in Cmd. As you can see, they are just stub methods, waiting to be overridden by sub-classes of Menu.
[python]
def set_menu(self):
"""Set up the menu text and dictionary. (None)"""
menu_lines = []
self.methods = {}
for attribute in dir(self):
if attribute.startswith('menu_'):
attr = getattr(self, attribute)
if hasattr(attr, '__doc__'):
menu_lines.append(attr.__doc__.strip().split('\n')[0].strip())
self.methods[attr.__doc__.split(':')[0].strip().lower()] = attr
self.sort_menu(menu_lines)
self.text = '\n' + '\n'.join(menu_lines)
def sort_menu(self, menu_lines):
"""
Sort the lines of the menu text. (None)
Parameters:
menu_lines: the lines of the menu. (list of str)
"""
menu_lines.sort(key = lambda line: line.split(':')[0])
[/python]
The set_menu and sort_menu methods create the data about the menu that menuloop and onechoice use to get and process the menu choices. The set_menu method runs through all the attributes of itself that start with 'menu_'. If that attribute has a docstring, the first line of that docstring is taken. That is stored as a line in the menu itself. Everything on that line before the first colon is used as a key for the methods dictionary attribute. The value for that key is the attribute (method) itself. For example, say you have a method menu_foo whose docstring is 'F: Do foo'. The line 'F: Do foo' will be added to the menu text. Additionally, self.methods['f'] will be set to do_foo. Note that the 'f' is lowercased for case insensitive choices.
After the lines of the actual menu are discovered, they are sorted by the sort_menu method. The sorting is done in a different method so that it is easier to override the sort order for subclasses. For example, say you want to number your menu items, and you have 12 of them. Given the default string sorting, 11 and 12 would end up between 1 and 2.
Note that you could theoretically modify this data (probably in postchoice) to create a dynamic menu that changes based on user responses. I'm not sure I would recommend doing that, but it is possible.
[python]
def unrecognized(self, choice):
"""
Handle choices not in the menu. (bool)
Parameters:
choice: The user's menu choice. (str)
"""
self.status = 'I do not recognize the choice {!r}. Please make another choice.'.format(choice)
return False
[/python]
Finally, there is the unrecognized method, which handles any responses that aren't in the methods dictionary. It just updates the status with a message. This method is meant to be overridden. Really, with this class you just don't want to mess with __init__, menuloop, onechoice, or set_menu. And you shouldn't need to. Almost any functionality you would get by modifying those four methods should be achievable by modifying one of the other methods.
In the next post I will give a basic example of a menu system using this class.