summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES1
-rw-r--r--lib/sqlalchemy/attributes.py30
-rw-r--r--lib/sqlalchemy/orm/dependency.py20
-rw-r--r--lib/sqlalchemy/orm/properties.py3
-rw-r--r--lib/sqlalchemy/orm/unitofwork.py5
-rw-r--r--test/orm/association.py80
6 files changed, 114 insertions, 25 deletions
diff --git a/CHANGES b/CHANGES
index 5b90beab0..e87f9f4ef 100644
--- a/CHANGES
+++ b/CHANGES
@@ -3,6 +3,7 @@
two mappers that referenced each other
- upgraded all unittests to insert './lib/' into sys.path,
working around new setuptools PYTHONPATH-killing behavior
+- further fixes with attributes/dependencies/etc....
0.2.4
- try/except when the mapper sets init.__name__ on a mapped class,
diff --git a/lib/sqlalchemy/attributes.py b/lib/sqlalchemy/attributes.py
index 2bf336398..5e2226e9d 100644
--- a/lib/sqlalchemy/attributes.py
+++ b/lib/sqlalchemy/attributes.py
@@ -10,6 +10,9 @@ import weakref
class InstrumentedAttribute(object):
"""a property object that instruments attribute access on object instances. All methods correspond to
a single attribute on a particular class."""
+
+ PASSIVE_NORESULT = object()
+
def __init__(self, manager, key, uselist, callable_, typecallable, trackparent=False, extension=None, **kwargs):
self.manager = manager
self.key = key
@@ -40,9 +43,16 @@ class InstrumentedAttribute(object):
item._state[('hasparent', self)] = value
def get_history(self, obj, passive=False):
- """returns a new AttributeHistory object for the given object for this
- InstrumentedAttribute's attribute."""
- return AttributeHistory(self, obj, passive=passive)
+ """return a new AttributeHistory object for the given object/this attribute's key.
+
+ if passive is True, then dont execute any callables; if the attribute's value
+ can only be achieved via executing a callable, then return None."""
+ # get the current state. this may trigger a lazy load if
+ # passive is False.
+ current = self.get(obj, passive=passive, raiseerr=False)
+ if current is InstrumentedAttribute.PASSIVE_NORESULT:
+ return None
+ return AttributeHistory(self, obj, current, passive=passive)
def set_callable(self, obj, callable_):
"""sets a callable function on the given object which will be executed when this attribute
@@ -123,7 +133,7 @@ class InstrumentedAttribute(object):
callable_ = self._get_callable(obj)
if callable_ is not None:
if passive:
- return None
+ return InstrumentedAttribute.PASSIVE_NORESULT
l = InstrumentedList(self, obj, self._adapt_list(callable_()), init=False)
# if a callable was executed, then its part of the "committed state"
# if any, so commit the newly loaded data
@@ -141,7 +151,7 @@ class InstrumentedAttribute(object):
callable_ = self._get_callable(obj)
if callable_ is not None:
if passive:
- return None
+ return InstrumentedAttribute.PASSIVE_NORESULT
obj.__dict__[self.key] = callable_()
# if a callable was executed, then its part of the "committed state"
# if any, so commit the newly loaded data
@@ -473,12 +483,9 @@ class CommittedState(object):
class AttributeHistory(object):
"""calculates the "history" of a particular attribute on a particular instance, based on the CommittedState
associated with the instance, if any."""
- def __init__(self, attr, obj, passive=False):
+ def __init__(self, attr, obj, current, passive=False):
self.attr = attr
- # get the current state. this may trigger a lazy load if
- # passive is False.
- current = attr.get(obj, passive=passive, raiseerr=False)
-
+
# get the "original" value. if a lazy load was fired when we got
# the 'current' value, this "original" was also populated just
# now as well (therefore we have to get it second)
@@ -492,7 +499,6 @@ class AttributeHistory(object):
self._current = current
else:
self._current = [current]
-
if attr.uselist:
s = util.Set(original or [])
self._added_items = []
@@ -592,7 +598,7 @@ class AttributeManager(object):
"""
attr = getattr(obj.__class__, key)
x = attr.get(obj, passive=passive)
- if x is None:
+ if x is InstrumentedAttribute.PASSIVE_NORESULT:
return []
elif attr.uselist:
return x
diff --git a/lib/sqlalchemy/orm/dependency.py b/lib/sqlalchemy/orm/dependency.py
index 6eca75ae1..8970dab50 100644
--- a/lib/sqlalchemy/orm/dependency.py
+++ b/lib/sqlalchemy/orm/dependency.py
@@ -118,9 +118,9 @@ class OneToManyDP(DependencyProcessor):
self._synchronize(obj, child, None, False)
if child is not None and self.post_update:
uowcommit.register_object(child, postupdate=True)
- for child in childlist.deleted_items():
- if not self.cascade.delete_orphan:
- self._synchronize(obj, child, None, True)
+ for child in childlist.deleted_items():
+ if not self.cascade.delete_orphan:
+ self._synchronize(obj, child, None, True)
def preprocess_dependencies(self, task, deplist, uowcommit, delete = False):
#print self.mapper.mapped_table.name + " " + self.key + " " + repr(len(deplist)) + " preprocess_dep isdelete " + repr(delete) + " direction " + repr(self.direction)
@@ -161,13 +161,13 @@ class OneToManyDP(DependencyProcessor):
for child in childlist.added_items():
if child is not None:
uowcommit.register_object(child)
- for child in childlist.deleted_items():
- if not self.cascade.delete_orphan:
- uowcommit.register_object(child, isdelete=False)
- elif childlist.hasparent(child) is False:
- uowcommit.register_object(child, isdelete=True)
- for c in self.mapper.cascade_iterator('delete', child):
- uowcommit.register_object(c, isdelete=True)
+ for child in childlist.deleted_items():
+ if not self.cascade.delete_orphan:
+ uowcommit.register_object(child, isdelete=False)
+ elif childlist.hasparent(child) is False:
+ uowcommit.register_object(child, isdelete=True)
+ for c in self.mapper.cascade_iterator('delete', child):
+ uowcommit.register_object(c, isdelete=True)
def _synchronize(self, obj, child, associationrow, clearkeys):
source = obj
diff --git a/lib/sqlalchemy/orm/properties.py b/lib/sqlalchemy/orm/properties.py
index f0ec8f556..705d53363 100644
--- a/lib/sqlalchemy/orm/properties.py
+++ b/lib/sqlalchemy/orm/properties.py
@@ -157,7 +157,8 @@ class PropertyLoader(mapper.MapperProperty):
if not type in self.cascade:
return
childlist = sessionlib.attribute_manager.get_history(object, self.key, passive=True)
-
+ if childlist is None:
+ return
mapper = self.mapper.primary_mapper()
for c in childlist.added_items() + childlist.deleted_items() + childlist.unchanged_items():
if c is not None and c not in recursive:
diff --git a/lib/sqlalchemy/orm/unitofwork.py b/lib/sqlalchemy/orm/unitofwork.py
index e243b3b31..92cf825d5 100644
--- a/lib/sqlalchemy/orm/unitofwork.py
+++ b/lib/sqlalchemy/orm/unitofwork.py
@@ -803,8 +803,9 @@ class UOWTask(object):
isdelete = taskelement.isdelete
# list of dependent objects from this object
- childlist = dep.get_object_dependencies(obj, trans, passive = True)
-
+ childlist = dep.get_object_dependencies(obj, trans, passive=True)
+ if childlist is None:
+ continue
# the task corresponding to saving/deleting of those dependent objects
childtask = trans.get_task_by_mapper(processor.mapper.primary_mapper())
diff --git a/test/orm/association.py b/test/orm/association.py
index e33151ed4..755f6cf89 100644
--- a/test/orm/association.py
+++ b/test/orm/association.py
@@ -138,6 +138,86 @@ class AssociationTest(testbase.PersistTest):
sess.flush()
self.assert_(item_keywords.count().scalar() == 0)
+class AssociationTest2(testbase.PersistTest):
+ def setUpAll(self):
+ global table_originals, table_people, table_isauthor, metadata, Originals, People, IsAuthor
+ metadata = BoundMetaData(testbase.db)
+ table_originals = Table('Originals', metadata,
+ Column('ID', Integer, primary_key=True),
+ Column('Title', String(200), nullable=False),
+ Column('Date', Date ),
+ )
+ table_people = Table('People', metadata,
+ Column('ID', Integer, primary_key=True),
+ Column('Name', String(140), nullable=False),
+ Column('Country', CHAR(2), default='es'),
+ )
+ table_isauthor = Table('IsAuthor', metadata,
+ Column('OriginalsID', Integer, ForeignKey('Originals.ID'),
+default=None),
+ Column('PeopleID', Integer, ForeignKey('People.ID'),
+default=None),
+ Column('Kind', CHAR(1), default='A'),
+ )
+ metadata.create_all()
+
+ class Base(object):
+ def __init__(self, **kw):
+ for k,v in kw.iteritems():
+ setattr(self, k, v)
+ def display(self):
+ c = [ "%s=%s" % (col.key, repr(getattr(self, col.key))) for col
+in self.c ]
+ return "%s(%s)" % (self.__class__.__name__, ', '.join(c))
+ def __repr__(self):
+ return self.display()
+ def __str__(self):
+ return self.display()
+ class Originals(Base):
+ order = [table_originals.c.Title, table_originals.c.Date]
+ class People(Base):
+ order = [table_people.c.Name]
+ class IsAuthor(Base):
+ pass
+
+ mapper(Originals, table_originals, order_by=Originals.order,
+ properties={
+ 'people': relation(IsAuthor, association=People),
+ 'authors': relation(People, secondary=table_isauthor, backref='written',
+ primaryjoin=and_(table_originals.c.ID==table_isauthor.c.OriginalsID,
+ table_isauthor.c.Kind=='A')),
+ 'title': table_originals.c.Title,
+ 'date': table_originals.c.Date,
+ })
+ mapper(People, table_people, order_by=People.order, properties= {
+ 'originals': relation(IsAuthor, association=Originals),
+ 'name': table_people.c.Name,
+ 'country': table_people.c.Country,
+ })
+ mapper(IsAuthor, table_isauthor,
+ primary_key=[table_isauthor.c.OriginalsID, table_isauthor.c.PeopleID,
+table_isauthor.c.Kind],
+ properties={
+ 'original': relation(Originals, lazy=False),
+ 'person': relation(People, lazy=False),
+ 'kind': table_isauthor.c.Kind,
+ })
+
+ def tearDown(self):
+ for t in metadata.table_iterator(reverse=True):
+ t.delete().execute()
+ def tearDownAll(self):
+ clear_mappers()
+ metadata.drop_all()
+
+ def testinsert(self):
+ # this test is sure to get more complex...
+ p = People(name='name', country='es')
+ sess = create_session()
+ sess.save(p)
+ sess.flush()
+
+
if __name__ == "__main__":
testbase.main()