From f739576f0a8461b8e40f3b278565a6b2a870c3ab 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 45866392..73709861 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 ca1845477d694f1b2bffcc72578d26a89f9dd4f5 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 1487800b6d1495f3ba48082dc6ec1141b29c794f 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 73709861..b1172811 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 5624ad4ec5b25da9b13c5a97c5d089df4d89bfd1 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 c10c1186a56becb825708bebfeb5590d954856e6 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 b1172811..c4e0d4e8 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 6c27cdd20e8a65467d8f6e05798a5a2270a9bae1 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 c4e0d4e8..47b82086 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 d43e23ddc601b09329410049a3f6e5920e46d918 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 0e86fc164fa47edfca30121a011eb3dc9238d1e8 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.