base.py 16.6 KB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433
# ext/declarative/base.py
# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors <see AUTHORS file>
#
# This module is part of SQLAlchemy and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
"""Internal implementation for declarative."""

from ...schema import Table, Column
from ...orm import mapper, class_mapper
from ...orm.interfaces import MapperProperty
from ...orm.properties import ColumnProperty, CompositeProperty
from ...orm.util import _is_mapped_class
from ... import util, exc
from ...sql import expression
from ... import event
from . import clsregistry


def _declared_mapping_info(cls):
    # deferred mapping
    if cls in _MapperConfig.configs:
        return _MapperConfig.configs[cls]
    # regular mapping
    elif _is_mapped_class(cls):
        return class_mapper(cls, configure=False)
    else:
        return None


def _as_declarative(cls, classname, dict_):
    from .api import declared_attr

    # dict_ will be a dictproxy, which we can't write to, and we need to!
    dict_ = dict(dict_)

    column_copies = {}
    potential_columns = {}

    mapper_args_fn = None
    table_args = inherited_table_args = None
    tablename = None

    declarative_props = (declared_attr, util.classproperty)

    for base in cls.__mro__:
        _is_declarative_inherits = hasattr(base, '_decl_class_registry')

        if '__declare_last__' in base.__dict__:
            @event.listens_for(mapper, "after_configured")
            def go():
                cls.__declare_last__()
        if '__abstract__' in base.__dict__:
            if (base is cls or
                (base in cls.__bases__ and not _is_declarative_inherits)
            ):
                return

        class_mapped = _declared_mapping_info(base) is not None

        for name, obj in vars(base).items():
            if name == '__mapper_args__':
                if not mapper_args_fn and (
                                        not class_mapped or
                                        isinstance(obj, declarative_props)
                                    ):
                    # don't even invoke __mapper_args__ until
                    # after we've determined everything about the
                    # mapped table.
                    mapper_args_fn = lambda: cls.__mapper_args__
            elif name == '__tablename__':
                if not tablename and (
                                        not class_mapped or
                                        isinstance(obj, declarative_props)
                                    ):
                    tablename = cls.__tablename__
            elif name == '__table_args__':
                if not table_args and (
                                        not class_mapped or
                                        isinstance(obj, declarative_props)
                                    ):
                    table_args = cls.__table_args__
                    if not isinstance(table_args, (tuple, dict, type(None))):
                        raise exc.ArgumentError(
                                "__table_args__ value must be a tuple, "
                                "dict, or None")
                    if base is not cls:
                        inherited_table_args = True
            elif class_mapped:
                if isinstance(obj, declarative_props):
                    util.warn("Regular (i.e. not __special__) "
                            "attribute '%s.%s' uses @declared_attr, "
                            "but owning class %s is mapped - "
                            "not applying to subclass %s."
                            % (base.__name__, name, base, cls))
                continue
            elif base is not cls:
                # we're a mixin.
                if isinstance(obj, Column):
                    if getattr(cls, name) is not obj:
                        # if column has been overridden
                        # (like by the InstrumentedAttribute of the
                        # superclass), skip
                        continue
                    if obj.foreign_keys:
                        raise exc.InvalidRequestError(
                        "Columns with foreign keys to other columns "
                        "must be declared as @declared_attr callables "
                        "on declarative mixin classes. ")
                    if name not in dict_ and not (
                            '__table__' in dict_ and
                            (obj.name or name) in dict_['__table__'].c
                            ) and name not in potential_columns:
                        potential_columns[name] = \
                                column_copies[obj] = \
                                obj.copy()
                        column_copies[obj]._creation_order = \
                                obj._creation_order
                elif isinstance(obj, MapperProperty):
                    raise exc.InvalidRequestError(
                        "Mapper properties (i.e. deferred,"
                        "column_property(), relationship(), etc.) must "
                        "be declared as @declared_attr callables "
                        "on declarative mixin classes.")
                elif isinstance(obj, declarative_props):
                    dict_[name] = ret = \
                            column_copies[obj] = getattr(cls, name)
                    if isinstance(ret, (Column, MapperProperty)) and \
                        ret.doc is None:
                        ret.doc = obj.__doc__

    # apply inherited columns as we should
    for k, v in potential_columns.items():
        dict_[k] = v

    if inherited_table_args and not tablename:
        table_args = None

    clsregistry.add_class(classname, cls)
    our_stuff = util.OrderedDict()

    for k in list(dict_):

        # TODO: improve this ?  all dunders ?
        if k in ('__table__', '__tablename__', '__mapper_args__'):
            continue

        value = dict_[k]
        if isinstance(value, declarative_props):
            value = getattr(cls, k)

        if (isinstance(value, tuple) and len(value) == 1 and
            isinstance(value[0], (Column, MapperProperty))):
            util.warn("Ignoring declarative-like tuple value of attribute "
                      "%s: possibly a copy-and-paste error with a comma "
                      "left at the end of the line?" % k)
            continue
        if not isinstance(value, (Column, MapperProperty)):
            if not k.startswith('__'):
                dict_.pop(k)
                setattr(cls, k, value)
            continue
        if k == 'metadata':
            raise exc.InvalidRequestError(
                "Attribute name 'metadata' is reserved "
                "for the MetaData instance when using a "
                "declarative base class."
            )
        prop = clsregistry._deferred_relationship(cls, value)
        our_stuff[k] = prop

    # set up attributes in the order they were created
    our_stuff.sort(key=lambda key: our_stuff[key]._creation_order)

    # extract columns from the class dict
    declared_columns = set()
    for key, c in our_stuff.iteritems():
        if isinstance(c, (ColumnProperty, CompositeProperty)):
            for col in c.columns:
                if isinstance(col, Column) and \
                    col.table is None:
                    _undefer_column_name(key, col)
                    declared_columns.add(col)
        elif isinstance(c, Column):
            _undefer_column_name(key, c)
            declared_columns.add(c)
            # if the column is the same name as the key,
            # remove it from the explicit properties dict.
            # the normal rules for assigning column-based properties
            # will take over, including precedence of columns
            # in multi-column ColumnProperties.
            if key == c.key:
                del our_stuff[key]
    declared_columns = sorted(
        declared_columns, key=lambda c: c._creation_order)
    table = None

    if hasattr(cls, '__table_cls__'):
        table_cls = util.unbound_method_to_callable(cls.__table_cls__)
    else:
        table_cls = Table

    if '__table__' not in dict_:
        if tablename is not None:

            args, table_kw = (), {}
            if table_args:
                if isinstance(table_args, dict):
                    table_kw = table_args
                elif isinstance(table_args, tuple):
                    if isinstance(table_args[-1], dict):
                        args, table_kw = table_args[0:-1], table_args[-1]
                    else:
                        args = table_args

            autoload = dict_.get('__autoload__')
            if autoload:
                table_kw['autoload'] = True

            cls.__table__ = table = table_cls(
                tablename, cls.metadata,
                *(tuple(declared_columns) + tuple(args)),
                **table_kw)
    else:
        table = cls.__table__
        if declared_columns:
            for c in declared_columns:
                if not table.c.contains_column(c):
                    raise exc.ArgumentError(
                        "Can't add additional column %r when "
                        "specifying __table__" % c.key
                    )

    if hasattr(cls, '__mapper_cls__'):
        mapper_cls = util.unbound_method_to_callable(cls.__mapper_cls__)
    else:
        mapper_cls = mapper

    for c in cls.__bases__:
        if _declared_mapping_info(c) is not None:
            inherits = c
            break
    else:
        inherits = None

    if table is None and inherits is None:
        raise exc.InvalidRequestError(
            "Class %r does not have a __table__ or __tablename__ "
            "specified and does not inherit from an existing "
            "table-mapped class." % cls
            )
    elif inherits:
        inherited_mapper = _declared_mapping_info(inherits)
        inherited_table = inherited_mapper.local_table
        inherited_mapped_table = inherited_mapper.mapped_table

        if table is None:
            # single table inheritance.
            # ensure no table args
            if table_args:
                raise exc.ArgumentError(
                    "Can't place __table_args__ on an inherited class "
                    "with no table."
                    )
            # add any columns declared here to the inherited table.
            for c in declared_columns:
                if c.primary_key:
                    raise exc.ArgumentError(
                        "Can't place primary key columns on an inherited "
                        "class with no table."
                        )
                if c.name in inherited_table.c:
                    if inherited_table.c[c.name] is c:
                        continue
                    raise exc.ArgumentError(
                        "Column '%s' on class %s conflicts with "
                        "existing column '%s'" %
                        (c, cls, inherited_table.c[c.name])
                    )
                inherited_table.append_column(c)
                if inherited_mapped_table is not None and \
                    inherited_mapped_table is not inherited_table:
                    inherited_mapped_table._refresh_for_new_column(c)

    mt = _MapperConfig(mapper_cls,
                       cls, table,
                       inherits,
                       declared_columns,
                       column_copies,
                       our_stuff,
                       mapper_args_fn)
    if not hasattr(cls, '_sa_decl_prepare'):
        mt.map()


