1""" 2Cocoa GUI for the Package Manager 3 4This is a first generation of the Cocoa GUI, it inherits some of the nasty 5features of the current Carbon version: 6 71. GUI blocks during some operations, such as downloading or installing 8 92. Checking on GUI packages may crash the application 10 11The first item can only be solved by rewriting parts of the pimp module, the 12second part will be solved by running at least some pimp related code in a 13seperate process. 14 15TODO: 16- Make sure 'File -> Open...' actually works 17 18XXX: 19- save preferences in the favorites db (for databases that are in in there)? 20""" 21 22from Cocoa import * 23import objc 24import threading 25 26from PyObjCTools import AppHelper 27 28import sys 29import pimp 30import webbrowser 31 32# File type for packman databases 33DB_FILE_TYPE="Python Package Database" 34 35# Extract class information from the NIB files 36# - MainMenu: Global application stuff 37# - OpenPanel: The 'Open URL...' window 38# - PackageDatabase: Document window 39 40def setString(field, value): 41 """ 42 Set an NSTextField to the specified value. Clears the field if 'value' 43 is None. 44 """ 45 if value is None: 46 field.setStringValue_("") 47 else: 48 field.setStringValue_(value) 49 50 51## 52# We break the abstraction of some of the objects in the pimp module. That 53# is necessary because we cannot get at the required information using the 54# public interfaces :-( 55# 56def DB_DESCRIPTION(pimpDB): 57 return pimpDB._description 58 59def DB_MAINTAINER(pimpDB): 60 return pimpDB._maintainer 61 62def DB_URL(pimpDB): 63 return pimpDB._urllist[0] 64 65def PKG_HIDDEN(package): 66 """ Return True iff the package is a hidden package """ 67 return (package._dict.get('Download-URL', None) is None) 68 69 70 71 72class PackageDatabase (NSDocument): 73 """ 74 The document class for a package database 75 """ 76 databaseMaintainer = objc.IBOutlet() 77 databaseName = objc.IBOutlet() 78 installButton = objc.IBOutlet() 79 installDependencies = objc.IBOutlet() 80 installationLocation = objc.IBOutlet() 81 installationLog = objc.IBOutlet() 82 installationPanel = objc.IBOutlet() 83 installationProgress = objc.IBOutlet() 84 installationTitle = objc.IBOutlet() 85 itemDescription = objc.IBOutlet() 86 itemHome = objc.IBOutlet() 87 itemInstalled = objc.IBOutlet() 88 itemStatus = objc.IBOutlet() 89 overwrite = objc.IBOutlet() 90 packageTable = objc.IBOutlet() 91 prerequisitesTable = objc.IBOutlet() 92 progressOK = objc.IBOutlet() 93 showHidden = objc.IBOutlet() 94 verbose = objc.IBOutlet() 95 96 97 def init(self): 98 """ 99 Initialize the document without a database 100 """ 101 102 self = super(PackageDatabase, self).init() 103 if self is None: return None 104 self.pimp = None 105 self._packages = [] 106 return self 107 108 109 def initWithContentsOfFile_ofType_(self, path, type): 110 """ 111 Open a local database. 112 """ 113 self = self.init() 114 if self is None: return self 115 116 url = NSURL.fileURLWithPath_(path) 117 118 self.openDB(url.absoluteString()) 119 return self 120 121 def __del__(self): 122 """ Clean up after ourselves """ 123 if hasattr(self, 'timer'): 124 self.timer.invalidate() 125 del self.timer 126 127 def close(self): 128 if hasattr(self, 'timer'): 129 self.timer.invalidate() 130 del self.timer 131 super(PackageDatabase, self).close() 132 133 def setDB(self, pimpURL, pimpDB): 134 self.pimp = pimpDB 135 self._packages = pimpDB.list() 136 self._prerequisites = [] 137 if self.databaseName is not None: 138 self.databaseName.setStringValue_(DB_DESCRIPTION(self.pimp)) 139 self.databaseMaintainer.setStringValue_(DB_MAINTAINER(self.pimp)) 140 141 if self.packageTable is not None: 142 self.packageTable.reloadData() 143 self.tableViewSelectionDidChange_(None) 144 145 self.setFileName_(pimpURL) 146 self.pimpURL = pimpURL 147 148 if hasattr(self, 'timer'): 149 self.timer.invalidate() 150 151 self.timer = NSTimer.scheduledTimerWithTimeInterval_target_selector_userInfo_repeats_( 152 10.0, 153 self, 154 self.checkUpdates_, 155 None, 156 True) 157 158 159 def openDB(self, dbUrl=None): 160 """ 161 Open a database at the specified URL 162 """ 163 prefs = pimp.PimpPreferences() 164 if dbUrl is not None: 165 prefs.pimpDatabase = dbUrl 166 else: 167 prefs.pimpDatabase = pimp.DEFAULT_PIMPDATABASE 168 169 db = pimp.PimpDatabase(prefs) 170 db.appendURL(prefs.pimpDatabase) 171 self.setDB(dbUrl, db) 172 173 def checkUpdates_(self, sender): 174 """ 175 Refresh the package information, the user may have installed or 176 removed a package. This method is called once in a while using a timer. 177 """ 178 if self.packageTable is None: return 179 180 self.sortPackages() 181 self.packageTable.reloadData() 182 183 def windowNibName(self): 184 """ Return the name of the document NIB """ 185 return 'PackageDatabase' 186 187 def displayName(self): 188 """ Return the document name for inside the window title """ 189 if self.pimp is None: 190 return "Untitled" 191 192 return DB_URL(self.pimp) 193 194 def awakeFromNib(self): 195 """ 196 Initialize the GUI now that the NIB has been loaded. 197 """ 198 if self.pimp is not None: 199 self.databaseName.setStringValue_(DB_DESCRIPTION(self.pimp)) 200 self.databaseMaintainer.setStringValue_(DB_MAINTAINER(self.pimp)) 201 202 else: 203 self.databaseName.setStringValue_("") 204 self.databaseMaintainer.setStringValue_("") 205 206 207 self.setBoolFromDefaults(self.verbose, 'verbose') 208 self.setBoolFromDefaults( 209 self.installDependencies, 'installDependencies') 210 self.setBoolFromDefaults(self.showHidden, 'showHidden') 211 self.setBoolFromDefaults(self.overwrite, 'forceInstallation') 212 213 b = NSUserDefaults.standardUserDefaults( 214 ).boolForKey_('installSystemWide') 215 if b: 216 self.installationLocation.setState_atRow_column_(NSOnState, 0, 0) 217 else: 218 self.installationLocation.setState_atRow_column_(NSOnState, 1, 0) 219 220 self.sortPackages() 221 222 def setBoolFromDefaults(self, field, name): 223 defaults = NSUserDefaults.standardUserDefaults() 224 b = defaults.boolForKey_(name) 225 if b: 226 field.setState_(NSOnState) 227 else: 228 field.setState_(NSOffState) 229 230 def saveBoolToDefaults(self, field, name): 231 defaults = NSUserDefaults.standardUserDefaults() 232 defaults.setBool_forKey_(field.state() == NSOnState, name) 233 defaults.synchronize() 234 235 @objc.IBAction 236 def savePreferences_(self, sender): 237 self.saveBoolToDefaults(self.verbose, 'verbose') 238 self.saveBoolToDefaults(self.installDependencies, 'installDependencies') 239 self.saveBoolToDefaults(self.showHidden, 'showHidden') 240 self.saveBoolToDefaults(self.overwrite, 'forceInstallation') 241 self.saveBoolToDefaults( 242 self.installationLocation.cellAtRow_column_(0, 0), 243 'installSystemWide') 244 245 def packages(self): 246 return self._packages 247 248 def selectedPackage(self): 249 row = self.packageTable.selectedRow() 250 if row == -1: return None 251 252 return self._packages[row] 253 254 255 def tableViewSelectionDidChange_(self, obj): 256 """ 257 Update the detail view 258 """ 259 260 package = self.selectedPackage() 261 262 if package is None: 263 # No selected package, clear the detail view 264 setString(self.itemHome, None) 265 setString(self.itemStatus, None) 266 setString(self.itemInstalled, None) 267 self.itemDescription.setString_("") 268 self.installButton.setEnabled_(False) 269 self._prerequisites = [] 270 self.prerequisitesTable.reloadData() 271 272 else: 273 # Update the detail view 274 275 setString(self.itemHome, package.homepage()) 276 277 # XXX: Could we use ReST for the the description? 278 # Recognizing and 'activating' URL's would be fairly easy. 279 self.itemDescription.setString_( 280 package.description() 281 ) 282 283 status, msg = package.installed() 284 setString(self.itemInstalled, status) 285 setString(self.itemStatus, msg) 286 self.installButton.setEnabled_(True) 287 self._prerequisites = package.prerequisites() 288 289 # XXX: Add the closure of all dependencies 290 291 self.prerequisitesTable.reloadData() 292 293 @objc.IBAction 294 def addToFavorites_(self, sender): 295 appdel = NSApplication.sharedApplication().delegate() 296 appdel.addFavorite(self.pimp._description, self.pimp._urllist[0]) 297 298 # 299 # NSTableDataSource implementation, for the package list 300 # 301 302 def numberOfRowsInTableView_(self, view): 303 304 if not hasattr(self, 'pimp') or self.pimp is None: 305 return 0 306 307 if view is self.packageTable: 308 return len(self._packages) 309 else: 310 return len(self._prerequisites) 311 312 313 def tableView_objectValueForTableColumn_row_(self, view, col, row): 314 315 colname = col.identifier() 316 317 if view is self.packageTable: 318 package = self._packages[row] 319 shortdescription = None 320 else: 321 package, shortdescription = self._prerequisites[row] 322 323 if colname == 'installed': 324 # XXX: Nicer formatting 325 return getattr(package, colname)()[0] 326 327 return getattr(package, colname)() 328 329 def tableView_sortDescriptorsDidChange_(self, view, oldDescriptors): 330 if view is self.packageTable: 331 self.sortPackages() 332 333 def sortPackages(self): 334 """ 335 Sort the package list in the order wished for by the user. 336 """ 337 if self.pimp is None: 338 return 339 340 if self.packageTable is None: 341 return 342 343 sortInfo = [ 344 (item.key(), item.ascending(), item.selector()) 345 for item in self.packageTable.sortDescriptors() 346 ] 347 348 if self.showHidden.state() == NSOnState: 349 self._packages = self.pimp.list()[:] 350 else: 351 self._packages = [ pkg 352 for pkg in self.pimp.list() if not PKG_HIDDEN(pkg) ] 353 354 if not sortInfo: 355 self.packageTable.reloadData() 356 self.tableViewSelectionDidChange_(None) 357 return 358 359 def cmpBySortInfo(l, r): 360 for key, ascending, meth in sortInfo: 361 if key == 'installed': 362 l_val = getattr(l, key)()[0] 363 r_val = getattr(r, key)()[0] 364 else: 365 l_val = getattr(l, key)() 366 r_val = getattr(r, key)() 367 if meth == 'compare:': 368 res = cmp(l_val, r_val) 369 else: 370 if isinstance(l_val, objc.pyobjc_unicode): 371 l_val = l_val.nsstring() 372 elif isinstance(l_val, (unicode, str)): 373 l_val = NSString.stringWithString_(l_val).nsstring() 374 res = getattr(l_val, meth)(r_val) 375 376 if not ascending: 377 res = -res 378 if res != 0: 379 return res 380 381 return 0 382 383 self._packages.sort(cmpBySortInfo) 384 self.packageTable.reloadData() 385 386 @objc.IBAction 387 def filterPackages_(self, sender): 388 """ 389 GUI action that is triggered when one of the view options 390 changes 391 """ 392 self.sortPackages() 393 394 @objc.IBAction 395 def visitHome_(self, sender): 396 """ 397 Open the homepage of the currently selected package in the 398 default webbrowser. 399 """ 400 package = self.selectedPackage() 401 if package is None: 402 return 403 404 home = package.homepage() 405 if home is None: 406 return 407 408 try: 409 webbrowser.open(home) 410 except Exception, msg: 411 NSBeginAlertSheet( 412 'Opening homepage failed', 413 'OK', None, None, self.windowForSheet(), None, None, None, 414 0, 'Could not open homepage: %s'%(msg,)) 415 416 417 @objc.IBAction 418 def installPackage_(self, sender): 419 """ 420 Install the currently selected package 421 """ 422 package = self.selectedPackage() 423 if package is None: return 424 425 force = self.overwrite.state() == NSOnState 426 recursive = self.installDependencies.state() == NSOnState 427 428 pimpInstaller = pimp.PimpInstaller(self.pimp) 429 lst, messages = pimpInstaller.prepareInstall(package, force, recursive) 430 431 if messages: 432 NSBeginAlertSheet( 433 'Cannot install packages', 434 'OK', None, None, 435 self.windowForSheet(), None, None, None, 0, 436 '\n'.join(messages)) 437 return 438 439 app = NSApplication.sharedApplication() 440 self.installationTitle.setStringValue_( 441 'Installing: %s ...'%(package.shortdescription(),)) 442 self.installationProgress.setHidden_(False) 443 self.installationProgress.startAnimation_(self) 444 self.progressOK.setEnabled_(False) 445 ts = self.installationLog.textStorage() 446 ts.deleteCharactersInRange_((0, ts.length())) 447 app.beginSheet_modalForWindow_modalDelegate_didEndSelector_contextInfo_( 448 self.installationPanel, 449 self.windowForSheet(), 450 None, None, 0) 451 452 # I'm not sure if this accidental or not, but prepareInstall() returns 453 # a list of package in the order that they should be installed in, 454 # and install() installs them in the reverse order :-( 455 # XXX: This seems to be a bug in pimp. 456 self.runner = InstallerThread( 457 self, 458 pimpInstaller, 459 lst[::-1], 460 self.verbose.state() == NSOnState, 461 self.installationLog.textStorage() 462 ) 463 464 self.runner.start() 465 466 @objc.IBAction 467 def closeProgress_(self, sender): 468 """ 469 Close the installation progress sheet 470 """ 471 self.installationPanel.close() 472 NSApplication.sharedApplication().endSheet_(self.installationPanel) 473 474 @objc.IBAction 475 def installationDone_(self, sender): 476 """ 477 The installer thread is ready, close the sheet. 478 """ 479 self.progressOK.setEnabled_(True) 480 self.installationProgress.setHidden_(False) 481 self.installationProgress.stopAnimation_(self) 482 483 messages = self.runner.result 484 if messages: 485 ts = self.installationLog.textStorage() 486 ts.appendAttributedString_( 487 NSAttributedString.alloc().initWithString_attributes_( 488 '\n\nCannot install packages\n\n', 489 { 490 NSFontAttributeName: NSFont.boldSystemFontOfSize_(12), 491 } 492 )) 493 494 ts.appendAttributedString_( 495 NSAttributedString.alloc().initWithString_( 496 '\n'.join(messages) + '\n')) 497 498 self.packageTable.reloadData() 499 self.tableViewSelectionDidChange_(None) 500 501 502class DownloadThread (threading.Thread): 503 """ 504 Thread for downloading a PackageManager database. 505 506 This is used by the application delegate to open databases. 507 """ 508 daemon_thread = True 509 510 def __init__(self, master, document, url): 511 """ 512 Initialize the thread. 513 514 master - NSObject implementing dbReceived: and dbProblem: 515 document - An PackageDatabase 516 url - The PackMan URL 517 """ 518 threading.Thread.__init__(self) 519 self.master = master 520 self.document = document 521 self.url = url 522 523 def run(self): 524 """ 525 Run the thread. This creates a new pimp.PimpDatabase, tells it to 526 download our database and then forwards the database to the 527 master. The last step is done on the main thread because of Cocoa 528 threading issues. 529 """ 530 pool = NSAutoreleasePool.alloc().init() 531 532 try: 533 prefs = pimp.PimpPreferences() 534 if self.url is not None: 535 prefs.pimpDatabase = self.url 536 else: 537 prefs.pimpDatabase = pimp.DEFAULT_PIMPDATABASE 538 539 db = pimp.PimpDatabase(prefs) 540 db.appendURL(prefs.pimpDatabase) 541 542 self.master.performSelectorOnMainThread_withObject_waitUntilDone_( 543 'dbReceived:', (self.document, self.url, db), False) 544 545 except: 546 self.master.performSelectorOnMainThread_withObject_waitUntilDone_( 547 'dbProblem:', (self.document, self.url, sys.exc_info()), False) 548 549 del pool 550 551 552 553 554class InstallerThread (threading.Thread): 555 """ 556 A thread for installing packages. 557 558 Like downloading a database, installing (and downloading!) packages is 559 a time-consuming task that is better done on a seperate thread. 560 """ 561 daemon_thread = True 562 563 def __init__(self, document, installer, packages, verbose, textStorage): 564 threading.Thread.__init__(self) 565 self.document = document 566 self.installer = installer 567 self.packages = packages 568 self.verbose = verbose 569 self.textStorage = textStorage 570 self.result = None 571 572 def write(self, data): 573 self.textStorage.performSelectorOnMainThread_withObject_waitUntilDone_( 574 'appendAttributedString:', 575 NSAttributedString.alloc().initWithString_(data), 576 False) 577 578 def run(self): 579 pool = NSAutoreleasePool.alloc().init() 580 581 if self.verbose: 582 result = self.installer.install(self.packages, self) 583 else: 584 result = self.installer.install(self.packages, None) 585 586 self.write('\nDone.\n') 587 588 self.document.performSelectorOnMainThread_withObject_waitUntilDone_( 589 'installationDone:', None, False) 590 591 del pool 592 593class URLOpener (NSObject): 594 """ 595 Model/controller for the 'File/Open URL...' panel 596 """ 597 okButton = objc.IBOutlet 598 urlField = objc.IBOutlet() 599 600 def __del__(self): 601 # XXX: I'm doing something wrong, this function is never called! 602 print "del URLOpener %#x"%(id(self),) 603 604 605 def awakeFromNib(self): 606 self.urlField.window().makeKeyAndOrderFront_(None) 607 608 @objc.IBAction 609 def doOpenURL_(self, sender): 610 url = self.urlField.stringValue() 611 if not url: 612 return 613 614 # Ask the application delegate to open the selected database 615 NSApplication.sharedApplication().delegate().openDatabase(url) 616 617 @objc.IBAction 618 def controlTextDidChange_(self, sender): 619 """ 620 The value of the URL input field changed, enable the OK button 621 if there is input, disable it otherwise. 622 """ 623 if self.urlField.stringValue() != "": 624 self.okButton.setEnabled_(True) 625 else: 626 self.okButton.setEnabled_(False) 627 628 629 630class PackageManager (NSObject): 631 """ 632 Application controller: application-level callbacks and actions 633 """ 634 favoritesPanel = objc.IBOutlet() 635 favoritesTable = objc.IBOutlet() 636 favoritesTitle = objc.IBOutlet() 637 favoritesURL = objc.IBOutlet() 638 639 # 640 # Standard actions 641 # 642 643 def awakeFromNib(self): 644 """ 645 We've been restored from the NIB 646 """ 647 self.loadFavorites() 648 649 # 650 # Working with favorites 651 # 652 # The favorites are stored in the user defaults for the application. 653 654 def loadFavorites(self): 655 """ 656 Load our favorite database 657 """ 658 self.favorites = NSUserDefaults.standardUserDefaults().arrayForKey_( 659 'favorites') 660 if self.favorites is None: 661 self.favorites = [] 662 else: 663 self.favorites = list(self.favorites) 664 665 def saveFavorites(self): 666 """ 667 Save the favorites database, must be called whenever self.favorites 668 is changed. 669 """ 670 defaults = NSUserDefaults.standardUserDefaults() 671 defaults.setObject_forKey_( 672 self.favorites, 673 'favorites') 674 defaults.synchronize() 675 676 def addFavorite(self, title, url): 677 """ 678 Add a new favorite, and save the database 679 """ 680 self.favorites.append({'title':title, 'URL':url}) 681 self.favoritesTable.reloadData() 682 self.saveFavorites() 683 684 def menuNeedsUpdate_(self, menu): 685 """ 686 We're the delegate for the Favorites menu 687 688 Update the menu: it should list the entries in the favorites database. 689 """ 690 menuLen = menu.numberOfItems() 691 692 # Remove old items 693 for i in range(menuLen-1, 2, -1): 694 menu.removeItemAtIndex_(i) 695 696 # Insert new ones 697 for item in self.favorites: 698 title = item['title'] 699 url = item['URL'] 700 701 mi = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_( 702 title, self.openFavorite_, "") 703 mi.setTarget_(self) 704 mi.setRepresentedObject_(item) 705 menu.addItem_(mi) 706 707 708 def tableViewSelectionDidChange_(self, obj): 709 """ 710 We're the delegate (and datasource) for the favorites list in the 711 edit pane for the favorites. 712 713 Update the input fields to show the current item. 714 """ 715 716 row = self.favoritesTable.selectedRow() 717 if row == -1: 718 self.favoritesTitle.setStringValue_('') 719 self.favoritesURL.setStringValue_('') 720 self.favoritesTitle.setEnabled_(False) 721 self.favoritesURL.setEnabled_(False) 722 else: 723 self.favoritesTitle.setStringValue_(self.favorites[row]['title']) 724 self.favoritesURL.setStringValue_(self.favorites[row]['URL']) 725 self.favoritesTitle.setEnabled_(True) 726 self.favoritesURL.setEnabled_(True) 727 728 729 def numberOfRowsInTableView_(self, view): 730 """ 731 We're the datasource for the favorites list in the Favorites panel 732 """ 733 if not hasattr(self, 'favorites'): 734 return 0 735 736 return len(self.favorites) 737 738 def tableView_objectValueForTableColumn_row_(self, view, col, row): 739 """ 740 We're the datasource for the favorites list in the Favorites panel 741 """ 742 return self.favorites[row]['title'] 743 744 @objc.IBAction 745 def changeFavoritesTitle_(self, sender): 746 """ 747 Update the title of the currently selected favorite item 748 """ 749 row = self.favoritesTable.selectedRow() 750 if row == -1: 751 return 752 753 self.favorites[row]['title'] = self.favoritesTitle.stringValue() 754 self.saveFavorites() 755 756 self.favoritesTable.reloadData() 757 758 759 @objc.IBAction 760 def changeFavoritesUrl_(self, sender): 761 """ 762 Update the URL of the currently selected favorite item 763 """ 764 row = self.favoritesTable.selectedRow() 765 if row == -1: 766 return 767 768 self.favorites[row]['URL'] = self.favoritesURL.stringValue() 769 self.saveFavorites() 770 771 self.favoritesTable.reloadData() 772 773 @objc.IBAction 774 def openFavorite_(self, sender): 775 """ 776 Open a favorite database (action for entries in the Favorites menu) 777 """ 778 self.openDatabase(sender.representedObject()['URL']) 779 780 781 # 782 # Global actions/callbacks 783 # 784 785 def openDatabase(self, url): 786 """ 787 Create a new NSDocument for the database at the specified URL. 788 """ 789 doc = NSDocumentController.sharedDocumentController( 790 ).openUntitledDocumentOfType_display_(DB_FILE_TYPE, False) 791 try: 792 downloader = DownloadThread(self, doc, url) 793 downloader.start() 794 except: 795 doc.close() 796 raise 797 798 def dbReceived_(self, (doc, url, db)): 799 doc.setDB(url, db) 800 doc.showWindows() 801 802 def dbProblem_(self, (doc, url, exc_info)): 803 NSRunAlertPanel( 804 "Cannot open database", 805 "Opening database at %s failed: %s"%(url, exc_info[1]), 806 "OK", None, None) 807 doc.close() 808 809 810 811 @objc.IBAction 812 def openURL_(self, sender): 813 """ 814 The user wants to open a package URL, show the user-interface. 815 """ 816 res = NSBundle.loadNibNamed_owner_('OpenPanel', self) 817 818 @objc.IBAction 819 def openStandardDatabase_(self, sender): 820 """ 821 Open the standard database. 822 """ 823 self.openDatabase(pimp.DEFAULT_PIMPDATABASE) 824 825 def applicationShouldOpenUntitledFile_(self, app): 826 """ 827 The default window is not an untitled window, but the default 828 database 829 """ 830 return False 831 832 def applicationDidFinishLaunching_(self, app): 833 """ 834 The application finished launching, show the default database. 835 """ 836 # XXX: We shouldn't open the standard database if the user explicitly 837 # opened another one! 838 self.openStandardDatabase_(None) 839 840# 841# Set some sensible defaults 842# 843NSUserDefaults.standardUserDefaults().registerDefaults_( 844 { 845 'verbose': True, 846 'installDependencies': True, 847 'showHidden': False, 848 'forceInstallation': False, 849 'installSystemWide': True, 850 }) 851 852# 853# A nasty hack. For some reason sys.prefix is /usr/bin/../../System/..., while 854# it is /System/... in Jack's PackageManager.app. At least one package 855# manager database relies on sys.prefix being /System/... (Bob's additional 856# packages). 857# 858import os 859sys.prefix = os.path.abspath(sys.prefix) 860 861AppHelper.runEventLoop() 862