diff --git a/lib/ClusterShell/RangeSet.py b/lib/ClusterShell/RangeSet.py index 7296db2d..19c7cdd6 100644 --- a/lib/ClusterShell/RangeSet.py +++ b/lib/ClusterShell/RangeSet.py @@ -139,13 +139,30 @@ def _parse(self, pattern): raise RangeSetParseError(subrange, "cannot convert string to integer") + begin_sign = end_sign = 1 # sign "scale factor" + if baserange.find('-') < 0: if step != 1: raise RangeSetParseError(subrange, "invalid step usage") begin = end = baserange else: # ignore whitespaces in a range - begin, end = (n.strip() for n in baserange.split('-', 1)) + try: + begin, end = (n.strip() for n in baserange.split('-')) + if not begin: # single negative number "-5" + begin = end + begin_sign = end_sign = -1 + except ValueError: + try: + # -0-3 + _, begin, end = (n.strip() + for n in baserange.split('-')) + begin_sign = -1 + except ValueError: + # -8--4 + _, begin, _, end = (n.strip() + for n in baserange.split('-')) + begin_sign = end_sign = -1 # compute padding and return node range info tuple try: @@ -169,6 +186,7 @@ def _parse(self, pattern): if (pad > 0 or endpad > 0) and len(begin) != len(end): raise RangeSetParseError(subrange, "padding length mismatch") + stop = int(ends) except ValueError: if len(subrange) == 0: @@ -178,10 +196,14 @@ def _parse(self, pattern): raise RangeSetParseError(subrange, msg) # check preconditions - if stop > 1e100 or start > stop or step < 1: + if pad > 0 and begin_sign < 0: + errmsg = "padding not supported in negative ranges" + raise RangeSetParseError(subrange, errmsg) + + if stop > 1e100 or start * begin_sign > stop * end_sign or step < 1: raise RangeSetParseError(subrange, "invalid values in range") - self.add_range(start, stop + 1, step, pad) + self.add_range(start * begin_sign, stop * end_sign + 1, step, pad) @classmethod def fromlist(cls, rnglist, autostep=None): @@ -261,8 +283,10 @@ def dim(self): def _sorted(self): """Get sorted list from inner set.""" - # for mixed padding support, sort by both string length and index - return sorted(set.__iter__(self), key=lambda x: (len(x), x)) + # For mixed padding support, sort by both string length and index + return sorted(set.__iter__(self), + key=lambda x: (-len(x), int(x)) if x.startswith('-') \ + else (len(x), x)) def __iter__(self): """Iterate over each element in RangeSet, currently as integers, with @@ -456,7 +480,7 @@ def _slices_padding(self, autostep=AUTOSTEP_DISABLED): if stepped or cur_step == 1: yield slice(cur_start, last_idx + 1, cur_step), cur_pad if cur_padded else 0 else: - for j in range(cur_start, idx + 1, cur_step): + for j in range(cur_start, last_idx + 1, cur_step): yield slice(j, j + 1, 1), cur_pad if cur_padded else 0 def _contiguous_slices(self): diff --git a/tests/NodeSetTest.py b/tests/NodeSetTest.py index 0dab8f89..0fcecd5f 100644 --- a/tests/NodeSetTest.py +++ b/tests/NodeSetTest.py @@ -144,7 +144,6 @@ def testBadRangeUsages(self): self._assertNS("nova[2-5/a]", NodeSetParseRangeError) self._assertNS("nova[3/2]", NodeSetParseRangeError) self._assertNS("nova[3-/2]", NodeSetParseRangeError) - self._assertNS("nova[-3/2]", NodeSetParseRangeError) self._assertNS("nova[-/2]", NodeSetParseRangeError) self._assertNS("nova[4-a/2]", NodeSetParseRangeError) self._assertNS("nova[4-3/2]", NodeSetParseRangeError) @@ -161,7 +160,6 @@ def testBadRangeUsages(self): self._assertNS("nova[2-5/a]p0", NodeSetParseRangeError) self._assertNS("nova[3/2]p0", NodeSetParseRangeError) self._assertNS("nova[3-/2]p0", NodeSetParseRangeError) - self._assertNS("nova[-3/2]p0", NodeSetParseRangeError) self._assertNS("nova[-/2]p0", NodeSetParseRangeError) self._assertNS("nova[4-a/2]p0", NodeSetParseRangeError) self._assertNS("nova[4-3/2]p0", NodeSetParseRangeError) @@ -177,7 +175,6 @@ def testBadRangeUsages(self): self._assertNS("x4nova[2-5/a]p0", NodeSetParseRangeError) self._assertNS("x4nova[3/2]p0", NodeSetParseRangeError) self._assertNS("x4nova[3-/2]p0", NodeSetParseRangeError) - self._assertNS("x4nova[-3/2]p0", NodeSetParseRangeError) self._assertNS("x4nova[-/2]p0", NodeSetParseRangeError) self._assertNS("x4nova[4-a/2]p0", NodeSetParseRangeError) self._assertNS("x4nova[4-3/2]p0", NodeSetParseRangeError) @@ -2858,3 +2855,32 @@ def test_fully_numeric(self): # trailing 0 along with mixed lengths padding self._assertNS("[0-10]0", NodeSetParseRangeError) self._assertNS("0[0-10]0", NodeSetParseRangeError) + + def test_negative_ranges(self): + # supported from 1.9.1 (#515) + n1 = NodeSet("n[-5]") + self.assertEqual(str(n1), "n-5") + self.assertEqual(len(n1), 1) + n1 = NodeSet("n[-1-0]") + self.assertEqual(str(n1), "n[-1-0]") + self.assertEqual(len(n1), 2) + n1 = NodeSet("n[-5-5]") + self.assertEqual(str(n1), "n[-5-5]") + self.assertEqual(len(n1), 2*5+1) + n1 = NodeSet("n[-12-12]") + self.assertEqual(str(n1), "n[-12-12]") + self.assertEqual(len(n1), 12*2+1) + n1 = NodeSet("n[-12,-10--9,-5--1,1-5]") + self.assertEqual(str(n1), "n[-12,-10--9,-5--1,1-5]") + self.assertEqual(len(n1), 13) + n1 = NodeSet("n[1-5,-12,-5--1,-10--9]") + self.assertEqual(str(n1), "n[-12,-10--9,-5--1,1-5]") + self.assertEqual(len(n1), 13) + n1 = NodeSet("n[1,-10,2-4,-12,5,-5,-4,-3,-9,-2--1]") + self.assertEqual(str(n1), "n[-12,-10--9,-5--1,1-5]") + self.assertEqual(len(n1), 13) + n1 = NodeSet("n[1,-10,2-4,-12],n[5,-5,-4,-3,-9,-2--1]") + self.assertEqual(str(n1), "n[-12,-10--9,-5--1,1-5]") + self.assertEqual(len(n1), 13) + # padding in negative range is not supported + self._assertNS("n[-001]", NodeSetParseRangeError) diff --git a/tests/RangeSetTest.py b/tests/RangeSetTest.py index 01124914..3bf54bc6 100644 --- a/tests/RangeSetTest.py +++ b/tests/RangeSetTest.py @@ -74,7 +74,6 @@ def test_bad_syntax(self): self.assertRaises(RangeSetParseError, RangeSet, "2-5/a") self.assertRaises(RangeSetParseError, RangeSet, "3/2") self.assertRaises(RangeSetParseError, RangeSet, "3-/2") - self.assertRaises(RangeSetParseError, RangeSet, "-3/2") self.assertRaises(RangeSetParseError, RangeSet, "-/2") self.assertRaises(RangeSetParseError, RangeSet, "4-a/2") self.assertRaises(RangeSetParseError, RangeSet, "4-3/2") @@ -1263,3 +1262,74 @@ def test_strip_whitespaces(self): self.assertRaises(RangeSetParseError, RangeSet, "1,5,") self.assertRaises(RangeSetParseError, RangeSet, "1,5, ") self.assertRaises(RangeSetParseError, RangeSet, "1,5,, ") + + def test_init_ranges(self): + """test RangeSet initialization with a range""" + r1 = RangeSet(range(5,7)) + self.assertEqual(str(r1), "5-6") + self.assertEqual(len(r1), 2) + + def test_init_negative_ranges(self): + """test RangeSet initialization with with negative ranges""" + # negative ranges (GH#515) + r1 = RangeSet(range(-1,1)) + self.assertEqual(str(r1), "-1-0") + self.assertEqual(len(r1), 2) + + r1 = RangeSet("-1-0") + self.assertEqual(str(r1), "-1-0") + self.assertEqual(len(r1), 2) + + r1 = RangeSet(range(-5,7)) + self.assertEqual(str(r1), "-5-6") + self.assertEqual(len(r1), 12) + + r1 = RangeSet("-5-6") + self.assertEqual(str(r1), "-5-6") + self.assertEqual(len(r1), 12) + + r1 = RangeSet(range(-9,-5)) + self.assertEqual(str(r1), "-9--6") + self.assertEqual(len(r1), 4) + + r1 = RangeSet("-9--6") + self.assertEqual(str(r1), "-9--6") + self.assertEqual(len(r1), 4) + + r1 = RangeSet(range(-12,-5)) + self.assertEqual(str(r1), "-12--6") + self.assertEqual(len(r1), 7) + + r1 = RangeSet(range(-100,-30)) + self.assertEqual(str(r1), "-100--31") + self.assertEqual(len(r1), 70) + + r1 = RangeSet(range(-10,-2,2)) + self.assertEqual(str(r1), "-10,-8,-6,-4") + self.assertEqual(len(r1), 4) + + r1 = RangeSet(range(-3, -2)) + self.assertEqual(str(r1), "-3") + self.assertEqual(len(r1), 1) + + r1 = RangeSet("-3/2") + self.assertEqual(str(r1), "-3") + self.assertEqual(len(r1), 1) + + r1 = RangeSet(range(-10,-2,2), autostep=3) + self.assertEqual(str(r1), "-10--4/2") + self.assertEqual(len(r1), 4) + + r1 = RangeSet(['-30', '-20', '-28', '-29']) + self.assertEqual(str(r1), "-30--28,-20") + self.assertEqual(len(r1), 4) + + r1 = RangeSet(['-31', '-20', '-27', '-29']) + self.assertEqual(str(r1), "-31,-29,-27,-20") + self.assertEqual(len(r1), 4) + + # parsing of negative range with padding is not supported + self.assertRaises(RangeSetParseError, RangeSet, "-001") + self.assertRaises(RangeSetParseError, RangeSet, "-009") + self.assertRaises(RangeSetParseError, RangeSet, "-01-00") + self.assertRaises(RangeSetParseError, RangeSet, "-003--001")