Using FS mtime to reload non recursive cache.
[pyTivo.git] / Cheetah / CheetahWrapper.py
blob68a8598b285a52b442cd451c99783fad4d9c10c8
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.
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.
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]
19 import getopt, glob, os, pprint, re, shutil, sys
20 import cPickle as pickle
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
27 optionDashesRE = re.compile( R"^-{1,2}" )
28 moduleNameRE = re.compile( R"^[a-zA-Z_][a-zA-Z_0-9]*$" )
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)
41 class Error(Exception):
42 pass
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)
52 def __repr__(self):
53 return "<Bundle %r>" % self.__dict__
56 class MyOptionParser(OptionParser):
57 standard_option_list = [] # We use commands for Optik's standard options.
59 def error(self, msg):
60 """Print our usage+error page."""
61 usage(HELP_PAGE2, msg)
63 def print_usage(self, file=None):
64 """Our usage+error page already has this."""
65 pass
68 ##################################################
69 ## USAGE FUNCTION & MESSAGES
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)
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>
92 """ % globals()
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
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.
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
131 Run "cheetah help" for the main help screen.
134 ##################################################
135 ## CheetahWrapper CLASS
137 class CheetahWrapper:
138 MAKE_BACKUPS = True
139 BACKUP_SUFFIX = ".bak"
140 _templateClass = None
141 _compilerSettings = None
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 = []
151 ##################################################
152 ## MAIN ROUTINE
154 def main(self, argv=None):
155 """The main program controller."""
157 if argv is None:
158 argv = sys.argv
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")
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)
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)
208 self.opts, self.pathArgs = opts, files = parser.parse_args(args)
209 D("""\
210 cheetah compile %s
211 Options are
213 Files are %s""", args, pprint.pformat(vars(opts)), files)
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
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
236 ##################################################
237 ## COMMAND METHODS
239 def compile(self):
240 self._compileOrFill()
242 def fill(self):
243 from Cheetah.ImportHooks import install
244 install()
245 self._compileOrFill()
247 def help(self):
248 usage(HELP_PAGE1, "", sys.stdout)
250 def options(self):
251 usage(HELP_PAGE2, "", sys.stdout)
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)
275 def version(self):
276 print Version
278 # If you add a command, also add it to the 'meths' variable in main().
280 ##################################################
281 ## LOGGING METHODS
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.
287 if self.opts.stdout or not self.opts.verbose:
288 return
289 fprintfMessage(sys.stdout, format, *args)
292 def debug(self, format, *args):
293 """Print a debugging message to stderr, but don't if .debug is
294 false.
296 if self.opts.debug:
297 fprintfMessage(sys.stderr, format, *args)
299 def warn(self, format, *args):
300 """Always print a warning message to stderr.
302 fprintfMessage(sys.stderr, format, *args)
304 def error(self, format, *args):
305 """Always print a warning message to stderr and exit with an error code.
307 fprintfMessage(sys.stderr, format, *args)
308 sys.exit(1)
310 ##################################################
311 ## HELPER METHODS
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
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)
349 def _checkForCollisions(self, bundles):
350 """Check for multiple source paths writing to the same destination
351 path.
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)
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.
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.
390 def _expandSourceFiles(self, files, recurse, addIextIfMissing):
391 """Calculate source paths from 'files' by applying the
392 command-line options.
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
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
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
471 modname = self.opts.templateClassName
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"')
481 modname, classname = modname.split(':')
483 C('using --templateAPIClass=%s:%s'%(modname, classname))
485 if p >= 0:
486 mod = getattr(__import__(modname[:p], {}, {}, [modname[p+1:]]), modname[p+1:])
487 else:
488 mod = __import__(modname, {}, {}, [])
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')
499 def _getCompilerSettings(self):
500 if self._compilerSettings:
501 return self._compilerSettings
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]
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)
520 self._compilerSettings = settings
521 return settings
522 else:
523 return {}
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)
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))
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()
585 ##################################################
586 ## if run from the command line
587 if __name__ == '__main__': CheetahWrapper().main()
589 # vim: shiftwidth=4 tabstop=4 expandtab