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