0001"""
0002SQLObject
0003---------
0004
0005:author: Ian Bicking <ianb@colorstudy.com>
0006
0007SQLObject is a object-relational mapper.  See SQLObject.html or
0008SQLObject.txt for more.
0009
0010With the help by Oleg Broytman and many other contributors.
0011See Authors.txt.
0012
0013This program is free software; you can redistribute it and/or modify
0014it under the terms of the GNU Lesser General Public License as
0015published by the Free Software Foundation; either version 2.1 of the
0016License, or (at your option) any later version.
0017
0018This program is distributed in the hope that it will be useful,
0019but WITHOUT ANY WARRANTY; without even the implied warranty of
0020MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0021GNU General Public License for more details.
0022
0023You should have received a copy of the GNU Lesser General Public
0024License along with this program; if not, write to the Free Software
0025Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301,
0026USA.
0027"""
0028
0029import threading
0030import weakref
0031import sqlbuilder
0032import dbconnection
0033import col
0034import styles
0035import types
0036import warnings
0037import joins
0038import index
0039import classregistry
0040import declarative
0041import events
0042from sresults import SelectResults
0043from util.threadinglocal import local
0044
0045import sys
0046if sys.version_info[:3] < (2, 6, 0):
0047    raise ImportError, "SQLObject requires Python 2.6.0 or later"
0048
0049"""
0050This thread-local storage is needed for RowCreatedSignals. It gathers
0051code-blocks to execute _after_ the whole hierachy of inherited SQLObjects
0052is created. See SQLObject._create
0053"""
0054
0055NoDefault = sqlbuilder.NoDefault
0056
0057class SQLObjectNotFound(LookupError): pass
0058class SQLObjectIntegrityError(Exception): pass
0059
0060def makeProperties(obj):
0061    """
0062    This function takes a dictionary of methods and finds
0063    methods named like:
0064    * _get_attr
0065    * _set_attr
0066    * _del_attr
0067    * _doc_attr
0068    Except for _doc_attr, these should be methods.  It
0069    then creates properties from these methods, like
0070    property(_get_attr, _set_attr, _del_attr, _doc_attr).
0071    Missing methods are okay.
0072    """
0073
0074    if isinstance(obj, dict):
0075        def setFunc(var, value):
0076            obj[var] = value
0077        d = obj
0078    else:
0079        def setFunc(var, value):
0080            setattr(obj, var, value)
0081        d = obj.__dict__
0082
0083    props = {}
0084    for var, value in d.items():
0085        if var.startswith('_set_'):
0086            props.setdefault(var[5:], {})['set'] = value
0087        elif var.startswith('_get_'):
0088            props.setdefault(var[5:], {})['get'] = value
0089        elif var.startswith('_del_'):
0090            props.setdefault(var[5:], {})['del'] = value
0091        elif var.startswith('_doc_'):
0092            props.setdefault(var[5:], {})['doc'] = value
0093    for var, setters in props.items():
0094        if len(setters) == 1 and 'doc' in setters:
0095            continue
0096        if var in d:
0097            if isinstance(d[var], (types.MethodType, types.FunctionType)):
0098                warnings.warn(
0099                    "I tried to set the property %r, but it was "
0100                    "already set, as a method (%r).  Methods have "
0101                    "significantly different semantics than properties, "
0102                    "and this may be a sign of a bug in your code."
0103                    % (var, d[var]))
0104            continue
0105        setFunc(var,
0106                property(setters.get('get'), setters.get('set'),
0107                         setters.get('del'), setters.get('doc')))
0108
0109def unmakeProperties(obj):
0110    if isinstance(obj, dict):
0111        def delFunc(obj, var):
0112            del obj[var]
0113        d = obj
0114    else:
0115        delFunc = delattr
0116        d = obj.__dict__
0117
0118    for var, value in d.items():
0119        if isinstance(value, property):
0120            for prop in [value.fget, value.fset, value.fdel]:
0121                if prop and not prop.__name__ in d:
0122                    delFunc(obj, var)
0123                    break
0124
0125def findDependencies(name, registry=None):
0126    depends = []
0127    for klass in classregistry.registry(registry).allClasses():
0128        if findDependantColumns(name, klass):
0129            depends.append(klass)
0130        else:
0131            for join in klass.sqlmeta.joins:
0132                if isinstance(join, joins.SORelatedJoin) and join.otherClassName == name:
0133                    depends.append(klass)
0134                    break
0135    return depends
0136
0137def findDependantColumns(name, klass):
0138    depends = []
0139    for col in klass.sqlmeta.columnList:
0140        if col.foreignKey == name and col.cascade is not None:
0141            depends.append(col)
0142    return depends
0143
0144def _collectAttributes(cls, new_attrs, look_for_class):
0145    """Finds all attributes in `new_attrs` that are instances of
0146    `look_for_class`. The ``.name`` attribute is set for any matching objects.
0147    Returns them as a list.
0148
0149    """
0150    result = []
0151    for attr, value in new_attrs.items():
0152        if isinstance(value, look_for_class):
0153            value.name = attr
0154            delattr(cls, attr)
0155            result.append(value)
0156    return result
0157
0158class CreateNewSQLObject:
0159    """
0160    Dummy singleton to use in place of an ID, to signal we want
0161    a new object.
0162    """
0163    pass
0164
0165class sqlmeta(object):
0166
0167    """
0168    This object is the object we use to keep track of all sorts of
0169    information.  Subclasses are made for each SQLObject subclass
0170    (dynamically if necessary), and instances are created to go
0171    alongside every SQLObject instance.
0172    """
0173
0174    table = None
0175    idName = None
0176    idSequence = None
0177    # This function is used to coerce IDs into the proper format,
0178    # so you should replace it with str, or another function, if you
0179    # aren't using integer IDs
0180    idType = int
0181    style = None
0182    lazyUpdate = False
0183    defaultOrder = None
0184    cacheValues = True
0185    registry = None
0186    fromDatabase = False
0187    # Default is false, but we set it to true for the *instance*
0188    # when necessary: (bad clever? maybe)
0189    expired = False
0190
0191    # This is a mapping from column names to SOCol (or subclass)
0192    # instances:
0193    columns = {}
0194    columnList = []
0195
0196    # This is a mapping from column names to Col (or subclass)
0197    # instances; these objects don't have the logic that the SOCol
0198    # objects do, and are not attached to this class closely.
0199    columnDefinitions = {}
0200
0201    # These are lists of the join and index objects:
0202    indexes = []
0203    indexDefinitions = []
0204    joins = []
0205    joinDefinitions = []
0206
0207    # These attributes shouldn't be shared with superclasses:
0208    _unshared_attributes = ['table', 'columns', 'childName']
0209
0210    # These are internal bookkeeping attributes; the class-level
0211    # definition is a default for the instances, instances will
0212    # reset these values.
0213
0214    # When an object is being created, it has an instance
0215    # variable _creating, which is true.  This way all the
0216    # setters can be captured until the object is complete,
0217    # and then the row is inserted into the database.  Once
0218    # that happens, _creating is deleted from the instance,
0219    # and only the class variable (which is always false) is
0220    # left.
0221    _creating = False
0222    _obsolete = False
0223    # Sometimes an intance is attached to a connection, not
0224    # globally available.  In that case, self.sqlmeta._perConnection
0225    # will be true.  It's false by default:
0226    _perConnection = False
0227
0228    # Inheritance definitions:
0229    parentClass = None # A reference to the parent class
0230    childClasses = {} # References to child classes, keyed by childName
0231    childName = None # Class name for inheritance child object creation
0232
0233    # Does the row require syncing?
0234    dirty = False
0235
0236    # Default encoding for UnicodeCol's
0237    dbEncoding = None
0238
0239    __metaclass__ = declarative.DeclarativeMeta
0240
0241    def __classinit__(cls, new_attrs):
0242        for attr in cls._unshared_attributes:
0243            if attr not in new_attrs:
0244                setattr(cls, attr, None)
0245        declarative.setup_attributes(cls, new_attrs)
0246
0247    def __init__(self, instance):
0248        self.instance = weakref.proxy(instance)
0249
0250    @classmethod
0251    def send(cls, signal, *args, **kw):
0252        events.send(signal, cls.soClass, *args, **kw)
0253
0254    @classmethod
0255    def setClass(cls, soClass):
0256        cls.soClass = soClass
0257        if not cls.style:
0258            cls.style = styles.defaultStyle
0259            try:
0260                if cls.soClass._connection and cls.soClass._connection.style:
0261                    cls.style = cls.soClass._connection.style
0262            except AttributeError:
0263                pass
0264        if cls.table is None:
0265            cls.table = cls.style.pythonClassToDBTable(cls.soClass.__name__)
0266        if cls.idName is None:
0267            cls.idName = cls.style.idForTable(cls.table)
0268
0269        # plainSetters are columns that haven't been overridden by the
0270        # user, so we can contact the database directly to set them.
0271        # Note that these can't set these in the SQLObject class
0272        # itself, because they specific to this subclass of SQLObject,
0273        # and cannot be shared among classes.
0274        cls._plainSetters = {}
0275        cls._plainGetters = {}
0276        cls._plainForeignSetters = {}
0277        cls._plainForeignGetters = {}
0278        cls._plainJoinGetters = {}
0279        cls._plainJoinAdders = {}
0280        cls._plainJoinRemovers = {}
0281
0282        # This is a dictionary of columnName: columnObject
0283        # None of these objects can be shared with superclasses
0284        cls.columns = {}
0285        cls.columnList = []
0286        # These, however, can be shared:
0287        cls.columnDefinitions = cls.columnDefinitions.copy()
0288        cls.indexes = []
0289        cls.indexDefinitions = cls.indexDefinitions[:]
0290        cls.joins = []
0291        cls.joinDefinitions = cls.joinDefinitions[:]
0292
0293    ############################################################
0294    ## Adding special values, like columns and indexes
0295    ############################################################
0296
0297    ########################################
0298    ## Column handling
0299    ########################################
0300
0301    @classmethod
0302    def addColumn(cls, columnDef, changeSchema=False, connection=None):
0303        post_funcs = []
0304        cls.send(events.AddColumnSignal, cls.soClass, connection,
0305                 columnDef.name, columnDef, changeSchema, post_funcs)
0306        sqlmeta = cls
0307        soClass = cls.soClass
0308        del cls
0309        column = columnDef.withClass(soClass)
0310        name = column.name
0311        assert name != 'id', (
0312            "The 'id' column is implicit, and should not be defined as "
0313            "a column")
0314        assert name not in sqlmeta.columns, (
0315            "The class %s.%s already has a column %r (%r), you cannot "
0316            "add the column %r"
0317            % (soClass.__module__, soClass.__name__, name,
0318               sqlmeta.columnDefinitions[name], columnDef))
0319        # Collect columns from the parent classes to test
0320        # if the column is not in a parent class
0321        parent_columns = []
0322        for base in soClass.__bases__:
0323            if hasattr(base, "sqlmeta"):
0324                parent_columns.extend(base.sqlmeta.columns.keys())
0325        if hasattr(soClass, name):
0326            assert  (name in parent_columns) or (name == "childName"), (
0327                "The class %s.%s already has a variable or method %r, you cannot "
0328                "add the column %r"
0329                % (soClass.__module__, soClass.__name__, name, name))
0330        sqlmeta.columnDefinitions[name] = columnDef
0331        sqlmeta.columns[name] = column
0332        # A stable-ordered version of the list...
0333        sqlmeta.columnList.append(column)
0334
0335        ###################################################
0336        # Create the getter function(s).  We'll start by
0337        # creating functions like _SO_get_columnName,
0338        # then if there's no function named _get_columnName
0339        # we'll alias that to _SO_get_columnName.  This
0340        # allows a sort of super call, even though there's
0341        # no superclass that defines the database access.
0342        if sqlmeta.cacheValues:
0343            # We create a method here, which is just a function
0344            # that takes "self" as the first argument.
0345            getter = eval('lambda self: self._SO_loadValue(%s)' % repr(instanceName(name)))
0346
0347        else:
0348            # If we aren't caching values, we just call the
0349            # function _SO_getValue, which fetches from the
0350            # database.
0351            getter = eval('lambda self: self._SO_getValue(%s)' % repr(name))
0352        setattr(soClass, rawGetterName(name), getter)
0353
0354        # Here if the _get_columnName method isn't in the
0355        # definition, we add it with the default
0356        # _SO_get_columnName definition.
0357        if not hasattr(soClass, getterName(name)) or (name == 'childName'):
0358            setattr(soClass, getterName(name), getter)
0359            sqlmeta._plainGetters[name] = 1
0360
0361        #################################################
0362        # Create the setter function(s)
0363        # Much like creating the getters, we will create
0364        # _SO_set_columnName methods, and then alias them
0365        # to _set_columnName if the user hasn't defined
0366        # those methods themself.
0367
0368        # @@: This is lame; immutable right now makes it unsettable,
0369        # making the table read-only
0370        if not column.immutable:
0371            # We start by just using the _SO_setValue method
0372            setter = eval('lambda self, val: self._SO_setValue(%s, val, self.%s, self.%s)' % (repr(name), '_SO_from_python_%s' % name, '_SO_to_python_%s' % name))
0373            setattr(soClass, '_SO_from_python_%s' % name, column.from_python)
0374            setattr(soClass, '_SO_to_python_%s' % name, column.to_python)
0375            setattr(soClass, rawSetterName(name), setter)
0376            # Then do the aliasing
0377            if not hasattr(soClass, setterName(name)) or (name == 'childName'):
0378                setattr(soClass, setterName(name), setter)
0379                # We keep track of setters that haven't been
0380                # overridden, because we can combine these
0381                # set columns into one SQL UPDATE query.
0382                sqlmeta._plainSetters[name] = 1
0383
0384        ##################################################
0385        # Here we check if the column is a foreign key, in
0386        # which case we need to make another method that
0387        # fetches the key and constructs the sister
0388        # SQLObject instance.
0389        if column.foreignKey:
0390
0391            # We go through the standard _SO_get_columnName deal
0392            # we're giving the object, not the ID of the
0393            # object this time:
0394            origName = column.origName
0395            if sqlmeta.cacheValues:
0396                # self._SO_class_className is a reference
0397                # to the class in question.
0398                getter = eval('lambda self: self._SO_foreignKey(self._SO_loadValue(%r), self._SO_class_%s, %s)' % (instanceName(name), column.foreignKey, column.refColumn and repr(column.refColumn)))
0399            else:
0400                # Same non-caching version as above.
0401                getter = eval('lambda self: self._SO_foreignKey(self._SO_getValue(%s), self._SO_class_%s, %s)' % (repr(name), column.foreignKey, column.refColumn and repr(column.refColumn)))
0402            setattr(soClass, rawGetterName(origName), getter)
0403
0404            # And we set the _get_columnName version
0405            if not hasattr(soClass, getterName(origName)):
0406                setattr(soClass, getterName(origName), getter)
0407                sqlmeta._plainForeignGetters[origName] = 1
0408
0409            if not column.immutable:
0410                # The setter just gets the ID of the object,
0411                # and then sets the real column.
0412                setter = eval('lambda self, val: setattr(self, %s, self._SO_getID(val, %s))' % (repr(name), column.refColumn and repr(column.refColumn)))
0413                setattr(soClass, rawSetterName(origName), setter)
0414                if not hasattr(soClass, setterName(origName)):
0415                    setattr(soClass, setterName(origName), setter)
0416                    sqlmeta._plainForeignSetters[origName] = 1
0417
0418            classregistry.registry(sqlmeta.registry).addClassCallback(
0419                column.foreignKey,
0420                lambda foreign, me, attr: setattr(me, attr, foreign),
0421                soClass, '_SO_class_%s' % column.foreignKey)
0422
0423        if column.alternateMethodName:
0424            func = eval('lambda cls, val, connection=None: cls._SO_fetchAlternateID(%s, %s, val, connection=connection)' % (repr(column.name), repr(column.dbName)))
0425            setattr(soClass, column.alternateMethodName, classmethod(func))
0426
0427        if changeSchema:
0428            conn = connection or soClass._connection
0429            conn.addColumn(sqlmeta.table, column)
0430
0431        if soClass._SO_finishedClassCreation:
0432            makeProperties(soClass)
0433
0434        for func in post_funcs:
0435            func(soClass, column)
0436
0437    @classmethod
0438    def addColumnsFromDatabase(sqlmeta, connection=None):
0439        soClass = sqlmeta.soClass
0440        conn = connection or soClass._connection
0441        for columnDef in conn.columnsFromSchema(sqlmeta.table, soClass):
0442            if columnDef.name not in sqlmeta.columnDefinitions:
0443                if isinstance(columnDef.name, unicode):
0444                    columnDef.name = columnDef.name.encode('ascii')
0445                sqlmeta.addColumn(columnDef)
0446
0447    @classmethod
0448    def delColumn(cls, column, changeSchema=False, connection=None):
0449        sqlmeta = cls
0450        soClass = sqlmeta.soClass
0451        if isinstance(column, str):
0452            if column in sqlmeta.columns:
0453                column = sqlmeta.columns[column]
0454            elif column+'ID' in sqlmeta.columns:
0455                column = sqlmeta.columns[column+'ID']
0456            else:
0457                raise ValueError('Unknown column ' + column)
0458        if isinstance(column, col.Col):
0459            for c in sqlmeta.columns.values():
0460                if column is c.columnDef:
0461                    column = c
0462                    break
0463            else:
0464                raise IndexError(
0465                    "Column with definition %r not found" % column)
0466        post_funcs = []
0467        cls.send(events.DeleteColumnSignal, cls.soClass, connection,
0468                 column.name, column, post_funcs)
0469        name = column.name
0470        del sqlmeta.columns[name]
0471        del sqlmeta.columnDefinitions[name]
0472        sqlmeta.columnList.remove(column)
0473        delattr(soClass, rawGetterName(name))
0474        if name in sqlmeta._plainGetters:
0475            delattr(soClass, getterName(name))
0476        delattr(soClass, rawSetterName(name))
0477        if name in sqlmeta._plainSetters:
0478            delattr(soClass, setterName(name))
0479        if column.foreignKey:
0480            delattr(soClass, rawGetterName(soClass.sqlmeta.style.instanceIDAttrToAttr(name)))
0481            if name in sqlmeta._plainForeignGetters:
0482                delattr(soClass, getterName(name))
0483            delattr(soClass, rawSetterName(soClass.sqlmeta.style.instanceIDAttrToAttr(name)))
0484            if name in sqlmeta._plainForeignSetters:
0485                delattr(soClass, setterName(name))
0486        if column.alternateMethodName:
0487            delattr(soClass, column.alternateMethodName)
0488
0489        if changeSchema:
0490            conn = connection or soClass._connection
0491            conn.delColumn(sqlmeta, column)
0492
0493        if soClass._SO_finishedClassCreation:
0494            unmakeProperties(soClass)
0495            makeProperties(soClass)
0496
0497        for func in post_funcs:
0498            func(soClass, column)
0499
0500    ########################################
0501    ## Join handling
0502    ########################################
0503
0504    @classmethod
0505    def addJoin(cls, joinDef):
0506        sqlmeta = cls
0507        soClass = cls.soClass
0508        # The name of the method we'll create.  If it's
0509        # automatically generated, it's generated by the
0510        # join class.
0511        join = joinDef.withClass(soClass)
0512        meth = join.joinMethodName
0513
0514        sqlmeta.joins.append(join)
0515        index = len(sqlmeta.joins)-1
0516        if joinDef not in sqlmeta.joinDefinitions:
0517            sqlmeta.joinDefinitions.append(joinDef)
0518
0519        # The function fetches the join by index, and
0520        # then lets the join object do the rest of the
0521        # work:
0522        func = eval('lambda self: self.sqlmeta.joins[%i].performJoin(self)' % index)
0523
0524        # And we do the standard _SO_get_... _get_... deal
0525        setattr(soClass, rawGetterName(meth), func)
0526        if not hasattr(soClass, getterName(meth)):
0527            setattr(soClass, getterName(meth), func)
0528            sqlmeta._plainJoinGetters[meth] = 1
0529
0530        # Some joins allow you to remove objects from the
0531        # join.
0532        if hasattr(join, 'remove'):
0533            # Again, we let it do the remove, and we do the
0534            # standard naming trick.
0535            func = eval('lambda self, obj: self.sqlmeta.joins[%i].remove(self, obj)' % index)
0536            setattr(soClass, '_SO_remove' + join.addRemoveName, func)
0537            if not hasattr(soClass, 'remove' + join.addRemoveName):
0538                setattr(soClass, 'remove' + join.addRemoveName, func)
0539                sqlmeta._plainJoinRemovers[meth] = 1
0540
0541        # Some joins allow you to add objects.
0542        if hasattr(join, 'add'):
0543            # And again...
0544            func = eval('lambda self, obj: self.sqlmeta.joins[%i].add(self, obj)' % index)
0545            setattr(soClass, '_SO_add' + join.addRemoveName, func)
0546            if not hasattr(soClass, 'add' + join.addRemoveName):
0547                setattr(soClass, 'add' + join.addRemoveName, func)
0548                sqlmeta._plainJoinAdders[meth] = 1
0549
0550        if soClass._SO_finishedClassCreation:
0551            makeProperties(soClass)
0552
0553    @classmethod
0554    def delJoin(sqlmeta, joinDef):
0555        soClass = sqlmeta.soClass
0556        for join in sqlmeta.joins:
0557            # previously deleted joins will be None, so it must
0558            # be skipped or it'll error out on the next line.
0559            if join is None:
0560                continue
0561            if joinDef is join.joinDef:
0562                break
0563        else:
0564            raise IndexError(
0565                "Join %r not found in class %r (from %r)"
0566                % (joinDef, soClass, sqlmeta.joins))
0567        meth = join.joinMethodName
0568        sqlmeta.joinDefinitions.remove(joinDef)
0569        for i in range(len(sqlmeta.joins)):
0570            if sqlmeta.joins[i] is join:
0571                # Have to leave None, because we refer to joins
0572                # by index.
0573                sqlmeta.joins[i] = None
0574        delattr(soClass, rawGetterName(meth))
0575        if meth in sqlmeta._plainJoinGetters:
0576            delattr(soClass, getterName(meth))
0577        if hasattr(join, 'remove'):
0578            delattr(soClass, '_SO_remove' + join.addRemovePrefix)
0579            if meth in sqlmeta._plainJoinRemovers:
0580                delattr(soClass, 'remove' + join.addRemovePrefix)
0581        if hasattr(join, 'add'):
0582            delattr(soClass, '_SO_add' + join.addRemovePrefix)
0583            if meth in sqlmeta._plainJoinAdders:
0584                delattr(soClass, 'add' + join.addRemovePrefix)
0585
0586        if soClass._SO_finishedClassCreation:
0587            unmakeProperties(soClass)
0588            makeProperties(soClass)
0589
0590    ########################################
0591    ## Indexes
0592    ########################################
0593
0594    @classmethod
0595    def addIndex(cls, indexDef):
0596        cls.indexDefinitions.append(indexDef)
0597        index = indexDef.withClass(cls.soClass)
0598        cls.indexes.append(index)
0599        setattr(cls.soClass, index.name, index)
0600
0601    ########################################
0602    ## Utility methods
0603    ########################################
0604
0605    @classmethod
0606    def getColumns(sqlmeta):
0607        return sqlmeta.columns.copy()
0608
0609    def asDict(self):
0610        """
0611        Return the object as a dictionary of columns to values.
0612        """
0613        result = {}
0614        for key in self.getColumns():
0615            result[key] = getattr(self.instance, key)
0616        result['id'] = self.instance.id
0617        return result
0618
0619    @classmethod
0620    def expireAll(sqlmeta, connection=None):
0621        """
0622        Expire all instances of this class.
0623        """
0624        soClass = sqlmeta.soClass
0625        connection = connection or soClass._connection
0626        cache_set = connection.cache
0627        cache_set.weakrefAll(soClass)
0628        for item in cache_set.getAll(soClass):
0629            item.expire()
0630
0631
0632sqlhub = dbconnection.ConnectionHub()
0633
0634
0635# Turning it on gives earlier warning about things
0636# that will be deprecated (having this off we won't flood people
0637# with warnings right away).
0638warnings_level = 1
0639exception_level = None
0640# Current levels:
0641#  1) Actively deprecated
0642#  2) Deprecated after 1
0643#  3) Deprecated after 2
0644
0645def deprecated(message, level=1, stacklevel=2):
0646    if exception_level is not None and exception_level <= level:
0647        raise NotImplementedError(message)
0648    if warnings_level is not None and warnings_level <= level:
0649        warnings.warn(message, DeprecationWarning, stacklevel=stacklevel)
0650
0651#if sys.version_info[:3] < (2, 5, 0):
0652#    deprecated("Support for Python 2.4 has been declared obsolete and will be removed in the next release of SQLObject")
0653
0654def setDeprecationLevel(warning=1, exception=None):
0655    """
0656    Set the deprecation level for SQLObject.  Low levels are more
0657    actively being deprecated.  Any warning at a level at or below
0658    ``warning`` will give a warning.  Any warning at a level at or
0659    below ``exception`` will give an exception.  You can use a higher
0660    ``exception`` level for tests to help upgrade your code.  ``None``
0661    for either value means never warn or raise exceptions.
0662
0663    The levels currently mean:
0664
0665      1) Deprecated in current version.  Will be removed in next version.
0666
0667      2) Planned to deprecate in next version, remove later.
0668
0669      3) Planned to deprecate sometime, remove sometime much later.
0670
0671    As the SQLObject versions progress, the deprecation level of
0672    specific features will go down, indicating the advancing nature of
0673    the feature's doom.  We'll try to keep features at 1 for a major
0674    revision.
0675
0676    As time continues there may be a level 0, which will give a useful
0677    error message (better than ``AttributeError``) but where the
0678    feature has been fully removed.
0679    """
0680    global warnings_level, exception_level
0681    warnings_level = warning
0682    exception_level = exception
0683
0684
0685class _sqlmeta_attr(object):
0686
0687    def __init__(self, name, deprecation_level):
0688        self.name = name
0689        self.deprecation_level = deprecation_level
0690
0691    def __get__(self, obj, type=None):
0692        if self.deprecation_level is not None:
0693            deprecated(
0694                'Use of this attribute should be replaced with '
0695                '.sqlmeta.%s' % self.name, level=self.deprecation_level)
0696        return getattr((type or obj).sqlmeta, self.name)
0697
0698
0699_postponed_local = local()
0700
0701# SQLObject is the superclass for all SQLObject classes, of
0702# course.  All the deeper magic is done in MetaSQLObject, and
0703# only lesser magic is done here.  All the actual work is done
0704# here, though -- just automatic method generation (like
0705# methods and properties for each column) is done in
0706# MetaSQLObject.
0707class SQLObject(object):
0708
0709    __metaclass__ = declarative.DeclarativeMeta
0710
0711    _connection = sqlhub
0712
0713    sqlmeta = sqlmeta
0714
0715    #DSM: The _inheritable attribute controls wheter the class can by
0716    #DSM: inherited 'logically' with a foreignKey and a back reference.
0717    _inheritable = False # Is this class inheritable?
0718    _parent = None # A reference to the parent instance
0719    childName = None # Children name (to be able to get a subclass)
0720
0721    # The law of Demeter: the class should not call another classes by name
0722    SelectResultsClass = SelectResults
0723
0724    def __classinit__(cls, new_attrs):
0725
0726        # This is true if we're initializing the SQLObject class,
0727        # instead of a subclass:
0728        is_base = cls.__bases__ == (object,)
0729
0730        cls._SO_setupSqlmeta(new_attrs, is_base)
0731
0732        implicitColumns = _collectAttributes(cls, new_attrs, col.Col)
0733        implicitJoins = _collectAttributes(cls, new_attrs, joins.Join)
0734        implicitIndexes = _collectAttributes(cls, new_attrs, index.DatabaseIndex)
0735
0736        if not is_base:
0737            cls._SO_cleanDeprecatedAttrs(new_attrs)
0738
0739        if '_connection' in new_attrs:
0740            connection = new_attrs['_connection']
0741            del cls._connection
0742            assert 'connection' not in new_attrs
0743        elif 'connection' in new_attrs:
0744            connection = new_attrs['connection']
0745            del cls.connection
0746        else:
0747            connection = None
0748
0749        cls._SO_finishedClassCreation = False
0750
0751        ######################################################
0752        # Set some attributes to their defaults, if necessary.
0753        # First we get the connection:
0754        if not connection and not getattr(cls, '_connection', None):
0755            mod = sys.modules[cls.__module__]
0756            # See if there's a __connection__ global in
0757            # the module, use it if there is.
0758            if hasattr(mod, '__connection__'):
0759                connection = mod.__connection__
0760
0761        # Do not check hasattr(cls, '_connection') here - it is possible
0762        # SQLObject parent class has a connection attribute that came
0763        # from sqlhub, e.g.; check __dict__ only.
0764        if connection and ('_connection' not in cls.__dict__):
0765            cls.setConnection(connection)
0766
0767        sqlmeta = cls.sqlmeta
0768
0769        # We have to check if there are columns in the inherited
0770        # _columns where the attribute has been set to None in this
0771        # class.  If so, then we need to remove that column from
0772        # _columns.
0773        for key in sqlmeta.columnDefinitions.keys():
0774            if (key in new_attrs
0775                and new_attrs[key] is None):
0776                del sqlmeta.columnDefinitions[key]
0777
0778        for column in sqlmeta.columnDefinitions.values():
0779            sqlmeta.addColumn(column)
0780
0781        for column in implicitColumns:
0782            sqlmeta.addColumn(column)
0783
0784        # Now the class is in an essentially OK-state, so we can
0785        # set up any magic attributes:
0786        declarative.setup_attributes(cls, new_attrs)
0787
0788        if sqlmeta.fromDatabase:
0789            sqlmeta.addColumnsFromDatabase()
0790
0791        for j in implicitJoins:
0792            sqlmeta.addJoin(j)
0793        for i in implicitIndexes:
0794            sqlmeta.addIndex(i)
0795
0796        order_getter = lambda o: o.creationOrder
0797        sqlmeta.columnList.sort(key=order_getter)
0798        sqlmeta.indexes.sort(key=order_getter)
0799        sqlmeta.indexDefinitions.sort(key=order_getter)
0800        # Joins cannot be sorted because addJoin created accessors
0801        # that remember indexes.
0802        #sqlmeta.joins.sort(key=order_getter)
0803        sqlmeta.joinDefinitions.sort(key=order_getter)
0804
0805        # We don't setup the properties until we're finished with the
0806        # batch adding of all the columns...
0807        cls._notifyFinishClassCreation()
0808        cls._SO_finishedClassCreation = True
0809        makeProperties(cls)
0810
0811        # We use the magic "q" attribute for accessing lazy
0812        # SQL where-clause generation.  See the sql module for
0813        # more.
0814        if not is_base:
0815            cls.q = sqlbuilder.SQLObjectTable(cls)
0816            cls.j = sqlbuilder.SQLObjectTableWithJoins(cls)
0817
0818        classregistry.registry(sqlmeta.registry).addClass(cls)
0819
0820    @classmethod
0821    def _SO_setupSqlmeta(cls, new_attrs, is_base):
0822        """
0823        This fixes up the sqlmeta attribute.  It handles both the case
0824        where no sqlmeta was given (in which we need to create another
0825        subclass), or the sqlmeta given doesn't have the proper
0826        inheritance.  Lastly it calls sqlmeta.setClass, which handles
0827        much of the setup.
0828        """
0829        if ('sqlmeta' not in new_attrs
0830            and not is_base):
0831            # We have to create our own subclass, usually.
0832            # type(className, bases_tuple, attr_dict) creates a new subclass.
0833            cls.sqlmeta = type('sqlmeta', (cls.sqlmeta,), {})
0834        if not issubclass(cls.sqlmeta, sqlmeta):
0835            # We allow no superclass and an object superclass, instead
0836            # of inheriting from sqlmeta; but in that case we replace
0837            # the class and just move over its attributes:
0838            assert cls.sqlmeta.__bases__ in ((), (object,)), (
0839                "If you do not inherit your sqlmeta class from "
0840                "sqlobject.sqlmeta, it must not inherit from any other "
0841                "class (your sqlmeta inherits from: %s)"
0842                % cls.sqlmeta.__bases__)
0843            for base in cls.__bases__:
0844                superclass = getattr(base, 'sqlmeta', None)
0845                if superclass:
0846                    break
0847            else:
0848                assert 0, (
0849                    "No sqlmeta class could be found in any superclass "
0850                    "(while fixing up sqlmeta %r inheritance)"
0851                    % cls.sqlmeta)
0852            values = dict(cls.sqlmeta.__dict__)
0853            for key in values.keys():
0854                if key.startswith('__') and key.endswith('__'):
0855                    # Magic values shouldn't be passed through:
0856                    del values[key]
0857            cls.sqlmeta = type('sqlmeta', (superclass,), values)
0858
0859        if not is_base: # Do not pollute the base sqlmeta class
0860            cls.sqlmeta.setClass(cls)
0861
0862    @classmethod
0863    def _SO_cleanDeprecatedAttrs(cls, new_attrs):
0864        """
0865        This removes attributes on SQLObject subclasses that have
0866        been deprecated; they are moved to the sqlmeta class, and
0867        a deprecation warning is given.
0868        """
0869        for attr in ():
0870            if attr in new_attrs:
0871                deprecated("%r is deprecated and read-only; please do "
0872                           "not use it in your classes until it is fully "
0873                           "deprecated" % attr, level=1, stacklevel=5)
0874
0875    @classmethod
0876    def get(cls, id, connection=None, selectResults=None):
0877
0878        assert id is not None, 'None is not a possible id for %s' % cls.__name__
0879
0880        id = cls.sqlmeta.idType(id)
0881
0882        if connection is None:
0883            cache = cls._connection.cache
0884        else:
0885            cache = connection.cache
0886
0887        # This whole sequence comes from Cache.CacheFactory's
0888        # behavior, where a None returned means a cache miss.
0889        val = cache.get(id, cls)
0890        if val is None:
0891            try:
0892                val = cls(_SO_fetch_no_create=1)
0893                val._SO_validatorState = sqlbuilder.SQLObjectState(val)
0894                val._init(id, connection, selectResults)
0895                cache.put(id, cls, val)
0896            finally:
0897                cache.finishPut(cls)
0898        elif selectResults and not val.sqlmeta.dirty:
0899            val._SO_writeLock.acquire()
0900            try:
0901                val._SO_selectInit(selectResults)
0902                val.sqlmeta.expired = False
0903            finally:
0904                val._SO_writeLock.release()
0905        return val
0906
0907    @classmethod
0908    def _notifyFinishClassCreation(cls):
0909        pass
0910
0911    def _init(self, id, connection=None, selectResults=None):
0912        assert id is not None
0913        # This function gets called only when the object is
0914        # created, unlike __init__ which would be called
0915        # anytime the object was returned from cache.
0916        self.id = id
0917        self._SO_writeLock = threading.Lock()
0918
0919        # If no connection was given, we'll inherit the class
0920        # instance variable which should have a _connection
0921        # attribute.
0922        if (connection is not None) and                   (getattr(self, '_connection', None) is not connection):
0924            self._connection = connection
0925            # Sometimes we need to know if this instance is
0926            # global or tied to a particular connection.
0927            # This flag tells us that:
0928            self.sqlmeta._perConnection = True
0929
0930        if not selectResults:
0931            dbNames = [col.dbName for col in self.sqlmeta.columnList]
0932            selectResults = self._connection._SO_selectOne(self, dbNames)
0933            if not selectResults:
0934                raise SQLObjectNotFound, "The object %s by the ID %s does not exist" % (self.__class__.__name__, self.id)
0935        self._SO_selectInit(selectResults)
0936        self._SO_createValues = {}
0937        self.sqlmeta.dirty = False
0938
0939    def _SO_loadValue(self, attrName):
0940        try:
0941            return getattr(self, attrName)
0942        except AttributeError:
0943            try:
0944                self._SO_writeLock.acquire()
0945                try:
0946                    # Maybe, just in the moment since we got the lock,
0947                    # some other thread did a _SO_loadValue and we
0948                    # have the attribute!  Let's try and find out!  We
0949                    # can keep trying this all day and still beat the
0950                    # performance on the database call (okay, we can
0951                    # keep trying this for a few msecs at least)...
0952                    result = getattr(self, attrName)
0953                except AttributeError:
0954                    pass
0955                else:
0956                    return result
0957                self.sqlmeta.expired = False
0958                dbNames = [col.dbName for col in self.sqlmeta.columnList]
0959                selectResults = self._connection._SO_selectOne(self, dbNames)
0960                if not selectResults:
0961                    raise SQLObjectNotFound, "The object %s by the ID %s has been deleted" % (self.__class__.__name__, self.id)
0962                self._SO_selectInit(selectResults)
0963                result = getattr(self, attrName)
0964                return result
0965            finally:
0966                self._SO_writeLock.release()
0967
0968    def sync(self):
0969        if self.sqlmeta.lazyUpdate and self._SO_createValues:
0970            self.syncUpdate()
0971        self._SO_writeLock.acquire()
0972        try:
0973            dbNames = [col.dbName for col in self.sqlmeta.columnList]
0974            selectResults = self._connection._SO_selectOne(self, dbNames)
0975            if not selectResults:
0976                raise SQLObjectNotFound, "The object %s by the ID %s has been deleted" % (self.__class__.__name__, self.id)
0977            self._SO_selectInit(selectResults)
0978            self.sqlmeta.expired = False
0979        finally:
0980            self._SO_writeLock.release()
0981
0982    def syncUpdate(self):
0983        if not self._SO_createValues:
0984            return
0985        self._SO_writeLock.acquire()
0986        try:
0987            if self.sqlmeta.columns:
0988                values = [(self.sqlmeta.columns[v[0]].dbName, v[1])
0989                          for v in self._SO_createValues.items()]
0990                self._connection._SO_update(self, values)
0991            self.sqlmeta.dirty = False
0992            self._SO_createValues = {}
0993        finally:
0994            self._SO_writeLock.release()
0995
0996        post_funcs = []
0997        self.sqlmeta.send(events.RowUpdatedSignal, self, post_funcs)
0998        for func in post_funcs:
0999            func(self)
1000
1001    def expire(self):
1002        if self.sqlmeta.expired:
1003            return
1004        self._SO_writeLock.acquire()
1005        try:
1006            if self.sqlmeta.expired:
1007                return
1008            for column in self.sqlmeta.columnList:
1009                delattr(self, instanceName(column.name))
1010            self.sqlmeta.expired = True
1011            self._connection.cache.expire(self.id, self.__class__)
1012            self._SO_createValues = {}
1013        finally:
1014            self._SO_writeLock.release()
1015
1016    def _SO_setValue(self, name, value, from_python, to_python):
1017        # This is the place where we actually update the
1018        # database.
1019
1020        # If we are _creating, the object doesn't yet exist
1021        # in the database, and we can't insert it until all
1022        # the parts are set.  So we just keep them in a
1023        # dictionary until later:
1024        d = {name: value}
1025        if not self.sqlmeta._creating and not getattr(self.sqlmeta, "row_update_sig_suppress", False):
1026            self.sqlmeta.send(events.RowUpdateSignal, self, d)
1027        if len(d) != 1 or name not in d:
1028            # Already called RowUpdateSignal, don't call it again
1029            # inside .set()
1030            self.sqlmeta.row_update_sig_suppress = True
1031            self.set(**d)
1032            del self.sqlmeta.row_update_sig_suppress
1033        value = d[name]
1034        if from_python:
1035            dbValue = from_python(value, self._SO_validatorState)
1036        else:
1037            dbValue = value
1038        if to_python:
1039            value = to_python(dbValue, self._SO_validatorState)
1040        if self.sqlmeta._creating or self.sqlmeta.lazyUpdate:
1041            self.sqlmeta.dirty = True
1042            self._SO_createValues[name] = dbValue
1043            setattr(self, instanceName(name), value)
1044            return
1045
1046        self._connection._SO_update(
1047            self, [(self.sqlmeta.columns[name].dbName,
1048                    dbValue)])
1049
1050        if self.sqlmeta.cacheValues:
1051            setattr(self, instanceName(name), value)
1052
1053        post_funcs = []
1054        self.sqlmeta.send(events.RowUpdatedSignal, self, post_funcs)
1055        for func in post_funcs:
1056            func(self)
1057
1058    def set(self, _suppress_set_sig=False, **kw):
1059        if not self.sqlmeta._creating and not getattr(self.sqlmeta, "row_update_sig_suppress", False) and not _suppress_set_sig:
1060            self.sqlmeta.send(events.RowUpdateSignal, self, kw)
1061        # set() is used to update multiple values at once,
1062        # potentially with one SQL statement if possible.
1063
1064        # Filter out items that don't map to column names.
1065        # Those will be set directly on the object using
1066        # setattr(obj, name, value).
1067        is_column = lambda _c: _c in self.sqlmeta._plainSetters
1068        f_is_column = lambda item: is_column(item[0])
1069        f_not_column = lambda item: not is_column(item[0])
1070        items = kw.items()
1071        extra = dict(filter(f_not_column, items))
1072        kw = dict(filter(f_is_column, items))
1073
1074        # _creating is special, see _SO_setValue
1075        if self.sqlmeta._creating or self.sqlmeta.lazyUpdate:
1076            for name, value in kw.items():
1077                from_python = getattr(self, '_SO_from_python_%s' % name, None)
1078                if from_python:
1079                    kw[name] = dbValue = from_python(value, self._SO_validatorState)
1080                else:
1081                    dbValue = value
1082                to_python = getattr(self, '_SO_to_python_%s' % name, None)
1083                if to_python:
1084                    value = to_python(dbValue, self._SO_validatorState)
1085                setattr(self, instanceName(name), value)
1086
1087            self._SO_createValues.update(kw)
1088
1089            for name, value in extra.items():
1090                try:
1091                    getattr(self.__class__, name)
1092                except AttributeError:
1093                    if name not in self.sqlmeta.columns:
1094                        raise TypeError, "%s.set() got an unexpected keyword argument %s" % (self.__class__.__name__, name)
1095                try:
1096                    setattr(self, name, value)
1097                except AttributeError, e:
1098                    raise AttributeError, '%s (with attribute %r)' % (e, name)
1099
1100            self.sqlmeta.dirty = True
1101            return
1102
1103        self._SO_writeLock.acquire()
1104
1105        try:
1106            # We have to go through and see if the setters are
1107            # "plain", that is, if the user has changed their
1108            # definition in any way (put in something that
1109            # normalizes the value or checks for consistency,
1110            # for instance).  If so then we have to use plain
1111            # old setattr() to change the value, since we can't
1112            # read the user's mind.  We'll combine everything
1113            # else into a single UPDATE, if necessary.
1114            toUpdate = {}
1115            for name, value in kw.items():
1116                from_python = getattr(self, '_SO_from_python_%s' % name, None)
1117                if from_python:
1118                    dbValue = from_python(value, self._SO_validatorState)
1119                else:
1120                    dbValue = value
1121                to_python = getattr(self, '_SO_to_python_%s' % name, None)
1122                if to_python:
1123                    value = to_python(dbValue, self._SO_validatorState)
1124                if self.sqlmeta.cacheValues:
1125                    setattr(self, instanceName(name), value)
1126                toUpdate[name] = dbValue
1127            for name, value in extra.items():
1128                try:
1129                    getattr(self.__class__, name)
1130                except AttributeError:
1131                    if name not in self.sqlmeta.columns:
1132                        raise TypeError, "%s.set() got an unexpected keyword argument %s" % (self.__class__.__name__, name)
1133                try:
1134                    setattr(self, name, value)
1135                except AttributeError, e:
1136                    raise AttributeError, '%s (with attribute %r)' % (e, name)
1137
1138            if toUpdate:
1139                args = [(self.sqlmeta.columns[name].dbName, value)
1140                        for name, value in toUpdate.items()]
1141                self._connection._SO_update(self, args)
1142        finally:
1143            self._SO_writeLock.release()
1144
1145        post_funcs = []
1146        self.sqlmeta.send(events.RowUpdatedSignal, self, post_funcs)
1147        for func in post_funcs:
1148            func(self)
1149
1150    def _SO_selectInit(self, row):
1151        for col, colValue in zip(self.sqlmeta.columnList, row):
1152            if col.to_python:
1153                colValue = col.to_python(colValue, self._SO_validatorState)
1154            setattr(self, instanceName(col.name), colValue)
1155
1156    def _SO_getValue(self, name):
1157        # Retrieves a single value from the database.  Simple.
1158        assert not self.sqlmeta._obsolete, (
1159            "%s with id %s has become obsolete"               % (self.__class__.__name__, self.id))
1161        # @@: do we really need this lock?
1162        #self._SO_writeLock.acquire()
1163        column = self.sqlmeta.columns[name]
1164        results = self._connection._SO_selectOne(self, [column.dbName])
1165        #self._SO_writeLock.release()
1166        assert results != None, "%s with id %s is not in the database"                  % (self.__class__.__name__, self.id)
1168        value = results[0]
1169        if column.to_python:
1170            value = column.to_python(value, self._SO_validatorState)
1171        return value
1172
1173    def _SO_foreignKey(self, value, joinClass, idName=None):
1174        if value is None:
1175            return None
1176        if self.sqlmeta._perConnection:
1177            connection = self._connection
1178        else:
1179            connection = None
1180        if idName is None: # Get by id
1181            return joinClass.get(value, connection=connection)
1182        return joinClass.select(
1183            getattr(joinClass.q, idName)==value, connection=connection).getOne()
1184
1185    def __init__(self, **kw):
1186        # If we are the outmost constructor of a hiearchy of
1187        # InheritableSQLObjects (or simlpy _the_ constructor of a "normal"
1188        # SQLObject), we create a threadlocal list that collects the
1189        # RowCreatedSignals, and executes them if this very constructor is left
1190        try:
1191            _postponed_local.postponed_calls
1192            postponed_created = False
1193        except AttributeError:
1194            _postponed_local.postponed_calls = []
1195            postponed_created = True
1196
1197        try:
1198            # We shadow the sqlmeta class with an instance of sqlmeta
1199            # that points to us (our sqlmeta buddy object; where the
1200            # sqlmeta class is our class's buddy class)
1201            self.sqlmeta = self.__class__.sqlmeta(self)
1202            # The get() classmethod/constructor uses a magic keyword
1203            # argument when it wants an empty object, fetched from the
1204            # database.  So we have nothing more to do in that case:
1205            if '_SO_fetch_no_create' in kw:
1206                return
1207
1208            post_funcs = []
1209            self.sqlmeta.send(events.RowCreateSignal, self, kw, post_funcs)
1210
1211            # Pass the connection object along if we were given one.
1212            if 'connection' in kw:
1213                connection = kw.pop('connection')
1214                if getattr(self, '_connection', None) is not connection:
1215                    self._connection = connection
1216                    self.sqlmeta._perConnection = True
1217
1218            self._SO_writeLock = threading.Lock()
1219
1220            if 'id' in kw:
1221                id = self.sqlmeta.idType(kw['id'])
1222                del kw['id']
1223            else:
1224                id = None
1225
1226            self._create(id, **kw)
1227
1228            for func in post_funcs:
1229                func(self)
1230        finally:
1231            # if we are the creator of the tl-storage, we
1232            # have to exectute and under all circumstances
1233            # remove the tl-storage
1234            if postponed_created:
1235                try:
1236                    for func in _postponed_local.postponed_calls:
1237                        func()
1238                finally:
1239                    del _postponed_local.postponed_calls
1240
1241    def _create(self, id, **kw):
1242
1243        self.sqlmeta._creating = True
1244        self._SO_createValues = {}
1245        self._SO_validatorState = sqlbuilder.SQLObjectState(self)
1246
1247        # First we do a little fix-up on the keywords we were
1248        # passed:
1249        for column in self.sqlmeta.columnList:
1250
1251            # Then we check if the column wasn't passed in, and
1252            # if not we try to get the default.
1253            if column.name not in kw and column.foreignName not in kw:
1254                default = column.default
1255
1256                # If we don't get it, it's an error:
1257                # If we specified an SQL DEFAULT, then we should use that
1258                if default is NoDefault:
1259                    if column.defaultSQL is None:
1260                        raise TypeError, "%s() did not get expected keyword argument '%s'" % (self.__class__.__name__, column.name)
1261                    else:
1262                        # There is defaultSQL for the column - do not put
1263                        # the column to kw so that the backend creates the value
1264                        continue
1265
1266                # Otherwise we put it in as though they did pass
1267                # that keyword:
1268
1269                kw[column.name] = default
1270
1271        self.set(**kw)
1272
1273        # Then we finalize the process:
1274        self._SO_finishCreate(id)
1275
1276    def _SO_finishCreate(self, id=None):
1277        # Here's where an INSERT is finalized.
1278        # These are all the column values that were supposed
1279        # to be set, but were delayed until now:
1280        setters = self._SO_createValues.items()
1281        # Here's their database names:
1282        names = [self.sqlmeta.columns[v[0]].dbName for v in setters]
1283        values = [v[1] for v in setters]
1284        # Get rid of _SO_create*, we aren't creating anymore.
1285        # Doesn't have to be threadsafe because we're still in
1286        # new(), which doesn't need to be threadsafe.
1287        self.sqlmeta.dirty = False
1288        if not self.sqlmeta.lazyUpdate:
1289            del self._SO_createValues
1290        else:
1291            self._SO_createValues = {}
1292        del self.sqlmeta._creating
1293
1294        # Do the insert -- most of the SQL in this case is left
1295        # up to DBConnection, since getting a new ID is
1296        # non-standard.
1297        id = self._connection.queryInsertID(self,
1298                                            id, names, values)
1299        cache = self._connection.cache
1300        cache.created(id, self.__class__, self)
1301        self._init(id)
1302        post_funcs = []
1303        kw = dict([('class', self.__class__), ('id', id)])
1304        def _send_RowCreatedSignal():
1305            self.sqlmeta.send(events.RowCreatedSignal, self, kw, post_funcs)
1306            for func in post_funcs:
1307                func(self)
1308        _postponed_local.postponed_calls.append(_send_RowCreatedSignal)
1309
1310    def _SO_getID(self, obj, refColumn=None):
1311        return getID(obj, refColumn)
1312
1313    @classmethod
1314    def _findAlternateID(cls, name, dbName, value, connection=None):
1315        if isinstance(name, str):
1316            name = (name,)
1317            value = (value,)
1318        if len(name) != len(value):
1319            raise ValueError, "'column' and 'value' tuples must be of the same size"
1320        new_value = []
1321        for n, v in zip(name, value):
1322            from_python = getattr(cls, '_SO_from_python_' + n)
1323            if from_python:
1324                v = from_python(v, sqlbuilder.SQLObjectState(cls, connection=connection))
1325            new_value.append(v)
1326        condition = sqlbuilder.AND(*[getattr(cls.q, n)==v for n,v in zip(name, new_value)])
1327        return (connection or cls._connection)._SO_selectOneAlt(
1328            cls,
1329            [cls.sqlmeta.idName] +
1330            [column.dbName for column in cls.sqlmeta.columnList],
1331            condition), None
1332
1333    @classmethod
1334    def _SO_fetchAlternateID(cls, name, dbName, value, connection=None, idxName=None):
1335        result, obj = cls._findAlternateID(name, dbName, value, connection)
1336        if not result:
1337            if idxName is None:
1338                raise SQLObjectNotFound, "The %s by alternateID %s = %s does not exist" % (cls.__name__, name, repr(value))
1339            else:
1340                names = []
1341                for i in xrange(len(name)):
1342                    names.append("%s = %s" % (name[i], repr(value[i])))
1343                names = ', '.join(names)
1344                raise SQLObjectNotFound, "The %s by unique index %s(%s) does not exist" % (cls.__name__, idxName, names)
1345        if obj:
1346            return obj
1347        if connection:
1348            obj = cls.get(result[0], connection=connection, selectResults=result[1:])
1349        else:
1350            obj = cls.get(result[0], selectResults=result[1:])
1351        return obj
1352
1353    @classmethod
1354    def _SO_depends(cls):
1355        return findDependencies(cls.__name__, cls.sqlmeta.registry)
1356
1357    @classmethod
1358    def select(cls, clause=None, clauseTables=None,
1359               orderBy=NoDefault, limit=None,
1360               lazyColumns=False, reversed=False,
1361               distinct=False, connection=None,
1362               join=None, forUpdate=False):
1363        return cls.SelectResultsClass(cls, clause,
1364                             clauseTables=clauseTables,
1365                             orderBy=orderBy,
1366                             limit=limit,
1367                             lazyColumns=lazyColumns,
1368                             reversed=reversed,
1369                             distinct=distinct,
1370                             connection=connection,
1371                             join=join, forUpdate=forUpdate)
1372
1373    @classmethod
1374    def selectBy(cls, connection=None, **kw):
1375        conn = connection or cls._connection
1376        return cls.SelectResultsClass(cls,
1377                             conn._SO_columnClause(cls, kw),
1378                             connection=conn)
1379
1380    @classmethod
1381    def tableExists(cls, connection=None):
1382        conn = connection or cls._connection
1383        return conn.tableExists(cls.sqlmeta.table)
1384
1385    @classmethod
1386    def dropTable(cls, ifExists=False, dropJoinTables=True, cascade=False,
1387                  connection=None):
1388        conn = connection or cls._connection
1389        if ifExists and not cls.tableExists(connection=conn):
1390            return
1391        extra_sql = []
1392        post_funcs = []
1393        cls.sqlmeta.send(events.DropTableSignal, cls, connection,
1394                         extra_sql, post_funcs)
1395        conn.dropTable(cls.sqlmeta.table, cascade)
1396        if dropJoinTables:
1397            cls.dropJoinTables(ifExists=ifExists, connection=conn)
1398        for sql in extra_sql:
1399            connection.query(sql)
1400        for func in post_funcs:
1401            func(cls, conn)
1402
1403    @classmethod
1404    def createTable(cls, ifNotExists=False, createJoinTables=True,
1405                    createIndexes=True, applyConstraints=True,
1406                    connection=None):
1407        conn = connection or cls._connection
1408        if ifNotExists and cls.tableExists(connection=conn):
1409            return
1410        extra_sql = []
1411        post_funcs = []
1412        cls.sqlmeta.send(events.CreateTableSignal, cls, connection,
1413                         extra_sql, post_funcs)
1414        constraints = conn.createTable(cls)
1415        if applyConstraints:
1416            for constraint in constraints:
1417                conn.query(constraint)
1418        else:
1419            extra_sql.extend(constraints)
1420        if createJoinTables:
1421            cls.createJoinTables(ifNotExists=ifNotExists,
1422                                 connection=conn)
1423        if createIndexes:
1424            cls.createIndexes(ifNotExists=ifNotExists,
1425                              connection=conn)
1426        for func in post_funcs:
1427            func(cls, conn)
1428        return extra_sql
1429
1430    @classmethod
1431    def createTableSQL(cls, createJoinTables=True, createIndexes=True,
1432                       connection=None):
1433        conn = connection or cls._connection
1434        sql, constraints = conn.createTableSQL(cls)
1435        if createJoinTables:
1436            join_sql = cls.createJoinTablesSQL(connection=conn)
1437            if join_sql:
1438                sql += ';\n' + join_sql
1439        if createIndexes:
1440            index_sql = cls.createIndexesSQL(connection=conn)
1441            if index_sql:
1442                sql += ';\n' + index_sql
1443        return sql, constraints
1444
1445    @classmethod
1446    def createJoinTables(cls, ifNotExists=False, connection=None):
1447        conn = connection or cls._connection
1448        for join in cls._getJoinsToCreate():
1449            if (ifNotExists and
1450                conn.tableExists(join.intermediateTable)):
1451                continue
1452            conn._SO_createJoinTable(join)
1453
1454    @classmethod
1455    def createJoinTablesSQL(cls, connection=None):
1456        conn = connection or cls._connection
1457        sql = []
1458        for join in cls._getJoinsToCreate():
1459            sql.append(conn._SO_createJoinTableSQL(join))
1460        return ';\n'.join(sql)
1461
1462    @classmethod
1463    def createIndexes(cls, ifNotExists=False, connection=None):
1464        conn = connection or cls._connection
1465        for index in cls.sqlmeta.indexes:
1466            if not index:
1467                continue
1468            conn._SO_createIndex(cls, index)
1469
1470    @classmethod
1471    def createIndexesSQL(cls, connection=None):
1472        conn = connection or cls._connection
1473        sql = []
1474        for index in cls.sqlmeta.indexes:
1475            if not index:
1476                continue
1477            sql.append(conn.createIndexSQL(cls, index))
1478        return ';\n'.join(sql)
1479
1480    @classmethod
1481    def _getJoinsToCreate(cls):
1482        joins = []
1483        for join in cls.sqlmeta.joins:
1484            if not join:
1485                continue
1486            if not join.hasIntermediateTable() or not getattr(join, 'createRelatedTable', True):
1487                continue
1488            if join.soClass.__name__ > join.otherClass.__name__:
1489                continue
1490            joins.append(join)
1491        return joins
1492
1493    @classmethod
1494    def dropJoinTables(cls, ifExists=False, connection=None):
1495        conn = connection or cls._connection
1496        for join in cls.sqlmeta.joins:
1497            if not join:
1498                continue
1499            if not join.hasIntermediateTable() or not getattr(join, 'createRelatedTable', True):
1500                continue
1501            if join.soClass.__name__ > join.otherClass.__name__:
1502                continue
1503            if ifExists and                  not conn.tableExists(join.intermediateTable):
1505                continue
1506            conn._SO_dropJoinTable(join)
1507
1508    @classmethod
1509    def clearTable(cls, connection=None, clearJoinTables=True):
1510        # 3-03 @@: Maybe this should check the cache... but it's
1511        # kind of crude anyway, so...
1512        conn = connection or cls._connection
1513        conn.clearTable(cls.sqlmeta.table)
1514        if clearJoinTables:
1515            for join in cls._getJoinsToCreate():
1516                conn.clearTable(join.intermediateTable)
1517
1518    def destroySelf(self):
1519        post_funcs = []
1520        self.sqlmeta.send(events.RowDestroySignal, self, post_funcs)
1521        # Kills this object.  Kills it dead!
1522
1523        klass = self.__class__
1524
1525        # Free related joins on the base class
1526        for join in klass.sqlmeta.joins:
1527            if isinstance(join, joins.SORelatedJoin):
1528                q = "DELETE FROM %s WHERE %s=%d" % (join.intermediateTable, join.joinColumn, self.id)
1529                self._connection.query(q)
1530
1531        depends = []
1532        depends = self._SO_depends()
1533        for k in depends:
1534            # Free related joins
1535            for join in k.sqlmeta.joins:
1536                if isinstance(join, joins.SORelatedJoin) and join.otherClassName == klass.__name__:
1537                    q = "DELETE FROM %s WHERE %s=%d" % (join.intermediateTable, join.otherColumn, self.id)
1538                    self._connection.query(q)
1539
1540            cols = findDependantColumns(klass.__name__, k)
1541
1542            # Don't confuse the rest of the process
1543            if len(cols) == 0:
1544                continue
1545
1546            query = []
1547            delete = setnull = restrict = False
1548            for col in cols:
1549                if col.cascade == False:
1550                    # Found a restriction
1551                    restrict = True
1552                query.append(getattr(k.q, col.name) == self.id)
1553                if col.cascade == 'null':
1554                    setnull = col.name
1555                elif col.cascade:
1556                    delete = True
1557            assert delete or setnull or restrict, (
1558                "Class %s depends on %s accoriding to "
1559                "findDependantColumns, but this seems inaccurate"
1560                % (k, klass))
1561            query = sqlbuilder.OR(*query)
1562            results = k.select(query, connection=self._connection)
1563            if restrict:
1564                if results.count():
1565                    # Restrictions only apply if there are
1566                    # matching records on the related table
1567                    raise SQLObjectIntegrityError, (
1568                        "Tried to delete %s::%s but "
1569                        "table %s has a restriction against it" %
1570                        (klass.__name__, self.id, k.__name__))
1571            else:
1572                for row in results:
1573                    if delete:
1574                        row.destroySelf()
1575                    else:
1576                        row.set(**{setnull: None})
1577
1578        self.sqlmeta._obsolete = True
1579        self._connection._SO_delete(self)
1580        self._connection.cache.expire(self.id, self.__class__)
1581
1582        for func in post_funcs:
1583            func(self)
1584
1585        post_funcs = []
1586        self.sqlmeta.send(events.RowDestroyedSignal, self, post_funcs)
1587        for func in post_funcs:
1588            func(self)
1589
1590    @classmethod
1591    def delete(cls, id, connection=None):
1592        obj = cls.get(id, connection=connection)
1593        obj.destroySelf()
1594
1595    @classmethod
1596    def deleteMany(cls, where=NoDefault, connection=None):
1597        conn = connection or cls._connection
1598        conn.query(conn.sqlrepr(sqlbuilder.Delete(cls.sqlmeta.table, where)))
1599
1600    @classmethod
1601    def deleteBy(cls, connection=None, **kw):
1602        conn = connection or cls._connection
1603        conn.query(conn.sqlrepr(sqlbuilder.Delete(cls.sqlmeta.table,
1604            conn._SO_columnClause(cls, kw))))
1605
1606    def __repr__(self):
1607        if not hasattr(self, 'id'):
1608            # Object initialization not finished.  No attributes can be read.
1609            return '<%s (not initialized)>' % self.__class__.__name__
1610        return '<%s %r %s>'                  % (self.__class__.__name__,
1612                  self.id,
1613                  ' '.join(['%s=%s' % (name, repr(value)) for name, value in self._reprItems()]))
1614
1615    def __sqlrepr__(self, db):
1616        return str(self.id)
1617
1618    @classmethod
1619    def sqlrepr(cls, value, connection=None):
1620        return (connection or cls._connection).sqlrepr(value)
1621
1622    @classmethod
1623    def coerceID(cls, value):
1624        if isinstance(value, cls):
1625            return value.id
1626        else:
1627            return cls.sqlmeta.idType(value)
1628
1629    def _reprItems(self):
1630        items = []
1631        for col in self.sqlmeta.columnList:
1632            value = getattr(self, col.name)
1633            r = repr(value)
1634            if len(r) > 20:
1635                value = r[:17] + "..." + r[-1]
1636            items.append((col.name, value))
1637        return items
1638
1639    @classmethod
1640    def setConnection(cls, value):
1641        if isinstance(value, basestring):
1642            value = dbconnection.connectionForURI(value)
1643        cls._connection = value
1644
1645    def tablesUsedImmediate(self):
1646        return [self.__class__.q]
1647
1648
1649    # Comparison
1650
1651    def __eq__(self, other):
1652        if self.__class__ is other.__class__:
1653            if self.id == other.id:
1654                return True
1655        return False
1656
1657    def __ne__(self, other):
1658        return not self.__eq__(other)
1659
1660    def __lt__(self, other):
1661        return NotImplemented
1662
1663    def __le__(self, other):
1664        return NotImplemented
1665
1666    def __gt__(self, other):
1667        return NotImplemented
1668
1669    def __ge__(self, other):
1670        return NotImplemented
1671
1672
1673    # (De)serialization (pickle, etc.)
1674
1675    def __getstate__(self):
1676        if self.sqlmeta._perConnection:
1677            from pickle import PicklingError
1678            raise PicklingError('Cannot pickle an SQLObject instance that has a per-instance connection')
1679        if self.sqlmeta.lazyUpdate and self._SO_createValues:
1680            self.syncUpdate()
1681        d = self.__dict__.copy()
1682        del d['sqlmeta']
1683        del d['_SO_validatorState']
1684        del d['_SO_writeLock']
1685        del d['_SO_createValues']
1686        return d
1687
1688    def __setstate__(self, d):
1689        self.__init__(_SO_fetch_no_create=1)
1690        self._SO_validatorState = sqlbuilder.SQLObjectState(self)
1691        self._SO_writeLock = threading.Lock()
1692        self._SO_createValues = {}
1693        self.__dict__.update(d)
1694        cls = self.__class__
1695        cache = self._connection.cache
1696        if cache.tryGet(self.id, cls) is not None:
1697            raise ValueError(
1698                "Cannot unpickle %s row with id=%s - a different instance with the id already exists in the cache" % (cls.__name__, self.id))
1699        cache.created(self.id, cls, self)
1700
1701
1702def setterName(name):
1703    return '_set_%s' % name
1704def rawSetterName(name):
1705    return '_SO_set_%s' % name
1706def getterName(name):
1707    return '_get_%s' % name
1708def rawGetterName(name):
1709    return '_SO_get_%s' % name
1710def instanceName(name):
1711    return '_SO_val_%s' % name
1712
1713
1714########################################
1715## Utility functions (for external consumption)
1716########################################
1717
1718def getID(obj, refColumn=None):
1719    if isinstance(obj, SQLObject):
1720        return getattr(obj, refColumn or 'id')
1721    elif isinstance(obj, int):
1722        return obj
1723    elif isinstance(obj, long):
1724        return int(obj)
1725    elif isinstance(obj, str):
1726        try:
1727            return int(obj)
1728        except ValueError:
1729            return obj
1730    elif obj is None:
1731        return None
1732
1733def getObject(obj, klass):
1734    if isinstance(obj, int):
1735        return klass(obj)
1736    elif isinstance(obj, long):
1737        return klass(int(obj))
1738    elif isinstance(obj, str):
1739        return klass(int(obj))
1740    elif obj is None:
1741        return None
1742    else:
1743        return obj
1744
1745__all__ = ['NoDefault', 'SQLObject',
1746           'SQLObjectIntegrityError', 'SQLObjectNotFound',
1747           'getID', 'getObject', 'sqlhub', 'sqlmeta',
1748          ]