1# ext/declarative/base.py
2# Copyright (C) 2005-2018 the SQLAlchemy authors and contributors
3# <see AUTHORS file>
4#
5# This module is part of SQLAlchemy and is released under
6# the MIT License: http://www.opensource.org/licenses/mit-license.php
7"""Internal implementation for declarative."""
8
9from ...schema import Table, Column
10from ...orm import mapper, class_mapper, synonym
11from ...orm.interfaces import MapperProperty
12from ...orm.properties import ColumnProperty, CompositeProperty
13from ...orm.attributes import QueryableAttribute
14from ...orm.base import _is_mapped_class
15from ... import util, exc
16from ...util import topological
17from ...sql import expression
18from ... import event
19from . import clsregistry
20import collections
21import weakref
22from sqlalchemy.orm import instrumentation
23
24declared_attr = declarative_props = None
25
26
27def _declared_mapping_info(cls):
28    # deferred mapping
29    if _DeferredMapperConfig.has_cls(cls):
30        return _DeferredMapperConfig.config_for_cls(cls)
31    # regular mapping
32    elif _is_mapped_class(cls):
33        return class_mapper(cls, configure=False)
34    else:
35        return None
36
37
38def _resolve_for_abstract(cls):
39    if cls is object:
40        return None
41
42    if _get_immediate_cls_attr(cls, '__abstract__', strict=True):
43        for sup in cls.__bases__:
44            sup = _resolve_for_abstract(sup)
45            if sup is not None:
46                return sup
47        else:
48            return None
49    else:
50        return cls
51
52
53def _get_immediate_cls_attr(cls, attrname, strict=False):
54    """return an attribute of the class that is either present directly
55    on the class, e.g. not on a superclass, or is from a superclass but
56    this superclass is a mixin, that is, not a descendant of
57    the declarative base.
58
59    This is used to detect attributes that indicate something about
60    a mapped class independently from any mapped classes that it may
61    inherit from.
62
63    """
64    if not issubclass(cls, object):
65        return None
66
67    for base in cls.__mro__:
68        _is_declarative_inherits = hasattr(base, '_decl_class_registry')
69        if attrname in base.__dict__ and (
70            base is cls or
71            ((base in cls.__bases__ if strict else True)
72                and not _is_declarative_inherits)
73        ):
74            return getattr(base, attrname)
75    else:
76        return None
77
78
79def _as_declarative(cls, classname, dict_):
80    global declared_attr, declarative_props
81    if declared_attr is None:
82        from .api import declared_attr
83        declarative_props = (declared_attr, util.classproperty)
84
85    if _get_immediate_cls_attr(cls, '__abstract__', strict=True):
86        return
87
88    _MapperConfig.setup_mapping(cls, classname, dict_)
89
90
91class _MapperConfig(object):
92
93    @classmethod
94    def setup_mapping(cls, cls_, classname, dict_):
95        defer_map = _get_immediate_cls_attr(
96            cls_, '_sa_decl_prepare_nocascade', strict=True) or \
97            hasattr(cls_, '_sa_decl_prepare')
98
99        if defer_map:
100            cfg_cls = _DeferredMapperConfig
101        else:
102            cfg_cls = _MapperConfig
103        cfg_cls(cls_, classname, dict_)
104
105    def __init__(self, cls_, classname, dict_):
106
107        self.cls = cls_
108
109        # dict_ will be a dictproxy, which we can't write to, and we need to!
110        self.dict_ = dict(dict_)
111        self.classname = classname
112        self.mapped_table = None
113        self.properties = util.OrderedDict()
114        self.declared_columns = set()
115        self.column_copies = {}
116        self._setup_declared_events()
117
118        # temporary registry.  While early 1.0 versions
119        # set up the ClassManager here, by API contract
120        # we can't do that until there's a mapper.
121        self.cls._sa_declared_attr_reg = {}
122
123        self._scan_attributes()
124
125        clsregistry.add_class(self.classname, self.cls)
126
127        self._extract_mappable_attributes()
128
129        self._extract_declared_columns()
130
131        self._setup_table()
132
133        self._setup_inheritance()
134
135        self._early_mapping()
136
137    def _early_mapping(self):
138        self.map()
139
140    def _setup_declared_events(self):
141        if _get_immediate_cls_attr(self.cls, '__declare_last__'):
142            @event.listens_for(mapper, "after_configured")
143            def after_configured():
144                self.cls.__declare_last__()
145
146        if _get_immediate_cls_attr(self.cls, '__declare_first__'):
147            @event.listens_for(mapper, "before_configured")
148            def before_configured():
149                self.cls.__declare_first__()
150
151    def _scan_attributes(self):
152        cls = self.cls
153        dict_ = self.dict_
154        column_copies = self.column_copies
155        mapper_args_fn = None
156        table_args = inherited_table_args = None
157        tablename = None
158
159        for base in cls.__mro__:
160            class_mapped = base is not cls and \
161                _declared_mapping_info(base) is not None and \
162                not _get_immediate_cls_attr(
163                    base, '_sa_decl_prepare_nocascade', strict=True)
164
165            if not class_mapped and base is not cls:
166                self._produce_column_copies(base)
167
168            for name, obj in vars(base).items():
169                if name == '__mapper_args__':
170                    if not mapper_args_fn and (
171                        not class_mapped or
172                        isinstance(obj, declarative_props)
173                    ):
174                        # don't even invoke __mapper_args__ until
175                        # after we've determined everything about the
176                        # mapped table.
177                        # make a copy of it so a class-level dictionary
178                        # is not overwritten when we update column-based
179                        # arguments.
180                        mapper_args_fn = lambda: dict(cls.__mapper_args__)
181                elif name == '__tablename__':
182                    if not tablename and (
183                        not class_mapped or
184                        isinstance(obj, declarative_props)
185                    ):
186                        tablename = cls.__tablename__
187                elif name == '__table_args__':
188                    if not table_args and (
189                        not class_mapped or
190                        isinstance(obj, declarative_props)
191                    ):
192                        table_args = cls.__table_args__
193                        if not isinstance(
194                                table_args, (tuple, dict, type(None))):
195                            raise exc.ArgumentError(
196                                "__table_args__ value must be a tuple, "
197                                "dict, or None")
198                        if base is not cls:
199                            inherited_table_args = True
200                elif class_mapped:
201                    if isinstance(obj, declarative_props):
202                        util.warn("Regular (i.e. not __special__) "
203                                  "attribute '%s.%s' uses @declared_attr, "
204                                  "but owning class %s is mapped - "
205                                  "not applying to subclass %s."
206                                  % (base.__name__, name, base, cls))
207                    continue
208                elif base is not cls:
209                    # we're a mixin, abstract base, or something that is
210                    # acting like that for now.
211                    if isinstance(obj, Column):
212                        # already copied columns to the mapped class.
213                        continue
214                    elif isinstance(obj, MapperProperty):
215                        raise exc.InvalidRequestError(
216                            "Mapper properties (i.e. deferred,"
217                            "column_property(), relationship(), etc.) must "
218                            "be declared as @declared_attr callables "
219                            "on declarative mixin classes.")
220                    elif isinstance(obj, declarative_props):
221                        oldclassprop = isinstance(obj, util.classproperty)
222                        if not oldclassprop and obj._cascading:
223                            dict_[name] = column_copies[obj] = \
224                                ret = obj.__get__(obj, cls)
225                            setattr(cls, name, ret)
226                        else:
227                            if oldclassprop:
228                                util.warn_deprecated(
229                                    "Use of sqlalchemy.util.classproperty on "
230                                    "declarative classes is deprecated.")
231                            dict_[name] = column_copies[obj] = \
232                                ret = getattr(cls, name)
233                        if isinstance(ret, (Column, MapperProperty)) and \
234                                ret.doc is None:
235                            ret.doc = obj.__doc__
236
237        if inherited_table_args and not tablename:
238            table_args = None
239
240        self.table_args = table_args
241        self.tablename = tablename
242        self.mapper_args_fn = mapper_args_fn
243
244    def _produce_column_copies(self, base):
245        cls = self.cls
246        dict_ = self.dict_
247        column_copies = self.column_copies
248        # copy mixin columns to the mapped class
249        for name, obj in vars(base).items():
250            if isinstance(obj, Column):
251                if getattr(cls, name) is not obj:
252                    # if column has been overridden
253                    # (like by the InstrumentedAttribute of the
254                    # superclass), skip
255                    continue
256                elif obj.foreign_keys:
257                    raise exc.InvalidRequestError(
258                        "Columns with foreign keys to other columns "
259                        "must be declared as @declared_attr callables "
260                        "on declarative mixin classes. ")
261                elif name not in dict_ and not (
262                        '__table__' in dict_ and
263                        (obj.name or name) in dict_['__table__'].c
264                ):
265                    column_copies[obj] = copy_ = obj.copy()
266                    copy_._creation_order = obj._creation_order
267                    setattr(cls, name, copy_)
268                    dict_[name] = copy_
269
270    def _extract_mappable_attributes(self):
271        cls = self.cls
272        dict_ = self.dict_
273
274        our_stuff = self.properties
275
276        for k in list(dict_):
277
278            if k in ('__table__', '__tablename__', '__mapper_args__'):
279                continue
280
281            value = dict_[k]
282            if isinstance(value, declarative_props):
283                value = getattr(cls, k)
284
285            elif isinstance(value, QueryableAttribute) and \
286                    value.class_ is not cls and \
287                    value.key != k:
288                # detect a QueryableAttribute that's already mapped being
289                # assigned elsewhere in userland, turn into a synonym()
290                value = synonym(value.key)
291                setattr(cls, k, value)
292
293            if (isinstance(value, tuple) and len(value) == 1 and
294                    isinstance(value[0], (Column, MapperProperty))):
295                util.warn("Ignoring declarative-like tuple value of attribute "
296                          "%s: possibly a copy-and-paste error with a comma "
297                          "left at the end of the line?" % k)
298                continue
299            elif not isinstance(value, (Column, MapperProperty)):
300                # using @declared_attr for some object that
301                # isn't Column/MapperProperty; remove from the dict_
302                # and place the evaluated value onto the class.
303                if not k.startswith('__'):
304                    dict_.pop(k)
305                    setattr(cls, k, value)
306                continue
307            # we expect to see the name 'metadata' in some valid cases;
308            # however at this point we see it's assigned to something trying
309            # to be mapped, so raise for that.
310            elif k == 'metadata':
311                raise exc.InvalidRequestError(
312                    "Attribute name 'metadata' is reserved "
313                    "for the MetaData instance when using a "
314                    "declarative base class."
315                )
316            prop = clsregistry._deferred_relationship(cls, value)
317            our_stuff[k] = prop
318
319    def _extract_declared_columns(self):
320        our_stuff = self.properties
321
322        # set up attributes in the order they were created
323        our_stuff.sort(key=lambda key: our_stuff[key]._creation_order)
324
325        # extract columns from the class dict
326        declared_columns = self.declared_columns
327        name_to_prop_key = collections.defaultdict(set)
328        for key, c in list(our_stuff.items()):
329            if isinstance(c, (ColumnProperty, CompositeProperty)):
330                for col in c.columns:
331                    if isinstance(col, Column) and \
332                            col.table is None:
333                        _undefer_column_name(key, col)
334                        if not isinstance(c, CompositeProperty):
335                            name_to_prop_key[col.name].add(key)
336                        declared_columns.add(col)
337            elif isinstance(c, Column):
338                _undefer_column_name(key, c)
339                name_to_prop_key[c.name].add(key)
340                declared_columns.add(c)
341                # if the column is the same name as the key,
342                # remove it from the explicit properties dict.
343                # the normal rules for assigning column-based properties
344                # will take over, including precedence of columns
345                # in multi-column ColumnProperties.
346                if key == c.key:
347                    del our_stuff[key]
348
349        for name, keys in name_to_prop_key.items():
350            if len(keys) > 1:
351                util.warn(
352                    "On class %r, Column object %r named "
353                    "directly multiple times, "
354                    "only one will be used: %s. "
355                    "Consider using orm.synonym instead" %
356                    (self.classname, name, (", ".join(sorted(keys))))
357                )
358
359    def _setup_table(self):
360        cls = self.cls
361        tablename = self.tablename
362        table_args = self.table_args
363        dict_ = self.dict_
364        declared_columns = self.declared_columns
365
366        declared_columns = self.declared_columns = sorted(
367            declared_columns, key=lambda c: c._creation_order)
368        table = None
369
370        if hasattr(cls, '__table_cls__'):
371            table_cls = util.unbound_method_to_callable(cls.__table_cls__)
372        else:
373            table_cls = Table
374
375        if '__table__' not in dict_:
376            if tablename is not None:
377
378                args, table_kw = (), {}
379                if table_args:
380                    if isinstance(table_args, dict):
381                        table_kw = table_args
382                    elif isinstance(table_args, tuple):
383                        if isinstance(table_args[-1], dict):
384                            args, table_kw = table_args[0:-1], table_args[-1]
385                        else:
386                            args = table_args
387
388                autoload = dict_.get('__autoload__')
389                if autoload:
390                    table_kw['autoload'] = True
391
392                cls.__table__ = table = table_cls(
393                    tablename, cls.metadata,
394                    *(tuple(declared_columns) + tuple(args)),
395                    **table_kw)
396        else:
397            table = cls.__table__
398            if declared_columns:
399                for c in declared_columns:
400                    if not table.c.contains_column(c):
401                        raise exc.ArgumentError(
402                            "Can't add additional column %r when "
403                            "specifying __table__" % c.key
404                        )
405        self.local_table = table
406
407    def _setup_inheritance(self):
408        table = self.local_table
409        cls = self.cls
410        table_args = self.table_args
411        declared_columns = self.declared_columns
412        for c in cls.__bases__:
413            c = _resolve_for_abstract(c)
414            if c is None:
415                continue
416            if _declared_mapping_info(c) is not None and \
417                    not _get_immediate_cls_attr(
418                        c, '_sa_decl_prepare_nocascade', strict=True):
419                self.inherits = c
420                break
421        else:
422            self.inherits = None
423
424        if table is None and self.inherits is None and \
425                not _get_immediate_cls_attr(cls, '__no_table__'):
426
427            raise exc.InvalidRequestError(
428                "Class %r does not have a __table__ or __tablename__ "
429                "specified and does not inherit from an existing "
430                "table-mapped class." % cls
431            )
432        elif self.inherits:
433            inherited_mapper = _declared_mapping_info(self.inherits)
434            inherited_table = inherited_mapper.local_table
435            inherited_mapped_table = inherited_mapper.mapped_table
436
437            if table is None:
438                # single table inheritance.
439                # ensure no table args
440                if table_args:
441                    raise exc.ArgumentError(
442                        "Can't place __table_args__ on an inherited class "
443                        "with no table."
444                    )
445                # add any columns declared here to the inherited table.
446                for c in declared_columns:
447                    if c.primary_key:
448                        raise exc.ArgumentError(
449                            "Can't place primary key columns on an inherited "
450                            "class with no table."
451                        )
452                    if c.name in inherited_table.c:
453                        if inherited_table.c[c.name] is c:
454                            continue
455                        raise exc.ArgumentError(
456                            "Column '%s' on class %s conflicts with "
457                            "existing column '%s'" %
458                            (c, cls, inherited_table.c[c.name])
459                        )
460                    inherited_table.append_column(c)
461                    if inherited_mapped_table is not None and \
462                            inherited_mapped_table is not inherited_table:
463                        inherited_mapped_table._refresh_for_new_column(c)
464
465    def _prepare_mapper_arguments(self):
466        properties = self.properties
467        if self.mapper_args_fn:
468            mapper_args = self.mapper_args_fn()
469        else:
470            mapper_args = {}
471
472        # make sure that column copies are used rather
473        # than the original columns from any mixins
474        for k in ('version_id_col', 'polymorphic_on',):
475            if k in mapper_args:
476                v = mapper_args[k]
477                mapper_args[k] = self.column_copies.get(v, v)
478
479        assert 'inherits' not in mapper_args, \
480            "Can't specify 'inherits' explicitly with declarative mappings"
481
482        if self.inherits:
483            mapper_args['inherits'] = self.inherits
484
485        if self.inherits and not mapper_args.get('concrete', False):
486            # single or joined inheritance
487            # exclude any cols on the inherited table which are
488            # not mapped on the parent class, to avoid
489            # mapping columns specific to sibling/nephew classes
490            inherited_mapper = _declared_mapping_info(self.inherits)
491            inherited_table = inherited_mapper.local_table
492
493            if 'exclude_properties' not in mapper_args:
494                mapper_args['exclude_properties'] = exclude_properties = \
495                    set(
496                        [c.key for c in inherited_table.c
497                         if c not in inherited_mapper._columntoproperty]
498                ).union(
499                    inherited_mapper.exclude_properties or ()
500                )
501                exclude_properties.difference_update(
502                    [c.key for c in self.declared_columns])
503
504            # look through columns in the current mapper that
505            # are keyed to a propname different than the colname
506            # (if names were the same, we'd have popped it out above,
507            # in which case the mapper makes this combination).
508            # See if the superclass has a similar column property.
509            # If so, join them together.
510            for k, col in list(properties.items()):
511                if not isinstance(col, expression.ColumnElement):
512                    continue
513                if k in inherited_mapper._props:
514                    p = inherited_mapper._props[k]
515                    if isinstance(p, ColumnProperty):
516                        # note here we place the subclass column
517                        # first.  See [ticket:1892] for background.
518                        properties[k] = [col] + p.columns
519        result_mapper_args = mapper_args.copy()
520        result_mapper_args['properties'] = properties
521        self.mapper_args = result_mapper_args
522
523    def map(self):
524        self._prepare_mapper_arguments()
525        if hasattr(self.cls, '__mapper_cls__'):
526            mapper_cls = util.unbound_method_to_callable(
527                self.cls.__mapper_cls__)
528        else:
529            mapper_cls = mapper
530
531        self.cls.__mapper__ = mp_ = mapper_cls(
532            self.cls,
533            self.local_table,
534            **self.mapper_args
535        )
536        del self.cls._sa_declared_attr_reg
537        return mp_
538
539
540class _DeferredMapperConfig(_MapperConfig):
541    _configs = util.OrderedDict()
542
543    def _early_mapping(self):
544        pass
545
546    @property
547    def cls(self):
548        return self._cls()
549
550    @cls.setter
551    def cls(self, class_):
552        self._cls = weakref.ref(class_, self._remove_config_cls)
553        self._configs[self._cls] = self
554
555    @classmethod
556    def _remove_config_cls(cls, ref):
557        cls._configs.pop(ref, None)
558
559    @classmethod
560    def has_cls(cls, class_):
561        # 2.6 fails on weakref if class_ is an old style class
562        return isinstance(class_, type) and \
563            weakref.ref(class_) in cls._configs
564
565    @classmethod
566    def config_for_cls(cls, class_):
567        return cls._configs[weakref.ref(class_)]
568
569    @classmethod
570    def classes_for_base(cls, base_cls, sort=True):
571        classes_for_base = [
572            m for m, cls_ in
573            [(m, m.cls) for m in cls._configs.values()]
574            if cls_ is not None and issubclass(cls_, base_cls)
575        ]
576
577        if not sort:
578            return classes_for_base
579
580        all_m_by_cls = dict(
581            (m.cls, m)
582            for m in classes_for_base
583        )
584
585        tuples = []
586        for m_cls in all_m_by_cls:
587            tuples.extend(
588                (all_m_by_cls[base_cls], all_m_by_cls[m_cls])
589                for base_cls in m_cls.__bases__
590                if base_cls in all_m_by_cls
591            )
592        return list(
593            topological.sort(
594                tuples,
595                classes_for_base
596            )
597        )
598
599    def map(self):
600        self._configs.pop(self._cls, None)
601        return super(_DeferredMapperConfig, self).map()
602
603
604def _add_attribute(cls, key, value):
605    """add an attribute to an existing declarative class.
606
607    This runs through the logic to determine MapperProperty,
608    adds it to the Mapper, adds a column to the mapped Table, etc.
609
610    """
611
612    if '__mapper__' in cls.__dict__:
613        if isinstance(value, Column):
614            _undefer_column_name(key, value)
615            cls.__table__.append_column(value)
616            cls.__mapper__.add_property(key, value)
617        elif isinstance(value, ColumnProperty):
618            for col in value.columns:
619                if isinstance(col, Column) and col.table is None:
620                    _undefer_column_name(key, col)
621                    cls.__table__.append_column(col)
622            cls.__mapper__.add_property(key, value)
623        elif isinstance(value, MapperProperty):
624            cls.__mapper__.add_property(
625                key,
626                clsregistry._deferred_relationship(cls, value)
627            )
628        elif isinstance(value, QueryableAttribute) and value.key != key:
629            # detect a QueryableAttribute that's already mapped being
630            # assigned elsewhere in userland, turn into a synonym()
631            value = synonym(value.key)
632            cls.__mapper__.add_property(
633                key,
634                clsregistry._deferred_relationship(cls, value)
635            )
636        else:
637            type.__setattr__(cls, key, value)
638    else:
639        type.__setattr__(cls, key, value)
640
641
642def _declarative_constructor(self, **kwargs):
643    """A simple constructor that allows initialization from kwargs.
644
645    Sets attributes on the constructed instance using the names and
646    values in ``kwargs``.
647
648    Only keys that are present as
649    attributes of the instance's class are allowed. These could be,
650    for example, any mapped columns or relationships.
651    """
652    cls_ = type(self)
653    for k in kwargs:
654        if not hasattr(cls_, k):
655            raise TypeError(
656                "%r is an invalid keyword argument for %s" %
657                (k, cls_.__name__))
658        setattr(self, k, kwargs[k])
659_declarative_constructor.__name__ = '__init__'
660
661
662def _undefer_column_name(key, column):
663    if column.key is None:
664        column.key = key
665    if column.name is None:
666        column.name = key
667