Remove NumberRange, say hello to intervals
This commit is contained in:
1
setup.py
1
setup.py
@@ -24,6 +24,7 @@ extras_require = {
|
|||||||
'anyjson': ['anyjson>=0.3.3'],
|
'anyjson': ['anyjson>=0.3.3'],
|
||||||
'babel': ['Babel>=1.3'],
|
'babel': ['Babel>=1.3'],
|
||||||
'arrow': ['arrow>=0.3.4'],
|
'arrow': ['arrow>=0.3.4'],
|
||||||
|
'intervals': ['intervals>=0.2.0'],
|
||||||
'phone': [
|
'phone': [
|
||||||
# The phonenumbers library has a split for 2.x and 3.x support.
|
# The phonenumbers library has a split for 2.x and 3.x support.
|
||||||
'phonenumbers3k==5.6b1' if PY3 else 'phonenumbers<5.6b1'
|
'phonenumbers3k==5.6b1' if PY3 else 'phonenumbers<5.6b1'
|
||||||
|
@@ -21,7 +21,6 @@ from .functions import (
|
|||||||
from .listeners import coercion_listener
|
from .listeners import coercion_listener
|
||||||
from .merge import merge, Merger
|
from .merge import merge, Merger
|
||||||
from .generic import generic_relationship
|
from .generic import generic_relationship
|
||||||
from .primitives import NumberRange, NumberRangeException
|
|
||||||
from .proxy_dict import ProxyDict, proxy_dict
|
from .proxy_dict import ProxyDict, proxy_dict
|
||||||
from .types import (
|
from .types import (
|
||||||
ArrowType,
|
ArrowType,
|
||||||
@@ -33,6 +32,7 @@ from .types import (
|
|||||||
EmailType,
|
EmailType,
|
||||||
instrumented_list,
|
instrumented_list,
|
||||||
InstrumentedList,
|
InstrumentedList,
|
||||||
|
IntRangeType,
|
||||||
IPAddressType,
|
IPAddressType,
|
||||||
JSONType,
|
JSONType,
|
||||||
LocaleType,
|
LocaleType,
|
||||||
@@ -40,14 +40,16 @@ from .types import (
|
|||||||
PasswordType,
|
PasswordType,
|
||||||
PhoneNumber,
|
PhoneNumber,
|
||||||
PhoneNumberType,
|
PhoneNumberType,
|
||||||
NumberRangeRawType,
|
|
||||||
NumberRangeType,
|
|
||||||
ScalarListType,
|
ScalarListType,
|
||||||
ScalarListException,
|
ScalarListException,
|
||||||
TimezoneType,
|
TimezoneType,
|
||||||
TSVectorType,
|
TSVectorType,
|
||||||
URLType,
|
URLType,
|
||||||
UUIDType,
|
UUIDType,
|
||||||
|
INT4RANGE,
|
||||||
|
INT8RANGE,
|
||||||
|
DATERANGE,
|
||||||
|
NUMRANGE,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -88,10 +90,7 @@ __all__ = (
|
|||||||
JSONType,
|
JSONType,
|
||||||
LocaleType,
|
LocaleType,
|
||||||
Merger,
|
Merger,
|
||||||
NumberRange,
|
IntRangeType,
|
||||||
NumberRangeException,
|
|
||||||
NumberRangeRawType,
|
|
||||||
NumberRangeType,
|
|
||||||
Password,
|
Password,
|
||||||
PasswordType,
|
PasswordType,
|
||||||
PhoneNumber,
|
PhoneNumber,
|
||||||
@@ -105,5 +104,9 @@ __all__ = (
|
|||||||
UUIDType,
|
UUIDType,
|
||||||
database_exists,
|
database_exists,
|
||||||
create_database,
|
create_database,
|
||||||
drop_database
|
drop_database,
|
||||||
|
INT4RANGE,
|
||||||
|
INT8RANGE,
|
||||||
|
DATERANGE,
|
||||||
|
NUMRANGE,
|
||||||
)
|
)
|
||||||
|
@@ -1,14 +1,8 @@
|
|||||||
from .number_range import (
|
|
||||||
NumberRange, NumberRangeException, RangeBoundsException
|
|
||||||
)
|
|
||||||
from .weekday import WeekDay
|
from .weekday import WeekDay
|
||||||
from .weekdays import WeekDays
|
from .weekdays import WeekDays
|
||||||
|
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
NumberRange,
|
|
||||||
NumberRangeException,
|
|
||||||
RangeBoundsException,
|
|
||||||
WeekDay,
|
WeekDay,
|
||||||
WeekDays
|
WeekDays
|
||||||
)
|
)
|
||||||
|
@@ -1,309 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from collections import Iterable
|
|
||||||
from decimal import Decimal
|
|
||||||
try:
|
|
||||||
from functools import total_ordering
|
|
||||||
except ImportError:
|
|
||||||
from total_ordering import total_ordering
|
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
|
|
||||||
class NumberRangeException(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class RangeBoundsException(NumberRangeException):
|
|
||||||
def __init__(self, min_value, max_value):
|
|
||||||
self.message = 'Min value %s is bigger than max value %s.' % (
|
|
||||||
min_value,
|
|
||||||
max_value
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def is_number(number):
|
|
||||||
return isinstance(number, (float, int, Decimal))
|
|
||||||
|
|
||||||
|
|
||||||
def parse_number(number):
|
|
||||||
if number is None or number == '':
|
|
||||||
return None
|
|
||||||
elif is_number(number):
|
|
||||||
return number
|
|
||||||
else:
|
|
||||||
return int(number)
|
|
||||||
|
|
||||||
|
|
||||||
@total_ordering
|
|
||||||
class NumberRange(object):
|
|
||||||
def __init__(self, *args):
|
|
||||||
"""
|
|
||||||
Parses given args and assigns lower and upper bound for this number
|
|
||||||
range.
|
|
||||||
|
|
||||||
1. Comma separated string argument
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
|
|
||||||
>>> range = NumberRange('[23, 45]')
|
|
||||||
>>> range.lower
|
|
||||||
23
|
|
||||||
>>> range.upper
|
|
||||||
45
|
|
||||||
|
|
||||||
|
|
||||||
>>> range = NumberRange('(23, 45]')
|
|
||||||
>>> range.lower_inc
|
|
||||||
False
|
|
||||||
|
|
||||||
>>> range = NumberRange('(23, 45)')
|
|
||||||
>>> range.lower_inc
|
|
||||||
False
|
|
||||||
>>> range.upper_inc
|
|
||||||
False
|
|
||||||
|
|
||||||
2. Sequence of arguments
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
|
|
||||||
>>> range = NumberRange(23, 45)
|
|
||||||
>>> range.lower
|
|
||||||
23
|
|
||||||
>>> range.upper
|
|
||||||
45
|
|
||||||
|
|
||||||
|
|
||||||
3. Lists and tuples as an argument
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
|
|
||||||
>>> range = NumberRange([23, 45])
|
|
||||||
>>> range.lower
|
|
||||||
23
|
|
||||||
>>> range.upper
|
|
||||||
45
|
|
||||||
>>> range.closed
|
|
||||||
True
|
|
||||||
|
|
||||||
|
|
||||||
>>> range = NumberRange((23, 45))
|
|
||||||
>>> range.lower
|
|
||||||
23
|
|
||||||
>>> range.closed
|
|
||||||
False
|
|
||||||
|
|
||||||
4. Integer argument
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
|
|
||||||
>>> range = NumberRange(34)
|
|
||||||
>>> range.lower == range.upper == 34
|
|
||||||
True
|
|
||||||
|
|
||||||
|
|
||||||
5. Object argument
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
>>> range = NumberRange(NumberRange(20, 30))
|
|
||||||
>>> range.lower
|
|
||||||
20
|
|
||||||
>>> range.upper
|
|
||||||
30
|
|
||||||
|
|
||||||
"""
|
|
||||||
if len(args) > 2:
|
|
||||||
raise NumberRangeException(
|
|
||||||
'NumberRange takes at most two arguments'
|
|
||||||
)
|
|
||||||
elif len(args) == 2:
|
|
||||||
self.parse_sequence(args)
|
|
||||||
else:
|
|
||||||
arg, = args
|
|
||||||
if isinstance(arg, six.integer_types):
|
|
||||||
self.parse_integer(arg)
|
|
||||||
elif isinstance(arg, six.string_types):
|
|
||||||
self.parse_string(arg)
|
|
||||||
elif isinstance(arg, Iterable):
|
|
||||||
self.parse_sequence(arg)
|
|
||||||
elif hasattr(arg, 'lower') and hasattr(arg, 'upper'):
|
|
||||||
self.parse_object(arg)
|
|
||||||
|
|
||||||
if self.lower > self.upper:
|
|
||||||
raise RangeBoundsException(self.lower, self.upper)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def lower(self):
|
|
||||||
return self._lower
|
|
||||||
|
|
||||||
@lower.setter
|
|
||||||
def lower(self, value):
|
|
||||||
if value is None:
|
|
||||||
self._lower = -float('inf')
|
|
||||||
else:
|
|
||||||
self._lower = value
|
|
||||||
|
|
||||||
@property
|
|
||||||
def upper(self):
|
|
||||||
return self._upper
|
|
||||||
|
|
||||||
@upper.setter
|
|
||||||
def upper(self, value):
|
|
||||||
if value is None:
|
|
||||||
self._upper = float('inf')
|
|
||||||
else:
|
|
||||||
self._upper = value
|
|
||||||
|
|
||||||
@property
|
|
||||||
def open(self):
|
|
||||||
"""
|
|
||||||
Returns whether or not this object is an open interval.
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
range = NumberRange('(23, 45)')
|
|
||||||
range.open # True
|
|
||||||
|
|
||||||
range = NumberRange('[23, 45]')
|
|
||||||
range.open # False
|
|
||||||
"""
|
|
||||||
return not self.lower_inc and not self.upper_inc
|
|
||||||
|
|
||||||
@property
|
|
||||||
def closed(self):
|
|
||||||
"""
|
|
||||||
Returns whether or not this object is a closed interval.
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
range = NumberRange('(23, 45)')
|
|
||||||
range.closed # False
|
|
||||||
|
|
||||||
range = NumberRange('[23, 45]')
|
|
||||||
range.closed # True
|
|
||||||
"""
|
|
||||||
return self.lower_inc and self.upper_inc
|
|
||||||
|
|
||||||
def parse_object(self, obj):
|
|
||||||
self.lower = obj.lower
|
|
||||||
self.upper = obj.upper
|
|
||||||
self.lower_inc = obj.lower_inc
|
|
||||||
self.upper_inc = obj.upper_inc
|
|
||||||
|
|
||||||
def parse_string(self, value):
|
|
||||||
if ',' not in value:
|
|
||||||
self.parse_hyphen_range(value)
|
|
||||||
else:
|
|
||||||
self.parse_bounded_range(value)
|
|
||||||
|
|
||||||
def parse_sequence(self, seq):
|
|
||||||
lower, upper = seq
|
|
||||||
self.lower = parse_number(lower)
|
|
||||||
self.upper = parse_number(upper)
|
|
||||||
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
|
|
||||||
self.lower_inc = self.upper_inc = True
|
|
||||||
|
|
||||||
def parse_bounded_range(self, value):
|
|
||||||
values = value.strip()[1:-1].split(',')
|
|
||||||
try:
|
|
||||||
lower, upper = map(
|
|
||||||
lambda a: parse_number(a.strip()), values
|
|
||||||
)
|
|
||||||
except ValueError as e:
|
|
||||||
raise NumberRangeException(e.message)
|
|
||||||
|
|
||||||
self.lower_inc = value[0] == '['
|
|
||||||
self.upper_inc = value[-1] == ']'
|
|
||||||
self.lower = lower
|
|
||||||
self.upper = upper
|
|
||||||
|
|
||||||
def parse_hyphen_range(self, value):
|
|
||||||
values = value.split('-')
|
|
||||||
if len(values) == 1:
|
|
||||||
self.lower = self.upper = parse_number(value.strip())
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
self.lower, self.upper = map(
|
|
||||||
lambda a: parse_number(a.strip()), values
|
|
||||||
)
|
|
||||||
except ValueError as e:
|
|
||||||
raise NumberRangeException(str(e))
|
|
||||||
self.lower_inc = self.upper_inc = True
|
|
||||||
|
|
||||||
@property
|
|
||||||
def normalized(self):
|
|
||||||
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):
|
|
||||||
if isinstance(other, six.integer_types):
|
|
||||||
return self.lower == other == self.upper
|
|
||||||
try:
|
|
||||||
return (
|
|
||||||
self.lower == other.lower and
|
|
||||||
self.upper == other.upper
|
|
||||||
)
|
|
||||||
except AttributeError:
|
|
||||||
return NotImplemented
|
|
||||||
|
|
||||||
def __ne__(self, other):
|
|
||||||
return not (self == other)
|
|
||||||
|
|
||||||
def __gt__(self, other):
|
|
||||||
if isinstance(other, six.integer_types):
|
|
||||||
return self.lower > other and self.upper > other
|
|
||||||
|
|
||||||
try:
|
|
||||||
return self.lower > other.lower and self.upper > other.upper
|
|
||||||
except AttributeError:
|
|
||||||
return NotImplemented
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return 'NumberRange(%r, %r)' % (self.lower, self.upper)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
if self.lower != self.upper:
|
|
||||||
return '%s - %s' % (self.lower, self.upper)
|
|
||||||
return str(self.lower)
|
|
||||||
|
|
||||||
def __add__(self, other):
|
|
||||||
"""
|
|
||||||
[a, b] + [c, d] = [a + c, b + d]
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
return NumberRange(
|
|
||||||
self.lower + other.lower,
|
|
||||||
self.upper + other.upper
|
|
||||||
)
|
|
||||||
except AttributeError:
|
|
||||||
return NotImplemented
|
|
||||||
|
|
||||||
def __sub__(self, other):
|
|
||||||
"""
|
|
||||||
Defines the substraction operator.
|
|
||||||
|
|
||||||
As defined in wikipedia:
|
|
||||||
|
|
||||||
[a, b] − [c, d] = [a − d, b − c]
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
return NumberRange(
|
|
||||||
self.lower - other.upper,
|
|
||||||
self.upper - other.lower
|
|
||||||
)
|
|
||||||
except AttributeError:
|
|
||||||
return NotImplemented
|
|
@@ -9,8 +9,11 @@ from .ip_address import IPAddressType
|
|||||||
from .json import JSONType
|
from .json import JSONType
|
||||||
from .locale import LocaleType
|
from .locale import LocaleType
|
||||||
from .number_range import (
|
from .number_range import (
|
||||||
NumberRangeRawType,
|
INT4RANGE,
|
||||||
NumberRangeType,
|
INT8RANGE,
|
||||||
|
DATERANGE,
|
||||||
|
NUMRANGE,
|
||||||
|
IntRangeType,
|
||||||
)
|
)
|
||||||
from .password import Password, PasswordType
|
from .password import Password, PasswordType
|
||||||
from .phone_number import PhoneNumber, PhoneNumberType
|
from .phone_number import PhoneNumber, PhoneNumberType
|
||||||
@@ -33,8 +36,7 @@ __all__ = (
|
|||||||
IPAddressType,
|
IPAddressType,
|
||||||
JSONType,
|
JSONType,
|
||||||
LocaleType,
|
LocaleType,
|
||||||
NumberRangeRawType,
|
IntRangeType,
|
||||||
NumberRangeType,
|
|
||||||
Password,
|
Password,
|
||||||
PasswordType,
|
PasswordType,
|
||||||
PhoneNumber,
|
PhoneNumber,
|
||||||
@@ -47,7 +49,11 @@ __all__ = (
|
|||||||
UUIDType,
|
UUIDType,
|
||||||
WeekDay,
|
WeekDay,
|
||||||
WeekDays,
|
WeekDays,
|
||||||
WeekDaysType
|
WeekDaysType,
|
||||||
|
INT4RANGE,
|
||||||
|
INT8RANGE,
|
||||||
|
DATERANGE,
|
||||||
|
NUMRANGE,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@@ -1,10 +1,13 @@
|
|||||||
import six
|
intervals = None
|
||||||
|
try:
|
||||||
|
import intervals
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
from sqlalchemy import types
|
from sqlalchemy import types
|
||||||
from sqlalchemy_utils.primitives import NumberRange
|
|
||||||
from .scalar_coercible import ScalarCoercible
|
from .scalar_coercible import ScalarCoercible
|
||||||
|
|
||||||
|
|
||||||
class NumberRangeRawType(types.UserDefinedType):
|
class INT4RANGE(types.UserDefinedType):
|
||||||
"""
|
"""
|
||||||
Raw number range type, only supports PostgreSQL for now.
|
Raw number range type, only supports PostgreSQL for now.
|
||||||
"""
|
"""
|
||||||
@@ -12,40 +15,55 @@ class NumberRangeRawType(types.UserDefinedType):
|
|||||||
return 'int4range'
|
return 'int4range'
|
||||||
|
|
||||||
|
|
||||||
class NumberRangeType(types.TypeDecorator, ScalarCoercible):
|
class INT8RANGE(types.UserDefinedType):
|
||||||
|
def get_col_spec(self):
|
||||||
|
return 'int8range'
|
||||||
|
|
||||||
|
|
||||||
|
class NUMRANGE(types.UserDefinedType):
|
||||||
|
def get_col_spec(self):
|
||||||
|
return 'numrange'
|
||||||
|
|
||||||
|
|
||||||
|
class DATERANGE(types.UserDefinedType):
|
||||||
|
def get_col_spec(self):
|
||||||
|
return 'daterange'
|
||||||
|
|
||||||
|
|
||||||
|
class IntRangeType(types.TypeDecorator, ScalarCoercible):
|
||||||
"""
|
"""
|
||||||
NumberRangeType provides way for saving range of numbers into database.
|
IntRangeType provides way for saving range of numbers into database.
|
||||||
|
|
||||||
Example ::
|
Example ::
|
||||||
|
|
||||||
|
|
||||||
from sqlalchemy_utils import NumberRangeType, NumberRange
|
from sqlalchemy_utils import IntRangeType
|
||||||
|
|
||||||
|
|
||||||
class Event(Base):
|
class Event(Base):
|
||||||
__tablename__ = 'user'
|
__tablename__ = 'user'
|
||||||
id = sa.Column(sa.Integer, autoincrement=True)
|
id = sa.Column(sa.Integer, autoincrement=True)
|
||||||
name = sa.Column(sa.Unicode(255))
|
name = sa.Column(sa.Unicode(255))
|
||||||
estimated_number_of_persons = sa.Column(NumberRangeType)
|
estimated_number_of_persons = sa.Column(IntRangeType)
|
||||||
|
|
||||||
|
|
||||||
party = Event(name=u'party')
|
party = Event(name=u'party')
|
||||||
|
|
||||||
# we estimate the party to contain minium of 10 persons and at max
|
# we estimate the party to contain minium of 10 persons and at max
|
||||||
# 100 persons
|
# 100 persons
|
||||||
party.estimated_number_of_persons = NumberRange(10, 100)
|
party.estimated_number_of_persons = [10, 100]
|
||||||
|
|
||||||
print party.estimated_number_of_persons
|
print party.estimated_number_of_persons
|
||||||
# '10-100'
|
# '10-100'
|
||||||
|
|
||||||
|
|
||||||
NumberRange supports some arithmetic operators:
|
IntRange returns the values as IntInterval objects. These objects support many arithmetic operators:
|
||||||
::
|
::
|
||||||
|
|
||||||
|
|
||||||
meeting = Event(name=u'meeting')
|
meeting = Event(name=u'meeting')
|
||||||
|
|
||||||
meeting.estimated_number_of_persons = NumberRange(20, 40)
|
meeting.estimated_number_of_persons = [20, 40]
|
||||||
|
|
||||||
total = (
|
total = (
|
||||||
meeting.estimated_number_of_persons +
|
meeting.estimated_number_of_persons +
|
||||||
@@ -55,19 +73,19 @@ class NumberRangeType(types.TypeDecorator, ScalarCoercible):
|
|||||||
# '30-140'
|
# '30-140'
|
||||||
"""
|
"""
|
||||||
|
|
||||||
impl = NumberRangeRawType
|
impl = INT4RANGE
|
||||||
|
|
||||||
def process_bind_param(self, value, dialect):
|
def process_bind_param(self, value, dialect):
|
||||||
if value is not None:
|
if value is not None:
|
||||||
return value.normalized
|
return str(value)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def process_result_value(self, value, dialect):
|
def process_result_value(self, value, dialect):
|
||||||
if value:
|
if value:
|
||||||
return NumberRange(value)
|
return intervals.IntInterval(value)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def _coerce(self, value):
|
def _coerce(self, value):
|
||||||
if value is not None:
|
if value is not None:
|
||||||
value = NumberRange(value)
|
value = intervals.IntInterval(value)
|
||||||
return value
|
return value
|
||||||
|
@@ -1,146 +0,0 @@
|
|||||||
from pytest import raises, mark
|
|
||||||
from sqlalchemy_utils.primitives import (
|
|
||||||
NumberRange, NumberRangeException, RangeBoundsException
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TestNumberRangeInit(object):
|
|
||||||
def test_support_range_object(self):
|
|
||||||
num_range = NumberRange(NumberRange(1, 3))
|
|
||||||
assert num_range.lower == 1
|
|
||||||
assert num_range.upper == 3
|
|
||||||
|
|
||||||
def test_supports_multiple_args(self):
|
|
||||||
num_range = NumberRange(1, 3)
|
|
||||||
assert num_range.lower == 1
|
|
||||||
assert num_range.upper == 3
|
|
||||||
|
|
||||||
def test_supports_strings(self):
|
|
||||||
num_range = NumberRange('1-3')
|
|
||||||
assert num_range.lower == 1
|
|
||||||
assert num_range.upper == 3
|
|
||||||
|
|
||||||
def test_supports_strings_with_spaces(self):
|
|
||||||
num_range = NumberRange('1 - 3')
|
|
||||||
assert num_range.lower == 1
|
|
||||||
assert num_range.upper == 3
|
|
||||||
|
|
||||||
def test_supports_strings_with_bounds(self):
|
|
||||||
num_range = NumberRange('[1, 3]')
|
|
||||||
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
|
|
||||||
assert num_range.upper == 3
|
|
||||||
|
|
||||||
def test_supports_integers(self):
|
|
||||||
num_range = NumberRange(3)
|
|
||||||
assert num_range.lower == 3
|
|
||||||
assert num_range.upper == 3
|
|
||||||
|
|
||||||
|
|
||||||
class TestComparisonOperators(object):
|
|
||||||
def test_eq_operator(self):
|
|
||||||
assert NumberRange(1, 3) == NumberRange(1, 3)
|
|
||||||
assert not NumberRange(1, 3) == NumberRange(1, 4)
|
|
||||||
|
|
||||||
def test_ne_operator(self):
|
|
||||||
assert not NumberRange(1, 3) != NumberRange(1, 3)
|
|
||||||
assert NumberRange(1, 3) != NumberRange(1, 4)
|
|
||||||
|
|
||||||
def test_gt_operator(self):
|
|
||||||
assert NumberRange(1, 3) > NumberRange(0, 2)
|
|
||||||
assert not NumberRange(2, 3) > NumberRange(2, 3)
|
|
||||||
|
|
||||||
def test_ge_operator(self):
|
|
||||||
assert NumberRange(1, 3) >= NumberRange(0, 2)
|
|
||||||
assert NumberRange(1, 3) >= NumberRange(1, 3)
|
|
||||||
|
|
||||||
def test_lt_operator(self):
|
|
||||||
assert NumberRange(0, 2) < NumberRange(1, 3)
|
|
||||||
assert not NumberRange(2, 3) < NumberRange(2, 3)
|
|
||||||
|
|
||||||
def test_le_operator(self):
|
|
||||||
assert NumberRange(0, 2) <= NumberRange(1, 3)
|
|
||||||
assert NumberRange(1, 3) >= NumberRange(1, 3)
|
|
||||||
|
|
||||||
def test_integer_comparison(self):
|
|
||||||
assert NumberRange(2, 2) <= 3
|
|
||||||
assert NumberRange(1, 3) >= 0
|
|
||||||
assert NumberRange(2, 2) == 2
|
|
||||||
assert NumberRange(2, 2) != 3
|
|
||||||
|
|
||||||
|
|
||||||
def test_str_representation():
|
|
||||||
assert str(NumberRange(1, 3)) == '1 - 3'
|
|
||||||
assert str(NumberRange(1, 1)) == '1'
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@mark.parametrize('number_range',
|
|
||||||
(
|
|
||||||
(3, 2),
|
|
||||||
[4, 2],
|
|
||||||
'5-2',
|
|
||||||
(float('inf'), 2),
|
|
||||||
'[4, 3]',
|
|
||||||
)
|
|
||||||
)
|
|
||||||
def test_raises_exception_for_badly_constructed_range(number_range):
|
|
||||||
with raises(RangeBoundsException):
|
|
||||||
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)
|
|
||||||
|
|
||||||
def test_sub_operator(self):
|
|
||||||
assert NumberRange(1, 3) - NumberRange(1, 2) == NumberRange(-1, 2)
|
|
||||||
|
|
||||||
def test_isub_operator(self):
|
|
||||||
range_ = NumberRange(1, 3)
|
|
||||||
range_ -= NumberRange(1, 2)
|
|
||||||
assert range_ == NumberRange(-1, 2)
|
|
||||||
|
|
||||||
def test_iadd_operator(self):
|
|
||||||
range_ = NumberRange(1, 2)
|
|
||||||
range_ += NumberRange(1, 2)
|
|
||||||
assert range_ == NumberRange(2, 4)
|
|
@@ -1,19 +1,21 @@
|
|||||||
from pytest import mark
|
from pytest import mark
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
intervals = None
|
||||||
|
try:
|
||||||
|
import intervals
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
from tests import TestCase
|
from tests import TestCase
|
||||||
from sqlalchemy_utils import (
|
from sqlalchemy_utils import IntRangeType
|
||||||
NumberRangeType,
|
|
||||||
NumberRange,
|
|
||||||
coercion_listener
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TestNumberRangeType(TestCase):
|
@mark.skipif('intervals is None')
|
||||||
|
class NumberRangeTestCase(TestCase):
|
||||||
def create_models(self):
|
def create_models(self):
|
||||||
class Building(self.Base):
|
class Building(self.Base):
|
||||||
__tablename__ = 'building'
|
__tablename__ = 'building'
|
||||||
id = sa.Column(sa.Integer, primary_key=True)
|
id = sa.Column(sa.Integer, primary_key=True)
|
||||||
persons_at_night = sa.Column(NumberRangeType)
|
persons_at_night = sa.Column(IntRangeType)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return 'Building(%r)' % self.id
|
return 'Building(%r)' % self.id
|
||||||
@@ -23,8 +25,8 @@ class TestNumberRangeType(TestCase):
|
|||||||
@mark.parametrize(
|
@mark.parametrize(
|
||||||
'number_range',
|
'number_range',
|
||||||
(
|
(
|
||||||
(1, 3),
|
[1, 3],
|
||||||
'1-3'
|
'1 - 3',
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
def test_save_number_range(self, number_range):
|
def test_save_number_range(self, number_range):
|
||||||
@@ -34,13 +36,14 @@ class TestNumberRangeType(TestCase):
|
|||||||
|
|
||||||
self.session.add(building)
|
self.session.add(building)
|
||||||
self.session.commit()
|
self.session.commit()
|
||||||
|
self.session.expire(building)
|
||||||
building = self.session.query(self.Building).first()
|
building = self.session.query(self.Building).first()
|
||||||
assert building.persons_at_night.lower == 1
|
assert building.persons_at_night.lower == 1
|
||||||
assert building.persons_at_night.upper == 3
|
assert building.persons_at_night.upper == 3
|
||||||
|
|
||||||
def test_infinite_upper_bound(self):
|
def test_infinite_upper_bound(self):
|
||||||
building = self.Building(
|
building = self.Building(
|
||||||
persons_at_night=NumberRange(1, float('inf'))
|
persons_at_night=intervals.IntInterval(1, float('inf'))
|
||||||
)
|
)
|
||||||
self.session.add(building)
|
self.session.add(building)
|
||||||
self.session.commit()
|
self.session.commit()
|
||||||
@@ -51,7 +54,7 @@ class TestNumberRangeType(TestCase):
|
|||||||
|
|
||||||
def test_infinite_lower_bound(self):
|
def test_infinite_lower_bound(self):
|
||||||
building = self.Building(
|
building = self.Building(
|
||||||
persons_at_night=NumberRange(-float('inf'), 1)
|
persons_at_night=intervals.IntInterval(-float('inf'), 1)
|
||||||
)
|
)
|
||||||
self.session.add(building)
|
self.session.add(building)
|
||||||
self.session.commit()
|
self.session.commit()
|
||||||
@@ -61,7 +64,7 @@ class TestNumberRangeType(TestCase):
|
|||||||
|
|
||||||
def test_nullify_number_range(self):
|
def test_nullify_number_range(self):
|
||||||
building = self.Building(
|
building = self.Building(
|
||||||
persons_at_night=NumberRange(1, 3)
|
persons_at_night=intervals.IntInterval(1, 3)
|
||||||
)
|
)
|
||||||
|
|
||||||
self.session.add(building)
|
self.session.add(building)
|
||||||
@@ -77,9 +80,18 @@ class TestNumberRangeType(TestCase):
|
|||||||
def test_string_coercion(self):
|
def test_string_coercion(self):
|
||||||
building = self.Building(persons_at_night='[12, 18]')
|
building = self.Building(persons_at_night='[12, 18]')
|
||||||
|
|
||||||
assert isinstance(building.persons_at_night, NumberRange)
|
assert isinstance(building.persons_at_night, intervals.IntInterval)
|
||||||
|
|
||||||
def test_integer_coercion(self):
|
def test_integer_coercion(self):
|
||||||
building = self.Building(persons_at_night=15)
|
building = self.Building(persons_at_night=15)
|
||||||
assert building.persons_at_night.lower == 15
|
assert building.persons_at_night.lower == 15
|
||||||
assert building.persons_at_night.upper == 15
|
assert building.persons_at_night.upper == 15
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class TestNumberRangeTypeOnPostgres(NumberRangeTestCase):
|
||||||
|
dns = 'postgres://postgres@localhost/sqlalchemy_utils_test'
|
||||||
|
|
||||||
|
|
||||||
|
class TestNumberRangeTypeOnSqlite(NumberRangeTestCase):
|
||||||
|
pass
|
||||||
|
Reference in New Issue
Block a user