import os, sys
from types import ClassType, DictType
try:
from cPickle import load, dump
except ImportError:
from pickle import load, dump
from MiscUtils.Configurable import Configurable
from MiscUtils import NoDefault
try:
True, False
except NameError:
True, False = 1, 0
class ModelError(Exception):
def __init__(self, error, line=None):
self._line = line
self._error = error
if line is not None:
args = (line, error)
else:
args = (error,)
Exception.__init__(self, *args)
def setLine(self, line):
self._line = line
def printError(self, filename):
self.args = (filename,) + self.args
if self._line:
print '%s:%d: %s' % (filename, self._line, self._error)
else:
print '%s: %s' % (filename, self._error)
class Model(Configurable):
"""A Model defines the classes, attributes and enumerations of an application.
It also provides access to the Python classes that implement these structures
for use by other MiddleKit entities including code generators and object stores.
"""
pickleVersion = 1
def __init__(self,
filename=None, classesFilename=None, configFilename=None,
customCoreClasses={}, rootModel=None, havePythonClasses=1):
Configurable.__init__(self)
self._havePythonClasses = havePythonClasses
self._filename = None
self._configFilename = configFilename or 'Settings.config'
self._coreClasses = customCoreClasses
self._klasses = None
self._name = None
self._parents = []
self._pyClassForName = {}
if rootModel:
self._allModelsByFilename = rootModel._allModelsByFilename
else:
self._allModelsByFilename = {}
self._rootModel = rootModel
if filename or classesFilename:
self.read(filename or classesFilename, classesFilename is not None)
def name(self):
if self._name is None:
if self._filename:
self._name = os.path.splitext(os.path.basename(self._filename))[0]
else:
self._name = 'unnamed-mk-model'
return self._name
def setName(self, name):
self._name = name
def filename(self):
return self._filename
def read(self, filename, isClassesFile=0):
import time
start = time.time()
assert self._filename is None, 'Cannot read twice.'
if os.path.splitext(filename)[1] == '':
filename += '.mkmodel'
self._filename = os.path.abspath(filename)
self._name = None
if isClassesFile:
self.dontReadParents()
else:
self.readParents()
try:
if isClassesFile:
self.readKlassesDirectly(filename)
else:
self.readKlassesInModelDir()
self.awakeFromRead()
except ModelError, e:
print
print 'Error while reading model:'
e.printError(filename)
raise
dur = time.time() - start
def readKlassesInModelDir(self):
"""Read the Classes.csv or Classes.pickle.cache file as appropriate."""
path = None
csvPath = os.path.join(self._filename, 'Classes.csv')
if os.path.exists(csvPath):
path = csvPath
xlPath = os.path.join(self._filename, 'Classes.xls')
if os.path.exists(xlPath):
path = xlPath
if path is None:
open(csvPath)
self.readKlassesDirectly(path)
def readKlassesDirectly(self, path):
data = None
shouldUseCache = self.setting('UsePickledClassesCache', 0)
if shouldUseCache:
from MiscUtils.PickleCache import readPickleCache, writePickleCache
data = readPickleCache(path, pickleVersion=1, source='MiddleKit')
if data is None:
self.klasses().read(path)
if shouldUseCache:
writePickleCache(self._klasses, path, pickleVersion=1, source='MiddleKit')
else:
self._klasses = data
self._klasses._model = self
def __getstate__(self):
raise Exception, 'Model instances were not designed to be pickled.'
def awakeFromRead(self):
models = list(self._searchOrder)
models.reverse()
byName = {}
inOrder = []
for model in models:
for klass in model.klasses().klassesInOrder():
name = klass.name()
if byName.has_key(name):
for i in range(len(inOrder)):
if inOrder[i].name() == name:
inOrder[i] = klass
else:
inOrder.append(klass)
byName[name] = klass
assert len(byName) == len(inOrder)
for name, klass in byName.items():
assert klass is self.klass(name)
for klass in inOrder:
assert klass is self.klass(klass.name())
self._allKlassesByName = byName
self._allKlassesInOrder = inOrder
self._klasses.awakeFromRead(self)
def readParents(self, parentFilenames=None):
"""Read parent models.
Reads the parent models of the current model, as specified in the
'Inherit' setting. The attributes _parents and _searchOrder are set.
"""
if parentFilenames is None:
parentFilenames = self.setting('Inherit', [])
for filename in parentFilenames:
filename = os.path.abspath(os.path.join(
os.path.dirname(self._filename), filename))
if self._allModelsByFilename.has_key(filename):
model = self._allModelsByFilename[filename]
assert model != self._rootModel
else:
model = self.__class__(filename,
customCoreClasses=self._coreClasses,
rootModel=self, havePythonClasses=self._havePythonClasses)
self._allModelsByFilename[filename] = model
self._parents.append(model)
searchOrder = self.allModelsDepthFirstLeftRight()
indexes = range(len(searchOrder))
indexes.reverse()
for i in indexes:
if i < len(searchOrder):
model = searchOrder[i]
j = 0
while j < i:
if searchOrder[j] is model:
del searchOrder[j]
i -= 1
else:
j += 1
self._searchOrder = searchOrder
def dontReadParents(self):
"""Set attributes _parents and _searchOrder.
Used internally for the rare case of reading class files directly
(instead of from a model directory).
"""
self._parents = []
self._searchOrder = [self]
def allModelsDepthFirstLeftRight(self, parents=None):
"""Return ordered list of models.
Returns a list of all models, including self, parents and
ancestors, in a depth-first, left-to-right order. Does not
remove duplicates (found in inheritance diamonds).
Mostly useful for readParents() to establish the lookup
order regarding model inheritance.
"""
if parents is None:
parents = []
parents.append(self)
for parent in self._parents:
parent.allModelsDepthFirstLeftRight(parents)
return parents
def coreClass(self, className):
"""Return code class.
For the given name, returns a class from MiddleKit.Core
or the custom set of classes that were passed in via initialization.
"""
pyClass = self._coreClasses.get(className, None)
if pyClass is None:
results = {}
exec 'import MiddleKit.Core.%s as module'%className in results
pyClass = getattr(results['module'], className)
assert type(pyClass) is ClassType
self._coreClasses[className] = pyClass
return pyClass
def coreClassNames(self):
"""Return a list of model class names found in MiddleKit.Core."""
import MiddleKit.Core as Core
return Core.__all__
def klasses(self):
"""Get klasses.
Returns an instance that inherits from Klasses, using the base
classes passed to __init__, if any.
See also: klass(), allKlassesInOrder(), allKlassesByName()
"""
if self._klasses is None:
Klasses = self.coreClass('Klasses')
self._klasses = Klasses(self)
return self._klasses
def klass(self, name, default=NoDefault):
"""Get klass.
Returns the klass with the given name, searching the parent
models if necessary.
"""
for model in self._searchOrder:
klass = model.klasses().get(name, None)
if klass:
return klass
if default is NoDefault:
raise KeyError, name
else:
return default
def allKlassesInOrder(self):
"""Get klasses in order.
Returns a sequence of all the klasses in this model, unique by
name, including klasses inherited from parent models.
The order is the order of declaration, top-down.
"""
return self._allKlassesInOrder
def allKlassesByName(self):
"""Get klasses by name.
Returns a dictionary of all the klasses in this model, unique
by name, including klasses inherited from parent models.
"""
return self._allKlassesByName
def allKlassesInDependencyOrder(self):
"""Get klasses in dependency order.
Returns a sequence of all the klasses in this model, in an
order such that klasses follow the klasses they refer to
(via obj ref attributes).
The typical use for such an order is to avoid SQL errors
about foreign keys referring to tables that do not exist.
A ModelError is raised if there is a dependency cycle
since there can be no definitive order when a cycle exists.
You can break cycles by setting Ref=False for some
attribute in the cycle.
"""
for klass in self._allKlassesInOrder:
klass.willBuildDependencies()
for klass in self._allKlassesInOrder:
klass.buildDependencies()
allKlasses = []
visited = {}
for klass in self._allKlassesInOrder:
if not klass._dependents:
klass.recordDependencyOrder(allKlasses, visited)
if len(allKlasses) < len(self._allKlassesInOrder):
raise ModelError("Cannot determine a dependency order"
" among the classes due to a cycle. Try setting Ref=0"
" for one of the attributes to break the cycle.")
assert len(allKlasses) == len(self._allKlassesInOrder), \
'%r, %r, %r' % (len(allKlasses), len(self._allKlassesInOrder), allKlasses)
return allKlasses
def pyClassForName(self, name):
"""Get Python class for name.
Returns the Python class for the given name, which must be present
in the object model. Accounts for self.setting('Package').
If you already have a reference to the model klass, then you can
just ask it for klass.pyClass().
"""
pyClass = self._pyClassForName.get(name, None)
if pyClass is None:
results = {}
pkg = self.setting('Package', '')
if pkg:
pkg += '.'
try:
exec 'import %s%s as module' % (pkg, name) in results
except ImportError, exc:
raise ModelError("Could not import module for class '%s' due to %r."
" If you've added this class recently,"
" you need to re-generate your model." % (name, exc.args[0]))
pyClass = getattr(results['module'], 'pyClass', None)
if pyClass is None:
pyClass = getattr(results['module'], name)
self._pyClassForName[name] = pyClass
return pyClass
def configFilename(self):
if self._filename is None:
return None
else:
return os.path.join(self._filename, self._configFilename)
def defaultConfig(self):
return {
'Threaded': True,
'ObjRefSuffixes': ('ClassId', 'ObjId'),
'UseBigIntObjRefColumns': False,
'PreSQL': '',
'PostSQL': '',
'DropStatements': 'database',
'SQLSerialColumnName': 'serialNum',
'AccessorStyle': 'methods',
'ExternalEnumsSQLNames': {
'Enable': False,
'TableName': '%(ClassName)s%(AttrName)sEnum',
'ValueColName': 'value',
'NameColName': 'name',
},
}
def usesExternalSQLEnums(self):
flag = getattr(self, '_usesExternalSQLEnums', None)
if flag is None:
flag = self.setting('ExternalEnumsSQLNames')['Enable']
self._usesExternalSQLEnums = flag
return flag
def printWarnings(self, out=None):
if out is None:
out = sys.stdout
if len(self._klasses.klassesInOrder()) < 1:
out.write("warning: Model '%s' doesn't contain any class definitions.\n"
% self.name())
for klass in self.klasses().klassesInOrder():
klass.printWarnings(out)