root/Cheetah/CheetahWrapper.py

Revision f17b49bd2a9cb5c693518283252cdbca4d04136b, 21.1 kB (checked in by Jason Michalski <armooo@armooo.net>, 2 years ago)

Lets try the import again

  • Property mode set to 100644
Line 
1 #!/usr/bin/env python
2 # $Id: CheetahWrapper.py,v 1.25 2006/02/04 00:59:46 tavis_rudd Exp $
3 """Cheetah command-line interface.
4
5 2002-09-03 MSO: Total rewrite.
6 2002-09-04 MSO: Bugfix, compile command was using wrong output ext.
7 2002-11-08 MSO: Another rewrite.
8
9 Meta-Data
10 ================================================================================
11 Author: Tavis Rudd <tavis@damnsimple.com> and Mike Orr <iron@mso.oz.net>
12 Version: $Revision: 1.25 $
13 Start Date: 2001/03/30
14 Last Revision Date: $Date: 2006/02/04 00:59:46 $
15 """
16 __author__ = "Tavis Rudd <tavis@damnsimple.com> and Mike Orr <iron@mso.oz.net>"
17 __revision__ = "$Revision: 1.25 $"[11:-2]
18
19 import getopt, glob, os, pprint, re, shutil, sys
20 import cPickle as pickle
21
22 from Cheetah.Version import Version
23 from Cheetah.Template import Template, DEFAULT_COMPILER_SETTINGS
24 from Cheetah.Utils.Misc import mkdirsWithPyInitFiles
25 from Cheetah.Utils.optik import OptionParser
26
27 optionDashesRE = re.compile(  R"^-{1,2}"  )
28 moduleNameRE = re.compile(  R"^[a-zA-Z_][a-zA-Z_0-9]*$"  )
29    
30 def fprintfMessage(stream, format, *args):
31     if format[-1:] == '^':
32         format = format[:-1]
33     else:
34         format += '\n'
35     if args:
36         message = format % args
37     else:
38         message = format
39     stream.write(message)
40
41 class Error(Exception):
42     pass
43
44
45 class Bundle:
46     """Wrap the source, destination and backup paths in one neat little class.
47        Used by CheetahWrapper.getBundles().
48     """
49     def __init__(self, **kw):
50         self.__dict__.update(kw)
51
52     def __repr__(self):
53         return "<Bundle %r>" % self.__dict__
54
55
56 class MyOptionParser(OptionParser):
57     standard_option_list = [] # We use commands for Optik's standard options.
58
59     def error(self, msg):
60         """Print our usage+error page."""
61         usage(HELP_PAGE2, msg)
62
63     def print_usage(self, file=None):
64         """Our usage+error page already has this."""
65         pass
66    
67
68 ##################################################
69 ## USAGE FUNCTION & MESSAGES
70
71 def usage(usageMessage, errorMessage="", out=sys.stderr):
72     """Write help text, an optional error message, and abort the program.
73     """
74     out.write(WRAPPER_TOP)
75     out.write(usageMessage)
76     exitStatus = 0
77     if errorMessage:
78         out.write('\n')
79         out.write("*** USAGE ERROR ***: %s\n" % errorMessage)
80         exitStatus = 1
81     sys.exit(exitStatus)
82              
83
84 WRAPPER_TOP = """\
85          __  ____________  __
86          \ \/            \/ /
87           \/    *   *     \/    CHEETAH %(Version)s Command-Line Tool
88            \      |       /
89             \  ==----==  /      by Tavis Rudd <tavis@damnsimple.com>
90              \__________/       and Mike Orr <iron@mso.oz.net>
91               
92 """ % globals()
93
94
95 HELP_PAGE1 = """\
96 USAGE:
97 ------
98   cheetah compile [options] [FILES ...]     : Compile template definitions
99   cheetah fill [options] [FILES ...]        : Fill template definitions
100   cheetah help                              : Print this help message
101   cheetah options                           : Print options help message
102   cheetah test [options]                    : Run Cheetah's regression tests
103                                             : (same as for unittest)
104   cheetah version                           : Print Cheetah version number
105
106 You may abbreviate the command to the first letter; e.g., 'h' == 'help'.
107 If FILES is a single "-", read standard input and write standard output.
108 Run "cheetah options" for the list of valid options.
109 """
110
111 HELP_PAGE2 = """\
112 OPTIONS FOR "compile" AND "fill":
113 ---------------------------------
114   --idir DIR, --odir DIR : input/output directories (default: current dir)
115   --iext EXT, --oext EXT : input/output filename extensions
116     (default for compile: tmpl/py,  fill: tmpl/html)
117   -R                 : recurse subdirectories looking for input files
118   --debug            : print lots of diagnostic output to standard error
119   --env              : put the environment in the searchList
120   --flat             : no destination subdirectories
121   --nobackup         : don't make backups
122   --pickle FILE      : unpickle FILE and put that object in the searchList
123   --stdout, -p       : output to standard output (pipe)
124   --settings         : a string representing the compiler settings to use
125                        e.g. --settings='useNameMapper=False,useFilters=False'
126                        This string is eval'd in Python so it should contain
127                        valid Python syntax.
128   --templateAPIClass : a string representing a subclass of
129                        Cheetah.Template:Template to use for compilation
130
131 Run "cheetah help" for the main help screen.
132 """
133
134 ##################################################
135 ## CheetahWrapper CLASS
136
137 class CheetahWrapper:
138     MAKE_BACKUPS = True
139     BACKUP_SUFFIX = ".bak"
140     _templateClass = None
141     _compilerSettings = None   
142
143     def __init__(self):
144         self.progName = None
145         self.command = None
146         self.opts = None
147         self.pathArgs = None
148         self.sourceFiles = []
149         self.searchList = []
150
151     ##################################################
152     ## MAIN ROUTINE
153
154     def main(self, argv=None):
155         """The main program controller."""
156
157         if argv is None:
158             argv = sys.argv
159
160         # Step 1: Determine the command and arguments.
161         try:
162             self.progName = progName = os.path.basename(argv[0])
163             self.command = command = optionDashesRE.sub("", argv[1])
164             if command == 'test':
165                 self.testOpts = argv[2:]
166             else:
167                 self.parseOpts(argv[2:])
168         except IndexError:
169             usage(HELP_PAGE1, "not enough command-line arguments")
170
171         # Step 2: Call the command
172         meths = (self.compile, self.fill, self.help, self.options,
173             self.test, self.version)
174         for meth in meths:
175             methName = meth.__name__
176             # Or meth.im_func.func_name
177             # Or meth.func_name (Python >= 2.1 only, sometimes works on 2.0)
178             methInitial = methName[0]
179             if command in (methName, methInitial):
180                 sys.argv[0] += (" " + methName)
181                 # @@MO: I don't necessarily agree sys.argv[0] should be
182                 # modified.
183                 meth()
184                 return
185         # If none of the commands matched.
186         usage(HELP_PAGE1, "unknown command '%s'" % command)
187
188     def parseOpts(self, args):
189         C, D, W = self.chatter, self.debug, self.warn
190         self.isCompile = isCompile = self.command[0] == 'c'
191         defaultOext = isCompile and ".py" or ".html"
192         parser = MyOptionParser()
193         pao = parser.add_option
194         pao("--idir", action="store", dest="idir", default="")
195         pao("--odir", action="store", dest="odir", default="")
196         pao("--iext", action="store", dest="iext", default=".tmpl")
197         pao("--oext", action="store", dest="oext", default=defaultOext)
198         pao("-R", action="store_true", dest="recurse", default=False)
199         pao("--stdout", "-p", action="store_true", dest="stdout", default=False)
200         pao("--debug", action="store_true", dest="debug", default=False)
201         pao("--env", action="store_true", dest="env", default=False)
202         pao("--pickle", action="store", dest="pickle", default="")
203         pao("--flat", action="store_true", dest="flat", default=False)
204         pao("--nobackup", action="store_true", dest="nobackup", default=False)
205         pao("--settings", action="store", dest="compilerSettingsString", default=None)
206         pao("--templateAPIClass", action="store", dest="templateClassName", default=None)
207
208         self.opts, self.pathArgs = opts, files = parser.parse_args(args)
209         D("""\
210 cheetah compile %s
211 Options are
212 %s
213 Files are %s""", args, pprint.pformat(vars(opts)), files)
214
215
216         #cleanup trailing path separators
217         seps = [sep for sep in [os.sep, os.altsep] if sep]
218         for attr in ['idir', 'odir']:
219             for sep in seps:
220                 path = getattr(opts, attr, None)
221                 if path and path.endswith(sep):
222                     path = path[:-len(sep)]
223                     setattr(opts, attr, path)
224                     break
225
226         self._fixExts()
227         if opts.env:
228             self.searchList.insert(0, os.environ)
229         if opts.pickle:
230             f = open(opts.pickle, 'rb')
231             unpickled = pickle.load(f)
232             f.close()
233             self.searchList.insert(0, unpickled)
234         opts.verbose = not opts.stdout
235
236     ##################################################
237     ## COMMAND METHODS
238
239     def compile(self):
240         self._compileOrFill()
241
242     def fill(self):
243         from Cheetah.ImportHooks import install
244         install()       
245         self._compileOrFill()
246
247     def help(self):
248         usage(HELP_PAGE1, "", sys.stdout)
249
250     def options(self):
251         usage(HELP_PAGE2, "", sys.stdout)
252
253     def test(self):
254         # @@MO: Ugly kludge.
255         TEST_WRITE_FILENAME = 'cheetah_test_file_creation_ability.tmp'
256         try:
257             f = open(TEST_WRITE_FILENAME, 'w')
258         except:
259             sys.exit("""\
260 Cannot run the tests because you don't have write permission in the current
261 directory.  The tests need to create temporary files.  Change to a directory
262 you do have write permission to and re-run the tests.""")
263         else:
264             f.close()
265             os.remove(TEST_WRITE_FILENAME)
266         # @@MO: End ugly kludge.
267         from Cheetah.Tests import Test
268         import Cheetah.Tests.unittest_local_copy as unittest
269         del sys.argv[1:] # Prevent unittest from misinterpreting options.
270         sys.argv.extend(self.testOpts)
271         #unittest.main(testSuite=Test.testSuite)
272         #unittest.main(testSuite=Test.testSuite)
273         unittest.main(module=Test)
274        
275     def version(self):
276         print Version
277
278     # If you add a command, also add it to the 'meths' variable in main().
279     
280     ##################################################
281     ## LOGGING METHODS
282
283     def chatter(self, format, *args):
284         """Print a verbose message to stdout.  But don't if .opts.stdout is
285            true or .opts.verbose is false.
286         """
287         if self.opts.stdout or not self.opts.verbose:
288             return
289         fprintfMessage(sys.stdout, format, *args)
290
291
292     def debug(self, format, *args):
293         """Print a debugging message to stderr, but don't if .debug is
294            false.
295         """
296         if self.opts.debug:
297             fprintfMessage(sys.stderr, format, *args)
298    
299     def warn(self, format, *args):
300         """Always print a warning message to stderr.
301         """
302         fprintfMessage(sys.stderr, format, *args)
303
304     def error(self, format, *args):
305         """Always print a warning message to stderr and exit with an error code.       
306         """
307         fprintfMessage(sys.stderr, format, *args)
308         sys.exit(1)
309
310     ##################################################
311     ## HELPER METHODS
312
313
314     def _fixExts(self):
315         assert self.opts.oext, "oext is empty!"
316         iext, oext = self.opts.iext, self.opts.oext
317         if iext and not iext.startswith("."):
318             self.opts.iext = "." + iext
319         if oext and not oext.startswith("."):
320             self.opts.oext = "." + oext
321    
322
323
324     def _compileOrFill(self):
325         C, D, W = self.chatter, self.debug, self.warn
326         opts, files = self.opts, self.pathArgs
327         if files == ["-"]:
328             self._compileOrFillStdin()
329             return
330         elif not files and opts.recurse:
331             which = opts.idir and "idir" or "current"
332             C("Drilling down recursively from %s directory.", which)
333             sourceFiles = []
334             dir = os.path.join(self.opts.idir, os.curdir)
335             os.path.walk(dir, self._expandSourceFilesWalk, sourceFiles)
336         elif not files:
337             usage(HELP_PAGE1, "Neither files nor -R specified!")
338         else:
339             sourceFiles = self._expandSourceFiles(files, opts.recurse, True)
340         sourceFiles = [os.path.normpath(x) for x in sourceFiles]
341         D("All source files found: %s", sourceFiles)
342         bundles = self._getBundles(sourceFiles)
343         D("All bundles: %s", pprint.pformat(bundles))
344         if self.opts.flat:
345             self._checkForCollisions(bundles)
346         for b in bundles:
347             self._compileOrFillBundle(b)
348
349     def _checkForCollisions(self, bundles):
350         """Check for multiple source paths writing to the same destination
351            path.
352         """
353         C, D, W = self.chatter, self.debug, self.warn
354         isError = False
355         dstSources = {}
356         for b in bundles:
357             if dstSources.has_key(b.dst):
358                 dstSources[b.dst].append(b.src)
359             else:
360                 dstSources[b.dst] = [b.src]
361         keys = dstSources.keys()
362         keys.sort()
363         for dst in keys:
364             sources = dstSources[dst]
365             if len(sources) > 1:
366                 isError = True
367                 sources.sort()
368                 fmt = "Collision: multiple source files %s map to one destination file %s"
369                 W(fmt, sources, dst)
370         if isError:
371             what = self.isCompile and "Compilation" or "Filling"
372             sys.exit("%s aborted due to collisions" % what)
373                
374
375     def _expandSourceFilesWalk(self, arg, dir, files):
376         """Recursion extension for .expandSourceFiles().
377            This method is a callback for os.path.walk().
378            'arg' is a list to which successful paths will be appended.
379         """
380         iext = self.opts.iext
381         for f in files:
382             path = os.path.join(dir, f)
383             if   path.endswith(iext) and os.path.isfile(path):
384                 arg.append(path)
385             elif os.path.islink(path) and os.path.isdir(path):
386                 os.path.walk(path, self._expandSourceFilesWalk, arg)
387             # If is directory, do nothing; 'walk' will eventually get it.
388
389
390     def _expandSourceFiles(self, files, recurse, addIextIfMissing):
391         """Calculate source paths from 'files' by applying the
392            command-line options.
393         """
394         C, D, W = self.chatter, self.debug, self.warn
395         idir = self.opts.idir
396         iext = self.opts.iext
397         files = []
398         for f in self.pathArgs:
399             oldFilesLen = len(files)
400             D("Expanding %s", f)
401             path = os.path.join(idir, f)
402             pathWithExt = path + iext # May or may not be valid.
403             if os.path.isdir(path):
404                 if recurse:
405                     os.path.walk(path, self._expandSourceFilesWalk, files)
406                 else:
407                     raise Error("source file '%s' is a directory" % path)
408             elif os.path.isfile(path):
409                 files.append(path)
410             elif (addIextIfMissing and not path.endswith(iext) and
411                   os.path.isfile(pathWithExt)):
412                 files.append(pathWithExt)
413                 # Do not recurse directories discovered by iext appending.
414             elif os.path.exists(path):
415                 W("Skipping source file '%s', not a plain file.", path)
416             else:
417                 W("Skipping source file '%s', not found.", path)
418             if len(files) > oldFilesLen:
419                 D("  ... found %s", files[oldFilesLen:])
420         return files
421
422
423     def _getBundles(self, sourceFiles):
424         flat = self.opts.flat
425         idir = self.opts.idir
426         iext = self.opts.iext
427         nobackup = self.opts.nobackup
428         odir = self.opts.odir
429         oext = self.opts.oext
430         idirSlash = idir + os.sep
431         bundles = []
432         for src in sourceFiles:
433             # 'base' is the subdirectory plus basename.
434             base = src
435             if idir and src.startswith(idirSlash):
436                 base = src[len(idirSlash):]
437             if iext and base.endswith(iext):
438                 base = base[:-len(iext)]
439             basename = os.path.basename(base)
440             if flat:
441                 dst = os.path.join(odir, basename + oext)
442             else:
443                 dbn = basename
444                 if odir and base.startswith(os.sep):
445                     odd = odir
446                     while odd != '':
447                         idx = base.find(odd)
448                         if idx == 0:
449                             dbn = base[len(odd):]
450                             if dbn[0] == '/':
451                                 dbn = dbn[1:]
452                             break
453                         odd = os.path.dirname(odd)
454                         if odd == '/':
455                             break
456                     dst = os.path.join(odir, dbn + oext)
457                 else:
458                     dst = os.path.join(odir, base + oext)
459             bak = dst + self.BACKUP_SUFFIX
460             b = Bundle(src=src, dst=dst, bak=bak, base=base, basename=basename)
461             bundles.append(b)
462         return bundles
463
464
465     def _getTemplateClass(self):
466         C, D, W = self.chatter, self.debug, self.warn
467         modname = None
468         if self._templateClass:
469             return self._templateClass
470
471         modname = self.opts.templateClassName
472
473         if not modname:
474             return Template
475         p = modname.rfind('.')
476         if ':' not in modname:
477             self.error('The value of option --templateAPIClass is invalid\n'
478                        'It must be in the form "module:class", '
479                        'e.g. "Cheetah.Template:Template"')
480            
481         modname, classname = modname.split(':')
482
483         C('using --templateAPIClass=%s:%s'%(modname, classname))
484        
485         if p >= 0:
486             mod = getattr(__import__(modname[:p], {}, {}, [modname[p+1:]]), modname[p+1:])
487         else:
488             mod = __import__(modname, {}, {}, [])
489
490         klass = getattr(mod, classname, None)
491         if klass:
492             self._templateClass = klass
493             return klass
494         else:
495             self.error('**Template class specified in option --templateAPIClass not found\n'
496                        '**Falling back on Cheetah.Template:Template')
497
498
499     def _getCompilerSettings(self):
500         if self._compilerSettings:
501             return self._compilerSettings
502
503         def getkws(**kws):
504             return kws
505         if self.opts.compilerSettingsString:
506             try:
507                 exec 'settings = getkws(%s)'%self.opts.compilerSettingsString
508             except:               
509                 self.error("There's an error in your --settings option."
510                           "It must be valid Python syntax.\n"
511                           +"    --settings='%s'\n"%self.opts.compilerSettingsString
512                           +"  %s: %s"%sys.exc_info()[:2]
513                           )
514
515             validKeys = DEFAULT_COMPILER_SETTINGS.keys()
516             if [k for k in settings.keys() if k not in validKeys]:
517                 self.error(
518                     'The --setting "%s" is not a valid compiler setting name.'%k)
519            
520             self._compilerSettings = settings
521             return settings
522         else:
523             return {}
524
525     def _compileOrFillStdin(self):
526         TemplateClass = self._getTemplateClass()
527         compilerSettings = self._getCompilerSettings()
528         if self.isCompile:
529             pysrc = TemplateClass.compile(file=sys.stdin,
530                                           compilerSettings=compilerSettings,
531                                           returnAClass=False)
532             output = pysrc
533         else:
534             output = str(TemplateClass(file=sys.stdin, compilerSettings=compilerSettings))
535         sys.stdout.write(output)
536
537     def _compileOrFillBundle(self, b):
538         C, D, W = self.chatter, self.debug, self.warn
539         TemplateClass = self._getTemplateClass()
540         compilerSettings = self._getCompilerSettings()
541         src = b.src
542         dst = b.dst
543         base = b.base
544         basename = b.basename
545         dstDir = os.path.dirname(dst)
546         what = self.isCompile and "Compiling" or "Filling"
547         C("%s %s -> %s^", what, src, dst) # No trailing newline.
548         if os.path.exists(dst) and not self.opts.nobackup:
549             bak = b.bak
550             C(" (backup %s)", bak) # On same line as previous message.
551         else:
552             bak = None
553             C("")
554         if self.isCompile:
555             if not moduleNameRE.match(basename):
556                 tup = basename, src
557                 raise Error("""\
558 %s: base name %s contains invalid characters.  It must
559 be named according to the same rules as Python modules.""" % tup)
560             pysrc = TemplateClass.compile(file=src, returnAClass=False,
561                                           moduleName=basename,
562                                           className=basename,
563                                           compilerSettings=compilerSettings)
564             output = pysrc
565         else:
566             #output = str(TemplateClass(file=src, searchList=self.searchList))
567             tclass = TemplateClass.compile(file=src, compilerSettings=compilerSettings)
568             output = str(tclass(searchList=self.searchList))
569            
570         if bak:
571             shutil.copyfile(dst, bak)
572         if dstDir and not os.path.exists(dstDir):
573             if self.isCompile:
574                 mkdirsWithPyInitFiles(dstDir)
575             else:
576                 os.makedirs(dstDir)
577         if self.opts.stdout:
578             sys.stdout.write(output)
579         else:
580             f = open(dst, 'w')
581             f.write(output)
582             f.close()
583            
584
585 ##################################################
586 ## if run from the command line
587 if __name__ == '__main__':  CheetahWrapper().main()
588
589 # vim: shiftwidth=4 tabstop=4 expandtab
590
Note: See TracBrowser for help on using the browser.