From 738ec90ca14154493a88fb38728537837a45eebf Mon Sep 17 00:00:00 2001
From: Raymond Hettinger <python@rcn.com>
Date: Sun, 29 Feb 2004 02:15:56 +0000
Subject: Improvements to collections.deque():

* Add doctests for the examples in the library reference.
* Add two methods, left() and right(), modeled after deques in C++ STL.
* Apply the new method to asynchat.py.
* Add comparison operators to make deques more substitutable for lists.
* Replace the LookupErrors with IndexErrors to more closely match lists.
---
 Doc/lib/libcollections.tex  |  29 ++++++++---
 Lib/asynchat.py             |   6 +--
 Lib/test/test_deque.py      |  92 +++++++++++++++++++++++++++++++--
 Modules/collectionsmodule.c | 120 ++++++++++++++++++++++++++++++++++++++++++--
 4 files changed, 229 insertions(+), 18 deletions(-)

diff --git a/Doc/lib/libcollections.tex b/Doc/lib/libcollections.tex
index 0378ea5..2793095 100644
--- a/Doc/lib/libcollections.tex
+++ b/Doc/lib/libcollections.tex
@@ -54,14 +54,24 @@ Deque objects support the following methods:
    reversing the order of elements in the iterable argument.
 \end{methoddesc}
 
+\begin{methoddesc}{left}{}
+   Return leftmost element from the deque.
+   If no elements are present, raises a \exception{IndexError}.   
+\end{methoddesc}
+
 \begin{methoddesc}{pop}{}
    Remove and return an element from the right side of the deque.
-   If no elements are present, raises a \exception{LookupError}.
+   If no elements are present, raises a \exception{IndexError}.
 \end{methoddesc}
 
 \begin{methoddesc}{popleft}{}
    Remove and return an element from the left side of the deque.
-   If no elements are present, raises a \exception{LookupError}.   
+   If no elements are present, raises a \exception{IndexError}.   
+\end{methoddesc}
+
+\begin{methoddesc}{right}{}
+   Return the rightmost element from the deque.
+   If no elements are present, raises a \exception{IndexError}.   
 \end{methoddesc}
 
 \begin{methoddesc}{rotate}{n}
@@ -80,22 +90,27 @@ Example:
 >>> from collections import deque
 >>> d = deque('ghi')                 # make a new deque with three items
 >>> for elem in d:                   # iterate over the deque's elements
-	print elem.upper()
-
-	
+...     print elem.upper()	
 G
 H
 I
+
 >>> d.append('j')                    # add a new entry to the right side
 >>> d.appendleft('f')                # add a new entry to the left side
 >>> d                                # show the representation of the deque
 deque(['f', 'g', 'h', 'i', 'j'])
+
 >>> d.pop()                          # return and remove the rightmost item
 'j'
 >>> d.popleft()                      # return and remove the leftmost item
 'f'
 >>> list(d)                          # list the contents of the deque
 ['g', 'h', 'i']
+
+>>> d.left()                         # peek at leftmost item
+'g'
+>>> d.right()                        # peek at rightmost item
+'i'
 >>> list(reversed(d))                # list the contents of a deque in reverse
 ['i', 'h', 'g']
 >>> 'h' in d                         # search the deque
@@ -109,15 +124,15 @@ deque(['l', 'g', 'h', 'i', 'j', 'k'])
 >>> d.rotate(-1)                     # left rotation
 >>> d
 deque(['g', 'h', 'i', 'j', 'k', 'l'])
+
 >>> deque(reversed(d))               # make a new deque in reverse order
 deque(['l', 'k', 'j', 'i', 'h', 'g'])
 >>> d.clear()                        # empty the deque
 >>> d.pop()                          # cannot pop from an empty deque
-
 Traceback (most recent call last):
   File "<pyshell#6>", line 1, in -toplevel-
     d.pop()
-LookupError: pop from an empty deque
+IndexError: pop from an empty deque
 
 >>> d.extendleft('abc')              # extendleft() reverses the input order
 >>> d
diff --git a/Lib/asynchat.py b/Lib/asynchat.py
index 4bfab30..2c0bc3e 100644
--- a/Lib/asynchat.py
+++ b/Lib/asynchat.py
@@ -262,11 +262,7 @@ class fifo:
         return self.list == []
 
     def first (self):
