diff options
-rw-r--r-- | lib/sqlalchemy/orm/__init__.py | 8 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/relationships.py | 63 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/session.py | 21 | ||||
-rw-r--r-- | lib/sqlalchemy/sql/util.py | 31 | ||||
-rw-r--r-- | test/orm/test_rel_fn.py | 6 | ||||
-rw-r--r-- | test/sql/test_selectable.py | 7 |
6 files changed, 67 insertions, 69 deletions
diff --git a/lib/sqlalchemy/orm/__init__.py b/lib/sqlalchemy/orm/__init__.py index ea8e5a36e..b8085ca3c 100644 --- a/lib/sqlalchemy/orm/__init__.py +++ b/lib/sqlalchemy/orm/__init__.py @@ -49,7 +49,6 @@ from .properties import ( from .relationships import ( foreign, remote, - remote_foreign ) from .session import ( Session, @@ -118,7 +117,6 @@ __all__ = ( 'relationship', 'relation', 'remote', - 'remote_foreign', 'scoped_session', 'sessionmaker', 'subqueryload', @@ -452,9 +450,9 @@ def relationship(argument, secondary=None, **kwargs): :param load_on_pending=False: Indicates loading behavior for transient or pending parent objects. - .. note:: - - load_on_pending is superseded by :meth:`.Session.enable_relationship_loading`. + .. versionchanged:: 0.8 + load_on_pending is superseded by + :meth:`.Session.enable_relationship_loading`. When set to ``True``, causes the lazy-loader to issue a query for a parent object that is not persistent, meaning it has diff --git a/lib/sqlalchemy/orm/relationships.py b/lib/sqlalchemy/orm/relationships.py index c861edf83..373fba785 100644 --- a/lib/sqlalchemy/orm/relationships.py +++ b/lib/sqlalchemy/orm/relationships.py @@ -26,74 +26,41 @@ def remote(expr): """Annotate a portion of a primaryjoin expression with a 'remote' annotation. - :func:`.remote`, :func:`.foreign`, and :func:`.remote_foreign` - are intended to be used with - :func:`.relationship` in conjunction with a - ``primaryjoin`` expression which contains - indirect equality conditions, meaning the comparison - of mapped columns involves extraneous SQL functions - such as :func:`.cast`. They can also be used in - lieu of the ``foreign_keys`` and ``remote_side`` - parameters to :func:`.relationship`, if a - primaryjoin expression is also being sent explicitly. - - Below, a mapped class ``DNSRecord`` relates to the - ``DHCPHost`` class using a primaryjoin that casts - the ``content`` column to a string. The :func:`.foreign` - and :func:`.remote` annotation functions are used - to mark with full accuracy those mapped columns that - are significant to the :func:`.relationship`, in terms - of how they are joined:: - - from sqlalchemy import cast, String - from sqlalchemy.orm import remote, foreign - from sqlalchemy.dialects.postgresql import INET - - class DNSRecord(Base): - __tablename__ = 'dns' - - id = Column(Integer, primary_key=True) - content = Column(INET) - dhcphost = relationship(DHCPHost, - primaryjoin=cast(foreign(content), String) == - remote(DHCPHost.ip_address) - ) + See the section :ref:`relationship_custom_foreign` for a + description of use. .. versionadded:: 0.8 - See also: + .. seealso:: - * :func:`.foreign` + :ref:`relationship_custom_foreign` - * :func:`.remote_foreign` + :func:`.foreign` """ - return _annotate_columns(expression._clause_element_as_expr(expr), {"remote":True}) + return _annotate_columns(expression._clause_element_as_expr(expr), + {"remote": True}) def foreign(expr): """Annotate a portion of a primaryjoin expression with a 'foreign' annotation. - See the example at :func:`.remote`. + See the section :ref:`relationship_custom_foreign` for a + description of use. .. versionadded:: 0.8 - """ - - return _annotate_columns(expression._clause_element_as_expr(expr), {"foreign":True}) + .. seealso:: -def remote_foreign(expr): - """Annotate a portion of a primaryjoin expression - with a 'remote' and 'foreign' annotation. + :ref:`relationship_custom_foreign` - See the example at :func:`.remote`. - - .. versionadded:: 0.8 + :func:`.remote` """ - return _annotate_columns(expr, {"foreign":True, - "remote":True}) + return _annotate_columns(expression._clause_element_as_expr(expr), + {"foreign": True}) + def _annotate_columns(element, annotations): def clone(elem): diff --git a/lib/sqlalchemy/orm/session.py b/lib/sqlalchemy/orm/session.py index 15a78b842..5a8b086d9 100644 --- a/lib/sqlalchemy/orm/session.py +++ b/lib/sqlalchemy/orm/session.py @@ -90,7 +90,7 @@ class SessionTransaction(object): :meth:`.Session.begin` method is called. Another detail of :class:`.SessionTransaction` behavior is that it is - capable of "nesting". This means that the :meth:`.begin` method can + capable of "nesting". This means that the :meth:`.Session.begin` method can be called while an existing :class:`.SessionTransaction` is already present, producing a new :class:`.SessionTransaction` that temporarily replaces the parent :class:`.SessionTransaction`. When a :class:`.SessionTransaction` @@ -101,8 +101,8 @@ class SessionTransaction(object): behavior is effectively a stack, where :attr:`.Session.transaction` refers to the current head of the stack. - The purpose of this stack is to allow nesting of :meth:`.rollback` or - :meth:`.commit` calls in context with various flavors of :meth:`.begin`. + The purpose of this stack is to allow nesting of :meth:`.Session.rollback` or + :meth:`.Session.commit` calls in context with various flavors of :meth:`.Session.begin`. This nesting behavior applies to when :meth:`.Session.begin_nested` is used to emit a SAVEPOINT transaction, and is also used to produce a so-called "subtransaction" which allows a block of code to use a @@ -1628,6 +1628,11 @@ class Session(_SessionClassMethods): """Associate an object with this :class:`.Session` for related object loading. + .. warning:: + + :meth:`.enable_relationship_loading` exists to serve special + use cases and is not recommended for general use. + Accesses of attributes mapped with :func:`.relationship` will attempt to load a value from the database using this :class:`.Session` as the source of connectivity. The values @@ -1636,7 +1641,7 @@ class Session(_SessionClassMethods): generally only works for many-to-one-relationships. The object will be attached to this session, but will - ''not'' participate in any persistence operations; its state + **not** participate in any persistence operations; its state for almost all purposes will remain either "transient" or "detached", except for the case of relationship loading. @@ -1988,18 +1993,18 @@ class Session(_SessionClassMethods): The "partial rollback" state refers to when an "inner" transaction, typically used during a flush, encounters an error and emits a rollback of the DBAPI connection. At this point, the :class:`.Session` - is in "partial rollback" and awaits for the user to call :meth:`.rollback`, + is in "partial rollback" and awaits for the user to call :meth:`.Session.rollback`, in order to close out the transaction stack. It is in this "partial rollback" period that the :attr:`.is_active` flag returns False. After - the call to :meth:`.rollback`, the :class:`.SessionTransaction` is replaced + the call to :meth:`.Session.rollback`, the :class:`.SessionTransaction` is replaced with a new one and :attr:`.is_active` returns ``True`` again. When a :class:`.Session` is used in ``autocommit=True`` mode, the :class:`.SessionTransaction` is only instantiated within the scope of a flush call, or when :meth:`.Session.begin` is called. So :attr:`.is_active` will always be ``False`` outside of a flush or - :meth:`.begin` block in this mode, and will be ``True`` within the - :meth:`.begin` block as long as it doesn't enter "partial rollback" + :meth:`.Session.begin` block in this mode, and will be ``True`` within the + :meth:`.Session.begin` block as long as it doesn't enter "partial rollback" state. From all the above, it follows that the only purpose to this flag is diff --git a/lib/sqlalchemy/sql/util.py b/lib/sqlalchemy/sql/util.py index 28c13398f..2c0769012 100644 --- a/lib/sqlalchemy/sql/util.py +++ b/lib/sqlalchemy/sql/util.py @@ -411,7 +411,7 @@ class Annotated(object): except KeyError: cls = annotated_classes[element.__class__] = type.__new__(type, "Annotated%s" % element.__class__.__name__, - (Annotated, element.__class__), {}) + (cls, element.__class__), {}) return object.__new__(cls) def __init__(self, element, values): @@ -462,7 +462,7 @@ class Annotated(object): # update the clone with any changes that have occurred # to this object's __dict__. clone.__dict__.update(self.__dict__) - return Annotated(clone, self._annotations) + return self.__class__(clone, self._annotations) def __hash__(self): return hash(self.__element) @@ -473,6 +473,23 @@ class Annotated(object): else: return hash(other) == hash(self) +class AnnotatedColumnElement(Annotated): + def __init__(self, element, values): + Annotated.__init__(self, element, values) + for attr in ('name', 'key'): + if self.__dict__.get(attr, False) is None: + self.__dict__.pop(attr) + + @util.memoized_property + def name(self): + """pull 'name' from parent, if not present""" + return self._Annotated__element.name + + @util.memoized_property + def key(self): + """pull 'key' from parent, if not present""" + return self._Annotated__element.key + # hard-generate Annotated subclasses. this technique # is used instead of on-the-fly types (i.e. type.__new__()) @@ -481,9 +498,13 @@ annotated_classes = {} for cls in expression.__dict__.values() + [schema.Column, schema.Table]: if isinstance(cls, type) and issubclass(cls, expression.ClauseElement): - exec "class Annotated%s(Annotated, cls):\n" \ - " pass" % (cls.__name__, ) in locals() - exec "annotated_classes[cls] = Annotated%s" % (cls.__name__) + if issubclass(cls, expression.ColumnElement): + annotation_cls = "AnnotatedColumnElement" + else: + annotation_cls = "Annotated" + exec "class Annotated%s(%s, cls):\n" \ + " pass" % (cls.__name__, annotation_cls) in locals() + exec "annotated_classes[cls] = Annotated%s" % (cls.__name__,) def _deep_annotate(element, annotations, exclude=None): """Deep copy the given ClauseElement, annotating each element diff --git a/test/orm/test_rel_fn.py b/test/orm/test_rel_fn.py index ac56f876b..bad3a0dd7 100644 --- a/test/orm/test_rel_fn.py +++ b/test/orm/test_rel_fn.py @@ -1,7 +1,7 @@ from sqlalchemy.testing import assert_raises, assert_raises_message, eq_, \ AssertsCompiledSQL, is_ from sqlalchemy.testing import fixtures -from sqlalchemy.orm import relationships, foreign, remote, remote_foreign +from sqlalchemy.orm import relationships, foreign, remote from sqlalchemy import MetaData, Table, Column, ForeignKey, Integer, \ select, ForeignKeyConstraint, exc, func, and_ from sqlalchemy.orm.interfaces import ONETOMANY, MANYTOONE, MANYTOMANY @@ -245,9 +245,9 @@ class _JoinFixtures(object): self.left, self.right, primaryjoin=(self.left.c.x + self.left.c.y) == \ - relationships.remote_foreign( + relationships.remote(relationships.foreign( self.right.c.x * self.right.c.y - ), + )), **kw ) diff --git a/test/sql/test_selectable.py b/test/sql/test_selectable.py index bbf7eeab1..35d5a0b05 100644 --- a/test/sql/test_selectable.py +++ b/test/sql/test_selectable.py @@ -1277,6 +1277,13 @@ class AnnotationsTest(fixtures.TestBase): assert x_p.compare(x_p_a) assert not x_p_a.compare(x_a) + def test_late_name_add(self): + from sqlalchemy.schema import Column + c1 = Column(Integer) + c1_a = c1._annotate({"foo": "bar"}) + c1.name = 'somename' + eq_(c1_a.name, 'somename') + def test_custom_constructions(self): from sqlalchemy.schema import Column class MyColumn(Column): |