From d469c325037d09de0c478f362b35d6035e3ac43a Mon Sep 17 00:00:00 2001 From: Chris Withers Date: Wed, 12 Feb 2014 08:11:59 +0000 Subject: [PATCH 1/8] 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/8] 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/8] 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/8] 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 From 4545d1636ca40d9aef05504a271cff862f0ed03b Mon Sep 17 00:00:00 2001 From: Daniele Varrazzo Date: Sat, 22 Feb 2014 21:51:28 +0000 Subject: [PATCH 5/8] Added implementation for Range gt and ge operators Using a common implementation for all the operators. Note that lt is the one used by sort so it's nice it's the fastest. --- lib/_range.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/lib/_range.py b/lib/_range.py index 47a3e0e4..29e18368 100644 --- a/lib/_range.py +++ b/lib/_range.py @@ -154,20 +154,22 @@ class Range(object): return False def __le__(self, other): - if not isinstance(other, Range): + 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 - 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 __ge__(self, other): + if self == other: + return True + else: + return self.__gt__(other) def register_range(pgrange, pyrange, conn_or_curs, globally=False): From 8937c635df2699c7869911764dd079d8fdd3d4ca Mon Sep 17 00:00:00 2001 From: Daniele Varrazzo Date: Sat, 22 Feb 2014 21:52:44 +0000 Subject: [PATCH 6/8] Hardcode the list of attributes to be used in comparison Comparing Range subclasses may lead to surprises. --- lib/_range.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/_range.py b/lib/_range.py index 29e18368..35516ac7 100644 --- a/lib/_range.py +++ b/lib/_range.py @@ -140,7 +140,7 @@ class Range(object): def __lt__(self, other): if not isinstance(other, Range): return NotImplemented - for attr in self.__slots__: + for attr in ('_lower', '_upper', '_bounds'): self_value = getattr(self, attr) other_value = getattr(other, attr) if self_value == other_value: From 212f4e353835b8717f99e2090286e8d68f306f42 Mon Sep 17 00:00:00 2001 From: Daniele Varrazzo Date: Sat, 22 Feb 2014 21:56:46 +0000 Subject: [PATCH 7/8] Docs wordsmithing about Range order --- doc/src/extras.rst | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/doc/src/extras.rst b/doc/src/extras.rst index 52a7056f..7fab3384 100644 --- a/doc/src/extras.rst +++ b/doc/src/extras.rst @@ -437,14 +437,17 @@ 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. 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. + 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 query arguments. To use range objects as query arguments you can either From 8d2744c550fef74c518ef14bd304e0bc880c84c7 Mon Sep 17 00:00:00 2001 From: Daniele Varrazzo Date: Sat, 22 Feb 2014 22:51:02 +0000 Subject: [PATCH 8/8] Mention Range order in the news file --- NEWS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/NEWS b/NEWS index 02953649..b7153209 100644 --- a/NEWS +++ b/NEWS @@ -4,6 +4,8 @@ Current release 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.