1#!/usr/bin/env python 2 3"""NibClassBuilder.py -- Tools for working with class definitions in 4"Next Interface Builder" files ("nibs"). 5 6NOTE: This module is deprecated and is not supported with modern versions 7of Interface Builder because those uses a NIB format that is not compatibility 8with NibClassBuilder. We have no intention whatsoever to fix the compatibility 9issues, use explicit definitions instead. On a more possitive note: IB 3.0 10fully supports reading class definitions from Python files, which removes 11the reason why we wrote this module in the first place. 12 13 14 15 16Extracting class definitions from nibs. 17 18The module maintains a global set of class definitions, extracted from 19nibs. To add the classes from a nib to this set, use the extractClasses() 20function. It can be called in two ways: 21 22 extractClasses(nibName, bundle=<main-bundle>) 23 This finds the nib by name from a bundle. If no bundle 24 if given, the main bundle is searched. 25 26 extractClasses(path=pathToNib) 27 This uses an explicit path to a nib. 28 29extractClasses() can be called multiple times for the same bundle: the 30results are cached so no almost extra overhead is caused. 31 32 33Using the class definitions. 34 35The module contains a "magic" base (super) class called AutoBaseClass. 36Subclassing AutoBaseClass will invoke some magic that will look up the 37proper base class in the class definitions extracted from the nib(s). 38If you use multiple inheritance to use Cocoa's "informal protocols", 39you _must_ list AutoBaseClass as the first base class. For example: 40 41 class PyModel(AutoBaseClass, NSTableSource): 42 ... 43 44 45The NibInfo class. 46 47The parsing of nibs and collecting the class definition is done by the 48NibInfo class. You normally don't use it directly, but it's here if you 49have special needs. 50 51 52The command line tool. 53 54When run from the command line, this module invokes a simple command 55line program, which you feed paths to nibs. This will print a Python 56template for all classes defined in the nib(s). For more documentation, see 57the commandline_doc variable, or simply run the program without 58arguments. It also contains a simple test program. 59""" 60 61# 62# Written by Just van Rossum <just@letterror.com>, borrowing heavily 63# from Ronald Oussoren's classnib.py module, which this module 64# supercedes. Lots of additional input from Bill Bumgarner and Jack 65# Jansen. 66# 67 68import sys 69import os 70import objc 71 72import warnings 73warnings.warn("PyObjCTools.NibClassBuilder is deprecated, use explicit definitions instead", DeprecationWarning) 74 75 76__all__ = ["AutoBaseClass", "NibInfo", "extractClasses"] 77 78 79from Foundation import NSDictionary, NSObject, NSBundle 80import AppKit # not used directly, but we look up classes from AppKit 81 # dynamically, so it has to be loaded. 82 83 84class NibLoaderError(Exception): pass 85 86 87class ClassInfo: 88 89 attrNames = ("nibs", "name", "super", "actions", "outlets") 90 91 def __repr__(self): 92 items = self.__dict__.items() 93 items.sort() 94 return self.__class__.__name__ + "(" + \ 95 ", ".join([ "%s=%s"%i for i in items ]) + ")" 96 97 def merge(self, other): 98 assert self.name == other.name 99 if self.super != other.super: 100 raise NibLoaderError, \ 101 "Incompatible superclass for %s" % self.name 102 self.nibs = mergeLists(self.nibs, other.nibs) 103 self.outlets = mergeLists(self.outlets, other.outlets) 104 self.actions = mergeLists(self.actions, other.actions) 105 106 def __cmp__(self, other): 107 s = [getattr(self, x) for x in self.attrNames] 108 o = [getattr(other, x) for x in self.attrNames] 109 return cmp(s, o) 110 111 112class NibInfo(object): 113 114 def __init__(self): 115 self.classes = {} 116 self.parsedNibs = {} 117 118 # we implement a subset of the dictionary protocol, for convenience. 119 120 def keys(self): 121 return self.classes.keys() 122 123 def has_key(self, name): 124 return self.classes.has_key(name) 125 126 def len(self): 127 return len(self.classes) 128 129 def __iter__(self): 130 return iter(self.classes) 131 132 def __getitem__(self, name): 133 return self.classes[name] 134 135 def get(self, name, default=None): 136 return self.classes.get(name, default) 137 138 def extractClasses(self, nibName=None, bundle=None, path=None): 139 """Extract the class definitions from a nib. 140 141 The nib can be specified by name, in which case it will be 142 searched in the main bundle (or in the bundle specified), or 143 by path. 144 """ 145 if path is None: 146 self._extractClassesFromNibFromBundle(nibName, bundle) 147 else: 148 if nibName is not None or bundle is not None: 149 raise ValueError, ("Can't specify 'nibName' or " 150 "'bundle' when specifying 'path'") 151 self._extractClassesFromNibFromPath(path) 152 153 def _extractClassesFromNibFromBundle(self, nibName, bundle=None): 154 if not bundle: 155 bundle = objc.currentBundle() 156 if nibName[-4:] == '.nib': 157 resType = None 158 else: 159 resType = "nib" 160 path = bundle.pathForResource_ofType_(nibName, resType) 161 if not path: 162 raise NibLoaderError, ("Could not find nib named '%s' " 163 "in bundle '%s'" % (nibName, bundle)) 164 self._extractClassesFromNibFromPath(path) 165 166 def _extractClassesFromNibFromPath(self, path): 167 path = os.path.normpath(path) 168 if self.parsedNibs.has_key(path): 169 return # we've already parsed this nib 170 nibName = os.path.basename(path) 171 nibInfo = NSDictionary.dictionaryWithContentsOfFile_( 172 os.path.join(path, 'classes.nib')) 173 if nibInfo is None: 174 raise NibLoaderError, "Invalid NIB file [%s]" % path 175 if not nibInfo.has_key('IBVersion'): 176 raise NibLoaderError, "Invalid NIB info" 177 if nibInfo['IBVersion'] != '1': 178 raise NibLoaderError, "Unsupported NIB version" 179 for rawClsInfo in nibInfo['IBClasses']: 180 self._addClass(nibName, rawClsInfo) 181 self.parsedNibs[path] = 1 182 183 def _addClass(self, nibName, rawClsInfo): 184 classes = self.classes 185 name = rawClsInfo['CLASS'] 186 if name == "FirstResponder": 187 # a FirstResponder never needs to be made 188 return 189 190 clsInfo = ClassInfo() 191 clsInfo.nibs = [nibName] # a class can occur in multiple nibs 192 clsInfo.name = name 193 clsInfo.super = rawClsInfo.get('SUPERCLASS', 'NSObject') 194 clsInfo.actions = [a + "_" for a in rawClsInfo.get('ACTIONS', ())] 195 clsInfo.outlets = list(rawClsInfo.get('OUTLETS', ())) 196 197 if not classes.has_key(name): 198 classes[name] = clsInfo 199 else: 200 classes[name].merge(clsInfo) 201 202 def makeClass(self, name, bases, methods): 203 """Construct a new class using the proper base class, as specified 204 in the nib. 205 """ 206 clsInfo = self.classes.get(name) 207 if clsInfo is None: 208 raise NibLoaderError, ("No class named '%s' found in " 209 "nibs" % name) 210 211 try: 212 superClass = objc.lookUpClass(clsInfo.super) 213 except objc.nosuchclass_error: 214 raise NibLoaderError, ("Superclass '%s' for '%s' not " 215 "found." % (clsInfo.super, name)) 216 bases = (superClass,) + bases 217 metaClass = superClass.__class__ 218 219 for o in clsInfo.outlets: 220 if not methods.has_key(o): 221 methods[o] = objc.IBOutlet(o) 222 223 for a in clsInfo.actions: 224 if not methods.has_key(a): 225 # XXX we could issue warning here! 226 pass 227 # don't insert a stub as it effectively disables 228 # AppKit's own method validation 229 #methods[a] = _actionStub 230 231 return metaClass(name, bases, methods) 232 233 def printTemplate(self, file=None): 234 """Print a Python template of classes, matching their specification 235 in the nib(s). 236 """ 237 if file is None: 238 file = sys.stdout 239 writer = IndentWriter(file) 240 self._printTemplateHeader(writer) 241 242 classes = self.classes.values() 243 classes.sort() # see ClassInfo.__cmp__ 244 for clsInfo in classes: 245 if _classExists(clsInfo.super): 246 self._printClass(writer, clsInfo) 247 else: 248 writer.writeln("if 0:") 249 writer.indent() 250 writer.writeln("# *** base class not found: %s" % clsInfo.super) 251 self._printClass(writer, clsInfo) 252 writer.dedent() 253 254 self._printTemplateFooter(writer) 255 256 def _printTemplateHeader(self, writer): 257 nibs = {} 258 for clsInfo in self.classes.values(): 259 for nib in clsInfo.nibs: 260 nibs[nib] = 1 261 262 writer.writeln("import objc") 263 writer.writeln("from Foundation import *") 264 writer.writeln("from AppKit import *") 265 writer.writeln("from PyObjCTools import NibClassBuilder, AppHelper") 266 writer.writeln() 267 writer.writeln() 268 nibs = nibs.keys() 269 nibs.sort() 270 for nib in nibs: 271 assert nib[-4:] == ".nib" 272 nib = nib[:-4] 273 writer.writeln("NibClassBuilder.extractClasses(\"%s\")" % nib) 274 writer.writeln() 275 writer.writeln() 276 277 def _printTemplateFooter(self, writer): 278 writer.writeln() 279 writer.writeln('if __name__ == "__main__":') 280 writer.indent() 281 writer.writeln('AppHelper.runEventLoop()') 282 writer.dedent() 283 284 def _printClass(self, writer, clsInfo): 285 nibs = clsInfo.nibs 286 if len(nibs) > 1: 287 nibs[-2] = nibs[-2] + " and " + nibs[-1] 288 del nibs[-1] 289 nibs = ", ".join(nibs) 290 writer.writeln("# class defined in %s" % nibs) 291 writer.writeln("class %s(NibClassBuilder.AutoBaseClass):" % clsInfo.name) 292 writer.indent() 293 writer.writeln("# the actual base class is %s" % clsInfo.super) 294 outlets = clsInfo.outlets 295 actions = clsInfo.actions 296 if outlets: 297 writer.writeln("# The following outlets are added to the class:") 298 outlets.sort() 299 for o in outlets: 300 writer.writeln("# %s" % o) 301 writer.writeln() 302 if not actions: 303 writer.writeln("pass") 304 writer.writeln() 305 else: 306 if actions: 307 actions.sort() 308 for a in actions: 309 writer.writeln("def %s(self, sender):" % a) 310 writer.indent() 311 writer.writeln("pass") 312 writer.dedent() 313 writer.writeln() 314 writer.writeln() 315 writer.dedent() 316 317 318def _frameworkForClass(className): 319 """Return the name of the framework containing the class.""" 320 try: 321 cls = objc.lookUpClass(className) 322 except objc.error: 323 return "" 324 path = NSBundle.bundleForClass_(cls).bundlePath() 325 if path == "/System/Library/Frameworks/Foundation.framework": 326 return "Foundation" 327 elif path == "/System/Library/Frameworks/AppKit.framework": 328 return "AppKit" 329 else: 330 return "" 331 332 333def _classExists(className): 334 """Return True if a class exists in the Obj-C runtime.""" 335 try: 336 objc.lookUpClass(className) 337 except objc.error: 338 return 0 339 else: 340 return 1 341 342 343class IndentWriter: 344 345 """Simple helper class for generating (Python) code.""" 346 347 def __init__(self, file=None, indentString=" "): 348 if file is None: 349 file = sys.stdout 350 self.file = file 351 self.indentString = indentString 352 self.indentLevel = 0 353 354 def writeln(self, line=""): 355 if line: 356 self.file.write(self.indentLevel * self.indentString + 357 line + "\n") 358 else: 359 self.file.write("\n") 360 361 def indent(self): 362 self.indentLevel += 1 363 364 def dedent(self): 365 assert self.indentLevel > 0, "negative dedent" 366 self.indentLevel -= 1 367 368 369def mergeLists(l1, l2): 370 r = {} 371 for i in l1: 372 r[i] = 1 373 for i in l2: 374 r[i] = 1 375 return r.keys() 376 377 378class _NibClassBuilder(type): 379 380 def _newSubclass(cls, name, bases, methods): 381 # Constructor for AutoBaseClass: create an actual 382 # instance of _NibClassBuilder that can be subclassed 383 # to invoke the magic behavior. 384 return type.__new__(cls, name, bases, methods) 385 _newSubclass = classmethod(_newSubclass) 386 387 def __new__(cls, name, bases, methods): 388 # __new__ would normally create a subclass of cls, but 389 # instead we create a completely different class. 390 if bases and bases[0].__class__ is cls: 391 # get rid of the AutoBaseClass base class 392 bases = bases[1:] 393 return _nibInfo.makeClass(name, bases, methods) 394 395 396# AutoBaseClass is a class that has _NibClassBuilder is its' metaclass. 397# This means that if you subclass from AutoBaseClass, _NibClassBuilder 398# will be used to create the new "subclass". This will however _not_ 399# be a real subclass of AutoBaseClass, but rather a subclass of the 400# Cocoa class specified in the nib. 401AutoBaseClass = _NibClassBuilder._newSubclass("AutoBaseClass", (), {}) 402 403 404_nibInfo = NibInfo() 405 406extractClasses = _nibInfo.extractClasses 407 408 409# 410# The rest of this file is a simple command line tool. 411# 412 413commandline_doc = """\ 414NibLoader.py [-th] nib1 [...nibN] 415 Print an overview of the classes found in the nib file(s) specified, 416 listing their superclass, actions and outlets as Python source. This 417 output can be used as a template or a stub. 418 -t Instead of printing the overview, perform a simple test on the 419 arguments. 420 -h Print this text.""" 421 422def usage(msg, code): 423 if msg: 424 print msg 425 print commandline_doc 426 sys.exit(code) 427 428def test(*nibFiles): 429 for path in nibFiles: 430 print "Loading", path 431 extractClasses(path=path) 432 print 433 classNames = _nibInfo.keys() 434 classNames.sort() 435 for className in classNames: 436 try: 437 # instantiate class, equivalent to 438 # class <className>(AutoBaseClass): 439 # pass 440 cls = type(className.encode('ascii'), (AutoBaseClass,), {}) 441 except NibLoaderError, why: 442 print "*** Failed class: %s; NibLoaderError: %s" % ( 443 className, why[0]) 444 else: 445 print "Created class: %s, superclass: %s" % (cls.__name__, 446 cls.__bases__[0].__name__) 447 448def printTemplate(*nibFiles): 449 for path in nibFiles: 450 extractClasses(path=path) 451 _nibInfo.printTemplate() 452 453def commandline(): 454 import getopt 455 456 try: 457 opts, nibFiles = getopt.getopt(sys.argv[1:], "th") 458 except getopt.error, msg: 459 usage(msg, 1) 460 461 doTest = 0 462 for opt, val in opts: 463 if opt == "-t": 464 doTest = 1 465 elif opt == "-h": 466 usage("", 0) 467 468 if not nibFiles: 469 usage("No nib file specified.", 1) 470 471 if doTest: 472 test(*nibFiles) 473 else: 474 printTemplate(*nibFiles) 475 476 477if __name__ == "__main__": 478 commandline() 479