Files
Nim/cogapp.py
Andreas Rumpf e792940f52 version 0.7.6
2009-04-22 15:55:27 +02:00

710 lines
26 KiB
Python

""" 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<hashsect> *\(checksum: (?P<hash>[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.