Merge lp:~leonardr/launchpadlib/trusted-workflow-tests into lp:launchpadlib

Proposed by Leonard Richardson
Status: Merged
Merged at revision: not available
Proposed branch: lp:~leonardr/launchpadlib/trusted-workflow-tests
Merge into: lp:launchpadlib
Diff against target: 586 lines
5 files modified
src/launchpadlib/apps.py (+131/-1)
src/launchpadlib/bin/launchpad-credentials-console (+54/-0)
src/launchpadlib/credentials.py (+37/-17)
src/launchpadlib/docs/command-line.txt (+173/-3)
src/launchpadlib/testing/helpers.py (+28/-3)
To merge this branch: bzr merge lp:~leonardr/launchpadlib/trusted-workflow-tests
Reviewer Review Type Date Requested Status
Abel Deuring (community) code Approve
Review via email: mp+14167@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Leonard Richardson (leonardr) wrote :

This branch does some refactoring and adds some more tests for the RequestTokenAuthorizationEngine, the core of any trusted request token authorization client other than the end-user's web browser.

1. The class itself is renamed from AuthorizeRequestTokenProcess to RequestTokenAuthorizationEngine. RTAE is not the best name but I found ARTP really confusing.

2. RTAE no longer overwrites 'allow_access_levels' after it gets the token information. This lets me use the same RTAE object twice in tests.

3. Added code to detect when Launchpad doesn't recognize the request token.

4. Gave default implementations for a lot of the RTAE plugin methods, which just print the suggested_message. I already defined this code in the ScriptableRequestTokenAuthorization test helper, and I don't want to define it again for the command-line client.

5. Added tests for trying to approve a token that was already approved, trying to approve a nonexistent token, and triggering a server error.

Revision history for this message
Abel Deuring (adeuring) :
review: Approve (code)
72. By Leonard Richardson

Fixed up tests and return codes.

73. By Leonard Richardson

Added docstrings.

74. By Leonard Richardson

Cleaned up the exit function.

75. By Leonard Richardson

Added another docstring.

76. By Leonard Richardson

Added the command-line script, made a lot of spacing tweaks.

77. By Leonard Richardson

