summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES20
-rw-r--r--doc/build/conf.py2
-rw-r--r--doc/build/intro.rst2
-rw-r--r--doc/build/ormtutorial.rst27
-rw-r--r--doc/build/reference/ext/declarative.rst14
-rw-r--r--lib/sqlalchemy/ext/declarative.py448
-rw-r--r--test/ext/test_declarative.py175
7 files changed, 515 insertions, 173 deletions
diff --git a/CHANGES b/CHANGES
index 82a15639e..5e0d3662d 100644
--- a/CHANGES
+++ b/CHANGES
@@ -201,11 +201,21 @@ CHANGES
in theory make it easier for custom metaclasses to modify
the state passed into _as_declarative.
- - the __mapper_args__ dict is copied when propagating to a subclass,
- and is taken straight off the class __dict__ to avoid any
- propagation from the parent. mapper inheritance already
- propagates the things you want from the parent mapper.
- [ticket:1393]
+ - declarative now accepts mixin classes directly, as a means
+ to provide common functional and column-based elements on
+ all subclasses, as well as a means to propagate a fixed
+ set of __table_args__ or __mapper_args__ to subclasses.
+ For custom combinations of __table_args__/__mapper_args__ from
+ an inherited mixin to local, descriptors can now be used.
+ New details are all up in the Declarative documentation.
+ Thanks to Chris Withers for putting up with my strife
+ on this. [ticket:1707]
+
+ - the __mapper_args__ dict is copied when propagating to a subclass,
+ and is taken straight off the class __dict__ to avoid any
+ propagation from the parent. mapper inheritance already
+ propagates the things you want from the parent mapper.
+ [ticket:1393]
- mysql
- Fixed reflection bug whereby when COLLATE was present,
diff --git a/doc/build/conf.py b/doc/build/conf.py
index 8b7650e14..bee23d308 100644
--- a/doc/build/conf.py
+++ b/doc/build/conf.py
@@ -67,7 +67,7 @@ release = sqlalchemy.__version__
#today_fmt = '%B %d, %Y'
# List of documents that shouldn't be included in the build.
-unused_docs = ['output.txt']
+unused_docs = ['output.txt','copyright']
# List of directories, relative to source directory, that shouldn't be searched
# for source files.
diff --git a/doc/build/intro.rst b/doc/build/intro.rst
index d4fdfccdf..dd4a97507 100644
--- a/doc/build/intro.rst
+++ b/doc/build/intro.rst
@@ -52,7 +52,7 @@ Installing SQLAlchemy from scratch is most easily achieved with `setuptools <htt
This command will download the latest version of SQLAlchemy from the `Python Cheese Shop <http://pypi.python.org/pypi/SQLAlchemy>`_ and install it to your system.
-* `setuptools <http://peak.telecommunity.com/DevCenter/setuptools>`_
+* setuptools_
* `install setuptools <http://peak.telecommunity.com/DevCenter/EasyInstall#installation-instructions>`_
* `pypi <http://pypi.python.org/pypi/SQLAlchemy>`_
diff --git a/doc/build/ormtutorial.rst b/doc/build/ormtutorial.rst
index 4b7eaffeb..e93364387 100644
--- a/doc/build/ormtutorial.rst
+++ b/doc/build/ormtutorial.rst
@@ -37,7 +37,7 @@ Next we want to tell SQLAlchemy about our tables. We will start with just a sin
... Column('password', String)
... )
-All about how to define :class:`~sqlalchemy.schema.Table` objects, as well as how to load their definition from an existing database (known as **reflection**), is described in :ref:`metadata_toplevel`.
+:ref:`metadata_toplevel` covers all about how to define :class:`~sqlalchemy.schema.Table` objects, as well as how to load their definition from an existing database (known as **reflection**).
Next, we can issue CREATE TABLE statements derived from our table metadata, by calling ``create_all()`` and passing it the ``engine`` instance which points to our database. This will check for the presence of a table first before creating, so it's safe to call multiple times:
@@ -124,7 +124,15 @@ Since we have not yet told SQLAlchemy to persist ``Ed Jones`` within the databas
Creating Table, Class and Mapper All at Once Declaratively
===========================================================
-The preceding approach to configuration involving a :class:`~sqlalchemy.schema.Table`, user-defined class, and ``mapper()`` call illustrate classical SQLAlchemy usage, which values the highest separation of concerns possible. A large number of applications don't require this degree of separation, and for those SQLAlchemy offers an alternate "shorthand" configurational style called **declarative**. For many applications, this is the only style of configuration needed. Our above example using this style is as follows::
+The preceding approach to configuration involved a
+:class:`~sqlalchemy.schema.Table`, a user-defined class, and
+a call to``mapper()``. This illustrates classical SQLAlchemy usage, which values
+the highest separation of concerns possible.
+A large number of applications don't require this degree of
+separation, and for those SQLAlchemy offers an alternate "shorthand"
+configurational style called :mod:`~sqlalchemy.ext.declarative`.
+For many applications, this is the only style of configuration needed.
+Our above example using this style is as follows::
>>> from sqlalchemy.ext.declarative import declarative_base
@@ -145,16 +153,25 @@ The preceding approach to configuration involving a :class:`~sqlalchemy.schema.T
... def __repr__(self):
... return "<User('%s','%s', '%s')>" % (self.name, self.fullname, self.password)
-Above, the ``declarative_base()`` function defines a new class which we name ``Base``, from which all of our ORM-enabled classes will derive. Note that we define :class:`~sqlalchemy.schema.Column` objects with no "name" field, since it's inferred from the given attribute name.
+Above, the ``declarative_base()`` function defines a new class which
+we name ``Base``, from which all of our ORM-enabled classes will
+derive. Note that we define :class:`~sqlalchemy.schema.Column`
+objects with no "name" field, since it's inferred from the given
+attribute name.
-The underlying :class:`~sqlalchemy.schema.Table` object created by our ``declarative_base()`` version of ``User`` is accessible via the ``__table__`` attribute::
+The underlying :class:`~sqlalchemy.schema.Table` object created by our
+``declarative_base()`` version of ``User`` is accessible via the
+``__table__`` attribute::
>>> users_table = User.__table__
-and the owning :class:`~sqlalchemy.schema.MetaData` object is available as well::
+The owning :class:`~sqlalchemy.schema.MetaData` object is available as well::
>>> metadata = Base.metadata
+Full documentation for :mod:`~sqlalchemy.ext.declarative` can be found
+in the :doc:`reference/index` section for :doc:`reference/ext/declarative`.
+
Yet another "declarative" method is available for SQLAlchemy as a third party library called `Elixir <http://elixir.ematia.de/>`_. This is a full-featured configurational product which also includes many higher level mapping configurations built in. Like declarative, once classes and mappings are defined, ORM usage is the same as with a classical SQLAlchemy configuration.
Creating a Session
diff --git a/doc/build/reference/ext/declarative.rst b/doc/build/reference/ext/declarative.rst
index 81c713df7..5762d0378 100644
--- a/doc/build/reference/ext/declarative.rst
+++ b/doc/build/reference/ext/declarative.rst
@@ -2,4 +2,16 @@ declarative
===========
.. automodule:: sqlalchemy.ext.declarative
- :members: \ No newline at end of file
+
+API Reference
+-------------
+
+.. autofunction:: declarative_base
+
+.. autofunction:: _declarative_constructor
+
+.. autofunction:: synonym_for
+
+.. autofunction:: comparable_using
+
+.. autofunction:: instrument_declarative
diff --git a/lib/sqlalchemy/ext/declarative.py b/lib/sqlalchemy/ext/declarative.py
index 6d3941858..65c9df133 100644
--- a/lib/sqlalchemy/ext/declarative.py
+++ b/lib/sqlalchemy/ext/declarative.py
@@ -1,13 +1,16 @@
-"""A simple declarative layer for SQLAlchemy ORM.
-
+"""
Synopsis
========
-SQLAlchemy object-relational configuration involves the usage of :class:`~sqlalchemy.schema.Table`,
-:func:`~sqlalchemy.orm.mapper`, and class objects to define the three areas of configuration.
-``declarative`` moves these three types of configuration underneath the individual
-mapped class. Regular SQLAlchemy schema and ORM constructs are used in most
-cases::
+SQLAlchemy object-relational configuration involves the use of
+:class:`~sqlalchemy.schema.Table`, :func:`~sqlalchemy.orm.mapper`, and
+class objects to define the three areas of configuration.
+:mod:`~sqlalchemy.ext.declarative` allows all three types of
+configuration to be expressed declaratively on an individual
+mapped class. Regular SQLAlchemy schema elements and ORM constructs
+are used in most cases.
+
+As a simple example::
from sqlalchemy.ext.declarative import declarative_base
@@ -18,16 +21,21 @@ cases::
id = Column(Integer, primary_key=True)
name = Column(String(50))
-Above, the :func:`declarative_base` callable produces a new base class from which
-all mapped classes inherit from. When the class definition is completed, a
-new :class:`~sqlalchemy.schema.Table` and :class:`~sqlalchemy.orm.mapper` have been generated, accessible via the
-``__table__`` and ``__mapper__`` attributes on the ``SomeClass`` class.
+Above, the :func:`declarative_base` callable returns a new base class from which
+all mapped classes should inherit. When the class definition is completed, a
+new :class:`~sqlalchemy.schema.Table` and
+:class:`~sqlalchemy.orm.mapper` will have been generated, accessible
+via the ``__table__`` and ``__mapper__`` attributes on the ``SomeClass`` class.
Defining Attributes
===================
-:class:`~sqlalchemy.schema.Column` objects may be explicitly named,
-including using a different name than the attribute in which they are associated.
+In the above example, the :class:`~sqlalchemy.schema.Column` objects are
+automatically named with the name of the attribute to which they are
+assigned.
+
+They can also be explicitly named, and that name does not have to be
+the same as name assigned on the class.
The column will be assigned to the :class:`~sqlalchemy.schema.Table` using the
given name, and mapped to the class using the attribute name::
@@ -36,52 +44,54 @@ given name, and mapped to the class using the attribute name::
id = Column("some_table_id", Integer, primary_key=True)
name = Column("name", String(50))
-Otherwise, you may omit the names from the Column definitions.
-Declarative will set the ``name`` attribute on the column when the class
-is initialized::
-
- class SomeClass(Base):
- __tablename__ = 'some_table'
- id = Column(Integer, primary_key=True)
- name = Column(String(50))
-
Attributes may be added to the class after its construction, and they will be
-added to the underlying :class:`~sqlalchemy.schema.Table` and :func:`~sqlalchemy.orm.mapper()` definitions as
-appropriate::
+added to the underlying :class:`~sqlalchemy.schema.Table` and
+:func:`~sqlalchemy.orm.mapper()` definitions as appropriate::
SomeClass.data = Column('data', Unicode)
SomeClass.related = relation(RelatedInfo)
-Classes which are mapped explicitly using :func:`~sqlalchemy.orm.mapper()` can interact freely
-with declarative classes. It is recommended, though not required, that all tables
-share the same underlying :class:`~sqlalchemy.schema.MetaData` object, so that
-string-configured :class:`~sqlalchemy.schema.ForeignKey` references can be resolved without issue.
+Classes which are mapped explicitly using
+:func:`~sqlalchemy.orm.mapper()` can interact freely with declarative
+classes.
+
+It is recommended, though not required, that all tables
+share the same underlying :class:`~sqlalchemy.schema.MetaData` object,
+so that string-configured :class:`~sqlalchemy.schema.ForeignKey`
+references can be resolved without issue.
Association of Metadata and Engine
==================================
-The :func:`declarative_base` base class contains a :class:`~sqlalchemy.schema.MetaData` object where newly
-defined :class:`~sqlalchemy.schema.Table` objects are collected. This is accessed via the
-:class:`~sqlalchemy.schema.MetaData` class level accessor, so to create tables we can say::
+The :func:`declarative_base` base class contains a
+:class:`~sqlalchemy.schema.MetaData` object where newly
+defined :class:`~sqlalchemy.schema.Table` objects are collected. This
+is accessed via the :class:`~sqlalchemy.schema.MetaData` class level
+accessor, so to create tables we can say::
engine = create_engine('sqlite://')
Base.metadata.create_all(engine)
-The :class:`~sqlalchemy.engine.base.Engine` created above may also be directly associated with the
-declarative base class using the ``bind`` keyword argument, where it will be
-associated with the underlying :class:`~sqlalchemy.schema.MetaData` object and allow SQL operations
+The :class:`~sqlalchemy.engine.base.Engine` created above may also be
+directly associated with the declarative base class using the ``bind``
+keyword argument, where it will be associated with the underlying
+:class:`~sqlalchemy.schema.MetaData` object and allow SQL operations
involving that metadata and its tables to make use of that engine
automatically::
Base = declarative_base(bind=create_engine('sqlite://'))
-Or, as :class:`~sqlalchemy.schema.MetaData` allows, at any time using the ``bind`` attribute::
+Alternatively, by way of the normal
+:class:`~sqlalchemy.schema.MetaData` behaviour, the ``bind`` attribute
+of the class level accessor can be assigned at any time as follows::
Base.metadata.bind = create_engine('sqlite://')
-The :func:`declarative_base` can also receive a pre-created :class:`~sqlalchemy.schema.MetaData` object,
-which allows a declarative setup to be associated with an already existing
-traditional collection of :class:`~sqlalchemy.schema.Table` objects::
+The :func:`declarative_base` can also receive a pre-created
+:class:`~sqlalchemy.schema.MetaData` object, which allows a
+declarative setup to be associated with an already
+existing traditional collection of :class:`~sqlalchemy.schema.Table`
+objects::
mymetadata = MetaData()
Base = declarative_base(metadata=mymetadata)
@@ -89,11 +99,12 @@ traditional collection of :class:`~sqlalchemy.schema.Table` objects::
Configuring Relations
=====================
-Relations to other classes are done in the usual way, with the added feature
-that the class specified to :func:`~sqlalchemy.orm.relation()` may be a string name. The "class
-registry" associated with ``Base`` is used at mapper compilation time to
-resolve the name into the actual class object, which is expected to have been
-defined once the mapper configuration is used::
+Relations to other classes are done in the usual way, with the added
+feature that the class specified to :func:`~sqlalchemy.orm.relation()`
+may be a string name. The "class registry" associated with ``Base``
+is used at mapper compilation time to resolve the name into the actual
+class object, which is expected to have been defined once the mapper
+configuration is used::
class User(Base):
__tablename__ = 'users'
@@ -109,8 +120,9 @@ defined once the mapper configuration is used::
email = Column(String(50))
user_id = Column(Integer, ForeignKey('users.id'))
-Column constructs, since they are just that, are immediately usable, as below
-where we define a primary join condition on the ``Address`` class using them::
+Column constructs, since they are just that, are immediately usable,
+as below where we define a primary join condition on the ``Address``
+class using them::
class Address(Base):
__tablename__ = 'addresses'
@@ -120,52 +132,60 @@ where we define a primary join condition on the ``Address`` class using them::
user_id = Column(Integer, ForeignKey('users.id'))
user = relation(User, primaryjoin=user_id == User.id)
-In addition to the main argument for :func:`~sqlalchemy.orm.relation`, other arguments
-which depend upon the columns present on an as-yet undefined class
-may also be specified as strings. These strings are evaluated as
-Python expressions. The full namespace available within this
-evaluation includes all classes mapped for this declarative base,
-as well as the contents of the ``sqlalchemy`` package, including
-expression functions like :func:`~sqlalchemy.sql.expression.desc` and :attr:`~sqlalchemy.sql.expression.func`::
+In addition to the main argument for :func:`~sqlalchemy.orm.relation`,
+other arguments which depend upon the columns present on an as-yet
+undefined class may also be specified as strings. These strings are
+evaluated as Python expressions. The full namespace available within
+this evaluation includes all classes mapped for this declarative base,
+as well as the contents of the ``sqlalchemy`` package, including
+expression functions like :func:`~sqlalchemy.sql.expression.desc` and
+:attr:`~sqlalchemy.sql.expression.func`::
class User(Base):
# ....
- addresses = relation("Address", order_by="desc(Address.email)",
- primaryjoin="Address.user_id==User.id")
+ addresses = relation("Address",
+ order_by="desc(Address.email)",
+ primaryjoin="Address.user_id==User.id")
As an alternative to string-based attributes, attributes may also be
defined after all classes have been created. Just add them to the target
class after the fact::
- User.addresses = relation(Address, primaryjoin=Address.user_id == User.id)
+ User.addresses = relation(Address,
+ primaryjoin=Address.user_id==User.id)
Configuring Many-to-Many Relations
==================================
-There's nothing special about many-to-many with declarative. The ``secondary``
-argument to :func:`~sqlalchemy.orm.relation` still requires a :class:`~sqlalchemy.schema.Table` object, not a declarative class.
-The :class:`~sqlalchemy.schema.Table` should share the same :class:`~sqlalchemy.schema.MetaData` object used by the declarative base::
-
- keywords = Table('keywords', Base.metadata,
- Column('author_id', Integer, ForeignKey('authors.id')),
- Column('keyword_id', Integer, ForeignKey('keywords.id'))
- )
+There's nothing special about many-to-many with declarative. The
+``secondary`` argument to :func:`~sqlalchemy.orm.relation` still
+requires a :class:`~sqlalchemy.schema.Table` object, not a declarative
+class. The :class:`~sqlalchemy.schema.Table` should share the same
+:class:`~sqlalchemy.schema.MetaData` object used by the declarative
+base::
+
+ keywords = Table(
+ 'keywords', Base.metadata,
+ Column('author_id', Integer, ForeignKey('authors.id')),
+ Column('keyword_id', Integer, ForeignKey('keywords.id'))
+ )
class Author(Base):
__tablename__ = 'authors'
id = Column(Integer, primary_key=True)
keywords = relation("Keyword", secondary=keywords)
-You should generally **not** map a class and also specify its table in a many-to-many
-relation, since the ORM may issue duplicate INSERT and DELETE statements.
+You should generally **not** map a class and also specify its table in
+a many-to-many relation, since the ORM may issue duplicate INSERT and
+DELETE statements.
Defining Synonyms
=================
-Synonyms are introduced in :ref:`synonyms`. To define a getter/setter which
-proxies to an underlying attribute, use :func:`~sqlalchemy.orm.synonym` with the
-``descriptor`` argument::
+Synonyms are introduced in :ref:`synonyms`. To define a getter/setter
+which proxies to an underlying attribute, use
+:func:`~sqlalchemy.orm.synonym` with the ``descriptor`` argument::
class MyClass(Base):
__tablename__ = 'sometable'
@@ -185,8 +205,8 @@ class-level expression construct::
x.attr = "some value"
session.query(MyClass).filter(MyClass.attr == 'some other value').all()
-For simple getters, the :func:`synonym_for` decorator can be used in conjunction
-with ``@property``::
+For simple getters, the :func:`synonym_for` decorator can be used in
+conjunction with ``@property``::
class MyClass(Base):
__tablename__ = 'sometable'
@@ -198,8 +218,8 @@ with ``@property``::
def attr(self):
return self._some_attr
-Similarly, :func:`comparable_using` is a front end for the :func:`~sqlalchemy.orm.comparable_property`
-ORM function::
+Similarly, :func:`comparable_using` is a front end for the
+:func:`~sqlalchemy.orm.comparable_property` ORM function::
class MyClass(Base):
__tablename__ = 'sometable'
@@ -214,18 +234,21 @@ ORM function::
Table Configuration
===================
-Table arguments other than the name, metadata, and mapped Column arguments
-are specified using the ``__table_args__`` class attribute. This attribute
-accommodates both positional as well as keyword arguments that are normally
-sent to the :class:`~sqlalchemy.schema.Table` constructor. The attribute can be specified
-in one of two forms. One is as a dictionary::
+Table arguments other than the name, metadata, and mapped Column
+arguments are specified using the ``__table_args__`` class attribute.
+This attribute accommodates both positional as well as keyword
+arguments that are normally sent to the
+:class:`~sqlalchemy.schema.Table` constructor.
+The attribute can be specified in one of two forms. One is as a
+dictionary::
class MyClass(Base):
__tablename__ = 'sometable'
__table_args__ = {'mysql_engine':'InnoDB'}
-The other, a tuple of the form ``(arg1, arg2, ..., {kwarg1:value, ...})``, which
-allows positional arguments to be specified as well (usually constraints)::
+The other, a tuple of the form
+``(arg1, arg2, ..., {kwarg1:value, ...})``, which allows positional
+arguments to be specified as well (usually constraints)::
class MyClass(Base):
__tablename__ = 'sometable'
@@ -235,12 +258,14 @@ allows positional arguments to be specified as well (usually constraints)::
{'autoload':True}
)
-Note that the dictionary is required in the tuple form even if empty.
+Note that the keyword parameters dictionary is required in the tuple
+form even if empty.
-As an alternative to ``__tablename__``, a direct :class:`~sqlalchemy.schema.Table`
-construct may be used. The :class:`~sqlalchemy.schema.Column` objects, which
-in this case require their names, will be
-added to the mapping just like a regular mapping to a table::
+As an alternative to ``__tablename__``, a direct
+:class:`~sqlalchemy.schema.Table` construct may be used. The
+:class:`~sqlalchemy.schema.Column` objects, which in this case require
+their names, will be added to the mapping just like a regular mapping
+to a table::
class MyClass(Base):
__table__ = Table('my_table', Base.metadata,
@@ -251,9 +276,14 @@ added to the mapping just like a regular mapping to a table::
Mapper Configuration
====================
-Mapper arguments are specified using the ``__mapper_args__`` class variable,
-which is a dictionary that accepts the same names as the :class:`~sqlalchemy.orm.mapper`
-function accepts as keywords::
+Configuration of mappers is done with the
+:func:`~sqlalchemy.orm.mapper` function and all the possible mapper
+configuration parameters can be found in the documentation for that
+function.
+
+:func:`~sqlalchemy.orm.mapper` is still used by declaratively mapped
+classes and keyword parameters to the function can be passed by
+placing them in the ``__mapper_args__`` class variable::
class Widget(Base):
__tablename__ = 'widgets'
@@ -265,7 +295,7 @@ Inheritance Configuration
=========================
Declarative supports all three forms of inheritance as intuitively
-as possible. The ``inherits`` mapper keyword argument is not needed,
+as possible. The ``inherits`` mapper keyword argument is not needed
as declarative will determine this from the class itself. The various
"polymorphic" keyword arguments are specified using ``__mapper_args__``.
@@ -287,11 +317,12 @@ table::
id = Column(Integer, ForeignKey('people.id'), primary_key=True)
primary_language = Column(String(50))
-Note that above, the ``Engineer.id`` attribute, since it shares the same
-attribute name as the ``Person.id`` attribute, will in fact represent the ``people.id``
-and ``engineers.id`` columns together, and will render inside a query as ``"people.id"``.
-To provide the ``Engineer`` class with an attribute that represents only the
-``engineers.id`` column, give it a different attribute name::
+Note that above, the ``Engineer.id`` attribute, since it shares the
+same attribute name as the ``Person.id`` attribute, will in fact
+represent the ``people.id`` and ``engineers.id`` columns together, and
+will render inside a query as ``"people.id"``.
+To provide the ``Engineer`` class with an attribute that represents
+only the ``engineers.id`` column, give it a different attribute name::
class Engineer(Person):
__tablename__ = 'engineers'
@@ -302,8 +333,9 @@ To provide the ``Engineer`` class with an attribute that represents only the
Single Table Inheritance
~~~~~~~~~~~~~~~~~~~~~~~~
-Single table inheritance is defined as a subclass that does not have its
-own table; you just leave out the ``__table__`` and ``__tablename__`` attributes::
+Single table inheritance is defined as a subclass that does not have
+its own table; you just leave out the ``__table__`` and ``__tablename__``
+attributes::
class Person(Base):
__tablename__ = 'people'
@@ -315,28 +347,31 @@ own table; you just leave out the ``__table__`` and ``__tablename__`` attributes
__mapper_args__ = {'polymorphic_identity': 'engineer'}
primary_language = Column(String(50))
-When the above mappers are configured, the ``Person`` class is mapped to the ``people``
-table *before* the ``primary_language`` column is defined, and this column will not be included
-in its own mapping. When ``Engineer`` then defines the ``primary_language``
-column, the column is added to the ``people`` table so that it is included in the mapping
-for ``Engineer`` and is also part of the table's full set of columns.
-Columns which are not mapped to ``Person`` are also excluded from any other
-single or joined inheriting classes using the ``exclude_properties`` mapper argument.
-Below, ``Manager`` will have all the attributes of ``Person`` and ``Manager`` but *not*
-the ``primary_language`` attribute of ``Engineer``::
+When the above mappers are configured, the ``Person`` class is mapped
+to the ``people`` table *before* the ``primary_language`` column is
+defined, and this column will not be included in its own mapping.
+When ``Engineer`` then defines the ``primary_language`` column, the
+column is added to the ``people`` table so that it is included in the
+mapping for ``Engineer`` and is also part of the table's full set of
+columns. Columns which are not mapped to ``Person`` are also excluded
+from any other single or joined inheriting classes using the
+``exclude_properties`` mapper argument. Below, ``Manager`` will have
+all the attributes of ``Person`` and ``Manager`` but *not* the
+``primary_language`` attribute of ``Engineer``::
class Manager(Person):
__mapper_args__ = {'polymorphic_identity': 'manager'}
golf_swing = Column(String(50))
-The attribute exclusion logic is provided by the ``exclude_properties`` mapper argument,
-and declarative's default behavior can be disabled by passing an explicit
-``exclude_properties`` collection (empty or otherwise) to the ``__mapper_args__``.
+The attribute exclusion logic is provided by the
+``exclude_properties`` mapper argument, and declarative's default
+behavior can be disabled by passing an explicit ``exclude_properties``
+collection (empty or otherwise) to the ``__mapper_args__``.
Concrete Table Inheritance
~~~~~~~~~~~~~~~~~~~~~~~~~~
-Concrete is defined as a subclass which has its own table and sets the
+Concrete is defined as a subclass which has its own table and sets the
``concrete`` keyword argument to ``True``::
class Person(Base):
@@ -351,8 +386,8 @@ Concrete is defined as a subclass which has its own table and sets the
primary_language = Column(String(50))
name = Column(String(50))
-Usage of an abstract base class is a little less straightforward as it requires
-usage of :func:`~sqlalchemy.orm.util.polymorphic_union`::
+Usage of an abstract base class is a little less straightforward as it
+requires usage of :func:`~sqlalchemy.orm.util.polymorphic_union`::
engineers = Table('engineers', Base.metadata,
Column('id', Integer, primary_key=True),
@@ -382,19 +417,78 @@ usage of :func:`~sqlalchemy.orm.util.polymorphic_union`::
__table__ = managers
__mapper_args__ = {'polymorphic_identity':'manager', 'concrete':True}
-Class Usage
-===========
+
+Mix-in Classes
+==============
+
+A common need when using :mod:`~sqlalchemy.ext.declarative` is to
+share some functionality, often a set of columns, across many
+classes. The normal python idiom would be to put this common code into
+a base class and have all the other classes subclass this class.
+
+When using :mod:`~sqlalchemy.ext.declarative`, this need is met by
+using a "mix-in class". A mix-in class is one that isn't mapped to a
+table and doesn't subclass the declarative :class:`Base`. For example::
+
+ class MyMixin(object):
+
+ __table_args__ = {'mysql_engine':'InnoDB'}
+ __mapper_args__=dict(always_refresh=True)
+ id = Column(Integer, primary_key=True)
+
+ def foo(self):
+ return 'bar'+str(self.id)
+
+ class MyModel(Base,MyMixin):
+ __tablename__='test'
+ name = Column(String(1000), nullable=False, index=True)
+
+As the above example shows, ``__table_args__`` and ``__mapper_args__``
+can both be abstracted out into a mix-in if you use common values for
+these across many classes.
+
+However, particularly in the case of ``__table_args__``, you may want
+to combine some parameters from several mix-ins with those you wish to
+define on the class iteself. To help with this, a
+:func:`~sqlalchemy.util.classproperty` decorator is provided that lets
+you implement a class property with a function. For example::
+
+ from sqlalchemy.util import classproperty
+
+ class MySQLSettings:
+ __table_args__ = {'mysql_engine':'InnoDB'}
+
+ class MyOtherMixin:
+ __table_args__ = {'info':'foo'}
+
+ class MyModel(Base,MySQLSettings,MyOtherMixin):
+ __tablename__='my_model'
+
+ @classproperty
+ def __table_args__(self):
+ args = dict()
+ args.update(MySQLSettings.__table_args__)
+ args.update(MyOtherMixin.__table_args__)
+ return args
+
+ id = Column(Integer, primary_key=True)
+
+Class Constructor
+=================
As a convenience feature, the :func:`declarative_base` sets a default
-constructor on classes which takes keyword arguments, and assigns them to the
-named attributes::
+constructor on classes which takes keyword arguments, and assigns them
+to the named attributes::
e = Engineer(primary_language='python')
-Note that ``declarative`` has no integration built in with sessions, and is
-only intended as an optional syntax for the regular usage of mappers and Table
-objects. A typical application setup using :func:`~sqlalchemy.orm.scoped_session` might look
-like::
+Sessions
+========
+
+Note that ``declarative`` does nothing special with sessions, and is
+only intended as an easier way to configure mappers and
+:class:`~sqlalchemy.schema.Table` objects. A typical application
+setup using :func:`~sqlalchemy.orm.scoped_session` might look like::
engine = create_engine('postgresql://scott:tiger@localhost/test')
Session = scoped_session(sessionmaker(autocommit=False,
@@ -402,7 +496,8 @@ like::
bind=engine))
Base = declarative_base()
-Mapped instances then make usage of :class:`~sqlalchemy.orm.session.Session` in the usual way.
+Mapped instances then make usage of
+:class:`~sqlalchemy.orm.session.Session` in the usual way.
"""
@@ -419,8 +514,8 @@ __all__ = 'declarative_base', 'synonym_for', 'comparable_using', 'instrument_dec
def instrument_declarative(cls, registry, metadata):
"""Given a class, configure the class declaratively,
- using the given registry (any dictionary) and MetaData object.
- This operation does not assume any kind of class hierarchy.
+ using the given registry, which can be any dictionary, and
+ MetaData object.
"""
if '_decl_class_registry' in cls.__dict__:
@@ -432,6 +527,31 @@ def instrument_declarative(cls, registry, metadata):
_as_declarative(cls, cls.__name__, cls.__dict__)
def _as_declarative(cls, classname, dict_):
+
+ # this spelling enables these attributes to be descriptors
+ mapper_args = '__mapper_args__' in dict_ and cls.__mapper_args__ or {}
+ table_args = '__table_args__' in dict_ and cls.__table_args__ or None
+
+ # dict_ will be a dictproxy, which we can't write to, and we need to!
+ dict_ = dict(dict_)
+
+ column_copies = dict()
+
+ for base in cls.__bases__:
+ names = dir(base)
+ if not _is_mapped_class(base):
+ for name in names:
+ obj = getattr(base,name)
+ if isinstance(obj, Column):
+ dict_[name]=column_copies[obj]=obj.copy()
+ mapper_args = mapper_args or getattr(base,'__mapper_args__',mapper_args)
+ table_args = table_args or getattr(base,'__table_args__',None)
+
+ # make sure that column copies are used rather than the original columns
+ # from any mixins
+ for k, v in mapper_args.iteritems():
+ mapper_args[k] = column_copies.get(v,v)
+
cls._decl_class_registry[classname] = cls
our_stuff = util.OrderedDict()
for k in dict_:
@@ -474,7 +594,6 @@ def _as_declarative(cls, classname, dict_):
if '__tablename__' in dict_:
tablename = cls.__tablename__
- table_args = dict_.get('__table_args__')
if isinstance(table_args, dict):
args, table_kw = (), table_args
elif isinstance(table_args, tuple):
@@ -500,12 +619,9 @@ def _as_declarative(cls, classname, dict_):
for c in cols:
if not table.c.contains_column(c):
raise exceptions.ArgumentError(
- "Can't add additional column %r when specifying __table__" % key)
+ "Can't add additional column %r when specifying __table__" % key
+ )
- if '__mapper_args__' in dict_:
- mapper_args = dict(dict_['__mapper_args__'])
- else:
- mapper_args = {}
if 'inherits' not in mapper_args:
for c in cls.__bases__:
if _is_mapped_class(c):
@@ -518,8 +634,10 @@ def _as_declarative(cls, classname, dict_):
mapper_cls = mapper
if table is None and 'inherits' not in mapper_args:
- raise exceptions.InvalidRequestError("Class %r does not have a __table__ or __tablename__ "
- "specified and does not inherit from an existing table-mapped class." % cls)
+ raise exceptions.InvalidRequestError(
+ "Class %r does not have a __table__ or __tablename__ "
+ "specified and does not inherit from an existing table-mapped class." % cls
+ )
elif 'inherits' in mapper_args and not mapper_args.get('concrete', False):
inherited_mapper = class_mapper(mapper_args['inherits'], compile=False)
@@ -536,25 +654,30 @@ def _as_declarative(cls, classname, dict_):
if table is None:
# single table inheritance.
# ensure no table args
- table_args = cls.__dict__.get('__table_args__')
if table_args is not None:
- raise exceptions.ArgumentError("Can't place __table_args__ on an inherited class with no table.")
+ raise exceptions.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 cols:
if c.primary_key:
- raise exceptions.ArgumentError("Can't place primary key columns on an inherited class with no table.")
+ raise exceptions.ArgumentError(
+ "Can't place primary key columns on an inherited class with no table."
+ )
inherited_table.append_column(c)
# single or joined inheritance
- # exclude any cols on the inherited table which are not mapped on the parent class, to avoid
+ # 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 = class_mapper(mapper_args['inherits'], compile=False)
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])
+ 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 cols])
cls.__mapper__ = mapper_cls(cls, table, properties=our_stuff, **mapper_args)
@@ -633,14 +756,16 @@ def _deferred_relation(cls, prop):
return return_cls
if isinstance(prop, PropertyLoader):
- for attr in ('argument', 'order_by', 'primaryjoin', 'secondaryjoin', 'secondary', '_foreign_keys', 'remote_side'):
+ for attr in ('argument', 'order_by', 'primaryjoin', 'secondaryjoin',
+ 'secondary', '_foreign_keys', 'remote_side'):
v = getattr(prop, attr)
if isinstance(v, basestring):
setattr(prop, attr, resolve_arg(v))
if prop.backref and isinstance(prop.backref, tuple):
key, kwargs = prop.backref
- for attr in ('primaryjoin', 'secondaryjoin', 'secondary', 'foreign_keys', 'remote_side', 'order_by'):
+ for attr in ('primaryjoin', 'secondaryjoin', 'secondary',
+ 'foreign_keys', 'remote_side', 'order_by'):
if attr in kwargs and isinstance(kwargs[attr], basestring):
kwargs[attr] = resolve_arg(kwargs[attr])
@@ -672,7 +797,8 @@ def synonym_for(name, map_column=False):
def comparable_using(comparator_factory):
"""Decorator, allow a Python @property to be used in query criteria.
- A decorator front end to :func:`~sqlalchemy.orm.comparable_property`, passes
+ This is a decorator front end to
+ :func:`~sqlalchemy.orm.comparable_property` that passes
through the comparator_factory and the function being decorated::
@comparable_using(MyComparatorType)
@@ -693,10 +819,12 @@ def comparable_using(comparator_factory):
def _declarative_constructor(self, **kwargs):
"""A simple constructor that allows initialization from kwargs.
- Sets kwargs on the constructed instance. Only keys that are present as
- attributes of type(self) are allowed (for example, any mapped column or
- relation).
-
+ 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 relations.
"""
for k in kwargs:
if not hasattr(type(self), k):
@@ -711,22 +839,25 @@ def declarative_base(bind=None, metadata=None, mapper=None, cls=object,
metaclass=DeclarativeMeta):
"""Construct a base class for declarative class definitions.
- The new base class will be given a metaclass that invokes
- :func:`instrument_declarative()` upon each subclass definition, and routes
- later Column- and Mapper-related attribute assignments made on the class
- into Table and Mapper assignments.
+ The new base class will be given a metaclass that produces
+ appropriate :class:`~sqlalchemy.schema.Table` objects and makes
+ the appropriate :func:`~sqlalchemy.orm.mapper` calls based on the
+ information provided declaratively in the class and any subclasses
+ of the class.
- :param bind: An optional :class:`~sqlalchemy.engine.base.Connectable`, will be assigned
- the ``bind`` attribute on the :class:`~sqlalchemy.MetaData` instance.
+ :param bind: An optional
+ :class:`~sqlalchemy.engine.base.Connectable`, will be assigned
+ the ``bind`` attribute on the :class:`~sqlalchemy.MetaData`
+ instance.
:param metadata:
- An optional :class:`~sqlalchemy.MetaData` instance. All :class:`~sqlalchemy.schema.Table`
- objects implicitly declared by
+ An optional :class:`~sqlalchemy.MetaData` instance. All
+ :class:`~sqlalchemy.schema.Table` objects implicitly declared by
subclasses of the base will share this MetaData. A MetaData instance
- will be create if none is provided. The MetaData instance will be
- available via the `metadata` attribute of the generated declarative
- base class.
+ will be created if none is provided. The
+ :class:`~sqlalchemy.MetaData` instance will be available via the
+ `metadata` attribute of the generated declarative base class.
:param mapper:
An optional callable, defaults to :func:`~sqlalchemy.orm.mapper`. Will be
@@ -734,7 +865,7 @@ def declarative_base(bind=None, metadata=None, mapper=None, cls=object,
:param cls:
Defaults to :class:`object`. A type to use as the base for the generated
- declarative base class. May be a type or tuple of types.
+ declarative base class. May be a class or tuple of classes.
:param name:
Defaults to ``Base``. The display name for the generated
@@ -742,11 +873,12 @@ def declarative_base(bind=None, metadata=None, mapper=None, cls=object,
tracebacks and debugging.
:param constructor:
- Defaults to declarative._declarative_constructor, an __init__
- implementation that assigns \**kwargs for declared fields and relations
- to an instance. If ``None`` is supplied, no __init__ will be installed
- and construction will fall back to cls.__init__ with normal Python
- semantics.
+ Defaults to
+ :func:`~sqlalchemy.ext.declarative._declarative_constructor`, an
+ __init__ implementation that assigns \**kwargs for declared
+ fields and relations to an instance. If ``None`` is supplied,
+ no __init__ will be provided and construction will fall back to
+ cls.__init__ by way of the normal Python semantics.
:param metaclass:
Defaults to :class:`DeclarativeMeta`. A metaclass or __metaclass__
diff --git a/test/ext/test_declarative.py b/test/ext/test_declarative.py
index 4722427d5..457936d78 100644
--- a/test/ext/test_declarative.py
+++ b/test/ext/test_declarative.py
@@ -8,7 +8,7 @@ from sqlalchemy import MetaData, Integer, String, ForeignKey, ForeignKeyConstrai
from sqlalchemy.test.schema import Table, Column
from sqlalchemy.orm import relation, create_session, class_mapper, eagerload, compile_mappers, backref, clear_mappers, polymorphic_union, deferred
from sqlalchemy.test.testing import eq_
-
+from sqlalchemy.util import classproperty
from test.orm._base import ComparableEntity, MappedTest
@@ -1473,7 +1473,10 @@ class DeclarativeInheritanceTest(DeclarativeTestBase):
class Engineer(Person):
__mapper_args__ = {'polymorphic_identity':'engineer'}
primary_language = Column('primary_language', String(50))
- __table_args__ = ()
+ # this should be on the Person class, as this is single
+ # table inheritance, which is why we test that this
+ # throws an exception!
+ __table_args__ = {'mysql_engine':'InnoDB'}
assert_raises_message(sa.exc.ArgumentError, "place __table_args__", go)
def test_concrete(self):
@@ -1789,3 +1792,171 @@ class DeclarativeReflectionTest(testing.TestBase):
eq_(a1, IMHandle(network='lol', handle='zomg'))
eq_(a1.user, User(name='u1'))
+class DeclarativeMixinTest(DeclarativeTestBase):
+
+ def test_simple(self):
+
+ class MyMixin(object):
+ id = Column(Integer, primary_key=True)
+
+ def foo(self):
+ return 'bar'+str(self.id)
+
+ class MyModel(Base,MyMixin):
+ __tablename__='test'
+ name = Column(String(1000), nullable=False, index=True)
+
+ Base.metadata.create_all()
+
+ session = create_session()
+ session.add(MyModel(name='testing'))
+ session.flush()
+ session.expunge_all()
+
+ obj = session.query(MyModel).one()
+ eq_(obj.id,1)
+ eq_(obj.name,'testing')
+ eq_(obj.foo(),'bar1')
+
+ def test_hierarchical_bases(self):
+
+ class MyMixinParent:
+ id = Column(Integer, primary_key=True)
+
+ def foo(self):
+ return 'bar'+str(self.id)
+
+ class MyMixin(MyMixinParent):
+ baz = Column(String(1000), nullable=False, index=True)
+
+ class MyModel(Base,MyMixin):
+ __tablename__='test'
+ name = Column(String(1000), nullable=False, index=True)
+
+ Base.metadata.create_all()
+
+ session = create_session()
+ session.add(MyModel(name='testing',baz='fu'))
+ session.flush()
+ session.expunge_all()
+
+ obj = session.query(MyModel).one()
+ eq_(obj.id,1)
+ eq_(obj.name,'testing')
+ eq_(obj.foo(),'bar1')
+ eq_(obj.baz,'fu')
+
+ def test_table_args_inherited(self):
+
+ class MyMixin:
+ __table_args__ = {'mysql_engine':'InnoDB'}
+
+ class MyModel(Base,MyMixin):
+ __tablename__='test'
+ id = Column(Integer, primary_key=True)
+
+ eq_(MyModel.__table__.kwargs,{'mysql_engine': 'InnoDB'})
+
+ def test_table_args_overridden(self):
+
+ class MyMixin:
+ __table_args__ = {'mysql_engine':'Foo'}
+
+ class MyModel(Base,MyMixin):
+ __tablename__='test'
+ __table_args__ = {'mysql_engine':'InnoDB'}
+ id = Column(Integer, primary_key=True)
+
+ eq_(MyModel.__table__.kwargs,{'mysql_engine': 'InnoDB'})
+
+ def test_table_args_composite(self):
+
+ class MyMixin1:
+ __table_args__ = {'info':{'baz':'bob'}}
+
+ class MyMixin2:
+ __table_args__ = {'info':{'foo':'bar'}}
+
+ class MyModel(Base,MyMixin1,MyMixin2):
+ __tablename__='test'
+
+ @classproperty
+ def __table_args__(self):
+ info = {}
+ args = dict(info=info)
+ info.update(MyMixin1.__table_args__['info'])
+ info.update(MyMixin2.__table_args__['info'])
+ return args
+
+ id = Column(Integer, primary_key=True)
+
+ eq_(MyModel.__table__.info,{
+ 'foo': 'bar',
+ 'baz': 'bob',
+ })
+
+ def test_mapper_args_inherited(self):
+
+ class MyMixin:
+ __mapper_args__=dict(always_refresh=True)
+
+ class MyModel(Base,MyMixin):
+ __tablename__='test'
+ id = Column(Integer, primary_key=True)
+
+ eq_(MyModel.__mapper__.always_refresh,True)
+
+
+ def test_mapper_args_polymorphic_on_inherited(self):
+
+ class MyMixin:
+ type_ = Column(String(50))
+ __mapper_args__=dict(polymorphic_on=type_)
+
+ class MyModel(Base,MyMixin):
+ __tablename__='test'
+ id = Column(Integer, primary_key=True)
+
+ col = MyModel.__mapper__.polymorphic_on
+ eq_(col.name,'type_')
+ assert col.table is not None
+
+
+ def test_mapper_args_overridden(self):
+
+ class MyMixin:
+ __mapper_args__=dict(always_refresh=True)
+
+ class MyModel(Base,MyMixin):
+ __tablename__='test'
+ __mapper_args__=dict(always_refresh=False)
+ id = Column(Integer, primary_key=True)
+
+ eq_(MyModel.__mapper__.always_refresh,False)
+
+ def test_mapper_args_composite(self):
+
+ class MyMixin1:
+ type_ = Column(String(50))
+ __mapper_args__=dict(polymorphic_on=type_)
+
+ class MyMixin2:
+ __mapper_args__=dict(always_refresh=True)
+
+ class MyModel(Base,MyMixin1,MyMixin2):
+ __tablename__='test'
+
+ @classproperty
+ def __mapper_args__(self):
+ args = {}
+ args.update(MyMixin1.__mapper_args__)
+ args.update(MyMixin2.__mapper_args__)
+ return args
+
+ id = Column(Integer, primary_key=True)
+
+ col = MyModel.__mapper__.polymorphic_on
+ eq_(col.name,'type_')
+ assert col.table is not None
+
+ eq_(MyModel.__mapper__.always_refresh,True)