Home | History | Annotate | Download | only in plat-mac
      1 """
      2 gensuitemodule - Generate an AE suite module from an aete/aeut resource
      3 
      4 Based on aete.py.
      5 
      6 Reading and understanding this code is left as an exercise to the reader.
      7 """
      8 
      9 from warnings import warnpy3k
     10 warnpy3k("In 3.x, the gensuitemodule module is removed.", stacklevel=2)
     11 
     12 import MacOS
     13 import EasyDialogs
     14 import os
     15 import string
     16 import sys
     17 import types
     18 import StringIO
     19 import keyword
     20 import macresource
     21 import aetools
     22 import distutils.sysconfig
     23 import OSATerminology
     24 from Carbon.Res import *
     25 import Carbon.Folder
     26 import MacOS
     27 import getopt
     28 import plistlib
     29 
     30 _MAC_LIB_FOLDER=os.path.dirname(aetools.__file__)
     31 DEFAULT_STANDARD_PACKAGEFOLDER=os.path.join(_MAC_LIB_FOLDER, 'lib-scriptpackages')
     32 DEFAULT_USER_PACKAGEFOLDER=distutils.sysconfig.get_python_lib()
     33 
     34 def 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 
     47 def 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 
     98 def 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 
    126 def 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 
    147 def 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 
    186 def 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 
    227 def 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 
    259 def 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 
    266 def dumpaetelist(aetelist, output):
    267     import pprint
    268     pprint.pprint(aetelist, output)
    269 
    270 def 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 
    283 def 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 
    297 def getbyte(f, *args):
    298     c = f.read(1)
    299     if not c:
    300         raise EOFError, 'in getbyte' + str(args)
    301     return ord(c)
    302 
    303 def 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 
    310 def 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 
    317 def 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 
    324 def 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 
    335 def getalign(f):
    336     if f.tell() & 1:
    337         c = f.read(1)
    338         ##if c != '\0':
    339         ##  print align:', repr(c)
    340 
    341 def 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 
    349 def 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 
    355 def 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 
    366 getdata = [
    367     (getostype, "type"),
    368     (getpstr, "description"),
    369     (getword, "flags")
    370     ]
    371 getargument = [
    372     (getpstr, "name"),
    373     (getostype, "keyword"),
    374     (getdata, "what")
    375     ]
    376 getevent = [
    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     ]
    385 getproperty = [
    386     (getpstr, "name"),
    387     (getostype, "code"),
    388     (getdata, "what")
    389     ]
    390 getelement = [
    391     (getostype, "type"),
    392     (getlist, "keyform", getostype)
    393     ]
    394 getclass = [
    395     (getpstr, "name"),
    396     (getostype, "class code"),
    397     (getpstr, "description"),
    398     (getlist, "properties", getproperty),
    399     (getlist, "elements", getelement)
    400     ]
    401 getcomparison = [
    402     (getpstr, "operator name"),
    403     (getostype, "operator ID"),
    404     (getpstr, "operator comment"),
    405     ]
    406 getenumerator = [
    407     (getpstr, "enumerator name"),
    408     (getostype, "enumerator ID"),
    409     (getpstr, "enumerator comment")
    410     ]
    411 getenumeration = [
    412     (getostype, "enumeration ID"),
    413     (getlist, "enumerator", getenumerator)
    414     ]
    415 getsuite = [
    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     ]
    426 getaete = [
    427     (getword, "major/minor version in BCD"),
    428     (getword, "language code"),
    429     (getword, "script code"),
    430     (getlist, "suites", getsuite)
    431     ]
    432 
    433 def 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 
    553 class 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 #
    826 class 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 
    890 class 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 
   1143 def compiledata(data):
   1144     [type, description, flags] = data
   1145     return "%r -- %r %s" % (type, description, compiledataflags(flags))
   1146 
   1147 def is_null(data):
   1148     return data[0] == 'null'
   1149 
   1150 def is_optional(data):
   1151     return (data[2] & 0x8000)
   1152 
   1153 def is_enum(data):
   1154     return (data[2] & 0x2000)
   1155 
   1156 def 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 
   1166 dataflagdict = {15: "optional", 14: "list", 13: "enum", 12: "mutable"}
   1167 def 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 
   1177 def 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 
   1189 def 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 
   1214 if __name__ == '__main__':
   1215     main()
   1216     sys.exit(1)
   1217