gensuitemodule.py revision 182b5aca27d376b08a2904bed42b751496f932f3
1"""
2gensuitemodule - Generate an AE suite module from an aete/aeut resource
3
4Based on aete.py.
5
6Reading and understanding this code is left as an exercise to the reader.
7"""
8
9import MacOS
10import EasyDialogs
11import os
12import string
13import sys
14import types
15import StringIO
16import keyword
17import macresource
18import aetools
19import distutils.sysconfig
20import OSATerminology
21from Carbon.Res import *
22import Carbon.Folder
23import MacOS
24import getopt
25import plistlib
26
27_MAC_LIB_FOLDER=os.path.dirname(aetools.__file__)
28DEFAULT_STANDARD_PACKAGEFOLDER=os.path.join(_MAC_LIB_FOLDER, 'lib-scriptpackages')
29DEFAULT_USER_PACKAGEFOLDER=distutils.sysconfig.get_python_lib()
30
31def usage():
32    sys.stderr.write("Usage: %s [opts] application-or-resource-file\n" % sys.argv[0])
33    sys.stderr.write("""Options:
34--output pkgdir  Pathname of the output package (short: -o)
35--resource       Parse resource file in stead of launching application (-r)
36--base package   Use another base package in stead of default StdSuites (-b)
37--edit old=new   Edit suite names, use empty new to skip a suite (-e)
38--creator code   Set creator code for package (-c)
39--dump           Dump aete resource to stdout in stead of creating module (-d)
40--verbose        Tell us what happens (-v)
41""")
42    sys.exit(1)
43
44def main():
45    if len(sys.argv) > 1:
46        SHORTOPTS = "rb:o:e:c:dv"
47        LONGOPTS = ("resource", "base=", "output=", "edit=", "creator=", "dump", "verbose")
48        try:
49            opts, args = getopt.getopt(sys.argv[1:], SHORTOPTS, LONGOPTS)
50        except getopt.GetoptError:
51            usage()
52
53        process_func = processfile
54        basepkgname = 'StdSuites'
55        output = None
56        edit_modnames = []
57        creatorsignature = None
58        dump = None
59        verbose = None
60
61        for o, a in opts:
62            if o in ('-r', '--resource'):
63                process_func = processfile_fromresource
64            if o in ('-b', '--base'):
65                basepkgname = a
66            if o in ('-o', '--output'):
67                output = a
68            if o in ('-e', '--edit'):
69                split = a.split('=')
70                if len(split) != 2:
71                    usage()
72                edit_modnames.append(split)
73            if o in ('-c', '--creator'):
74                if len(a) != 4:
75                    sys.stderr.write("creator must be 4-char string\n")
76                    sys.exit(1)
77                creatorsignature = a
78            if o in ('-d', '--dump'):
79                dump = sys.stdout
80            if o in ('-v', '--verbose'):
81                verbose = sys.stderr
82
83
84        if output and len(args) > 1:
85            sys.stderr.write("%s: cannot specify --output with multiple inputs\n" % sys.argv[0])
86            sys.exit(1)
87
88        for filename in args:
89            process_func(filename, output=output, basepkgname=basepkgname,
90                edit_modnames=edit_modnames, creatorsignature=creatorsignature,
91                dump=dump, verbose=verbose)
92    else:
93        main_interactive()
94
95def main_interactive(interact=0, basepkgname='StdSuites'):
96    if interact:
97        # Ask for save-filename for each module
98        edit_modnames = None
99    else:
100        # Use default filenames for each module
101        edit_modnames = []
102    appsfolder = Carbon.Folder.FSFindFolder(-32765, 'apps', 0)
103    filename = EasyDialogs.AskFileForOpen(
104        message='Select scriptable application',
105        dialogOptionFlags=0x1056,       # allow selection of .app bundles
106        defaultLocation=appsfolder)
107    if not filename:
108        return
109    if not is_scriptable(filename):
110        if EasyDialogs.AskYesNoCancel(
111                "Warning: application does not seem scriptable",
112                yes="Continue", default=2, no="") <= 0:
113            return
114    try:
115        processfile(filename, edit_modnames=edit_modnames, basepkgname=basepkgname,
116        verbose=sys.stderr)
117    except MacOS.Error, arg:
118        print "Error getting terminology:", arg
119        print "Retry, manually parsing resources"
120        processfile_fromresource(filename, edit_modnames=edit_modnames,
121            basepkgname=basepkgname, verbose=sys.stderr)
122
123def is_scriptable(application):
124    """Return true if the application is scriptable"""
125    if os.path.isdir(application):
126        plistfile = os.path.join(application, 'Contents', 'Info.plist')
127        if not os.path.exists(plistfile):
128            return False
129        plist = plistlib.Plist.fromFile(plistfile)
130        return plist.get('NSAppleScriptEnabled', False)
131    # If it is a file test for an aete/aeut resource.
132    currf = CurResFile()
133    try:
134        refno = macresource.open_pathname(application)
135    except MacOS.Error:
136        return False
137    UseResFile(refno)
138    n_terminology = Count1Resources('aete') + Count1Resources('aeut') + \
139        Count1Resources('scsz') + Count1Resources('osiz')
140    CloseResFile(refno)
141    UseResFile(currf)
142    return n_terminology > 0
143
144def processfile_fromresource(fullname, output=None, basepkgname=None,
145        edit_modnames=None, creatorsignature=None, dump=None, verbose=None):
146    """Process all resources in a single file"""
147    if not is_scriptable(fullname) and verbose:
148        print >>verbose, "Warning: app does not seem scriptable: %s" % fullname
149    cur = CurResFile()
150    if verbose:
151        print >>verbose, "Processing", fullname
152    rf = macresource.open_pathname(fullname)
153    try:
154        UseResFile(rf)
155        resources = []
156        for i in range(Count1Resources('aete')):
157            res = Get1IndResource('aete', 1+i)
158            resources.append(res)
159        for i in range(Count1Resources('aeut')):
160            res = Get1IndResource('aeut', 1+i)
161            resources.append(res)
162        if verbose:
163            print >>verbose, "\nLISTING aete+aeut RESOURCES IN", repr(fullname)
164        aetelist = []
165        for res in resources:
166            if verbose:
167                print >>verbose, "decoding", res.GetResInfo(), "..."
168            data = res.data
169            aete = decode(data, verbose)
170            aetelist.append((aete, res.GetResInfo()))
171    finally:
172        if rf <> cur:
173            CloseResFile(rf)
174            UseResFile(cur)
175    # switch back (needed for dialogs in Python)
176    UseResFile(cur)
177    if dump:
178        dumpaetelist(aetelist, dump)
179    compileaetelist(aetelist, fullname, output=output,
180        basepkgname=basepkgname, edit_modnames=edit_modnames,
181        creatorsignature=creatorsignature, verbose=verbose)
182
183def processfile(fullname, output=None, basepkgname=None,
184        edit_modnames=None, creatorsignature=None, dump=None,
185        verbose=None):
186    """Ask an application for its terminology and process that"""
187    if not is_scriptable(fullname) and verbose:
188        print >>verbose, "Warning: app does not seem scriptable: %s" % fullname
189    if verbose:
190        print >>verbose, "\nASKING FOR aete DICTIONARY IN", repr(fullname)
191    try:
192        aedescobj, launched = OSATerminology.GetAppTerminology(fullname)
193    except MacOS.Error, arg:
194        if arg[0] in (-1701, -192): # errAEDescNotFound, resNotFound
195            if verbose:
196                print >>verbose, "GetAppTerminology failed with errAEDescNotFound/resNotFound, trying manually"
197            aedata, sig = getappterminology(fullname, verbose=verbose)
198            if not creatorsignature:
199                creatorsignature = sig
200        else:
201            raise
202    else:
203        if launched:
204            if verbose:
205                print >>verbose, "Launched", fullname
206        raw = aetools.unpack(aedescobj)
207        if not raw:
208            if verbose:
209                print >>verbose, 'Unpack returned empty value:', raw
210            return
211        if not raw[0].data:
212            if verbose:
213                print >>verbose, 'Unpack returned value without data:', raw
214            return
215        aedata = raw[0]
216    aete = decode(aedata.data, verbose)
217    if dump:
218        dumpaetelist([aete], dump)
219        return
220    compileaete(aete, None, fullname, output=output, basepkgname=basepkgname,
221        creatorsignature=creatorsignature, edit_modnames=edit_modnames,
222        verbose=verbose)
223
224def getappterminology(fullname, verbose=None):
225    """Get application terminology by sending an AppleEvent"""
226    # First check that we actually can send AppleEvents
227    if not MacOS.WMAvailable():
228        raise RuntimeError, "Cannot send AppleEvents, no access to window manager"
229    # Next, a workaround for a bug in MacOS 10.2: sending events will hang unless
230    # you have created an event loop first.
231    import Carbon.Evt
232    Carbon.Evt.WaitNextEvent(0,0)
233    if os.path.isdir(fullname):
234        # Now get the signature of the application, hoping it is a bundle
235        pkginfo = os.path.join(fullname, 'Contents', 'PkgInfo')
236        if not os.path.exists(pkginfo):
237            raise RuntimeError, "No PkgInfo file found"
238        tp_cr = open(pkginfo, 'rb').read()
239        cr = tp_cr[4:8]
240    else:
241        # Assume it is a file
242        cr, tp = MacOS.GetCreatorAndType(fullname)
243    # Let's talk to it and ask for its AETE
244    talker = aetools.TalkTo(cr)
245    try:
246        talker._start()
247    except (MacOS.Error, aetools.Error), arg:
248        if verbose:
249            print >>verbose, 'Warning: start() failed, continuing anyway:', arg
250    reply = talker.send("ascr", "gdte")
251    #reply2 = talker.send("ascr", "gdut")
252    # Now pick the bits out of the return that we need.
253    return reply[1]['----'], cr
254
255
256def compileaetelist(aetelist, fullname, output=None, basepkgname=None,
257            edit_modnames=None, creatorsignature=None, verbose=None):
258    for aete, resinfo in aetelist:
259        compileaete(aete, resinfo, fullname, output=output,
260            basepkgname=basepkgname, edit_modnames=edit_modnames,
261            creatorsignature=creatorsignature, verbose=verbose)
262
263def dumpaetelist(aetelist, output):
264    import pprint
265    pprint.pprint(aetelist, output)
266
267def decode(data, verbose=None):
268    """Decode a resource into a python data structure"""
269    f = StringIO.StringIO(data)
270    aete = generic(getaete, f)
271    aete = simplify(aete)
272    processed = f.tell()
273    unprocessed = len(f.read())
274    total = f.tell()
275    if unprocessed and verbose:
276        verbose.write("%d processed + %d unprocessed = %d total\n" %
277                         (processed, unprocessed, total))
278    return aete
279
280def simplify(item):
281    """Recursively replace singleton tuples by their constituent item"""
282    if type(item) is types.ListType:
283        return map(simplify, item)
284    elif type(item) == types.TupleType and len(item) == 2:
285        return simplify(item[1])
286    else:
287        return item
288
289
290# Here follows the aete resource decoder.
291# It is presented bottom-up instead of top-down because there are  direct
292# references to the lower-level part-decoders from the high-level part-decoders.
293
294def getbyte(f, *args):
295    c = f.read(1)
296    if not c:
297        raise EOFError, 'in getbyte' + str(args)
298    return ord(c)
299
300def getword(f, *args):
301    getalign(f)
302    s = f.read(2)
303    if len(s) < 2:
304        raise EOFError, 'in getword' + str(args)
305    return (ord(s[0])<<8) | ord(s[1])
306
307def getlong(f, *args):
308    getalign(f)
309    s = f.read(4)
310    if len(s) < 4:
311        raise EOFError, 'in getlong' + str(args)
312    return (ord(s[0])<<24) | (ord(s[1])<<16) | (ord(s[2])<<8) | ord(s[3])
313
314def getostype(f, *args):
315    getalign(f)
316    s = f.read(4)
317    if len(s) < 4:
318        raise EOFError, 'in getostype' + str(args)
319    return s
320
321def getpstr(f, *args):
322    c = f.read(1)
323    if len(c) < 1:
324        raise EOFError, 'in getpstr[1]' + str(args)
325    nbytes = ord(c)
326    if nbytes == 0: return ''
327    s = f.read(nbytes)
328    if len(s) < nbytes:
329        raise EOFError, 'in getpstr[2]' + str(args)
330    return s
331
332def getalign(f):
333    if f.tell() & 1:
334        c = f.read(1)
335        ##if c <> '\0':
336        ##  print align:', repr(c)
337
338def getlist(f, description, getitem):
339    count = getword(f)
340    list = []
341    for i in range(count):
342        list.append(generic(getitem, f))
343        getalign(f)
344    return list
345
346def alt_generic(what, f, *args):
347    print "generic", repr(what), args
348    res = vageneric(what, f, args)
349    print '->', repr(res)
350    return res
351
352def generic(what, f, *args):
353    if type(what) == types.FunctionType:
354        return apply(what, (f,) + args)
355    if type(what) == types.ListType:
356        record = []
357        for thing in what:
358            item = apply(generic, thing[:1] + (f,) + thing[1:])
359            record.append((thing[1], item))
360        return record
361    return "BAD GENERIC ARGS: %r" % (what,)
362
363getdata = [
364    (getostype, "type"),
365    (getpstr, "description"),
366    (getword, "flags")
367    ]
368getargument = [
369    (getpstr, "name"),
370    (getostype, "keyword"),
371    (getdata, "what")
372    ]
373getevent = [
374    (getpstr, "name"),
375    (getpstr, "description"),
376    (getostype, "suite code"),
377    (getostype, "event code"),
378    (getdata, "returns"),
379    (getdata, "accepts"),
380    (getlist, "optional arguments", getargument)
381    ]
382getproperty = [
383    (getpstr, "name"),
384    (getostype, "code"),
385    (getdata, "what")
386    ]
387getelement = [
388    (getostype, "type"),
389    (getlist, "keyform", getostype)
390    ]
391getclass = [
392    (getpstr, "name"),
393    (getostype, "class code"),
394    (getpstr, "description"),
395    (getlist, "properties", getproperty),
396    (getlist, "elements", getelement)
397    ]
398getcomparison = [
399    (getpstr, "operator name"),
400    (getostype, "operator ID"),
401    (getpstr, "operator comment"),
402    ]
403getenumerator = [
404    (getpstr, "enumerator name"),
405    (getostype, "enumerator ID"),
406    (getpstr, "enumerator comment")
407    ]
408getenumeration = [
409    (getostype, "enumeration ID"),
410    (getlist, "enumerator", getenumerator)
411    ]
412getsuite = [
413    (getpstr, "suite name"),
414    (getpstr, "suite description"),
415    (getostype, "suite ID"),
416    (getword, "suite level"),
417    (getword, "suite version"),
418    (getlist, "events", getevent),
419    (getlist, "classes", getclass),
420    (getlist, "comparisons", getcomparison),
421    (getlist, "enumerations", getenumeration)
422    ]
423getaete = [
424    (getword, "major/minor version in BCD"),
425    (getword, "language code"),
426    (getword, "script code"),
427    (getlist, "suites", getsuite)
428    ]
429
430def compileaete(aete, resinfo, fname, output=None, basepkgname=None,
431        edit_modnames=None, creatorsignature=None, verbose=None):
432    """Generate code for a full aete resource. fname passed for doc purposes"""
433    [version, language, script, suites] = aete
434    major, minor = divmod(version, 256)
435    if not creatorsignature:
436        creatorsignature, dummy = MacOS.GetCreatorAndType(fname)
437    packagename = identify(os.path.splitext(os.path.basename(fname))[0])
438    if language:
439        packagename = packagename+'_lang%d'%language
440    if script:
441        packagename = packagename+'_script%d'%script
442    if len(packagename) > 27:
443        packagename = packagename[:27]
444    if output:
445        # XXXX Put this in site-packages if it isn't a full pathname?
446        if not os.path.exists(output):
447            os.mkdir(output)
448        pathname = output
449    else:
450        pathname = EasyDialogs.AskFolder(message='Create and select package folder for %s'%packagename,
451            defaultLocation=DEFAULT_USER_PACKAGEFOLDER)
452        output = pathname
453    if not pathname:
454        return
455    packagename = os.path.split(os.path.normpath(pathname))[1]
456    if not basepkgname:
457        basepkgname = EasyDialogs.AskFolder(message='Package folder for base suite (usually StdSuites)',
458            defaultLocation=DEFAULT_STANDARD_PACKAGEFOLDER)
459    if basepkgname:
460        dirname, basepkgname = os.path.split(os.path.normpath(basepkgname))
461        if dirname and not dirname in sys.path:
462            sys.path.insert(0, dirname)
463        basepackage = __import__(basepkgname)
464    else:
465        basepackage = None
466    suitelist = []
467    allprecompinfo = []
468    allsuites = []
469    for suite in suites:
470        compiler = SuiteCompiler(suite, basepackage, output, edit_modnames, verbose)
471        code, modname, precompinfo = compiler.precompilesuite()
472        if not code:
473            continue
474        allprecompinfo = allprecompinfo + precompinfo
475        suiteinfo = suite, pathname, modname
476        suitelist.append((code, modname))
477        allsuites.append(compiler)
478    for compiler in allsuites:
479        compiler.compilesuite(major, minor, language, script, fname, allprecompinfo)
480    initfilename = os.path.join(output, '__init__.py')
481    fp = open(initfilename, 'w')
482    MacOS.SetCreatorAndType(initfilename, 'Pyth', 'TEXT')
483    fp.write('"""\n')
484    fp.write("Package generated from %s\n"%ascii(fname))
485    if resinfo:
486        fp.write("Resource %s resid %d %s\n"%(ascii(resinfo[1]), resinfo[0], ascii(resinfo[2])))
487    fp.write('"""\n')
488    fp.write('import aetools\n')
489    fp.write('Error = aetools.Error\n')
490    suitelist.sort()
491    for code, modname in suitelist:
492        fp.write("import %s\n" % modname)
493    fp.write("\n\n_code_to_module = {\n")
494    for code, modname in suitelist:
495        fp.write("    '%s' : %s,\n"%(ascii(code), modname))
496    fp.write("}\n\n")
497    fp.write("\n\n_code_to_fullname = {\n")
498    for code, modname in suitelist:
499        fp.write("    '%s' : ('%s.%s', '%s'),\n"%(ascii(code), packagename, modname, modname))
500    fp.write("}\n\n")
501    for code, modname in suitelist:
502        fp.write("from %s import *\n"%modname)
503
504    # Generate property dicts and element dicts for all types declared in this module
505    fp.write("\ndef getbaseclasses(v):\n")
506    fp.write("    if not getattr(v, '_propdict', None):\n")
507    fp.write("        v._propdict = {}\n")
508    fp.write("        v._elemdict = {}\n")
509    fp.write("        for superclassname in getattr(v, '_superclassnames', []):\n")
510    fp.write("            superclass = eval(superclassname)\n")
511    fp.write("            getbaseclasses(superclass)\n")
512    fp.write("            v._propdict.update(getattr(superclass, '_propdict', {}))\n")
513    fp.write("            v._elemdict.update(getattr(superclass, '_elemdict', {}))\n")
514    fp.write("        v._propdict.update(getattr(v, '_privpropdict', {}))\n")
515    fp.write("        v._elemdict.update(getattr(v, '_privelemdict', {}))\n")
516    fp.write("\n")
517    fp.write("import StdSuites\n")
518    allprecompinfo.sort()
519    if allprecompinfo:
520        fp.write("\n#\n# Set property and element dictionaries now that all classes have been defined\n#\n")
521        for codenamemapper in allprecompinfo:
522            for k, v in codenamemapper.getall('class'):
523                fp.write("getbaseclasses(%s)\n" % v)
524
525    # Generate a code-to-name mapper for all of the types (classes) declared in this module
526    application_class = None
527    if allprecompinfo:
528        fp.write("\n#\n# Indices of types declared in this module\n#\n")
529        fp.write("_classdeclarations = {\n")
530        for codenamemapper in allprecompinfo:
531            for k, v in codenamemapper.getall('class'):
532                fp.write("    %r : %s,\n" % (k, v))
533                if k == 'capp':
534                    application_class = v
535        fp.write("}\n")
536
537
538    if suitelist:
539        fp.write("\n\nclass %s(%s_Events"%(packagename, suitelist[0][1]))
540        for code, modname in suitelist[1:]:
541            fp.write(",\n        %s_Events"%modname)
542        fp.write(",\n        aetools.TalkTo):\n")
543        fp.write("    _signature = %r\n\n"%(creatorsignature,))
544        fp.write("    _moduleName = '%s'\n\n"%packagename)
545        if application_class:
546            fp.write("    _elemdict = %s._elemdict\n" % application_class)
547            fp.write("    _propdict = %s._propdict\n" % application_class)
548    fp.close()
549
550class SuiteCompiler:
551    def __init__(self, suite, basepackage, output, edit_modnames, verbose):
552        self.suite = suite
553        self.basepackage = basepackage
554        self.edit_modnames = edit_modnames
555        self.output = output
556        self.verbose = verbose
557
558        # Set by precompilesuite
559        self.pathname = None
560        self.modname = None
561
562        # Set by compilesuite
563        self.fp = None
564        self.basemodule = None
565        self.enumsneeded = {}
566
567    def precompilesuite(self):
568        """Parse a single suite without generating the output. This step is needed
569        so we can resolve recursive references by suites to enums/comps/etc declared
570        in other suites"""
571        [name, desc, code, level, version, events, classes, comps, enums] = self.suite
572
573        modname = identify(name)
574        if len(modname) > 28:
575            modname = modname[:27]
576        if self.edit_modnames is None:
577            self.pathname = EasyDialogs.AskFileForSave(message='Python output file',
578                savedFileName=modname+'.py')
579        else:
580            for old, new in self.edit_modnames:
581                if old == modname:
582                    modname = new
583            if modname:
584                self.pathname = os.path.join(self.output, modname + '.py')
585            else:
586                self.pathname = None
587        if not self.pathname:
588            return None, None, None
589
590        self.modname = os.path.splitext(os.path.split(self.pathname)[1])[0]
591
592        if self.basepackage and self.basepackage._code_to_module.has_key(code):
593            # We are an extension of a baseclass (usually an application extending
594            # Standard_Suite or so). Import everything from our base module
595            basemodule = self.basepackage._code_to_module[code]
596        else:
597            # We are not an extension.
598            basemodule = None
599
600        self.enumsneeded = {}
601        for event in events:
602            self.findenumsinevent(event)
603
604        objc = ObjectCompiler(None, self.modname, basemodule, interact=(self.edit_modnames is None),
605            verbose=self.verbose)
606        for cls in classes:
607            objc.compileclass(cls)
608        for cls in classes:
609            objc.fillclasspropsandelems(cls)
610        for comp in comps:
611            objc.compilecomparison(comp)
612        for enum in enums:
613            objc.compileenumeration(enum)
614
615        for enum in self.enumsneeded.keys():
616            objc.checkforenum(enum)
617
618        objc.dumpindex()
619
620        precompinfo = objc.getprecompinfo(self.modname)
621
622        return code, self.modname, precompinfo
623
624    def compilesuite(self, major, minor, language, script, fname, precompinfo):
625        """Generate code for a single suite"""
626        [name, desc, code, level, version, events, classes, comps, enums] = self.suite
627        # Sort various lists, so re-generated source is easier compared
628        def class_sorter(k1, k2):
629            """Sort classes by code, and make sure main class sorts before synonyms"""
630            # [name, code, desc, properties, elements] = cls
631            if k1[1] < k2[1]: return -1
632            if k1[1] > k2[1]: return 1
633            if not k2[3] or k2[3][0][1] == 'c@#!':
634                # This is a synonym, the other one is better
635                return -1
636            if not k1[3] or k1[3][0][1] == 'c@#!':
637                # This is a synonym, the other one is better
638                return 1
639            return 0
640
641        events.sort()
642        classes.sort(class_sorter)
643        comps.sort()
644        enums.sort()
645
646        self.fp = fp = open(self.pathname, 'w')
647        MacOS.SetCreatorAndType(self.pathname, 'Pyth', 'TEXT')
648
649        fp.write('"""Suite %s: %s\n' % (ascii(name), ascii(desc)))
650        fp.write("Level %d, version %d\n\n" % (level, version))
651        fp.write("Generated from %s\n"%ascii(fname))
652        fp.write("AETE/AEUT resource version %d/%d, language %d, script %d\n" % \
653            (major, minor, language, script))
654        fp.write('"""\n\n')
655
656        fp.write('import aetools\n')
657        fp.write('import MacOS\n\n')
658        fp.write("_code = %r\n\n"% (code,))
659        if self.basepackage and self.basepackage._code_to_module.has_key(code):
660            # We are an extension of a baseclass (usually an application extending
661            # Standard_Suite or so). Import everything from our base module
662            fp.write('from %s import *\n'%self.basepackage._code_to_fullname[code][0])
663            basemodule = self.basepackage._code_to_module[code]
664        elif self.basepackage and self.basepackage._code_to_module.has_key(code.lower()):
665            # This is needed by CodeWarrior and some others.
666            fp.write('from %s import *\n'%self.basepackage._code_to_fullname[code.lower()][0])
667            basemodule = self.basepackage._code_to_module[code.lower()]
668        else:
669            # We are not an extension.
670            basemodule = None
671        self.basemodule = basemodule
672        self.compileclassheader()
673
674        self.enumsneeded = {}
675        if events:
676            for event in events:
677                self.compileevent(event)
678        else:
679            fp.write("    pass\n\n")
680
681        objc = ObjectCompiler(fp, self.modname, basemodule, precompinfo, interact=(self.edit_modnames is None),
682            verbose=self.verbose)
683        for cls in classes:
684            objc.compileclass(cls)
685        for cls in classes:
686            objc.fillclasspropsandelems(cls)
687        for comp in comps:
688            objc.compilecomparison(comp)
689        for enum in enums:
690            objc.compileenumeration(enum)
691
692        for enum in self.enumsneeded.keys():
693            objc.checkforenum(enum)
694
695        objc.dumpindex()
696
697    def compileclassheader(self):
698        """Generate class boilerplate"""
699        classname = '%s_Events'%self.modname
700        if self.basemodule:
701            modshortname = string.split(self.basemodule.__name__, '.')[-1]
702            baseclassname = '%s_Events'%modshortname
703            self.fp.write("class %s(%s):\n\n"%(classname, baseclassname))
704        else:
705            self.fp.write("class %s:\n\n"%classname)
706
707    def compileevent(self, event):
708        """Generate code for a single event"""
709        [name, desc, code, subcode, returns, accepts, arguments] = event
710        fp = self.fp
711        funcname = identify(name)
712        #
713        # generate name->keyword map
714        #
715        if arguments:
716            fp.write("    _argmap_%s = {\n"%funcname)
717            for a in arguments:
718                fp.write("        %r : %r,\n"%(identify(a[0]), a[1]))
719            fp.write("    }\n\n")
720
721        #
722        # Generate function header
723        #
724        has_arg = (not is_null(accepts))
725        opt_arg = (has_arg and is_optional(accepts))
726
727        fp.write("    def %s(self, "%funcname)
728        if has_arg:
729            if not opt_arg:
730                fp.write("_object, ")       # Include direct object, if it has one
731            else:
732                fp.write("_object=None, ")  # Also include if it is optional
733        else:
734            fp.write("_no_object=None, ")   # For argument checking
735        fp.write("_attributes={}, **_arguments):\n")    # include attribute dict and args
736        #
737        # Generate doc string (important, since it may be the only
738        # available documentation, due to our name-remaping)
739        #
740        fp.write('        """%s: %s\n'%(ascii(name), ascii(desc)))
741        if has_arg:
742            fp.write("        Required argument: %s\n"%getdatadoc(accepts))
743        elif opt_arg:
744            fp.write("        Optional argument: %s\n"%getdatadoc(accepts))
745        for arg in arguments:
746            fp.write("        Keyword argument %s: %s\n"%(identify(arg[0]),
747                    getdatadoc(arg[2])))
748        fp.write("        Keyword argument _attributes: AppleEvent attribute dictionary\n")
749        if not is_null(returns):
750            fp.write("        Returns: %s\n"%getdatadoc(returns))
751        fp.write('        """\n')
752        #
753        # Fiddle the args so everything ends up in 'arguments' dictionary
754        #
755        fp.write("        _code = %r\n"% (code,))
756        fp.write("        _subcode = %r\n\n"% (subcode,))
757        #
758        # Do keyword name substitution
759        #
760        if arguments:
761            fp.write("        aetools.keysubst(_arguments, self._argmap_%s)\n"%funcname)
762        else:
763            fp.write("        if _arguments: raise TypeError, 'No optional args expected'\n")
764        #
765        # Stuff required arg (if there is one) into arguments
766        #
767        if has_arg:
768            fp.write("        _arguments['----'] = _object\n")
769        elif opt_arg:
770            fp.write("        if _object:\n")
771            fp.write("            _arguments['----'] = _object\n")
772        else:
773            fp.write("        if _no_object != None: raise TypeError, 'No direct arg expected'\n")
774        fp.write("\n")
775        #
776        # Do enum-name substitution
777        #
778        for a in arguments:
779            if is_enum(a[2]):
780                kname = a[1]
781                ename = a[2][0]
782                if ename <> '****':
783                    fp.write("        aetools.enumsubst(_arguments, %r, _Enum_%s)\n" %
784                        (kname, identify(ename)))
785                    self.enumsneeded[ename] = 1
786        fp.write("\n")
787        #
788        # Do the transaction
789        #
790        fp.write("        _reply, _arguments, _attributes = self.send(_code, _subcode,\n")
791        fp.write("                _arguments, _attributes)\n")
792        #
793        # Error handling
794        #
795        fp.write("        if _arguments.get('errn', 0):\n")
796        fp.write("            raise aetools.Error, aetools.decodeerror(_arguments)\n")
797        fp.write("        # XXXX Optionally decode result\n")
798        #
799        # Decode result
800        #
801        fp.write("        if _arguments.has_key('----'):\n")
802        if is_enum(returns):
803            fp.write("            # XXXX Should do enum remapping here...\n")
804        fp.write("            return _arguments['----']\n")
805        fp.write("\n")
806
807    def findenumsinevent(self, event):
808        """Find all enums for a single event"""
809        [name, desc, code, subcode, returns, accepts, arguments] = event
810        for a in arguments:
811            if is_enum(a[2]):
812                ename = a[2][0]
813                if ename <> '****':
814                    self.enumsneeded[ename] = 1
815
816#
817# This class stores the code<->name translations for a single module. It is used
818# to keep the information while we're compiling the module, but we also keep these objects
819# around so if one suite refers to, say, an enum in another suite we know where to
820# find it. Finally, if we really can't find a code, the user can add modules by
821# hand.
822#
823class CodeNameMapper:
824
825    def __init__(self, interact=1, verbose=None):
826        self.code2name = {
827            "property" : {},
828            "class" : {},
829            "enum" : {},
830            "comparison" : {},
831        }
832        self.name2code =  {
833            "property" : {},
834            "class" : {},
835            "enum" : {},
836            "comparison" : {},
837        }
838        self.modulename = None
839        self.star_imported = 0
840        self.can_interact = interact
841        self.verbose = verbose
842
843    def addnamecode(self, type, name, code):
844        self.name2code[type][name] = code
845        if not self.code2name[type].has_key(code):
846            self.code2name[type][code] = name
847
848    def hasname(self, name):
849        for dict in self.name2code.values():
850            if dict.has_key(name):
851                return True
852        return False
853
854    def hascode(self, type, code):
855        return self.code2name[type].has_key(code)
856
857    def findcodename(self, type, code):
858        if not self.hascode(type, code):
859            return None, None, None
860        name = self.code2name[type][code]
861        if self.modulename and not self.star_imported:
862            qualname = '%s.%s'%(self.modulename, name)
863        else:
864            qualname = name
865        return name, qualname, self.modulename
866
867    def getall(self, type):
868        return self.code2name[type].items()
869
870    def addmodule(self, module, name, star_imported):
871        self.modulename = name
872        self.star_imported = star_imported
873        for code, name in module._propdeclarations.items():
874            self.addnamecode('property', name, code)
875        for code, name in module._classdeclarations.items():
876            self.addnamecode('class', name, code)
877        for code in module._enumdeclarations.keys():
878            self.addnamecode('enum', '_Enum_'+identify(code), code)
879        for code, name in module._compdeclarations.items():
880            self.addnamecode('comparison', name, code)
881
882    def prepareforexport(self, name=None):
883        if not self.modulename:
884            self.modulename = name
885        return self
886
887class ObjectCompiler:
888    def __init__(self, fp, modname, basesuite, othernamemappers=None, interact=1,
889            verbose=None):
890        self.fp = fp
891        self.verbose = verbose
892        self.basesuite = basesuite
893        self.can_interact = interact
894        self.modulename = modname
895        self.namemappers = [CodeNameMapper(self.can_interact, self.verbose)]
896        if othernamemappers:
897            self.othernamemappers = othernamemappers[:]
898        else:
899            self.othernamemappers = []
900        if basesuite:
901            basemapper = CodeNameMapper(self.can_interact, self.verbose)
902            basemapper.addmodule(basesuite, '', 1)
903            self.namemappers.append(basemapper)
904
905    def getprecompinfo(self, modname):
906        list = []
907        for mapper in self.namemappers:
908            emapper = mapper.prepareforexport(modname)
909            if emapper:
910                list.append(emapper)
911        return list
912
913    def findcodename(self, type, code):
914        while 1:
915            # First try: check whether we already know about this code.
916            for mapper in self.namemappers:
917                if mapper.hascode(type, code):
918                    return mapper.findcodename(type, code)
919            # Second try: maybe one of the other modules knows about it.
920            for mapper in self.othernamemappers:
921                if mapper.hascode(type, code):
922                    self.othernamemappers.remove(mapper)
923                    self.namemappers.append(mapper)
924                    if self.fp:
925                        self.fp.write("import %s\n"%mapper.modulename)
926                    break
927            else:
928                # If all this has failed we ask the user for a guess on where it could
929                # be and retry.
930                if self.fp:
931                    m = self.askdefinitionmodule(type, code)
932                else:
933                    m = None
934                if not m: return None, None, None
935                mapper = CodeNameMapper(self.can_interact, self.verbose)
936                mapper.addmodule(m, m.__name__, 0)
937                self.namemappers.append(mapper)
938
939    def hasname(self, name):
940        for mapper in self.othernamemappers:
941            if mapper.hasname(name) and mapper.modulename != self.modulename:
942                if self.verbose:
943                    print >>self.verbose, "Duplicate Python identifier:", name, self.modulename, mapper.modulename
944                return True
945        return False
946
947    def askdefinitionmodule(self, type, code):
948        if not self.can_interact:
949            if self.verbose:
950                print >>self.verbose, "** No definition for %s '%s' found" % (type, code)
951            return None
952        path = EasyDialogs.AskFileForSave(message='Where is %s %s declared?'%(type, code))
953        if not path: return
954        path, file = os.path.split(path)
955        modname = os.path.splitext(file)[0]
956        if not path in sys.path:
957            sys.path.insert(0, path)
958        m = __import__(modname)
959        self.fp.write("import %s\n"%modname)
960        return m
961
962    def compileclass(self, cls):
963        [name, code, desc, properties, elements] = cls
964        pname = identify(name)
965        if self.namemappers[0].hascode('class', code):
966            # plural forms and such
967            othername, dummy, dummy = self.namemappers[0].findcodename('class', code)
968            if self.fp:
969                self.fp.write("\n%s = %s\n"%(pname, othername))
970        else:
971            if self.fp:
972                self.fp.write('\nclass %s(aetools.ComponentItem):\n' % pname)
973                self.fp.write('    """%s - %s """\n' % (ascii(name), ascii(desc)))
974                self.fp.write('    want = %r\n' % (code,))
975        self.namemappers[0].addnamecode('class', pname, code)
976        is_application_class = (code == 'capp')
977        properties.sort()
978        for prop in properties:
979            self.compileproperty(prop, is_application_class)
980        elements.sort()
981        for elem in elements:
982            self.compileelement(elem)
983
984    def compileproperty(self, prop, is_application_class=False):
985        [name, code, what] = prop
986        if code == 'c@#!':
987            # Something silly with plurals. Skip it.
988            return
989        pname = identify(name)
990        if self.namemappers[0].hascode('property', code):
991            # plural forms and such
992            othername, dummy, dummy = self.namemappers[0].findcodename('property', code)
993            if pname == othername:
994                return
995            if self.fp:
996                self.fp.write("\n_Prop_%s = _Prop_%s\n"%(pname, othername))
997        else:
998            if self.fp:
999                self.fp.write("class _Prop_%s(aetools.NProperty):\n" % pname)
1000                self.fp.write('    """%s - %s """\n' % (ascii(name), ascii(what[1])))
1001                self.fp.write("    which = %r\n" % (code,))
1002                self.fp.write("    want = %r\n" % (what[0],))
1003        self.namemappers[0].addnamecode('property', pname, code)
1004        if is_application_class and self.fp:
1005            self.fp.write("%s = _Prop_%s()\n" % (pname, pname))
1006
1007    def compileelement(self, elem):
1008        [code, keyform] = elem
1009        if self.fp:
1010            self.fp.write("#        element %r as %s\n" % (code, keyform))
1011
1012    def fillclasspropsandelems(self, cls):
1013        [name, code, desc, properties, elements] = cls
1014        cname = identify(name)
1015        if self.namemappers[0].hascode('class', code) and \
1016                self.namemappers[0].findcodename('class', code)[0] != cname:
1017            # This is an other name (plural or so) for something else. Skip.
1018            if self.fp and (elements or len(properties) > 1 or (len(properties) == 1 and
1019                properties[0][1] != 'c@#!')):
1020                if self.verbose:
1021                    print >>self.verbose, '** Skip multiple %s of %s (code %r)' % (cname, self.namemappers[0].findcodename('class', code)[0], code)
1022                raise RuntimeError, "About to skip non-empty class"
1023            return
1024        plist = []
1025        elist = []
1026        superclasses = []
1027        for prop in properties:
1028            [pname, pcode, what] = prop
1029            if pcode == "c@#^":
1030                superclasses.append(what)
1031            if pcode == 'c@#!':
1032                continue
1033            pname = identify(pname)
1034            plist.append(pname)
1035
1036        superclassnames = []
1037        for superclass in superclasses:
1038            superId, superDesc, dummy = superclass
1039            superclassname, fullyqualifiedname, module = self.findcodename("class", superId)
1040            # I don't think this is correct:
1041            if superclassname == cname:
1042                pass # superclassnames.append(fullyqualifiedname)
1043            else:
1044                superclassnames.append(superclassname)
1045
1046        if self.fp:
1047            self.fp.write("%s._superclassnames = %r\n"%(cname, superclassnames))
1048
1049        for elem in elements:
1050            [ecode, keyform] = elem
1051            if ecode == 'c@#!':
1052                continue
1053            name, ename, module = self.findcodename('class', ecode)
1054            if not name:
1055                if self.fp:
1056                    self.fp.write("# XXXX %s element %r not found!!\n"%(cname, ecode))
1057            else:
1058                elist.append((name, ename))
1059
1060        plist.sort()
1061        elist.sort()
1062
1063        if self.fp:
1064            self.fp.write("%s._privpropdict = {\n"%cname)
1065            for n in plist:
1066                self.fp.write("    '%s' : _Prop_%s,\n"%(n, n))
1067            self.fp.write("}\n")
1068            self.fp.write("%s._privelemdict = {\n"%cname)
1069            for n, fulln in elist:
1070                self.fp.write("    '%s' : %s,\n"%(n, fulln))
1071            self.fp.write("}\n")
1072
1073    def compilecomparison(self, comp):
1074        [name, code, comment] = comp
1075        iname = identify(name)
1076        self.namemappers[0].addnamecode('comparison', iname, code)
1077        if self.fp:
1078            self.fp.write("class %s(aetools.NComparison):\n" % iname)
1079            self.fp.write('    """%s - %s """\n' % (ascii(name), ascii(comment)))
1080
1081    def compileenumeration(self, enum):
1082        [code, items] = enum
1083        name = "_Enum_%s" % identify(code)
1084        if self.fp:
1085            self.fp.write("%s = {\n" % name)
1086            for item in items:
1087                self.compileenumerator(item)
1088            self.fp.write("}\n\n")
1089        self.namemappers[0].addnamecode('enum', name, code)
1090        return code
1091
1092    def compileenumerator(self, item):
1093        [name, code, desc] = item
1094        self.fp.write("    %r : %r,\t# %s\n" % (identify(name), code, ascii(desc)))
1095
1096    def checkforenum(self, enum):
1097        """This enum code is used by an event. Make sure it's available"""
1098        name, fullname, module = self.findcodename('enum', enum)
1099        if not name:
1100            if self.fp:
1101                self.fp.write("_Enum_%s = None # XXXX enum %s not found!!\n"%(identify(enum), ascii(enum)))
1102            return
1103        if module:
1104            if self.fp:
1105                self.fp.write("from %s import %s\n"%(module, name))
1106
1107    def dumpindex(self):
1108        if not self.fp:
1109            return
1110        self.fp.write("\n#\n# Indices of types declared in this module\n#\n")
1111
1112        self.fp.write("_classdeclarations = {\n")
1113        classlist = self.namemappers[0].getall('class')
1114        classlist.sort()
1115        for k, v in classlist:
1116            self.fp.write("    %r : %s,\n" % (k, v))
1117        self.fp.write("}\n")
1118
1119        self.fp.write("\n_propdeclarations = {\n")
1120        proplist = self.namemappers[0].getall('property')
1121        proplist.sort()
1122        for k, v in proplist:
1123            self.fp.write("    %r : _Prop_%s,\n" % (k, v))
1124        self.fp.write("}\n")
1125
1126        self.fp.write("\n_compdeclarations = {\n")
1127        complist = self.namemappers[0].getall('comparison')
1128        complist.sort()
1129        for k, v in complist:
1130            self.fp.write("    %r : %s,\n" % (k, v))
1131        self.fp.write("}\n")
1132
1133        self.fp.write("\n_enumdeclarations = {\n")
1134        enumlist = self.namemappers[0].getall('enum')
1135        enumlist.sort()
1136        for k, v in enumlist:
1137            self.fp.write("    %r : %s,\n" % (k, v))
1138        self.fp.write("}\n")
1139
1140def compiledata(data):
1141    [type, description, flags] = data
1142    return "%r -- %r %s" % (type, description, compiledataflags(flags))
1143
1144def is_null(data):
1145    return data[0] == 'null'
1146
1147def is_optional(data):
1148    return (data[2] & 0x8000)
1149
1150def is_enum(data):
1151    return (data[2] & 0x2000)
1152
1153def getdatadoc(data):
1154    [type, descr, flags] = data
1155    if descr:
1156        return ascii(descr)
1157    if type == '****':
1158        return 'anything'
1159    if type == 'obj ':
1160        return 'an AE object reference'
1161    return "undocumented, typecode %r"%(type,)
1162
1163dataflagdict = {15: "optional", 14: "list", 13: "enum", 12: "mutable"}
1164def compiledataflags(flags):
1165    bits = []
1166    for i in range(16):
1167        if flags & (1<<i):
1168            if i in dataflagdict.keys():
1169                bits.append(dataflagdict[i])
1170            else:
1171                bits.append(repr(i))
1172    return '[%s]' % string.join(bits)
1173
1174def ascii(str):
1175    """Return a string with all non-ascii characters hex-encoded"""
1176    if type(str) != type(''):
1177        return map(ascii, str)
1178    rv = ''
1179    for c in str:
1180        if c in ('\t', '\n', '\r') or ' ' <= c < chr(0x7f):
1181            rv = rv + c
1182        else:
1183            rv = rv + '\\' + 'x%02.2x' % ord(c)
1184    return rv
1185
1186def identify(str):
1187    """Turn any string into an identifier:
1188    - replace space by _
1189    - replace other illegal chars by _xx_ (hex code)
1190    - append _ if the result is a python keyword
1191    """
1192    if not str:
1193        return "empty_ae_name_"
1194    rv = ''
1195    ok = string.ascii_letters + '_'
1196    ok2 = ok + string.digits
1197    for c in str:
1198        if c in ok:
1199            rv = rv + c
1200        elif c == ' ':
1201            rv = rv + '_'
1202        else:
1203            rv = rv + '_%02.2x_'%ord(c)
1204        ok = ok2
1205    if keyword.iskeyword(rv):
1206        rv = rv + '_'
1207    return rv
1208
1209# Call the main program
1210
1211if __name__ == '__main__':
1212    main()
1213    sys.exit(1)
1214