class _MapperConfig(object):
    configs = util.OrderedDict()
    mapped_table = None

    def __init__(self, mapper_cls,
                        cls,
                        table,
                        inherits,
                        declared_columns,
                        column_copies,
                        properties, mapper_args_fn):
        self.mapper_cls = mapper_cls
        self.cls = cls
        self.local_table = table
        self.inherits = inherits
        self.properties = properties
        self.mapper_args_fn = mapper_args_fn
        self.declared_columns = declared_columns
        self.column_copies = column_copies
        self.configs[cls] = self

    def _prepare_mapper_arguments(self):
        properties = self.properties
        if self.mapper_args_fn:
            mapper_args = self.mapper_args_fn()
        else:
            mapper_args = {}

        # make sure that column copies are used rather
        # than the original columns from any mixins
        for k in ('version_id_col', 'polymorphic_on',):
            if k in mapper_args:
                v = mapper_args[k]
                mapper_args[k] = self.column_copies.get(v, v)

        assert 'inherits' not in mapper_args, \
            "Can't specify 'inherits' explicitly with declarative mappings"

        if self.inherits:
            mapper_args['inherits'] = self.inherits

        if self.inherits and not mapper_args.get('concrete', False):
            # single or joined inheritance
            # exclude any cols on the inherited table which are
            # not mapped on the parent class, to avoid
            # mapping columns specific to sibling/nephew classes
            inherited_mapper = _declared_mapping_info(self.inherits)
            inherited_table = inherited_mapper.local_table

            if 'exclude_properties' not in mapper_args:
                mapper_args['exclude_properties'] = exclude_properties = \
                    set([c.key for c in inherited_table.c
                         if c not in inherited_mapper._columntoproperty])
                exclude_properties.difference_update(
                        [c.key for c in self.declared_columns])

            # look through columns in the current mapper that
            # are keyed to a propname different than the colname
            # (if names were the same, we'd have popped it out above,
            # in which case the mapper makes this combination).
            # See if the superclass has a similar column property.
            # If so, join them together.
            for k, col in properties.items():
                if not isinstance(col, expression.ColumnElement):
                    continue
                if k in inherited_mapper._props:
                    p = inherited_mapper._props[k]
                    if isinstance(p, ColumnProperty):
                        # note here we place the subclass column
                        # first.  See [ticket:1892] for background.
                        properties[k] = [col] + p.columns
        result_mapper_args = mapper_args.copy()
        result_mapper_args['properties'] = properties
        return result_mapper_args

    def map(self):
        self.configs.pop(self.cls, None)
        mapper_args = self._prepare_mapper_arguments()
        self.cls.__mapper__ = self.mapper_cls(
            self.cls,
            self.local_table,
            **mapper_args
        )


def _add_attribute(cls, key, value):
    """add an attribute to an existing declarative class.

    This runs through the logic to determine MapperProperty,
    adds it to the Mapper, adds a column to the mapped Table, etc.

    """
    if '__mapper__' in cls.__dict__:
        if isinstance(value, Column):
            _undefer_column_name(key, value)
            cls.__table__.append_column(value)
            cls.__mapper__.add_property(key, value)
        elif isinstance(value, ColumnProperty):
            for col in value.columns:
                if isinstance(col, Column) and col.table is None:
                    _undefer_column_name(key, col)
                    cls.__table__.append_column(col)
            cls.__mapper__.add_property(key, value)
        elif isinstance(value, MapperProperty):
            cls.__mapper__.add_property(
                key,
                clsregistry._deferred_relationship(cls, value)
            )
        else:
            type.__setattr__(cls, key, value)
    else:
        type.__setattr__(cls, key, value)


def _declarative_constructor(self, **kwargs):
    """A simple constructor that allows initialization from kwargs.

    Sets attributes on the constructed instance using the names and
    values in ``kwargs``.

    Only keys that are present as
    attributes of the instance's class are allowed. These could be,
    for example, any mapped columns or relationships.
    """
    cls_ = type(self)
    for k in kwargs:
        if not hasattr(cls_, k):
            raise TypeError(
                "%r is an invalid keyword argument for %s" %
                (k, cls_.__name__))
        setattr(self, k, kwargs[k])
_declarative_constructor.__name__ = '__init__'


def _undefer_column_name(key, column):
    if column.key is None:
        column.key = key
    if column.name is None:
        column.name = key