appypod-rattail/gen/generator.py
2009-06-29 14:06:01 +02:00

421 lines
20 KiB
Python
Executable file

# ------------------------------------------------------------------------------
import os, os.path, sys, parser, symbol, token
from optparse import OptionParser
from appy.gen import Type, State, Config, Tool, Flavour
from appy.gen.descriptors import *
from appy.gen.utils import produceNiceMessage
import appy.pod, appy.pod.renderer
from appy.shared.utils import FolderDeleter
# ------------------------------------------------------------------------------
class GeneratorError(Exception): pass
# I need the following classes to parse Python classes and find in which
# order the attributes are defined. --------------------------------------------
class AstMatcher:
'''Allows to find a given pattern within an ast (part).'''
def _match(pattern, node):
res = None
if pattern[0] == node[0]:
# This level matches
if len(pattern) == 1:
return node
else:
if type(node[1]) == tuple:
return AstMatcher._match(pattern[1:], node[1])
return res
_match = staticmethod(_match)
def match(pattern, node):
res = []
for subNode in node[1:]:
# Do I find the pattern among the subnodes ?
occurrence = AstMatcher._match(pattern, subNode)
if occurrence:
res.append(occurrence)
return res
match = staticmethod(match)
# ------------------------------------------------------------------------------
class AstClass:
'''Python class.'''
def __init__(self, node):
# Link to the Python ast node
self.node = node
self.name = node[2][1]
self.attributes = [] # We are only interested in parsing static
# attributes to now their order
if sys.version_info[:2] >= (2,5):
self.statementPattern = (
symbol.stmt, symbol.simple_stmt, symbol.small_stmt,
symbol.expr_stmt, symbol.testlist, symbol.test, symbol.or_test,
symbol.and_test, symbol.not_test, symbol.comparison, symbol.expr,
symbol.xor_expr, symbol.and_expr, symbol.shift_expr,
symbol.arith_expr, symbol.term, symbol.factor, symbol.power)
else:
self.statementPattern = (
symbol.stmt, symbol.simple_stmt, symbol.small_stmt,
symbol.expr_stmt, symbol.testlist, symbol.test, symbol.and_test,
symbol.not_test, symbol.comparison, symbol.expr, symbol.xor_expr,
symbol.and_expr, symbol.shift_expr, symbol.arith_expr,
symbol.term, symbol.factor, symbol.power)
for subNode in node[1:]:
if subNode[0] == symbol.suite:
# We are in the class body
self.getStaticAttributes(subNode)
def getStaticAttributes(self, classBody):
statements = AstMatcher.match(self.statementPattern, classBody)
for statement in statements:
if len(statement) == 2 and statement[1][0] == symbol.atom and \
statement[1][1][0] == token.NAME:
attrName = statement[1][1][1]
self.attributes.append(attrName)
def __repr__(self):
return '<class %s has attrs %s>' % (self.name, str(self.attributes))
# ------------------------------------------------------------------------------
class Ast:
'''Python AST.'''
classPattern = (symbol.stmt, symbol.compound_stmt, symbol.classdef)
def __init__(self, pyFile):
f = file(pyFile)
fContent = f.read()
f.close()
fContent = fContent.replace('\r', '')
ast = parser.suite(fContent).totuple()
# Get all the classes defined within this module.
self.classes = {}
classNodes = AstMatcher.match(self.classPattern, ast)
for node in classNodes:
astClass = AstClass(node)
self.classes[astClass.name] = astClass
# ------------------------------------------------------------------------------
WARN_NO_TEMPLATE = 'Warning: the code generator should have a folder "%s" ' \
'containing all code templates.'
CODE_HEADER = '''# -*- coding: utf-8 -*-
#
# GNU General Public License (GPL)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.
#
'''
class Generator:
'''Abstract base class for building a generator.'''
def __init__(self, application, outputFolder, options):
self.application = application
# Determine application name
self.applicationName = os.path.basename(application)
if application.endswith('.py'):
self.applicationName = self.applicationName[:-3]
# Determine output folder (where to store the generated product)
self.outputFolder = '%s/%s' % (outputFolder, self.applicationName)
self.options = options
# Determine templates folder
exec 'import %s as genModule' % self.__class__.__module__
self.templatesFolder = os.path.join(os.path.dirname(genModule.__file__),
'templates')
if not os.path.exists(self.templatesFolder):
print WARN_NO_TEMPLATE % self.templatesFolder
# Default descriptor classes
self.classDescriptor = ClassDescriptor
self.workflowDescriptor = WorkflowDescriptor
self.customToolClassDescriptor = ClassDescriptor
self.customFlavourClassDescriptor = ClassDescriptor
# Custom tool and flavour classes, if they are defined in the
# application
self.customToolDescr = None
self.customFlavourDescr = None
# The following dict contains a series of replacements that need to be
# applied to file templates to generate files.
self.repls = {'applicationName': self.applicationName,
'applicationPath': os.path.dirname(self.application),
'codeHeader': CODE_HEADER}
# List of Appy classes and workflows found in the application
self.classes = []
self.workflows = []
self.initialize()
self.config = Config.getDefault()
def determineAppyType(self, klass):
'''Is p_klass an Appy class ? An Appy workflow? None of this ?
If it (or a parent) declares at least one appy type definition,
it will be considered an Appy class. If it (or a parent) declares at
least one state definition, it will be considered an Appy
workflow.'''
res = 'none'
for attrValue in klass.__dict__.itervalues():
if isinstance(attrValue, Type):
res = 'class'
elif isinstance(attrValue, State):
res = 'workflow'
if not res:
for baseClass in klass.__bases__:
baseClassType = self.determineAppyType(baseClass)
if baseClassType != 'none':
res = baseClassType
break
return res
IMPORT_ERROR = 'Warning: error while importing module %s (%s)'
SYNTAX_ERROR = 'Warning: error while parsing module %s (%s)'
def walkModule(self, moduleName):
'''Visits a given (sub-*)module into the application.'''
try:
exec 'import %s' % moduleName
exec 'moduleObj = %s' % moduleName
moduleFile = moduleObj.__file__
if moduleFile.endswith('.pyc'):
moduleFile = moduleFile[:-1]
astClasses = Ast(moduleFile).classes
except ImportError, ie:
# True import error or, simply, this is a simple folder within
# the application, not a sub-module.
print self.IMPORT_ERROR % (moduleName, str(ie))
return
except SyntaxError, se:
print self.SYNTAX_ERROR % (moduleName, str(se))
return
classType = type(Generator)
# Find all classes in this module
for moduleElemName in moduleObj.__dict__.keys():
exec 'moduleElem = moduleObj.%s' % moduleElemName
if (type(moduleElem) == classType) and \
(moduleElem.__module__ == moduleObj.__name__):
# We have found a Python class definition in this module.
appyType = self.determineAppyType(moduleElem)
if appyType != 'none':
# Produce a list of static class attributes (in the order
# of their definition).
attrs = astClasses[moduleElem.__name__].attributes
if appyType == 'class':
if issubclass(moduleElem, Tool):
descrClass = self.customToolClassDescriptor
self.customToolDescr = descrClass(
moduleElem, attrs, self)
elif issubclass(moduleElem, Flavour):
descrClass = self.customFlavourClassDescriptor
self.customFlavourDescr = descrClass(
moduleElem, attrs, self)
else:
descrClass = self.classDescriptor
self.classes.append(
descrClass(moduleElem, attrs, self))
elif appyType == 'workflow':
descrClass = self.workflowDescriptor
self.workflows.append(
descrClass(moduleElem, attrs, self))
elif isinstance(moduleElem, Config):
self.config = moduleElem
# Walk potential sub-modules
if moduleFile.find('__init__.py') != -1:
# Potentially, sub-modules exist
moduleFolder = os.path.dirname(moduleFile)
for elem in os.listdir(moduleFolder):
subModuleName, ext = os.path.splitext(elem)
if ((ext == '.py') and (subModuleName != '__init__')) or \
os.path.isdir(os.path.join(moduleFolder, subModuleName)):
# Submodules may be sub-folders or Python files
subModuleName = '%s.%s' % (moduleName, subModuleName)
self.walkModule(subModuleName)
def walkApplication(self):
'''This method walks into the application and creates the corresponding
meta-classes in self.classes, self.workflows, etc.'''
# Where is the application located ?
containingFolder = os.path.dirname(self.application)
sys.path.append(containingFolder)
# What is the name of the application ?
appName = os.path.basename(self.application)
if os.path.isfile(self.application):
appName = os.path.splitext(appName)[0]
self.walkModule(appName)
sys.path.pop()
def generateClass(self, classDescr):
'''This method is called whenever a Python class declaring Appy type
definition(s) is encountered within the application.'''
def generateWorkflow(self, workflowDescr):
'''This method is called whenever a Python class declaring states and
transitions is encountered within the application.'''
def initialize(self):
'''Called before the old product is removed (if any), in __init__.'''
def finalize(self):
'''Called at the end of the generation process.'''
def copyFile(self, fileName, replacements, destName=None, destFolder=None,
isPod=False):
'''This method will copy p_fileName from self.templatesFolder to
self.outputFolder (or in a subFolder if p_destFolder is given)
after having replaced all p_replacements. If p_isPod is True,
p_fileName is a POD template and the copied file is the result of
applying p_fileName with context p_replacements.'''
# Get the path of the template file to copy
templatePath = os.path.join(self.templatesFolder, fileName)
# Get (or create if needed) the path of the result file
destFile = fileName
if destName: destFile = destName
if destFolder: destFile = '%s/%s' % (destFolder, destFile)
absDestFolder = self.outputFolder
if destFolder:
absDestFolder = os.path.join(self.outputFolder, destFolder)
if not os.path.exists(absDestFolder):
os.makedirs(absDestFolder)
resultPath = os.path.join(self.outputFolder, destFile)
if os.path.exists(resultPath): os.remove(resultPath)
if not isPod:
# Copy the template file to result file after having performed some
# replacements
f = file(templatePath)
fileContent = f.read()
f.close()
if not fileName.endswith('.png'):
for rKey, rValue in replacements.iteritems():
fileContent = fileContent.replace(
'<!%s!>' % rKey, str(rValue))
f = file(resultPath, 'w')
f.write(fileContent)
f.close()
else:
# Call the POD renderer to produce the result
rendererParams = {'template': templatePath,
'context': replacements,
'result': resultPath}
renderer = appy.pod.renderer.Renderer(**rendererParams)
renderer.run()
def run(self):
self.walkApplication()
for classDescr in self.classes: self.generateClass(classDescr)
for wfDescr in self.workflows: self.generateWorkflow(wfDescr)
self.finalize()
print 'Done.'
# ------------------------------------------------------------------------------
ERROR_CODE = 1
VALID_PRODUCT_TYPES = ('plone25', 'odt')
APP_NOT_FOUND = 'Application not found at %s.'
WRONG_NG_OF_ARGS = 'Wrong number of arguments.'
WRONG_OUTPUT_FOLDER = 'Output folder not found. Please create it first.'
PRODUCT_TYPE_ERROR = 'Wrong product type. Product type may be one of the ' \
'following: %s' % str(VALID_PRODUCT_TYPES)
C_OPTION = 'Removes from i18n files all labels that are not automatically ' \
'generated from your gen-application. It can be useful during ' \
'development, when you do lots of name changes (classes, ' \
'attributes, states, transitions, etc): in this case, the Appy ' \
'i18n label generation machinery produces lots of labels that ' \
'then become obsolete.'
S_OPTION = 'Sorts all i18n labels. If you use this option, among the ' \
'generated i18n files, you will find first all labels ' \
'that are automatically generated by appy.gen, in some logical ' \
'order (ie: field-related labels appear together, in the order ' \
'they are declared in the gen-class). Then, if you have added ' \
'labels manually, they will appear afterwards. Sorting labels ' \
'may not be desired under development. Indeed, when no sorting ' \
'occurs, every time you add or modify a field, class, state, etc, ' \
'newly generated labels will all appear together at the end of ' \
'the file; so it will be easy to translate them all. When sorting ' \
'occurs, those elements may be spread at different places in the ' \
'i18n file. When the development is finished, it may be a good ' \
'idea to sort the labels to get a clean and logically ordered ' \
'set of translation files.'
class GeneratorScript:
'''usage: %prog [options] app productType outputFolder
"app" is the path to your Appy application, which may be a
Python module (= a file than ends with .py) or a Python
package (= a folder containing a file named __init__.py).
Your app may reside anywhere (but it needs to be
accessible by the underlying application server, ie Zope),
excepted within the generated product. Typically, if you
generate a Plone product, it may reside within
<yourZopeInstance>/lib/python, but not within the
generated product (typically stored in
<yourZopeInstance>/Products).
"productType" is the kind of product you want to generate
(currently, only "plone25" and 'odt' are supported;
in the near future, the "plone25" target will also produce
Plone 3-compliant code that will still work with
Plone 2.5).
"outputFolder" is the folder where the product will be generated.
For example, if you specify /my/output/folder for your
application /home/gde/MyApp.py, this script will create
a folder /my/output/folder/MyApp and put in it the
generated product.
Example: generating a Plone product
-----------------------------------
In your Zope instance named myZopeInstance, create a folder
"myZopeInstance/lib/python/MyApp". Create into it your Appy application
(we suppose here that it is a Python package, containing a __init__.py
file and other files). Then, chdir into this folder and type
"python <appyPath>/gen/generator.py . plone25 ../../../Products" and the
product will be generated in myZopeInstance/Products/MyApp.
"python" must refer to a Python interpreter that knows package appy.'''
def generateProduct(self, options, application, productType, outputFolder):
exec 'from appy.gen.%s.generator import Generator' % productType
Generator(application, outputFolder, options).run()
def manageArgs(self, parser, options, args):
# Check number of args
if len(args) != 3:
print WRONG_NG_OF_ARGS
parser.print_help()
sys.exit(ERROR_CODE)
# Check productType
if args[1] not in VALID_PRODUCT_TYPES:
print PRODUCT_TYPE_ERROR
sys.exit(ERROR_CODE)
# Check existence of application
if not os.path.exists(args[0]):
print APP_NOT_FOUND % args[0]
sys.exit(ERROR_CODE)
# Check existence of outputFolder basic type
if not os.path.exists(args[2]):
print WRONG_OUTPUT_FOLDER
sys.exit(ERROR_CODE)
# Convert all paths in absolute paths
for i in (0,2):
args[i] = os.path.abspath(args[i])
def run(self):
optParser = OptionParser(usage=GeneratorScript.__doc__)
optParser.add_option("-c", "--i18n-clean", action='store_true',
dest='i18nClean', default=False, help=C_OPTION)
optParser.add_option("-s", "--i18n-sort", action='store_true',
dest='i18nSort', default=False, help=S_OPTION)
(options, args) = optParser.parse_args()
try:
self.manageArgs(optParser, options, args)
print 'Generating %s product in %s...' % (args[1], args[2])
self.generateProduct(options, *args)
except GeneratorError, ge:
sys.stderr.write(str(ge))
sys.stderr.write('\n')
optParser.print_help()
sys.exit(ERROR_CODE)
# ------------------------------------------------------------------------------
if __name__ == '__main__':
GeneratorScript().run()
# ------------------------------------------------------------------------------