4 DialCentral - Front end for Google's Grand Central service.
5 Copyright (C) 2008 Mark Bergman bergman AT merctech DOT com
7 This library is free software; you can redistribute it and/or
8 modify it under the terms of the GNU Lesser General Public
9 License as published by the Free Software Foundation; either
10 version 2.1 of the License, or (at your option) any later version.
12 This library is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 Lesser General Public License for more details.
17 You should have received a copy of the GNU Lesser General Public
18 License along with this library; if not, write to the Free Software
19 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
21 @todo Try gogol's bigger tree view items
22 @todo Try gogol's wrapping for messages
23 @bug Does special notifiers even work?
24 @bug Messaging UI is bad(?)
25 @bug Not logging back in on network reconnect
27 @todo Figure out how to integrate with the Maemo contacts app
28 @bug Session timeouts are bad, possible solutions:
29 @li For every X minutes, if logged in, attempt login
30 @li Restructure so code handling login/dial/sms is beneath everything else and login attempts are made if those fail
31 @todo Add logging support to make debugging issues for people a lot easier
35 from __future__ import with_statement
58 def getmtime_nothrow(path):
60 return os.path.getmtime(path)
65 def display_error_message(msg):
66 error_dialog = gtk.MessageDialog(None, 0, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, msg)
68 def close(dialog, response):
70 error_dialog.connect("response", close)
74 class Dialcentral(object):
77 os.path.join(os.path.dirname(__file__), "dialcentral.glade"),
78 os.path.join(os.path.dirname(__file__), "../lib/dialcentral.glade"),
79 '/usr/lib/dialcentral/dialcentral.glade',
91 BACKENDS = (NULL_BACKEND, GC_BACKEND, GV_BACKEND)
94 self._initDone = False
95 self._connection = None
97 self._clipboard = gtk.clipboard_get()
99 self._credentials = ("", "")
100 self._selectedBackendId = self.NULL_BACKEND
101 self._defaultBackendId = self.GC_BACKEND
102 self._phoneBackends = None
103 self._dialpads = None
104 self._accountViews = None
105 self._messagesViews = None
106 self._recentViews = None
107 self._contactsViews = None
108 self._alarmHandler = None
109 self._ledHandler = None
110 self._originalCurrentLabels = []
112 for path in self._glade_files:
113 if os.path.isfile(path):
114 self._widgetTree = gtk.glade.XML(path)
117 display_error_message("Cannot find dialcentral.glade")
121 self._window = self._widgetTree.get_widget("mainWindow")
122 self._notebook = self._widgetTree.get_widget("notebook")
123 self._errorDisplay = gtk_toolbox.ErrorDisplay(self._widgetTree)
124 self._credentialsDialog = gtk_toolbox.LoginWindow(self._widgetTree)
127 self._isFullScreen = False
128 if hildon is not None:
129 self._app = hildon.Program()
130 oldWindow = self._window
131 self._window = hildon.Window()
132 oldWindow.get_child().reparent(self._window)
133 self._app.add_window(self._window)
136 self._widgetTree.get_widget("usernameentry").set_property('hildon-input-mode', 7)
137 self._widgetTree.get_widget("passwordentry").set_property('hildon-input-mode', 7|(1 << 29))
138 self._widgetTree.get_widget("callbackcombo").get_child().set_property('hildon-input-mode', (1 << 4))
140 warnings.warn(e.message)
141 for scrollingWidget in (
142 'recent_scrolledwindow',
143 'message_scrolledwindow',
144 'contacts_scrolledwindow',
145 "phoneSelectionMessage_scrolledwindow",
146 "phonetypes_scrolledwindow",
147 "smsMessage_scrolledwindow",
148 "smsMessage_scrolledEntry",
150 hildon.hildon_helper_set_thumb_scrollbar(self._widgetTree.get_widget(scrollingWidget), True)
152 gtkMenu = self._widgetTree.get_widget("dialpad_menubar")
154 for child in gtkMenu.get_children():
156 self._window.set_menu(menu)
159 self._window.connect("key-press-event", self._on_key_press)
160 self._window.connect("window-state-event", self._on_window_state_change)
162 pass # warnings.warn("No Hildon", UserWarning, 2)
164 # If under hildon, rely on the application name being shown
166 self._window.set_title("%s" % constants.__pretty_app_name__)
169 "on_dialpad_quit": self._on_close,
171 self._widgetTree.signal_autoconnect(callbackMapping)
173 self._window.connect("destroy", self._on_close)
174 self._window.set_default_size(800, 300)
175 self._window.show_all()
177 self._loginSink = gtk_toolbox.threaded_stage(
180 gtk_toolbox.null_sink(),
184 backgroundSetup = threading.Thread(target=self._idle_setup)
185 backgroundSetup.setDaemon(True)
186 backgroundSetup.start()
188 def _idle_setup(self):
190 If something can be done after the UI loads, push it here so it's not blocking the UI
193 # Barebones UI handlers
197 self._phoneBackends = {self.NULL_BACKEND: null_backend.NullDialer()}
198 with gtk_toolbox.gtk_lock():
199 self._dialpads = {self.NULL_BACKEND: null_views.Dialpad(self._widgetTree)}
200 self._accountViews = {self.NULL_BACKEND: null_views.AccountInfo(self._widgetTree)}
201 self._recentViews = {self.NULL_BACKEND: null_views.RecentCallsView(self._widgetTree)}
202 self._messagesViews = {self.NULL_BACKEND: null_views.MessagesView(self._widgetTree)}
203 self._contactsViews = {self.NULL_BACKEND: null_views.ContactsView(self._widgetTree)}
205 self._dialpads[self._selectedBackendId].enable()
206 self._accountViews[self._selectedBackendId].enable()
207 self._recentViews[self._selectedBackendId].enable()
208 self._messagesViews[self._selectedBackendId].enable()
209 self._contactsViews[self._selectedBackendId].enable()
211 # Setup maemo specifics
218 self._osso = osso.Context(constants.__app_name__, constants.__version__, False)
219 device = osso.DeviceState(self._osso)
220 device.set_device_state_callback(self._on_device_state_change, 0)
222 pass # warnings.warn("No OSSO", UserWarning, 2)
226 self._alarmHandler = alarm_handler.AlarmHandler()
230 with gtk_toolbox.gtk_lock():
231 self._errorDisplay.push_exception()
233 if hildon is not None:
235 self._ledHandler = led_handler.LedHandler()
237 # Setup maemo specifics
242 self._connection = None
243 if conic is not None:
244 self._connection = conic.Connection()
245 self._connection.connect("connection-event", self._on_connection_change, constants.__app_magic__)
246 self._connection.request_connection(conic.CONNECT_FLAG_NONE)
248 pass # warnings.warn("No Internet Connectivity API ", UserWarning)
250 # Setup costly backends
257 os.makedirs(constants._data_path_)
261 gcCookiePath = os.path.join(constants._data_path_, "gc_cookies.txt")
262 gvCookiePath = os.path.join(constants._data_path_, "gv_cookies.txt")
263 self._defaultBackendId = self._guess_preferred_backend((
264 (self.GC_BACKEND, gcCookiePath),
265 (self.GV_BACKEND, gvCookiePath),
268 self._phoneBackends.update({
269 self.GC_BACKEND: gc_backend.GCDialer(gcCookiePath),
270 self.GV_BACKEND: gv_backend.GVDialer(gvCookiePath),
272 with gtk_toolbox.gtk_lock():
273 unifiedDialpad = gc_views.Dialpad(self._widgetTree, self._errorDisplay)
274 unifiedDialpad.set_number("")
275 self._dialpads.update({
276 self.GC_BACKEND: unifiedDialpad,
277 self.GV_BACKEND: unifiedDialpad,
279 self._accountViews.update({
280 self.GC_BACKEND: gc_views.AccountInfo(
281 self._widgetTree, self._phoneBackends[self.GC_BACKEND], None, self._errorDisplay
283 self.GV_BACKEND: gc_views.AccountInfo(
284 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._alarmHandler, self._errorDisplay
287 self._accountViews[self.GC_BACKEND].save_everything = lambda *args: None
288 self._accountViews[self.GV_BACKEND].save_everything = self._save_settings
289 self._recentViews.update({
290 self.GC_BACKEND: gc_views.RecentCallsView(
291 self._widgetTree, self._phoneBackends[self.GC_BACKEND], self._errorDisplay
293 self.GV_BACKEND: gc_views.RecentCallsView(
294 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
297 self._messagesViews.update({
298 self.GC_BACKEND: null_views.MessagesView(self._widgetTree),
299 self.GV_BACKEND: gc_views.MessagesView(
300 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
303 self._contactsViews.update({
304 self.GC_BACKEND: gc_views.ContactsView(
305 self._widgetTree, self._phoneBackends[self.GC_BACKEND], self._errorDisplay
307 self.GV_BACKEND: gc_views.ContactsView(
308 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
312 fsContactsPath = os.path.join(constants._data_path_, "contacts")
313 fileBackend = file_backend.FilesystemAddressBookFactory(fsContactsPath)
314 for backendId in (self.GV_BACKEND, self.GC_BACKEND):
315 self._dialpads[backendId].number_selected = self._select_action
316 self._recentViews[backendId].number_selected = self._select_action
317 self._messagesViews[backendId].number_selected = self._select_action
318 self._contactsViews[backendId].number_selected = self._select_action
321 self._phoneBackends[backendId],
324 mergedBook = gc_views.MergedAddressBook(addressBooks, gc_views.MergedAddressBook.advanced_lastname_sorter)
325 self._contactsViews[backendId].append(mergedBook)
326 self._contactsViews[backendId].extend(addressBooks)
327 self._contactsViews[backendId].open_addressbook(*self._contactsViews[backendId].get_addressbooks().next()[0][0:2])
330 "on_paste": self._on_paste,
331 "on_refresh": self._on_menu_refresh,
332 "on_clearcookies_clicked": self._on_clearcookies_clicked,
333 "on_notebook_switch_page": self._on_notebook_switch_page,
334 "on_about_activate": self._on_about_activate,
336 self._widgetTree.signal_autoconnect(callbackMapping)
338 with gtk_toolbox.gtk_lock():
339 self._originalCurrentLabels = [
340 self._notebook.get_tab_label(self._notebook.get_nth_page(pageIndex)).get_text()
341 for pageIndex in xrange(self._notebook.get_n_pages())
343 self._notebookTapHandler = gtk_toolbox.TapOrHold(self._notebook)
344 self._notebookTapHandler.enable()
345 self._notebookTapHandler.on_tap = self._reset_tab_refresh
346 self._notebookTapHandler.on_hold = self._on_tab_refresh
347 self._notebookTapHandler.on_holding = self._set_tab_refresh
348 self._notebookTapHandler.on_cancel = self._reset_tab_refresh
350 self._initDone = True
352 config = ConfigParser.SafeConfigParser()
353 config.read(constants._user_settings_)
354 with gtk_toolbox.gtk_lock():
355 self.load_settings(config)
357 self._spawn_attempt_login(2)
359 with gtk_toolbox.gtk_lock():
360 self._errorDisplay.push_exception()
362 def attempt_login(self, numOfAttempts = 10, force = False):
364 @todo Handle user notification better like attempting to login and failed login
366 @note This must be run outside of the UI lock
369 assert 0 <= numOfAttempts, "That was pointless having 0 or less login attempts"
370 assert self._initDone, "Attempting login before app is fully loaded"
372 serviceId = self.NULL_BACKEND
376 self.refresh_session()
377 serviceId = self._defaultBackendId
379 except StandardError, e:
380 warnings.warn('Session refresh failed with the following message "%s"' % e.message, UserWarning, 2)
383 loggedIn, serviceId = self._login_by_user(numOfAttempts)
385 with gtk_toolbox.gtk_lock():
386 self._change_loggedin_status(serviceId)
387 except StandardError, e:
388 with gtk_toolbox.gtk_lock():
389 self._errorDisplay.push_exception()
391 def _spawn_attempt_login(self, *args):
392 self._loginSink.send(args)
394 def refresh_session(self):
396 @note Thread agnostic
398 assert self._initDone, "Attempting login before app is fully loaded"
402 loggedIn = self._login_by_cookie()
404 loggedIn = self._login_by_settings()
407 raise RuntimeError("Login Failed")
409 def _login_by_cookie(self):
411 @note Thread agnostic
413 loggedIn = self._phoneBackends[self._defaultBackendId].is_authed()
416 "Logged into %r through cookies" % self._phoneBackends[self._defaultBackendId],
421 def _login_by_settings(self):
423 @note Thread agnostic
425 username, password = self._credentials
426 loggedIn = self._phoneBackends[self._defaultBackendId].login(username, password)
428 self._credentials = username, password
430 "Logged into %r through settings" % self._phoneBackends[self._defaultBackendId],
435 def _login_by_user(self, numOfAttempts):
437 @note This must be run outside of the UI lock
439 loggedIn, (username, password) = False, self._credentials
440 tmpServiceId = self.NULL_BACKEND
441 for attemptCount in xrange(numOfAttempts):
444 availableServices = (
445 (self.GV_BACKEND, "Google Voice"),
446 (self.GC_BACKEND, "Grand Central"),
448 with gtk_toolbox.gtk_lock():
449 credentials = self._credentialsDialog.request_credentials_from(
450 availableServices, defaultCredentials = self._credentials
452 tmpServiceId, username, password = credentials
453 loggedIn = self._phoneBackends[tmpServiceId].login(username, password)
456 serviceId = tmpServiceId
457 self._credentials = username, password
459 "Logged into %r through user request" % self._phoneBackends[serviceId],
463 serviceId = self.NULL_BACKEND
465 return loggedIn, serviceId
467 def _select_action(self, action, number, message):
468 self.refresh_session()
469 if action == "select":
470 self._dialpads[self._selectedBackendId].set_number(number)
471 self._notebook.set_current_page(self.KEYPAD_TAB)
472 elif action == "dial":
473 self._on_dial_clicked(number)
474 elif action == "sms":
475 self._on_sms_clicked(number, message)
477 assert False, "Unknown action: %s" % action
479 def _change_loggedin_status(self, newStatus):
480 oldStatus = self._selectedBackendId
481 if oldStatus == newStatus:
484 self._dialpads[oldStatus].disable()
485 self._accountViews[oldStatus].disable()
486 self._recentViews[oldStatus].disable()
487 self._messagesViews[oldStatus].disable()
488 self._contactsViews[oldStatus].disable()
490 self._dialpads[newStatus].enable()
491 self._accountViews[newStatus].enable()
492 self._recentViews[newStatus].enable()
493 self._messagesViews[newStatus].enable()
494 self._contactsViews[newStatus].enable()
496 if self._phoneBackends[self._selectedBackendId].get_callback_number() is None:
497 self._phoneBackends[self._selectedBackendId].set_sane_callback()
498 self._accountViews[self._selectedBackendId].update()
500 self._selectedBackendId = newStatus
502 def load_settings(self, config):
507 self._defaultBackendId = int(config.get(constants.__pretty_app_name__, "active"))
509 config.get(constants.__pretty_app_name__, "bin_blob_%i" % i)
510 for i in xrange(len(self._credentials))
513 base64.b64decode(blob)
516 self._credentials = tuple(creds)
518 if self._alarmHandler is not None:
519 self._alarmHandler.load_settings(config, "alarm")
520 except ConfigParser.NoOptionError, e:
522 "Settings file %s is missing section %s" % (
523 constants._user_settings_,
528 except ConfigParser.NoSectionError, e:
530 "Settings file %s is missing section %s" % (
531 constants._user_settings_,
537 for backendId, view in itertools.chain(
538 self._dialpads.iteritems(),
539 self._accountViews.iteritems(),
540 self._messagesViews.iteritems(),
541 self._recentViews.iteritems(),
542 self._contactsViews.iteritems(),
544 sectionName = "%s - %s" % (backendId, view.name())
546 view.load_settings(config, sectionName)
547 except ConfigParser.NoOptionError, e:
549 "Settings file %s is missing section %s" % (
550 constants._user_settings_,
555 except ConfigParser.NoSectionError, e:
557 "Settings file %s is missing section %s" % (
558 constants._user_settings_,
564 def save_settings(self, config):
566 @note Thread Agnostic
568 config.add_section(constants.__pretty_app_name__)
569 config.set(constants.__pretty_app_name__, "active", str(self._selectedBackendId))
570 for i, value in enumerate(self._credentials):
571 blob = base64.b64encode(value)
572 config.set(constants.__pretty_app_name__, "bin_blob_%i" % i, blob)
573 config.add_section("alarm")
574 if self._alarmHandler is not None:
575 self._alarmHandler.save_settings(config, "alarm")
577 for backendId, view in itertools.chain(
578 self._dialpads.iteritems(),
579 self._accountViews.iteritems(),
580 self._messagesViews.iteritems(),
581 self._recentViews.iteritems(),
582 self._contactsViews.iteritems(),
584 sectionName = "%s - %s" % (backendId, view.name())
585 config.add_section(sectionName)
586 view.save_settings(config, sectionName)
588 def _guess_preferred_backend(self, backendAndCookiePaths):
590 (getmtime_nothrow(path), backendId, path)
591 for backendId, path in backendAndCookiePaths
593 modTimeAndPath.sort()
594 return modTimeAndPath[-1][1]
596 def _save_settings(self):
598 @note Thread Agnostic
600 config = ConfigParser.SafeConfigParser()
601 self.save_settings(config)
602 with open(constants._user_settings_, "wb") as configFile:
603 config.write(configFile)
605 def _refresh_active_tab(self):
606 pageIndex = self._notebook.get_current_page()
607 if pageIndex == self.CONTACTS_TAB:
608 self._contactsViews[self._selectedBackendId].update(force=True)
609 elif pageIndex == self.RECENT_TAB:
610 self._recentViews[self._selectedBackendId].update(force=True)
611 elif pageIndex == self.MESSAGES_TAB:
612 self._messagesViews[self._selectedBackendId].update(force=True)
614 if pageIndex in (self.RECENT_TAB, self.MESSAGES_TAB):
615 if self._ledHandler is not None:
616 self._ledHandler.off()
618 def _on_close(self, *args, **kwds):
620 if self._osso is not None:
624 self._save_settings()
628 def _on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
630 For shutdown or save_unsaved_data, our only state is cookies and I think the cookie manager handles that for us.
631 For system_inactivity, we have no background tasks to pause
633 @note Hildon specific
636 for backendId in self.BACKENDS:
637 self._phoneBackends[backendId].clear_caches()
638 self._contactsViews[self._selectedBackendId].clear_caches()
641 if save_unsaved_data or shutdown:
642 self._save_settings()
644 def _on_connection_change(self, connection, event, magicIdentifier):
646 @note Hildon specific
650 status = event.get_status()
651 error = event.get_error()
652 iap_id = event.get_iap_id()
653 bearer = event.get_bearer_type()
655 if status == conic.STATUS_CONNECTED:
657 self._spawn_attempt_login(2)
658 elif status == conic.STATUS_DISCONNECTED:
660 self._defaultBackendId = self._selectedBackendId
661 self._change_loggedin_status(self.NULL_BACKEND)
663 def _on_window_state_change(self, widget, event, *args):
665 @note Hildon specific
667 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
668 self._isFullScreen = True
670 self._isFullScreen = False
672 def _on_key_press(self, widget, event, *args):
674 @note Hildon specific
676 if event.keyval == gtk.keysyms.F6:
677 if self._isFullScreen:
678 self._window.unfullscreen()
680 self._window.fullscreen()
682 def _on_clearcookies_clicked(self, *args):
683 self._phoneBackends[self._selectedBackendId].logout()
684 self._accountViews[self._selectedBackendId].clear()
685 self._recentViews[self._selectedBackendId].clear()
686 self._messagesViews[self._selectedBackendId].clear()
687 self._contactsViews[self._selectedBackendId].clear()
688 self._change_loggedin_status(self.NULL_BACKEND)
690 self._spawn_attempt_login(2, True)
692 def _on_notebook_switch_page(self, notebook, page, pageIndex):
693 self._reset_tab_refresh()
695 didRecentUpdate = False
696 didMessagesUpdate = False
698 if pageIndex == self.RECENT_TAB:
699 didRecentUpdate = self._recentViews[self._selectedBackendId].update()
700 elif pageIndex == self.MESSAGES_TAB:
701 didMessagesUpdate = self._messagesViews[self._selectedBackendId].update()
702 elif pageIndex == self.CONTACTS_TAB:
703 self._contactsViews[self._selectedBackendId].update()
704 elif pageIndex == self.ACCOUNT_TAB:
705 self._accountViews[self._selectedBackendId].update()
707 if didRecentUpdate or didMessagesUpdate:
708 if self._ledHandler is not None:
709 self._ledHandler.off()
711 def _set_tab_refresh(self, *args):
712 pageIndex = self._notebook.get_current_page()
713 child = self._notebook.get_nth_page(pageIndex)
714 self._notebook.get_tab_label(child).set_text("Refresh?")
717 def _reset_tab_refresh(self, *args):
718 pageIndex = self._notebook.get_current_page()
719 child = self._notebook.get_nth_page(pageIndex)
720 self._notebook.get_tab_label(child).set_text(self._originalCurrentLabels[pageIndex])
723 def _on_tab_refresh(self, *args):
724 self._refresh_active_tab()
725 self._reset_tab_refresh()
728 def _on_sms_clicked(self, number, message):
729 assert number, "No number specified"
730 assert message, "Empty message"
732 loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
733 except StandardError, e:
735 self._errorDisplay.push_exception()
739 self._errorDisplay.push_message(
740 "Backend link with grandcentral is not working, please try again"
746 self._phoneBackends[self._selectedBackendId].send_sms(number, message)
748 except StandardError, e:
749 self._errorDisplay.push_exception()
750 except ValueError, e:
751 self._errorDisplay.push_exception()
754 self._dialpads[self._selectedBackendId].clear()
756 def _on_dial_clicked(self, number):
757 assert number, "No number to call"
759 loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
760 except StandardError, e:
762 self._errorDisplay.push_exception()
766 self._errorDisplay.push_message(
767 "Backend link with grandcentral is not working, please try again"
773 assert self._phoneBackends[self._selectedBackendId].get_callback_number() != "", "No callback number specified"
774 self._phoneBackends[self._selectedBackendId].dial(number)
776 except StandardError, e:
777 self._errorDisplay.push_exception()
778 except ValueError, e:
779 self._errorDisplay.push_exception()
782 self._dialpads[self._selectedBackendId].clear()
784 def _on_menu_refresh(self, *args):
785 self._refresh_active_tab()
787 def _on_paste(self, *args):
788 contents = self._clipboard.wait_for_text()
789 self._dialpads[self._selectedBackendId].set_number(contents)
791 def _on_about_activate(self, *args):
792 dlg = gtk.AboutDialog()
793 dlg.set_name(constants.__pretty_app_name__)
794 dlg.set_version(constants.__version__)
795 dlg.set_copyright("Copyright 2008 - LGPL")
796 dlg.set_comments("Dialcentral is a touch screen enhanced interface to your GoogleVoice/Grandcentral account. This application is not affiliated with Google in any way")
797 dlg.set_website("http://gc-dialer.garage.maemo.org/")
798 dlg.set_authors(["<z2n@merctech.com>", "Eric Warnke <ericew@gmail.com>", "Ed Page <edpage@byu.net>"])
806 failureCount, testCount = doctest.testmod()
808 print "Tests Successful"
815 _lock_file = os.path.join(constants._data_path_, ".lock")
816 #with gtk_toolbox.flock(_lock_file, 0):
817 gtk.gdk.threads_init()
819 if hildon is not None:
820 gtk.set_application_name(constants.__pretty_app_name__)
821 handle = Dialcentral()
825 class DummyOptions(object):
831 if __name__ == "__main__":
832 if len(sys.argv) > 1:
838 if optparse is not None:
839 parser = optparse.OptionParser()
840 parser.add_option("-t", "--test", action="store_true", dest="test", help="Run tests")
841 (commandOptions, commandArgs) = parser.parse_args()
843 commandOptions = DummyOptions()
846 if commandOptions.test: