1
# ext/declarative/base.py
2
# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors <see AUTHORS file>
4
# This module is part of SQLAlchemy and is released under
5
# the MIT License: http://www.opensource.org/licenses/mit-license.php
6
"""Internal implementation for declarative."""
8
from ...schema import Table, Column
9
from ...orm import mapper, class_mapper
10
from ...orm.interfaces import MapperProperty
11
from ...orm.properties import ColumnProperty, CompositeProperty
12
from ...orm.util import _is_mapped_class
13
from ... import util, exc
14
from ...sql import expression
16
from . import clsregistry
19
def _declared_mapping_info(cls):
21
if cls in _MapperConfig.configs:
22
return _MapperConfig.configs[cls]
24
elif _is_mapped_class(cls):
25
return class_mapper(cls, configure=False)
30
def _as_declarative(cls, classname, dict_):
31
from .api import declared_attr
33
# dict_ will be a dictproxy, which we can't write to, and we need to!
37
potential_columns = {}
40
table_args = inherited_table_args = None
43
declarative_props = (declared_attr, util.classproperty)
45
for base in cls.__mro__:
46
_is_declarative_inherits = hasattr(base, '_decl_class_registry')
48
if '__declare_last__' in base.__dict__:
49
@event.listens_for(mapper, "after_configured")
51
cls.__declare_last__()
52
if '__abstract__' in base.__dict__:
54
(base in cls.__bases__ and not _is_declarative_inherits)
58
class_mapped = _declared_mapping_info(base) is not None
60
for name, obj in vars(base).items():
61
if name == '__mapper_args__':
62
if not mapper_args_fn and (
64
isinstance(obj, declarative_props)
66
# don't even invoke __mapper_args__ until
67
# after we've determined everything about the
69
mapper_args_fn = lambda: cls.__mapper_args__
70
elif name == '__tablename__':
71
if not tablename and (
73
isinstance(obj, declarative_props)
75
tablename = cls.__tablename__
76
elif name == '__table_args__':
77
if not table_args and (
79
isinstance(obj, declarative_props)
81
table_args = cls.__table_args__
82
if not isinstance(table_args, (tuple, dict, type(None))):
83
raise exc.ArgumentError(
84
"__table_args__ value must be a tuple, "
87
inherited_table_args = True
89
if isinstance(obj, declarative_props):
90
util.warn("Regular (i.e. not __special__) "
91
"attribute '%s.%s' uses @declared_attr, "
92
"but owning class %s is mapped - "
93
"not applying to subclass %s."
94
% (base.__name__, name, base, cls))
98
if isinstance(obj, Column):
99
if getattr(cls, name) is not obj:
100
# if column has been overridden
101
# (like by the InstrumentedAttribute of the
105
raise exc.InvalidRequestError(
106
"Columns with foreign keys to other columns "
107
"must be declared as @declared_attr callables "
108
"on declarative mixin classes. ")
109
if name not in dict_ and not (
110
'__table__' in dict_ and
111
(obj.name or name) in dict_['__table__'].c
112
) and name not in potential_columns:
113
potential_columns[name] = \
114
column_copies[obj] = \
116
column_copies[obj]._creation_order = \
118
elif isinstance(obj, MapperProperty):
119
raise exc.InvalidRequestError(
120
"Mapper properties (i.e. deferred,"
121
"column_property(), relationship(), etc.) must "
122
"be declared as @declared_attr callables "
123
"on declarative mixin classes.")
124
elif isinstance(obj, declarative_props):
125
dict_[name] = ret = \
126
column_copies[obj] = getattr(cls, name)
127
if isinstance(ret, (Column, MapperProperty)) and \
129
ret.doc = obj.__doc__
131
# apply inherited columns as we should
132
for k, v in potential_columns.items():
135
if inherited_table_args and not tablename:
138
clsregistry.add_class(classname, cls)
139
our_stuff = util.OrderedDict()
141
for k in list(dict_):
143
# TODO: improve this ? all dunders ?
144
if k in ('__table__', '__tablename__', '__mapper_args__'):
148
if isinstance(value, declarative_props):
149
value = getattr(cls, k)
151
if (isinstance(value, tuple) and len(value) == 1 and
152
isinstance(value[0], (Column, MapperProperty))):
153
util.warn("Ignoring declarative-like tuple value of attribute "
154
"%s: possibly a copy-and-paste error with a comma "
155
"left at the end of the line?" % k)
157
if not isinstance(value, (Column, MapperProperty)):
158
if not k.startswith('__'):
160
setattr(cls, k, value)
163
raise exc.InvalidRequestError(
164
"Attribute name 'metadata' is reserved "
165
"for the MetaData instance when using a "
166
"declarative base class."
168
prop = clsregistry._deferred_relationship(cls, value)
171
# set up attributes in the order they were created
172
our_stuff.sort(key=lambda key: our_stuff[key]._creation_order)
174
# extract columns from the class dict
175
declared_columns = set()
176
for key, c in our_stuff.iteritems():
177
if isinstance(c, (ColumnProperty, CompositeProperty)):
178
for col in c.columns:
179
if isinstance(col, Column) and \
181
_undefer_column_name(key, col)
182
declared_columns.add(col)
183
elif isinstance(c, Column):
184
_undefer_column_name(key, c)
185
declared_columns.add(c)
186
# if the column is the same name as the key,
187
# remove it from the explicit properties dict.
188
# the normal rules for assigning column-based properties
189
# will take over, including precedence of columns
190
# in multi-column ColumnProperties.
193
declared_columns = sorted(
194
declared_columns, key=lambda c: c._creation_order)
197
if hasattr(cls, '__table_cls__'):
198
table_cls = util.unbound_method_to_callable(cls.__table_cls__)
202
if '__table__' not in dict_:
203
if tablename is not None:
205
args, table_kw = (), {}
207
if isinstance(table_args, dict):
208
table_kw = table_args
209
elif isinstance(table_args, tuple):
210
if isinstance(table_args[-1], dict):
211
args, table_kw = table_args[0:-1], table_args[-1]
215
autoload = dict_.get('__autoload__')
217
table_kw['autoload'] = True
219
cls.__table__ = table = table_cls(
220
tablename, cls.metadata,
221
*(tuple(declared_columns) + tuple(args)),
224
table = cls.__table__
226
for c in declared_columns:
227
if not table.c.contains_column(c):
228
raise exc.ArgumentError(
229
"Can't add additional column %r when "
230
"specifying __table__" % c.key
233
if hasattr(cls, '__mapper_cls__'):
234
mapper_cls = util.unbound_method_to_callable(cls.__mapper_cls__)
238
for c in cls.__bases__:
239
if _declared_mapping_info(c) is not None:
245
if table is None and inherits is None:
246
raise exc.InvalidRequestError(
247
"Class %r does not have a __table__ or __tablename__ "
248
"specified and does not inherit from an existing "
249
"table-mapped class." % cls
252
inherited_mapper = _declared_mapping_info(inherits)
253
inherited_table = inherited_mapper.local_table
254
inherited_mapped_table = inherited_mapper.mapped_table
257
# single table inheritance.
258
# ensure no table args
260
raise exc.ArgumentError(
261
"Can't place __table_args__ on an inherited class "
264
# add any columns declared here to the inherited table.
265
for c in declared_columns:
267
raise exc.ArgumentError(
268
"Can't place primary key columns on an inherited "
269
"class with no table."
271
if c.name in inherited_table.c:
272
if inherited_table.c[c.name] is c:
274
raise exc.ArgumentError(
275
"Column '%s' on class %s conflicts with "
276
"existing column '%s'" %
277
(c, cls, inherited_table.c[c.name])
279
inherited_table.append_column(c)
280
if inherited_mapped_table is not None and \
281
inherited_mapped_table is not inherited_table:
282
inherited_mapped_table._refresh_for_new_column(c)
284
mt = _MapperConfig(mapper_cls,
291
if not hasattr(cls, '_sa_decl_prepare'):
295
class _MapperConfig(object):
296
configs = util.OrderedDict()
299
def __init__(self, mapper_cls,
305
properties, mapper_args_fn):
306
self.mapper_cls = mapper_cls
308
self.local_table = table
309
self.inherits = inherits
310
self.properties = properties
311
self.mapper_args_fn = mapper_args_fn
312
self.declared_columns = declared_columns
313
self.column_copies = column_copies
314
self.configs[cls] = self
316
def _prepare_mapper_arguments(self):
317
properties = self.properties
318
if self.mapper_args_fn:
319
mapper_args = self.mapper_args_fn()
323
# make sure that column copies are used rather
324
# than the original columns from any mixins
325
for k in ('version_id_col', 'polymorphic_on',):
328
mapper_args[k] = self.column_copies.get(v, v)
330
assert 'inherits' not in mapper_args, \
331
"Can't specify 'inherits' explicitly with declarative mappings"
334
mapper_args['inherits'] = self.inherits
336
if self.inherits and not mapper_args.get('concrete', False):
337
# single or joined inheritance
338
# exclude any cols on the inherited table which are
339
# not mapped on the parent class, to avoid
340
# mapping columns specific to sibling/nephew classes
341
inherited_mapper = _declared_mapping_info(self.inherits)
342
inherited_table = inherited_mapper.local_table
344
if 'exclude_properties' not in mapper_args:
345
mapper_args['exclude_properties'] = exclude_properties = \
346
set([c.key for c in inherited_table.c
347
if c not in inherited_mapper._columntoproperty])
348
exclude_properties.difference_update(
349
[c.key for c in self.declared_columns])
351
# look through columns in the current mapper that
352
# are keyed to a propname different than the colname
353
# (if names were the same, we'd have popped it out above,
354
# in which case the mapper makes this combination).
355
# See if the superclass has a similar column property.
356
# If so, join them together.
357
for k, col in properties.items():
358
if not isinstance(col, expression.ColumnElement):
360
if k in inherited_mapper._props:
361
p = inherited_mapper._props[k]
362
if isinstance(p, ColumnProperty):
363
# note here we place the subclass column
364
# first. See [ticket:1892] for background.
365
properties[k] = [col] + p.columns
366
result_mapper_args = mapper_args.copy()
367
result_mapper_args['properties'] = properties
368
return result_mapper_args
371
self.configs.pop(self.cls, None)
372
mapper_args = self._prepare_mapper_arguments()
373
self.cls.__mapper__ = self.mapper_cls(
380
def _add_attribute(cls, key, value):
381
"""add an attribute to an existing declarative class.
383
This runs through the logic to determine MapperProperty,
384
adds it to the Mapper, adds a column to the mapped Table, etc.
387
if '__mapper__' in cls.__dict__:
388
if isinstance(value, Column):
389
_undefer_column_name(key, value)
390
cls.__table__.append_column(value)
391
cls.__mapper__.add_property(key, value)
392
elif isinstance(value, ColumnProperty):
393
for col in value.columns:
394
if isinstance(col, Column) and col.table is None:
395
_undefer_column_name(key, col)
396
cls.__table__.append_column(col)
397
cls.__mapper__.add_property(key, value)
398
elif isinstance(value, MapperProperty):
399
cls.__mapper__.add_property(
401
clsregistry._deferred_relationship(cls, value)
404
type.__setattr__(cls, key, value)
406
type.__setattr__(cls, key, value)
409
def _declarative_constructor(self, **kwargs):
410
"""A simple constructor that allows initialization from kwargs.
412
Sets attributes on the constructed instance using the names and
413
values in ``kwargs``.
415
Only keys that are present as
416
attributes of the instance's class are allowed. These could be,
417
for example, any mapped columns or relationships.
421
if not hasattr(cls_, k):
423
"%r is an invalid keyword argument for %s" %
425
setattr(self, k, kwargs[k])
426
_declarative_constructor.__name__ = '__init__'
429
def _undefer_column_name(key, column):
430
if column.key is None:
432
if column.name is None: