"Fossies" - the Fresh Open Source Software Archive

Member "cheetah3-3.2.6.post2/Cheetah/CheetahWrapper.py" (20 Apr 2021, 24329 Bytes) of package /linux/www/cheetah3-3.2.6.post2.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. For more information about "CheetahWrapper.py" see the Fossies "Dox" file reference documentation and the last Fossies "Diffs" side-by-side code changes report: 3-3.2.4_vs_3-3.2.5.

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