Skip to content

Commit

Permalink
Added operators that corresponds with Python's set(). Added tests and…
Browse files Browse the repository at this point in the history
… type checks for lots of operations. Fixed infinite iterator support in intrange
  • Loading branch information
runfalk committed Jun 7, 2017
1 parent d56bc1d commit 4cb2a99
Show file tree
Hide file tree
Showing 7 changed files with 238 additions and 39 deletions.
16 changes: 16 additions & 0 deletions doc/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,24 @@ Version 1.0.0
-------------
Released on <unreleased>

- Added ``NotImplemented`` for ``<<`` and ``>>`` operators when there is a type
mismatch
- Added ``|`` operator for unions of :class:`~spans.types.Range` and
``NotImplemented`` support for :class:`~spans.settypes.RangeSet`
- Added ``&`` operator for intersections of :class:`~spans.types.Range` and
``NotImplemented`` support for :class:`~spans.settypes.RangeSet`
- Added ``-`` operator for differences of :class:`~spans.types.Range` and
``NotImplemented`` support for :class:`~spans.settypes.RangeSet`
- Fixed overlap with empty range incorrectly returns ``True``
(`bug #7 <https://github.com/runfalk/spans/issues/7>`_)
- Fixed issue with :meth:`~spans.types.Range.contains` for scalars on unbounded
ranges
- Fixed type check for :meth:`~spans.types.Range.right_of`
- Fixed type check for :meth:`~spans.settypes.RangeSet.union`
- Fixed type check for :meth:`~spans.settypes.RangeSet.intersection`
- Fixed type check for :meth:`~spans.settypes.RangeSet.difference`
- Fixed infinite iterators not being supported for
:class:`~spans.types.intrange`


Version 0.5.0
Expand Down
12 changes: 6 additions & 6 deletions doc/postgresql.rst
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ Operators
Most operators are not overloaded in Python to their PostgreSQL equivalents.
Instead Spans implements the functionality using methods.

=============================== ======================== ===============================
=============================== ======================== ==================================
Operator PostgreSQL Python
=============================== ======================== ===============================
=============================== ======================== ==================================
Equal ``a = b`` ``a == b``
Not equal ``a != b`` or ``a <> b`` ``a != b``
Less than ``a < b`` ``a < b``
Expand All @@ -44,10 +44,10 @@ Strictly right of ``a >> b`` ``a.right_of(b)`` or
Does not extend to the right of ``a &< b`` ``a.endsbefore(b)``
Does not extend to the left of ``a &> b`` ``a.startsafter(b)``
Is adjacent to ``a -|- b`` ``a.adjacent(b)``
Union ``a + b`` ``a.union(b)``
Intersection ``a * b`` ``a.intersection(b)``
Difference ``a - b`` ``a.difference(b)``
=============================== ======================== ===============================
Union ``a + b`` ``a.union(b)`` or ``a | b``
Intersection ``a * b`` ``a.intersection(b)`` or ``a & b``
Difference ``a - b`` ``a.difference(b)`` or ``a - b``
=============================== ======================== ==================================


Functions
Expand Down
55 changes: 44 additions & 11 deletions spans/settypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,8 +249,24 @@ def __invert__(self):

return self.__class__([self.type()]).difference(self)

def _test_type(self, item):
if not isinstance(item, self.type):
@classmethod
def is_valid_rangeset(cls, obj):
return isinstance(obj, cls)

@classmethod
def is_valid_range(cls, obj):
return isinstance(obj, cls.type)

def _test_rangeset_type(self, item):
if not self.is_valid_rangeset(item):
raise TypeError((
"Invalid range type '{range_type.__name__}' expected "
"'{expected_type.__name__}'").format(
expected_type=self.type,
range_type=item.__class__))

