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