summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy/ext/declarative/base.py
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2012-08-05 15:14:51 -0400
committerMike Bayer <mike_mp@zzzcomputing.com>2012-08-05 15:14:51 -0400
commit6bd46945ccd585c494eb7550a0dfea22f17727c0 (patch)
tree4412d12bb42af58a1c30f7f961f77acb2fa35386 /lib/sqlalchemy/ext/declarative/base.py
parenta4f2db890322a225e6c9754b711f5c16d04f377c (diff)
downloadsqlalchemy-6bd46945ccd585c494eb7550a0dfea22f17727c0.tar.gz
- reorganization of declarative such that file sizes are managable again.
the vast majority of file lines are spent on documentation, which moves into package __init__. The core declarative idea lives in base and is back down to its originally low size of under 500 lines. The various helpers and such move into api.py, and the full span of string lookup moves into a new module clsregistry. the rest of declarative only refers to two functions in clsregistry in three places inside of base. - [feature] Declarative now maintains a registry of classes by string name as well as by full module-qualified name. Multiple classes with the same name can now be looked up based on a module-qualified string within relationship(). Simple class name lookups where more than one class shares the same name now raises an informative error message. [ticket:2338] - lots of tests to ensure the new weak referencing memory management is maintained by the new class registry system. this ticket was served very well by waiting to do #2526 first, else this would have needed to be rewritten anyway.
Diffstat (limited to 'lib/sqlalchemy/ext/declarative/base.py')
-rw-r--r--lib/sqlalchemy/ext/declarative/base.py418
1 files changed, 418 insertions, 0 deletions
diff --git a/lib/sqlalchemy/ext/declarative/base.py b/lib/sqlalchemy/ext/declarative/base.py
new file mode 100644
index 000000000..100a68678
--- /dev/null
+++ b/lib/sqlalchemy/ext/declarative/base.py
@@ -0,0 +1,418 @@
+# ext/declarative/base.py
+# Copyright (C) 2005-2012 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
+ parent_columns = ()
+
+ 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
+ if class_mapped:
+ parent_columns = base.__table__.c.keys()
+
+ 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 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():
+ if tablename or (v.name or k) not in parent_columns:
+ 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 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)):
+ 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
+
+ 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:
+ 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)
+
+ 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()
+
+ 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