-        it = iter(self.list)
-        try:
-            return it.next()
-        except StopIteration:
-            raise IndexError
+        return self.list.left()
 
     def push (self, data):
         self.list.append(data)
diff --git a/Lib/test/test_deque.py b/Lib/test/test_deque.py
index f3bc59f..db9733e 100644
--- a/Lib/test/test_deque.py
+++ b/Lib/test/test_deque.py
@@ -28,6 +28,23 @@ class TestBasic(unittest.TestCase):
         self.assertEqual(right, range(150, 400))
         self.assertEqual(list(d), range(50, 150))
 
+    def test_comparisons(self):
+        d = deque('xabc'); d.popleft()
+        for e in [d, deque('abc'), deque('ab'), deque(), list(d)]:
+            self.assertEqual(d==e, type(d)==type(e) and list(d)==list(e))
+            self.assertEqual(d!=e, not(type(d)==type(e) and list(d)==list(e)))
+
+        args = map(deque, ('', 'a', 'b', 'ab', 'ba', 'abc', 'xba', 'xabc', 'cba'))
+        for x in args:
+            for y in args:
+                self.assertEqual(x == y, list(x) == list(y), (x,y))
+                self.assertEqual(x != y, list(x) != list(y), (x,y))
+                self.assertEqual(x <  y, list(x) <  list(y), (x,y))
+                self.assertEqual(x <= y, list(x) <= list(y), (x,y))
+                self.assertEqual(x >  y, list(x) >  list(y), (x,y))
+                self.assertEqual(x >= y, list(x) >= list(y), (x,y))
+                self.assertEqual(cmp(x,y), cmp(list(x),list(y)), (x,y))
+
     def test_extend(self):
         d = deque('a')
         self.assertRaises(TypeError, d.extend, 1)
@@ -40,6 +57,14 @@ class TestBasic(unittest.TestCase):
         d.extendleft('bcd')
         self.assertEqual(list(d), list(reversed('abcd')))
 
+    def test_leftright(self):
+        d = deque('superman')
+        self.assertEqual(d.left(), 's')
+        self.assertEqual(d.right(), 'n')
+        d = deque()
+        self.assertRaises(IndexError, d.left)
+        self.assertRaises(IndexError, d.right)
+
     def test_rotate(self):
         s = tuple('abcde')
         n = len(s)
@@ -93,7 +118,7 @@ class TestBasic(unittest.TestCase):
         self.assertEqual(len(d), 1)
         d.pop()
         self.assertEqual(len(d), 0)
-        self.assertRaises(LookupError, d.pop)
+        self.assertRaises(IndexError, d.pop)
         self.assertEqual(len(d), 0)
         d.append('c')
         self.assertEqual(len(d), 1)
@@ -104,8 +129,8 @@ class TestBasic(unittest.TestCase):
 
     def test_underflow(self):
         d = deque()
-        self.assertRaises(LookupError, d.pop)
-        self.assertRaises(LookupError, d.popleft)
+        self.assertRaises(IndexError, d.pop)
+        self.assertRaises(IndexError, d.popleft)
 
     def test_clear(self):
         d = deque(xrange(100))
@@ -374,6 +399,63 @@ class TestSubclass(unittest.TestCase):
 
 #==============================================================================
 
