4 DialCentral - Front end for Google's GoogleVoice 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 Add "login failed" and "attempting login" notifications
25 from __future__ import with_statement
44 def getmtime_nothrow(path):
46 return os.path.getmtime(path)
51 def display_error_message(msg):
52 error_dialog = gtk.MessageDialog(None, 0, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, msg)
54 def close(dialog, response):
56 error_dialog.connect("response", close)
60 class Dialcentral(object):
63 os.path.join(os.path.dirname(__file__), "dialcentral.glade"),
64 os.path.join(os.path.dirname(__file__), "../lib/dialcentral.glade"),
65 '/usr/lib/dialcentral/dialcentral.glade',
76 BACKENDS = (NULL_BACKEND, GV_BACKEND)
79 self._initDone = False
80 self._connection = None
82 self._clipboard = gtk.clipboard_get()
84 self._credentials = ("", "")
85 self._selectedBackendId = self.NULL_BACKEND
86 self._defaultBackendId = self.GV_BACKEND
87 self._phoneBackends = None
89 self._accountViews = None
90 self._messagesViews = None
91 self._recentViews = None
92 self._contactsViews = None
93 self._alarmHandler = None
94 self._ledHandler = None
95 self._originalCurrentLabels = []
97 for path in self._glade_files:
98 if os.path.isfile(path):
99 self._widgetTree = gtk.glade.XML(path)
102 display_error_message("Cannot find dialcentral.glade")
106 self._window = self._widgetTree.get_widget("mainWindow")
107 self._notebook = self._widgetTree.get_widget("notebook")
108 self._errorDisplay = gtk_toolbox.ErrorDisplay(self._widgetTree)
109 self._credentialsDialog = gtk_toolbox.LoginWindow(self._widgetTree)
111 self._isFullScreen = False
112 self._app = hildonize.get_app_class()()
113 self._window = hildonize.hildonize_window(self._app, self._window)
114 hildonize.hildonize_text_entry(self._widgetTree.get_widget("usernameentry"))
115 hildonize.hildonize_password_entry(self._widgetTree.get_widget("passwordentry"))
117 for scrollingWidget in (
118 'recent_scrolledwindow',
119 'message_scrolledwindow',
120 'contacts_scrolledwindow',
121 "phoneSelectionMessages_scrolledwindow",
122 "smsMessages_scrolledwindow",
124 hildonize.hildonize_scrollwindow(self._widgetTree.get_widget(scrollingWidget))
125 for scrollingWidget in (
126 "phonetypes_scrolledwindow",
127 "smsMessage_scrolledEntry",
129 hildonize.hildonize_scrollwindow_with_viewport(self._widgetTree.get_widget(scrollingWidget))
131 replacementButtons = [gtk.Button("Test")]
132 menu = hildonize.hildonize_menu(
134 self._widgetTree.get_widget("dialpad_menubar"),
138 self._window.connect("key-press-event", self._on_key_press)
139 self._window.connect("window-state-event", self._on_window_state_change)
140 if not hildonize.IS_HILDON_SUPPORTED:
141 logging.warning("No hildonization support")
143 hildonize.set_application_title(self._window, "%s" % constants.__pretty_app_name__)
145 self._window.connect("destroy", self._on_close)
146 self._window.set_default_size(800, 300)
147 self._window.show_all()
149 self._loginSink = gtk_toolbox.threaded_stage(
152 gtk_toolbox.null_sink(),
156 backgroundSetup = threading.Thread(target=self._idle_setup)
157 backgroundSetup.setDaemon(True)
158 backgroundSetup.start()
160 def _idle_setup(self):
162 If something can be done after the UI loads, push it here so it's not blocking the UI
164 # Barebones UI handlers
169 self._phoneBackends = {self.NULL_BACKEND: null_backend.NullDialer()}
170 with gtk_toolbox.gtk_lock():
171 self._dialpads = {self.NULL_BACKEND: null_views.Dialpad(self._widgetTree)}
172 self._accountViews = {self.NULL_BACKEND: null_views.AccountInfo(self._widgetTree)}
173 self._recentViews = {self.NULL_BACKEND: null_views.RecentCallsView(self._widgetTree)}
174 self._messagesViews = {self.NULL_BACKEND: null_views.MessagesView(self._widgetTree)}
175 self._contactsViews = {self.NULL_BACKEND: null_views.ContactsView(self._widgetTree)}
177 self._dialpads[self._selectedBackendId].enable()
178 self._accountViews[self._selectedBackendId].enable()
179 self._recentViews[self._selectedBackendId].enable()
180 self._messagesViews[self._selectedBackendId].enable()
181 self._contactsViews[self._selectedBackendId].enable()
183 with gtk_toolbox.gtk_lock():
184 self._errorDisplay.push_exception()
186 # Setup maemo specifics
190 except (ImportError, OSError):
194 self._osso = osso.Context(constants.__app_name__, constants.__version__, False)
195 device = osso.DeviceState(self._osso)
196 device.set_device_state_callback(self._on_device_state_change, 0)
198 logging.warning("No device state support")
202 self._alarmHandler = alarm_handler.AlarmHandler()
203 except (ImportError, OSError):
206 with gtk_toolbox.gtk_lock():
207 self._errorDisplay.push_exception()
209 logging.warning("No notification support")
210 if hildonize.IS_HILDON_SUPPORTED:
213 self._ledHandler = led_handler.LedHandler()
215 logging.exception('LED Handling failed: "%s"' % str(e))
216 self._ledHandler = None
218 self._ledHandler = None
222 except (ImportError, OSError):
224 self._connection = None
225 if conic is not None:
226 self._connection = conic.Connection()
227 self._connection.connect("connection-event", self._on_connection_change, constants.__app_magic__)
228 self._connection.request_connection(conic.CONNECT_FLAG_NONE)
230 logging.warning("No connection support")
232 with gtk_toolbox.gtk_lock():
233 self._errorDisplay.push_exception()
235 # Setup costly backends
242 os.makedirs(constants._data_path_)
246 gvCookiePath = os.path.join(constants._data_path_, "gv_cookies.txt")
248 self._phoneBackends.update({
249 self.GV_BACKEND: gv_backend.GVDialer(gvCookiePath),
251 with gtk_toolbox.gtk_lock():
252 unifiedDialpad = gv_views.Dialpad(self._widgetTree, self._errorDisplay)
253 self._dialpads.update({
254 self.GV_BACKEND: unifiedDialpad,
256 self._accountViews.update({
257 self.GV_BACKEND: gv_views.AccountInfo(
258 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._alarmHandler, self._errorDisplay
261 self._accountViews[self.GV_BACKEND].save_everything = self._save_settings
262 self._recentViews.update({
263 self.GV_BACKEND: gv_views.RecentCallsView(
264 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
267 self._messagesViews.update({
268 self.GV_BACKEND: gv_views.MessagesView(
269 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
272 self._contactsViews.update({
273 self.GV_BACKEND: gv_views.ContactsView(
274 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
278 fsContactsPath = os.path.join(constants._data_path_, "contacts")
279 fileBackend = file_backend.FilesystemAddressBookFactory(fsContactsPath)
281 self._dialpads[self.GV_BACKEND].number_selected = self._select_action
282 self._recentViews[self.GV_BACKEND].number_selected = self._select_action
283 self._messagesViews[self.GV_BACKEND].number_selected = self._select_action
284 self._contactsViews[self.GV_BACKEND].number_selected = self._select_action
287 self._phoneBackends[self.GV_BACKEND],
290 mergedBook = gv_views.MergedAddressBook(addressBooks, gv_views.MergedAddressBook.advanced_lastname_sorter)
291 self._contactsViews[self.GV_BACKEND].append(mergedBook)
292 self._contactsViews[self.GV_BACKEND].extend(addressBooks)
293 self._contactsViews[self.GV_BACKEND].open_addressbook(*self._contactsViews[self.GV_BACKEND].get_addressbooks().next()[0][0:2])
296 "on_paste": self._on_paste,
297 "on_refresh": self._on_menu_refresh,
298 "on_clearcookies_clicked": self._on_clearcookies_clicked,
299 "on_about_activate": self._on_about_activate,
301 if hildonize.GTK_MENU_USED:
302 self._widgetTree.signal_autoconnect(callbackMapping)
303 self._notebook.connect("switch-page", self._on_notebook_switch_page)
304 self._widgetTree.get_widget("clearcookies").connect("clicked", self._on_clearcookies_clicked)
306 with gtk_toolbox.gtk_lock():
307 self._originalCurrentLabels = [
308 self._notebook.get_tab_label(self._notebook.get_nth_page(pageIndex)).get_text()
309 for pageIndex in xrange(self._notebook.get_n_pages())
311 self._notebookTapHandler = gtk_toolbox.TapOrHold(self._notebook)
312 self._notebookTapHandler.enable()
313 self._notebookTapHandler.on_tap = self._reset_tab_refresh
314 self._notebookTapHandler.on_hold = self._on_tab_refresh
315 self._notebookTapHandler.on_holding = self._set_tab_refresh
316 self._notebookTapHandler.on_cancel = self._reset_tab_refresh
318 self._initDone = True
320 config = ConfigParser.SafeConfigParser()
321 config.read(constants._user_settings_)
322 with gtk_toolbox.gtk_lock():
323 self.load_settings(config)
325 with gtk_toolbox.gtk_lock():
326 self._errorDisplay.push_exception()
328 self._spawn_attempt_login(2)
330 def _spawn_attempt_login(self, *args):
331 self._loginSink.send(args)
333 def _attempt_login(self, numOfAttempts = 10, force = False):
335 @note This must be run outside of the UI lock
338 assert 0 <= numOfAttempts, "That was pointless having 0 or less login attempts"
339 assert self._initDone, "Attempting login before app is fully loaded"
341 serviceId = self.NULL_BACKEND
345 self.refresh_session()
346 serviceId = self._defaultBackendId
349 logging.exception('Session refresh failed with the following message "%s"' % str(e))
352 loggedIn, serviceId = self._login_by_user(numOfAttempts)
354 with gtk_toolbox.gtk_lock():
355 self._change_loggedin_status(serviceId)
357 hildonize.show_information_banner(self._window, "Logged In")
359 with gtk_toolbox.gtk_lock():
360 self._errorDisplay.push_exception()
362 def refresh_session(self):
364 @note Thread agnostic
366 assert self._initDone, "Attempting login before app is fully loaded"
370 loggedIn = self._login_by_cookie()
372 loggedIn = self._login_by_settings()
375 raise RuntimeError("Login Failed")
377 def _login_by_cookie(self):
379 @note Thread agnostic
381 loggedIn = self._phoneBackends[self._defaultBackendId].is_authed()
383 logging.info("Logged into %r through cookies" % self._phoneBackends[self._defaultBackendId])
386 def _login_by_settings(self):
388 @note Thread agnostic
390 username, password = self._credentials
391 loggedIn = self._phoneBackends[self._defaultBackendId].login(username, password)
393 self._credentials = username, password
394 logging.info("Logged into %r through settings" % self._phoneBackends[self._defaultBackendId])
397 def _login_by_user(self, numOfAttempts):
399 @note This must be run outside of the UI lock
401 loggedIn, (username, password) = False, self._credentials
402 tmpServiceId = self.GV_BACKEND
403 for attemptCount in xrange(numOfAttempts):
406 with gtk_toolbox.gtk_lock():
407 credentials = self._credentialsDialog.request_credentials(
408 defaultCredentials = self._credentials
410 if not self._phoneBackends[tmpServiceId].get_callback_number():
411 # subtle reminder to the users to configure things
412 self._notebook.set_current_page(self.ACCOUNT_TAB)
413 username, password = credentials
414 loggedIn = self._phoneBackends[tmpServiceId].login(username, password)
417 serviceId = tmpServiceId
418 self._credentials = username, password
419 logging.info("Logged into %r through user request" % self._phoneBackends[serviceId])
421 serviceId = self.NULL_BACKEND
422 self._notebook.set_current_page(self.ACCOUNT_TAB)
424 return loggedIn, serviceId
426 def _select_action(self, action, number, message):
427 self.refresh_session()
428 if action == "select":
429 self._dialpads[self._selectedBackendId].set_number(number)
430 self._notebook.set_current_page(self.KEYPAD_TAB)
431 elif action == "dial":
432 self._on_dial_clicked(number)
433 elif action == "sms":
434 self._on_sms_clicked(number, message)
436 assert False, "Unknown action: %s" % action
438 def _change_loggedin_status(self, newStatus):
439 oldStatus = self._selectedBackendId
440 if oldStatus == newStatus:
443 self._dialpads[oldStatus].disable()
444 self._accountViews[oldStatus].disable()
445 self._recentViews[oldStatus].disable()
446 self._messagesViews[oldStatus].disable()
447 self._contactsViews[oldStatus].disable()
449 self._dialpads[newStatus].enable()
450 self._accountViews[newStatus].enable()
451 self._recentViews[newStatus].enable()
452 self._messagesViews[newStatus].enable()
453 self._contactsViews[newStatus].enable()
455 if self._phoneBackends[self._selectedBackendId].get_callback_number() is None:
456 self._phoneBackends[self._selectedBackendId].set_sane_callback()
458 self._selectedBackendId = newStatus
460 self._accountViews[self._selectedBackendId].update()
461 self._refresh_active_tab()
463 def load_settings(self, config):
468 self._defaultBackendId = config.getint(constants.__pretty_app_name__, "active")
470 config.get(constants.__pretty_app_name__, "bin_blob_%i" % i)
471 for i in xrange(len(self._credentials))
474 base64.b64decode(blob)
477 self._credentials = tuple(creds)
479 if self._alarmHandler is not None:
480 self._alarmHandler.load_settings(config, "alarm")
481 except ConfigParser.NoOptionError, e:
483 "Settings file %s is missing section %s" % (
484 constants._user_settings_,
488 except ConfigParser.NoSectionError, e:
490 "Settings file %s is missing section %s" % (
491 constants._user_settings_,
496 for backendId, view in itertools.chain(
497 self._dialpads.iteritems(),
498 self._accountViews.iteritems(),
499 self._messagesViews.iteritems(),
500 self._recentViews.iteritems(),
501 self._contactsViews.iteritems(),
503 sectionName = "%s - %s" % (backendId, view.name())
505 view.load_settings(config, sectionName)
506 except ConfigParser.NoOptionError, e:
508 "Settings file %s is missing section %s" % (
509 constants._user_settings_,
513 except ConfigParser.NoSectionError, e:
515 "Settings file %s is missing section %s" % (
516 constants._user_settings_,
522 previousOrientation = config.getint(constants.__pretty_app_name__, "orientation")
523 if previousOrientation == gtk.ORIENTATION_HORIZONTAL:
524 hildonize.window_to_landscape(self._window)
525 elif previousOrientation == gtk.ORIENTATION_VERTICAL:
526 hildonize.window_to_portrait(self._window)
527 except ConfigParser.NoOptionError, e:
529 "Settings file %s is missing section %s" % (
530 constants._user_settings_,
534 except ConfigParser.NoSectionError, e:
536 "Settings file %s is missing section %s" % (
537 constants._user_settings_,
542 def save_settings(self, config):
544 @note Thread Agnostic
546 config.add_section(constants.__pretty_app_name__)
547 config.set(constants.__pretty_app_name__, "active", str(self._selectedBackendId))
548 config.set(constants.__pretty_app_name__, "orientation", str(int(gtk_toolbox.get_screen_orientation())))
549 for i, value in enumerate(self._credentials):
550 blob = base64.b64encode(value)
551 config.set(constants.__pretty_app_name__, "bin_blob_%i" % i, blob)
552 config.add_section("alarm")
553 if self._alarmHandler is not None:
554 self._alarmHandler.save_settings(config, "alarm")
556 for backendId, view in itertools.chain(
557 self._dialpads.iteritems(),
558 self._accountViews.iteritems(),
559 self._messagesViews.iteritems(),
560 self._recentViews.iteritems(),
561 self._contactsViews.iteritems(),
563 sectionName = "%s - %s" % (backendId, view.name())
564 config.add_section(sectionName)
565 view.save_settings(config, sectionName)
567 def _save_settings(self):
569 @note Thread Agnostic
571 config = ConfigParser.SafeConfigParser()
572 self.save_settings(config)
573 with open(constants._user_settings_, "wb") as configFile:
574 config.write(configFile)
576 def _refresh_active_tab(self):
577 pageIndex = self._notebook.get_current_page()
578 if pageIndex == self.CONTACTS_TAB:
579 self._contactsViews[self._selectedBackendId].update(force=True)
580 elif pageIndex == self.RECENT_TAB:
581 self._recentViews[self._selectedBackendId].update(force=True)
582 elif pageIndex == self.MESSAGES_TAB:
583 self._messagesViews[self._selectedBackendId].update(force=True)
585 if pageIndex in (self.RECENT_TAB, self.MESSAGES_TAB):
586 if self._ledHandler is not None:
587 self._ledHandler.off()
589 def _on_close(self, *args, **kwds):
591 if self._osso is not None:
595 self._save_settings()
599 def _on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
601 For shutdown or save_unsaved_data, our only state is cookies and I think the cookie manager handles that for us.
602 For system_inactivity, we have no background tasks to pause
604 @note Hildon specific
608 for backendId in self.BACKENDS:
609 self._phoneBackends[backendId].clear_caches()
610 self._contactsViews[self._selectedBackendId].clear_caches()
613 if save_unsaved_data or shutdown:
614 self._save_settings()
616 self._errorDisplay.push_exception()
618 def _on_connection_change(self, connection, event, magicIdentifier):
620 @note Hildon specific
625 status = event.get_status()
626 error = event.get_error()
627 iap_id = event.get_iap_id()
628 bearer = event.get_bearer_type()
630 if status == conic.STATUS_CONNECTED:
632 self._spawn_attempt_login(2)
633 elif status == conic.STATUS_DISCONNECTED:
635 self._defaultBackendId = self._selectedBackendId
636 self._change_loggedin_status(self.NULL_BACKEND)
638 self._errorDisplay.push_exception()
640 def _on_window_state_change(self, widget, event, *args):
642 @note Hildon specific
645 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
646 self._isFullScreen = True
648 self._isFullScreen = False
650 self._errorDisplay.push_exception()
652 def _on_key_press(self, widget, event, *args):
654 @note Hildon specific
658 event.keyval == gtk.keysyms.F6 or
659 event.keyval == gtk.keysyms.Return and event.get_state() & gtk.gdk.CONTROL_MASK
661 if self._isFullScreen:
662 self._window.unfullscreen()
664 self._window.fullscreen()
666 self._errorDisplay.push_exception()
668 def _on_clearcookies_clicked(self, *args):
670 self._phoneBackends[self._selectedBackendId].logout()
671 self._accountViews[self._selectedBackendId].clear()
672 self._recentViews[self._selectedBackendId].clear()
673 self._messagesViews[self._selectedBackendId].clear()
674 self._contactsViews[self._selectedBackendId].clear()
675 self._change_loggedin_status(self.NULL_BACKEND)
677 self._spawn_attempt_login(2, True)
679 self._errorDisplay.push_exception()
681 def _on_notebook_switch_page(self, notebook, page, pageIndex):
683 self._reset_tab_refresh()
685 didRecentUpdate = False
686 didMessagesUpdate = False
688 if pageIndex == self.RECENT_TAB:
689 didRecentUpdate = self._recentViews[self._selectedBackendId].update()
690 elif pageIndex == self.MESSAGES_TAB:
691 didMessagesUpdate = self._messagesViews[self._selectedBackendId].update()
692 elif pageIndex == self.CONTACTS_TAB:
693 self._contactsViews[self._selectedBackendId].update()
694 elif pageIndex == self.ACCOUNT_TAB:
695 self._accountViews[self._selectedBackendId].update()
697 if didRecentUpdate or didMessagesUpdate:
698 if self._ledHandler is not None:
699 self._ledHandler.off()
701 self._errorDisplay.push_exception()
703 def _set_tab_refresh(self, *args):
705 pageIndex = self._notebook.get_current_page()
706 child = self._notebook.get_nth_page(pageIndex)
707 self._notebook.get_tab_label(child).set_text("Refresh?")
709 self._errorDisplay.push_exception()
712 def _reset_tab_refresh(self, *args):
714 pageIndex = self._notebook.get_current_page()
715 child = self._notebook.get_nth_page(pageIndex)
716 self._notebook.get_tab_label(child).set_text(self._originalCurrentLabels[pageIndex])
718 self._errorDisplay.push_exception()
721 def _on_tab_refresh(self, *args):
723 self._refresh_active_tab()
724 self._reset_tab_refresh()
726 self._errorDisplay.push_exception()
729 def _on_sms_clicked(self, number, message):
731 assert number, "No number specified"
732 assert message, "Empty message"
734 loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
737 self._errorDisplay.push_exception()
741 self._errorDisplay.push_message(
742 "Backend link with GoogleVoice is not working, please try again"
748 self._phoneBackends[self._selectedBackendId].send_sms(number, message)
749 hildonize.show_information_banner(self._window, "Sending to %s" % number)
752 self._errorDisplay.push_exception()
755 self._dialpads[self._selectedBackendId].clear()
757 self._errorDisplay.push_exception()
759 def _on_dial_clicked(self, number):
761 assert number, "No number to call"
763 loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
766 self._errorDisplay.push_exception()
770 self._errorDisplay.push_message(
771 "Backend link with GoogleVoice is not working, please try again"
777 assert self._phoneBackends[self._selectedBackendId].get_callback_number() != "", "No callback number specified"
778 self._phoneBackends[self._selectedBackendId].dial(number)
779 hildonize.show_information_banner(self._window, "Calling %s" % number)
782 self._errorDisplay.push_exception()
785 self._dialpads[self._selectedBackendId].clear()
787 self._errorDisplay.push_exception()
789 def _on_menu_refresh(self, *args):
791 self._refresh_active_tab()
793 self._errorDisplay.push_exception()
795 def _on_paste(self, *args):
797 contents = self._clipboard.wait_for_text()
798 if contents is not None:
799 self._dialpads[self._selectedBackendId].set_number(contents)
801 self._errorDisplay.push_exception()
803 def _on_about_activate(self, *args):
805 dlg = gtk.AboutDialog()
806 dlg.set_name(constants.__pretty_app_name__)
807 dlg.set_version("%s-%d" % (constants.__version__, constants.__build__))
808 dlg.set_copyright("Copyright 2008 - LGPL")
809 dlg.set_comments("Dialcentral is a touch screen enhanced interface to your GoogleVoice account. This application is not affiliated with Google in any way")
810 dlg.set_website("http://gc-dialer.garage.maemo.org/")
811 dlg.set_authors(["<z2n@merctech.com>", "Eric Warnke <ericew@gmail.com>", "Ed Page <edpage@byu.net>"])
815 self._errorDisplay.push_exception()
821 failureCount, testCount = doctest.testmod()
823 print "Tests Successful"
830 _lock_file = os.path.join(constants._data_path_, ".lock")
831 #with gtk_toolbox.flock(_lock_file, 0):
832 gtk.gdk.threads_init()
834 if hildonize.IS_HILDON_SUPPORTED:
835 gtk.set_application_name(constants.__pretty_app_name__)
836 handle = Dialcentral()
840 class DummyOptions(object):
846 if __name__ == "__main__":
847 logging.basicConfig(level=logging.DEBUG)
849 if len(sys.argv) > 1:
855 if optparse is not None:
856 parser = optparse.OptionParser()
857 parser.add_option("-t", "--test", action="store_true", dest="test", help="Run tests")
858 (commandOptions, commandArgs) = parser.parse_args()
860 commandOptions = DummyOptions()
863 if commandOptions.test: