From 1d7a27413f77bd47072f5c0cbda0036c6aaa78d7 Mon Sep 17 00:00:00 2001 From: Konsta Vesterinen Date: Sun, 5 Jan 2014 05:28:58 +0200 Subject: [PATCH] Improved NumberRange handling --- sqlalchemy_utils/primitives/number_range.py | 43 ++++++++++++++------- tests/primitives/test_number_range.py | 35 +++++++++++++++++ tests/types/test_number_range.py | 7 ++-- 3 files changed, 68 insertions(+), 17 deletions(-) diff --git a/sqlalchemy_utils/primitives/number_range.py b/sqlalchemy_utils/primitives/number_range.py index e8349f6..548cc84 100644 --- a/sqlalchemy_utils/primitives/number_range.py +++ b/sqlalchemy_utils/primitives/number_range.py @@ -65,7 +65,8 @@ class NumberRange(object): def lower(self, value): if value is None: self._lower = -float('inf') - self._lower = value + else: + self._lower = value @property def upper(self): @@ -75,7 +76,22 @@ class NumberRange(object): def upper(self, value): if value is None: self._upper = float('inf') - self._upper = value + else: + self._upper = value + + @property + def open(self): + """ + Returns whether or not this object is an open interval. + """ + return not self.lower_inc and not self.upper_inc + + @property + def closed(self): + """ + Returns whether or not this object is a closed interval. + """ + return self.lower_inc and self.upper_inc def parse_object(self, obj): self.lower = obj.lower @@ -93,7 +109,10 @@ class NumberRange(object): lower, upper = seq self.lower = parse_number(lower) self.upper = parse_number(upper) - self.lower_inc = self.upper_inc = True + if isinstance(seq, tuple): + self.lower_inc = self.upper_inc = False + else: + self.lower_inc = self.upper_inc = True def parse_integer(self, value): self.lower = self.upper = value @@ -125,14 +144,8 @@ class NumberRange(object): except ValueError as e: raise NumberRangeException(e.message) - self.lower_inc = value[0] == '(' - if self.lower_inc: - lower += 1 - - self.upper_inc = value[-1] == ')' - if self.upper_inc: - upper -= 1 - + self.lower_inc = value[0] == '[' + self.upper_inc = value[-1] == ']' self.lower = lower self.upper = upper @@ -151,9 +164,11 @@ class NumberRange(object): @property def normalized(self): - return '[%s, %s]' % ( - self.lower if self.lower is not None else '', - self.upper if self.upper is not None else '' + return '%s%s, %s%s' % ( + '[' if self.lower_inc else '(', + self.lower if self.lower != -float('inf') else '', + self.upper if self.upper != float('inf') else '', + ']' if self.upper_inc else ')' ) def __eq__(self, other): diff --git a/tests/primitives/test_number_range.py b/tests/primitives/test_number_range.py index 2255aa3..6b9f70f 100644 --- a/tests/primitives/test_number_range.py +++ b/tests/primitives/test_number_range.py @@ -30,6 +30,11 @@ class TestNumberRangeInit(object): assert num_range.lower == 1 assert num_range.upper == 3 + def test_empty_string_as_upper_bound(self): + num_range = NumberRange('[1,)') + assert num_range.lower == 1 + assert num_range.upper == float('inf') + def test_supports_exact_ranges_as_strings(self): num_range = NumberRange('3') assert num_range.lower == 3 @@ -93,6 +98,36 @@ def test_raises_exception_for_badly_constructed_range(number_range): NumberRange(number_range) +@mark.parametrize(('number_range', 'is_open'), + ( + ((2, 3), True), + ('(2, 5)', True), + ('[3, 4)', False), + ('(4, 5]', False), + ('3 - 4', False), + ([4, 5], False), + ('[4, 5]', False) + ) +) +def test_open(number_range, is_open): + assert NumberRange(number_range).open == is_open + + +@mark.parametrize(('number_range', 'is_closed'), + ( + ((2, 3), False), + ('(2, 5)', False), + ('[3, 4)', False), + ('(4, 5]', False), + ('3 - 4', True), + ([4, 5], True), + ('[4, 5]', True) + ) +) +def test_closed(number_range, is_closed): + assert NumberRange(number_range).closed == is_closed + + class TestArithmeticOperators(object): def test_add_operator(self): assert NumberRange(1, 2) + NumberRange(1, 2) == NumberRange(2, 4) diff --git a/tests/types/test_number_range.py b/tests/types/test_number_range.py index b33f2d0..18e142c 100644 --- a/tests/types/test_number_range.py +++ b/tests/types/test_number_range.py @@ -36,9 +36,10 @@ class TestNumberRangeType(TestCase): ) self.session.add(building) self.session.commit() + building = self.session.query(self.Building).first() assert building.persons_at_night.lower == 1 - assert building.persons_at_night.upper is None + assert building.persons_at_night.upper == float('inf') def test_infinite_lower_bound(self): building = self.Building( @@ -47,8 +48,8 @@ class TestNumberRangeType(TestCase): self.session.add(building) self.session.commit() building = self.session.query(self.Building).first() - assert building.persons_at_night.lower is None - assert building.persons_at_night.upper is 1 + assert building.persons_at_night.lower == -float('inf') + assert building.persons_at_night.upper == 1 def test_nullify_number_range(self): building = self.Building(