From 1cf80dc5b273dc92607863bdd3af859840aa3364 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Wed, 14 Sep 2011 11:31:33 -0400 Subject: [PATCH] - Changed the update() method on association proxy dictionary to use a duck typing approach, i.e. checks for "keys", to discern between update({}) and update((a, b)). Previously, passing a dictionary that had tuples as keys would be misinterpreted as a sequence. [ticket:2275] --- CHANGES | 7 +++ lib/sqlalchemy/ext/associationproxy.py | 17 +++++-- lib/sqlalchemy/sql/expression.py | 2 +- test/ext/test_associationproxy.py | 64 +++++++++++++++++++++++++- test/sql/test_selectable.py | 1 - 5 files changed, 83 insertions(+), 8 deletions(-) diff --git a/CHANGES b/CHANGES index a98f3d0c46..99af9e4a3e 100644 --- a/CHANGES +++ b/CHANGES @@ -157,6 +157,13 @@ CHANGES to the proxied attributes at the class level. [ticket:2236] + - Changed the update() method on association proxy + dictionary to use a duck typing approach, i.e. + checks for "keys", to discern between update({}) + and update((a, b)). Previously, passing a + dictionary that had tuples as keys would be misinterpreted + as a sequence. [ticket:2275] + 0.7.2 ===== - orm diff --git a/lib/sqlalchemy/ext/associationproxy.py b/lib/sqlalchemy/ext/associationproxy.py index 47b21ab5b0..eb5b3e3db6 100644 --- a/lib/sqlalchemy/ext/associationproxy.py +++ b/lib/sqlalchemy/ext/associationproxy.py @@ -768,11 +768,20 @@ class _AssociationDict(_AssociationCollection): len(a)) elif len(a) == 1: seq_or_map = a[0] - for item in seq_or_map: - if isinstance(item, tuple): - self[item[0]] = item[1] - else: + # discern dict from sequence - took the advice + # from http://www.voidspace.org.uk/python/articles/duck_typing.shtml + # still not perfect :( + if hasattr(seq_or_map, 'keys'): + for item in seq_or_map: self[item] = seq_or_map[item] + else: + try: + for k, v in seq_or_map: + self[k] = v + except ValueError: + raise ValueError( + "dictionary update sequence " + "requires 2-element tuples") for key, value in kw: self[key] = value diff --git a/lib/sqlalchemy/sql/expression.py b/lib/sqlalchemy/sql/expression.py index 9e920c34f7..84fcbd5695 100644 --- a/lib/sqlalchemy/sql/expression.py +++ b/lib/sqlalchemy/sql/expression.py @@ -4699,7 +4699,7 @@ class Select(_SelectBase): """ self._should_correlate = False - if fromclauses == (None,): + if fromclauses and fromclauses[0] is None: self._correlate = set() else: self._correlate = self._correlate.union(fromclauses) diff --git a/test/ext/test_associationproxy.py b/test/ext/test_associationproxy.py index ddd0bd8f12..c9feddf4c9 100644 --- a/test/ext/test_associationproxy.py +++ b/test/ext/test_associationproxy.py @@ -4,10 +4,11 @@ import pickle from sqlalchemy import * from sqlalchemy.orm import * -from sqlalchemy.orm.collections import collection +from sqlalchemy.orm.collections import collection, attribute_mapped_collection from sqlalchemy.ext.associationproxy import * from sqlalchemy.ext.associationproxy import _AssociationList from test.lib import * +from test.lib.testing import assert_raises_message from test.lib.util import gc_collect from sqlalchemy.sql import not_ from test.lib import fixtures @@ -1306,4 +1307,63 @@ class ComparatorTest(fixtures.MappedTest, AssertsCompiledSQL): "FROM users JOIN userkeywords ON users.id = " "userkeywords.user_id JOIN keywords ON keywords.id = " "userkeywords.keyword_id" - ) \ No newline at end of file + ) + +class DictOfTupleUpdateTest(fixtures.TestBase): + def setup(self): + class B(object): + def __init__(self, key, elem): + self.key = key + self.elem = elem + + class A(object): + elements = association_proxy("orig", "elem", creator=B) + + m = MetaData() + a = Table('a', m, Column('id', Integer, primary_key=True)) + b = Table('b', m, Column('id', Integer, primary_key=True), + Column('aid', Integer, ForeignKey('a.id'))) + mapper(A, a, properties={ + 'orig':relationship(B, collection_class=attribute_mapped_collection('key')) + }) + mapper(B, b) + self.A = A + self.B = B + + def test_update_one_elem_dict(self): + a1 = self.A() + a1.elements.update({("B", 3): 'elem2'}) + eq_(a1.elements, {("B",3):'elem2'}) + + def test_update_multi_elem_dict(self): + a1 = self.A() + a1.elements.update({("B", 3): 'elem2', ("C", 4): "elem3"}) + eq_(a1.elements, {("B",3):'elem2', ("C", 4): "elem3"}) + + def test_update_one_elem_list(self): + a1 = self.A() + a1.elements.update([(("B", 3), 'elem2')]) + eq_(a1.elements, {("B",3):'elem2'}) + + def test_update_multi_elem_list(self): + a1 = self.A() + a1.elements.update([(("B", 3), 'elem2'), (("C", 4), "elem3")]) + eq_(a1.elements, {("B",3):'elem2', ("C", 4): "elem3"}) + + def test_update_one_elem_varg(self): + a1 = self.A() + assert_raises_message( + ValueError, + "dictionary update sequence requires " + "2-element tuples", + a1.elements.update, (("B", 3), 'elem2') + ) + + def test_update_multi_elem_varg(self): + a1 = self.A() + assert_raises_message( + TypeError, + "update expected at most 1 arguments, got 2", + a1.elements.update, + (("B", 3), 'elem2'), (("C", 4), "elem3") + ) diff --git a/test/sql/test_selectable.py b/test/sql/test_selectable.py index 9c1f44e1a0..4bbcf61d28 100644 --- a/test/sql/test_selectable.py +++ b/test/sql/test_selectable.py @@ -161,7 +161,6 @@ class SelectableTest(fixtures.TestBase, AssertsExecutionResults, AssertsCompiled criterion = a.c.col1 == table2.c.col2 self.assert_(criterion.compare(j.onclause)) - def test_union(self): # tests that we can correspond a column in a Select statement -- 2.47.3