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., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
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, 4, 0):
0047    raise ImportError, "SQLObject requires Python 2.4.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_postponed_local = local()
0055
0056NoDefault = sqlbuilder.NoDefault
0057
0058class SQLObjectNotFound(LookupError): pass
0059class SQLObjectIntegrityError(Exception): pass
0060
0061def makeProperties(obj):
0062    """
0063    This function takes a dictionary of methods and finds
0064    methods named like:
0065    * _get_attr
0066    * _set_attr
0067    * _del_attr
0068    * _doc_attr
0069    Except for _doc_attr, these should be methods.  It
0070    then creates properties from these methods, like
0071    property(_get_attr, _set_attr, _del_attr, _doc_attr).
0072    Missing methods are okay.
0073    """
0074
0075    if isinstance(obj, dict):
0076        def setFunc(var, value):
0077            obj[var] = value
0078        d = obj
0079    else:
0080        def setFunc(var, value):
0081            setattr(obj, var, value)
0082        d = obj.__dict__
0083
0084    props = {}
0085    for var, value in d.items():
0086        if var.startswith('_set_'):
0087            props.setdefault(var[5:], {})['set'] = value
0088        elif var.startswith('_get_'):
0089            props.setdefault(var[5:], {})['get'] = value
0090        elif var.startswith('_del_'):
0091            props.setdefault(var[5:], {})['del'] = value
0092        elif var.startswith('_doc_'):
0093            props.setdefault(var[5:], {})['doc'] = value
0094    for var, setters in props.items():
0095        if len(setters) == 1 and 'doc' in setters:
0096            continue
0097        if var in d:
0098            if isinstance(d[var], (types.MethodType, types.FunctionType)):
0099                warnings.warn(
0100                    "I tried to set the property %r, but it was "
0101                    "already set, as a method (%r).  Methods have "
0102                    "significantly different semantics than properties, "
0103                    "and this may be a sign of a bug in your code."
0104                    % (var, d[var]))
0105            continue
0106        setFunc(var,
0107                property(setters.get('get'), setters.get('set'),
0108                         setters.get('del'), setters.get('doc')))
0109
0110def unmakeProperties(obj):
0111    if isinstance(obj, dict):
0112        def delFunc(obj, var):
0113            del obj[var]
0114        d = obj
0115    else:
0116        delFunc = delattr
0117        d = obj.__dict__
0118
0119    for var, value in d.items():
0120        if isinstance(value, property):
0121            for prop in [value.fget, value.fset, value.fdel]:
0122                if prop and not prop.__name__ in d:
0123                    delFunc(obj, var)
0124                    break
0125
0126def findDependencies(name, registry=None):
0127    depends = []
0128    for klass in classregistry.registry(registry).allClasses():
0129        if findDependantColumns(name, klass):
0130            depends.append(klass)
0131        else:
0132            for join in klass.sqlmeta.joins:
0133                if isinstance(join, joins.SORelatedJoin) and join.otherClassName == name:
0134                    depends.append(klass)
0135                    break
0136    return depends
0137
0138def findDependantColumns(name, klass):
0139    depends = []
0140    for col in klass.sqlmeta.columnList:
0141        if col.foreignKey == name and col.cascade is not None:
0142            depends.append(col)
0143    return depends
0144
0145def _collectAttributes(cls, new_attrs, look_for_class):
0146    """Finds all attributes in `new_attrs` that are instances of
0147    `look_for_class`. The ``.name`` attribute is set for any matching objects.
0148    Returns them as a list.
0149
0150    """
0151    result = []
0152    for attr, value in new_attrs.items():
0153        if isinstance(value, look_for_class):
0154            value.name = attr
0155            delattr(cls, attr)
0156            result.append(value)
0157    return result
0158
0159class CreateNewSQLObject:
0160    """
0161    Dummy singleton to use in place of an ID, to signal we want
0162    a new object.
0163    """
0164    pass
0165
0166class sqlmeta(object):
0167
0168    """
0169    This object is the object we use to keep track of all sorts of
0170    information.  Subclasses are made for each SQLObject subclass
0171    (dynamically if necessary), and instances are created to go
0172    alongside every SQLObject instance.
0173    """
0174
0175    table = None
0176    idName = None
0177    idSequence = None
0178    # This function is used to coerce IDs into the proper format,
0179    # so you should replace it with str, or another function, if you
0180    # aren't using integer IDs
0181    idType = int
0182    style = None
0183    lazyUpdate = False
0184    defaultOrder = None
0185    cacheValues = True
0186    registry = None
0187    fromDatabase = False
0188    # Default is false, but we set it to true for the *instance*
0189    # when necessary: (bad clever? maybe)
0190    expired = False
0191
0192    # This is a mapping from column names to SOCol (or subclass)
0193    # instances:
0194    columns = {}
0195    columnList = []
0196
0197    # This is a mapping from column names to Col (or subclass)
0198    # instances; these objects don't have the logic that the SOCol
0199    # objects do, and are not attached to this class closely.
0200    columnDefinitions = {}
0201
0202    # These are lists of the join and index objects:
0203    indexes = []
0204    indexDefinitions = []
0205    joins = []
0206    joinDefinitions = []
0207
0208    # These attributes shouldn't be shared with superclasses:
0209    _unshared_attributes = ['table', 'columns', 'childName']
0210
0211    # These are internal bookkeeping attributes; the class-level
0212    # definition is a default for the instances, instances will
0213    # reset these values.
0214
0215    # When an object is being created, it has an instance
0216    # variable _creating, which is true.  This way all the
0217    # setters can be captured until the object is complete,
0218    # and then the row is inserted into the database.  Once
0219    # that happens, _creating is deleted from the instance,
0220    # and only the class variable (which is always false) is
0221    # left.
0222    _creating = False
0223    _obsolete = False
0224    # Sometimes an intance is attached to a connection, not
0225    # globally available.  In that case, self.sqlmeta._perConnection
0226    # will be true.  It's false by default:
0227    _perConnection = False
0228
0229    # Inheritance definitions:
0230    parentClass = None # A reference to the parent class
0231    childClasses = {} # References to child classes, keyed by childName
0232    childName = None # Class name for inheritance child object creation
0233
0234    # Does the row require syncing?
0235    dirty = False
0236
0237    # Default encoding for UnicodeCol's
0238    dbEncoding = None
0239
0240    __metaclass__ = declarative.DeclarativeMeta
0241
0242    def __classinit__(cls, new_attrs):
0243        for attr in cls._unshared_attributes:
0244            if attr not in new_attrs:
0245                setattr(cls, attr, None)
0246        declarative.setup_attributes(cls, new_attrs)
0247
0248    def __init__(self, instance):
0249        self.instance = weakref.proxy(instance)
0250
0251    @classmethod
0252    def send(cls, signal, *args, **kw):
0253        events.send(signal, cls.soClass, *args, **kw)
0254
0255    @classmethod
0256    def setClass(cls, soClass):
0257        cls.soClass = soClass
0258        if not cls.style:
0259            cls.style = styles.defaultStyle
0260            try:
0261                if cls.soClass._connection and cls.soClass._connection.style:
0262                    cls.style = cls.soClass._connection.style
0263            except AttributeError:
0264                pass
0265        if cls.table is None:
0266            cls.table = cls.style.pythonClassToDBTable(cls.soClass.__name__)
0267        if cls.idName is None:
0268            cls.idName = cls.style.idForTable(cls.table)
0269
0270        # plainSetters are columns that haven't been overridden by the
0271        # user, so we can contact the database directly to set them.
0272        # Note that these can't set these in the SQLObject class
0273        # itself, because they specific to this subclass of SQLObject,
0274        # and cannot be shared among classes.
0275        cls._plainSetters = {}
0276        cls._plainGetters = {}
0277        cls._plainForeignSetters = {}
0278        cls._plainForeignGetters = {}
0279        cls._plainJoinGetters = {}
0280        cls._plainJoinAdders = {}
0281        cls._plainJoinRemovers = {}
0282
0283        # This is a dictionary of columnName: columnObject
0284        # None of these objects can be shared with superclasses
0285        cls.columns = {}
0286        cls.columnList = []
0287        # These, however, can be shared:
0288        cls.columnDefinitions = cls.columnDefinitions.copy()
0289        cls.indexes = []
0290        cls.indexDefinitions = cls.indexDefinitions[:]
0291        cls.joins = []
0292        cls.joinDefinitions = cls.joinDefinitions[:]
0293
0294    ############################################################
0295    ## Adding special values, like columns and indexes
0296    ############################################################
0297
0298    ########################################
0299    ## Column handling
0300    ########################################
0301
0302    @classmethod
0303    def addColumn(cls, columnDef, changeSchema=False, connection=None):
0304        post_funcs = []
0305        cls.send(events.AddColumnSignal, cls.soClass, connection,
0306                 columnDef.name, columnDef, changeSchema, post_funcs)
0307        sqlmeta = cls
0308        soClass = cls.soClass
0309        del cls
0310        column = columnDef.withClass(soClass)
0311        name = column.name
0312        assert name != 'id', (
0313            "The 'id' column is implicit, and should not be defined as "
0314            "a column")
0315        assert name not in sqlmeta.columns, (
0316            "The class %s.%s already has a column %r (%r), you cannot "
0317            "add the column %r"
0318            % (soClass.__module__, soClass.__name__, name,
0319               sqlmeta.columnDefinitions[name], columnDef))
0320        # Collect columns from the parent classes to test
0321        # if the column is not in a parent class
0322        parent_columns = []
0323        for base in soClass.__bases__:
0324            if hasattr(base, "sqlmeta"):
0325                parent_columns.extend(base.sqlmeta.columns.keys())
0326        if hasattr(soClass, name):
0327            assert  (name in parent_columns) or (name == "childName"), (
0328                "The class %s.%s already has a variable or method %r, you cannot "
0329                "add the column %r"
0330                % (soClass.__module__, soClass.__name__, name, name))
0331        sqlmeta.columnDefinitions[name] = columnDef
0332        sqlmeta.columns[name] = column
0333        # A stable-ordered version of the list...
0334        sqlmeta.columnList.append(column)
0335
0336        ###################################################
0337        # Create the getter function(s).  We'll start by
0338        # creating functions like _SO_get_columnName,
0339        # then if there's no function named _get_columnName
0340        # we'll alias that to _SO_get_columnName.  This
0341        # allows a sort of super call, even though there's
0342        # no superclass that defines the database access.
0343        if sqlmeta.cacheValues:
0344            # We create a method here, which is just a function
0345            # that takes "self" as the first argument.
0346            getter = eval('lambda self: self._SO_loadValue(%s)' % repr(instanceName(name)))
0347
0348        else:
0349            # If we aren't caching values, we just call the
0350            # function _SO_getValue, which fetches from the
0351            # database.
0352            getter = eval('lambda self: self._SO_getValue(%s)' % repr(name))
0353        setattr(soClass, rawGetterName(name), getter)
0354
0355        # Here if the _get_columnName method isn't in the
0356        # definition, we add it with the default
0357        # _SO_get_columnName definition.
0358        if not hasattr(soClass, getterName(name)) or (name == 'childName'):
0359            setattr(soClass, getterName(name), getter)
0360            sqlmeta._plainGetters[name] = 1
0361
0362        #################################################
0363        # Create the setter function(s)
0364        # Much like creating the getters, we will create
0365        # _SO_set_columnName methods, and then alias them
0366        # to _set_columnName if the user hasn't defined
0367        # those methods themself.
0368
0369        # @@: This is lame; immutable right now makes it unsettable,
0370        # making the table read-only
0371        if not column.immutable:
0372            # We start by just using the _SO_setValue method
0373            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))
0374            setattr(soClass, '_SO_from_python_%s' % name, column.from_python)
0375            setattr(soClass, '_SO_to_python_%s' % name, column.to_python)
0376            setattr(soClass, rawSetterName(name), setter)
0377            # Then do the aliasing
0378            if not hasattr(soClass, setterName(name)) or (name == 'childName'):
0379                setattr(soClass, setterName(name), setter)
0380                # We keep track of setters that haven't been
0381                # overridden, because we can combine these
0382                # set columns into one SQL UPDATE query.
0383                sqlmeta._plainSetters[name] = 1
0384
0385        ##################################################
0386        # Here we check if the column is a foreign key, in
0387        # which case we need to make another method that
0388        # fetches the key and constructs the sister
0389        # SQLObject instance.
0390        if column.foreignKey:
0391
0392            # We go through the standard _SO_get_columnName deal
0393            # we're giving the object, not the ID of the
0394            # object this time:
0395            origName = column.origName
0396            if sqlmeta.cacheValues:
0397                # self._SO_class_className is a reference
0398                # to the class in question.
0399                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)))
0400            else:
0401                # Same non-caching version as above.
0402                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)))
0403            setattr(soClass, rawGetterName(origName), getter)
0404
0405            # And we set the _get_columnName version
0406            if not hasattr(soClass, getterName(origName)):
0407                setattr(soClass, getterName(origName), getter)
0408                sqlmeta._plainForeignGetters[origName] = 1
0409
0410            if not column.immutable:
0411                # The setter just gets the ID of the object,
0412                # and then sets the real column.
0413                setter = eval('lambda self, val: setattr(self, %s, self._SO_getID(val, %s))' % (repr(name), column.refColumn and repr(column.refColumn)))
0414                setattr(soClass, rawSetterName(origName), setter)
0415                if not hasattr(soClass, setterName(origName)):
0416                    setattr(soClass, setterName(origName), setter)
0417                    sqlmeta._plainForeignSetters[origName] = 1
0418
0419            classregistry.registry(sqlmeta.registry).addClassCallback(
0420                column.foreignKey,
0421                lambda foreign, me, attr: setattr(me, attr, foreign),
0422                soClass, '_SO_class_%s' % column.foreignKey)
0423
0424        if column.alternateMethodName:
0425            func = eval('lambda cls, val, connection=None: cls._SO_fetchAlternateID(%s, %s, val, connection=connection)' % (repr(column.name), repr(column.dbName)))
0426            setattr(soClass, column.alternateMethodName, classmethod(func))
0427
0428        if changeSchema:
0429            conn = connection or soClass._connection
0430            conn.addColumn(sqlmeta.table, column)
0431
0432        if soClass._SO_finishedClassCreation:
0433            makeProperties(soClass)
0434
0435        for func in post_funcs:
0436            func(soClass, column)
0437
0438    @classmethod
0439    def addColumnsFromDatabase(sqlmeta, connection=None):
0440        soClass = sqlmeta.soClass
0441        conn = connection or soClass._connection
0442        for columnDef in conn.columnsFromSchema(sqlmeta.table, soClass):
0443            if columnDef.name not in sqlmeta.columnDefinitions:
0444                if isinstance(columnDef.name, unicode):
0445                    columnDef.name = columnDef.name.encode('ascii')
0446                sqlmeta.addColumn(columnDef)
0447
0448    @classmethod
0449    def delColumn(cls, column, changeSchema=False, connection=None):
0450        sqlmeta = cls
0451        soClass = sqlmeta.soClass
0452        if isinstance(column, str):
0453            if column in sqlmeta.columns:
0454                column = sqlmeta.columns[column]
0455            elif column+'ID' in sqlmeta.columns:
0456                column = sqlmeta.columns[column+'ID']
0457            else:
0458                raise ValueError('Unknown column ' + column)
0459        if isinstance(column, col.Col):
0460            for c in sqlmeta.columns.values():
0461                if column is c.columnDef:
0462                    column = c
0463                    break
0464            else:
0465                raise IndexError(
0466                    "Column with definition %r not found" % column)
0467        post_funcs = []
0468        cls.send(events.DeleteColumnSignal, cls.soClass, connection,
0469                 column.name, column, post_funcs)
0470        name = column.name
0471        del sqlmeta.columns[name]
0472        del sqlmeta.columnDefinitions[name]
0473        sqlmeta.columnList.remove(column)
0474        delattr(soClass, rawGetterName(name))
0475        if name in sqlmeta._plainGetters:
0476            delattr(soClass, getterName(name))
0477        delattr(soClass, rawSetterName(name))
0478        if name in sqlmeta._plainSetters:
0479            delattr(soClass, setterName(name))
0480        if column.foreignKey:
0481            delattr(soClass, rawGetterName(soClass.sqlmeta.style.instanceIDAttrToAttr(name)))
0482            if name in sqlmeta._plainForeignGetters:
0483                delattr(soClass, getterName(name))
0484            delattr(soClass, rawSetterName(soClass.sqlmeta.style.instanceIDAttrToAttr(name)))
0485            if name in sqlmeta._plainForeignSetters:
0486                delattr(soClass, setterName(name))
0487        if column.alternateMethodName:
0488            delattr(soClass, column.alternateMethodName)
0489
0490        if changeSchema:
0491            conn = connection or soClass._connection
0492            conn.delColumn(sqlmeta, column)
0493
0494        if soClass._SO_finishedClassCreation:
0495            unmakeProperties(soClass)
0496            makeProperties(soClass)
0497
0498        for func in post_funcs:
0499            func(soClass, column)
0500
0501    ########################################
0502    ## Join handling
0503    ########################################
0504
0505    @classmethod
0506    def addJoin(cls, joinDef):
0507        sqlmeta = cls
0508        soClass = cls.soClass
0509        # The name of the method we'll create.  If it's
0510        # automatically generated, it's generated by the
0511        # join class.
0512        join = joinDef.withClass(soClass)
0513        meth = join.joinMethodName
0514
0515        sqlmeta.joins.append(join)
0516        index = len(sqlmeta.joins)-1
0517        if joinDef not in sqlmeta.joinDefinitions:
0518            sqlmeta.joinDefinitions.append(joinDef)
0519
0520        # The function fetches the join by index, and
0521        # then lets the join object do the rest of the
0522        # work:
0523        func = eval('lambda self: self.sqlmeta.joins[%i].performJoin(self)' % index)
0524
0525        # And we do the standard _SO_get_... _get_... deal
0526        setattr(soClass, rawGetterName(meth), func)
0527        if not hasattr(soClass, getterName(meth)):
0528            setattr(soClass, getterName(meth), func)
0529            sqlmeta._plainJoinGetters[meth] = 1
0530
0531        # Some joins allow you to remove objects from the
0532        # join.
0533        if hasattr(join, 'remove'):
0534            # Again, we let it do the remove, and we do the
0535            # standard naming trick.
0536            func = eval('lambda self, obj: self.sqlmeta.joins[%i].remove(self, obj)' % index)
0537            setattr(soClass, '_SO_remove' + join.addRemoveName, func)
0538            if not hasattr(soClass, 'remove' + join.addRemoveName):
0539                setattr(soClass, 'remove' + join.addRemoveName, func)
0540                sqlmeta._plainJoinRemovers[meth] = 1
0541
0542        # Some joins allow you to add objects.
0543        if hasattr(join, 'add'):
0544            # And again...
0545            func = eval('lambda self, obj: self.sqlmeta.joins[%i].add(self, obj)' % index)
0546            setattr(soClass, '_SO_add' + join.addRemoveName, func)
0547            if not hasattr(soClass, 'add' + join.addRemoveName):
0548                setattr(soClass, 'add' + join.addRemoveName, func)
0549                sqlmeta._plainJoinAdders[meth] = 1
0550
0551        if soClass._SO_finishedClassCreation:
0552            makeProperties(soClass)
0553
0554    @classmethod
0555    def delJoin(sqlmeta, joinDef):
0556        soClass = sqlmeta.soClass
0557        for join in sqlmeta.joins:
0558            # previously deleted joins will be None, so it must
0559            # be skipped or it'll error out on the next line.
0560            if join is None:
0561                continue
0562            if joinDef is join.joinDef:
0563                break
0564        else:
0565            raise IndexError(
0566                "Join %r not found in class %r (from %r)"
0567                % (joinDef, soClass, sqlmeta.joins))
0568        meth = join.joinMethodName
0569        sqlmeta.joinDefinitions.remove(joinDef)
0570        for i in range(len(sqlmeta.joins)):
0571            if sqlmeta.joins[i] is join:
0572                # Have to leave None, because we refer to joins
0573                # by index.
0574                sqlmeta.joins[i] = None
0575        delattr(soClass, rawGetterName(meth))
0576        if meth in sqlmeta._plainJoinGetters:
0577            delattr(soClass, getterName(meth))
0578        if hasattr(join, 'remove'):
0579            delattr(soClass, '_SO_remove' + join.addRemovePrefix)
0580            if meth in sqlmeta._plainJoinRemovers:
0581                delattr(soClass, 'remove' + join.addRemovePrefix)
0582        if hasattr(join, 'add'):
0583            delattr(soClass, '_SO_add' + join.addRemovePrefix)
0584            if meth in sqlmeta._plainJoinAdders:
0585                delattr(soClass, 'add' + join.addRemovePrefix)
0586
0587        if soClass._SO_finishedClassCreation:
0588            unmakeProperties(soClass)
0589            makeProperties(soClass)
0590
0591    ########################################
0592    ## Indexes
0593    ########################################
0594
0595    @classmethod
0596    def addIndex(cls, indexDef):
0597        cls.indexDefinitions.append(indexDef)
0598        index = indexDef.withClass(cls.soClass)
0599        cls.indexes.append(index)
0600        setattr(cls.soClass, index.name, index)
0601
0602    ########################################
0603    ## Utility methods
0604    ########################################
0605
0606    @classmethod
0607    def getColumns(sqlmeta):
0608        return sqlmeta.columns.copy()
0609
0610    def asDict(self):
0611        """
0612        Return the object as a dictionary of columns to values.
0613        """
0614        result = {}
0615        for key in self.getColumns():
0616            result[key] = getattr(self.instance, key)
0617        result['id'] = self.instance.id
0618        return result
0619
0620    @classmethod
0621    def expireAll(sqlmeta, connection=None):
0622        """
0623        Expire all instances of this class.
0624        """
0625        soClass = sqlmeta.soClass
0626        connection = connection or soClass._connection
0627        cache_set = connection.cache
0628        cache_set.weakrefAll(soClass)
0629        for item in cache_set.getAll(soClass):
0630            item.expire()
0631
0632
0633sqlhub = dbconnection.ConnectionHub()
0634
0635class _sqlmeta_attr(object):
0636
0637    def __init__(self, name, deprecation_level):
0638        self.name = name
0639        self.deprecation_level = deprecation_level
0640
0641    def __get__(self, obj, type=None):
0642        if self.deprecation_level is not None:
0643            deprecated(
0644                'Use of this attribute should be replaced with '
0645                '.sqlmeta.%s' % self.name, level=self.deprecation_level)
0646        return getattr((type or obj).sqlmeta, self.name)
0647
0648
0649# @@: This should become a public interface or documented or
0650# something.  Turning it on gives earlier warning about things
0651# that will be deprecated (having this off we won't flood people
0652# with warnings right away).
0653warnings_level = 1
0654exception_level = None
0655# Current levels:
0656#  1) Actively deprecated
0657#  2) Deprecated after 1
0658#  3) Deprecated after 2
0659
0660def deprecated(message, level=1, stacklevel=2):
0661    if exception_level is not None and exception_level <= level:
0662        raise NotImplementedError(message)
0663    if warnings_level is not None and warnings_level <= level:
0664        warnings.warn(message, DeprecationWarning, stacklevel=stacklevel)
0665
0666#if sys.version_info[:3] < (2, 5, 0):
0667#    deprecated("Support for Python 2.4 has been declared obsolete and will be removed in the next release of SQLObject")
0668
0669def setDeprecationLevel(warning=1, exception=None):
0670    """
0671    Set the deprecation level for SQLObject.  Low levels are more
0672    actively being deprecated.  Any warning at a level at or below
0673    ``warning`` will give a warning.  Any warning at a level at or
0674    below ``exception`` will give an exception.  You can use a higher
0675    ``exception`` level for tests to help upgrade your code.  ``None``
0676    for either value means never warn or raise exceptions.
0677
0678    The levels currently mean:
0679
0680      1) Deprecated in current version (0.9).  Will be removed in next
0681         version (0.10)
0682
0683      2) Planned to deprecate in next version, remove later.
0684
0685      3) Planned to deprecate sometime, remove sometime much later ;)
0686
0687    As the SQLObject versions progress, the deprecation level of
0688    specific features will go down, indicating the advancing nature of
0689    the feature's doom.  We'll try to keep features at 1 for a major
0690    revision.
0691
0692    As time continues there may be a level 0, which will give a useful
0693    error message (better than ``AttributeError``) but where the
0694    feature has been fully removed.
0695    """
0696    global warnings_level, exception_level
0697    warnings_level = warning
0698    exception_level = exception
0699
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        d = self.__dict__.copy()
1680        del d['sqlmeta']
1681        del d['_SO_writeLock']
1682        return d
1683
1684    def __setstate__(self, d):
1685        self.__init__(_SO_fetch_no_create=1)
1686        self._SO_writeLock = threading.Lock()
1687        self.__dict__.update(d)
1688        cls = self.__class__
1689        cache = self._connection.cache
1690        if cache.tryGet(self.id, cls) is not None:
1691            raise ValueError(
1692                "Cannot unpickle %s row with id=%s - a different instance with the id already exists in the cache" % (cls.__name__, self.id))
1693        cache.created(id, cls, self)
1694
1695
1696def setterName(name):
1697    return '_set_%s' % name
1698def rawSetterName(name):
1699    return '_SO_set_%s' % name
1700def getterName(name):
1701    return '_get_%s' % name
1702def rawGetterName(name):
1703    return '_SO_get_%s' % name
1704def instanceName(name):
1705    return '_SO_val_%s' % name
1706
1707
1708########################################
1709## Utility functions (for external consumption)
1710########################################
1711
1712def getID(obj, refColumn=None):
1713    if isinstance(obj, SQLObject):
1714        return getattr(obj, refColumn or 'id')
1715    elif isinstance(obj, int):
1716        return obj
1717    elif isinstance(obj, long):
1718        return int(obj)
1719    elif isinstance(obj, str):
1720        try:
1721            return int(obj)
1722        except ValueError:
1723            return obj
1724    elif obj is None:
1725        return None
1726
1727def getObject(obj, klass):
1728    if isinstance(obj, int):
1729        return klass(obj)
1730    elif isinstance(obj, long):
1731        return klass(int(obj))
1732    elif isinstance(obj, str):
1733        return klass(int(obj))
1734    elif obj is None:
1735        return None
1736    else:
1737        return obj
1738
1739__all__ = ['NoDefault', 'SQLObject', 'sqlmeta', 'sqlhub',
1740           'getID', 'getObject', 'setDeprecationLevel',
1741           'SQLObjectNotFound', 'SQLObjectIntegrityError',
1742          ]