root/galaxy-central/eggs/Cheetah-2.2.2-py2.6-macosx-10.6-universal-ucs2.egg/Cheetah/CheetahWrapper.py @ 3

リビジョン 3, 23.2 KB (コミッタ: kohda, 14 年 前)

Install Unix tools  http://hannonlab.cshl.edu/galaxy_unix_tools/galaxy.html

行番号 
1# $Id: CheetahWrapper.py,v 1.26 2007/10/02 01:22:04 tavis_rudd Exp $
2"""Cheetah command-line interface.
3
42002-09-03 MSO: Total rewrite.
52002-09-04 MSO: Bugfix, compile command was using wrong output ext.
62002-11-08 MSO: Another rewrite.
7
8Meta-Data
9================================================================================
10Author: Tavis Rudd <tavis@damnsimple.com> and Mike Orr <sluggoster@gmail.com>>
11Version: $Revision: 1.26 $
12Start Date: 2001/03/30
13Last 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
18import getopt, glob, os, pprint, re, shutil, sys
19import cPickle as pickle
20from optparse import OptionParser
21
22from Cheetah.Version import Version
23from Cheetah.Template import Template, DEFAULT_COMPILER_SETTINGS
24from Cheetah.Utils.Misc import mkdirsWithPyInitFiles
25
26optionDashesRE = re.compile(  R"^-{1,2}"  )
27moduleNameRE = re.compile(  R"^[a-zA-Z_][a-zA-Z_0-9]*$"  )
28   
29def 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
40class Error(Exception):
41    pass
42
43
44class 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
58def 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
71WRAPPER_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
82HELP_PAGE1 = """\
83USAGE:
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
93You may abbreviate the command to the first letter; e.g., 'h' == 'help'.
94If FILES is a single "-", read standard input and write standard output.
95Run "cheetah options" for the list of valid options.
96"""
97
98##################################################
99## CheetahWrapper CLASS
100
101class 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("""\
190cheetah compile %s
191Options are
192%s
193Files 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("""\
251Cannot run the tests because you don't have write permission in the current
252directory.  The tests need to create temporary files.  Change to a directory
253you 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
592be 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`
620def _cheetah():
621    CheetahWrapper().main()
622
623# Called when invoked as `cheetah-compile`
624def _cheetah_compile():
625    sys.argv.insert(1, "compile")
626    CheetahWrapper().main()
627
628
629##################################################
630## if run from the command line
631if __name__ == '__main__':  CheetahWrapper().main()
632
633# vim: shiftwidth=4 tabstop=4 expandtab
Note: リポジトリブラウザについてのヘルプは TracBrowser を参照してください。