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
23 from __future__ import with_statement
42 def getmtime_nothrow(path):
44 return os.path.getmtime(path)
49 def display_error_message(msg):
50 error_dialog = gtk.MessageDialog(None, 0, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, msg)
52 def close(dialog, response):
54 error_dialog.connect("response", close)
58 class Dialcentral(object):
61 os.path.join(os.path.dirname(__file__), "dialcentral.glade"),
62 os.path.join(os.path.dirname(__file__), "../lib/dialcentral.glade"),
63 '/usr/lib/dialcentral/dialcentral.glade',
74 BACKENDS = (NULL_BACKEND, GV_BACKEND)
77 self._initDone = False
78 self._connection = None
80 self._clipboard = gtk.clipboard_get()
82 self._credentials = ("", "")
83 self._selectedBackendId = self.NULL_BACKEND
84 self._defaultBackendId = self.GV_BACKEND
85 self._phoneBackends = None
87 self._accountViews = None
88 self._messagesViews = None
89 self._recentViews = None
90 self._contactsViews = None
91 self._alarmHandler = None
92 self._ledHandler = None
93 self._originalCurrentLabels = []
95 for path in self._glade_files:
96 if os.path.isfile(path):
97 self._widgetTree = gtk.glade.XML(path)
100 display_error_message("Cannot find dialcentral.glade")
104 self._window = self._widgetTree.get_widget("mainWindow")
105 self._notebook = self._widgetTree.get_widget("notebook")
106 self._errorDisplay = gtk_toolbox.ErrorDisplay(self._widgetTree)
107 self._credentialsDialog = gtk_toolbox.LoginWindow(self._widgetTree)
109 self._isFullScreen = False
110 self._app = hildonize.get_app_class()()
111 self._window = hildonize.hildonize_window(self._app, self._window)
112 hildonize.hildonize_text_entry(self._widgetTree.get_widget("usernameentry"))
113 hildonize.hildonize_password_entry(self._widgetTree.get_widget("passwordentry"))
114 hildonize.hildonize_combo_entry(self._widgetTree.get_widget("callbackcombo").get_child())
116 for scrollingWidget in (
117 'recent_scrolledwindow',
118 'message_scrolledwindow',
119 'contacts_scrolledwindow',
120 "phoneSelectionMessages_scrolledwindow",
121 "smsMessages_scrolledwindow",
123 hildonize.hildonize_scrollwindow(self._widgetTree.get_widget(scrollingWidget))
124 for scrollingWidget in (
125 "phonetypes_scrolledwindow",
126 "smsMessage_scrolledEntry",
128 hildonize.hildonize_scrollwindow_with_viewport(self._widgetTree.get_widget(scrollingWidget))
130 replacementButtons = [gtk.Button("Test")]
131 menu = hildonize.hildonize_menu(
133 self._widgetTree.get_widget("dialpad_menubar"),
137 self._window.connect("key-press-event", self._on_key_press)
138 self._window.connect("window-state-event", self._on_window_state_change)
139 if not hildonize.IS_HILDON_SUPPORTED:
140 logging.warning("No hildonization support")
142 hildonize.set_application_title(self._window, "%s" % constants.__pretty_app_name__)
144 self._window.connect("destroy", self._on_close)
145 self._window.set_default_size(800, 300)
146 self._window.show_all()
148 self._loginSink = gtk_toolbox.threaded_stage(
151 gtk_toolbox.null_sink(),
155 backgroundSetup = threading.Thread(target=self._idle_setup)
156 backgroundSetup.setDaemon(True)
157 backgroundSetup.start()
159 def _idle_setup(self):
161 If something can be done after the UI loads, push it here so it's not blocking the UI
163 # Barebones UI handlers
168 self._phoneBackends = {self.NULL_BACKEND: null_backend.NullDialer()}
169 with gtk_toolbox.gtk_lock():
170 self._dialpads = {self.NULL_BACKEND: null_views.Dialpad(self._widgetTree)}
171 self._accountViews = {self.NULL_BACKEND: null_views.AccountInfo(self._widgetTree)}
172 self._recentViews = {self.NULL_BACKEND: null_views.RecentCallsView(self._widgetTree)}
173 self._messagesViews = {self.NULL_BACKEND: null_views.MessagesView(self._widgetTree)}
174 self._contactsViews = {self.NULL_BACKEND: null_views.ContactsView(self._widgetTree)}
176 self._dialpads[self._selectedBackendId].enable()
177 self._accountViews[self._selectedBackendId].enable()
178 self._recentViews[self._selectedBackendId].enable()
179 self._messagesViews[self._selectedBackendId].enable()
180 self._contactsViews[self._selectedBackendId].enable()
182 with gtk_toolbox.gtk_lock():
183 self._errorDisplay.push_exception()
185 # Setup maemo specifics
189 except (ImportError, OSError):
193 self._osso = osso.Context(constants.__app_name__, constants.__version__, False)
194 device = osso.DeviceState(self._osso)
195 device.set_device_state_callback(self._on_device_state_change, 0)
197 logging.warning("No device state support")
201 self._alarmHandler = alarm_handler.AlarmHandler()
202 except (ImportError, OSError):
205 with gtk_toolbox.gtk_lock():
206 self._errorDisplay.push_exception()
208 logging.warning("No notification support")
209 if hildonize.IS_HILDON_SUPPORTED:
212 self._ledHandler = led_handler.LedHandler()
214 logging.exception('LED Handling failed: "%s"' % str(e))
215 self._ledHandler = None
217 self._ledHandler = None
221 except (ImportError, OSError):
223 self._connection = None
224 if conic is not None:
225 self._connection = conic.Connection()
226 self._connection.connect("connection-event", self._on_connection_change, constants.__app_magic__)
227 self._connection.request_connection(conic.CONNECT_FLAG_NONE)
229 logging.warning("No connection support")
231 with gtk_toolbox.gtk_lock():
232 self._errorDisplay.push_exception()
234 # Setup costly backends
241 os.makedirs(constants._data_path_)
245 gvCookiePath = os.path.join(constants._data_path_, "gv_cookies.txt")
247 self._phoneBackends.update({
248 self.GV_BACKEND: gv_backend.GVDialer(gvCookiePath),
250 with gtk_toolbox.gtk_lock():
251 unifiedDialpad = gv_views.Dialpad(self._widgetTree, self._errorDisplay)
252 self._dialpads.update({
253 self.GV_BACKEND: unifiedDialpad,
255 self._accountViews.update({
256 self.GV_BACKEND: gv_views.AccountInfo(
257 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._alarmHandler, self._errorDisplay
260 self._accountViews[self.GV_BACKEND].save_everything = self._save_settings
261 self._recentViews.update({
262 self.GV_BACKEND: gv_views.RecentCallsView(
263 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
266 self._messagesViews.update({
267 self.GV_BACKEND: gv_views.MessagesView(
268 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
271 self._contactsViews.update({
272 self.GV_BACKEND: gv_views.ContactsView(
273 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
277 fsContactsPath = os.path.join(constants._data_path_, "contacts")
278 fileBackend = file_backend.FilesystemAddressBookFactory(fsContactsPath)
280 self._dialpads[self.GV_BACKEND].number_selected = self._select_action
281 self._recentViews[self.GV_BACKEND].number_selected = self._select_action
282 self._messagesViews[self.GV_BACKEND].number_selected = self._select_action
283 self._contactsViews[self.GV_BACKEND].number_selected = self._select_action
286 self._phoneBackends[self.GV_BACKEND],
289 mergedBook = gv_views.MergedAddressBook(addressBooks, gv_views.MergedAddressBook.advanced_lastname_sorter)
290 self._contactsViews[self.GV_BACKEND].append(mergedBook)
291 self._contactsViews[self.GV_BACKEND].extend(addressBooks)
292 self._contactsViews[self.GV_BACKEND].open_addressbook(*self._contactsViews[self.GV_BACKEND].get_addressbooks().next()[0][0:2])
295 "on_paste": self._on_paste,
296 "on_refresh": self._on_menu_refresh,
297 "on_clearcookies_clicked": self._on_clearcookies_clicked,
298 "on_about_activate": self._on_about_activate,
300 if hildonize.GTK_MENU_USED:
301 self._widgetTree.signal_autoconnect(callbackMapping)
302 self._notebook.connect("switch-page", self._on_notebook_switch_page)
303 self._widgetTree.get_widget("clearcookies").connect("clicked", self._on_clearcookies_clicked)
305 with gtk_toolbox.gtk_lock():
306 self._originalCurrentLabels = [
307 self._notebook.get_tab_label(self._notebook.get_nth_page(pageIndex)).get_text()
308 for pageIndex in xrange(self._notebook.get_n_pages())
310 self._notebookTapHandler = gtk_toolbox.TapOrHold(self._notebook)
311 self._notebookTapHandler.enable()
312 self._notebookTapHandler.on_tap = self._reset_tab_refresh
313 self._notebookTapHandler.on_hold = self._on_tab_refresh
314 self._notebookTapHandler.on_holding = self._set_tab_refresh
315 self._notebookTapHandler.on_cancel = self._reset_tab_refresh
317 self._initDone = True
319 config = ConfigParser.SafeConfigParser()
320 config.read(constants._user_settings_)
321 with gtk_toolbox.gtk_lock():
322 self.load_settings(config)
324 with gtk_toolbox.gtk_lock():
325 self._errorDisplay.push_exception()
327 self._spawn_attempt_login(2)
329 def _spawn_attempt_login(self, *args):
330 self._loginSink.send(args)
332 def _attempt_login(self, numOfAttempts = 10, force = False):
334 @note This must be run outside of the UI lock
337 assert 0 <= numOfAttempts, "That was pointless having 0 or less login attempts"
338 assert self._initDone, "Attempting login before app is fully loaded"
340 serviceId = self.NULL_BACKEND
344 self.refresh_session()
345 serviceId = self._defaultBackendId
348 logging.exception('Session refresh failed with the following message "%s"' % str(e))
351 loggedIn, serviceId = self._login_by_user(numOfAttempts)
353 with gtk_toolbox.gtk_lock():
354 self._change_loggedin_status(serviceId)
356 hildonize.show_information_banner(self._window, "Logged In")
358 with gtk_toolbox.gtk_lock():
359 self._errorDisplay.push_exception()
361 def refresh_session(self):
363 @note Thread agnostic
365 assert self._initDone, "Attempting login before app is fully loaded"
369 loggedIn = self._login_by_cookie()
371 loggedIn = self._login_by_settings()
374 raise RuntimeError("Login Failed")
376 def _login_by_cookie(self):
378 @note Thread agnostic
380 loggedIn = self._phoneBackends[self._defaultBackendId].is_authed()
382 logging.info("Logged into %r through cookies" % self._phoneBackends[self._defaultBackendId])
385 def _login_by_settings(self):
387 @note Thread agnostic
389 username, password = self._credentials
390 loggedIn = self._phoneBackends[self._defaultBackendId].login(username, password)
392 self._credentials = username, password
393 logging.info("Logged into %r through settings" % self._phoneBackends[self._defaultBackendId])
396 def _login_by_user(self, numOfAttempts):
398 @note This must be run outside of the UI lock
400 loggedIn, (username, password) = False, self._credentials
401 tmpServiceId = self.GV_BACKEND
402 for attemptCount in xrange(numOfAttempts):
405 with gtk_toolbox.gtk_lock():
406 credentials = self._credentialsDialog.request_credentials(
407 defaultCredentials = self._credentials
409 if not self._phoneBackends[tmpServiceId].get_callback_number():
410 # subtle reminder to the users to configure things
411 self._notebook.set_current_page(self.ACCOUNT_TAB)
412 username, password = credentials
413 loggedIn = self._phoneBackends[tmpServiceId].login(username, password)
416 serviceId = tmpServiceId
417 self._credentials = username, password
418 logging.info("Logged into %r through user request" % self._phoneBackends[serviceId])
420 serviceId = self.NULL_BACKEND
421 self._notebook.set_current_page(self.ACCOUNT_TAB)
423 return loggedIn, serviceId
425 def _select_action(self, action, number, message):
426 self.refresh_session()
427 if action == "select":
428 self._dialpads[self._selectedBackendId].set_number(number)
429 self._notebook.set_current_page(self.KEYPAD_TAB)
430 elif action == "dial":
431 self._on_dial_clicked(number)
432 elif action == "sms":
433 self._on_sms_clicked(number, message)
435 assert False, "Unknown action: %s" % action
437 def _change_loggedin_status(self, newStatus):
438 oldStatus = self._selectedBackendId
439 if oldStatus == newStatus:
442 self._dialpads[oldStatus].disable()
443 self._accountViews[oldStatus].disable()
444 self._recentViews[oldStatus].disable()
445 self._messagesViews[oldStatus].disable()
446 self._contactsViews[oldStatus].disable()
448 self._dialpads[newStatus].enable()
449 self._accountViews[newStatus].enable()
450 self._recentViews[newStatus].enable()
451 self._messagesViews[newStatus].enable()
452 self._contactsViews[newStatus].enable()
454 if self._phoneBackends[self._selectedBackendId].get_callback_number() is None:
455 self._phoneBackends[self._selectedBackendId].set_sane_callback()
457 self._selectedBackendId = newStatus
459 self._accountViews[self._selectedBackendId].update()
460 self._refresh_active_tab()
462 def load_settings(self, config):
467 self._defaultBackendId = config.getint(constants.__pretty_app_name__, "active")
469 config.get(constants.__pretty_app_name__, "bin_blob_%i" % i)
470 for i in xrange(len(self._credentials))
473 base64.b64decode(blob)
476 self._credentials = tuple(creds)
478 if self._alarmHandler is not None:
479 self._alarmHandler.load_settings(config, "alarm")
480 except ConfigParser.NoOptionError, e:
482 "Settings file %s is missing section %s" % (
483 constants._user_settings_,
487 except ConfigParser.NoSectionError, e:
489 "Settings file %s is missing section %s" % (
490 constants._user_settings_,
495 for backendId, view in itertools.chain(
496 self._dialpads.iteritems(),
497 self._accountViews.iteritems(),
498 self._messagesViews.iteritems(),
499 self._recentViews.iteritems(),
500 self._contactsViews.iteritems(),
502 sectionName = "%s - %s" % (backendId, view.name())
504 view.load_settings(config, sectionName)
505 except ConfigParser.NoOptionError, e:
507 "Settings file %s is missing section %s" % (
508 constants._user_settings_,
512 except ConfigParser.NoSectionError, e:
514 "Settings file %s is missing section %s" % (
515 constants._user_settings_,
521 previousOrientation = config.getint(constants.__pretty_app_name__, "orientation")
522 if previousOrientation == gtk.ORIENTATION_HORIZONTAL:
523 hildonize.window_to_landscape(self._window)
524 elif previousOrientation == gtk.ORIENTATION_VERTICAL:
525 hildonize.window_to_portrait(self._window)
526 except ConfigParser.NoOptionError, e:
528 "Settings file %s is missing section %s" % (
529 constants._user_settings_,
533 except ConfigParser.NoSectionError, e:
535 "Settings file %s is missing section %s" % (
536 constants._user_settings_,
541 def save_settings(self, config):
543 @note Thread Agnostic
545 config.add_section(constants.__pretty_app_name__)
546 config.set(constants.__pretty_app_name__, "active", str(self._selectedBackendId))
547 config.set(constants.__pretty_app_name__, "orientation", str(int(gtk_toolbox.get_screen_orientation())))
548 for i, value in enumerate(self._credentials):
549 blob = base64.b64encode(value)
550 config.set(constants.__pretty_app_name__, "bin_blob_%i" % i, blob)
551 config.add_section("alarm")
552 if self._alarmHandler is not None:
553 self._alarmHandler.save_settings(config, "alarm")
555 for backendId, view in itertools.chain(
556 self._dialpads.iteritems(),
557 self._accountViews.iteritems(),
558 self._messagesViews.iteritems(),
559 self._recentViews.iteritems(),
560 self._contactsViews.iteritems(),
562 sectionName = "%s - %s" % (backendId, view.name())
563 config.add_section(sectionName)
564 view.save_settings(config, sectionName)
566 def _save_settings(self):
568 @note Thread Agnostic
570 config = ConfigParser.SafeConfigParser()
571 self.save_settings(config)
572 with open(constants._user_settings_, "wb") as configFile:
573 config.write(configFile)
575 def _refresh_active_tab(self):
576 pageIndex = self._notebook.get_current_page()
577 if pageIndex == self.CONTACTS_TAB:
578 self._contactsViews[self._selectedBackendId].update(force=True)
579 elif pageIndex == self.RECENT_TAB:
580 self._recentViews[self._selectedBackendId].update(force=True)
581 elif pageIndex == self.MESSAGES_TAB:
582 self._messagesViews[self._selectedBackendId].update(force=True)
584 if pageIndex in (self.RECENT_TAB, self.MESSAGES_TAB):
585 if self._ledHandler is not None:
586 self._ledHandler.off()
588 def _on_close(self, *args, **kwds):
590 if self._osso is not None:
594 self._save_settings()
598 def _on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
600 For shutdown or save_unsaved_data, our only state is cookies and I think the cookie manager handles that for us.
601 For system_inactivity, we have no background tasks to pause
603 @note Hildon specific
607 for backendId in self.BACKENDS:
608 self._phoneBackends[backendId].clear_caches()
609 self._contactsViews[self._selectedBackendId].clear_caches()
612 if save_unsaved_data or shutdown:
613 self._save_settings()
615 self._errorDisplay.push_exception()
617 def _on_connection_change(self, connection, event, magicIdentifier):
619 @note Hildon specific
624 status = event.get_status()
625 error = event.get_error()
626 iap_id = event.get_iap_id()
627 bearer = event.get_bearer_type()
629 if status == conic.STATUS_CONNECTED:
631 self._spawn_attempt_login(2)
632 elif status == conic.STATUS_DISCONNECTED:
634 self._defaultBackendId = self._selectedBackendId
635 self._change_loggedin_status(self.NULL_BACKEND)
637 self._errorDisplay.push_exception()
639 def _on_window_state_change(self, widget, event, *args):
641 @note Hildon specific
644 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
645 self._isFullScreen = True
647 self._isFullScreen = False
649 self._errorDisplay.push_exception()
651 def _on_key_press(self, widget, event, *args):
653 @note Hildon specific
657 event.keyval == gtk.keysyms.F6 or
658 event.keyval == gtk.keysyms.Return and event.get_state() & gtk.gdk.CONTROL_MASK
660 if self._isFullScreen:
661 self._window.unfullscreen()
663 self._window.fullscreen()
665 self._errorDisplay.push_exception()
667 def _on_clearcookies_clicked(self, *args):
669 self._phoneBackends[self._selectedBackendId].logout()
670 self._accountViews[self._selectedBackendId].clear()
671 self._recentViews[self._selectedBackendId].clear()
672 self._messagesViews[self._selectedBackendId].clear()
673 self._contactsViews[self._selectedBackendId].clear()
674 self._change_loggedin_status(self.NULL_BACKEND)
676 self._spawn_attempt_login(2, True)
678 self._errorDisplay.push_exception()
680 def _on_notebook_switch_page(self, notebook, page, pageIndex):
682 self._reset_tab_refresh()
684 didRecentUpdate = False
685 didMessagesUpdate = False
687 if pageIndex == self.RECENT_TAB:
688 didRecentUpdate = self._recentViews[self._selectedBackendId].update()
689 elif pageIndex == self.MESSAGES_TAB:
690 didMessagesUpdate = self._messagesViews[self._selectedBackendId].update()
691 elif pageIndex == self.CONTACTS_TAB:
692 self._contactsViews[self._selectedBackendId].update()
693 elif pageIndex == self.ACCOUNT_TAB:
694 self._accountViews[self._selectedBackendId].update()
696 if didRecentUpdate or didMessagesUpdate:
697 if self._ledHandler is not None:
698 self._ledHandler.off()
700 self._errorDisplay.push_exception()
702 def _set_tab_refresh(self, *args):
704 pageIndex = self._notebook.get_current_page()
705 child = self._notebook.get_nth_page(pageIndex)
706 self._notebook.get_tab_label(child).set_text("Refresh?")
708 self._errorDisplay.push_exception()
711 def _reset_tab_refresh(self, *args):
713 pageIndex = self._notebook.get_current_page()
714 child = self._notebook.get_nth_page(pageIndex)
715 self._notebook.get_tab_label(child).set_text(self._originalCurrentLabels[pageIndex])
717 self._errorDisplay.push_exception()
720 def _on_tab_refresh(self, *args):
722 self._refresh_active_tab()
723 self._reset_tab_refresh()
725 self._errorDisplay.push_exception()
728 def _on_sms_clicked(self, number, message):
730 assert number, "No number specified"
731 assert message, "Empty message"
733 loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
736 self._errorDisplay.push_exception()
740 self._errorDisplay.push_message(
741 "Backend link with GoogleVoice is not working, please try again"
747 self._phoneBackends[self._selectedBackendId].send_sms(number, message)
748 hildonize.show_information_banner(self._window, "Sending to %s" % number)
751 self._errorDisplay.push_exception()
754 self._dialpads[self._selectedBackendId].clear()
756 self._errorDisplay.push_exception()
758 def _on_dial_clicked(self, number):
760 assert number, "No number to call"
762 loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
765 self._errorDisplay.push_exception()
769 self._errorDisplay.push_message(
770 "Backend link with GoogleVoice is not working, please try again"
776 assert self._phoneBackends[self._selectedBackendId].get_callback_number() != "", "No callback number specified"
777 self._phoneBackends[self._selectedBackendId].dial(number)
778 hildonize.show_information_banner(self._window, "Calling %s" % number)
781 self._errorDisplay.push_exception()
784 self._dialpads[self._selectedBackendId].clear()
786 self._errorDisplay.push_exception()
788 def _on_menu_refresh(self, *args):
790 self._refresh_active_tab()
792 self._errorDisplay.push_exception()
794 def _on_paste(self, *args):
796 contents = self._clipboard.wait_for_text()
797 if contents is not None:
798 self._dialpads[self._selectedBackendId].set_number(contents)
800 self._errorDisplay.push_exception()
802 def _on_about_activate(self, *args):
804 dlg = gtk.AboutDialog()
805 dlg.set_name(constants.__pretty_app_name__)
806 dlg.set_version("%s-%d" % (constants.__version__, constants.__build__))
807 dlg.set_copyright("Copyright 2008 - LGPL")
808 dlg.set_comments("Dialcentral is a touch screen enhanced interface to your GoogleVoice account. This application is not affiliated with Google in any way")
809 dlg.set_website("http://gc-dialer.garage.maemo.org/")
810 dlg.set_authors(["<z2n@merctech.com>", "Eric Warnke <ericew@gmail.com>", "Ed Page <edpage@byu.net>"])
814 self._errorDisplay.push_exception()
820 failureCount, testCount = doctest.testmod()
822 print "Tests Successful"
829 _lock_file = os.path.join(constants._data_path_, ".lock")
830 #with gtk_toolbox.flock(_lock_file, 0):
831 gtk.gdk.threads_init()
833 if hildonize.IS_HILDON_SUPPORTED:
834 gtk.set_application_name(constants.__pretty_app_name__)
835 handle = Dialcentral()
839 class DummyOptions(object):
845 if __name__ == "__main__":
846 logging.basicConfig(level=logging.DEBUG)
848 if len(sys.argv) > 1:
854 if optparse is not None:
855 parser = optparse.OptionParser()
856 parser.add_option("-t", "--test", action="store_true", dest="test", help="Run tests")
857 (commandOptions, commandArgs) = parser.parse_args()
859 commandOptions = DummyOptions()
862 if commandOptions.test: