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