def _test_range_type(self, item):
if not self.is_valid_range(item):
raise TypeError((
"Invalid range type '{range_type.__name__}' expected "
"'{expected_type.__name__}'").format(
Expand Down Expand Up @@ -308,7 +324,7 @@ def contains(self, item):
# All range sets contain the empty range. However, we must verify the
# type of what is being passed as well to make sure we indeed got an
# empty set of the correct type.
if isinstance(item, self.type) and not item:
if not item and self.is_valid_range(item):
return True

for r in self._list:
Expand Down Expand Up @@ -337,7 +353,7 @@ def add(self, item):
:raises TypeError: If any of the given ranges are of incorrect type.
"""

self._test_type(item)
self._test_range_type(item)

# If item is empty, do not add it
if not item:
Expand Down Expand Up @@ -384,7 +400,7 @@ def remove(self, item):
:param item: Range to remove from this set.
"""

self._test_type(item)
self._test_range_type(item)

# If the list currently only have an empty range do nothing since an
# empty RangeSet can't be removed from anyway.
Expand Down Expand Up @@ -462,6 +478,7 @@ def union(self, *others):
# Make a copy of self and add all its ranges to the copy
union = self.copy()
for other in others:
self._test_rangeset_type(other)
for r in other:
union.add(r)
return union
Expand All @@ -482,6 +499,7 @@ def difference(self, *others):
# Make a copy of self and remove all its ranges from the copy
difference = self.copy()
for other in others:
self._test_rangeset_type(other)
for r in other:
difference.remove(r)
return difference
Expand All @@ -505,6 +523,8 @@ def intersection(self, *others):
output = self

for other in others:
self._test_rangeset_type(other)

# Intermediate RangeSet containing intersection for this current
# iteration.
intersection = self.__class__([])
Expand All @@ -528,13 +548,26 @@ def intersection(self, *others):

return output

__contains__ = contains
def __or__(self, other):
try:
return self.union(other)
except TypeError:
return NotImplemented

# Some operators that set() has:
# TODO: Use NotImplemented
__or__ = union
__and__ = intersection
__sub__ = difference
def __and__(self, other):
try:
return self.intersection(other)
except TypeError:
return NotImplemented

def __sub__(self, other):
try:
return self.difference(other)
except TypeError:
return NotImplemented

# ``in`` operator support
__contains__ = contains

# Python 3 support
__bool__ = __nonzero__
Expand Down
70 changes: 63 additions & 7 deletions spans/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from collections import namedtuple
from datetime import date, datetime, timedelta
from functools import wraps

from ._compat import *
from ._utils import date_from_iso_week, PartialOrderingMixin, PicklableSlotMixin
Expand Down Expand Up @@ -467,6 +468,10 @@ def union(self, other):
:raises ValueError: If `other` can not be merged with this range.
"""

if not self.is_valid_range(other):
msg = "Unsupported type to test for union '{.__class__.__name__}'"
raise TypeError(msg.format(other))

# Optimize empty ranges
if not self:
return other
Expand Down Expand Up @@ -528,6 +533,10 @@ def difference(self, other):
computed.
"""

if not self.is_valid_range(other):
msg = "Unsupported type to test for difference '{.__class__.__name__}'"
raise TypeError(msg.format(other))

# Consider empty ranges or no overlap
if not self or not other or not self.overlap(other):
return self
Expand Down Expand Up @@ -561,6 +570,10 @@ def intersection(self, other):
:return: A new range that is the intersection between this and `other`.
"""

if not self.is_valid_range(other):
msg = "Unsupported type to test for intersection '{.__class__.__name__}'"
raise TypeError(msg.format(other))

# Handle ranges not intersecting
if not self or not other or not self.overlap(other):
return self.empty()
Expand Down Expand Up @@ -723,6 +736,12 @@ def left_of(self, other):
:return: ``True`` if this range is completely to the left of ``other``.
"""

if not self.is_valid_range(other):
msg = (
"Left of is not supported for '{}', provide a proper range "
"class").format(other.__class__.__name__)
raise TypeError(msg)

return self < other and not self.overlap(other)

def right_of(self, other):
Expand All @@ -748,12 +767,46 @@ def right_of(self, other):
:return: ``True`` if this range is completely to the right of ``other``.
"""

if not self.is_valid_range(other):
msg = (
"Right of is not supported for '{}', provide a proper range "
"class").format(other.__class__.__name__)
raise TypeError(msg)

return other.left_of(self)

# TODO: Properly implement NotImplemented
def __lshift__(self, other):
try:
return self.left_of(other)
except TypeError:
return NotImplemented

def __rshift__(self, other):
try:
return self.right_of(other)
except TypeError:
return NotImplemented

def __or__(self, other):
try:
return self.union(other)
except TypeError:
return NotImplemented

def __and__(self, other):
try:
return self.intersection(other)
except TypeError:
return NotImplemented

def __sub__(self, other):
try:
return self.difference(other)
except TypeError:
return NotImplemented

# ``in`` operator support
__contains__ = contains
__lshift__ = left_of
__rshift__ = right_of

# Python 3 support
__bool__ = __nonzero__
Expand Down Expand Up @@ -875,10 +928,13 @@ def endswith(self, other):
return super(DiscreteRange, self).endswith(other)

def __iter__(self):
next = self.lower
while next < self.upper:
yield next
next = self.next(next)
if self.lower_inf:
raise TypeError("Range with no lower bound can't be iterated over")

value = self.lower
while self.upper_inf or value < self.upper:
yield value
value = self.next(value)


class OffsetableRangeMixin(object):
Expand Down
11 changes: 11 additions & 0 deletions tests/test_intrange.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,14 @@

def test_len():
assert len(intrange(0, 5)) == 5

def test_iter():
assert list(intrange(0, 5)) == list(range(5))

infinite_iter = iter(intrange(0))
for i in range(100):
assert i == next(infinite_iter)

def test_no_lower_bound_iter():
with pytest.raises(TypeError):
next(iter(intrange(upper=1)))

0 comments on commit 4cb2a99

Please sign in to comment.