summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--NEWS2
-rw-r--r--doc/src/extras.rst12
-rw-r--r--lib/_range.py39
-rwxr-xr-xtests/test_types_extras.py73
-rw-r--r--tests/testutils.py10
5 files changed, 125 insertions, 11 deletions
diff --git a/NEWS b/NEWS
index 8ce77c4..adaa143 100644
--- a/NEWS
+++ b/NEWS
@@ -13,6 +13,8 @@ Bug fixes:
What's new in psycopg 2.5.3
^^^^^^^^^^^^^^^^^^^^^^^^^^^
+- Added arbitrary but stable order to `Range` objects, thanks to
+ Chris Withers (:ticket:`#193`).
- Fixed debug build on Windows, thanks to James Emerton.
diff --git a/doc/src/extras.rst b/doc/src/extras.rst
index a0a2d1c..7fab338 100644
--- a/doc/src/extras.rst
+++ b/doc/src/extras.rst
@@ -437,8 +437,16 @@ user-defined |range| types can be adapted using `register_range()`.
`!Range` objects are immutable, hashable, and support the ``in`` operator
(checking if an element is within the range). They can be tested for
- equivalence but not for ordering. Empty ranges evaluate to `!False` in
- boolean context, nonempty evaluate to `!True`.
+ equivalence. Empty ranges evaluate to `!False` in boolean context,
+ nonempty evaluate to `!True`.
+
+ .. versionchanged:: 2.5.3
+
+ `!Range` objects can be sorted although, as on the server-side, this
+ ordering is not particularly meangingful. It is only meant to be used
+ by programs assuming objects using `!Range` as primary key can be
+ sorted on them. In previous versions comparing `!Range`\s raises
+ `!TypeError`.
Although it is possible to instantiate `!Range` objects, the class doesn't
have an adapter registered, so you cannot normally pass these instances as
diff --git a/lib/_range.py b/lib/_range.py
index 4586639..47b8208 100644
--- a/lib/_range.py
+++ b/lib/_range.py
@@ -133,12 +133,43 @@ class Range(object):
def __hash__(self):
return hash((self._lower, self._upper, self._bounds))
+ # as the postgres docs describe for the server-side stuff,
+ # ordering is rather arbitrary, but will remain stable
+ # and consistent.
+
def __lt__(self, other):
- raise TypeError(
- 'Range objects cannot be ordered; please refer to the PostgreSQL'
- ' documentation to perform this operation in the database')
+ if not isinstance(other, Range):
+ return NotImplemented
+ for attr in ('_lower', '_upper', '_bounds'):
+ self_value = getattr(self, attr)
+ other_value = getattr(other, attr)
+ if self_value == other_value:
+ pass
+ elif self_value is None:
+ return True
+ elif other_value is None:
+ return False
+ else:
+ return self_value < other_value
+ return False
- __le__ = __gt__ = __ge__ = __lt__
+ def __le__(self, other):
+ if self == other:
+ return True
+ else:
+ return self.__lt__(other)
+
+ def __gt__(self, other):
+ if isinstance(other, Range):
+ return other.__lt__(self)
+ else:
+ return NotImplemented
+
+ def __ge__(self, other):
+ if self == other:
+ return True
+ else:
+ return self.__gt__(other)
def register_range(pgrange, pyrange, conn_or_curs, globally=False):
diff --git a/tests/test_types_extras.py b/tests/test_types_extras.py
index 4fedd5e..4625995 100755
--- a/tests/test_types_extras.py
+++ b/tests/test_types_extras.py
@@ -13,6 +13,7 @@
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
# License for more details.
+from __future__ import with_statement
import re
import sys
@@ -22,6 +23,7 @@ from functools import wraps
from testutils import unittest, skip_if_no_uuid, skip_before_postgres
from testutils import ConnectingTestCase, decorate_all_tests
+from testutils import py3_raises_typeerror
import psycopg2
import psycopg2.extras
@@ -1236,12 +1238,73 @@ class RangeTestCase(unittest.TestCase):
self.assertEqual(Range(10, 20), IntRange(10, 20))
self.assertEqual(PositiveIntRange(10, 20), IntRange(10, 20))
- def test_not_ordered(self):
+ # as the postgres docs describe for the server-side stuff,
+ # ordering is rather arbitrary, but will remain stable
+ # and consistent.
+
+ def test_lt_ordering(self):
+ from psycopg2.extras import Range
+ self.assert_(Range(empty=True) < Range(0, 4))
+ self.assert_(not Range(1, 2) < Range(0, 4))
+ self.assert_(Range(0, 4) < Range(1, 2))
+ self.assert_(not Range(1, 2) < Range())
+ self.assert_(Range() < Range(1, 2))
+ self.assert_(not Range(1) < Range(upper=1))
+ self.assert_(not Range() < Range())
+ self.assert_(not Range(empty=True) < Range(empty=True))
+ self.assert_(not Range(1, 2) < Range(1, 2))
+ with py3_raises_typeerror():
+ self.assert_(1 < Range(1, 2))
+ with py3_raises_typeerror():
+ self.assert_(not Range(1, 2) < 1)
+
+ def test_gt_ordering(self):
+ from psycopg2.extras import Range
+ self.assert_(not Range(empty=True) > Range(0, 4))
+ self.assert_(Range(1, 2) > Range(0, 4))
+ self.assert_(not Range(0, 4) > Range(1, 2))
+ self.assert_(Range(1, 2) > Range())
+ self.assert_(not Range() > Range(1, 2))
+ self.assert_(Range(1) > Range(upper=1))
+ self.assert_(not Range() > Range())
+ self.assert_(not Range(empty=True) > Range(empty=True))
+ self.assert_(not Range(1, 2) > Range(1, 2))
+ with py3_raises_typeerror():
+ self.assert_(not 1 > Range(1, 2))
+ with py3_raises_typeerror():
+ self.assert_(Range(1, 2) > 1)
+
+ def test_le_ordering(self):
+ from psycopg2.extras import Range
+ self.assert_(Range(empty=True) <= Range(0, 4))
+ self.assert_(not Range(1, 2) <= Range(0, 4))
+ self.assert_(Range(0, 4) <= Range(1, 2))
+ self.assert_(not Range(1, 2) <= Range())
+ self.assert_(Range() <= Range(1, 2))
+ self.assert_(not Range(1) <= Range(upper=1))
+ self.assert_(Range() <= Range())
+ self.assert_(Range(empty=True) <= Range(empty=True))
+ self.assert_(Range(1, 2) <= Range(1, 2))
+ with py3_raises_typeerror():
+ self.assert_(1 <= Range(1, 2))
+ with py3_raises_typeerror():
+ self.assert_(not Range(1, 2) <= 1)
+
+ def test_ge_ordering(self):
from psycopg2.extras import Range
- self.assertRaises(TypeError, lambda: Range(empty=True) < Range(0,4))
- self.assertRaises(TypeError, lambda: Range(1,2) > Range(0,4))
- self.assertRaises(TypeError, lambda: Range(1,2) <= Range())
- self.assertRaises(TypeError, lambda: Range(1,2) >= Range())
+ self.assert_(not Range(empty=True) >= Range(0, 4))
+ self.assert_(Range(1, 2) >= Range(0, 4))
+ self.assert_(not Range(0, 4) >= Range(1, 2))
+ self.assert_(Range(1, 2) >= Range())
+ self.assert_(not Range() >= Range(1, 2))
+ self.assert_(Range(1) >= Range(upper=1))
+ self.assert_(Range() >= Range())
+ self.assert_(Range(empty=True) >= Range(empty=True))
+ self.assert_(Range(1, 2) >= Range(1, 2))
+ with py3_raises_typeerror():
+ self.assert_(not 1 >= Range(1, 2))
+ with py3_raises_typeerror():
+ self.assert_(Range(1, 2) >= 1)
def skip_if_no_range(f):
diff --git a/tests/testutils.py b/tests/testutils.py
index 708dd22..0569ede 100644
--- a/tests/testutils.py
+++ b/tests/testutils.py
@@ -329,3 +329,13 @@ def script_to_py3(script):
f2.close()
os.remove(filename)
+class py3_raises_typeerror(object):
+
+ def __enter__(self):
+ pass
+
+ def __exit__(self, type, exc, tb):
+ if sys.version_info[0] >= 3:
+ assert type is TypeError
+ return True
+