Merged with trunk.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/launchpadlib/apps.py'
2--- src/launchpadlib/apps.py 2009-10-28 17:27:53 +0000
3+++ src/launchpadlib/apps.py 2009-10-30 19:21:14 +0000
4@@ -20,9 +20,18 @@
5 themselves are kept in bin/.
6 """
7
8+__all__ = [
9+ 'RequestTokenApp',
10+ 'TrustedTokenAuthorizationConsoleApp',
11+ ]
12+
13+import getpass
14+import sys
15+
16 import simplejson
17
18-from launchpadlib.credentials import Credentials
19+from launchpadlib.credentials import (
20+ Credentials, RequestTokenAuthorizationEngine, TokenAuthorizationException)
21 from launchpadlib.uris import lookup_web_root
22
23
24@@ -43,3 +52,124 @@
25 return simplejson.dumps(token)
26
27
28+class TrustedTokenAuthorizationConsoleApp(RequestTokenAuthorizationEngine):
29+ """An application that authorizes request tokens."""
30+
31+ def __init__(self, web_root, consumer_name, request_token,
32+ access_levels='', input_method=raw_input):
33+ """Constructor.
34+
35+ :param access_levels: A string of comma-separated access level
36+ values. To get an up-to-date list of access levels, pass
37+ token_format=Credentials.DICT_TOKEN_FORMAT into
38+ Credentials.get_request_token, load the dict as JSON, and look
39+ in 'access_levels'.
40+ """
41+ access_levels = [level.strip() for level in access_levels.split(',')]
42+ super(TrustedTokenAuthorizationConsoleApp, self).__init__(
43+ web_root, consumer_name, request_token, access_levels)
44+
45+ self.input_method = input_method
46+
47+ def run(self):
48+ """Try to authorize a request token from user input."""
49+ self.error_code = -1 # Start off assuming failure.
50+ start = "Launchpad credential client (console)"
51+ self.output(start)
52+ self.output("-" * len(start))
53+
54+ try:
55+ self()
56+ except TokenAuthorizationException, e:
57+ print str(e)
58+ self.error_code = -1
59+ return self.pressEnterToExit()
60+ return self.pressEnterToExit()
61+
62+ def exit_with(self, code):
63+ """Exit the app with the specified error code."""
64+ sys.exit(code)
65+
66+ def getSingleCharInput(self, prompt, valid):
67+ """Retrieve a single-character line from the input stream."""
68+ valid = valid.upper()
69+ input = None
70+ while input is None:
71+ input = self.input_method(prompt).upper()
72+ if len(input) != 1 or input not in valid:
73+ input = None
74+ return input
75+
76+ def pressEnterToExit(self):
77+ """Make the user hit enter, and then exit with an error code."""
78+ prompt = '\nPress enter to go back to "%s". ' % self.consumer_name
79+ self.input_method(prompt)
80+ self.exit_with(self.error_code)
81+ return self.error_code
82+
83+ def input_username(self, cached_username, suggested_message):
84+ """Collect the Launchpad username from the end-user.
85+
86+ :param cached_username: A username from a previous entry attempt,
87+ to be presented as the default.
88+ """
89+ if cached_username is not None:
90+ extra = " [%s] " % cached_username
91+ else:
92+ extra = "\n(No Launchpad account? Just hit enter.) "
93+ username = self.input_method(suggested_message + extra)
94+ if username == '':
95+ return cached_username
96+ return username
97+
98+ def input_password(self, suggested_message):
99+ """Collect the Launchpad password from the end-user."""
100+ if self.input_method is raw_input:
101+ password = getpass.getpass(suggested_message + " ")
102+ else:
103+ password = self.input_method(suggested_message)
104+ return password
105+
106+ def input_access_level(self, available_levels, suggested_message,
107+ only_one_option=None):
108+ """Collect the desired level of access from the end-user."""
109+ if only_one_option is not None:
110+ self.output(suggested_message)
111+ prompt = self.message(
112+ 'Do you want to give "%(app)s" this level of access? [YN] ')
113+ allow = self.getSingleCharInput(prompt, "YN")
114+ if allow == "Y":
115+ return only_one_option['value']
116+ else:
117+ return self.UNAUTHORIZED_ACCESS_LEVEL
118+ else:
119+ levels_except_unauthorized = [
120+ level for level in available_levels
121+ if level['value'] != self.UNAUTHORIZED_ACCESS_LEVEL]
122+ options = []
123+ for i in range(0, len(levels_except_unauthorized)):
124+ options.append(
125+ "%d: %s" % (i+1, levels_except_unauthorized[i]['title']))
126+ self.output(suggested_message)
127+ for option in options:
128+ self.output(option)
129+ allowed = ("".join(map(str, range(1, i+2)))) + "Q"
130+ prompt = self.message(
131+ 'What should "%(app)s" be allowed to do using your '
132+ 'Launchpad account? [1-%(max)d or Q] ',
133+ extra_variables = {'max' : i+1})
134+ allow = self.getSingleCharInput(prompt, allowed)
135+ if allow == "Q":
136+ return self.UNAUTHORIZED_ACCESS_LEVEL
137+ else:
138+ return levels_except_unauthorized[int(allow)-1]['value']
139+
140+ def user_refused_to_authorize(self, suggested_message):
141+ """The user refused to authorize a request token."""
142+ self.output(suggested_message)
143+ self.error_code = -2
144+
145+ def user_authorized(self, access_level, suggested_message):
146+ """The user authorized a request token with some access level."""
147+ self.output(suggested_message)
148+ self.error_code = 0
149
150=== added file 'src/launchpadlib/bin/launchpad-credentials-console'
151--- src/launchpadlib/bin/launchpad-credentials-console 1970-01-01 00:00:00 +0000
152+++ src/launchpadlib/bin/launchpad-credentials-console 2009-10-30 19:21:14 +0000
153@@ -0,0 +1,54 @@
154+#!/usr/bin/python
155+
156+# Copyright 2009 Canonical Ltd.
157+
158+# This file is part of launchpadlib.
159+#
160+# launchpadlib is free software: you can redistribute it and/or modify it
161+# under the terms of the GNU Lesser General Public License as published by the
162+# Free Software Foundation, version 3 of the License.
163+#
164+# launchpadlib is distributed in the hope that it will be useful, but WITHOUT
165+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
166+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
167+# for more details.
168+#
169+# You should have received a copy of the GNU Lesser General Public License
170+# along with launchpadlib. If not, see <http://www.gnu.org/licenses/>.
171+
172+"""Take the user's Launchpad credentials and authorize a request token.
173+
174+This script will guide the user through the process of authorizing a
175+request token with their username and password. It's not as secure as
176+opening the user's web browser, but if you are writing a console
177+application and want to keep the authorization process console-based,
178+it's better to use this script than write the authorization code yourself.
179+"""
180+
181+__metaclass__ = type
182+
183+from optparse import OptionParser
184+from launchpadlib.apps import (
185+ RequestTokenApp, TrustedTokenAuthorizationConsoleApp)
186+
187+parser = OptionParser()
188+parser.usage = "%prog CONSUMER_NAME REQUEST_TOKEN [-r URL] [-a LEVEL,LEVEL,...]"
189+parser.add_option("-s", "--site", dest="web_root",
190+ help=("The name of the Launchpad site on which the request "
191+ "token can be found (default: %default). This can "
192+ "also be the URL to the root of the site."),
193+ metavar="URL", default="staging")
194+parser.add_option("-a", "--access-level", dest="access_levels",
195+ help="Any restrictions on your clients' access levels",
196+ metavar="[LEVEL,LEVEL,...]", default="")
197+
198+if __name__ == '__main__':
199+ (options, args) = parser.parse_args()
200+ if len(args) < 2:
201+ parser.error("You must provide both a consumer name and the ID of "
202+ "an OAuth request token on Launchpad.")
203+ consumer_name, token_id = args[:2]
204+ TrustedTokenAuthorizationConsoleApp(
205+ options.web_root, consumer_name, token_id,
206+ options.access_levels).run()
207+
208
209=== modified file 'src/launchpadlib/credentials.py'
210--- src/launchpadlib/credentials.py 2009-10-30 14:48:21 +0000
211+++ src/launchpadlib/credentials.py 2009-10-30 19:21:14 +0000
212@@ -27,8 +27,10 @@
213 import base64
214 import cgi
215 import httplib2
216+import textwrap
217 from urllib import urlencode, quote
218 from urlparse import urljoin
219+import webbrowser
220
221 import simplejson
222
223@@ -265,7 +267,7 @@
224
225 INPUT_USERNAME = "What email address do you use on Launchpad?"
226
227- INPUT_PASSWORD = "What's your Launchpad password?"
228+ INPUT_PASSWORD = "What's your Launchpad password? "
229
230 NONEXISTENT_REQUEST_TOKEN = """Launchpad couldn't find an outstanding request for integration between "%(app)s" and your Launchpad account. Either someone (hopefully you) already set up the integration, or else "%(app)s" is simply wrong and didn't actually set this up with Launchpad. If you still can't use "%(app)s" with Launchpad, try this process again from the beginning."""
231
232@@ -273,9 +275,9 @@
233
234 SERVER_ERROR = """There seems to be something wrong on the Launchpad server side, and I can't continue. Hopefully this is a temporary problem, but if it persists, it's probably because of a bug in Lauchpad or (less likely) a bug in "%(app)s"."""
235
236- STARTUP_MESSAGE = """An application identified as "%(app)s" wants to access Launchpad on your behalf. I'm the Launchpad credential client and I'm here to ask for your Launchpad username and password.
237+ STARTUP_MESSAGE = """An application identified as "%(app)s" wants to access Launchpad on your behalf. I'm the Launchpad credential client and I'm here to ask for your Launchpad username and password."""
238
239-I'll use your Launchpad password to give "%(app)s" limited access to your Launchpad account. I will not show your password to "%(app)s" itself."""
240+ STARTUP_MESSAGE_2 = """I'll use your Launchpad password to give "%(app)s" limited access to your Launchpad account. I will not show your password to "%(app)s" itself."""
241
242 SUCCESS = """You're all done! You should now be able to use Launchpad integration features of "%(app)s." """
243
244@@ -289,16 +291,20 @@
245
246 def __init__(self, web_root, consumer_name, request_token,
247 allow_access_levels=[], max_failed_attempts=3):
248- self.web_root = web_root
249+ self.web_root = uris.lookup_web_root(web_root)
250 self.consumer_name = consumer_name
251 self.request_token = request_token
252 self.browser = SimulatedLaunchpadBrowser(self.web_root)
253 self.max_failed_attempts = max_failed_attempts
254 self.allow_access_levels = allow_access_levels
255+ self.text_wrapper = textwrap.TextWrapper(
256+ replace_whitespace=False, width=78)
257
258 def __call__(self):
259
260- self.startup(self.message(self.STARTUP_MESSAGE))
261+ self.startup(
262+ [self.message(self.STARTUP_MESSAGE),
263+ self.message(self.STARTUP_MESSAGE_2)])
264
265 # Have the end-user enter their Launchpad username and password.
266 # Make sure the credentials are valid, and get information
267@@ -315,14 +321,17 @@
268 # There's only one choice: allow access at a certain level
269 # or don't allow access at all.
270 message = self.CHOOSE_ACCESS_LEVEL_ONE
271- level = [level['title'] for level in self.reconciled_access_levels
272- if level['value'] != self.UNAUTHORIZED_ACCESS_LEVEL]
273- extra = {'level' : level[0]}
274+ level = [level for level in self.reconciled_access_levels
275+ if level['value'] != self.UNAUTHORIZED_ACCESS_LEVEL][0]
276+ extra = {'level' : level['title']}
277+ only_one_option = level
278 else:
279 message = self.CHOOSE_ACCESS_LEVEL
280 extra = None
281+ only_one_option = None
282 access_level = self.input_access_level(
283- self.reconciled_access_levels, self.message(message, extra))
284+ self.reconciled_access_levels, self.message(message, extra),
285+ only_one_option)
286
287 # Notify the program of the user's choice.
288 if access_level == self.UNAUTHORIZED_ACCESS_LEVEL:
289@@ -420,8 +429,8 @@
290
291 # You should define these methods in your subclass.
292
293- def startup(self, suggested_message):
294- """Hook method called on startup."""
295+ def output(self, message):
296+ print self.text_wrapper.fill(message)
297
298 def input_username(self, cached_username, suggested_message):
299 """Collect the Launchpad username from the end-user.
300@@ -435,22 +444,31 @@
301 """Collect the Launchpad password from the end-user."""
302 raise NotImplementedError()
303
304- def input_access_level(self, available_levels, suggested_message):
305+ def input_access_level(self, available_levels, suggested_message,
306+ only_one_option=None):
307 """Collect the desired level of access from the end-user."""
308 raise NotImplementedError()
309
310+ def startup(self, suggested_messages):
311+ """Hook method called on startup."""
312+ for message in suggested_messages:
313+ self.output(message)
314+ self.output("\n")
315
316 def authentication_failure(self, suggested_message):
317 """The user entered invalid credentials."""
318- print suggested_message
319+ self.output(suggested_message)
320+ self.output("\n")
321
322 def user_refused_to_authorize(self, suggested_message):
323 """The user refused to authorize a request token."""
324- print suggested_message
325+ self.output(suggested_message)
326+ self.output("\n")
327
328 def user_authorized(self, access_level, suggested_message):
329 """The user authorized a request token with some access level."""
330- print suggested_message
331+ self.output(suggested_message)
332+ self.output("\n")
333
334 def server_consumer_differs_from_client_consumer(
335 self, client_name, real_name, suggested_message):
336@@ -461,11 +479,13 @@
337 end-user that its name is "bar". Something is fishy and at the very
338 least the end-user should be warned about this.
339 """
340- print suggested_message
341+ self.output("\n")
342+ self.output(suggested_message)
343+ self.output("\n")
344
345 def success(self, suggested_message):
346 """The token was successfully authorized."""
347- print suggested_message
348+ self.output(suggested_message)
349
350
351 class TokenAuthorizationException(Exception):
352
353=== modified file 'src/launchpadlib/docs/command-line.txt'
354--- src/launchpadlib/docs/command-line.txt 2009-10-27 16:26:06 +0000
355+++ src/launchpadlib/docs/command-line.txt 2009-10-30 19:21:14 +0000
356@@ -20,9 +20,11 @@
357
358 >>> import simplejson
359 >>> from launchpadlib.apps import RequestTokenApp
360- >>> app = RequestTokenApp(
361- ... "http://launchpad.dev:8085/", "consumer", "context")
362- >>> json = simplejson.loads(app.run())
363+
364+ >>> web_root = "http://launchpad.dev:8085/"
365+ >>> consumer_name = "consumer"
366+ >>> token_app = RequestTokenApp(web_root, consumer_name, "context")
367+ >>> json = simplejson.loads(token_app.run())
368
369 >>> sorted(json.keys())
370 ['access_levels', 'lp.context', 'oauth_token',
371@@ -33,3 +35,171 @@
372
373 >>> print json['oauth_token_consumer']
374 consumer
375+
376+TrustedTokenAuthorizationConsoleApp
377+===================================
378+
379+This class is called by the command-line script
380+launchpad-credentials-console. It asks for the user's Launchpad
381+username and password, and authorizes a request token on their
382+behalf.
383+
384+ >>> from launchpadlib.apps import TrustedTokenAuthorizationConsoleApp
385+ >>> from launchpadlib.testing.helpers import UserInput
386+
387+This class does not create the request token, or exchange it for the
388+access token--that's the job of the program that calls
389+launchpad-credentials-console. So we'll use the request token created
390+earlier by RequestTokenApp.
391+
392+ >>> request_token = json['oauth_token']
393+
394+Since this is a test, we don't want the application to call sys.exit()
395+or try to open up pages in a web browser. This subclass of
396+TrustedTokenAuthorizationConsoleApp will print messages instead of
397+performing such un-doctest-like actions.
398+
399+ >>> class ConsoleApp(TrustedTokenAuthorizationConsoleApp):
400+ ... def open_login_page_in_user_browser(self, url):
401+ ... """Print a status message."""
402+ ... self.output("[If this were a real application, the "
403+ ... "end-user's web browser would be opened "
404+ ... "to %s]" % url)
405+ ...
406+ ... def exit_with(self, code):
407+ ... print "Application exited with code %d" % code
408+
409+We'll use a UserInput object to simulate a user typing things in at
410+the prompt and hitting enter. This UserInput runs the program
411+correctly, entering the Launchpad username and password, choosing an
412+access level, and hitting Enter to exit.
413+
414+ >>> username = "salgado@ubuntu.com"
415+ >>> password = "zeca"
416+ >>> fake_input = UserInput([username, password, "1", ""])
417+
418+Here's a successful run of the application. When the request token is
419+authorized, the script's response code is 0.
420+
421+ >>> app = ConsoleApp(
422+ ... web_root, consumer_name, request_token,
423+ ... 'READ_PRIVATE, READ_PUBLIC', input_method=fake_input)
424+ >>> ignore = app.run()
425+ Launchpad credential client (console)
426+ -------------------------------------
427+ An application identified as "consumer" wants to access Launchpad...
428+ What email address do you use on Launchpad?
429+ (No Launchpad account? Just hit enter.) [User input: salgado@ubuntu.com]
430+ What's your Launchpad password? [User input: zeca]
431+ Now it's time for you to decide how much power to give "consumer"...
432+ 1: Read Non-Private Data
433+ 2: Read Anything
434+ What should "consumer" be allowed to do...? [1-2 or Q] [User input: 1]
435+ Okay, I'm telling Launchpad to grant "consumer" access to your account.
436+ You're all done!...
437+ Press enter to go back to "consumer". [User input: ]
438+ Application exited with code 0
439+
440+Now that the request token has been authorized, we'll need to create
441+another one to continue the test.
442+
443+ >>> json = simplejson.loads(token_app.run())
444+ >>> request_token = json['oauth_token']
445+ >>> app.request_token = request_token
446+
447+Invalid input is ignored. The user may enter 'Q' instead of a number
448+to refuse to authorize the request token. When the user denies access,
449+the exit code is -2.
450+
451+ >>> fake_input = UserInput([username, password, "A", "99", "Q", ""])
452+ >>> app.input_method = fake_input
453+ >>> ignore = app.run()
454+ Launchpad credential client (console)
455+ -------------------------------------
456+ An application identified as "consumer"...
457+ What should "consumer" be allowed to do...? [1-2 or Q] [User input: A]
458+ What should "consumer" be allowed to do...? [1-2 or Q] [User input: 99]
459+ What should "consumer" be allowed to do...? [1-2 or Q] [User input: Q]
460+ Okay, I'm going to cancel the request...
461+ You're all done! "consumer" still doesn't have access...
462+ <BLANKLINE>
463+ Press enter to go back to "consumer". [User input: ]
464+ Application exited with code -2
465+
466+When the third-party application will allow only one level of access,
467+the end-user is presented with a yes-or-no choice instead of a list to
468+choose from. Again, invalid input is ignored.
469+
470+ >>> json = simplejson.loads(token_app.run())
471+ >>> request_token = json['oauth_token']
472+ >>> fake_input = UserInput([username, password, "1", "Q", "Y", ""])
473+
474+ >>> app = ConsoleApp(
475+ ... web_root, consumer_name, request_token,
476+ ... 'READ_PRIVATE', input_method=fake_input)
477+
478+ >>> ignore = app.run()
479+ Launchpad credential client (console)
480+ -------------------------------------
481+ An application identified as "consumer"...
482+ Do you want to give "consumer" this level of access? [YN] [User input: 1]
483+ Do you want to give "consumer" this level of access? [YN] [User input: Q]
484+ Do you want to give "consumer" this level of access? [YN] [User input: Y]
485+ ...
486+ Application exited with code 0
487+
488+
489+Error handling
490+--------------
491+
492+When the end-user refuses to authorize the request token, the app
493+exits with a return code of -2, as seen above. When any other error
494+gets in the way of the authorization of the request token, the app's
495+return code is -1.
496+
497+If the user hits enter when asked for their email address, indicating
498+that they don't have a Launchpad account, the app opens their browser
499+to the Launchpad login page.
500+
501+ >>> json = simplejson.loads(token_app.run())
502+ >>> app.request_token = json['oauth_token']
503+
504+ >>> input_nothing = UserInput(["", ""])
505+ >>> app.input_method = input_nothing
506+
507+ >>> ignore = app.run()
508+ Launchpad credential client (console)
509+ -------------------------------------
510+ An application identified as "consumer"...
511+ [If this were a real application, the end-user's web browser...]
512+ OK, you'll need to get yourself a Launchpad account before...
513+ <BLANKLINE>
514+ I'm opening the Launchpad registration page in your web browser...
515+ Press enter to go back to "consumer". [User input: ]
516+ Application exited with code -1
517+
518+If the user keeps entering bad passwords, the app eventually gives up.
519+
520+ >>> input_bad_password = UserInput(
521+ ... [username, "badpw", "", "badpw", "", "badpw", ""])
522+ >>> json = simplejson.loads(token_app.run())
523+ >>> request_token = json['oauth_token']
524+ >>> app.request_token = request_token
525+ >>> app.input_method = input_bad_password
526+ >>> ignore = app.run()
527+ Launchpad credential client (console)
528+ -------------------------------------
529+ An application identified as "consumer"...
530+ What email address do you use on Launchpad?
531+ (No Launchpad account? Just hit enter.) [User input: salgado@ubuntu.com]
532+ What's your Launchpad password? [User input: badpw]
533+ I can't log in with the credentials you gave me. Let's try again.
534+ What email address do you use on Launchpad? [salgado@ubuntu.com] [User input: ]
535+ What's your Launchpad password? [User input: badpw]
536+ I can't log in with the credentials you gave me. Let's try again.
537+ What email address do you use on Launchpad? [salgado@ubuntu.com] [User input: ]
538+ What's your Launchpad password? [User input: badpw]
539+ You've failed the password entry too many times...
540+ Press enter to go back to "consumer". [User input: ]
541+ Application exited with code -1
542+
543
544=== modified file 'src/launchpadlib/testing/helpers.py'
545--- src/launchpadlib/testing/helpers.py 2009-10-30 14:48:21 +0000
546+++ src/launchpadlib/testing/helpers.py 2009-10-30 19:21:14 +0000
547@@ -119,11 +119,36 @@
548 print suggested_message
549 return self.password
550
551- def input_access_level(self, available_levels, suggested_message):
552+ def input_access_level(self, available_levels, suggested_message,
553+ only_one_option):
554 """Collect the desired level of access from the end-user."""
555 print suggested_message
556 print [level['value'] for level in available_levels]
557 return self.choose_access_level
558
559- def startup(self, suggested_message):
560- print suggested_message
561+ def startup(self, suggested_messages):
562+ for message in suggested_messages:
563+ print message
564+
565+
566+class UserInput(object):
567+ """A class to store fake user input in a readable way.
568+
569+ An instance of this class can be used as a substitute for the
570+ raw_input() function.
571+ """
572+
573+ def __init__(self, inputs):
574+ """Initialize with a line of user inputs."""
575+ self.stream = iter(inputs)
576+
577+ def __call__(self, prompt):
578+ """Print and return the next line of input."""
579+ line = self.readline()
580+ print prompt + "[User input: %s]" % line
581+ return line
582+
583+ def readline(self):
584+ """Return the next line of input."""
585+ next_input = self.stream.next()
586+ return str(next_input)

Subscribers

People subscribed via source and target branches