Package ClusterShell :: Module NodeSet
[hide private]
[frames] | no frames]

Source Code for Module ClusterShell.NodeSet

   1  # 
   2  # Copyright CEA/DAM/DIF (2007, 2008, 2009) 
   3  #  Contributor: Stephane THIELL <stephane.thiell@cea.fr> 
   4  # 
   5  # This file is part of the ClusterShell library. 
   6  # 
   7  # This software is governed by the CeCILL-C license under French law and 
   8  # abiding by the rules of distribution of free software.  You can  use, 
   9  # modify and/ or redistribute the software under the terms of the CeCILL-C 
  10  # license as circulated by CEA, CNRS and INRIA at the following URL 
  11  # "http://www.cecill.info". 
  12  # 
  13  # As a counterpart to the access to the source code and  rights to copy, 
  14  # modify and redistribute granted by the license, users are provided only 
  15  # with a limited warranty  and the software's author,  the holder of the 
  16  # economic rights,  and the successive licensors  have only  limited 
  17  # liability. 
  18  # 
  19  # In this respect, the user's attention is drawn to the risks associated 
  20  # with loading,  using,  modifying and/or developing or reproducing the 
  21  # software by the user in light of its specific status of free software, 
  22  # that may mean  that it is complicated to manipulate,  and  that  also 
  23  # therefore means  that it is reserved for developers  and  experienced 
  24  # professionals having in-depth computer knowledge. Users are therefore 
  25  # encouraged to load and test the software's suitability as regards their 
  26  # requirements in conditions enabling the security of their systems and/or 
  27  # data to be ensured and,  more generally, to use and operate it in the 
  28  # same conditions as regards security. 
  29  # 
  30  # The fact that you are presently reading this means that you have had 
  31  # knowledge of the CeCILL-C license and that you accept its terms. 
  32  # 
  33  # $Id: NodeSet.py 160 2009-10-28 12:33:53Z st-cea $ 
  34   
  35  """ 
  36  Cluster node set. 
  37   
  38  A module to deal efficiently with pdsh-like rangesets and nodesets. 
  39  Instances of RangeSet and NodeSet both provide similar operations than 
  40  the builtin set() type and Set object. 
  41     [ See http://www.python.org/doc/lib/set-objects.html ] 
  42   
  43  Usage example: 
  44   
  45      # Import NodeSet class 
  46      from ClusterShell.NodeSet import NodeSet 
  47   
  48      # Create a new nodeset from pdsh-like pattern 
  49      nodeset = NodeSet("cluster[1-30]") 
  50   
  51      # Add cluster32 to nodeset 
  52      nodeset.update("cluster32") 
  53   
  54      # Remove from nodeset 
  55      nodeset.difference_update("cluster[2-5]") 
  56   
  57      # Print nodeset as a pdsh-like pattern 
  58      print nodeset 
  59   
  60      # Iterate over node names in nodeset 
  61      for node in nodeset: 
  62          print node 
  63  """ 
  64   
  65  import copy 
  66  import re 
  67   