+libreftest = """
+Example from the Library Reference:  Doc/lib/libcollections.tex
+
+>>> from collections import deque
+>>> d = deque('ghi')                 # make a new deque with three items
+>>> for elem in d:                   # iterate over the deque's elements
+...     print elem.upper()	
+G
+H
+I
+>>> d.append('j')                    # add a new entry to the right side
+>>> d.appendleft('f')                # add a new entry to the left side
+>>> d                                # show the representation of the deque
+deque(['f', 'g', 'h', 'i', 'j'])
+>>> d.pop()                          # return and remove the rightmost item
+'j'
+>>> d.popleft()                      # return and remove the leftmost item
+'f'
+>>> list(d)                          # list the contents of the deque
+['g', 'h', 'i']
+>>> d.left()                         # peek at leftmost item
+'g'
+>>> d.right()                        # peek at rightmost item
+'i'
+>>> list(reversed(d))                # list the contents of a deque in reverse
+['i', 'h', 'g']
+>>> 'h' in d                         # search the deque
+True
+>>> d.extend('jkl')                  # add multiple elements at once
+>>> d
+deque(['g', 'h', 'i', 'j', 'k', 'l'])
+>>> d.rotate(1)                      # right rotation
+>>> d
+deque(['l', 'g', 'h', 'i', 'j', 'k'])
+>>> d.rotate(-1)                     # left rotation
+>>> d
+deque(['g', 'h', 'i', 'j', 'k', 'l'])
+>>> deque(reversed(d))               # make a new deque in reverse order
+deque(['l', 'k', 'j', 'i', 'h', 'g'])
+>>> d.clear()                        # empty the deque
+>>> d.pop()                          # cannot pop from an empty deque
+Traceback (most recent call last):
+  File "<pyshell#6>", line 1, in -toplevel-
+    d.pop()
+IndexError: pop from an empty deque
+
+>>> d.extendleft('abc')              # extendleft() reverses the input order
+>>> d
+deque(['c', 'b', 'a'])
+
+"""
+
+
+#==============================================================================
+
+__test__ = {'libreftest' : libreftest}
+
 def test_main(verbose=None):
     import sys
     from test import test_sets
@@ -394,6 +476,10 @@ def test_main(verbose=None):
             gc.collect()
             counts[i] = sys.gettotalrefcount()
         print counts
+        
+    # doctests
+    from test import test_deque
+    test_support.run_doctest(test_deque, verbose)
 
 if __name__ == "__main__":
     test_main(verbose=True)
diff --git a/Modules/collectionsmodule.c b/Modules/collectionsmodule.c
index d534aa9..2b5cb10 100644
--- a/Modules/collectionsmodule.c
+++ b/Modules/collectionsmodule.c
@@ -34,6 +34,8 @@ typedef struct {
 	int len;
 } dequeobject;
 
