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