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: # for Python < 2.3 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 # increment this if a non-compatible change is made in Klasses, # Klass or Attr 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 = [] # e.g., parent models self._pyClassForName = {} # _allModelsByFilename is used to avoid loading the same parent model twice 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.' # Assume the .mkmodel extension if none is given if os.path.splitext(filename)[1] == '': filename += '.mkmodel' self._filename = os.path.abspath(filename) self._name = None if isClassesFile: self.dontReadParents() else: self.readParents() # the norm try: if isClassesFile: self.readKlassesDirectly(filename) else: self.readKlassesInModelDir() # the norm self.awakeFromRead() except ModelError, e: print print 'Error while reading model:' e.printError(filename) raise #sys.exit(1) dur = time.time() - start # print '%.2f seconds\n' % dur 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) # to get a properly constructed IOError self.readKlassesDirectly(path) def readKlassesDirectly(self, path): # read the pickled version of Classes if possible data = None shouldUseCache = self.setting('UsePickledClassesCache', 0) if shouldUseCache: from MiscUtils.PickleCache import readPickleCache, writePickleCache data = readPickleCache(path, pickleVersion=1, source='MiddleKit') # read the regular file if necessary 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): # create containers for all klasses, uniqued by name 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) # establish the search order # algorithm taken from http://www.python.org/2.2/descrintro.html#mro searchOrder = self.allModelsDepthFirstLeftRight() # remove duplicates: 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.""" # a little cheesy, but it does the job: 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 = {} # better use Set() in Python 2.3 and set() in Python >= 2.4 for klass in self._allKlassesInOrder: if not klass._dependents: # print '>>', klass.name() klass.recordDependencyOrder(allKlasses, visited) # The above loop fails to capture classes that are in cycles, # but in that case there really is no dependency order. 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) # print '>> allKlassesInDependencyOrder() =', ' '.join([k.name() for k in 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) # Note: The 'pyClass' variable name that is first looked for is a hook for # those modules that have replaced the class variable by something else, # like a function. I did this in a project with a class called UniqueString() # in order to guarantee uniqueness per string. self._pyClassForName[name] = pyClass return pyClass ## Being configurable ## 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, # 'SQLLog': { 'File': 'stdout', }, 'PreSQL': '', 'PostSQL': '', 'DropStatements': 'database', # database, tables 'SQLSerialColumnName': 'serialNum', # can use [cC]lassName, _ClassName 'AccessorStyle': 'methods', # can be 'methods' or 'properties' 'ExternalEnumsSQLNames': { 'Enable': False, 'TableName': '%(ClassName)s%(AttrName)sEnum', 'ValueColName': 'value', 'NameColName': 'name', }, # can use: [cC]lassName, _ClassName, [aA]ttrName, _AttrName. # "_" prefix means "as is", the others control the case of the first character. } def usesExternalSQLEnums(self): flag = getattr(self, '_usesExternalSQLEnums', None) if flag is None: flag = self.setting('ExternalEnumsSQLNames')['Enable'] self._usesExternalSQLEnums = flag return flag ## Warnings ## 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)