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