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 @bug When switching to tab while logging in, it doesn't refresh once logged int
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"))
116 hildonize.hildonize_combo_entry(self._widgetTree.get_widget("callbackcombo").get_child())
118 for scrollingWidget in (
119 'recent_scrolledwindow',
120 'message_scrolledwindow',
121 'contacts_scrolledwindow',
122 "phoneSelectionMessages_scrolledwindow",
123 "smsMessages_scrolledwindow",
125 hildonize.hildonize_scrollwindow(self._widgetTree.get_widget(scrollingWidget))
126 for scrollingWidget in (
127 "phonetypes_scrolledwindow",
128 "smsMessage_scrolledEntry",
130 hildonize.hildonize_scrollwindow_with_viewport(self._widgetTree.get_widget(scrollingWidget))
132 replacementButtons = [gtk.Button("Test")]
133 menu = hildonize.hildonize_menu(
135 self._widgetTree.get_widget("dialpad_menubar"),
139 if hildonize.IS_HILDON_SUPPORTED:
140 self._window.connect("key-press-event", self._on_key_press)
141 self._window.connect("window-state-event", self._on_window_state_change)
143 logging.warning("No hildonization support")
145 hildonize.set_application_title(self._window, "%s" % constants.__pretty_app_name__)
147 self._window.connect("destroy", self._on_close)
148 self._window.set_default_size(800, 300)
149 self._window.show_all()
151 self._loginSink = gtk_toolbox.threaded_stage(
154 gtk_toolbox.null_sink(),
158 backgroundSetup = threading.Thread(target=self._idle_setup)
159 backgroundSetup.setDaemon(True)
160 backgroundSetup.start()
162 def _idle_setup(self):
164 If something can be done after the UI loads, push it here so it's not blocking the UI
166 # Barebones UI handlers
171 self._phoneBackends = {self.NULL_BACKEND: null_backend.NullDialer()}
172 with gtk_toolbox.gtk_lock():
173 self._dialpads = {self.NULL_BACKEND: null_views.Dialpad(self._widgetTree)}
174 self._accountViews = {self.NULL_BACKEND: null_views.AccountInfo(self._widgetTree)}
175 self._recentViews = {self.NULL_BACKEND: null_views.RecentCallsView(self._widgetTree)}
176 self._messagesViews = {self.NULL_BACKEND: null_views.MessagesView(self._widgetTree)}
177 self._contactsViews = {self.NULL_BACKEND: null_views.ContactsView(self._widgetTree)}
179 self._dialpads[self._selectedBackendId].enable()
180 self._accountViews[self._selectedBackendId].enable()
181 self._recentViews[self._selectedBackendId].enable()
182 self._messagesViews[self._selectedBackendId].enable()
183 self._contactsViews[self._selectedBackendId].enable()
185 with gtk_toolbox.gtk_lock():
186 self._errorDisplay.push_exception()
188 # Setup maemo specifics
192 except (ImportError, OSError):
196 self._osso = osso.Context(constants.__app_name__, constants.__version__, False)
197 device = osso.DeviceState(self._osso)
198 device.set_device_state_callback(self._on_device_state_change, 0)
200 logging.warning("No device state support")
204 self._alarmHandler = alarm_handler.AlarmHandler()
205 except (ImportError, OSError):
208 with gtk_toolbox.gtk_lock():
209 self._errorDisplay.push_exception()
211 logging.warning("No notification support")
212 if hildonize.IS_HILDON_SUPPORTED:
215 self._ledHandler = led_handler.LedHandler()
217 logging.exception('LED Handling failed: "%s"' % str(e))
218 self._ledHandler = None
220 self._ledHandler = None
224 except (ImportError, OSError):
226 self._connection = None
227 if conic is not None:
228 self._connection = conic.Connection()
229 self._connection.connect("connection-event", self._on_connection_change, constants.__app_magic__)
230 self._connection.request_connection(conic.CONNECT_FLAG_NONE)
232 logging.warning("No connection support")
234 with gtk_toolbox.gtk_lock():
235 self._errorDisplay.push_exception()
237 # Setup costly backends
244 os.makedirs(constants._data_path_)
248 gvCookiePath = os.path.join(constants._data_path_, "gv_cookies.txt")
250 self._phoneBackends.update({
251 self.GV_BACKEND: gv_backend.GVDialer(gvCookiePath),
253 with gtk_toolbox.gtk_lock():
254 unifiedDialpad = gv_views.Dialpad(self._widgetTree, self._errorDisplay)
255 self._dialpads.update({
256 self.GV_BACKEND: unifiedDialpad,
258 self._accountViews.update({
259 self.GV_BACKEND: gv_views.AccountInfo(
260 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._alarmHandler, self._errorDisplay
263 self._accountViews[self.GV_BACKEND].save_everything = self._save_settings
264 self._recentViews.update({
265 self.GV_BACKEND: gv_views.RecentCallsView(
266 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
269 self._messagesViews.update({
270 self.GV_BACKEND: gv_views.MessagesView(
271 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
274 self._contactsViews.update({
275 self.GV_BACKEND: gv_views.ContactsView(
276 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
280 fsContactsPath = os.path.join(constants._data_path_, "contacts")
281 fileBackend = file_backend.FilesystemAddressBookFactory(fsContactsPath)
283 self._dialpads[self.GV_BACKEND].number_selected = self._select_action
284 self._recentViews[self.GV_BACKEND].number_selected = self._select_action
285 self._messagesViews[self.GV_BACKEND].number_selected = self._select_action
286 self._contactsViews[self.GV_BACKEND].number_selected = self._select_action
289 self._phoneBackends[self.GV_BACKEND],
292 mergedBook = gv_views.MergedAddressBook(addressBooks, gv_views.MergedAddressBook.advanced_lastname_sorter)
293 self._contactsViews[self.GV_BACKEND].append(mergedBook)
294 self._contactsViews[self.GV_BACKEND].extend(addressBooks)
295 self._contactsViews[self.GV_BACKEND].open_addressbook(*self._contactsViews[self.GV_BACKEND].get_addressbooks().next()[0][0:2])
298 "on_paste": self._on_paste,
299 "on_refresh": self._on_menu_refresh,
300 "on_clearcookies_clicked": self._on_clearcookies_clicked,
301 "on_about_activate": self._on_about_activate,
303 if hildonize.GTK_MENU_USED:
304 self._widgetTree.signal_autoconnect(callbackMapping)
305 self._notebook.connect("switch-page", self._on_notebook_switch_page)
306 self._widgetTree.get_widget("clearcookies").connect("clicked", self._on_clearcookies_clicked)
308 with gtk_toolbox.gtk_lock():
309 self._originalCurrentLabels = [
310 self._notebook.get_tab_label(self._notebook.get_nth_page(pageIndex)).get_text()
311 for pageIndex in xrange(self._notebook.get_n_pages())
313 self._notebookTapHandler = gtk_toolbox.TapOrHold(self._notebook)
314 self._notebookTapHandler.enable()
315 self._notebookTapHandler.on_tap = self._reset_tab_refresh
316 self._notebookTapHandler.on_hold = self._on_tab_refresh
317 self._notebookTapHandler.on_holding = self._set_tab_refresh
318 self._notebookTapHandler.on_cancel = self._reset_tab_refresh
320 self._initDone = True
322 config = ConfigParser.SafeConfigParser()
323 config.read(constants._user_settings_)
324 with gtk_toolbox.gtk_lock():
325 self.load_settings(config)
327 with gtk_toolbox.gtk_lock():
328 self._errorDisplay.push_exception()
330 self._spawn_attempt_login(2)
332 def _spawn_attempt_login(self, *args):
333 self._loginSink.send(args)
335 def _attempt_login(self, numOfAttempts = 10, force = False):
337 @todo Handle user notification better like attempting to login and failed login
339 @note This must be run outside of the UI lock
342 assert 0 <= numOfAttempts, "That was pointless having 0 or less login attempts"
343 assert self._initDone, "Attempting login before app is fully loaded"
345 serviceId = self.NULL_BACKEND
349 self.refresh_session()
350 serviceId = self._defaultBackendId
353 logging.exception('Session refresh failed with the following message "%s"' % str(e))
356 loggedIn, serviceId = self._login_by_user(numOfAttempts)
358 with gtk_toolbox.gtk_lock():
359 self._change_loggedin_status(serviceId)
361 hildonize.show_information_banner(self._window, "Logged In")
363 with gtk_toolbox.gtk_lock():
364 self._errorDisplay.push_exception()
366 def refresh_session(self):
368 @note Thread agnostic
370 assert self._initDone, "Attempting login before app is fully loaded"
374 loggedIn = self._login_by_cookie()
376 loggedIn = self._login_by_settings()
379 raise RuntimeError("Login Failed")
381 def _login_by_cookie(self):
383 @note Thread agnostic
385 loggedIn = self._phoneBackends[self._defaultBackendId].is_authed()
387 logging.info("Logged into %r through cookies" % self._phoneBackends[self._defaultBackendId])
390 def _login_by_settings(self):
392 @note Thread agnostic
394 username, password = self._credentials
395 loggedIn = self._phoneBackends[self._defaultBackendId].login(username, password)
397 self._credentials = username, password
398 logging.info("Logged into %r through settings" % self._phoneBackends[self._defaultBackendId])
401 def _login_by_user(self, numOfAttempts):
403 @note This must be run outside of the UI lock
405 loggedIn, (username, password) = False, self._credentials
406 tmpServiceId = self.GV_BACKEND
407 for attemptCount in xrange(numOfAttempts):
410 with gtk_toolbox.gtk_lock():
411 credentials = self._credentialsDialog.request_credentials(
412 defaultCredentials = self._credentials
414 if not self._phoneBackends[tmpServiceId].get_callback_number():
415 # subtle reminder to the users to configure things
416 self._notebook.set_current_page(self.ACCOUNT_TAB)
417 username, password = credentials
418 loggedIn = self._phoneBackends[tmpServiceId].login(username, password)
421 serviceId = tmpServiceId
422 self._credentials = username, password
423 logging.info("Logged into %r through user request" % self._phoneBackends[serviceId])
425 serviceId = self.NULL_BACKEND
426 self._notebook.set_current_page(self.ACCOUNT_TAB)
428 return loggedIn, serviceId
430 def _select_action(self, action, number, message):
431 self.refresh_session()
432 if action == "select":
433 self._dialpads[self._selectedBackendId].set_number(number)
434 self._notebook.set_current_page(self.KEYPAD_TAB)
435 elif action == "dial":
436 self._on_dial_clicked(number)
437 elif action == "sms":
438 self._on_sms_clicked(number, message)
440 assert False, "Unknown action: %s" % action
442 def _change_loggedin_status(self, newStatus):
443 oldStatus = self._selectedBackendId
444 if oldStatus == newStatus:
447 self._dialpads[oldStatus].disable()
448 self._accountViews[oldStatus].disable()
449 self._recentViews[oldStatus].disable()
450 self._messagesViews[oldStatus].disable()
451 self._contactsViews[oldStatus].disable()
453 self._dialpads[newStatus].enable()
454 self._accountViews[newStatus].enable()
455 self._recentViews[newStatus].enable()
456 self._messagesViews[newStatus].enable()
457 self._contactsViews[newStatus].enable()
459 if self._phoneBackends[self._selectedBackendId].get_callback_number() is None:
460 self._phoneBackends[self._selectedBackendId].set_sane_callback()
461 self._accountViews[self._selectedBackendId].update()
462 self._refresh_active_tab()
464 self._selectedBackendId = newStatus
466 def load_settings(self, config):
471 self._defaultBackendId = config.getint(constants.__pretty_app_name__, "active")
473 config.get(constants.__pretty_app_name__, "bin_blob_%i" % i)
474 for i in xrange(len(self._credentials))
477 base64.b64decode(blob)
480 self._credentials = tuple(creds)
482 if self._alarmHandler is not None:
483 self._alarmHandler.load_settings(config, "alarm")
484 except ConfigParser.NoOptionError, e:
486 "Settings file %s is missing section %s" % (
487 constants._user_settings_,
491 except ConfigParser.NoSectionError, e:
493 "Settings file %s is missing section %s" % (
494 constants._user_settings_,
499 for backendId, view in itertools.chain(
500 self._dialpads.iteritems(),
501 self._accountViews.iteritems(),
502 self._messagesViews.iteritems(),
503 self._recentViews.iteritems(),
504 self._contactsViews.iteritems(),
506 sectionName = "%s - %s" % (backendId, view.name())
508 view.load_settings(config, sectionName)
509 except ConfigParser.NoOptionError, e:
511 "Settings file %s is missing section %s" % (
512 constants._user_settings_,
516 except ConfigParser.NoSectionError, e:
518 "Settings file %s is missing section %s" % (
519 constants._user_settings_,
525 previousOrientation = config.getint(constants.__pretty_app_name__, "orientation")
526 if previousOrientation == gtk.ORIENTATION_HORIZONTAL:
527 hildonize.window_to_landscape(self._window)
528 elif previousOrientation == gtk.ORIENTATION_VERTICAL:
529 hildonize.window_to_portrait(self._window)
530 except ConfigParser.NoOptionError, e:
532 "Settings file %s is missing section %s" % (
533 constants._user_settings_,
537 except ConfigParser.NoSectionError, e:
539 "Settings file %s is missing section %s" % (
540 constants._user_settings_,
545 def save_settings(self, config):
547 @note Thread Agnostic
549 config.add_section(constants.__pretty_app_name__)
550 config.set(constants.__pretty_app_name__, "active", str(self._selectedBackendId))
551 config.set(constants.__pretty_app_name__, "orientation", str(int(gtk_toolbox.get_screen_orientation())))
552 for i, value in enumerate(self._credentials):
553 blob = base64.b64encode(value)
554 config.set(constants.__pretty_app_name__, "bin_blob_%i" % i, blob)
555 config.add_section("alarm")
556 if self._alarmHandler is not None:
557 self._alarmHandler.save_settings(config, "alarm")
559 for backendId, view in itertools.chain(
560 self._dialpads.iteritems(),
561 self._accountViews.iteritems(),
562 self._messagesViews.iteritems(),
563 self._recentViews.iteritems(),
564 self._contactsViews.iteritems(),
566 sectionName = "%s - %s" % (backendId, view.name())
567 config.add_section(sectionName)
568 view.save_settings(config, sectionName)
570 def _save_settings(self):
572 @note Thread Agnostic
574 config = ConfigParser.SafeConfigParser()
575 self.save_settings(config)
576 with open(constants._user_settings_, "wb") as configFile:
577 config.write(configFile)
579 def _refresh_active_tab(self):
580 pageIndex = self._notebook.get_current_page()
581 if pageIndex == self.CONTACTS_TAB:
582 self._contactsViews[self._selectedBackendId].update(force=True)
583 elif pageIndex == self.RECENT_TAB:
584 self._recentViews[self._selectedBackendId].update(force=True)
585 elif pageIndex == self.MESSAGES_TAB:
586 self._messagesViews[self._selectedBackendId].update(force=True)
588 if pageIndex in (self.RECENT_TAB, self.MESSAGES_TAB):
589 if self._ledHandler is not None:
590 self._ledHandler.off()
592 def _on_close(self, *args, **kwds):
594 if self._osso is not None:
598 self._save_settings()
602 def _on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
604 For shutdown or save_unsaved_data, our only state is cookies and I think the cookie manager handles that for us.
605 For system_inactivity, we have no background tasks to pause
607 @note Hildon specific
611 for backendId in self.BACKENDS:
612 self._phoneBackends[backendId].clear_caches()
613 self._contactsViews[self._selectedBackendId].clear_caches()
616 if save_unsaved_data or shutdown:
617 self._save_settings()
619 self._errorDisplay.push_exception()
621 def _on_connection_change(self, connection, event, magicIdentifier):
623 @note Hildon specific
628 status = event.get_status()
629 error = event.get_error()
630 iap_id = event.get_iap_id()
631 bearer = event.get_bearer_type()
633 if status == conic.STATUS_CONNECTED:
635 self._spawn_attempt_login(2)
636 elif status == conic.STATUS_DISCONNECTED:
638 self._defaultBackendId = self._selectedBackendId
639 self._change_loggedin_status(self.NULL_BACKEND)
641 self._errorDisplay.push_exception()
643 def _on_window_state_change(self, widget, event, *args):
645 @note Hildon specific
648 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
649 self._isFullScreen = True
651 self._isFullScreen = False
653 self._errorDisplay.push_exception()
655 def _on_key_press(self, widget, event, *args):
657 @note Hildon specific
660 if event.keyval == gtk.keysyms.F6:
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: