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 @bug Crashes when switching contact lists, see http://talk.maemo.org/showpost.php?p=312920&postcount=176
22 @bug Refeshing SMS a lot, then go to contacts and send a message, see http://talk.maemo.org/showpost.php?p=312920&postcount=176
23 @bug Can't send sms from dialpad, see http://talk.maemo.org/showpost.php?p=312922&postcount=177
24 @bug Sending an sms from contacts gave an error
25 @bug Getting into a bad state on connection loss, see http://talk.maemo.org/showpost.php?p=312912&postcount=175
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 hildon.hildon_helper_set_thumb_scrollbar(self._widgetTree.get_widget('recent_scrolledwindow'), True)
142 hildon.hildon_helper_set_thumb_scrollbar(self._widgetTree.get_widget('message_scrolledwindow'), True)
143 hildon.hildon_helper_set_thumb_scrollbar(self._widgetTree.get_widget('contacts_scrolledwindow'), True)
145 gtkMenu = self._widgetTree.get_widget("dialpad_menubar")
147 for child in gtkMenu.get_children():
149 self._window.set_menu(menu)
152 self._window.connect("key-press-event", self._on_key_press)
153 self._window.connect("window-state-event", self._on_window_state_change)
155 pass # warnings.warn("No Hildon", UserWarning, 2)
157 # If under hildon, rely on the application name being shown
159 self._window.set_title("%s" % constants.__pretty_app_name__)
162 "on_dialpad_quit": self._on_close,
164 self._widgetTree.signal_autoconnect(callbackMapping)
166 self._window.connect("destroy", self._on_close)
167 self._window.set_default_size(800, 300)
168 self._window.show_all()
170 self._loginSink = gtk_toolbox.threaded_stage(
173 gtk_toolbox.null_sink(),
177 backgroundSetup = threading.Thread(target=self._idle_setup)
178 backgroundSetup.setDaemon(True)
179 backgroundSetup.start()
181 def _idle_setup(self):
183 If something can be done after the UI loads, push it here so it's not blocking the UI
186 # Barebones UI handlers
190 self._phoneBackends = {self.NULL_BACKEND: null_backend.NullDialer()}
191 with gtk_toolbox.gtk_lock():
192 self._dialpads = {self.NULL_BACKEND: null_views.Dialpad(self._widgetTree)}
193 self._accountViews = {self.NULL_BACKEND: null_views.AccountInfo(self._widgetTree)}
194 self._recentViews = {self.NULL_BACKEND: null_views.RecentCallsView(self._widgetTree)}
195 self._messagesViews = {self.NULL_BACKEND: null_views.MessagesView(self._widgetTree)}
196 self._contactsViews = {self.NULL_BACKEND: null_views.ContactsView(self._widgetTree)}
198 self._dialpads[self._selectedBackendId].enable()
199 self._accountViews[self._selectedBackendId].enable()
200 self._recentViews[self._selectedBackendId].enable()
201 self._messagesViews[self._selectedBackendId].enable()
202 self._contactsViews[self._selectedBackendId].enable()
204 # Setup maemo specifics
211 self._osso = osso.Context(constants.__app_name__, constants.__version__, False)
212 device = osso.DeviceState(self._osso)
213 device.set_device_state_callback(self._on_device_state_change, 0)
215 pass # warnings.warn("No OSSO", UserWarning, 2)
219 self._alarmHandler = alarm_handler.AlarmHandler()
223 with gtk_toolbox.gtk_lock():
224 self._errorDisplay.push_exception()
226 if hildon is not None:
228 self._ledHandler = led_handler.LedHandler()
230 # Setup maemo specifics
235 self._connection = None
236 if conic is not None:
237 self._connection = conic.Connection()
238 self._connection.connect("connection-event", self._on_connection_change, constants.__app_magic__)
239 self._connection.request_connection(conic.CONNECT_FLAG_NONE)
241 pass # warnings.warn("No Internet Connectivity API ", UserWarning)
243 # Setup costly backends
250 os.makedirs(constants._data_path_)
254 gcCookiePath = os.path.join(constants._data_path_, "gc_cookies.txt")
255 gvCookiePath = os.path.join(constants._data_path_, "gv_cookies.txt")
256 self._defaultBackendId = self._guess_preferred_backend((
257 (self.GC_BACKEND, gcCookiePath),
258 (self.GV_BACKEND, gvCookiePath),
261 self._phoneBackends.update({
262 self.GC_BACKEND: gc_backend.GCDialer(gcCookiePath),
263 self.GV_BACKEND: gv_backend.GVDialer(gvCookiePath),
265 with gtk_toolbox.gtk_lock():
266 unifiedDialpad = gc_views.Dialpad(self._widgetTree, self._errorDisplay)
267 unifiedDialpad.set_number("")
268 self._dialpads.update({
269 self.GC_BACKEND: unifiedDialpad,
270 self.GV_BACKEND: unifiedDialpad,
272 self._accountViews.update({
273 self.GC_BACKEND: gc_views.AccountInfo(
274 self._widgetTree, self._phoneBackends[self.GC_BACKEND], None, self._errorDisplay
276 self.GV_BACKEND: gc_views.AccountInfo(
277 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._alarmHandler, self._errorDisplay
280 self._accountViews[self.GC_BACKEND].save_everything = lambda *args: None
281 self._accountViews[self.GV_BACKEND].save_everything = self._save_settings
282 self._recentViews.update({
283 self.GC_BACKEND: gc_views.RecentCallsView(
284 self._widgetTree, self._phoneBackends[self.GC_BACKEND], self._errorDisplay
286 self.GV_BACKEND: gc_views.RecentCallsView(
287 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
290 self._messagesViews.update({
291 self.GC_BACKEND: null_views.MessagesView(self._widgetTree),
292 self.GV_BACKEND: gc_views.MessagesView(
293 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
296 self._contactsViews.update({
297 self.GC_BACKEND: gc_views.ContactsView(
298 self._widgetTree, self._phoneBackends[self.GC_BACKEND], self._errorDisplay
300 self.GV_BACKEND: gc_views.ContactsView(
301 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
305 fsContactsPath = os.path.join(constants._data_path_, "contacts")
306 fileBackend = file_backend.FilesystemAddressBookFactory(fsContactsPath)
307 for backendId in (self.GV_BACKEND, self.GC_BACKEND):
308 self._dialpads[backendId].number_selected = self._select_action
309 self._recentViews[backendId].number_selected = self._select_action
310 self._messagesViews[backendId].number_selected = self._select_action
311 self._contactsViews[backendId].number_selected = self._select_action
314 self._phoneBackends[backendId],
317 mergedBook = gc_views.MergedAddressBook(addressBooks, gc_views.MergedAddressBook.advanced_lastname_sorter)
318 self._contactsViews[backendId].append(mergedBook)
319 self._contactsViews[backendId].extend(addressBooks)
320 self._contactsViews[backendId].open_addressbook(*self._contactsViews[backendId].get_addressbooks().next()[0][0:2])
323 "on_paste": self._on_paste,
324 "on_refresh": self._on_menu_refresh,
325 "on_clearcookies_clicked": self._on_clearcookies_clicked,
326 "on_notebook_switch_page": self._on_notebook_switch_page,
327 "on_about_activate": self._on_about_activate,
329 self._widgetTree.signal_autoconnect(callbackMapping)
331 with gtk_toolbox.gtk_lock():
332 self._originalCurrentLabels = [
333 self._notebook.get_tab_label(self._notebook.get_nth_page(pageIndex)).get_text()
334 for pageIndex in xrange(self._notebook.get_n_pages())
336 self._notebookTapHandler = gtk_toolbox.TapOrHold(self._notebook)
337 self._notebookTapHandler.enable()
338 self._notebookTapHandler.on_tap = self._reset_tab_refresh
339 self._notebookTapHandler.on_hold = self._on_tab_refresh
340 self._notebookTapHandler.on_holding = self._set_tab_refresh
341 self._notebookTapHandler.on_cancel = self._reset_tab_refresh
343 self._initDone = True
345 config = ConfigParser.SafeConfigParser()
346 config.read(constants._user_settings_)
347 with gtk_toolbox.gtk_lock():
348 self.load_settings(config)
350 self._spawn_attempt_login(2)
352 with gtk_toolbox.gtk_lock():
353 self._errorDisplay.push_exception()
355 def attempt_login(self, numOfAttempts = 10, force = False):
357 @todo Handle user notification better like attempting to login and failed login
359 @note This must be run outside of the UI lock
362 assert 0 <= numOfAttempts, "That was pointless having 0 or less login attempts"
363 assert self._initDone, "Attempting login before app is fully loaded"
365 serviceId = self.NULL_BACKEND
369 self.refresh_session()
370 serviceId = self._defaultBackendId
372 except StandardError, e:
373 warnings.warn('Session refresh failed with the following message "%s"' % e.message, UserWarning, 2)
376 loggedIn, serviceId = self._login_by_user(numOfAttempts)
378 with gtk_toolbox.gtk_lock():
379 self._change_loggedin_status(serviceId)
380 except StandardError, e:
381 with gtk_toolbox.gtk_lock():
382 self._errorDisplay.push_exception()
384 def _spawn_attempt_login(self, *args):
385 self._loginSink.send(args)
387 def refresh_session(self):
389 @note Thread agnostic
391 assert self._initDone, "Attempting login before app is fully loaded"
395 loggedIn = self._login_by_cookie()
397 loggedIn = self._login_by_settings()
400 raise RuntimeError("Login Failed")
402 def _login_by_cookie(self):
404 @note Thread agnostic
406 loggedIn = self._phoneBackends[self._defaultBackendId].is_authed()
409 "Logged into %r through cookies" % self._phoneBackends[self._defaultBackendId],
414 def _login_by_settings(self):
416 @note Thread agnostic
418 username, password = self._credentials
419 loggedIn = self._phoneBackends[self._defaultBackendId].login(username, password)
421 self._credentials = username, password
423 "Logged into %r through settings" % self._phoneBackends[self._defaultBackendId],
428 def _login_by_user(self, numOfAttempts):
430 @note This must be run outside of the UI lock
432 loggedIn, (username, password) = False, self._credentials
433 tmpServiceId = self.NULL_BACKEND
434 for attemptCount in xrange(numOfAttempts):
437 availableServices = (
438 (self.GV_BACKEND, "Google Voice"),
439 (self.GC_BACKEND, "Grand Central"),
441 with gtk_toolbox.gtk_lock():
442 credentials = self._credentialsDialog.request_credentials_from(
443 availableServices, defaultCredentials = self._credentials
445 tmpServiceId, username, password = credentials
446 loggedIn = self._phoneBackends[tmpServiceId].login(username, password)
449 serviceId = tmpServiceId
450 self._credentials = username, password
452 "Logged into %r through user request" % self._phoneBackends[serviceId],
456 serviceId = self.NULL_BACKEND
458 return loggedIn, serviceId
460 def _select_action(self, action, number, message):
461 self.refresh_session()
462 if action == "select":
463 self._dialpads[self._selectedBackendId].set_number(number)
464 self._notebook.set_current_page(self.KEYPAD_TAB)
465 elif action == "dial":
466 self._on_dial_clicked(number)
467 elif action == "sms":
468 self._on_sms_clicked(number, message)
470 assert False, "Unknown action: %s" % action
472 def _change_loggedin_status(self, newStatus):
473 oldStatus = self._selectedBackendId
474 if oldStatus == newStatus:
477 self._dialpads[oldStatus].disable()
478 self._accountViews[oldStatus].disable()
479 self._recentViews[oldStatus].disable()
480 self._messagesViews[oldStatus].disable()
481 self._contactsViews[oldStatus].disable()
483 self._dialpads[newStatus].enable()
484 self._accountViews[newStatus].enable()
485 self._recentViews[newStatus].enable()
486 self._messagesViews[newStatus].enable()
487 self._contactsViews[newStatus].enable()
489 if self._phoneBackends[self._selectedBackendId].get_callback_number() is None:
490 self._phoneBackends[self._selectedBackendId].set_sane_callback()
491 self._accountViews[self._selectedBackendId].update()
493 self._selectedBackendId = newStatus
495 def load_settings(self, config):
500 self._defaultBackendId = int(config.get(constants.__pretty_app_name__, "active"))
502 config.get(constants.__pretty_app_name__, "bin_blob_%i" % i)
503 for i in xrange(len(self._credentials))
506 base64.b64decode(blob)
509 self._credentials = tuple(creds)
511 if self._alarmHandler is not None:
512 self._alarmHandler.load_settings(config, "alarm")
513 except ConfigParser.NoOptionError, e:
515 "Settings file %s is missing section %s" % (
516 constants._user_settings_,
521 except ConfigParser.NoSectionError, e:
523 "Settings file %s is missing section %s" % (
524 constants._user_settings_,
530 for backendId, view in itertools.chain(
531 self._dialpads.iteritems(),
532 self._accountViews.iteritems(),
533 self._messagesViews.iteritems(),
534 self._recentViews.iteritems(),
535 self._contactsViews.iteritems(),
537 sectionName = "%s - %s" % (backendId, view.name())
539 view.load_settings(config, sectionName)
540 except ConfigParser.NoOptionError, e:
542 "Settings file %s is missing section %s" % (
543 constants._user_settings_,
548 except ConfigParser.NoSectionError, e:
550 "Settings file %s is missing section %s" % (
551 constants._user_settings_,
557 def save_settings(self, config):
559 @note Thread Agnostic
561 config.add_section(constants.__pretty_app_name__)
562 config.set(constants.__pretty_app_name__, "active", str(self._selectedBackendId))
563 for i, value in enumerate(self._credentials):
564 blob = base64.b64encode(value)
565 config.set(constants.__pretty_app_name__, "bin_blob_%i" % i, blob)
566 config.add_section("alarm")
567 if self._alarmHandler is not None:
568 self._alarmHandler.save_settings(config, "alarm")
570 for backendId, view in itertools.chain(
571 self._dialpads.iteritems(),
572 self._accountViews.iteritems(),
573 self._messagesViews.iteritems(),
574 self._recentViews.iteritems(),
575 self._contactsViews.iteritems(),
577 sectionName = "%s - %s" % (backendId, view.name())
578 config.add_section(sectionName)
579 view.save_settings(config, sectionName)
581 def _guess_preferred_backend(self, backendAndCookiePaths):
583 (getmtime_nothrow(path), backendId, path)
584 for backendId, path in backendAndCookiePaths
586 modTimeAndPath.sort()
587 return modTimeAndPath[-1][1]
589 def _save_settings(self):
591 @note Thread Agnostic
593 config = ConfigParser.SafeConfigParser()
594 self.save_settings(config)
595 with open(constants._user_settings_, "wb") as configFile:
596 config.write(configFile)
598 def _refresh_active_tab(self):
599 pageIndex = self._notebook.get_current_page()
600 if pageIndex == self.CONTACTS_TAB:
601 self._contactsViews[self._selectedBackendId].update(force=True)
602 elif pageIndex == self.RECENT_TAB:
603 self._recentViews[self._selectedBackendId].update(force=True)
604 elif pageIndex == self.MESSAGES_TAB:
605 self._messagesViews[self._selectedBackendId].update(force=True)
607 if pageIndex in (self.RECENT_TAB, self.MESSAGES_TAB):
608 if self._ledHandler is not None:
609 self._ledHandler.off()
611 def _on_close(self, *args, **kwds):
613 if self._osso is not None:
617 self._save_settings()
621 def _on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
623 For shutdown or save_unsaved_data, our only state is cookies and I think the cookie manager handles that for us.
624 For system_inactivity, we have no background tasks to pause
626 @note Hildon specific
629 for backendId in self.BACKENDS:
630 self._phoneBackends[backendId].clear_caches()
631 self._contactsViews[self._selectedBackendId].clear_caches()
634 if save_unsaved_data or shutdown:
635 self._save_settings()
637 def _on_connection_change(self, connection, event, magicIdentifier):
639 @note Hildon specific
643 status = event.get_status()
644 error = event.get_error()
645 iap_id = event.get_iap_id()
646 bearer = event.get_bearer_type()
648 if status == conic.STATUS_CONNECTED:
650 self._spawn_attempt_login(2)
651 elif status == conic.STATUS_DISCONNECTED:
653 self._defaultBackendId = self._selectedBackendId
654 self._change_loggedin_status(self.NULL_BACKEND)
656 def _on_window_state_change(self, widget, event, *args):
658 @note Hildon specific
660 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
661 self._isFullScreen = True
663 self._isFullScreen = False
665 def _on_key_press(self, widget, event, *args):
667 @note Hildon specific
669 if event.keyval == gtk.keysyms.F6:
670 if self._isFullScreen:
671 self._window.unfullscreen()
673 self._window.fullscreen()
675 def _on_clearcookies_clicked(self, *args):
676 self._phoneBackends[self._selectedBackendId].logout()
677 self._accountViews[self._selectedBackendId].clear()
678 self._recentViews[self._selectedBackendId].clear()
679 self._messagesViews[self._selectedBackendId].clear()
680 self._contactsViews[self._selectedBackendId].clear()
681 self._change_loggedin_status(self.NULL_BACKEND)
683 self._spawn_attempt_login(2, True)
685 def _on_notebook_switch_page(self, notebook, page, pageIndex):
686 self._reset_tab_refresh()
688 didRecentUpdate = False
689 didMessagesUpdate = False
691 if pageIndex == self.RECENT_TAB:
692 didRecentUpdate = self._recentViews[self._selectedBackendId].update()
693 elif pageIndex == self.MESSAGES_TAB:
694 didMessagesUpdate = self._messagesViews[self._selectedBackendId].update()
695 elif pageIndex == self.CONTACTS_TAB:
696 self._contactsViews[self._selectedBackendId].update()
697 elif pageIndex == self.ACCOUNT_TAB:
698 self._accountViews[self._selectedBackendId].update()
700 if didRecentUpdate or didMessagesUpdate:
701 if self._ledHandler is not None:
702 self._ledHandler.off()
704 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?")
710 def _reset_tab_refresh(self, *args):
711 pageIndex = self._notebook.get_current_page()
712 child = self._notebook.get_nth_page(pageIndex)
713 self._notebook.get_tab_label(child).set_text(self._originalCurrentLabels[pageIndex])
716 def _on_tab_refresh(self, *args):
717 self._refresh_active_tab()
718 self._reset_tab_refresh()
721 def _on_sms_clicked(self, number, message):
722 assert number, "No number specified"
723 assert message, "Empty message"
725 loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
726 except StandardError, e:
728 self._errorDisplay.push_exception()
732 self._errorDisplay.push_message(
733 "Backend link with grandcentral is not working, please try again"
739 self._phoneBackends[self._selectedBackendId].send_sms(number, message)
741 except StandardError, e:
742 self._errorDisplay.push_exception()
743 except ValueError, e:
744 self._errorDisplay.push_exception()
747 self._dialpads[self._selectedBackendId].clear()
749 def _on_dial_clicked(self, number):
750 assert number, "No number to call"
752 loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
753 except StandardError, e:
755 self._errorDisplay.push_exception()
759 self._errorDisplay.push_message(
760 "Backend link with grandcentral is not working, please try again"
766 assert self._phoneBackends[self._selectedBackendId].get_callback_number() != "", "No callback number specified"
767 self._phoneBackends[self._selectedBackendId].dial(number)
769 except StandardError, e:
770 self._errorDisplay.push_exception()
771 except ValueError, e:
772 self._errorDisplay.push_exception()
775 self._dialpads[self._selectedBackendId].clear()
777 def _on_menu_refresh(self, *args):
778 self._refresh_active_tab()
780 def _on_paste(self, *args):
781 contents = self._clipboard.wait_for_text()
782 self._dialpads[self._selectedBackendId].set_number(contents)
784 def _on_about_activate(self, *args):
785 dlg = gtk.AboutDialog()
786 dlg.set_name(constants.__pretty_app_name__)
787 dlg.set_version(constants.__version__)
788 dlg.set_copyright("Copyright 2008 - LGPL")
789 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")
790 dlg.set_website("http://gc-dialer.garage.maemo.org/")
791 dlg.set_authors(["<z2n@merctech.com>", "Eric Warnke <ericew@gmail.com>", "Ed Page <edpage@byu.net>"])
799 failureCount, testCount = doctest.testmod()
801 print "Tests Successful"
808 _lock_file = os.path.join(constants._data_path_, ".lock")
809 #with gtk_toolbox.flock(_lock_file, 0):
810 gtk.gdk.threads_init()
812 if hildon is not None:
813 gtk.set_application_name(constants.__pretty_app_name__)
814 handle = Dialcentral()
818 class DummyOptions(object):
824 if __name__ == "__main__":
825 if len(sys.argv) > 1:
831 if optparse is not None:
832 parser = optparse.OptionParser()
833 parser.add_option("-t", "--test", action="store_true", dest="test", help="Run tests")
834 (commandOptions, commandArgs) = parser.parse_args()
836 commandOptions = DummyOptions()
839 if commandOptions.test: