diff options
author | Mike Bayer <mike_mp@zzzcomputing.com> | 2010-08-08 17:50:44 -0400 |
---|---|---|
committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2010-08-08 17:50:44 -0400 |
commit | bb5a85feca1a6ed1aa7d18a04c0dfff3afa306c1 (patch) | |
tree | 12d6f26d360310bfa8b718e0876eff8eedd42c11 /lib/sqlalchemy | |
parent | 0b9afe1412898ba31282e75d90ca4d728613ca2b (diff) | |
parent | bb3be98d3bee4b2bcef791be022ddb2510b9cf9c (diff) | |
download | sqlalchemy-bb5a85feca1a6ed1aa7d18a04c0dfff3afa306c1.tar.gz |
merge tip
Diffstat (limited to 'lib/sqlalchemy')
-rw-r--r-- | lib/sqlalchemy/dialects/sqlite/base.py | 2 | ||||
-rw-r--r-- | lib/sqlalchemy/engine/base.py | 38 | ||||
-rw-r--r-- | lib/sqlalchemy/exc.py | 4 | ||||
-rwxr-xr-x | lib/sqlalchemy/ext/declarative.py | 272 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/__init__.py | 390 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/attributes.py | 4 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/collections.py | 2 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/dynamic.py | 39 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/interfaces.py | 2 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/mapper.py | 86 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/properties.py | 112 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/query.py | 6 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/session.py | 5 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/strategies.py | 14 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/util.py | 55 | ||||
-rw-r--r-- | lib/sqlalchemy/schema.py | 53 | ||||
-rw-r--r-- | lib/sqlalchemy/sql/expression.py | 36 |
17 files changed, 659 insertions, 461 deletions
diff --git a/lib/sqlalchemy/dialects/sqlite/base.py b/lib/sqlalchemy/dialects/sqlite/base.py index 2d9e56baf..b84b18e68 100644 --- a/lib/sqlalchemy/dialects/sqlite/base.py +++ b/lib/sqlalchemy/dialects/sqlite/base.py @@ -600,5 +600,5 @@ def _pragma_cursor(cursor): """work around SQLite issue whereby cursor.description is blank when PRAGMA returns no rows.""" if cursor.closed: - cursor._fetchone_impl = lambda: None + cursor.fetchone = lambda: None return cursor diff --git a/lib/sqlalchemy/engine/base.py b/lib/sqlalchemy/engine/base.py index 8533e19fc..4260e923a 100644 --- a/lib/sqlalchemy/engine/base.py +++ b/lib/sqlalchemy/engine/base.py @@ -908,7 +908,7 @@ class Connection(Connectable): self.__connection = self.engine.raw_connection() self.__invalid = False return self.__connection - raise exc.InvalidRequestError("This Connection is closed") + raise exc.ResourceClosedError("This Connection is closed") @property def info(self): @@ -956,7 +956,7 @@ class Connection(Connectable): """ if self.closed: - raise exc.InvalidRequestError("This Connection is closed") + raise exc.ResourceClosedError("This Connection is closed") if self.__connection.is_valid: self.__connection.invalidate(exception) @@ -2359,7 +2359,9 @@ class ResultProxy(object): if _autoclose_connection and \ self.connection.should_close_with_result: self.connection.close() - + # allow consistent errors + self.cursor = None + def __iter__(self): while True: row = self.fetchone() @@ -2441,14 +2443,32 @@ class ResultProxy(object): return self.dialect.supports_sane_multi_rowcount def _fetchone_impl(self): - return self.cursor.fetchone() + try: + return self.cursor.fetchone() + except AttributeError: + self._non_result() def _fetchmany_impl(self, size=None): - return self.cursor.fetchmany(size) + try: + return self.cursor.fetchmany(size) + except AttributeError: + self._non_result() def _fetchall_impl(self): - return self.cursor.fetchall() - + try: + return self.cursor.fetchall() + except AttributeError: + self._non_result() + + def _non_result(self): + if self._metadata is None: + raise exc.ResourceClosedError( + "This result object does not return rows. " + "It has been closed automatically.", + ) + else: + raise exc.ResourceClosedError("This result object is closed.") + def process_rows(self, rows): process_row = self._process_row metadata = self._metadata @@ -2505,7 +2525,6 @@ class ResultProxy(object): Else the cursor is automatically closed and None is returned. """ - try: row = self._fetchone_impl() if row is not None: @@ -2525,6 +2544,9 @@ class ResultProxy(object): Returns None if no row is present. """ + if self._metadata is None: + self._non_result() + try: row = self._fetchone_impl() except Exception, e: diff --git a/lib/sqlalchemy/exc.py b/lib/sqlalchemy/exc.py index 31826f44f..1c412824c 100644 --- a/lib/sqlalchemy/exc.py +++ b/lib/sqlalchemy/exc.py @@ -60,6 +60,10 @@ class InvalidRequestError(SQLAlchemyError): """ +class ResourceClosedError(InvalidRequestError): + """An operation was requested from a connection, cursor, or other + object that's in a closed state.""" + class NoSuchColumnError(KeyError, InvalidRequestError): """A nonexistent column is requested from a ``RowProxy``.""" diff --git a/lib/sqlalchemy/ext/declarative.py b/lib/sqlalchemy/ext/declarative.py index 3370a764c..2310f01ce 100755 --- a/lib/sqlalchemy/ext/declarative.py +++ b/lib/sqlalchemy/ext/declarative.py @@ -2,13 +2,14 @@ Synopsis ======== -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. +SQLAlchemy object-relational configuration involves the +combination of :class:`.Table`, :func:`.mapper`, and class +objects to define a mapped class. +:mod:`~sqlalchemy.ext.declarative` allows all three to be +expressed at once within the class declaration. As much as +possible, regular SQLAlchemy schema and ORM constructs are +used directly, so that configuration between "classical" ORM +usage and declarative remain highly similar. As a simple example:: @@ -23,72 +24,74 @@ As a simple example:: 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. +completed, a new :class:`.Table` and +:func:`.mapper` will have been generated. + +The resulting table and mapper are accessible via +``__table__`` and ``__mapper__`` attributes on the +``SomeClass`` class:: + + # access the mapped Table + SomeClass.__table__ + + # access the Mapper + SomeClass.__mapper__ Defining Attributes =================== -In the above example, the :class:`~sqlalchemy.schema.Column` objects are +In the previous example, the :class:`.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:: +To name columns explicitly with a name distinct from their mapped attribute, +just give the column a name. Below, column "some_table_id" is mapped to the +"id" attribute of `SomeClass`, but in SQL will be represented as "some_table_id":: class SomeClass(Base): __tablename__ = 'some_table' id = Column("some_table_id", Integer, primary_key=True) - name = Column("name", 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:`.Table` and +:func:`.mapper()` definitions as appropriate:: SomeClass.data = Column('data', Unicode) SomeClass.related = relationship(RelatedInfo) -Classes which are mapped explicitly using -:func:`~sqlalchemy.orm.mapper()` can interact freely with declarative -classes. +Classes which are constructed using declarative can interact freely +with classes that are mapped explicitly with :func:`mapper`. 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 -================================== +Accessing the MetaData +======================= 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:: +:class:`.MetaData` object where newly defined +:class:`.Table` objects are collected. This object is +intended to be accessed directly for +:class:`.MetaData`-specific operations. Such as, to issue +CREATE statements for all tables:: 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 -involving that metadata and its tables to make use of that engine -automatically:: +The usual techniques of associating :class:`.MetaData:` with :class:`.Engine` +apply, such as assigning to the ``bind`` attribute:: - Base = declarative_base(bind=create_engine('sqlite://')) + Base.metadata.bind = create_engine('sqlite://') -Alternatively, by way of the normal -:class:`~sqlalchemy.schema.MetaData` behavior, the ``bind`` attribute -of the class level accessor can be assigned at any time as follows:: +To associate the engine with the :func:`declarative_base` at time +of construction, the ``bind`` argument is accepted:: - Base.metadata.bind = create_engine('sqlite://') + Base = declarative_base(bind=create_engine('sqlite://')) -The :func:`declarative_base` can also receive a pre-created -:class:`~sqlalchemy.schema.MetaData` object, which allows a +:func:`declarative_base` can also receive a pre-existing +:class:`.MetaData` object, which allows a declarative setup to be associated with an already existing traditional collection of :class:`~sqlalchemy.schema.Table` objects:: @@ -157,12 +160,13 @@ class after the fact:: Configuring Many-to-Many Relationships ====================================== -There's nothing special about many-to-many with declarative. The -``secondary`` argument to :func:`~sqlalchemy.orm.relationship` 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:: +Many-to-many relationships are also declared in the same way +with declarative as with traditional mappings. The +``secondary`` argument to +:func:`.relationship` is as usual passed a +:class:`.Table` object, which is typically declared in the +traditional way. The :class:`.Table` usually shares +the :class:`.MetaData` object used by the declarative base:: keywords = Table( 'keywords', Base.metadata, @@ -175,10 +179,11 @@ base:: id = Column(Integer, primary_key=True) keywords = relationship("Keyword", secondary=keywords) -You should generally **not** map a class and also specify its table in -a many-to-many relationship, since the ORM may issue duplicate INSERT and -DELETE statements. - +As with traditional mapping, its generally not a good idea to use +a :class:`.Table` as the "secondary" argument which is also mapped to +a class, unless the :class:`.relationship` is declared with ``viewonly=True``. +Otherwise, the unit-of-work system may attempt duplicate INSERT and +DELETE statements against the underlying table. .. _declarative_synonyms: @@ -187,18 +192,25 @@ 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:: +:func:`~.synonym` with the ``descriptor`` argument. Here we present +using Python 2.6 style properties:: class MyClass(Base): __tablename__ = 'sometable' + id = Column(Integer, primary_key=True) + _attr = Column('attr', String) - def _get_attr(self): - return self._some_attr - def _set_attr(self, attr): - self._some_attr = attr - attr = synonym('_attr', descriptor=property(_get_attr, _set_attr)) + @property + def attr(self): + return self._attr + + @attr.setter + def attr(self, attr): + self._attr = attr + + attr = synonym('_attr', descriptor=attr) The above synonym is then usable as an instance attribute as well as a class-level expression construct:: @@ -212,16 +224,17 @@ conjunction with ``@property``:: class MyClass(Base): __tablename__ = 'sometable' - + + id = Column(Integer, primary_key=True) _attr = Column('attr', String) @synonym_for('_attr') @property def attr(self): - return self._some_attr + return self._attr Similarly, :func:`comparable_using` is a front end for the -:func:`~sqlalchemy.orm.comparable_property` ORM function:: +:func:`~.comparable_property` ORM function:: class MyClass(Base): __tablename__ = 'sometable' @@ -233,6 +246,61 @@ Similarly, :func:`comparable_using` is a front end for the def uc_name(self): return self.name.upper() +.. _declarative_sql_expressions: + +Defining SQL Expressions +======================== + +The usage of :func:`.column_property` with Declarative is +pretty much the same as that described in +:ref:`mapper_sql_expressions`. Local columns within the same +class declaration can be referenced directly:: + + class User(Base): + __tablename__ = 'user' + id = Column(Integer, primary_key=True) + firstname = Column(String) + lastname = Column(String) + fullname = column_property( + firstname + " " + lastname + ) + +Correlated subqueries reference the :class:`Column` objects they +need either from the local class definition or from remote +classes:: + + from sqlalchemy.sql import func + + class Address(Base): + __tablename__ = 'address' + + id = Column('id', Integer, primary_key=True) + user_id = Column(Integer, ForeignKey('user.id')) + + class User(Base): + __tablename__ = 'user' + + id = Column(Integer, primary_key=True) + name = Column(String) + + address_count = column_property( + select([func.count(Address.id)]).\\ + where(Address.user_id==id) + ) + +In the case that the ``address_count`` attribute above doesn't have access to +``Address`` when ``User`` is defined, the ``address_count`` attribute should +be added to ``User`` when both ``User`` and ``Address`` are available (i.e. +there is no string based "late compilation" feature like there is with +:func:`.relationship` at this time). Note we reference the ``id`` column +attribute of ``User`` with its class when we are no longer in the declaration +of the ``User`` class:: + + User.address_count = column_property( + select([func.count(Address.id)]).\\ + where(Address.user_id==User.id) + ) + Table Configuration =================== @@ -263,6 +331,9 @@ arguments to be specified as well (usually constraints):: Note that the keyword parameters dictionary is required in the tuple form even if empty. +Using a Hybrid Approach with __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 @@ -275,23 +346,48 @@ to a table:: Column('name', String(50)) ) +``__table__`` provides a more focused point of control for establishing +table metadata, while still getting most of the benefits of using declarative. +An application that uses reflection might want to load table metadata elsewhere +and simply pass it to declarative classes:: + + from sqlalchemy.ext.declarative import declarative_base + + Base = declarative_base() + Base.metadata.reflect(some_engine) + + class User(Base): + __table__ = metadata['user'] + + class Address(Base): + __table__ = metadata['address'] + +Some configuration schemes may find it more appropriate to use ``__table__``, +such as those which already take advantage of the data-driven nature of +:class:`.Table` to customize and/or automate schema definition. + Mapper Configuration ==================== -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:: +Declarative makes use of the :func:`~.orm.mapper` function internally +when it creates the mapping to the declared table. The options +for :func:`~.orm.mapper` are passed directly through via the ``__mapper_args__`` +class attribute. As always, arguments which reference locally +mapped columns can reference them directly from within the +class declaration:: + from datetime import datetime + class Widget(Base): __tablename__ = 'widgets' + id = Column(Integer, primary_key=True) + timestamp = Column(DateTime, nullable=False) - __mapper_args__ = {'extension': MyWidgetExtension()} + __mapper_args__ = { + 'version_id_col': timestamp, + 'version_id_generator': lambda v:datetime.now() + } Inheritance Configuration ========================= @@ -531,12 +627,12 @@ Mixing in Relationships ~~~~~~~~~~~~~~~~~~~~~~~ Relationships created by :func:`~sqlalchemy.orm.relationship` are provided -exclusively using the :func:`~sqlalchemy.util.classproperty` approach, -eliminating any ambiguity which could arise when copying a relationship -and its possibly column-bound contents. Below is an example which -combines a foreign key column and a relationship so that two classes -``Foo`` and ``Bar`` can both be configured to reference a common -target class via many-to-one:: +with declarative mixin classes exclusively using the +:func:`~sqlalchemy.util.classproperty` approach, eliminating any ambiguity +which could arise when copying a relationship and its possibly column-bound +contents. Below is an example which combines a foreign key column and a +relationship so that two classes ``Foo`` and ``Bar`` can both be configured to +reference a common target class via many-to-one:: class RefTargetMixin(object): @classproperty @@ -586,9 +682,9 @@ Mixing in deferred(), column_property(), etc. Like :func:`~sqlalchemy.orm.relationship`, all :class:`~sqlalchemy.orm.interfaces.MapperProperty` subclasses such as :func:`~sqlalchemy.orm.deferred`, :func:`~sqlalchemy.orm.column_property`, -etc. ultimately involve references to columns, and therefore have the -:func:`~sqlalchemy.util.classproperty` requirement so that no reliance on -copying is needed:: +etc. ultimately involve references to columns, and therefore, when +used with declarative mixins, have the :func:`~sqlalchemy.util.classproperty` +requirement so that no reliance on copying is needed:: class SomethingMixin(object): @@ -607,7 +703,8 @@ Controlling table inheritance with mixins ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The ``__tablename__`` attribute in conjunction with the hierarchy of -the classes involved controls what type of table inheritance, if any, +classes involved in a declarative mixin scenario controls what type of +table inheritance, if any, is configured by the declarative extension. If the ``__tablename__`` is computed by a mixin, you may need to @@ -700,12 +797,13 @@ classes:: Combining Table/Mapper Arguments from Multiple Mixins ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -In the case of ``__table_args__`` or ``__mapper_args__``, you may want -to combine some parameters from several mixins with those you wish to -define on the class iteself. The -:func:`~sqlalchemy.util.classproperty` decorator can be used here -to create user-defined collation routines that pull from multiple -collections:: +In the case of ``__table_args__`` or ``__mapper_args__`` +specified with declarative mixins, you may want to combine +some parameters from several mixins with those you wish to +define on the class iteself. The +:func:`~sqlalchemy.util.classproperty` decorator can be used +here to create user-defined collation routines that pull +from multiple collections:: from sqlalchemy.util import classproperty @@ -866,8 +964,12 @@ def _as_declarative(cls, classname, dict_): cls._decl_class_registry[classname] = cls our_stuff = util.OrderedDict() + for k in dict_: value = dict_[k] + if isinstance(value, util.classproperty): + 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 " @@ -933,7 +1035,7 @@ def _as_declarative(cls, classname, dict_): if not table.c.contains_column(c): raise exceptions.ArgumentError( "Can't add additional column %r when " - "specifying __table__" % key + "specifying __table__" % c.key ) if 'inherits' not in mapper_args: diff --git a/lib/sqlalchemy/orm/__init__.py b/lib/sqlalchemy/orm/__init__.py index 77281ee56..2004dccd1 100644 --- a/lib/sqlalchemy/orm/__init__.py +++ b/lib/sqlalchemy/orm/__init__.py @@ -1,5 +1,6 @@ # sqlalchemy/orm/__init__.py -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010 Michael Bayer mike_mp@zzzcomputing.com +# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010 Michael Bayer +# mike_mp@zzzcomputing.com # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php @@ -51,11 +52,11 @@ from sqlalchemy.orm import strategies from sqlalchemy.orm.query import AliasOption, Query from sqlalchemy.sql import util as sql_util from sqlalchemy.orm.session import Session as _Session -from sqlalchemy.orm.session import object_session, sessionmaker, make_transient +from sqlalchemy.orm.session import object_session, sessionmaker, \ + make_transient from sqlalchemy.orm.scoping import ScopedSession from sqlalchemy import util as sa_util - __all__ = ( 'EXT_CONTINUE', 'EXT_STOP', @@ -174,10 +175,11 @@ def relationship(argument, secondary=None, **kwargs): """Provide a relationship of a primary Mapper to a secondary Mapper. .. note:: This function is known as :func:`relation` in all versions - of SQLAlchemy prior to version 0.6beta2, including the 0.5 and 0.4 series. - :func:`~sqlalchemy.orm.relationship()` is only available starting with - SQLAlchemy 0.6beta2. The :func:`relation` name will remain available for - the foreseeable future in order to enable cross-compatibility. + of SQLAlchemy prior to version 0.6beta2, including the 0.5 and 0.4 + series. :func:`~sqlalchemy.orm.relationship()` is only available + starting with SQLAlchemy 0.6beta2. The :func:`relation` name will + remain available for the foreseeable future in order to enable + cross-compatibility. This corresponds to a parent-child or associative table relationship. The constructed class is an instance of :class:`RelationshipProperty`. @@ -223,7 +225,7 @@ def relationship(argument, secondary=None, **kwargs): Available cascades are: - * ``save-update`` - cascade the :meth:`~sqlalchemy.orm.session.Session.add` + * ``save-update`` - cascade the :meth:`.Session.add` operation. This cascade applies both to future and past calls to :meth:`~sqlalchemy.orm.session.Session.add`, meaning new items added to a collection or scalar relationship @@ -234,10 +236,10 @@ def relationship(argument, secondary=None, **kwargs): * ``merge`` - cascade the :meth:`~sqlalchemy.orm.session.Session.merge` operation - * ``expunge`` - cascade the :meth:`~sqlalchemy.orm.session.Session.expunge` + * ``expunge`` - cascade the :meth:`.Session.expunge` operation - * ``delete`` - cascade the :meth:`~sqlalchemy.orm.session.Session.delete` + * ``delete`` - cascade the :meth:`.Session.delete` operation * ``delete-orphan`` - if an item of the child's type with no @@ -245,7 +247,7 @@ def relationship(argument, secondary=None, **kwargs): option prevents a pending item of the child's class from being persisted without a parent present. - * ``refresh-expire`` - cascade the :meth:`~sqlalchemy.orm.session.Session.expire` + * ``refresh-expire`` - cascade the :meth:`.Session.expire` and :meth:`~sqlalchemy.orm.session.Session.refresh` operations * ``all`` - shorthand for "save-update,merge, refresh-expire, @@ -501,7 +503,8 @@ def dynamic_loader(argument, secondary=None, primaryjoin=None, return RelationshipProperty( argument, secondary=secondary, primaryjoin=primaryjoin, - secondaryjoin=secondaryjoin, foreign_keys=foreign_keys, backref=backref, + secondaryjoin=secondaryjoin, foreign_keys=foreign_keys, + backref=backref, post_update=post_update, cascade=cascade, remote_side=remote_side, enable_typechecks=enable_typechecks, passive_deletes=passive_deletes, order_by=order_by, comparator_factory=comparator_factory,doc=doc, @@ -522,9 +525,9 @@ def column_property(*args, **kwargs): :param \*cols: list of Column objects to be mapped. - :param comparator_factory: - a class which extends :class:`sqlalchemy.orm.properties.ColumnProperty.Comparator` - which provides custom SQL clause generation for comparison operations. + :param comparator_factory: a class which extends + :class:`.ColumnProperty.Comparator` which provides custom SQL clause + generation for comparison operations. :param group: a group name for this property when marked as deferred. @@ -571,9 +574,9 @@ def composite(class_, *cols, **kwargs): load immediately, and is instead loaded when the attribute is first accessed on an instance. See also :func:`~sqlalchemy.orm.deferred`. - :param comparator_factory: - a class which extends :class:`sqlalchemy.orm.properties.CompositeProperty.Comparator` - which provides custom SQL clause generation for comparison operations. + :param comparator_factory: a class which extends + :class:`.CompositeProperty.Comparator` which provides custom SQL clause + generation for comparison operations. :param doc: optional string that will be applied as the doc on the @@ -616,140 +619,150 @@ def mapper(class_, local_table=None, *args, **params): :param class\_: The class to be mapped. - :param local_table: The table to which the class is mapped, or None if this mapper - inherits from another mapper using concrete table inheritance. - - :param always_refresh: If True, all query operations for this mapped class will overwrite all - data within object instances that already exist within the session, - erasing any in-memory changes with whatever information was loaded - from the database. Usage of this flag is highly discouraged; as an - alternative, see the method `populate_existing()` on - :class:`~sqlalchemy.orm.query.Query`. - - :param allow_null_pks: This flag is deprecated - this is stated as allow_partial_pks - which defaults to True. - - :param allow_partial_pks: Defaults to True. Indicates that a composite primary key with - some NULL values should be considered as possibly existing - within the database. This affects whether a mapper will assign - an incoming row to an existing identity, as well as if - session.merge() will check the database first for a particular - primary key value. A "partial primary key" can occur if one - has mapped to an OUTER JOIN, for example. - - :param batch: Indicates that save operations of multiple entities can be batched - together for efficiency. setting to False indicates that an instance - will be fully saved before saving the next instance, which includes - inserting/updating all table rows corresponding to the entity as well - as calling all :class:`MapperExtension` methods corresponding to the save - operation. - - :param column_prefix: A string which will be prepended to the `key` name of all Columns when - creating column-based properties from the given Table. Does not - affect explicitly specified column-based properties - - :param concrete: If True, indicates this mapper should use concrete table inheritance - with its parent mapper. - - :param exclude_properties: A list of properties not to map. Columns present in the mapped table - and present in this list will not be automatically converted into - properties. Note that neither this option nor include_properties will - allow an end-run around Python inheritance. If mapped class ``B`` - inherits from mapped class ``A``, no combination of includes or - excludes will allow ``B`` to have fewer properties than its - superclass, ``A``. - - - :param extension: A :class:`~sqlalchemy.orm.interfaces.MapperExtension` instance or list of - :class:`~sqlalchemy.orm.interfaces.MapperExtension` instances which will be applied to all - operations by this :class:`~sqlalchemy.orm.mapper.Mapper`. - - :param include_properties: An inclusive list of properties to map. Columns present in the mapped - table but not present in this list will not be automatically converted - into properties. + :param local_table: The table to which the class is mapped, or None if + this mapper inherits from another mapper using concrete table + inheritance. + + :param always_refresh: If True, all query operations for this mapped + class will overwrite all data within object instances that already + exist within the session, erasing any in-memory changes with + whatever information was loaded from the database. Usage of this + flag is highly discouraged; as an alternative, see the method + `populate_existing()` on :class:`~sqlalchemy.orm.query.Query`. + + :param allow_null_pks: This flag is deprecated - this is stated as + allow_partial_pks which defaults to True. + + :param allow_partial_pks: Defaults to True. Indicates that a + composite primary key with some NULL values should be considered as + possibly existing within the database. This affects whether a + mapper will assign an incoming row to an existing identity, as well + as if session.merge() will check the database first for a + particular primary key value. A "partial primary key" can occur if + one has mapped to an OUTER JOIN, for example. + + :param batch: Indicates that save operations of multiple entities + can be batched together for efficiency. setting to False indicates + that an instance will be fully saved before saving the next + instance, which includes inserting/updating all table rows + corresponding to the entity as well as calling all + :class:`MapperExtension` methods corresponding to the save + operation. + + :param column_prefix: A string which will be prepended to the `key` + name of all Columns when creating column-based properties from the + given Table. Does not affect explicitly specified column-based + properties + + :param concrete: If True, indicates this mapper should use concrete + table inheritance with its parent mapper. + + :param exclude_properties: A list of properties not to map. Columns + present in the mapped table and present in this list will not be + automatically converted into properties. Note that neither this + option nor include_properties will allow an end-run around Python + inheritance. If mapped class ``B`` inherits from mapped class + ``A``, no combination of includes or excludes will allow ``B`` to + have fewer properties than its superclass, ``A``. + + :param extension: A :class:`.MapperExtension` instance or + list of :class:`~sqlalchemy.orm.interfaces.MapperExtension` + instances which will be applied to all operations by this + :class:`~sqlalchemy.orm.mapper.Mapper`. + + :param include_properties: An inclusive list of properties to map. + Columns present in the mapped table but not present in this list + will not be automatically converted into properties. :param inherits: Another :class:`~sqlalchemy.orm.Mapper` for which this :class:`~sqlalchemy.orm.Mapper` will have an inheritance relationship with. - - :param inherit_condition: For joined table inheritance, a SQL expression (constructed - :class:`~sqlalchemy.expression.sql.ClauseElement`) which will define how the two tables are joined; - defaults to a natural join between the two tables. - - :param inherit_foreign_keys: When inherit_condition is used and the condition contains no - ForeignKey columns, specify the "foreign" columns of the join - condition in this list. else leave as None. - - :param non_primary: Construct a :class:`Mapper` that will define only the selection of - instances, not their persistence. Any number of non_primary mappers - may be created for a particular class. - - :param order_by: A single :class:`Column` or list of :class:`Column` objects for which - selection operations should use as the default ordering for entities. - Defaults to the OID/ROWID of the table if any, or the first primary - key column of the table. - - :param passive_updates: Indicates UPDATE behavior of foreign keys when a primary key changes - on a joined-table inheritance or other joined table mapping. - + :param inherit_condition: For joined table inheritance, a SQL + expression (constructed + :class:`~sqlalchemy.expression.sql.ClauseElement`) which will + define how the two tables are joined; defaults to a natural join + between the two tables. + + :param inherit_foreign_keys: When inherit_condition is used and the + condition contains no ForeignKey columns, specify the "foreign" + columns of the join condition in this list. else leave as None. + + :param non_primary: Construct a :class:`Mapper` that will define only + the selection of instances, not their persistence. Any number of + non_primary mappers may be created for a particular class. + + :param order_by: A single :class:`Column` or list of :class:`Column` + objects for which selection operations should use as the default + ordering for entities. Defaults to the OID/ROWID of the table if + any, or the first primary key column of the table. + + :param passive_updates: Indicates UPDATE behavior of foreign keys + when a primary key changes on a joined-table inheritance or other + joined table mapping. + When True, it is assumed that ON UPDATE CASCADE is configured on - the foreign key in the database, and that the database will - handle propagation of an UPDATE from a source column to - dependent rows. Note that with databases which enforce - referential integrity (i.e. PostgreSQL, MySQL with InnoDB tables), - ON UPDATE CASCADE is required for this operation. The - relationship() will update the value of the attribute on related - items which are locally present in the session during a flush. - + the foreign key in the database, and that the database will handle + propagation of an UPDATE from a source column to dependent rows. + Note that with databases which enforce referential integrity (i.e. + PostgreSQL, MySQL with InnoDB tables), ON UPDATE CASCADE is + required for this operation. The relationship() will update the + value of the attribute on related items which are locally present + in the session during a flush. + When False, it is assumed that the database does not enforce - referential integrity and will not be issuing its own CASCADE - operation for an update. The relationship() will issue the - appropriate UPDATE statements to the database in response to the - change of a referenced key, and items locally present in the - session during a flush will also be refreshed. - + referential integrity and will not be issuing its own CASCADE + operation for an update. The relationship() will issue the + appropriate UPDATE statements to the database in response to the + change of a referenced key, and items locally present in the + session during a flush will also be refreshed. + This flag should probably be set to False if primary key changes - are expected and the database in use doesn't support CASCADE - (i.e. SQLite, MySQL MyISAM tables). - + are expected and the database in use doesn't support CASCADE (i.e. + SQLite, MySQL MyISAM tables). + Also see the passive_updates flag on :func:`relationship()`. - + A future SQLAlchemy release will provide a "detect" feature for - this flag. - - :param polymorphic_on: Used with mappers in an inheritance relationship, a ``Column`` which - will identify the class/mapper combination to be used with a - particular row. Requires the ``polymorphic_identity`` value to be set - for all mappers in the inheritance hierarchy. The column specified by - ``polymorphic_on`` is usually a column that resides directly within - the base mapper's mapped table; alternatively, it may be a column that - is only present within the <selectable> portion of the - ``with_polymorphic`` argument. - - :param polymorphic_identity: A value which will be stored in the Column denoted by polymorphic_on, - corresponding to the *class identity* of this mapper. - - :param properties: A dictionary mapping the string names of object attributes to - ``MapperProperty`` instances, which define the persistence behavior of - that attribute. Note that the columns in the mapped table are - automatically converted into ``ColumnProperty`` instances based on the - `key` property of each ``Column`` (although they can be overridden - using this dictionary). - - :param primary_key: A list of ``Column`` objects which define the *primary key* to be used - against this mapper's selectable unit. This is normally simply the - primary key of the `local_table`, but can be overridden here. - - :param version_id_col: A ``Column`` which must have an integer type that will be used to keep - a running *version id* of mapped entities in the database. this is - used during save operations to ensure that no other thread or process - has updated the instance during the lifetime of the entity, else a - :class:`StaleDataError` exception is thrown. - - :param version_id_generator: A callable which defines the algorithm used to generate new version - ids. Defaults to an integer generator. Can be replaced with one that - generates timestamps, uuids, etc. e.g.:: + this flag. + + :param polymorphic_on: Used with mappers in an inheritance + relationship, a ``Column`` which will identify the class/mapper + combination to be used with a particular row. Requires the + ``polymorphic_identity`` value to be set for all mappers in the + inheritance hierarchy. The column specified by ``polymorphic_on`` + is usually a column that resides directly within the base mapper's + mapped table; alternatively, it may be a column that is only + present within the <selectable> portion of the ``with_polymorphic`` + argument. + + :param polymorphic_identity: A value which will be stored in the + Column denoted by polymorphic_on, corresponding to the *class + identity* of this mapper. + + :param properties: A dictionary mapping the string names of object + attributes to ``MapperProperty`` instances, which define the + persistence behavior of that attribute. Note that the columns in + the mapped table are automatically converted into + ``ColumnProperty`` instances based on the `key` property of each + ``Column`` (although they can be overridden using this dictionary). + + :param primary_key: A list of ``Column`` objects which define the + *primary key* to be used against this mapper's selectable unit. + This is normally simply the primary key of the `local_table`, but + can be overridden here. + + :param version_id_col: A ``Column`` which must have an integer type + that will be used to keep a running *version id* of mapped entities + in the database. this is used during save operations to ensure that + no other thread or process has updated the instance during the + lifetime of the entity, else a :class:`StaleDataError` exception is + thrown. + + :param version_id_generator: A callable which defines the algorithm + used to generate new version ids. Defaults to an integer + generator. Can be replaced with one that generates timestamps, + uuids, etc. e.g.:: import uuid @@ -761,29 +774,31 @@ def mapper(class_, local_table=None, *args, **params): The callable receives the current version identifier as its single argument. - :param with_polymorphic: A tuple in the form ``(<classes>, <selectable>)`` indicating the - default style of "polymorphic" loading, that is, which tables are - queried at once. <classes> is any single or list of mappers and/or - classes indicating the inherited classes that should be loaded at - once. The special value ``'*'`` may be used to indicate all descending - classes should be loaded immediately. The second tuple argument - <selectable> indicates a selectable that will be used to query for - multiple classes. Normally, it is left as None, in which case this - mapper will form an outer join from the base mapper's table to that of - all desired sub-mappers. When specified, it provides the selectable - to be used for polymorphic loading. When with_polymorphic includes - mappers which load from a "concrete" inheriting table, the - <selectable> argument is required, since it usually requires more - complex UNION queries. + :param with_polymorphic: A tuple in the form ``(<classes>, + <selectable>)`` indicating the default style of "polymorphic" + loading, that is, which tables are queried at once. <classes> is + any single or list of mappers and/or classes indicating the + inherited classes that should be loaded at once. The special value + ``'*'`` may be used to indicate all descending classes should be + loaded immediately. The second tuple argument <selectable> + indicates a selectable that will be used to query for multiple + classes. Normally, it is left as None, in which case this mapper + will form an outer join from the base mapper's table to that of + all desired sub-mappers. When specified, it provides the + selectable to be used for polymorphic loading. When + with_polymorphic includes mappers which load from a "concrete" + inheriting table, the <selectable> argument is required, since it + usually requires more complex UNION queries. - """ return Mapper(class_, local_table, *args, **params) -def synonym(name, map_column=False, descriptor=None, comparator_factory=None, doc=None): +def synonym(name, map_column=False, descriptor=None, + comparator_factory=None, doc=None): """Set up `name` as a synonym to another mapped property. - Used with the ``properties`` dictionary sent to :func:`~sqlalchemy.orm.mapper`. + Used with the ``properties`` dictionary sent to + :func:`~sqlalchemy.orm.mapper`. Any existing attributes on the class which map the key name sent to the ``properties`` dictionary will be used by the synonym to provide @@ -848,12 +863,13 @@ def comparable_property(comparator_factory, descriptor=None): mapper(MyClass, mytable, properties={ 'myprop': comparable_property(MyComparator)}) - Used with the ``properties`` dictionary sent to :func:`~sqlalchemy.orm.mapper`. + Used with the ``properties`` dictionary sent to + :func:`~sqlalchemy.orm.mapper`. Note that :func:`comparable_property` is usually not needed for basic - needs. The recipe at :mod:`.derived_attributes` offers a simpler pure-Python - method of achieving a similar result using class-bound attributes with - SQLAlchemy expression constructs. + needs. The recipe at :mod:`.derived_attributes` offers a simpler + pure-Python method of achieving a similar result using class-bound + attributes with SQLAlchemy expression constructs. :param comparator_factory: A PropComparator subclass or factory that defines operator behavior @@ -913,9 +929,9 @@ def joinedload(*keys, **kw): name into an joined eager load. .. note:: This function is known as :func:`eagerload` in all versions - of SQLAlchemy prior to version 0.6beta3, including the 0.5 and 0.4 series. - :func:`eagerload` will remain available for - the foreseeable future in order to enable cross-compatibility. + of SQLAlchemy prior to version 0.6beta3, including the 0.5 and 0.4 + series. :func:`eagerload` will remain available for the foreseeable + future in order to enable cross-compatibility. Used with :meth:`~sqlalchemy.orm.query.Query.options`. @@ -937,10 +953,11 @@ def joinedload(*keys, **kw): query(Order).options(joinedload(Order.user, innerjoin=True)) - Note that the join created by :func:`joinedload` is aliased such that - no other aspects of the query will affect what it loads. To use joined eager - loading with a join that is constructed manually using :meth:`~sqlalchemy.orm.query.Query.join` - or :func:`~sqlalchemy.orm.join`, see :func:`contains_eager`. + Note that the join created by :func:`joinedload` is aliased such that no + other aspects of the query will affect what it loads. To use joined eager + loading with a join that is constructed manually using + :meth:`~sqlalchemy.orm.query.Query.join` or :func:`~sqlalchemy.orm.join`, + see :func:`contains_eager`. See also: :func:`subqueryload`, :func:`lazyload` @@ -960,9 +977,9 @@ def joinedload_all(*keys, **kw): given dot-separated path into an joined eager load. .. note:: This function is known as :func:`eagerload_all` in all versions - of SQLAlchemy prior to version 0.6beta3, including the 0.5 and 0.4 series. - :func:`eagerload_all` will remain available for - the foreseeable future in order to enable cross-compatibility. + of SQLAlchemy prior to version 0.6beta3, including the 0.5 and 0.4 + series. :func:`eagerload_all` will remain available for the + foreseeable future in order to enable cross-compatibility. Used with :meth:`~sqlalchemy.orm.query.Query.options`. @@ -977,8 +994,9 @@ def joinedload_all(*keys, **kw): query.options(joinedload_all(User.orders, Order.items, Item.keywords)) - The keyword arguments accept a flag `innerjoin=True|False` which will - override the value of the `innerjoin` flag specified on the relationship(). + The keyword arguments accept a flag `innerjoin=True|False` which will + override the value of the `innerjoin` flag specified on the + relationship(). See also: :func:`subqueryload_all`, :func:`lazyload` @@ -1043,7 +1061,8 @@ def subqueryload_all(*keys): Individual descriptors are accepted as arguments as well:: - query.options(subqueryload_all(User.orders, Order.items, Item.keywords)) + query.options(subqueryload_all(User.orders, Order.items, + Item.keywords)) See also: :func:`joinedload_all`, :func:`lazyload` @@ -1102,10 +1121,10 @@ def contains_eager(*keys, **kwargs): ``User`` entity, and the returned ``Order`` objects would have the ``Order.user`` attribute pre-populated. - :func:`contains_eager` also accepts an `alias` argument, which - is the string name of an alias, an :func:`~sqlalchemy.sql.expression.alias` - construct, or an :func:`~sqlalchemy.orm.aliased` construct. Use this - when the eagerly-loaded rows are to come from an aliased table:: + :func:`contains_eager` also accepts an `alias` argument, which is the + string name of an alias, an :func:`~sqlalchemy.sql.expression.alias` + construct, or an :func:`~sqlalchemy.orm.aliased` construct. Use this when + the eagerly-loaded rows are to come from an aliased table:: user_alias = aliased(User) sess.query(Order).\\ @@ -1118,12 +1137,11 @@ def contains_eager(*keys, **kwargs): """ alias = kwargs.pop('alias', None) if kwargs: - raise exceptions.ArgumentError("Invalid kwargs for contains_eager: %r" % kwargs.keys()) - - return ( - strategies.EagerLazyOption(keys, lazy='joined', propagate_to_loaders=False), - strategies.LoadEagerFromAliasOption(keys, alias=alias) - ) + raise exceptions.ArgumentError('Invalid kwargs for contains_eag' + 'er: %r' % kwargs.keys()) + return strategies.EagerLazyOption(keys, lazy='joined', + propagate_to_loaders=False), \ + strategies.LoadEagerFromAliasOption(keys, alias=alias) @sa_util.accepts_a_list_as_starargs(list_deprecation='deprecated') def defer(*keys): diff --git a/lib/sqlalchemy/orm/attributes.py b/lib/sqlalchemy/orm/attributes.py index 2701fc879..a09463a4f 100644 --- a/lib/sqlalchemy/orm/attributes.py +++ b/lib/sqlalchemy/orm/attributes.py @@ -484,6 +484,7 @@ class ScalarAttributeImpl(AttributeImpl): accepts_scalar_loader = True uses_objects = False + supports_population = True def delete(self, state, dict_): @@ -538,6 +539,7 @@ class MutableScalarAttributeImpl(ScalarAttributeImpl): """ uses_objects = False + supports_population = True def __init__(self, class_, key, callable_, events, class_manager, copy_function=None, @@ -604,6 +606,7 @@ class ScalarObjectAttributeImpl(ScalarAttributeImpl): accepts_scalar_loader = False uses_objects = True + supports_population = True def __init__(self, class_, key, callable_, events, trackparent=False, extension=None, copy_function=None, @@ -694,6 +697,7 @@ class CollectionAttributeImpl(AttributeImpl): """ accepts_scalar_loader = False uses_objects = True + supports_population = True def __init__(self, class_, key, callable_, events, typecallable=None, trackparent=False, extension=None, diff --git a/lib/sqlalchemy/orm/collections.py b/lib/sqlalchemy/orm/collections.py index 0ea17cd8b..b5c4353b3 100644 --- a/lib/sqlalchemy/orm/collections.py +++ b/lib/sqlalchemy/orm/collections.py @@ -129,7 +129,7 @@ def column_mapped_collection(mapping_spec): from sqlalchemy.orm.util import _state_mapper from sqlalchemy.orm.attributes import instance_state - cols = [expression._no_literals(q) for q in util.to_list(mapping_spec)] + cols = [expression._only_column_elements(q) for q in util.to_list(mapping_spec)] if len(cols) == 1: def keyfunc(value): state = instance_state(value) diff --git a/lib/sqlalchemy/orm/dynamic.py b/lib/sqlalchemy/orm/dynamic.py index d0aa2c20b..d55838011 100644 --- a/lib/sqlalchemy/orm/dynamic.py +++ b/lib/sqlalchemy/orm/dynamic.py @@ -43,10 +43,12 @@ log.class_logger(DynaLoader) class DynamicAttributeImpl(attributes.AttributeImpl): uses_objects = True accepts_scalar_loader = False - + supports_population = False + def __init__(self, class_, key, typecallable, target_mapper, order_by, query_class=None, **kwargs): - super(DynamicAttributeImpl, self).__init__(class_, key, typecallable, **kwargs) + super(DynamicAttributeImpl, self).\ + __init__(class_, key, typecallable, **kwargs) self.target_mapper = target_mapper self.order_by = order_by if not query_class: @@ -58,15 +60,18 @@ class DynamicAttributeImpl(attributes.AttributeImpl): def get(self, state, dict_, passive=False): if passive: - return self._get_collection_history(state, passive=True).added_items + return self._get_collection_history(state, + passive=True).added_items else: return self.query_class(self, state) def get_collection(self, state, dict_, user_data=None, passive=True): if passive: - return self._get_collection_history(state, passive=passive).added_items + return self._get_collection_history(state, + passive=passive).added_items else: - history = self._get_collection_history(state, passive=passive) + history = self._get_collection_history(state, + passive=passive) return history.added_items + history.unchanged_items def fire_append_event(self, state, dict_, value, initiator): @@ -105,30 +110,36 @@ class DynamicAttributeImpl(attributes.AttributeImpl): dict_[self.key] = True return state.committed_state[self.key] - def set(self, state, dict_, value, initiator, passive=attributes.PASSIVE_OFF): + def set(self, state, dict_, value, initiator, + passive=attributes.PASSIVE_OFF): if initiator is self: return self._set_iterable(state, dict_, value) def _set_iterable(self, state, dict_, iterable, adapter=None): - collection_history = self._modified_event(state, dict_) new_values = list(iterable) - if state.has_identity: old_collection = list(self.get(state, dict_)) else: old_collection = [] - - collections.bulk_replace(new_values, DynCollectionAdapter(self, state, old_collection), DynCollectionAdapter(self, state, new_values)) + collections.bulk_replace(new_values, DynCollectionAdapter(self, + state, old_collection), + DynCollectionAdapter(self, state, + new_values)) def delete(self, *args, **kwargs): raise NotImplementedError() + def set_committed_value(self, state, dict_, value): + raise NotImplementedError("Dynamic attributes don't support " + "collection population.") + def get_history(self, state, dict_, passive=False): c = self._get_collection_history(state, passive) - return attributes.History(c.added_items, c.unchanged_items, c.deleted_items) + return attributes.History(c.added_items, c.unchanged_items, + c.deleted_items) def _get_collection_history(self, state, passive=False): if self.key in state.committed_state: @@ -193,7 +204,8 @@ class AppenderMixin(object): def __session(self): sess = object_session(self.instance) - if sess is not None and self.autoflush and sess.autoflush and self.instance in sess: + if sess is not None and self.autoflush and sess.autoflush \ + and self.instance in sess: sess.flush() if not has_identity(self.instance): return None @@ -283,7 +295,8 @@ class CollectionHistory(object): deleted = util.IdentitySet(apply_to.deleted_items) added = apply_to.added_items coll = AppenderQuery(attr, state).autoflush(False) - self.unchanged_items = [o for o in util.IdentitySet(coll) if o not in deleted] + self.unchanged_items = [o for o in util.IdentitySet(coll) + if o not in deleted] self.added_items = apply_to.added_items self.deleted_items = apply_to.deleted_items else: diff --git a/lib/sqlalchemy/orm/interfaces.py b/lib/sqlalchemy/orm/interfaces.py index 6ecc202b5..945b657e4 100644 --- a/lib/sqlalchemy/orm/interfaces.py +++ b/lib/sqlalchemy/orm/interfaces.py @@ -474,7 +474,7 @@ class MapperProperty(object): return iter(()) - def set_parent(self, parent): + def set_parent(self, parent, init): self.parent = parent def instrument_class(self, mapper): diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py index b566610f1..b268cf6b5 100644 --- a/lib/sqlalchemy/orm/mapper.py +++ b/lib/sqlalchemy/orm/mapper.py @@ -57,8 +57,6 @@ _COMPILE_MUTEX = util.threading.RLock() # initialize these lazily ColumnProperty = None -SynonymProperty = None -ComparableProperty = None RelationshipProperty = None ConcreteInheritedProperty = None _expire_state = None @@ -708,35 +706,11 @@ class Mapper(object): for col in col.proxy_set: self._columntoproperty[col] = prop - elif isinstance(prop, (ComparableProperty, SynonymProperty)) and \ - setparent: - if prop.descriptor is None: - desc = getattr(self.class_, key, None) - if self._is_userland_descriptor(desc): - prop.descriptor = desc - if getattr(prop, 'map_column', False): - if key not in self.mapped_table.c: - raise sa_exc.ArgumentError( - "Can't compile synonym '%s': no column on table " - "'%s' named '%s'" - % (prop.name, self.mapped_table.description, key)) - elif self.mapped_table.c[key] in self._columntoproperty and \ - self._columntoproperty[ - self.mapped_table.c[key] - ].key == prop.name: - raise sa_exc.ArgumentError( - "Can't call map_column=True for synonym %r=%r, " - "a ColumnProperty already exists keyed to the name " - "%r for column %r" % - (key, prop.name, prop.name, key) - ) - p = ColumnProperty(self.mapped_table.c[key]) - self._configure_property( - prop.name, p, - init=init, - setparent=setparent) - p._mapped_by_synonym = key - + prop.key = key + + if setparent: + prop.set_parent(self, init) + if key in self._props and \ getattr(self._props[key], '_mapped_by_synonym', False): syn = self._props[key]._mapped_by_synonym @@ -747,10 +721,6 @@ class Mapper(object): ) self._props[key] = prop - prop.key = key - - if setparent: - prop.set_parent(self) if not self.non_primary: prop.instrument_class(self) @@ -904,26 +874,36 @@ class Mapper(object): def has_property(self, key): return key in self._props - def get_property(self, key, resolve_synonyms=False, raiseerr=True): - """return a MapperProperty associated with the given key.""" + def get_property(self, key, + resolve_synonyms=False, + raiseerr=True, _compile_mappers=True): + + """return a :class:`.MapperProperty` associated with the given key. + + resolve_synonyms=False and raiseerr=False are deprecated. + + """ - if not self.compiled: + if _compile_mappers and not self.compiled: self.compile() - return self._get_property(key, - resolve_synonyms=resolve_synonyms, - raiseerr=raiseerr) - - def _get_property(self, key, resolve_synonyms=False, raiseerr=True): - prop = self._props.get(key, None) - if resolve_synonyms: - while isinstance(prop, SynonymProperty): - prop = self._props.get(prop.name, None) - if prop is None and raiseerr: - raise sa_exc.InvalidRequestError( - "Mapper '%s' has no property '%s'" % - (self, key)) - return prop - + + if not resolve_synonyms: + prop = self._props.get(key, None) + if prop is None and raiseerr: + raise sa_exc.InvalidRequestError( + "Mapper '%s' has no property '%s'" % + (self, key)) + return prop + else: + try: + return getattr(self.class_, key).property + except AttributeError: + if raiseerr: + raise sa_exc.InvalidRequestError( + "Mapper '%s' has no property '%s'" % (self, key)) + else: + return None + @property def iterate_properties(self): """return an iterator of all MapperProperty objects.""" diff --git a/lib/sqlalchemy/orm/properties.py b/lib/sqlalchemy/orm/properties.py index cf5f31162..cbfba91f3 100644 --- a/lib/sqlalchemy/orm/properties.py +++ b/lib/sqlalchemy/orm/properties.py @@ -237,7 +237,22 @@ class CompositeProperty(ColumnProperty): def __str__(self): return str(self.parent.class_.__name__) + "." + self.key -class ConcreteInheritedProperty(MapperProperty): +class DescriptorProperty(MapperProperty): + """:class:`MapperProperty` which proxies access to a + plain descriptor.""" + + def setup(self, context, entity, path, adapter, **kwargs): + pass + + def create_row_processor(self, selectcontext, path, mapper, row, adapter): + return (None, None) + + def merge(self, session, source_state, source_dict, + dest_state, dest_dict, load, _recursive): + pass + + +class ConcreteInheritedProperty(DescriptorProperty): """A 'do nothing' :class:`MapperProperty` that disables an attribute on a concrete subclass that is only present on the inherited mapper, not the concrete classes' mapper. @@ -254,18 +269,6 @@ class ConcreteInheritedProperty(MapperProperty): """ - extension = None - - def setup(self, context, entity, path, adapter, **kwargs): - pass - - def create_row_processor(self, selectcontext, path, mapper, row, adapter): - return (None, None) - - def merge(self, session, source_state, source_dict, dest_state, - dest_dict, load, _recursive): - pass - def instrument_class(self, mapper): def warn(): raise AttributeError("Concrete %s does not implement " @@ -284,7 +287,7 @@ class ConcreteInheritedProperty(MapperProperty): comparator_callable = None # TODO: put this process into a deferred callable? for m in self.parent.iterate_to_root(): - p = m._get_property(self.key) + p = m.get_property(self.key, _compile_mappers=False) if not isinstance(p, ConcreteInheritedProperty): comparator_callable = p.comparator_factory break @@ -299,9 +302,7 @@ class ConcreteInheritedProperty(MapperProperty): ) -class SynonymProperty(MapperProperty): - - extension = None +class SynonymProperty(DescriptorProperty): def __init__(self, name, map_column=None, descriptor=None, comparator_factory=None, @@ -313,14 +314,40 @@ class SynonymProperty(MapperProperty): self.doc = doc or (descriptor and descriptor.__doc__) or None util.set_creation_order(self) - def setup(self, context, entity, path, adapter, **kwargs): - pass + def set_parent(self, parent, init): + if self.map_column: + # implement the 'map_column' option. + if self.key not in parent.mapped_table.c: + raise sa_exc.ArgumentError( + "Can't compile synonym '%s': no column on table " + "'%s' named '%s'" + % (self.name, parent.mapped_table.description, self.key)) + elif parent.mapped_table.c[self.key] in \ + parent._columntoproperty and \ + parent._columntoproperty[ + parent.mapped_table.c[self.key] + ].key == self.name: + raise sa_exc.ArgumentError( + "Can't call map_column=True for synonym %r=%r, " + "a ColumnProperty already exists keyed to the name " + "%r for column %r" % + (self.key, self.name, self.name, self.key) + ) + p = ColumnProperty(parent.mapped_table.c[self.key]) + parent._configure_property( + self.name, p, + init=init, + setparent=True) + p._mapped_by_synonym = self.key - def create_row_processor(self, selectcontext, path, mapper, row, adapter): - return (None, None) + self.parent = parent def instrument_class(self, mapper): - class_ = self.parent.class_ + + if self.descriptor is None: + desc = getattr(mapper.class_, self.key, None) + if mapper._is_userland_descriptor(desc): + self.descriptor = desc if self.descriptor is None: class SynonymProp(object): @@ -337,8 +364,9 @@ class SynonymProperty(MapperProperty): def comparator_callable(prop, mapper): def comparator(): - prop = self.parent._get_property( - self.key, resolve_synonyms=True) + prop = mapper.get_property( + self.name, resolve_synonyms=True, + _compile_mappers=False) if self.comparator_factory: return self.comparator_factory(prop, mapper) else: @@ -355,17 +383,10 @@ class SynonymProperty(MapperProperty): doc=self.doc ) - def merge(self, session, source_state, source_dict, dest_state, - dest_dict, load, _recursive): - pass -log.class_logger(SynonymProperty) - -class ComparableProperty(MapperProperty): +class ComparableProperty(DescriptorProperty): """Instruments a Python property for use in query expressions.""" - extension = None - def __init__(self, comparator_factory, descriptor=None, doc=None): self.descriptor = descriptor self.comparator_factory = comparator_factory @@ -374,6 +395,11 @@ class ComparableProperty(MapperProperty): def instrument_class(self, mapper): """Set up a proxy to the unmanaged descriptor.""" + + if self.descriptor is None: + desc = getattr(mapper.class_, self.key, None) + if mapper._is_userland_descriptor(desc): + self.descriptor = desc attributes.register_descriptor( mapper.class_, @@ -385,16 +411,6 @@ class ComparableProperty(MapperProperty): doc=self.doc, ) - def setup(self, context, entity, path, adapter, **kwargs): - pass - - def create_row_processor(self, selectcontext, path, mapper, row, adapter): - return (None, None) - - def merge(self, session, source_state, source_dict, - dest_state, dest_dict, load, _recursive): - pass - class RelationshipProperty(StrategizedProperty): """Describes an object property that holds a single item or list @@ -837,7 +853,7 @@ class RelationshipProperty(StrategizedProperty): yield (c, instance_mapper, instance_state) def _add_reverse_property(self, key): - other = self.mapper._get_property(key) + other = self.mapper.get_property(key, _compile_mappers=False) self._reverse_property.add(other) other._reverse_property.add(self) @@ -924,8 +940,7 @@ class RelationshipProperty(StrategizedProperty): if not self.parent.concrete: for inheriting in self.parent.iterate_to_root(): if inheriting is not self.parent \ - and inheriting._get_property(self.key, - raiseerr=False): + and inheriting.has_property(self.key): util.warn("Warning: relationship '%s' on mapper " "'%s' supercedes the same relationship " "on inherited mapper '%s'; this can " @@ -1216,7 +1231,7 @@ class RelationshipProperty(StrategizedProperty): def _assert_is_primary(self): if not self.is_primary() \ and not mapper.class_mapper(self.parent.class_, - compile=False)._get_property(self.key, raiseerr=False): + compile=False).has_property(self.key): raise sa_exc.ArgumentError("Attempting to assign a new " "relationship '%s' to a non-primary mapper on " "class '%s'. New relationships can only be added " @@ -1234,8 +1249,7 @@ class RelationshipProperty(StrategizedProperty): else: backref_key, kwargs = self.backref mapper = self.mapper.primary_mapper() - if mapper._get_property(backref_key, raiseerr=False) \ - is not None: + if mapper.has_property(backref_key): raise sa_exc.ArgumentError("Error creating backref " "'%s' on relationship '%s': property of that " "name exists on mapper '%s'" % (backref_key, @@ -1416,7 +1430,5 @@ PropertyLoader = RelationProperty = RelationshipProperty log.class_logger(RelationshipProperty) mapper.ColumnProperty = ColumnProperty -mapper.SynonymProperty = SynonymProperty -mapper.ComparableProperty = ComparableProperty mapper.RelationshipProperty = RelationshipProperty mapper.ConcreteInheritedProperty = ConcreteInheritedProperty diff --git a/lib/sqlalchemy/orm/query.py b/lib/sqlalchemy/orm/query.py index cc6d15a74..c269c8bb9 100644 --- a/lib/sqlalchemy/orm/query.py +++ b/lib/sqlalchemy/orm/query.py @@ -864,7 +864,7 @@ class Query(object): """apply the given filtering criterion to the query and return the newly resulting ``Query``.""" - clauses = [_entity_descriptor(self._joinpoint_zero(), key)[0] == value + clauses = [_entity_descriptor(self._joinpoint_zero(), key) == value for key, value in kwargs.iteritems()] return self.filter(sql.and_(*clauses)) @@ -1158,7 +1158,7 @@ class Query(object): if isinstance(onclause, basestring): left_entity = self._joinpoint_zero() - descriptor, prop = _entity_descriptor(left_entity, onclause) + descriptor = _entity_descriptor(left_entity, onclause) onclause = descriptor # check for q.join(Class.propname, from_joinpoint=True) @@ -1171,7 +1171,7 @@ class Query(object): _entity_info(self._joinpoint_zero()) if left_mapper is left_entity: left_entity = self._joinpoint_zero() - descriptor, prop = _entity_descriptor(left_entity, + descriptor = _entity_descriptor(left_entity, onclause.key) onclause = descriptor diff --git a/lib/sqlalchemy/orm/session.py b/lib/sqlalchemy/orm/session.py index d092375a6..8516a22de 100644 --- a/lib/sqlalchemy/orm/session.py +++ b/lib/sqlalchemy/orm/session.py @@ -235,7 +235,7 @@ class SessionTransaction(object): def _assert_is_open(self, error_msg="The transaction is closed"): if self.session is None: - raise sa_exc.InvalidRequestError(error_msg) + raise sa_exc.ResourceClosedError(error_msg) @property def _is_transaction_boundary(self): @@ -427,7 +427,8 @@ class SessionTransaction(object): return self def __exit__(self, type, value, traceback): - self._assert_is_open("Cannot end transaction context. The transaction was closed from within the context") + self._assert_is_open("Cannot end transaction context. The transaction " + "was closed from within the context") if self.session.transaction is None: return if type is None: diff --git a/lib/sqlalchemy/orm/strategies.py b/lib/sqlalchemy/orm/strategies.py index 62602ff37..1c4571aed 100644 --- a/lib/sqlalchemy/orm/strategies.py +++ b/lib/sqlalchemy/orm/strategies.py @@ -806,6 +806,12 @@ class SubqueryLoader(AbstractRelationshipLoader): ] def create_row_processor(self, context, path, mapper, row, adapter): + if not self.parent.class_manager[self.key].impl.supports_population: + raise sa_exc.InvalidRequestError( + "'%s' does not support object " + "population - eager loading cannot be applied." % + self) + path = path + (self.key,) path = interfaces._reduce_path(path) @@ -876,6 +882,7 @@ class EagerLoader(AbstractRelationshipLoader): **kwargs): """Add a left outer join to the statement thats being constructed.""" + if not context.query._enable_eagerloads: return @@ -1069,7 +1076,14 @@ class EagerLoader(AbstractRelationshipLoader): return False def create_row_processor(self, context, path, mapper, row, adapter): + if not self.parent.class_manager[self.key].impl.supports_population: + raise sa_exc.InvalidRequestError( + "'%s' does not support object " + "population - eager loading cannot be applied." % + self) + path = path + (self.key,) + eager_adapter = self._create_eager_adapter( context, diff --git a/lib/sqlalchemy/orm/util.py b/lib/sqlalchemy/orm/util.py index ef5413724..0f4adec00 100644 --- a/lib/sqlalchemy/orm/util.py +++ b/lib/sqlalchemy/orm/util.py @@ -347,9 +347,12 @@ class AliasedClass(object): return queryattr def __getattr__(self, key): - prop = self.__mapper._get_property(key, raiseerr=False) - if prop: - return self.__adapt_prop(prop) + if self.__mapper.has_property(key): + return self.__adapt_prop( + self.__mapper.get_property( + key, _compile_mappers=False + ) + ) for base in self.__target.__mro__: try: @@ -537,40 +540,22 @@ def _entity_info(entity, compile=True): return mapper, mapper._with_polymorphic_selectable, False def _entity_descriptor(entity, key): - """Return attribute/property information given an entity and string name. - - Returns a 2-tuple representing InstrumentedAttribute/MapperProperty. + """Return a class attribute given an entity and string name. + + May return :class:`.InstrumentedAttribute` or user-defined + attribute. """ - if isinstance(entity, AliasedClass): - try: - desc = getattr(entity, key) - return desc, desc.property - except AttributeError: - raise sa_exc.InvalidRequestError( - "Entity '%s' has no property '%s'" % - (entity, key) - ) - - elif isinstance(entity, type): - try: - desc = attributes.manager_of_class(entity)[key] - return desc, desc.property - except KeyError: - raise sa_exc.InvalidRequestError( - "Entity '%s' has no property '%s'" % - (entity, key) - ) - - else: - try: - desc = entity.class_manager[key] - return desc, desc.property - except KeyError: - raise sa_exc.InvalidRequestError( - "Entity '%s' has no property '%s'" % - (entity, key) - ) + if not isinstance(entity, (AliasedClass, type)): + entity = entity.class_ + + try: + return getattr(entity, key) + except AttributeError: + raise sa_exc.InvalidRequestError( + "Entity '%s' has no property '%s'" % + (entity, key) + ) def _orm_columns(entity): mapper, selectable, is_aliased_class = _entity_info(entity) diff --git a/lib/sqlalchemy/schema.py b/lib/sqlalchemy/schema.py index 75bbfd070..5f82a0200 100644 --- a/lib/sqlalchemy/schema.py +++ b/lib/sqlalchemy/schema.py @@ -806,7 +806,11 @@ class Column(SchemaItem, expression.ColumnClause): for fk in col.foreign_keys: col.foreign_keys.remove(fk) table.foreign_keys.remove(fk) - table.constraints.remove(fk.constraint) + if fk.constraint in table.constraints: + # this might have been removed + # already, if it's a composite constraint + # and more than one col being replaced + table.constraints.remove(fk.constraint) table._columns.replace(self) @@ -1019,7 +1023,20 @@ class ForeignKey(SchemaItem): return "ForeignKey(%r)" % self._get_colspec() def copy(self, schema=None): - """Produce a copy of this ForeignKey object.""" + """Produce a copy of this :class:`ForeignKey` object. + + The new :class:`ForeignKey` will not be bound + to any :class:`Column`. + + This method is usually used by the internal + copy procedures of :class:`Column`, :class:`Table`, + and :class:`MetaData`. + + :param schema: The returned :class:`ForeignKey` will + reference the original table and column name, qualified + by the given string schema name. + + """ return ForeignKey( self._get_colspec(schema=schema), @@ -1033,6 +1050,12 @@ class ForeignKey(SchemaItem): ) def _get_colspec(self, schema=None): + """Return a string based 'column specification' for this :class:`ForeignKey`. + + This is usually the equivalent of the string-based "tablename.colname" + argument first passed to the object's constructor. + + """ if schema: return schema + "." + self.column.table.name + \ "." + self.column.key @@ -1048,15 +1071,16 @@ class ForeignKey(SchemaItem): target_fullname = property(_get_colspec) def references(self, table): - """Return True if the given table is referenced by this ForeignKey.""" + """Return True if the given :class:`Table` is referenced by this :class:`ForeignKey`.""" return table.corresponding_column(self.column) is not None def get_referent(self, table): - """Return the column in the given table referenced by this ForeignKey. + """Return the :class:`.Column` in the given :class:`.Table` + referenced by this :class:`ForeignKey`. - Returns None if this ``ForeignKey`` does not reference the given - table. + Returns None if this :class:`ForeignKey` does not reference the given + :class:`Table`. """ @@ -1064,6 +1088,18 @@ class ForeignKey(SchemaItem): @util.memoized_property def column(self): + """Return the target :class:`.Column` referenced by this :class:`.ForeignKey`. + + If this :class:`ForeignKey` was created using a + string-based target column specification, this + attribute will on first access initiate a resolution + process to locate the referenced remote + :class:`.Column`. The resolution process traverses + to the parent :class:`.Column`, :class:`.Table`, and + :class:`.MetaData` to proceed - if any of these aren't + yet present, an error is raised. + + """ # ForeignKey inits its remote column as late as possible, so tables # can be defined without dependencies if isinstance(self._colspec, basestring): @@ -1108,8 +1144,9 @@ class ForeignKey(SchemaItem): if _get_table_key(tname, schema) not in parenttable.metadata: raise exc.NoReferencedTableError( - "Could not find table '%s' with which to generate a " - "foreign key" % tname) + "Foreign key assocated with column '%s' could not find " + "table '%s' with which to generate a " + "foreign key to target column '%s'" % (self.parent, tname, colname)) table = Table(tname, parenttable.metadata, mustexist=True, schema=schema) diff --git a/lib/sqlalchemy/sql/expression.py b/lib/sqlalchemy/sql/expression.py index 050b5c05b..0a5edb42f 100644 --- a/lib/sqlalchemy/sql/expression.py +++ b/lib/sqlalchemy/sql/expression.py @@ -428,8 +428,8 @@ def case(whens, value=None, else_=None): The expressions used for THEN and ELSE, when specified as strings, will be interpreted as bound values. To specify textual SQL expressions - for these, use the literal_column(<string>) or - text(<string>) construct. + for these, use the :func:`literal_column` + construct. The expressions used for the WHEN criterion may only be literal strings when "value" is @@ -1035,6 +1035,14 @@ def _no_literals(element): else: return element +def _only_column_elements(element): + if hasattr(element, '__clause_element__'): + element = element.__clause_element__() + if not isinstance(element, ColumnElement): + raise exc.ArgumentError("Column-based expression object expected; " + "got: %r" % element) + return element + def _corresponding_column_or_error(fromclause, column, require_embedded=False): c = fromclause.corresponding_column(column, require_embedded=require_embedded) @@ -1813,7 +1821,7 @@ class ColumnElement(ClauseElement, _CompareMixin): else: name = str(self) co = ColumnClause(self.anon_label, selectable, type_=getattr(self, 'type', None)) - + co.proxies = [self] selectable.columns[name] = co return co @@ -2428,7 +2436,7 @@ class _TextClause(Executable, ClauseElement): if self.typemap is not None and len(self.typemap) == 1: return list(self.typemap)[0] else: - return None + return sqltypes.NULLTYPE def self_group(self, against=None): if against is operators.in_op: @@ -3179,7 +3187,8 @@ class _Label(ColumnElement): self._element = element self._type = type_ self.quote = element.quote - + self.proxies = [element] + @util.memoized_property def type(self): return sqltypes.to_instance( @@ -3190,17 +3199,13 @@ class _Label(ColumnElement): def element(self): return self._element.self_group(against=operators.as_) - def _proxy_attr(name): - get = attrgetter(name) - def attr(self): - return get(self.element) - return property(attr) + @property + def primary_key(self): + return self.element.primary_key - proxies = _proxy_attr('proxies') - base_columns = _proxy_attr('base_columns') - proxy_set = _proxy_attr('proxy_set') - primary_key = _proxy_attr('primary_key') - foreign_keys = _proxy_attr('foreign_keys') + @property + def foreign_keys(self): + return self.element.foreign_keys def get_children(self, **kwargs): return self.element, @@ -3217,6 +3222,7 @@ class _Label(ColumnElement): e = self.element._make_proxy(selectable, name=self.name) else: e = column(self.name)._make_proxy(selectable=selectable) + e.proxies.append(self) return e |