[rset] New method: ResultSet.one()

This method will return exactly one entity while enforcing its existence and unicity. The idea is shamelessly borowed from SQLAlchemy Query.one(). Closes #3352314

[jcr: use len(self) instead of len(self.rows)]

authorChristophe de Vienne <cdevienne@gmail.com>
changesetbd841d6ae723
branchdefault
phasepublic
hiddenno
parent revision#4da3ef764395 [predicates] allow multiple transition names in on_fire_transition (closes #3013720)
child revision#eacd02792332 [req] New method: RequestSessionBase.find().
files modified by this revision
_exceptions.py
rset.py
test/unittest_rset.py
# HG changeset patch
# User Christophe de Vienne <cdevienne@gmail.com>
# Date 1386780774 -3600
# Wed Dec 11 17:52:54 2013 +0100
# Node ID bd841d6ae7237a8952a24cd4a3423f9710ac84ba
# Parent 4da3ef76439516a572b6b661a3fb011f8afcca4a
[rset] New method: ResultSet.one()

This method will return exactly one entity while enforcing its existence and unicity.
The idea is shamelessly borowed from SQLAlchemy Query.one().
Closes #3352314

[jcr: use len(self) instead of len(self.rows)]

diff --git a/_exceptions.py b/_exceptions.py
@@ -132,10 +132,19 @@
1  class NotAnEntity(CubicWebRuntimeError):
2      """raised when get_entity is called for a column which doesn't contain
3      a non final entity
4      """
5 
6 +class MultipleResultsError(CubicWebRuntimeError):
7 +    """raised when ResultSet.one() is called on a resultset with multiple rows
8 +    of multiple columns.
9 +    """
10 +
11 +class NoResultError(CubicWebRuntimeError):
12 +    """raised when no result is found but at least one is expected.
13 +    """
14 +
15  class UndoTransactionException(QueryError):
16      """Raised when undoing a transaction could not be performed completely.
17 
18      Note that :
19        1) the partial undo operation might be acceptable
diff --git a/rset.py b/rset.py
@@ -21,11 +21,11 @@
20 
21  from logilab.common.decorators import cached, clear_cache, copy_cache
22 
23  from rql import nodes, stmts
24 
25 -from cubicweb import NotAnEntity
26 +from cubicweb import NotAnEntity, NoResultError, MultipleResultsError
27 
28 
29  class ResultSet(object):
30      """A result set wraps a RQL query result. This object implements
31      partially the list protocol to allow direct use as a list of
@@ -435,10 +435,29 @@
32                  raise NotAnEntity(etype)
33          except KeyError:
34              raise NotAnEntity(etype)
35          return self._build_entity(row, col)
36 
37 +    def one(self, col=0):
38 +        """Retrieve exactly one entity from the query.
39 +
40 +        If the result set is empty, raises :exc:`NoResultError`.
41 +        If the result set has more than one row, raises
42 +        :exc:`MultipleResultsError`.
43 +
44 +        :type col: int
45 +        :param col: The column localising the entity in the unique row
46 +
47 +        :return: the partially initialized `Entity` instance
48 +        """
49 +        if len(self) == 1:
50 +            return self.get_entity(0, col)
51 +        elif len(self) == 0:
52 +            raise NoResultError("No row was found for one()")
53 +        else:
54 +            raise MultipleResultsError("Multiple rows were found for one()")
55 +
56      def _build_entity(self, row, col):
57          """internal method to get a single entity, returns a partially
58          initialized Entity instance.
59 
60          partially means that only attributes selected in the RQL query will be
diff --git a/test/unittest_rset.py b/test/unittest_rset.py
@@ -26,10 +26,12 @@
61  from logilab.common.testlib import TestCase, unittest_main, mock_object
62 
63  from cubicweb.devtools.testlib import CubicWebTC
64  from cubicweb.rset import NotAnEntity, ResultSet, attr_desc_iterator
65 
66 +from cubicweb import NoResultError, MultipleResultsError
67 +
68 
69  def pprelcachedict(d):
70      res = {}
71      for k, (rset, related) in d.items():
72          res[k] = sorted(v.eid for v in related)
@@ -366,10 +368,43 @@
73              etype, n = expected[entity.cw_row]
74              self.assertEqual(entity.cw_etype, etype)
75              attr = etype == 'Bookmark' and 'title' or 'name'
76              self.assertEqual(entity.cw_attr_cache[attr], n)
77 
78 +    def test_one(self):
79 +        self.request().create_entity('CWUser', login=u'cdevienne',
80 +                                     upassword=u'cdevienne',
81 +                                     surname=u'de Vienne',
82 +                                     firstname=u'Christophe')
83 +        e = self.execute('Any X WHERE X login "cdevienne"').one()
84 +
85 +        self.assertEqual(e.surname, u'de Vienne')
86 +
87 +        e = self.execute(
88 +            'Any X, N WHERE X login "cdevienne", X surname N').one()
89 +        self.assertEqual(e.surname, u'de Vienne')
90 +
91 +        e = self.execute(
92 +            'Any N, X WHERE X login "cdevienne", X surname N').one(col=1)
93 +        self.assertEqual(e.surname, u'de Vienne')
94 +
95 +    def test_one_no_rows(self):
96 +        with self.assertRaises(NoResultError):
97 +            self.execute('Any X WHERE X login "patanok"').one()
98 +
99 +    def test_one_multiple_rows(self):
100 +        self.request().create_entity(
101 +            'CWUser', login=u'cdevienne', upassword=u'cdevienne',
102 +            surname=u'de Vienne', firstname=u'Christophe')
103 +
104 +        self.request().create_entity(
105 +            'CWUser', login=u'adim', upassword='adim', surname=u'di mascio',
106 +            firstname=u'adrien')
107 +
108 +        with self.assertRaises(MultipleResultsError):
109 +            self.execute('Any X WHERE X is CWUser').one()
110 +
111      def test_related_entity_optional(self):
112          e = self.request().create_entity('Bookmark', title=u'aaaa', path=u'path')
113          rset = self.execute('Any B,U,L WHERE B bookmarked_by U?, U login L')
114          entity, rtype = rset.related_entity(0, 2)
115          self.assertEqual(entity, None)