diff options
-rw-r--r-- | CHANGES | 20 | ||||
-rw-r--r-- | doc/build/conf.py | 2 | ||||
-rw-r--r-- | doc/build/intro.rst | 2 | ||||
-rw-r--r-- | doc/build/ormtutorial.rst | 27 | ||||
-rw-r--r-- | doc/build/reference/ext/declarative.rst | 14 | ||||
-rw-r--r-- | lib/sqlalchemy/ext/declarative.py | 448 | ||||
-rw-r--r-- | test/ext/test_declarative.py | 175 |
7 files changed, 515 insertions, 173 deletions
@@ -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) |