Some lint stuff and fixing a cosmetic bug
[gc-dialer] / src / gc_dialer.py
1 #!/usr/bin/python2.5
2
3
4 """
5 Grandcentral Dialer
6 Python front-end to a wget script to use grandcentral.com to place outbound VOIP calls.
7 (C) 2008 Mark Bergman
8 bergman@merctech.com
9 """
10
11
12 import sys
13 import gc
14 import os
15 import threading
16 import time
17 import re
18 import warnings
19
20 import gobject
21 import gtk
22 import gtk.glade
23
24 try:
25         import hildon
26 except ImportError:
27         hildon = None
28
29 try:
30         import osso
31         try:
32                 import abook
33                 import evolution.ebook as evobook
34         except ImportError:
35                 abook = None
36                 evobook = None
37 except ImportError:
38         osso = None
39
40 try:
41         import conic
42 except ImportError:
43         conic = None
44
45 try:
46         import doctest
47         import optparse
48 except ImportError:
49         doctest = None
50         optparse = None
51
52 from gcbackend import GCDialer
53
54 import socket
55
56
57 socket.setdefaulttimeout(5)
58
59
60 def make_ugly(prettynumber):
61         """
62         function to take a phone number and strip out all non-numeric
63         characters
64
65         >>> make_ugly("+012-(345)-678-90")
66         '01234567890'
67         """
68         uglynumber = re.sub('\D', '', prettynumber)
69         return uglynumber
70
71
72 def make_pretty(phonenumber):
73         """
74         Function to take a phone number and return the pretty version
75         pretty numbers:
76                 if phonenumber begins with 0:
77                         ...-(...)-...-....
78                 if phonenumber begins with 1: ( for gizmo callback numbers )
79                         1 (...)-...-....
80                 if phonenumber is 13 digits:
81                         (...)-...-....
82                 if phonenumber is 10 digits:
83                         ...-....
84         >>> make_pretty("12")
85         '12'
86         >>> make_pretty("1234567")
87         '123-4567'
88         >>> make_pretty("2345678901")
89         '(234)-567-8901'
90         >>> make_pretty("12345678901")
91         '1 (234)-567-8901'
92         >>> make_pretty("01234567890")
93         '+012-(345)-678-90'
94         """
95         if phonenumber is None:
96                 return ""
97
98         if len(phonenumber) < 3:
99                 return phonenumber
100
101         if phonenumber[0] == "0":
102                 prettynumber = ""
103                 prettynumber += "+%s" % phonenumber[0:3]
104                 if 3 < len(phonenumber):
105                         prettynumber += "-(%s)" % phonenumber[3:6]
106                         if 6 < len(phonenumber):
107                                 prettynumber += "-%s" % phonenumber[6:9]
108                                 if 9 < len(phonenumber):
109                                         prettynumber += "-%s" % phonenumber[9:]
110                 return prettynumber
111         elif len(phonenumber) <= 7:
112                 prettynumber = "%s-%s" % (phonenumber[0:3], phonenumber[3:])
113         elif len(phonenumber) > 8 and phonenumber[0] == "1":
114                 prettynumber = "1 (%s)-%s-%s" % (phonenumber[1:4], phonenumber[4:7], phonenumber[7:])
115         elif len(phonenumber) > 7:
116                 prettynumber = "(%s)-%s-%s" % (phonenumber[0:3], phonenumber[3:6], phonenumber[6:])
117         return prettynumber
118
119
120 def make_idler(func):
121         a = []
122
123         def callable(*args, **kwds):
124                 if not a:
125                         a.append(func(*args, **kwds))
126                 try:
127                         a[0].next()
128                         return True
129                 except StopIteration:
130                         return False
131         
132         callable.__name__ = func.__name__
133         callable.__doc__ = func.__doc__
134         callable.__dict__.update(func.__dict__)
135
136         return callable
137
138
139 class PhoneTypeSelector(object):
140
141         def __init__(self, widgetTree, gcBackend):
142                 self._gcBackend = gcBackend
143                 self._widgetTree = widgetTree
144                 self._dialog = self._widgetTree.get_widget("phonetype_dialog")
145
146                 self._selectButton = self._widgetTree.get_widget("select_button")
147                 self._selectButton.connect("clicked", self._on_phonetype_select)
148
149                 self._cancelButton = self._widgetTree.get_widget("cancel_button")
150                 self._cancelButton.connect("clicked", self._on_phonetype_cancel)
151
152                 self._typemodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
153                 self._typeviewselection = None
154
155                 typeview = self._widgetTree.get_widget("phonetypes")
156                 typeview.connect("row-activated", self._on_phonetype_select)
157                 typeview.set_model(self._typemodel)
158                 textrenderer = gtk.CellRendererText()
159
160                 # Add the column to the treeview
161                 column = gtk.TreeViewColumn("Phone Numbers", textrenderer, text=1)
162                 column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
163
164                 typeview.append_column(column)
165
166                 self._typeviewselection = typeview.get_selection()
167                 self._typeviewselection.set_mode(gtk.SELECTION_SINGLE)
168
169         def run(self, contactDetails):
170                 self._typemodel.clear()
171
172                 for phoneType, phoneNumber in contactDetails:
173                         self._typemodel.append((phoneNumber, "%s - %s" % (make_pretty(phoneNumber), phoneType)))
174
175                 userResponse = self._dialog.run()
176
177                 if userResponse == gtk.RESPONSE_OK:
178                         model, itr = self._typeviewselection.get_selected()
179                         if itr:
180                                 phoneNumber = self._typemodel.get_value(itr, 0)
181                 else:
182                         phoneNumber = ""
183
184                 self._typeviewselection.unselect_all()
185                 self._dialog.hide()
186                 return phoneNumber
187         
188         def _on_phonetype_select(self, *args):
189                 self._dialog.response(gtk.RESPONSE_OK)
190
191         def _on_phonetype_cancel(self, *args):
192                 self._dialog.response(gtk.RESPONSE_CANCEL)
193
194
195 class Dialpad(object):
196
197         __app_name__ = "gc_dialer"
198         __version__ = "0.7.0"
199         __app_magic__ = 0xdeadbeef
200
201         _glade_files = [
202                 './gc_dialer.glade',
203                 '../lib/gc_dialer.glade',
204                 '/usr/local/lib/gc_dialer.glade',
205         ]
206
207         def __init__(self):
208                 self._phonenumber = ""
209                 self._prettynumber = ""
210                 self._areacode = "518"
211
212                 self._clipboard = gtk.clipboard_get()
213
214                 self._deviceIsOnline = True
215                 self._callbackNeedsSetup = True
216
217                 self._recenttime = 0.0
218                 self._recentmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
219                 self._recentviewselection = None
220
221                 self._contactstime = 0.0
222                 self._contactsmodel = gtk.ListStore(
223                         gtk.gdk.Pixbuf,
224                         gobject.TYPE_STRING,
225                         gobject.TYPE_STRING,
226                         gobject.TYPE_STRING,
227                         gobject.TYPE_STRING
228                 )
229                 self._contactsviewselection = None
230
231                 for path in Dialpad._glade_files:
232                         if os.path.isfile(path):
233                                 self._widgetTree = gtk.glade.XML(path)
234                                 break
235                 else:
236                         self.display_error_message("Cannot find gc_dialer.glade")
237                         gtk.main_quit()
238                         return
239
240                 aboutHeader = self._widgetTree.get_widget("about_title")
241                 aboutHeader.set_label("%s\nVersion %s" % (aboutHeader.get_label(), Dialpad.__version__))
242
243                 #Get the buffer associated with the number display
244                 self._numberdisplay = self._widgetTree.get_widget("numberdisplay")
245                 self.set_number("")
246                 self._notebook = self._widgetTree.get_widget("notebook")
247
248                 self._window = self._widgetTree.get_widget("Dialpad")
249
250                 global hildon
251                 self._app = None
252                 self._isFullScreen = False
253                 if hildon is not None and isinstance(self._window, gtk.Window):
254                         warnings.warn("Hildon installed but glade file not updated to work with hildon", UserWarning, 2)
255                         hildon = None
256                 elif hildon is not None:
257                         self._app = hildon.Program()
258                         self._window.set_title("Keypad")
259                         self._app.add_window(self._window)
260                         self._widgetTree.get_widget("callbackcombo").get_child().set_property('hildon-input-mode', (1 << 4))
261                         self._widgetTree.get_widget("usernameentry").set_property('hildon-input-mode', 7)
262                         self._widgetTree.get_widget("passwordentry").set_property('hildon-input-mode', 7|(1 << 29))
263
264                         gtkMenu = self._widgetTree.get_widget("menubar1")
265                         menu = gtk.Menu()
266                         for child in gtkMenu.get_children():
267                                 child.reparent(menu)
268                         self._window.set_menu(menu)
269                         gtkMenu.destroy()
270
271                         self._window.connect("key-press-event", self._on_key_press)
272                         self._window.connect("window-state-event", self._on_window_state_change)
273                 else:
274                         warnings.warn("No Hildon", UserWarning, 2)
275
276                 self._osso = None
277                 self._ebook = None
278                 if osso is not None:
279                         self._osso = osso.Context(Dialpad.__app_name__, Dialpad.__version__, False)
280                         device = osso.DeviceState(self._osso)
281                         device.set_device_state_callback(self._on_device_state_change, 0)
282                         if abook is not None and evobook is not None:
283                                 abook.init_with_name(Dialpad.__app_name__, self._osso)
284                                 self._ebook = evobook.open_addressbook("default")
285                         else:
286                                 warnings.warn("No abook and No evolution address book support", UserWarning, 2)
287                 else:
288                         warnings.warn("No OSSO", UserWarning, 2)
289
290                 self._connection = None
291                 if conic is not None:
292                         self._connection = conic.Connection()
293                         self._connection.connect("connection-event", self._on_connection_change, Dialpad.__app_magic__)
294                         self._connection.request_connection(conic.CONNECT_FLAG_NONE)
295                 else:
296                         warnings.warn("No Internet Connectivity API ", UserWarning, 2)
297
298                 callbackMapping = {
299                         # Process signals from buttons
300                         "on_loginbutton_clicked": self._on_loginbutton_clicked,
301                         "on_loginclose_clicked": self._on_loginclose_clicked,
302
303                         "on_dialpad_quit": self._on_close,
304                         "on_paste": self._on_paste,
305                         "on_clear_number": self._on_clear_number,
306
307                         "on_clearcookies_clicked": self._on_clearcookies_clicked,
308                         "on_notebook_switch_page": self._on_notebook_switch_page,
309                         "on_recentview_row_activated": self._on_recentview_row_activated,
310                         "on_contactsview_row_activated" : self._on_contactsview_row_activated,
311
312                         "on_digit_clicked": self._on_digit_clicked,
313                         "on_back_clicked": self._on_backspace,
314                         "on_dial_clicked": self._on_dial_clicked,
315                 }
316                 self._widgetTree.signal_autoconnect(callbackMapping)
317                 self._widgetTree.get_widget("callbackcombo").get_child().connect("changed", self._on_callbackentry_changed)
318
319                 if self._window:
320                         self._window.connect("destroy", gtk.main_quit)
321                         self._window.show_all()
322
323                 self._gcBackend = GCDialer()
324
325                 self._phoneTypeSelector = PhoneTypeSelector(self._widgetTree, self._gcBackend)
326
327                 if not self._gcBackend.is_authed():
328                         self.attempt_login(2)
329                 else:
330                         self.set_account_number()
331                 gobject.idle_add(self._idly_init_recent_view)
332                 gobject.idle_add(self._idly_init_contacts_view)
333
334         def _idly_init_recent_view(self):
335                 """ Deferred initalization of the recent view treeview """
336
337                 recentview = self._widgetTree.get_widget("recentview")
338                 recentview.set_model(self._recentmodel)
339                 textrenderer = gtk.CellRendererText()
340
341                 # Add the column to the treeview
342                 column = gtk.TreeViewColumn("Calls", textrenderer, text=1)
343                 column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
344
345                 recentview.append_column(column)
346
347                 self._recentviewselection = recentview.get_selection()
348                 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
349
350                 return False
351
352         def _idly_init_contacts_view(self):
353                 """ deferred initalization of the contacts view treeview """
354
355                 contactsview = self._widgetTree.get_widget("contactsview")
356                 contactsview.set_model(self._contactsmodel)
357
358                 # Add the column to the treeview
359                 column = gtk.TreeViewColumn("Contact")
360
361                 iconrenderer = gtk.CellRendererPixbuf()
362                 column.pack_start(iconrenderer, expand=False)
363                 column.add_attribute(iconrenderer, 'pixbuf', 0)
364
365                 textrenderer = gtk.CellRendererText()
366                 column.pack_start(textrenderer, expand=True)
367                 column.add_attribute(textrenderer, 'text', 1)
368
369                 textrenderer = gtk.CellRendererText()
370                 column.pack_start(textrenderer, expand=True)
371                 column.add_attribute(textrenderer, 'text', 4)
372
373                 column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
374                 column.set_sort_column_id(1)
375                 column.set_visible(True)
376                 contactsview.append_column(column)
377
378                 #textrenderer = gtk.CellRendererText()
379                 #column = gtk.TreeViewColumn("Location", textrenderer, text=2)
380                 #column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
381                 #column.set_sort_column_id(2)
382                 #column.set_visible(True)
383                 #contactsview.append_column(column)
384
385                 #textrenderer = gtk.CellRendererText()
386                 #column = gtk.TreeViewColumn("Phone", textrenderer, text=3)
387                 #column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
388                 #column.set_sort_column_id(3)
389                 #column.set_visible(True)
390                 #contactsview.append_column(column)
391
392                 self._contactsviewselection = contactsview.get_selection()
393                 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
394
395                 return False
396
397         def _idly_setup_callback_combo(self):
398                 combobox = self._widgetTree.get_widget("callbackcombo")
399                 self.callbacklist = gtk.ListStore(gobject.TYPE_STRING)
400                 combobox.set_model(self.callbacklist)
401                 combobox.set_text_column(0)
402                 for number, description in self._gcBackend.get_callback_numbers().iteritems():
403                         self.callbacklist.append([make_pretty(number)])
404
405                 combobox.get_child().set_text(make_pretty(self._gcBackend.get_callback_number()))
406                 self._callbackNeedsSetup = False
407
408         def _idly_populate_recentview(self):
409                 self._recentmodel.clear()
410
411                 for personsName, phoneNumber, date, action in self._gcBackend.get_recent():
412                         description = "%s on %s from/to %s - %s" % (action.capitalize(), date, personsName, phoneNumber)
413                         item = (phoneNumber, description)
414                         self._recentmodel.append(item)
415
416                 self._recenttime = time.time()
417                 return False
418
419         @make_idler
420         def _idly_populate_contactsview(self):
421                 self._contactsmodel.clear()
422
423                 # completely disable updating the treeview while we populate the data
424                 contactsview = self._widgetTree.get_widget("contactsview")
425                 contactsview.freeze_child_notify()
426                 contactsview.set_model(None)
427
428                 # get gc icon
429                 gc_icon = gtk.gdk.pixbuf_new_from_file_at_size('gc_contact.png', 16, 16)
430                 for contactId, contactName in self._gcBackend.get_contacts():
431                         self._contactsmodel.append((gc_icon,) + (contactName, "", contactId) + ("",))
432                         yield
433
434                 # restart the treeview data rendering
435                 contactsview.set_model(self._contactsmodel)
436                 contactsview.thaw_child_notify()
437
438                 self._contactstime = time.time()
439
440         def attempt_login(self, numOfAttempts = 1):
441                 """
442                 @note Assumes that you are already logged in
443                 """
444                 assert 0 < numOfAttempts, "That was pointless having 0 or less login attempts"
445                 dialog = self._widgetTree.get_widget("login_dialog")
446
447                 for i in range(numOfAttempts):
448                         dialog.run()
449
450                         username = self._widgetTree.get_widget("usernameentry").get_text()
451                         password = self._widgetTree.get_widget("passwordentry").get_text()
452                         self._widgetTree.get_widget("passwordentry").set_text("")
453
454                         loggedIn = self._gcBackend.login(username, password)
455                         dialog.hide()
456                         if loggedIn:
457                                 if self._gcBackend.get_callback_number() is None:
458                                         self._gcBackend.set_sane_callback()
459                                 self.set_account_number()
460                                 return True
461
462                 return False
463
464         def display_error_message(self, msg):
465                 error_dialog = gtk.MessageDialog(None, 0, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, msg)
466
467                 def close(dialog, response, editor):
468                         editor.about_dialog = None
469                         dialog.destroy()
470                 error_dialog.connect("response", close, self)
471                 error_dialog.run()
472
473         def set_number(self, number):
474                 self._phonenumber = make_ugly(number)
475                 self._prettynumber = make_pretty(self._phonenumber)
476                 self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % ( self._prettynumber ) )
477
478         def set_account_number(self):
479                 accountnumber = self._gcBackend.get_account_number()
480                 self._widgetTree.get_widget("gcnumberlabel").set_label("<span size='23000' weight='bold'>%s</span>" % (accountnumber))
481
482         def _on_close(self, *args):
483                 gtk.main_quit()
484
485         def _on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
486                 """
487                 For shutdown or save_unsaved_data, our only state is cookies and I think the cookie manager handles that for us.
488                 For system_inactivity, we have no background tasks to pause
489
490                 @note Hildon specific
491                 """
492                 if memory_low:
493                         self._gcBackend.clear_caches()
494                         re.purge()
495                         gc.collect()
496
497         def _on_connection_change(self, connection, event, magicIdentifier):
498                 """
499                 @note Hildon specific
500                 """
501                 status = event.get_status()
502                 error = event.get_error()
503                 iap_id = event.get_iap_id()
504                 bearer = event.get_bearer_type()
505
506                 if status == conic.STATUS_CONNECTED:
507                         self._window.set_sensitive(True)
508                         self._deviceIsOnline = True
509                 elif status == conic.STATUS_DISCONNECTED:
510                         self._window.set_sensitive(False)
511                         self._deviceIsOnline = False
512
513         def _on_window_state_change(self, widget, event, *args):
514                 """
515                 @note Hildon specific
516                 """
517                 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
518                         self._isFullScreen = True
519                 else:
520                         self._isFullScreen = False
521
522         def _on_key_press(self, widget, event, *args):
523                 """
524                 @note Hildon specific
525                 """
526                 if event.keyval == gtk.keysyms.F6:
527                         if self._isFullScreen:
528                                 self._window.unfullscreen()
529                         else:
530                                 self._window.fullscreen()
531
532         def _on_loginbutton_clicked(self, *args):
533                 self._widgetTree.get_widget("login_dialog").response(gtk.RESPONSE_OK)
534
535         def _on_loginclose_clicked(self, *args):
536                 self._on_close()
537                 sys.exit(0)
538
539         def _on_clearcookies_clicked(self, *args):
540                 self._gcBackend.reset()
541                 self._callbackNeedsSetup = True
542                 self._recenttime = 0.0
543                 self._contactstime = 0.0
544                 self._recentmodel.clear()
545                 self._widgetTree.get_widget("callbackcombo").get_child().set_text("")
546
547                 # re-run the inital grandcentral setup
548                 self.attempt_login(2)
549                 gobject.idle_add(self._idly_setup_callback_combo)
550
551         def _on_callbackentry_changed(self, *args):
552                 """
553                 @todo Potential blocking on web access, maybe we should defer this or put up a dialog?
554                 """
555                 text = make_ugly(self._widgetTree.get_widget("callbackcombo").get_child().get_text())
556                 if not self._gcBackend.is_valid_syntax(text):
557                         warnings.warn("%s is not a valid callback number" % text, UserWarning, 2)
558                 elif text != self._gcBackend.get_callback_number():
559                         self._gcBackend.set_callback_number(text)
560                 else:
561                         warnings.warn("Callback number already is %s" % self._gcBackend.get_callback_number(), UserWarning, 2)
562
563         def _on_recentview_row_activated(self, treeview, path, view_column):
564                 model, itr = self._recentviewselection.get_selected()
565                 if not itr:
566                         return
567
568                 self.set_number(self._recentmodel.get_value(itr, 0))
569                 self._notebook.set_current_page(0)
570                 self._recentviewselection.unselect_all()
571
572         def _on_contactsview_row_activated(self, treeview, path, view_column):
573                 model, itr = self._contactsviewselection.get_selected()
574                 if not itr:
575                         return
576
577                 contactId = self._contactsmodel.get_value(itr, 3)
578                 contactDetails = self._gcBackend.get_contact_details(contactId)
579                 contactDetails = [phoneNumber for phoneNumber in contactDetails]
580
581                 if len(contactDetails) == 0:
582                         phoneNumber = ""
583                 elif len(contactDetails) == 1:
584                         phoneNumber = contactDetails[0][1]
585                 else:
586                         phoneNumber = self._phoneTypeSelector.run(contactDetails)
587
588                 if 0 < len(phoneNumber):
589                         self.set_number(phoneNumber)
590                         self._notebook.set_current_page(0)
591
592                 self._contactsviewselection.unselect_all()
593
594         def _on_notebook_switch_page(self, notebook, page, page_num):
595                 if page_num == 1 and 300 < (time.time() - self._contactstime):
596                         gobject.idle_add(self._idly_populate_contactsview)
597                 elif page_num == 2 and 300 < (time.time() - self._recenttime):
598                         gobject.idle_add(self._idly_populate_recentview)
599                 elif page_num == 3 and self._callbackNeedsSetup:
600                         gobject.idle_add(self._idly_setup_callback_combo)
601
602                 if hildon:
603                         hildonTitle = self._notebook.get_tab_label(self._notebook.get_nth_page(page_num)).get_text()
604                         self._window.set_title(hildonTitle)
605
606         def _on_dial_clicked(self, widget):
607                 """
608                 @todo Potential blocking on web access, maybe we should defer parts of this or put up a dialog?
609                 """
610                 loggedIn = self._gcBackend.is_authed()
611                 if not loggedIn:
612                         loggedIn = self.attempt_login(2)
613
614                 if not loggedIn or not self._gcBackend.is_authed() or self._gcBackend.get_callback_number() == "":
615                         self.display_error_message("Backend link with grandcentral is not working, please try again")
616                         warnings.warn("Backend Status: Logged in? %s, Authenticated? %s, Callback=%s" % (loggedIn, self._gcBackend.is_authed(), self._gcBackend.get_callback_number()), UserWarning, 2)
617                         return
618
619                 try:
620                         callSuccess = self._gcBackend.dial(self._phonenumber)
621                 except ValueError, e:
622                         self._gcBackend._msg = e.message
623                         callSuccess = False
624
625                 if not callSuccess:
626                         self.display_error_message(self._gcBackend._msg)
627                 else:
628                         self.set_number("")
629
630                 self._recentmodel.clear()
631                 self._recenttime = 0.0
632
633         def _on_paste(self, *args):
634                 contents = self._clipboard.wait_for_text()
635                 phoneNumber = re.sub('\D', '', contents)
636                 self.set_number(phoneNumber)
637
638         def _on_clear_number(self, *args):
639                 self.set_number("")
640
641         def _on_digit_clicked(self, widget):
642                 self.set_number(self._phonenumber + widget.get_name()[5])
643
644         def _on_backspace(self, widget):
645                 self.set_number(self._phonenumber[:-1])
646
647
648 def run_doctest():
649         failureCount, testCount = doctest.testmod()
650         if not failureCount:
651                 print "Tests Successful"
652                 sys.exit(0)
653         else:
654                 sys.exit(1)
655
656
657 def run_dialpad():
658         gtk.gdk.threads_init()
659         title = 'Dialpad'
660         handle = Dialpad()
661         gtk.main()
662
663
664 class DummyOptions(object):
665
666         def __init__(self):
667                 self.test = False
668
669
670 if __name__ == "__main__":
671         if hildon is not None:
672                 gtk.set_application_name("Dialer")
673
674         if optparse is not None:
675                 parser = optparse.OptionParser()
676                 parser.add_option("-t", "--test", action="store_true", dest="test", help="Run tests")
677                 (commandOptions, commandArgs) = parser.parse_args()
678         else:
679                 commandOptions = DummyOptions()
680                 commandArgs = []
681
682         if commandOptions.test:
683                 run_doctest()
684         else:
685                 run_dialpad()