""" Cog code generation tool. http://nedbatchelder.com/code/cog Copyright 2004-2008, Ned Batchelder. """ # $Id: cogapp.py 141 2008-05-22 10:56:43Z nedbat $ # modified to run with Python1.5.2 and Python3.0 by Andreas Rumpf import os, re, string, sys, traceback, types, imp, copy, getopt, shlex from pycompab import * __all__ = ['Cog', 'CogUsageError'] __version__ = '2.1.20080522' # History at the end of the file. usage = """\ cog - generate code with inlined Python code. cog [OPTIONS] [INFILE | @FILELIST] ... INFILE is the name of an input file. FILELIST is the name of a text file containing file names or other @FILELISTs. OPTIONS: -c Checksum the output to protect it against accidental change. -d Delete the generator code from the output file. -D name=val Define a global string available to your generator code. -e Warn if a file has no cog code in it. -I PATH Add PATH to the list of directories for data files and modules. -o OUTNAME Write the output to OUTNAME. -r Replace the input file with the output. -s STRING Suffix all generated output lines with STRING. -U Write the output with Unix newlines (only LF line-endings). -w CMD Use CMD if the output file needs to be made writable. A $1 in the CMD will be filled with the filename. -x Excise all the generated output without running the generators. -z The [[[end]]] marker can be omitted, and is assumed at eof. -v Print the version of cog and exit. -h Print this help. """ gErrorMsg = "" # this is needed to support Python 1.5.2 till 3.0 # Other package modules from whiteutils import * class CogError(Exception): """ Any exception raised by Cog. """ def __init__(self, msg, file='', line=0): global gErrorMsg if file: gErrorMsg = Subs("$1($2): $2", file, line, msg) else: gErrorMsg = msg Exception.__init__(self, gErrorMsg) class CogUsageError(CogError): """ An error in usage of command-line arguments in cog. """ pass #pragma: no cover class CogInternalError(CogError): """ An error in the coding of Cog. Should never happen. """ pass #pragma: no cover class CogGeneratedError(CogError): """ An error raised by a user's cog generator. """ pass #pragma: no cover class Redirectable: """ An object with its own stdout and stderr files. """ def __init__(self): self.stdout = sys.stdout self.stderr = sys.stderr def setOutput(self, stdout=None, stderr=None): """ Assign new files for standard out and/or standard error. """ if stdout: self.stdout = stdout if stderr: self.stderr = stderr class CogGenerator(Redirectable): """ A generator pulled from a source file. """ def __init__(self): Redirectable.__init__(self) self.markers = [] self.lines = [] def parseMarker(self, L): self.markers.append(L) def parseLine(self, L): s = 0 e = len(L) while s < len(L) and L[s] == '\n': s = s + 1 while e >= 1 and L[e-1] == '\n': e = e - 1 self.lines.append(L[s:e+1]) def getCode(self): """ Extract the executable Python code from the generator. """ # If the markers and lines all have the same prefix # (end-of-line comment chars, for example), # then remove it from all the lines. prefIn = commonPrefix(self.markers + self.lines) if prefIn: tmp = [] for L in self.markers: tmp.append(replace(L, prefIn, '', 1)) self.markers = tmp tmp = [] for L in self.lines: tmp.append(replace(L, prefIn, '', 1)) self.lines = tmp #self.markers = [ l.replace(prefIn, '', 1) for l in self.markers ] #self.lines = [ l.replace(prefIn, '', 1) for l in self.lines ] return reindentBlock(self.lines, '') def evaluate(self, cog, globals, fname='cog generator'): # figure out the right whitespace prefix for the output prefOut = whitePrefix(self.markers) intext = self.getCode() if not intext: return '' # In Python 2.2, the last line has to end in a newline. intext = "import cog\n" + replace(intext, "\r\n", "\n") + "\n" code = None try: import compiler except ImportError: code = compile(intext, str(fname), 'exec') if code == None: code = compiler.compile(intext, filename=str(fname), mode='exec') # Make sure the "cog" module has our state. cog.cogmodule.msg = self.msg cog.cogmodule.out = self.out cog.cogmodule.outl = self.outl cog.cogmodule.error = self.error self.outstring = '' eval(code, globals) # We need to make sure that the last line in the output # ends with a newline, or it will be joined to the # end-output line, ruining cog's idempotency. if self.outstring and self.outstring[-1] != '\n': self.outstring = self.outstring + '\n' return reindentBlock(self.outstring, prefOut) def msg(self, s): self.stdout.write("Message: "+s+"\n") def out(self, sOut='', dedent=false, trimblanklines=false): """ The cog.out function. """ if trimblanklines and ('\n' in sOut): lines = split(sOut, '\n') if strip(lines[0]) == '': del lines[0] if lines and strip(lines[-1]) == '': del lines[-1] sOut = join(lines,'\n')+'\n' if dedent: sOut = reindentBlock(sOut) self.outstring = self.outstring + sOut def outl(self, sOut='', dedent=false, trimblanklines=false): """ The cog.outl function. """ self.out(sOut, dedent, trimblanklines) self.out('\n') def error(self, msg='Error raised by cog generator.'): """ The cog.error function. Instead of raising standard python errors, cog generators can use this function. It will display the error without a scary Python traceback. """ raise CogGeneratedError(msg) class NumberedFileReader: """ A decorator for files that counts the readline()'s called. """ def __init__(self, f): self.f = f self.n = 0 def readline(self): L = self.f.readline() if L: self.n = self.n + 1 return L def linenumber(self): return self.n class CogOptions: """ Options for a run of cog. """ def __init__(self): # Defaults for argument values. self.args = [] self.includePath = [] self.defines = {} self.bShowVersion = false self.sMakeWritableCmd = None self.bReplace = false self.bNoGenerate = false self.sOutputName = None self.bWarnEmpty = false self.bHashOutput = false self.bDeleteCode = false self.bEofCanBeEnd = false self.sSuffix = None self.bNewlines = false def __cmp__(self, other): """ Comparison operator for tests to use. """ return self.__dict__.__cmp__(other.__dict__) def clone(self): """ Make a clone of these options, for further refinement. """ return copy.deepcopy(self) def addToIncludePath(self, dirs): """ Add directories to the include path. """ dirs = split(dirs, os.pathsep) self.includePath.extend(dirs) def parseArgs(self, argv): # Parse the command line arguments. try: opts, self.args = getopt.getopt(argv, 'cdD:eI:o:rs:Uvw:xz') except getopt.error: raise CogUsageError("invalid command line") # Handle the command line arguments. for o, a in opts: if o == '-c': self.bHashOutput = true elif o == '-d': self.bDeleteCode = true elif o == '-D': if a.count('=') < 1: raise CogUsageError("-D takes a name=value argument") name, value = split(a, '=', 1) self.defines[name] = value elif o == '-e': self.bWarnEmpty = true elif o == '-I': self.addToIncludePath(a) elif o == '-o': self.sOutputName = a elif o == '-r': self.bReplace = true elif o == '-s': self.sSuffix = a elif o == '-U': self.bNewlines = true elif o == '-v': self.bShowVersion = true elif o == '-w': self.sMakeWritableCmd = a elif o == '-x': self.bNoGenerate = true elif o == '-z': self.bEofCanBeEnd = true else: # Since getopt.getopt is given a list of possible flags, # this is an internal error. raise CogInternalError(Subs("Don't understand argument $1", o)) def validate(self): """ Does nothing if everything is OK, raises CogError's if it's not. """ if self.bReplace and self.bDeleteCode: raise CogUsageError("Can't use -d with -r (or you would delete all your source!)") if self.bReplace and self.sOutputName: raise CogUsageError("Can't use -o with -r (they are opposites)") class Cog(Redirectable): """ The Cog engine. """ def __init__(self): Redirectable.__init__(self) self.sBeginSpec = '[[[cog' self.sEndSpec = ']]]' self.sEndOutput = '[[[end]]]' self.reEndOutput = re.compile(r'\[\[\[end]]](?P *\(checksum: (?P[a-f0-9]+)\))') self.sEndFormat = '[[[end]]] (checksum: $1)' self.options = CogOptions() self.sOutputMode = 'w' self.installCogModule() def showWarning(self, msg): self.stdout.write("Warning: " + msg + "\n") def isBeginSpecLine(self, s): return find(s, self.sBeginSpec) >= 0 def isEndSpecLine(self, s): return find(s, self.sEndSpec) >= 0 and \ not self.isEndOutputLine(s) def isEndOutputLine(self, s): return find(s, self.sEndOutput) >= 0 def installCogModule(self): """ Magic mumbo-jumbo so that imported Python modules can say "import cog" and get our state. """ self.cogmodule = imp.new_module('cog') self.cogmodule.path = [] sys.modules['cog'] = self.cogmodule def processFile(self, fIn, fOut, fname=None, globals=None): """ Process an input file object to an output file object. fIn and fOut can be file objects, or file names. """ sFileIn = fname or '' sFileOut = fname or '' # Convert filenames to files. if type(fIn) == type(""): # Open the input file. sFileIn = fIn fIn = open(fIn, 'r') if type(fOut) == type(""): # Open the output file. sFileOut = fOut fOut = open(fOut, self.sOutputMode) fIn = NumberedFileReader(fIn) bSawCog = false self.cogmodule.inFile = sFileIn self.cogmodule.outFile = sFileOut # The globals dict we'll use for this file. if globals is None: globals = {} # If there are any global defines, put them in the globals. globals.update(self.options.defines) # loop over generator chunks L = fIn.readline() while L: # Find the next spec begin while L and not self.isBeginSpecLine(L): if self.isEndSpecLine(L): raise CogError(Subs("Unexpected '$1'", self.sEndSpec), file=sFileIn, line=fIn.linenumber()) if self.isEndOutputLine(L): raise CogError(Subs("Unexpected '$1'", self.sEndOutput), file=sFileIn, line=fIn.linenumber()) fOut.write(L) L = fIn.readline() if not L: break if not self.options.bDeleteCode: fOut.write(L) # L is the begin spec gen = CogGenerator() gen.setOutput(stdout=self.stdout) gen.parseMarker(L) firstLineNum = fIn.linenumber() self.cogmodule.firstLineNum = firstLineNum # If the spec begin is also a spec end, then process the single # line of code inside. if self.isEndSpecLine(L): beg = find(L, self.sBeginSpec) end = find(L, self.sEndSpec) if beg > end: raise CogError("Cog code markers inverted", file=sFileIn, line=firstLineNum) else: sCode = strip(L[beg+len(self.sBeginSpec):end]) gen.parseLine(sCode) else: # Deal with an ordinary code block. L = fIn.readline() # Get all the lines in the spec while L and not self.isEndSpecLine(L): if self.isBeginSpecLine(L): raise CogError(Subs("Unexpected '$1'", self.sBeginSpec), file=sFileIn, line=fIn.linenumber()) if self.isEndOutputLine(L): raise CogError(Subs("Unexpected '$1'", self.sEndOutput), file=sFileIn, line=fIn.linenumber()) if not self.options.bDeleteCode: fOut.write(L) gen.parseLine(L) L = fIn.readline() if not L: raise CogError( "Cog block begun but never ended.", file=sFileIn, line=firstLineNum) if not self.options.bDeleteCode: fOut.write(L) gen.parseMarker(L) L = fIn.readline() # Eat all the lines in the output section. While reading past # them, compute the md5 hash of the old output. hasher = newMD5() while L and not self.isEndOutputLine(L): if self.isBeginSpecLine(L): raise CogError(Subs("Unexpected '$1'", self.sBeginSpec), file=sFileIn, line=fIn.linenumber()) if self.isEndSpecLine(L): raise CogError(Subs("Unexpected '$1'", self.sEndSpec), file=sFileIn, line=fIn.linenumber()) MD5update(hasher, L) L = fIn.readline() curHash = mydigest(hasher) if not L and not self.options.bEofCanBeEnd: # We reached end of file before we found the end output line. raise CogError(Subs("Missing '$1' before end of file.", self.sEndOutput), file=sFileIn, line=fIn.linenumber()) # Write the output of the spec to be the new output if we're # supposed to generate code. hasher = newMD5() if not self.options.bNoGenerate: sFile = Subs("$1+$2", sFileIn, firstLineNum) sGen = gen.evaluate(cog=self, globals=globals, fname=sFile) sGen = self.suffixLines(sGen) MD5update(hasher, sGen) fOut.write(sGen) newHash = mydigest(hasher) bSawCog = true # Write the ending output line hashMatch = self.reEndOutput.search(L) if self.options.bHashOutput: if hashMatch: oldHash = hashMatch.groupdict()['hash'] if oldHash != curHash: raise CogError("Output has been edited! Delete old checksum to unprotect.", file=sFileIn, line=fIn.linenumber()) # Create a new end line with the correct hash. endpieces = split(L, hashMatch.group(0), 1) else: # There was no old hash, but we want a new hash. endpieces = split(L, self.sEndOutput, 1) L = join(endpieces, Subs(self.sEndFormat, newHash)) else: # We don't want hashes output, so if there was one, get rid of # it. if hashMatch: L = replace(L, hashMatch.groupdict()['hashsect'], '', 1) if not self.options.bDeleteCode: fOut.write(L) L = fIn.readline() if not bSawCog and self.options.bWarnEmpty: self.showWarning("no cog code found in " + sFileIn) # A regex for non-empty lines, used by suffixLines. reNonEmptyLines = re.compile("^\s*\S+.*$", re.MULTILINE) def suffixLines(self, text): """ Add suffixes to the lines in text, if our options desire it. text is many lines, as a single string. """ if self.options.sSuffix: # Find all non-blank lines, and add the suffix to the end. repl = r"\g<0>" + replace(self.options.sSuffix, '\\', '\\\\') text = self.reNonEmptyLines.sub(repl, text) return text def processString(self, sInput, fname=None): """ Process sInput as the text to cog. Return the cogged output as a string. """ fOld = StringIO(sInput) fNew = StringIO() self.processFile(fOld, fNew, fname=fname) return fNew.getvalue() def replaceFile(self, sOldPath, sNewText): """ Replace file sOldPath with the contents sNewText """ if not os.access(sOldPath, os.W_OK): # Need to ensure we can write. if self.options.sMakeWritableCmd: # Use an external command to make the file writable. cmd = replace(self.options.sMakeWritableCmd, '$1', sOldPath) self.stdout.write(os.popen(cmd).read()) if not os.access(sOldPath, os.W_OK): raise CogError(Subs("Couldn't make $1 writable", sOldPath)) else: # Can't write! raise CogError("Can't overwrite " + sOldPath) f = open(sOldPath, self.sOutputMode) f.write(sNewText) f.close() def saveIncludePath(self): self.savedInclude = self.options.includePath[:] self.savedSysPath = sys.path[:] def restoreIncludePath(self): self.options.includePath = self.savedInclude self.cogmodule.path = self.options.includePath sys.path = self.savedSysPath def addToIncludePath(self, includePath): self.cogmodule.path.extend(includePath) sys.path.extend(includePath) def processOneFile(self, sFile): """ Process one filename through cog. """ self.saveIncludePath() try: self.addToIncludePath(self.options.includePath) # Since we know where the input file came from, # push its directory onto the include path. self.addToIncludePath([os.path.dirname(sFile)]) # Set the file output mode based on whether we want \n or native # line endings. self.sOutputMode = 'w' if self.options.bNewlines: self.sOutputMode = 'wb' # How we process the file depends on where the output is going. if self.options.sOutputName: self.processFile(sFile, self.options.sOutputName, sFile) elif self.options.bReplace: # We want to replace the cog file with the output, # but only if they differ. self.stdout.write("Cogging " + sFile) bNeedNewline = true try: fOldFile = open(sFile) sOldText = fOldFile.read() fOldFile.close() sNewText = self.processString(sOldText, fname=sFile) if sOldText != sNewText: self.stdout.write(" (changed)\n") bNeedNewline = false self.replaceFile(sFile, sNewText) finally: # The try-finally block is so we can print a partial line # with the name of the file, and print (changed) on the # same line, but also make sure to break the line before # any traceback. if bNeedNewline: self.stdout.write('\n') else: self.processFile(sFile, self.stdout, sFile) finally: self.restoreIncludePath() def processFileList(self, sFileList): """ Process the files in a file list. """ for L in open(sFileList).readlines(): # Use shlex to parse the line like a shell. lex = shlex.shlex(L, posix=true) lex.whitespace_split = true lex.commenters = '#' # No escapes, so that backslash can be part of the path lex.escape = '' args = list(lex) if args: self.processArguments(args) def processArguments(self, args): """ Process one command-line. """ saved_options = self.options self.options = self.options.clone() self.options.parseArgs(args[1:]) self.options.validate() if args[0][0] == '@': if self.options.sOutputName: raise CogUsageError("Can't use -o with @file") self.processFileList(args[0][1:]) else: self.processOneFile(args[0]) self.options = saved_options def callableMain(self, argv): """ All of command-line cog, but in a callable form. This is used by main. argv is the equivalent of sys.argv. """ argv0 = argv.pop(0) # Provide help if asked for anywhere in the command line. if '-?' in argv or '-h' in argv: self.stderr.write(usage) return self.options.parseArgs(argv) self.options.validate() if self.options.bShowVersion: self.stdout.write(Subs("Cog version $1\n", __version__)) return if self.options.args: for a in self.options.args: self.processArguments([a]) else: raise CogUsageError("No files to process") def main(self, argv): """ Handle the command-line execution for cog. """ global gErrorMsg try: self.callableMain(argv) return 0 except CogUsageError: self.stderr.write(gErrorMsg + "\n") self.stderr.write("(for help use -?)\n") return 2 except CogGeneratedError: self.stderr.write(Subs("Error: $1\n", gErrorMsg)) return 3 except CogError: self.stderr.write(gErrorMsg + "\n") return 1 except: traceback.print_exc(None, self.stderr) return 1 # History: # 20040210: First public version. # 20040220: Text preceding the start and end marker are removed from Python lines. # -v option on the command line shows the version. # 20040311: Make sure the last line of output is properly ended with a newline. # 20040605: Fixed some blank line handling in cog. # Fixed problems with assigning to xml elements in handyxml. # 20040621: Changed all line-ends to LF from CRLF. # 20041002: Refactor some option handling to simplify unittesting the options. # 20041118: cog.out and cog.outl have optional string arguments. # 20041119: File names weren't being properly passed around for warnings, etc. # 20041122: Added cog.firstLineNum: a property with the line number of the [[[cog line. # Added cog.inFile and cog.outFile: the names of the input and output file. # 20041218: Single-line cog generators, with start marker and end marker on # the same line. # 20041230: Keep a single globals dict for all the code fragments in a single # file so they can share state. # 20050206: Added the -x switch to remove all generated output. # 20050218: Now code can be on the marker lines as well. # 20050219: Added -c switch to checksum the output so that edits can be # detected before they are obliterated. # 20050521: Added cog.error, contributed by Alexander Belchenko. # 20050720: Added code deletion and settable globals contributed by Blake Winton. # 20050724: Many tweaks to improve code coverage. # 20050726: Error messages are now printed with no traceback. # Code can no longer appear on the marker lines, # except for single-line style. # -z allows omission of the [[[end]]] marker, and it will be assumed # at the end of the file. # 20050729: Refactor option parsing into a separate class, in preparation for # future features. # 20050805: The cogmodule.path wasn't being properly maintained. # 20050808: Added the -D option to define a global value. # 20050810: The %s in the -w command is dealt with more robustly. # Added the -s option to suffix output lines with a marker. # 20050817: Now @files can have arguments on each line to change the cog's # behavior for that line. # 20051006: Version 2.0 # 20080521: -U options lets you create Unix newlines on Windows. Thanks, # Alexander Belchenko. # 20080522: It's now ok to have -d with output to stdout, and now we validate # the args after each line of an @file.