From d469c325037d09de0c478f362b35d6035e3ac43a Mon Sep 17 00:00:00 2001 From: Chris Withers Date: Wed, 12 Feb 2014 08:11:59 +0000 Subject: [PATCH 1/4] Provide a stable and consistent sort order for Range objects. This matches postgres server-side behaviour and helps client applications that need to sort based on the primary key of tables where the primary key is or contains a range. --- lib/_range.py | 19 +++++++++--- tests/test_types_extras.py | 63 +++++++++++++++++++++++++++++++++++--- 2 files changed, 72 insertions(+), 10 deletions(-) diff --git a/lib/_range.py b/lib/_range.py index 0f159908..5a794103 100644 --- a/lib/_range.py +++ b/lib/_range.py @@ -133,12 +133,21 @@ class Range(object): def __hash__(self): return hash((self._lower, self._upper, self._bounds)) - def __lt__(self, other): - raise TypeError( - 'Range objects cannot be ordered; please refer to the PostgreSQL' - ' documentation to perform this operation in the database') + # as the postgres docs describe for the server-side stuff, + # ordering is rather arbitrary, but will remain stable + # and consistent. - __le__ = __gt__ = __ge__ = __lt__ + def __lt__(self, other): + if not isinstance(other, Range): + return False + return ((self._lower, self._upper, self._bounds) < + (other._lower, other._upper, other._bounds)) + + def __le__(self, other): + if not isinstance(other, Range): + return False + return ((self._lower, self._upper, self._bounds) <= + (other._lower, other._upper, other._bounds)) 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 96ffcd3c..30ff9c7a 100755 --- a/tests/test_types_extras.py +++ b/tests/test_types_extras.py @@ -1225,12 +1225,65 @@ 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_ordering_lt(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.assertTrue(Range(empty=True) < Range(0, 4)) + self.assertFalse(Range(1, 2) < Range(0, 4)) + self.assertTrue(Range(0, 4) < Range(1, 2)) + self.assertFalse(Range(1, 2) < Range()) + self.assertTrue(Range() < Range(1, 2)) + self.assertFalse(Range(1) < Range(upper=1)) + self.assertFalse(Range() < Range()) + self.assertFalse(Range(empty=True) < Range(empty=True)) + self.assertFalse(Range(1, 2) < Range(1, 2)) + self.assertTrue(1 < Range(1, 2)) + self.assertFalse(Range(1, 2) < 1) + + def test_ordering_gt(self): + from psycopg2.extras import Range + self.assertFalse(Range(empty=True) > Range(0, 4)) + self.assertTrue(Range(1, 2) > Range(0, 4)) + self.assertFalse(Range(0, 4) > Range(1, 2)) + self.assertTrue(Range(1, 2) > Range()) + self.assertFalse(Range() > Range(1, 2)) + self.assertTrue(Range(1) > Range(upper=1)) + self.assertFalse(Range() > Range()) + self.assertFalse(Range(empty=True) > Range(empty=True)) + self.assertFalse(Range(1, 2) > Range(1, 2)) + self.assertFalse(1 > Range(1, 2)) + self.assertTrue(Range(1, 2) > 1) + + def test_ordering_le(self): + from psycopg2.extras import Range + self.assertTrue(Range(empty=True) <= Range(0, 4)) + self.assertFalse(Range(1, 2) <= Range(0, 4)) + self.assertTrue(Range(0, 4) <= Range(1, 2)) + self.assertFalse(Range(1, 2) <= Range()) + self.assertTrue(Range() <= Range(1, 2)) + self.assertFalse(Range(1) <= Range(upper=1)) + self.assertTrue(Range() <= Range()) + self.assertTrue(Range(empty=True) <= Range(empty=True)) + self.assertTrue(Range(1, 2) <= Range(1, 2)) + self.assertTrue(1 <= Range(1, 2)) + self.assertFalse(Range(1, 2) <= 1) + + def test_ordering_ge(self): + from psycopg2.extras import Range + self.assertFalse(Range(empty=True) >= Range(0, 4)) + self.assertTrue(Range(1, 2) >= Range(0, 4)) + self.assertFalse(Range(0, 4) >= Range(1, 2)) + self.assertTrue(Range(1, 2) >= Range()) + self.assertFalse(Range() >= Range(1, 2)) + self.assertTrue(Range(1) >= Range(upper=1)) + self.assertTrue(Range() >= Range()) + self.assertTrue(Range(empty=True) >= Range(empty=True)) + self.assertTrue(Range(1, 2) >= Range(1, 2)) + self.assertFalse(1 >= Range(1, 2)) + self.assertTrue(Range(1, 2) >= 1) def skip_if_no_range(f): From bae508ffa65dae9d99ccb9e4acd6c47109a9d3a6 Mon Sep 17 00:00:00 2001 From: Chris Withers Date: Fri, 14 Feb 2014 07:46:32 +0000 Subject: [PATCH 2/4] Coding style changes. --- tests/test_types_extras.py | 96 +++++++++++++++++++------------------- 1 file changed, 48 insertions(+), 48 deletions(-) diff --git a/tests/test_types_extras.py b/tests/test_types_extras.py index 30ff9c7a..a7d9da28 100755 --- a/tests/test_types_extras.py +++ b/tests/test_types_extras.py @@ -1229,61 +1229,61 @@ class RangeTestCase(unittest.TestCase): # ordering is rather arbitrary, but will remain stable # and consistent. - def test_ordering_lt(self): + def test_lt_ordering(self): from psycopg2.extras import Range - self.assertTrue(Range(empty=True) < Range(0, 4)) - self.assertFalse(Range(1, 2) < Range(0, 4)) - self.assertTrue(Range(0, 4) < Range(1, 2)) - self.assertFalse(Range(1, 2) < Range()) - self.assertTrue(Range() < Range(1, 2)) - self.assertFalse(Range(1) < Range(upper=1)) - self.assertFalse(Range() < Range()) - self.assertFalse(Range(empty=True) < Range(empty=True)) - self.assertFalse(Range(1, 2) < Range(1, 2)) - self.assertTrue(1 < Range(1, 2)) - self.assertFalse(Range(1, 2) < 1) + 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)) + self.assert_(1 < Range(1, 2)) + self.assert_(not Range(1, 2) < 1) - def test_ordering_gt(self): + def test_gt_ordering(self): from psycopg2.extras import Range - self.assertFalse(Range(empty=True) > Range(0, 4)) - self.assertTrue(Range(1, 2) > Range(0, 4)) - self.assertFalse(Range(0, 4) > Range(1, 2)) - self.assertTrue(Range(1, 2) > Range()) - self.assertFalse(Range() > Range(1, 2)) - self.assertTrue(Range(1) > Range(upper=1)) - self.assertFalse(Range() > Range()) - self.assertFalse(Range(empty=True) > Range(empty=True)) - self.assertFalse(Range(1, 2) > Range(1, 2)) - self.assertFalse(1 > Range(1, 2)) - self.assertTrue(Range(1, 2) > 1) + 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)) + self.assert_(not 1 > Range(1, 2)) + self.assert_(Range(1, 2) > 1) - def test_ordering_le(self): + def test_le_ordering(self): from psycopg2.extras import Range - self.assertTrue(Range(empty=True) <= Range(0, 4)) - self.assertFalse(Range(1, 2) <= Range(0, 4)) - self.assertTrue(Range(0, 4) <= Range(1, 2)) - self.assertFalse(Range(1, 2) <= Range()) - self.assertTrue(Range() <= Range(1, 2)) - self.assertFalse(Range(1) <= Range(upper=1)) - self.assertTrue(Range() <= Range()) - self.assertTrue(Range(empty=True) <= Range(empty=True)) - self.assertTrue(Range(1, 2) <= Range(1, 2)) - self.assertTrue(1 <= Range(1, 2)) - self.assertFalse(Range(1, 2) <= 1) + 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)) + self.assert_(1 <= Range(1, 2)) + self.assert_(not Range(1, 2) <= 1) - def test_ordering_ge(self): + def test_ge_ordering(self): from psycopg2.extras import Range - self.assertFalse(Range(empty=True) >= Range(0, 4)) - self.assertTrue(Range(1, 2) >= Range(0, 4)) - self.assertFalse(Range(0, 4) >= Range(1, 2)) - self.assertTrue(Range(1, 2) >= Range()) - self.assertFalse(Range() >= Range(1, 2)) - self.assertTrue(Range(1) >= Range(upper=1)) - self.assertTrue(Range() >= Range()) - self.assertTrue(Range(empty=True) >= Range(empty=True)) - self.assertTrue(Range(1, 2) >= Range(1, 2)) - self.assertFalse(1 >= Range(1, 2)) - self.assertTrue(Range(1, 2) >= 1) + 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)) + self.assert_(not 1 >= Range(1, 2)) + self.assert_(Range(1, 2) >= 1) def skip_if_no_range(f): From e60266c4c5273807f3a097b4a10c437f0ce079c5 Mon Sep 17 00:00:00 2001 From: Chris Withers Date: Tue, 18 Feb 2014 20:55:00 +0000 Subject: [PATCH 3/4] New implementation of Range sorting that works for Python 2.5 to 3.3, at least. --- lib/_range.py | 32 ++++++++++++++++++++++++++------ tests/test_types_extras.py | 26 ++++++++++++++++++-------- tests/testutils.py | 10 ++++++++++ 3 files changed, 54 insertions(+), 14 deletions(-) diff --git a/lib/_range.py b/lib/_range.py index 5a794103..47a3e0e4 100644 --- a/lib/_range.py +++ b/lib/_range.py @@ -139,15 +139,35 @@ class Range(object): def __lt__(self, other): if not isinstance(other, Range): - return False - return ((self._lower, self._upper, self._bounds) < - (other._lower, other._upper, other._bounds)) + return NotImplemented + for attr in self.__slots__: + 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 def __le__(self, other): if not isinstance(other, Range): - return False - return ((self._lower, self._upper, self._bounds) <= - (other._lower, other._upper, other._bounds)) + return NotImplemented + for attr in self.__slots__: + 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 True 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 a7d9da28..de3bebe9 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 @@ -1240,8 +1242,10 @@ class RangeTestCase(unittest.TestCase): self.assert_(not Range() < Range()) self.assert_(not Range(empty=True) < Range(empty=True)) self.assert_(not Range(1, 2) < Range(1, 2)) - self.assert_(1 < Range(1, 2)) - self.assert_(not Range(1, 2) < 1) + 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 @@ -1254,8 +1258,10 @@ class RangeTestCase(unittest.TestCase): self.assert_(not Range() > Range()) self.assert_(not Range(empty=True) > Range(empty=True)) self.assert_(not Range(1, 2) > Range(1, 2)) - self.assert_(not 1 > Range(1, 2)) - self.assert_(Range(1, 2) > 1) + 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 @@ -1268,8 +1274,10 @@ class RangeTestCase(unittest.TestCase): self.assert_(Range() <= Range()) self.assert_(Range(empty=True) <= Range(empty=True)) self.assert_(Range(1, 2) <= Range(1, 2)) - self.assert_(1 <= Range(1, 2)) - self.assert_(not Range(1, 2) <= 1) + 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 @@ -1282,8 +1290,10 @@ class RangeTestCase(unittest.TestCase): self.assert_(Range() >= Range()) self.assert_(Range(empty=True) >= Range(empty=True)) self.assert_(Range(1, 2) >= Range(1, 2)) - self.assert_(not 1 >= Range(1, 2)) - self.assert_(Range(1, 2) >= 1) + 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 708dd224..0569edef 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 + From 6cd0647da96fdc009b4f30e520bb67778c2a8738 Mon Sep 17 00:00:00 2001 From: Chris Withers Date: Tue, 18 Feb 2014 21:24:59 +0000 Subject: [PATCH 4/4] documentation changes now that Range objects can be ordered --- doc/src/extras.rst | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/doc/src/extras.rst b/doc/src/extras.rst index a0a2d1ca..52a7056f 100644 --- a/doc/src/extras.rst +++ b/doc/src/extras.rst @@ -437,9 +437,14 @@ 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 + equivalence. Empty ranges evaluate to `!False` in boolean context, nonempty evaluate to `!True`. + `!Range` objects can be sorted although, as on the server-side, + this ordering is not particularly meangingful. + + .. versionchanged:: 2.5.3 + Although it is possible to instantiate `!Range` objects, the class doesn't have an adapter registered, so you cannot normally pass these instances as query arguments. To use range objects as query arguments you can either