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.
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 $
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:] == '^':
36 message
= format
% args
41 class Error(Exception):
46 """Wrap the source, destination and backup paths in one neat little class.
47 Used by CheetahWrapper.getBundles().
49 def __init__(self
, **kw
):
50 self
.__dict
__.update(kw
)
53 return "<Bundle %r>" % self
.__dict
__
56 class MyOptionParser(OptionParser
):
57 standard_option_list
= [] # We use commands for Optik's standard options.
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."""
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.
74 out
.write(WRAPPER_TOP
)
75 out
.write(usageMessage
)
79 out
.write("*** USAGE ERROR ***: %s\n" % errorMessage
)
87 \/ * * \/ CHEETAH %(Version)s Command-Line Tool
89 \ ==----== / by Tavis Rudd <tavis@damnsimple.com>
90 \__________/ and Mike Orr <iron@mso.oz.net>
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.
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
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
:
139 BACKUP_SUFFIX
= ".bak"
140 _templateClass
= None
141 _compilerSettings
= None
148 self
.sourceFiles
= []
151 ##################################################
154 def main(self
, argv
=None):
155 """The main program controller."""
160 # Step 1: Determine the command and arguments.
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:]
167 self
.parseOpts(argv
[2:])
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
)
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
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
)
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']:
220 path
= getattr(opts
, attr
, None)
221 if path
and path
.endswith(sep
):
222 path
= path
[:-len(sep
)]
223 setattr(opts
, attr
, path
)
228 self
.searchList
.insert(0, os
.environ
)
230 f
= open(opts
.pickle
, 'rb')
231 unpickled
= pickle
.load(f
)
233 self
.searchList
.insert(0, unpickled
)
234 opts
.verbose
= not opts
.stdout
236 ##################################################
240 self
._compileOrFill
()
243 from Cheetah
.ImportHooks
import install
245 self
._compileOrFill
()
248 usage(HELP_PAGE1
, "", sys
.stdout
)
251 usage(HELP_PAGE2
, "", sys
.stdout
)
255 TEST_WRITE_FILENAME
= 'cheetah_test_file_creation_ability.tmp'
257 f
= open(TEST_WRITE_FILENAME
, 'w')
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.""")
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
)
278 # If you add a command, also add it to the 'meths' variable in main().
280 ##################################################
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
:
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
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
)
310 ##################################################
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
328 self
._compileOrFillStdin
()
330 elif not files
and opts
.recurse
:
331 which
= opts
.idir
and "idir" or "current"
332 C("Drilling down recursively from %s directory.", which
)
334 dir = os
.path
.join(self
.opts
.idir
, os
.curdir
)
335 os
.path
.walk(dir, self
._expandSourceFilesWalk
, sourceFiles
)
337 usage(HELP_PAGE1
, "Neither files nor -R specified!")
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
))
345 self
._checkForCollisions
(bundles
)
347 self
._compileOrFillBundle
(b
)
349 def _checkForCollisions(self
, bundles
):
350 """Check for multiple source paths writing to the same destination
353 C
, D
, W
= self
.chatter
, self
.debug
, self
.warn
357 if dstSources
.has_key(b
.dst
):
358 dstSources
[b
.dst
].append(b
.src
)
360 dstSources
[b
.dst
] = [b
.src
]
361 keys
= dstSources
.keys()
364 sources
= dstSources
[dst
]
368 fmt
= "Collision: multiple source files %s map to one destination file %s"
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
382 path
= os
.path
.join(dir, f
)
383 if path
.endswith(iext
) and os
.path
.isfile(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
398 for f
in self
.pathArgs
:
399 oldFilesLen
= len(files
)
401 path
= os
.path
.join(idir
, f
)
402 pathWithExt
= path
+ iext
# May or may not be valid.
403 if os
.path
.isdir(path
):
405 os
.path
.walk(path
, self
._expandSourceFilesWalk
, files
)
407 raise Error("source file '%s' is a directory" % path
)
408 elif os
.path
.isfile(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
)
417 W("Skipping source file '%s', not found.", path
)
418 if len(files
) > oldFilesLen
:
419 D(" ... found %s", files
[oldFilesLen
:])
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
432 for src
in sourceFiles
:
433 # 'base' is the subdirectory plus basename.
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
)
441 dst
= os
.path
.join(odir
, basename
+ oext
)
444 if odir
and base
.startswith(os
.sep
):
449 dbn
= base
[len(odd
):]
453 odd
= os
.path
.dirname(odd
)
456 dst
= os
.path
.join(odir
, dbn
+ oext
)
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
)
465 def _getTemplateClass(self
):
466 C
, D
, W
= self
.chatter
, self
.debug
, self
.warn
468 if self
._templateClass
:
469 return self
._templateClass
471 modname
= self
.opts
.templateClassName
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
))
486 mod
= getattr(__import__(modname
[:p
], {}, {}, [modname
[p
+1:]]), modname
[p
+1:])
488 mod
= __import__(modname
, {}, {}, [])
490 klass
= getattr(mod
, classname
, None)
492 self
._templateClass
= klass
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
505 if self
.opts
.compilerSettingsString
:
507 exec 'settings = getkws(%s)'%self
.opts
.compilerSettingsString
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
]:
518 'The --setting "%s" is not a valid compiler setting name.'%k
)
520 self
._compilerSettings
= settings
525 def _compileOrFillStdin(self
):
526 TemplateClass
= self
._getTemplateClass
()
527 compilerSettings
= self
._getCompilerSettings
()
529 pysrc
= TemplateClass
.compile(file=sys
.stdin
,
530 compilerSettings
=compilerSettings
,
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
()
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
:
550 C(" (backup %s)", bak
) # On same line as previous message.
555 if not moduleNameRE
.match(basename
):
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,
563 compilerSettings
=compilerSettings
)
566 #output = str(TemplateClass(file=src, searchList=self.searchList))
567 tclass
= TemplateClass
.compile(file=src
, compilerSettings
=compilerSettings
)
568 output
= str(tclass(searchList
=self
.searchList
))
571 shutil
.copyfile(dst
, bak
)
572 if dstDir
and not os
.path
.exists(dstDir
):
574 mkdirsWithPyInitFiles(dstDir
)
578 sys
.stdout
.write(output
)
585 ##################################################
586 ## if run from the command line
587 if __name__
== '__main__': CheetahWrapper().main()
589 # vim: shiftwidth=4 tabstop=4 expandtab