+PyTypeObject deque_type;
+
 static PyObject *
 deque_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
 {
@@ -109,7 +111,7 @@ deque_pop(dequeobject *deque, PyObject *unused)
 	block *prevblock;
 
 	if (deque->len == 0) {
-		PyErr_SetString(PyExc_LookupError, "pop from an empty deque");
+		PyErr_SetString(PyExc_IndexError, "pop from an empty deque");
 		return NULL;
 	}
 	item = deque->rightblock->data[deque->rightindex];
@@ -144,7 +146,7 @@ deque_popleft(dequeobject *deque, PyObject *unused)
 	block *prevblock;
 
 	if (deque->len == 0) {
-		PyErr_SetString(PyExc_LookupError, "pop from an empty deque");
+		PyErr_SetString(PyExc_IndexError, "pop from an empty deque");
 		return NULL;
 	}
 	item = deque->leftblock->data[deque->leftindex];
@@ -175,6 +177,38 @@ deque_popleft(dequeobject *deque, PyObject *unused)
 PyDoc_STRVAR(popleft_doc, "Remove and return the leftmost element.");
 
 static PyObject *
+deque_right(dequeobject *deque, PyObject *unused)
+{
+	PyObject *item;
+
+	if (deque->len == 0) {
+		PyErr_SetString(PyExc_IndexError, "deque is empty");
+		return NULL;
+	}
+	item = deque->rightblock->data[deque->rightindex];
+	Py_INCREF(item);
+	return item;
+}
+
+PyDoc_STRVAR(right_doc, "Return the rightmost element.");
+
+static PyObject *
+deque_left(dequeobject *deque, PyObject *unused)
+{
+	PyObject *item;
+
+	if (deque->len == 0) {
+		PyErr_SetString(PyExc_IndexError, "deque is empty");
+		return NULL;
+	}
+	item = deque->leftblock->data[deque->leftindex];
+	Py_INCREF(item);
+	return item;
+}
+
+PyDoc_STRVAR(left_doc, "Return the leftmost element.");
+
+static PyObject *
 deque_extend(dequeobject *deque, PyObject *iterable)
 {
 	PyObject *it, *item;
@@ -467,6 +501,82 @@ deque_tp_print(PyObject *deque, FILE *fp, int flags)
 	return 0;
 }
 
+static PyObject *
+deque_richcompare(PyObject *v, PyObject *w, int op)
+{
+	PyObject *it1=NULL, *it2=NULL, *x, *y;
+	int i, b, vs, ws, minlen, cmp=-1;
+
+	if (v->ob_type != &deque_type || w->ob_type != &deque_type) {
+		Py_INCREF(Py_NotImplemented);
+		return Py_NotImplemented;
+	}
+
+	/* Shortcuts */
+	vs = ((dequeobject *)v)->len;
+	ws = ((dequeobject *)w)->len;
+	if (op == Py_EQ) {
+		if (v == w)
+			Py_RETURN_TRUE;
+		if (vs != ws)
+			Py_RETURN_FALSE;
+	}
+	if (op == Py_NE) {
+		if (v == w)
+			Py_RETURN_FALSE;
+		if (vs != ws)
+			Py_RETURN_TRUE;
+	}
+
+	/* Search for the first index where items are different */
+	it1 = PyObject_GetIter(v);
+	if (it1 == NULL)
+		goto done;
+	it2 = PyObject_GetIter(w);
+	if (it2 == NULL)
+		goto done;
+	minlen = (vs < ws)  ?  vs  :  ws;
+	for (i=0 ; i < minlen ; i++) {
+		x = PyIter_Next(it1);
+		if (x == NULL)
+			goto done;
+		y = PyIter_Next(it2);
+		if (y == NULL) {
+			Py_DECREF(x);
+			goto done;
+		}
+		b = PyObject_RichCompareBool(x, y, Py_EQ);
+		if (b == 0) {
+			cmp = PyObject_RichCompareBool(x, y, op);
+			Py_DECREF(x);
+			Py_DECREF(y);
+			goto done;
+		}
+		Py_DECREF(x);
+		Py_DECREF(y);
+		if (b == -1)
+			goto done;
+	}
+	/* Elements are equal through minlen.  The longest input is the greatest */
+	switch (op) {
+	case Py_LT: cmp = vs <  ws; break;
+	case Py_LE: cmp = vs <= ws; break;
+	case Py_EQ: cmp = vs == ws; break;
+	case Py_NE: cmp = vs != ws; break;
+	case Py_GT: cmp = vs >  ws; break;
+	case Py_GE: cmp = vs >= ws; break;
+	}
+	
+done:
+	Py_XDECREF(it1);
+	Py_XDECREF(it2);
+	if (cmp == 1)
+		Py_RETURN_TRUE;
+	if (cmp == 0)
+		Py_RETURN_FALSE;
+	return NULL;
+}
+
 static int
 deque_init(dequeobject *deque, PyObject *args, PyObject *kwds)
 {
@@ -509,6 +619,8 @@ static PyMethodDef deque_methods[] = {
 		METH_O,		 extend_doc},
 	{"extendleft",	(PyCFunction)deque_extendleft,	
 		METH_O,		 extendleft_doc},
+	{"left",		(PyCFunction)deque_left,	
+		METH_NOARGS,	 left_doc},
 	{"pop",			(PyCFunction)deque_pop,	
 		METH_NOARGS,	 pop_doc},
 	{"popleft",		(PyCFunction)deque_popleft,	
@@ -517,6 +629,8 @@ static PyMethodDef deque_methods[] = {
 		METH_NOARGS,	 reduce_doc},
 	{"__reversed__",	(PyCFunction)deque_reviter,	
 		METH_NOARGS,	 reversed_doc},
+	{"right",		(PyCFunction)deque_right,	
+		METH_NOARGS,	 right_doc},
 	{"rotate",		(PyCFunction)deque_rotate,	
 		METH_VARARGS,	rotate_doc},
 	{NULL,		NULL}	/* sentinel */
@@ -553,7 +667,7 @@ PyTypeObject deque_type = {
 	deque_doc,			/* tp_doc */
 	(traverseproc)set_traverse,	/* tp_traverse */
 	(inquiry)deque_clear,		/* tp_clear */
-	0,				/* tp_richcompare */
+	(richcmpfunc)deque_richcompare,	/* tp_richcompare */
 	0,				/* tp_weaklistoffset*/
 	(getiterfunc)deque_iter,	/* tp_iter */
 	0,				/* tp_iternext */
-- 
cgit v0.12

