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