68 -class RangeSetException(Exception):
69 """used by RangeSet"""
70 - def __init__(self, msg):
71 self.msg = msg
72
73 - def __str__(self):
74 return self.msg
75
76 -class RangeSetParseError(RangeSetException):
77 """used by RangeSet when a parse cannot be done"""
78 - def __init__(self, subrange, msg):
79 # faulty subrange; this allows you to target the error 80 self.msg = "%s : \"%s\"" % (msg, subrange)
81
82 -class RangeSetPaddingError(RangeSetException):
83 """used by RangeSet when a fatal padding incoherency occurs"""
84 85
86 -class NodeSetException(Exception):
87 """used by NodeSet"""
88 - def __init__(self, msg):
89 self.msg = msg
90
91 - def __str__(self):
92 return self.msg
93
94 -class NodeSetParseError(NodeSetException):
95 """used by NodeSet when a parse cannot be done"""
96 - def __init__(self, part, msg):
97 # faulty part; this allows you to target the error 98 self.part = part 99 self.msg = msg
100
101 -class NodeSetParseRangeError(NodeSetParseError):
102 """used by NodeSet when bad range is encountered during a a parse"""
103 - def __init__(self, rset_exc):
104 # faulty part; this allows you to target the error 105 self.msg = rset_exc.msg
106 107
108 -class RangeSet:
109 """ 110 Advanced range sets. 111 112 RangeSet creation examples: 113 rset = RangeSet() # empty RangeSet 114 rset = RangeSet("5,10-42") # contains 5, 10 to 42 115 rset = RangeSet("0-10/2") # contains 0, 2, 4, 6, 8, 10 116 117 Also, RangeSet provides methods like update(), intersection_update() 118 or difference_update(), which conform to the Python Set API. 119 """
120 - def __init__(self, pattern=None, autostep=None):
121 """ 122 Initialize RangeSet with optional pdsh-like string pattern and 123 autostep threshold. 124 """ 125 if autostep is None: 126 # disabled by default for pdsh compat (+inf is 1E400, but a bug in 127 # python 2.4 makes it impossible to be pickled, so we use less). 128 self._autostep = 1E100 129 else: 130 # - 1 because user means node count, but we means 131 # real steps. 132 self._autostep = int(autostep) - 1 133 134 self._length = 0 135 self._ranges = [] 136 137 if pattern is not None: 138 139 # Comma separated ranges 140 if pattern.find(',') < 0: 141 subranges = [pattern] 142 else: 143 subranges = pattern.split(',') 144 145 for subrange in subranges: 146 if subrange.find('/') < 0: 147 step = 1 148 baserange = subrange 149 else: 150 baserange, step = subrange.split('/', 1) 151 152 try: 153 step = int(step) 154 except ValueError: 155 raise RangeSetParseError(subrange, 156 "cannot convert string to integer") 157 158 if baserange.find('-') < 0: 159 if step != 1: 160 raise RangeSetParseError(subrange, 161 "invalid step usage") 162 begin = end = baserange 163 else: 164 begin, end = baserange.split('-', 1) 165 166 # compute padding and return node range info tuple 167 try: 168 pad = 0 169 if int(begin) != 0: 170 begins = begin.lstrip("0") 171 if len(begin) - len(begins) > 0: 172 pad = len(begin) 173 start = int(begins) 174 else: 175 if len(begin) > 1: 176 pad = len(begin) 177 start = 0 178 if int(end) != 0: 179 ends = end.lstrip("0") 180 else: 181 ends = end 182 stop = int(ends) 183 except ValueError: 184 raise RangeSetParseError(subrange, 185 "cannot convert string to integer") 186 187 # check preconditions 188 if start > stop or step < 1: 189 raise RangeSetParseError(subrange, 190 "invalid values in range") 191 192 self.add_range(start, stop, step, pad)
193
194 - def fromlist(cls, l, autostep=None):
195 """ 196 Class method that returns a new RangeSet with ranges from 197 provided list. 198 """ 199 inst = RangeSet(autostep=autostep) 200 for rg in l: 201 if isinstance(rg, RangeSet): 202 inst.update(rg) 203 else: 204 inst.update(RangeSet(rg)) 205 return inst
206 fromlist = classmethod(fromlist) 207
208 - def __iter__(self):
209 """ 210 Iterate over each item in RangeSet. 211 """ 212 for start, stop, step, pad in self._ranges: 213 for i in range(start, stop + 1, step): 214 yield "%*d" % (pad, i)
215
216 - def __len__(self):
217 """ 218 Get the number of items in RangeSet. 219 """ 220 return self._length
221
222 - def __str__(self):
223 """ 224 Get range-based string. 225 """ 226 cnt = 0 227 s = "" 228 for start, stop, step, pad in self._ranges: 229 assert pad != None 230 if cnt > 0: 231 s += "," 232 if start == stop: 233 s += "%0*d" % (pad, start) 234 else: 235 assert step >= 0, "Internal error: step < 0" 236 if step == 1: 237 s += "%0*d-%0*d" % (pad, start, pad, stop) 238 else: 239 s += "%0*d-%0*d/%d" % (pad, start, pad, stop, step) 240 cnt += stop - start + 1 241 return s
242 243 # __repr__ is the same as __str__ as it is a valid expression that 244 # could be used to recreate a RangeSet with the same value 245 __repr__ = __str__ 246
247 - def __contains__(self, elem):
248 """ 249 Is element contained in RangeSet? Element can be either a 250 string with optional padding (eg. "002") or an integer 251 (obviously, no padding check is performed for integer). 252 """ 253 # support str type with padding support, eg. `"003" in rangeset' 254 if type(elem) is str: 255 pad = 0 256 if int(elem) != 0: 257 selem = elem.lstrip("0") 258 if len(elem) - len(selem) > 0: 259 pad = len(elem) 260 ielem = int(selem) 261 else: 262 if len(elem) > 1: 263 pad = len(elem) 264 ielem = 0 265 return self._contains_with_padding(ielem, pad) 266 267 # the following cast raises TypeError if elem is not an integer 268 return self._contains(int(elem))
269
270 - def _contains(self, ielem):
271 """ 272 Contains subroutine that takes an integer. 273 """ 274 for rgstart, rgstop, rgstep, rgpad in self._ranges: 275 if ielem >= rgstart and ielem <= rgstop and \ 276 (ielem - rgstart) % rgstep == 0: 277 return True 278 return False
279
280 - def _contains_with_padding(self, ielem, pad):
281 """ 282 Contains subroutine that takes an integer and a padding value. 283 """ 284 for rgstart, rgstop, rgstep, rgpad in self._ranges: 285 # for each ranges, check for inclusion + padding matching 286 # + step matching 287 if ielem >= rgstart and ielem <= rgstop and \ 288 (pad == rgpad or (pad == 0 and len(str(ielem)) >= rgpad)) and \ 289 (ielem - rgstart) % rgstep == 0: 290 return True 291 return False
292
293 - def _binary_sanity_check(self, other):
294 # check that the other argument to a binary operation is also 295 # a RangeSet, raising a TypeError otherwise. 296 if not isinstance(other, RangeSet): 297 raise TypeError, "Binary operation only permitted between RangeSets"
298
299 - def issubset(self, rangeset):
300 """ 301 Report whether another rangeset contains this rangeset. 302 """ 303 self._binary_sanity_check(rangeset) 304 305 for start, stop, step, pad in self._ranges: 306 for i in range(start, stop + 1, step): 307 if not rangeset._contains_with_padding(i, pad): 308 return False 309 return True
310
311 - def issuperset(self, rangeset):
312 """ 313 Report whether this rangeset contains another rangeset. 314 """ 315 self._binary_sanity_check(rangeset) 316 return rangeset.issubset(self)
317 318 # inequality comparisons using the is-subset relation 319 __le__ = issubset 320 __ge__ = issuperset 321
322 - def __lt__(self, other):
323 """ 324 x.__lt__(y) <==> x<y 325 """ 326 self._binary_sanity_check(other) 327 return len(self) < len(other) and self.issubset(other)
328
329 - def __gt__(self, other):
330 """ 331 x.__gt__(y) <==> x>y 332 """ 333 self._binary_sanity_check(other) 334 return len(self) > len(other) and self.issuperset(other)
335
336 - def __getitem__(self, i):
337 """ 338 Return the element at index i. 339 """ 340 length = 0 341 for start, stop, step, pad in self._ranges: 342 cnt = (stop - start) / step + 1 343 if i < length + cnt: 344 return start + (i - length) * step 345 length += cnt
346
347 - def _expand(self):
348 """ 349 Expand all items. Internal use. 350 """ 351 items = [] 352 pad = 0 353 for rgstart, rgstop, rgstep, rgpad in self._ranges: 354 items += range(rgstart, rgstop + 1, rgstep) 355 pad = pad or rgpad 356 return items, pad
357
358 - def _fold(self, items, pad):
359 """ 360 Fold items as ranges and group them by step. 361 Return: (ranges, total_length) 362 """ 363 cnt, k, m, istart, rg = 0, -1, 0, None, [] 364 365 # iterate over items and regroup them using steps 366 for i in items: 367 if i > k: 368 cnt += 1 369 if istart is None: 370 istart = k = i 371 elif m > 0: # check step length (m) 372 if m != i - k: 373 if m == 1 or k - istart >= self._autostep * m: 374 # add one range with possible autostep 375 rg.append((istart, k, m, pad)) 376 istart = k = i 377 elif k - istart > m: 378 # stepped without autostep 379 # be careful to let the last one "pending" 380 for j in range(istart, k, m): 381 rg.append((j, j, 1, pad)) 382 istart = k 383 else: 384 rg.append((istart, istart, 1, pad)) 385 istart = k 386 m = i - k 387 k = i 388 389 # finishing 390 if istart is not None: # istart might be 0 391 if m > 0: 392 if m == 1 or k - istart >= self._autostep * m: 393 # add one range with possible autostep 394 rg.append((istart, k, m, pad)) 395 elif k - istart > m: 396 # stepped without autostep 397 for j in range(istart, k + m, m): 398 rg.append((j, j, 1, pad)) 399 else: 400 rg.append((istart, istart, 1, pad)) 401 rg.append((k, k, 1, pad)) 402 else: 403 rg.append((istart, istart, 1, pad)) 404 405 return rg, cnt
406
407 - def add_range(self, start, stop, step=1, pad=0):
408 """ 409 Add a range (start, stop, step and padding length) to RangeSet. 410 """ 411 assert start <= stop, "please provide ordered node index ranges" 412 assert step != None 413 assert step > 0 414 assert pad != None 415 assert pad >= 0 416 assert stop - start < 1e9, "range too large" 417 418 if self._length == 0: 419 # first add optimization 420 stop_adjust = stop - (stop - start) % step 421 if step == 1 or stop_adjust - start >= self._autostep * step: 422 self._ranges = [ (start, stop_adjust, step, pad) ] 423 else: 424 for j in range(start, stop_adjust + step, step): 425 self._ranges.append((j, j, step, pad)) 426 self._length = (stop_adjust - start) / step + 1 427 else: 428 self._ranges, self._length = self._add_range_exfold(start, stop, \ 429 step, pad)
430
431 - def _add_range_exfold(self, start, stop, step, pad):
432 """ 433 Add range expanding then folding all items. 434 """ 435 assert start <= stop, "please provide ordered node index ranges" 436 assert step > 0 437 assert pad != None 438 assert pad >= 0 439 440 items, rgpad = self._expand() 441 items += range(start, stop + 1, step) 442 items.sort() 443 444 return self._fold(items, pad or rgpad)
445
446 - def union(self, other):
447 """ 448 s.union(t) returns a new rangeset with elements from both s and t. 449 """ 450 self_copy = copy.deepcopy(self) 451 self_copy.update(other) 452 return self_copy
453
454 - def __or__(self, other):
455 """ 456 Implements the | operator. So s | t returns a new rangeset with 457 elements from both s and t. 458 """ 459 self._binary_sanity_check(other) 460 return self.union(other)
461
462 - def add(self, elem):
463 """ 464 Add element to RangeSet. 465 """ 466 self.add_range(elem, elem, step=1, pad=0)
467
468 - def update(self, rangeset):
469 """ 470 Update a rangeset with the union of itself and another. 471 """ 472 for start, stop, step, pad in rangeset._ranges: 473 self.add_range(start, stop, step, pad)
474
475 - def clear(self):
476 """ 477 Remove all ranges from this rangeset. 478 """ 479 self._ranges = [] 480 self._length = 0
481
482 - def __ior__(self, other):
483 """ 484 Implements the |= operator. So s |= t returns rangeset s with 485 elements added from t. (Python version 2.5+ required) 486 """ 487 self._binary_sanity_check(other) 488 return self.update(other)
489
490 - def intersection(self, rangeset):
491 """ 492 s.intersection(t) returns a new rangeset with elements common 493 to s and t. 494 """ 495 self_copy = copy.deepcopy(self) 496 self_copy.intersection_update(rangeset) 497 return self_copy
498
499 - def __and__(self, other):
500 """ 501 Implements the & operator. So s & t returns a new rangeset with 502 elements common to s and t. 503 """ 504 self._binary_sanity_check(other) 505 return self.intersection(other)
506
507 - def intersection_update(self, rangeset):
508 """ 509 Intersection with provided RangeSet. 510 """ 511 self._ranges, self._length = self._intersect_exfold(rangeset)
512
513 - def __iand__(self, other):
514 """ 515 Implements the &= operator. So s &= t returns rangeset s keeping 516 only elements also found in t. (Python version 2.5+ required) 517 """ 518 self._binary_sanity_check(other) 519 return self.intersection_update(other)
520
521 - def _intersect_exfold(self, rangeset):
522 """ 523 Calc intersection with the expand/fold method. 524 """ 525 # expand both rangesets 526 items1, pad1 = self._expand() 527 items2, pad2 = rangeset._expand() 528 529 # create a temporary dict with keys from items2 530 iset = dict.fromkeys(items2) 531 532 # fold items that are in both sets 533 return self._fold([e for e in items1 if e in iset], pad1 or pad2)
534
535 - def difference(self, rangeset):
536 """ 537 s.difference(t) returns a new rangeset with elements in s but 538 not in t. 539 in t. 540 """ 541 self_copy = copy.deepcopy(self) 542 self_copy.difference_update(rangeset) 543 return self_copy
544
545 - def __sub__(self, other):
546 """ 547 Implement the - operator. So s - t returns a new rangeset with 548 elements in s but not in t. 549 """ 550 self._binary_sanity_check(other) 551 return self.difference(other)
552
553 - def difference_update(self, rangeset, strict=False):
554 """ 555 s.difference_update(t) returns rangeset s after removing 556 elements found in t. If strict is True, raise KeyError 557 if an element cannot be removed. 558 """ 559 self._ranges, self._length = self._sub_exfold(rangeset, strict)
560
561 - def __isub__(self, other):
562 """ 563 Implement the -= operator. So s -= t returns rangeset s after 564 removing elements found in t. (Python version 2.5+ required) 565 """ 566 self._binary_sanity_check(other) 567 return self.difference_update(other)
568
569 - def remove(self, elem):
570 """ 571 Remove element elem from the RangeSet. Raise KeyError if elem 572 is not contained in the RangeSet. 573 """ 574 items1, pad1 = self._expand() 575 576 try: 577 items1.remove(elem) 578 except ValueError, e: 579 raise KeyError, elem 580 581 self._ranges, self._length = self._fold(items1, pad1)
582
583 - def _sub_exfold(self, rangeset, strict):
584 """ 585 Calc sub/exclusion with the expand/fold method. If strict is 586 True, raise KeyError if the rangeset is not included. 587 """ 588 # expand both rangesets 589 items1, pad1 = self._expand() 590 items2, pad2 = rangeset._expand() 591 592 # create a temporary dict with keys from items2 593 iset = dict.fromkeys(items2) 594 595 if strict: 596 # create a list of remaining items (lst) and update iset 597 lst = [] 598 for e in items1: 599 if e not in iset: 600 lst.append(e) 601 else: 602 del iset[e] 603 604 # if iset is not empty, some elements were not removed 605 if len(iset) > 0: 606 # give the user an indication of the range that cannot be removed 607 missing = RangeSet() 608 missing._ranges, missing._length = self._fold(iset.keys(), pad2) 609 # repr(missing) is implicit here 610 raise KeyError, missing 611 612 return self._fold(lst, pad1 or pad2) 613 else: 614 # fold items that are in set 1 and not in set 2 615 return self._fold([e for e in items1 if e not in iset], pad1 or pad2)
616
617 - def symmetric_difference(self, other):
618 """ 619 s.symmetric_difference(t) returns the symmetric difference of 620 two rangesets as a new RangeSet. 621 622 (ie. all elements that are in exactly one of the rangesets.) 623 """ 624 self_copy = copy.deepcopy(self) 625 self_copy.symmetric_difference_update(other) 626 return self_copy
627
628 - def __xor__(self, other):
629 """ 630 Implement the ^ operator. So s ^ t returns a new rangeset with 631 elements that are in exactly one of the rangesets. 632 """ 633 self._binary_sanity_check(other) 634 return self.symmetric_difference(other)
635
636 - def symmetric_difference_update(self, rangeset):
637 """ 638 s.symmetric_difference_update(t) returns rangeset s keeping all 639 elements that are in exactly one of the rangesets. 640 """ 641 self._ranges, self._length = self._xor_exfold(rangeset)
642
643 - def __ixor__(self, other):
644 """ 645 Implement the ^= operator. So s ^= t returns rangeset s after 646 keeping all elements that are in exactly one of the rangesets. 647 (Python version 2.5+ required) 648 """ 649 self._binary_sanity_check(other) 650 return self.symmetric_difference_update(other)
651
652 - def _xor_exfold(self, rangeset):
653 """ 654 Calc symmetric difference (xor). 655 """ 656 # expand both rangesets 657 items1, pad1 = self._expand() 658 items2, pad2 = rangeset._expand() 659 660 if pad1 != pad2: 661 raise RangeSetPaddingError() 662 # same padding, we're clean... 663 664 # create a temporary dicts 665 iset1 = dict.fromkeys(items1) 666 iset2 = dict.fromkeys(items2) 667 668 # keep items that are in one list only 669 allitems = items1 + items2 670 lst = [e for e in allitems if not e in iset1 or not e in iset2] 671 lst.sort() 672 673 return self._fold(lst, pad1)
674 675
676 -def _NodeSetParse(ns, autostep):
677 """ 678 Internal RangeSet generator for NodeSet or nodeset string pattern 679 parsing. 680 """ 681 # is ns a NodeSet instance? 682 if isinstance(ns, NodeSet): 683 for pat, rangeset in ns._patterns.iteritems(): 684 yield pat, rangeset 685 # or is ns a string? 686 elif type(ns) is str: 687 single_node_re = None 688 pat = str(ns) 689 # avoid misformatting 690 if pat.find('%') >= 0: 691 pat = pat.replace('%', '%%') 692 while pat is not None: 693 # Ignore whitespace(s) for convenience 694 pat = pat.lstrip() 695 696 # What's first: a simple node or a pattern of nodes? 697 comma_idx = pat.find(',') 698 bracket_idx = pat.find('[') 699 700 # Check if the comma is after the bracket, or if there 701 # is no comma at all but some brackets. 702 if bracket_idx >= 0 and (comma_idx > bracket_idx or comma_idx < 0): 703 704 # In this case, we have a pattern of potentially several 705 # nodes. 706 707 # Fill prefix, range and suffix from pattern 708 # eg. "forbin[3,4-10]-ilo" -> "forbin", "3,4-10", "-ilo" 709 pfx, sfx = pat.split('[', 1) 710 711 try: 712 rg, sfx = sfx.split(']', 1) 713 except ValueError: 714 raise NodeSetParseError(pat, "missing bracket") 715 716 # Check if we have a next comma-separated node or pattern 717 if sfx.find(',') < 0: 718 pat = None 719 else: 720 sfx, pat = sfx.split(',', 1) 721 722 # Ignore whitespace(s) 723 sfx = sfx.rstrip() 724 725 # pfx + sfx cannot be empty 726 if len(pfx) + len(sfx) == 0: 727 raise NodeSetParseError(pat, "empty node name") 728 729 # Process comma-separated ranges 730 try: 731 rset = RangeSet(rg, autostep) 732 except RangeSetParseError, e: 733 raise NodeSetParseRangeError(e) 734 735 yield "%s%%s%s" % (pfx, sfx), rset 736 else: 737 # In this case, either there is no comma and no bracket, 738 # or the bracket is after the comma, then just return 739 # the node. 740 if comma_idx < 0: 741 node = pat 742 pat = None # break next time 743 else: 744 node, pat = pat.split(',', 1) 745 # Ignore whitespace(s) 746 node = node.strip() 747 748 if len(node) == 0: 749 raise NodeSetParseError(pat, "empty node name") 750 751 # single node parsing 752 if single_node_re is None: 753 single_node_re = re.compile("(\D*)(\d*)(.*)") 754 755 mo = single_node_re.match(node) 756 if not mo: 757 raise NodeSetParseError(pat, "parse error") 758 pfx, idx, sfx = mo.groups() 759 pfx, sfx = pfx or "", sfx or "" 760 761 # pfx+sfx cannot be empty 762 if len(pfx) + len(sfx) == 0: 763 raise NodeSetParseError(pat, "empty node name") 764 765 if idx: 766 try: 767 rset = RangeSet(idx, autostep) 768 except RangeSetParseError, e: 769 raise NodeSetParseRangeError(e) 770 p = "%s%%s%s" % (pfx, sfx) 771 yield p, rset 772 else: 773 # undefined pad means no node index 774 yield pfx, None
775 776
777 -class NodeSet(object):
778 """ 779 Iterable class of nodes with node ranges support. 780 781 NodeSet creation examples: 782 nodeset = NodeSet() # empty NodeSet 783 nodeset = NodeSet("clustername3") # contains only clustername3 784 nodeset = NodeSet("clustername[5,10-42]") 785 nodeset = NodeSet("clustername[0-10/2]") 786 nodeset = NodeSet("clustername[0-10/2],othername[7-9,120-300]") 787 788 NodeSet provides methods like update(), intersection_update() or 789 difference_update() methods, which conform to the Python Set API. 790 However, unlike RangeSet or standard Set, NodeSet is somewhat not 791 so strict for convenience, and understands NodeSet instance or 792 NodeSet string as argument. Also, there is no strict definition of 793 one element, for example, it IS allowed to do: 794 nodeset.remove("blue[36-40]"). 795 """
796 - def __init__(self, pattern=None, autostep=None):
797 """ 798 Initialize a NodeSet. If no pattern is specified, an empty 799 NodeSet is created. 800 """ 801 self._autostep = autostep 802 self._length = 0 803 self._patterns = {} 804 if pattern is not None: 805 self.update(pattern)
806
807 - def fromlist(cls, l, autostep=None):
808 """ 809 Class method that returns a new NodeSet with nodes from 810 provided list. 811 """ 812 inst = NodeSet(autostep=autostep) 813 for pat in l: 814 inst.update(pat) 815 return inst
816 fromlist = classmethod(fromlist) 817
818 - def __iter__(self):
819 """ 820 Iterate over concret nodes. 821 """ 822 for pat, rangeset in self._patterns.iteritems(): 823 if rangeset: 824 for start, stop, step, pad in rangeset._ranges: 825 while start <= stop: 826 yield pat % ("%0*d" % (pad, start)) 827 start += step 828 else: 829 yield pat
830
831 - def __len__(self):
832 """ 833 Get the number of nodes in NodeSet. 834 """ 835 cnt = 0 836 for rangeset in self._patterns.itervalues(): 837 if rangeset: 838 cnt += len(rangeset) 839 else: 840 cnt += 1 841 return cnt
842
843 - def __str__(self):
844 """ 845 Get pdsh-like, ranges-based pattern of node list. 846 """ 847 result = "" 848 for pat, rangeset in self._patterns.iteritems(): 849 if rangeset: 850 s = str(rangeset) 851 cnt = len(rangeset) 852 if cnt > 1: 853 s = "[" + s + "]" 854 result += pat % s 855 else: 856 result += pat 857 result += "," 858 return result[:-1]
859
860 - def __contains__(self, other):
861 """ 862 Is node contained in NodeSet ? 863 """ 864 return self.issuperset(other)
865
866 - def _binary_sanity_check(self, other):
867 # check that the other argument to a binary operation is also 868 # a NodeSet, raising a TypeError otherwise. 869 if not isinstance(other, NodeSet): 870 raise TypeError, "Binary operation only permitted between NodeSets"
871
872 - def issubset(self, other):
873 """ 874 Report whether another nodeset contains this nodeset. 875 """ 876 binary = None 877 878 # type check is needed for this case... 879 if isinstance(other, NodeSet): 880 binary = other 881 elif type(other) is str: 882 binary = NodeSet(other) 883 else: 884 raise TypeError, "Binary operation only permitted between NodeSets or string" 885 886 return binary.issuperset(self)
887
888 - def issuperset(self, other):
889 """ 890 Report whether this nodeset contains another nodeset. 891 """ 892 status = False 893 for pat, erangeset in _NodeSetParse(other, self._autostep): 894 rangeset = self._patterns.get(pat) 895 if rangeset: 896 status = rangeset.issuperset(erangeset) 897 else: 898 # might be an unnumbered node (key in dict but no value) 899 status = self._patterns.has_key(pat) 900 return status
901 902 # inequality comparisons using the is-subset relation 903 __le__ = issubset 904 __ge__ = issuperset 905
906 - def __lt__(self, other):
907 """ 908 x.__lt__(y) <==> x<y 909 """ 910 return len(self) < len(other) and self.issubset(other)
911
912 - def __gt__(self, other):
913 """ 914 x.__gt__(y) <==> x>y 915 """ 916 return len(self) > len(other) and self.issuperset(other)
917
918 - def __getitem__(self, i):
919 """ 920 Return the node at index i. For convenience only, not 921 optimized as of version 1.0. 922 """ 923 return list(self)[i]
924
925 - def __getslice__(self, i, j):
926 """ 927 Return the slice from index i to index j-1. For convenience 928 only, not optimized as of version 1.0. 929 """ 930 return NodeSet.fromlist(list(self)[i:j])
931
932 - def _add_rangeset(self, pat, rangeset):
933 """ 934 Add a rangeset to a new or existing pattern. 935 """ 936 # get patterns dict entry 937 pat_e = self._patterns.get(pat) 938 939 if pat_e: 940 # don't play with prefix: if there is a value, there is a 941 # rangeset. 942 assert rangeset != None 943 944 # add rangeset in corresponding pattern rangeset 945 pat_e.update(rangeset) 946 else: 947 # create new pattern (with possibly rangeset=None) 948 self._patterns[pat] = copy.copy(rangeset)
949
950 - def union(self, other):
951 """ 952 s.union(t) returns a new set with elements from both s and t. 953 """ 954 self_copy = copy.deepcopy(self) 955 self_copy.update(other) 956 return self_copy
957
958 - def __or__(self, other):
959 """ 960 Implements the | operator. So s | t returns a new nodeset with 961 elements from both s and t. 962 """ 963 self._binary_sanity_check(other) 964 return self.union(other)
965
966 - def add(self, other):
967 """ 968 Add node to NodeSet. 969 """ 970 self.update(other)
971
972 - def update(self, other):
973 """ 974 s.update(t) returns nodeset s with elements added from t. 975 """ 976 for pat, rangeset in _NodeSetParse(other, self._autostep): 977 self._add_rangeset(pat, rangeset)
978
979 - def clear(self):
980 """ 981 Remove all nodes from this nodeset. 982 """ 983 self._patterns.clear() 984 self._length = 0
985
986 - def __ior__(self, other):
987 """ 988 Implements the |= operator. So s |= t returns nodeset s with 989 elements added from t. (Python version 2.5+ required) 990 """ 991 self._binary_sanity_check(other) 992 return self.update(other)
993
994 - def intersection(self, other):
995 """ 996 s.intersection(t) returns a new set with elements common to s 997 and t. 998 """ 999 self_copy = copy.deepcopy(self) 1000 self_copy.intersection_update(other) 1001 return self_copy
1002
1003 - def __and__(self, other):
1004 """ 1005 Implements the & operator. So s & t returns a new nodeset with 1006 elements common to s and t. 1007 """ 1008 self._binary_sanity_check(other) 1009 return self.intersection(other)
1010
1011 - def intersection_update(self, other):
1012 """ 1013 s.intersection_update(t) returns nodeset s keeping only 1014 elements also found in t. 1015 """ 1016 if other is self: 1017 return 1018 1019 tmp_ns = NodeSet() 1020 1021 for pat, irangeset in _NodeSetParse(other, self._autostep): 1022 rangeset = self._patterns.get(pat) 1023 if rangeset: 1024 rs = copy.copy(rangeset) 1025 rs.intersection_update(irangeset) 1026 # ignore pattern if empty rangeset 1027 if len(rs) > 0: 1028 tmp_ns._add_rangeset(pat, rs) 1029 elif not irangeset and pat in self._patterns: 1030 # intersect two nodes with no rangeset 1031 tmp_ns._add_rangeset(pat, None) 1032 1033 # Substitute 1034 self._patterns = tmp_ns._patterns
1035
1036 - def __iand__(self, other):
1037 """ 1038 Implements the &= operator. So s &= t returns nodeset s keeping 1039 only elements also found in t. (Python version 2.5+ required) 1040 """ 1041 self._binary_sanity_check(other) 1042 return self.intersection_update(other)
1043
1044 - def difference(self, other):
1045 """ 1046 s.difference(t) returns a new NodeSet with elements in s but not 1047 in t. 1048 """ 1049 self_copy = copy.deepcopy(self) 1050 self_copy.difference_update(other) 1051 return self_copy
1052
1053 - def __sub__(self, other):
1054 """ 1055 Implement the - operator. So s - t returns a new nodeset with 1056 elements in s but not in t. 1057 """ 1058 self._binary_sanity_check(other) 1059 return self.difference(other)
1060
1061 - def difference_update(self, other, strict=False):
1062 """ 1063 s.difference_update(t) returns nodeset s after removing 1064 elements found in t. If strict is True, raise KeyError 1065 if an element cannot be removed. 1066 """ 1067 # the purge of each empty pattern is done afterward to allow self = ns 1068 purge_patterns = [] 1069 1070 # iterate first over exclude nodeset rangesets which is usually smaller 1071 for pat, erangeset in _NodeSetParse(other, self._autostep): 1072 # if pattern is found, deal with it 1073 rangeset = self._patterns.get(pat) 1074 if rangeset: 1075 # sub rangeset, raise KeyError if not found 1076 rangeset.difference_update(erangeset, strict) 1077 1078 # check if no range left and add pattern to purge list 1079 if len(rangeset) == 0: 1080 purge_patterns.append(pat) 1081 else: 1082 # unnumbered node exclusion 1083 if self._patterns.has_key(pat): 1084 purge_patterns.append(pat) 1085 elif strict: 1086 raise KeyError, pat 1087 1088 for pat in purge_patterns: 1089 del self._patterns[pat]
1090
1091 - def __isub__(self, other):
1092 """ 1093 Implement the -= operator. So s -= t returns nodeset s after 1094 removing elements found in t. (Python version 2.5+ required) 1095 """ 1096 self._binary_sanity_check(other) 1097 return self.difference_update(other)
1098
1099 - def remove(self, elem):
1100 """ 1101 Remove element elem from the nodeset. Raise KeyError if elem 1102 is not contained in the nodeset. 1103 """ 1104 self.difference_update(elem, True)
1105
1106 - def symmetric_difference(self, other):
1107 """ 1108 s.symmetric_difference(t) returns the symmetric difference of 1109 two nodesets as a new NodeSet. 1110 1111 (ie. all nodes that are in exactly one of the nodesets.) 1112 """ 1113 self_copy = copy.deepcopy(self) 1114 self_copy.symmetric_difference_update(other) 1115 return self_copy
1116
1117 - def __xor__(self, other):
1118 """ 1119 Implement the ^ operator. So s ^ t returns a new NodeSet with 1120 nodes that are in exactly one of the nodesets. 1121 """ 1122 self._binary_sanity_check(other) 1123 return self.symmetric_difference(other)
1124
1125 - def symmetric_difference_update(self, other):
1126 """ 1127 s.symmetric_difference_update(t) returns nodeset s keeping all 1128 nodes that are in exactly one of the nodesets. 1129 """ 1130 binary = None 1131 1132 # type check is needed for this case... 1133 if isinstance(other, NodeSet): 1134 binary = other 1135 elif type(other) is str: 1136 binary = NodeSet(other) 1137 else: 1138 raise TypeError, "Binary operation only permitted between NodeSets or string" 1139 1140 purge_patterns = [] 1141 1142 # iterate over our rangesets 1143 for pat, rangeset in self._patterns.iteritems(): 1144 brangeset = binary._patterns.get(pat) 1145 if brangeset: 1146 rangeset.symmetric_difference_update(brangeset) 1147 else: 1148 if binary._patterns.has_key(pat): 1149 purge_patterns.append(pat) 1150 1151 # iterate over binary's rangesets 1152 for pat, brangeset in binary._patterns.iteritems(): 1153 rangeset = self._patterns.get(pat) 1154 if not rangeset and not self._patterns.has_key(pat): 1155 self._add_rangeset(pat, brangeset) 1156 1157 # check for patterns cleanup 1158 for pat, rangeset in self._patterns.iteritems(): 1159 if rangeset is not None and len(rangeset) == 0: 1160 purge_patterns.append(pat) 1161 1162 # cleanup 1163 for pat in purge_patterns: 1164 del self._patterns[pat]
1165
1166 - def __ixor__(self, other):
1167 """ 1168 Implement the ^= operator. So s ^= t returns nodeset s after 1169 keeping all nodes that are in exactly one of the nodesets. 1170 (Python version 2.5+ required) 1171 """ 1172 self._binary_sanity_check(other) 1173 return self.symmetric_difference_update(other)
1174 1175
1176 -def expand(pat):
1177 """ 1178 Commodity function that expands a pdsh-like pattern into a list of 1179 nodes. 1180 """ 1181 return list(NodeSet(pat))
1182
1183 -def fold(pat):
1184 """ 1185 Commodity function that clean dups and fold provided pattern with 1186 ranges and "/step" support. 1187 """ 1188 return str(NodeSet(pat))
1189 1190 1191 # doctest 1192
1193 -def _test():
1194 import doctest 1195 doctest.testmod()
1196 1197 if __name__ == '__main__': 1198 _test() 1199