diff --git a/src/solrq/__init__.py b/src/solrq/__init__.py index 64ef346..4b77625 100644 --- a/src/solrq/__init__.py +++ b/src/solrq/__init__.py @@ -116,14 +116,25 @@ def __repr__(self): class Range(Value): """Wrapper around range values. - Wraps two values with Solr's ``[ TO ]`` syntax with respect to - restricted character esaping. + Wraps two values with Solr's ``[ TO ]`` syntax (defaults to + inclusive boundaries) with respect to restricted character escaping. + + Wraps two values with Solr's ``[ TO ]`` (defaults to inclusive + boundaries) syntax with respect to restricted character escaping. Args: from_ (object): start of range, same as parameter ``raw`` in :class:`Value`. to (object): end of range, same as parameter ``raw`` in :class:`Value`. + boundaries (str): type of boundaries for the range. Defaults to + ``'inclusive'``. Allowed values are: + * ``inclusive``, ``ii``, or ``[]``: translates to + ``[ TO ]`` + * ``exclusive``, ``ee``, or ``{}``: translates to + ``{ TO }`` + * ``ei``, or ``{]``: translates to ``{ TO ]`` + * ``ie``, or ``[}``: translates to ``[ TO }`` Examples: @@ -146,6 +157,15 @@ class Range(Value): >>> Range(timedelta(days=2), timedelta()) + To use exclusive or mixed boundaries use ``boundaries`` argument: + + >>> Range(0, 20, boundaries='exclusive') + + >>> Range(0, 20, boundaries='ei') + + >>> Range(0, 20, boundaries='[}') + + Note: We could treat any iterables always as ranges when initializing :class:`Q` objects but "explicit is better than implicit" and also @@ -153,8 +173,30 @@ class Range(Value): want to do that. """ - def __init__(self, from_, to, safe=None): - """Initialize range value.""" + BOUNDARY_BRACKETS = { + 'exclusive': '{}', + 'inclusive': '[]', + 'ee': '{}', + 'ei': '{]', + 'ii': '[]', + 'ie': '[}' + } + # DRY + BOUNDARY_BRACKETS.update( + # compat: py26 does not support dict comprehensions + dict((value, value) for value in BOUNDARY_BRACKETS.values()) + ) + + def __init__(self, from_, to, safe=None, boundaries='inclusive'): + """Initialize range value and set boundary brackets.""" + try: + brackets = self.BOUNDARY_BRACKETS[boundaries] + except KeyError: + raise ValueError( + "boundaries value must be one of {}" + "".format(self.BOUNDARY_BRACKETS.keys()) + ) + self.from_ = ( from_ if isinstance(from_, Value) else Value(from_, safe or False) ) @@ -168,7 +210,8 @@ def __init__(self, from_, to, safe=None): self.to.safe = safe super(Range, self).__init__( - "[{from_} TO {to}]".format(from_=self.from_, to=self.to), + "{brackets[0]}{from_} TO {to}{brackets[1]}" + "".format(from_=self.from_, to=self.to, brackets=brackets), # Note: parts will be safe'd or not so no need for further escaping True ) diff --git a/tests/test_squery.py b/tests/test_squery.py index fba4c69..b0a4975 100644 --- a/tests/test_squery.py +++ b/tests/test_squery.py @@ -158,6 +158,29 @@ def test_range(): ) == "[{from_} TO {to}]".format(from_=td_from, to=td_to) +def test_range_boundaries_unsupported(): + with pytest.raises(ValueError): + Range(1, 2, boundaries='<>') + + with pytest.raises(ValueError): + Range(1, 2, boundaries='anything') + + +def test_range_boundaries(): + assert str(Range(0, 1, boundaries='inclusive')) == '[0 TO 1]' + assert str(Range(0, 1, boundaries='exclusive')) == '{0 TO 1}' + + assert str(Range(0, 1, boundaries='ee')) == '{0 TO 1}' + assert str(Range(0, 1, boundaries='ii')) == '[0 TO 1]' + assert str(Range(0, 1, boundaries='ei')) == '{0 TO 1]' + assert str(Range(0, 1, boundaries='ie')) == '[0 TO 1}' + + assert str(Range(0, 1, boundaries='{}')) == '{0 TO 1}' + assert str(Range(0, 1, boundaries='[]')) == '[0 TO 1]' + assert str(Range(0, 1, boundaries='{]')) == '{0 TO 1]' + assert str(Range(0, 1, boundaries='[}')) == '[0 TO 1}' + + def test_special(): assert str(SET) == '[* TO *]' assert str(ANY) == '*'