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

Source Code for Module ClusterShell.NodeSet

   1  # 
   2  # Copyright (C) 2007-2016 CEA/DAM 
   3  # Copyright (C) 2007-2016 Aurelien Degremont <aurelien.degremont@cea.fr> 
   4  # Copyright (C) 2015-2016 Stephane Thiell <sthiell@stanford.edu> 
   5  # 
   6  # This file is part of ClusterShell. 
   7  # 
   8  # ClusterShell is free software; you can redistribute it and/or 
   9  # modify it under the terms of the GNU Lesser General Public 
  10  # License as published by the Free Software Foundation; either 
  11  # version 2.1 of the License, or (at your option) any later version. 
  12  # 
  13  # ClusterShell is distributed in the hope that it will be useful, 
  14  # but WITHOUT ANY WARRANTY; without even the implied warranty of 
  15  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU 
  16  # Lesser General Public License for more details. 
  17  # 
  18  # You should have received a copy of the GNU Lesser General Public 
  19  # License along with ClusterShell; if not, write to the Free Software 
  20  # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 
  21   
  22  """ 
  23  Cluster node set module. 
  24   
  25  A module to efficiently deal with node sets and node groups. 
  26  Instances of NodeSet provide similar operations than the builtin set() type, 
  27  see http://www.python.org/doc/lib/set-objects.html 
  28   
  29  Usage example 
  30  ============= 
  31    >>> # Import NodeSet class 
  32    ... from ClusterShell.NodeSet import NodeSet 
  33    >>> 
  34    >>> # Create a new nodeset from string 
  35    ... nodeset = NodeSet("cluster[1-30]") 
  36    >>> # Add cluster32 to nodeset 
  37    ... nodeset.update("cluster32") 
  38    >>> # Remove from nodeset 
  39    ... nodeset.difference_update("cluster[2-5,8-31]") 
  40    >>> # Print nodeset as a pdsh-like pattern 
  41    ... print nodeset 
  42    cluster[1,6-7,32] 
  43    >>> # Iterate over node names in nodeset 
  44    ... for node in nodeset: 
  45    ...     print node 
  46    cluster1 
  47    cluster6 
  48    cluster7 
  49    cluster32 
  50  """ 
  51   
  52  import re 
  53  import string 
  54  import sys 
  55   
  56  from ClusterShell.Defaults import config_paths 
  57  import ClusterShell.NodeUtils as NodeUtils 
  58   
  59  # Import all RangeSet module public objects 
  60  from ClusterShell.RangeSet import RangeSet, RangeSetND, AUTOSTEP_DISABLED 
  61  from ClusterShell.RangeSet import RangeSetException, RangeSetParseError 
  62  from ClusterShell.RangeSet import RangeSetPaddingError 
  63   
  64   
  65  # Define default GroupResolver object used by NodeSet 
  66  DEF_GROUPS_CONFIGS = config_paths('groups.conf') 
  67  ILLEGAL_GROUP_CHARS = set("@,!&^*") 
  68  _DEF_RESOLVER_STD_GROUP = NodeUtils.GroupResolverConfig(DEF_GROUPS_CONFIGS, 
  69                                                          ILLEGAL_GROUP_CHARS) 
  70  # Standard group resolver 
  71  RESOLVER_STD_GROUP = _DEF_RESOLVER_STD_GROUP 
  72  # Special constants for NodeSet's resolver parameter 
  73  #   RESOLVER_NOGROUP => avoid any group resolution at all 
  74  #   RESOLVER_NOINIT  => reserved use for optimized copy() 
  75  RESOLVER_NOGROUP = -1 
  76  RESOLVER_NOINIT = -2 
  77  # 1.5 compat (deprecated) 
  78  STD_GROUP_RESOLVER = RESOLVER_STD_GROUP 
  79  NOGROUP_RESOLVER = RESOLVER_NOGROUP 
80 81 82 -class NodeSetException(Exception):
83 """Base NodeSet exception class."""
84
85 -class NodeSetError(NodeSetException):
86 """Raised when an error is encountered."""
87
88 -class NodeSetParseError(NodeSetError):
89 """Raised when NodeSet parsing cannot be done properly."""
90 - def __init__(self, part, msg):
91 if part: 92 msg = "%s: \"%s\"" % (msg, part) 93 NodeSetError.__init__(self, msg) 94 # faulty part; this allows you to target the error 95 self.part = part
96
97 -class NodeSetParseRangeError(NodeSetParseError):
98 """Raised when bad range is encountered during NodeSet parsing."""
99 - def __init__(self, rset_exc):
100 NodeSetParseError.__init__(self, str(rset_exc), "bad range")
101
102 -class NodeSetExternalError(NodeSetError):
103 """Raised when an external error is encountered."""
104
105 106 -class NodeSetBase(object):
107 """ 108 Base class for NodeSet. 109 110 This class allows node set base object creation from specified string 111 pattern and rangeset object. If optional copy_rangeset boolean flag is 112 set to True (default), provided rangeset object is copied (if needed), 113 otherwise it may be referenced (should be seen as an ownership transfer 114 upon creation). 115 116 This class implements core node set arithmetics (no string parsing here). 117 118 Example: 119 >>> nsb = NodeSetBase('node%s-ipmi', RangeSet('1-5,7'), False) 120 >>> str(nsb) 121 'node[1-5,7]-ipmi' 122 >>> nsb = NodeSetBase('node%s-ib%s', RangeSetND([['1-5,7', '1-2']]), False) 123 >>> str(nsb) 124 'node[1-5,7]-ib[1-2]' 125 """
126 - def __init__(self, pattern=None, rangeset=None, copy_rangeset=True, 127 autostep=None, fold_axis=None):
128 """New NodeSetBase object initializer""" 129 self._autostep = autostep 130 self._length = 0 131 self._patterns = {} 132 self.fold_axis = fold_axis #: iterable over nD 0-indexed axis 133 if pattern: 134 self._add(pattern, rangeset, copy_rangeset) 135 elif rangeset: 136 raise ValueError("missing pattern")
137
138 - def get_autostep(self):
139 """Get autostep value (property)""" 140 return self._autostep
141
142 - def set_autostep(self, val):
143 """Set autostep value (property)""" 144 if val is None: 145 self._autostep = None 146 else: 147 # Work around the pickling issue of sys.maxint (+inf) in py2.4 148 self._autostep = min(int(val), AUTOSTEP_DISABLED) 149 150 # Update our RangeSet/RangeSetND objects 151 for pat, rset in self._patterns.iteritems(): 152 if rset: 153 rset.autostep = self._autostep
154 155 autostep = property(get_autostep, set_autostep) 156
157 - def _iter(self):
158 """Iterator on internal item tuples 159 (pattern, indexes, padding, autostep).""" 160 for pat, rset in sorted(self._patterns.iteritems()): 161 if rset: 162 autostep = rset.autostep 163 if rset.dim() == 1: 164 assert isinstance(rset, RangeSet) 165 padding = rset.padding 166 for idx in rset: 167 yield pat, (idx,), (padding,), autostep 168 else: 169 for args, padding in rset.iter_padding(): 170 yield pat, args, padding, autostep 171 else: 172 yield pat, None, None, None
173
174 - def _iterbase(self):
175 """Iterator on single, one-item NodeSetBase objects.""" 176 for pat, ivec, pad, autostep in self._iter(): 177 rset = None # 'no node index' by default 178 if ivec is not None: 179 assert len(ivec) > 0 180 if len(ivec) == 1: 181 rset = RangeSet.fromone(ivec[0], pad[0] or 0, autostep) 182 else: 183 rset = RangeSetND([ivec], pad, autostep) 184 yield NodeSetBase(pat, rset)
185
186 - def __iter__(self):
187 """Iterator on single nodes as string.""" 188 # Does not call self._iterbase() + str() for better performance. 189 for pat, ivec, pads, _ in self._iter(): 190 if ivec is not None: 191 # For performance reasons, add a special case for 1D RangeSet 192 if len(ivec) == 1: 193 yield pat % ("%0*d" % (pads[0] or 0, ivec[0])) 194 else: 195 yield pat % tuple(["%0*d" % (pad or 0, i) \ 196 for pad, i in zip(pads, ivec)]) 197 else: 198 yield pat % ()
199 200 # define striter() alias for convenience (to match RangeSet.striter()) 201 striter = __iter__ 202 203 # define nsiter() as an object-based iterator that could be used for 204 # __iter__() in the future... 205
206 - def nsiter(self):
207 """Object-based NodeSet iterator on single nodes.""" 208 for pat, ivec, pads, autostep in self._iter(): 209 nodeset = self.__class__() 210 if ivec is not None: 211 if len(ivec) == 1: 212 pad = pads[0] or 0 213 nodeset._add_new(pat, RangeSet.fromone(ivec[0], pad)) 214 else: 215 nodeset._add_new(pat, RangeSetND([ivec], pads, autostep)) 216 else: 217 nodeset._add_new(pat, None) 218 yield nodeset
219
220 - def contiguous(self):
221 """Object-based NodeSet iterator on contiguous node sets. 222 223 Contiguous node set contains nodes with same pattern name and a 224 contiguous range of indexes, like foobar[1-100].""" 225 for pat, rangeset in sorted(self._patterns.iteritems()): 226 if rangeset: 227 for cont_rset in rangeset.contiguous(): 228 nodeset = self.__class__() 229 nodeset._add_new(pat, cont_rset) 230 yield nodeset 231 else: 232 nodeset = self.__class__() 233 nodeset._add_new(pat, None) 234 yield nodeset
235
236 - def __len__(self):
237 """Get the number of nodes in NodeSet.""" 238 cnt = 0 239 for rangeset in self._patterns.itervalues(): 240 if rangeset: 241 cnt += len(rangeset) 242 else: 243 cnt += 1 244 return cnt
245
246 - def _iter_nd_pat(self, pat, rset):
247 """ 248 Take a pattern and a RangeSetND object and iterate over nD computed 249 nodeset strings while following fold_axis constraints. 250 """ 251 try: 252 dimcnt = rset.dim() 253 if self.fold_axis is None: 254 # fold along all axis (default) 255 fold_axis = range(dimcnt) 256 else: 257 # set of user-provided fold axis (support negative numbers) 258 fold_axis = [int(x) % dimcnt for x in self.fold_axis 259 if -dimcnt <= int(x) < dimcnt] 260 except (TypeError, ValueError), exc: 261 raise NodeSetParseError("fold_axis=%s" % self.fold_axis, exc) 262 263 for rgvec in rset.vectors(): 264 rgnargs = [] # list of str rangeset args 265 for axis, rangeset in enumerate(rgvec): 266 # build an iterator over rangeset strings to add 267 if len(rangeset) > 1: 268 if axis not in fold_axis: # expand 269 rgstrit = rangeset.striter() 270 else: 271 rgstrit = ["[%s]" % rangeset] 272 else: 273 rgstrit = [str(rangeset)] 274 275 # aggregate/expand along previous computed axis... 276 t_rgnargs = [] 277 for rgstr in rgstrit: # 1-time when not expanding 278 if not rgnargs: 279 t_rgnargs.append([rgstr]) 280 else: 281 for rga in rgnargs: 282 t_rgnargs.append(rga + [rgstr]) 283 rgnargs = t_rgnargs 284 285 # get nodeset patterns formatted with range strings 286 for rgargs in rgnargs: 287 yield pat % tuple(rgargs)
288
289 - def __str__(self):
290 """Get ranges-based pattern of node list.""" 291 results = [] 292 try: 293 for pat, rset in sorted(self._patterns.iteritems()): 294 if not rset: 295 results.append(pat % ()) 296 elif rset.dim() == 1: 297 # check if allowed to fold even for 1D pattern 298 if self.fold_axis is None or \ 299 list(x for x in self.fold_axis if -1 <= int(x) < 1): 300 rgs = str(rset) 301 cnt = len(rset) 302 if cnt > 1: 303 rgs = "[%s]" % rgs 304 results.append(pat % rgs) 305 else: 306 results.extend((pat % rgs for rgs in rset.striter())) 307 elif rset.dim() > 1: 308 results.extend(self._iter_nd_pat(pat, rset)) 309 except TypeError: 310 raise NodeSetParseError(pat, "Internal error: node pattern and " 311 "ranges mismatch") 312 return ",".join(results)
313
314 - def copy(self):
315 """Return a shallow copy.""" 316 cpy = self.__class__() 317 cpy.fold_axis = self.fold_axis 318 cpy._autostep = self._autostep 319 cpy._length = self._length 320 dic = {} 321 for pat, rangeset in self._patterns.iteritems(): 322 if rangeset is None: 323 dic[pat] = None 324 else: 325 dic[pat] = rangeset.copy() 326 cpy._patterns = dic 327 return cpy
328
329 - def __contains__(self, other):
330 """Is node contained in NodeSet ?""" 331 return self.issuperset(other)
332
333 - def _binary_sanity_check(self, other):
334 # check that the other argument to a binary operation is also 335 # a NodeSet, raising a TypeError otherwise. 336 if not isinstance(other, NodeSetBase): 337 raise TypeError, \ 338 "Binary operation only permitted between NodeSetBase"
339
340 - def issubset(self, other):
341 """Report whether another nodeset contains this nodeset.""" 342 self._binary_sanity_check(other) 343 return other.issuperset(self)
344
345 - def issuperset(self, other):
346 """Report whether this nodeset contains another nodeset.""" 347 self._binary_sanity_check(other) 348 status = True 349 for pat, erangeset in other._patterns.iteritems(): 350 rangeset = self._patterns.get(pat) 351 if rangeset: 352 status = rangeset.issuperset(erangeset) 353 else: 354 # might be an unnumbered node (key in dict but no value) 355 status = self._patterns.has_key(pat) 356 if not status: 357 break 358 return status
359
360 - def __eq__(self, other):
361 """NodeSet equality comparison.""" 362 # See comment for for RangeSet.__eq__() 363 if not isinstance(other, NodeSetBase): 364 return NotImplemented 365 return len(self) == len(other) and self.issuperset(other)
366 367 # inequality comparisons using the is-subset relation 368 __le__ = issubset 369 __ge__ = issuperset 370
371 - def __lt__(self, other):
372 """x.__lt__(y) <==> x<y""" 373 self._binary_sanity_check(other) 374 return len(self) < len(other) and self.issubset(other)
375
376 - def __gt__(self, other):
377 """x.__gt__(y) <==> x>y""" 378 self._binary_sanity_check(other) 379 return len(self) > len(other) and self.issuperset(other)
380
381 - def _extractslice(self, index):
382 """Private utility function: extract slice parameters from slice object 383 `index` for an list-like object of size `length`.""" 384 length = len(self) 385 if index.start is None: 386 sl_start = 0 387 elif index.start < 0: 388 sl_start = max(0, length + index.start) 389 else: 390 sl_start = index.start 391 if index.stop is None: 392 sl_stop = sys.maxint 393 elif index.stop < 0: 394 sl_stop = max(0, length + index.stop) 395 else: 396 sl_stop = index.stop 397 if index.step is None: 398 sl_step = 1 399 elif index.step < 0: 400 # We support negative step slicing with no start/stop, ie. r[::-n]. 401 if index.start is not None or index.stop is not None: 402 raise IndexError, \ 403 "illegal start and stop when negative step is used" 404 # As RangeSet elements are ordered internally, adjust sl_start 405 # to fake backward stepping in case of negative slice step. 406 stepmod = (length + -index.step - 1) % -index.step 407 if stepmod > 0: 408 sl_start += stepmod 409 sl_step = -index.step 410 else: 411 sl_step = index.step 412 if not isinstance(sl_start, int) or not isinstance(sl_stop, int) \ 413 or not isinstance(sl_step, int): 414 raise TypeError, "slice indices must be integers" 415 return sl_start, sl_stop, sl_step
416
417 - def __getitem__(self, index):
418 """Return the node at specified index or a subnodeset when a slice is 419 specified.""" 420 if isinstance(index, slice): 421 inst = NodeSetBase() 422 sl_start, sl_stop, sl_step = self._extractslice(index) 423 sl_next = sl_start 424 if sl_stop <= sl_next: 425 return inst 426 length = 0 427 for pat, rangeset in sorted(self._patterns.iteritems()): 428 if rangeset: 429 cnt = len(rangeset) 430 offset = sl_next - length 431 if offset < cnt: 432 num = min(sl_stop - sl_next, cnt - offset) 433 inst._add(pat, rangeset[offset:offset + num:sl_step]) 434 else: 435 #skip until sl_next is reached 436 length += cnt 437 continue 438 else: 439 cnt = num = 1 440 if sl_next > length: 441 length += cnt 442 continue 443 inst._add(pat, None) 444 # adjust sl_next... 445 sl_next += num 446 if (sl_next - sl_start) % sl_step: 447 sl_next = sl_start + \ 448 ((sl_next - sl_start)/sl_step + 1) * sl_step 449 if sl_next >= sl_stop: 450 break 451 length += cnt 452 return inst 453 elif isinstance(index, int): 454 if index < 0: 455 length = len(self) 456 if index >= -length: 457 index = length + index # - -index 458 else: 459 raise IndexError, "%d out of range" % index 460 length = 0 461 for pat, rangeset in sorted(self._patterns.iteritems()): 462 if rangeset: 463 cnt = len(rangeset) 464 if index < length + cnt: 465 # return a subrangeset of size 1 to manage padding 466 if rangeset.dim() == 1: 467 return pat % rangeset[index-length:index-length+1] 468 else: 469 sub = rangeset[index-length:index-length+1] 470 for rgvec in sub.vectors(): 471 return pat % (tuple(rgvec)) 472 else: 473 cnt = 1 474 if index == length: 475 return pat 476 length += cnt 477 raise IndexError, "%d out of range" % index 478 else: 479 raise TypeError, "NodeSet indices must be integers"
480
481 - def _add_new(self, pat, rangeset):
482 """Add nodes from a (pat, rangeset) tuple. 483 Predicate: pattern does not exist in current set. RangeSet object is 484 referenced (not copied).""" 485 assert pat not in self._patterns 486 self._patterns[pat] = rangeset
487
488 - def _add(self, pat, rangeset, copy_rangeset=True):
489 """Add nodes from a (pat, rangeset) tuple. 490 `pat' may be an existing pattern and `rangeset' may be None. 491 RangeSet or RangeSetND objects are copied if re-used internally 492 when provided and if copy_rangeset flag is set. 493 """ 494 if pat in self._patterns: 495 # existing pattern: get RangeSet or RangeSetND entry... 496 pat_e = self._patterns[pat] 497 # sanity checks 498 if (pat_e is None) is not (rangeset is None): 499 raise NodeSetError("Invalid operation") 500 # entry may exist but set to None (single node) 501 if pat_e: 502 pat_e.update(rangeset) 503 else: 504 # new pattern... 505 if rangeset and copy_rangeset: 506 # default is to inherit rangeset autostep value 507 rangeset = rangeset.copy() 508 # but if set, self._autostep does override it 509 if self._autostep is not None: 510 # works with rangeset 1D or nD 511 rangeset.autostep = self._autostep 512 self._add_new(pat, rangeset)
513
514 - def union(self, other):
515 """ 516 s.union(t) returns a new set with elements from both s and t. 517 """ 518 self_copy = self.copy() 519 self_copy.update(other) 520 return self_copy
521
522 - def __or__(self, other):
523 """ 524 Implements the | operator. So s | t returns a new nodeset with 525 elements from both s and t. 526 """ 527 if not isinstance(other, NodeSetBase): 528 return NotImplemented 529 return self.union(other)
530
531 - def add(self, other):
532 """ 533 Add node to NodeSet. 534 """ 535 self.update(other)
536
537 - def update(self, other):
538 """ 539 s.update(t) returns nodeset s with elements added from t. 540 """ 541 for pat, rangeset in other._patterns.iteritems(): 542 self._add(pat, rangeset)
543
544 - def updaten(self, others):
545 """ 546 s.updaten(list) returns nodeset s with elements added from given list. 547 """ 548 for other in others: 549 self.update(other)
550
551 - def clear(self):
552 """ 553 Remove all nodes from this nodeset. 554 """ 555 self._patterns.clear()
556
557 - def __ior__(self, other):
558 """ 559 Implements the |= operator. So ``s |= t`` returns nodeset s with 560 elements added from t. (Python version 2.5+ required) 561 """ 562 self._binary_sanity_check(other) 563 self.update(other) 564 return self
565
566 - def intersection(self, other):
567 """ 568 s.intersection(t) returns a new set with elements common to s 569 and t. 570 """ 571 self_copy = self.copy() 572 self_copy.intersection_update(other) 573 return self_copy
574
575 - def __and__(self, other):
576 """ 577 Implements the & operator. So ``s & t`` returns a new nodeset with 578 elements common to s and t. 579 """ 580 if not isinstance(other, NodeSet): 581 return NotImplemented 582 return self.intersection(other)
583
584 - def intersection_update(self, other):
585 """ 586 ``s.intersection_update(t)`` returns nodeset s keeping only 587 elements also found in t. 588 """ 589 if other is self: 590 return 591 592 tmp_ns = NodeSetBase() 593 594 for pat, irangeset in other._patterns.iteritems(): 595 rangeset = self._patterns.get(pat) 596 if rangeset: 597 irset = rangeset.intersection(irangeset) 598 # ignore pattern if empty rangeset 599 if len(irset) > 0: 600 tmp_ns._add(pat, irset, copy_rangeset=False) 601 elif not irangeset and pat in self._patterns: 602 # intersect two nodes with no rangeset 603 tmp_ns._add(pat, None) 604 605 # Substitute 606 self._patterns = tmp_ns._patterns
607
608 - def __iand__(self, other):
609 """ 610 Implements the &= operator. So ``s &= t`` returns nodeset s keeping 611 only elements also found in t. (Python version 2.5+ required) 612 """ 613 self._binary_sanity_check(other) 614 self.intersection_update(other) 615 return self
616
617 - def difference(self, other):
618 """ 619 ``s.difference(t)`` returns a new NodeSet with elements in s but not 620 in t. 621 """ 622 self_copy = self.copy() 623 self_copy.difference_update(other) 624 return self_copy
625
626 - def __sub__(self, other):
627 """ 628 Implement the - operator. So ``s - t`` returns a new nodeset with 629 elements in s but not in t. 630 """ 631 if not isinstance(other, NodeSetBase): 632 return NotImplemented 633 return self.difference(other)
634
635 - def difference_update(self, other, strict=False):
636 """ 637 ``s.difference_update(t)`` removes from s all the elements found in t. 638 639 :raises KeyError: an element cannot be removed (only if strict is 640 True) 641 """ 642 # the purge of each empty pattern is done afterward to allow self = ns 643 purge_patterns = [] 644 645 # iterate first over exclude nodeset rangesets which is usually smaller 646 for pat, erangeset in other._patterns.iteritems(): 647 # if pattern is found, deal with it 648 rangeset = self._patterns.get(pat) 649 if rangeset: 650 # sub rangeset, raise KeyError if not found 651 rangeset.difference_update(erangeset, strict) 652 653 # check if no range left and add pattern to purge list 654 if len(rangeset) == 0: 655 purge_patterns.append(pat) 656 else: 657 # unnumbered node exclusion 658 if self._patterns.has_key(pat): 659 purge_patterns.append(pat) 660 elif strict: 661 raise KeyError, pat 662 663 for pat in purge_patterns: 664 del self._patterns[pat]
665
666 - def __isub__(self, other):
667 """ 668 Implement the -= operator. So ``s -= t`` returns nodeset s after 669 removing elements found in t. (Python version 2.5+ required) 670 """ 671 self._binary_sanity_check(other) 672 self.difference_update(other) 673 return self
674
675 - def remove(self, elem):
676 """ 677 Remove element elem from the nodeset. Raise KeyError if elem 678 is not contained in the nodeset. 679 680 :raises KeyError: elem is not contained in the nodeset 681 """ 682 self.difference_update(elem, True)
683
684 - def symmetric_difference(self, other):
685 """ 686 ``s.symmetric_difference(t)`` returns the symmetric difference of 687 two nodesets as a new NodeSet. 688 689 (ie. all nodes that are in exactly one of the nodesets.) 690 """ 691 self_copy = self.copy() 692 self_copy.symmetric_difference_update(other) 693 return self_copy
694
695 - def __xor__(self, other):
696 """ 697 Implement the ^ operator. So ``s ^ t`` returns a new NodeSet with 698 nodes that are in exactly one of the nodesets. 699 """ 700 if not isinstance(other, NodeSet): 701 return NotImplemented 702 return self.symmetric_difference(other)
703
704 - def symmetric_difference_update(self, other):
705 """ 706 ``s.symmetric_difference_update(t)`` returns nodeset s keeping all 707 nodes that are in exactly one of the nodesets. 708 """ 709 purge_patterns = [] 710 711 # iterate over our rangesets 712 for pat, rangeset in self._patterns.iteritems(): 713 brangeset = other._patterns.get(pat) 714 if brangeset: 715 rangeset.symmetric_difference_update(brangeset) 716 else: 717 if other._patterns.has_key(pat): 718 purge_patterns.append(pat) 719 720 # iterate over other's rangesets 721 for pat, brangeset in other._patterns.iteritems(): 722 rangeset = self._patterns.get(pat) 723 if not rangeset and not pat in self._patterns: 724 self._add(pat, brangeset) 725 726 # check for patterns cleanup 727 for pat, rangeset in self._patterns.iteritems(): 728 if rangeset is not None and len(rangeset) == 0: 729 purge_patterns.append(pat) 730 731 # cleanup 732 for pat in purge_patterns: 733 del self._patterns[pat]
734
735 - def __ixor__(self, other):
736 """ 737 Implement the ^= operator. So ``s ^= t`` returns nodeset s after 738 keeping all nodes that are in exactly one of the nodesets. 739 (Python version 2.5+ required) 740 """ 741 self._binary_sanity_check(other) 742 self.symmetric_difference_update(other) 743 return self
744
745 746 -def _strip_escape(nsstr):
747 """ 748 Helper to prepare a nodeset string for parsing: trim boundary 749 whitespaces and escape special characters. 750 """ 751 return nsstr.strip().replace('%', '%%')
752
753 754 -class ParsingEngine(object):
755 """ 756 Class that is able to transform a source into a NodeSetBase. 757 """ 758 OP_CODES = { 'update': ',', 759 'difference_update': '!', 760 'intersection_update': '&', 761 'symmetric_difference_update': '^' } 762 763 BRACKET_OPEN = '[' 764 BRACKET_CLOSE = ']' 765
766 - def __init__(self, group_resolver):
767 """ 768 Initialize Parsing Engine. 769 """ 770 self.group_resolver = group_resolver 771 self.base_node_re = re.compile("(\D*)(\d*)")
772
773 - def parse(self, nsobj, autostep):
774 """ 775 Parse provided object if possible and return a NodeSetBase object. 776 """ 777 # passing None is supported 778 if nsobj is None: 779 return NodeSetBase() 780 781 # is nsobj a NodeSetBase instance? 782 if isinstance(nsobj, NodeSetBase): 783 return nsobj 784 785 # or is nsobj a string? 786 if isinstance(nsobj, basestring): 787 try: 788 return self.parse_string(str(nsobj), autostep) 789 except (NodeUtils.GroupSourceQueryFailed, RuntimeError), exc: 790 raise NodeSetParseError(nsobj, str(exc)) 791 792 raise TypeError("Unsupported NodeSet input %s" % type(nsobj))
793
794 - def parse_string(self, nsstr, autostep, namespace=None):
795 """Parse provided string in optional namespace. 796 797 This method parses string, resolves all node groups, and 798 computes set operations. 799 800 Return a NodeSetBase object. 801 """ 802 nodeset = NodeSetBase() 803 nsstr = _strip_escape(nsstr) 804 805 for opc, pat, rgnd in self._scan_string(nsstr, autostep): 806 # Parser main debugging: 807 #print "OPC %s PAT %s RANGESETS %s" % (opc, pat, rgnd) 808 if self.group_resolver and pat[0] == '@': 809 ns_group = NodeSetBase() 810 for nodegroup in NodeSetBase(pat, rgnd): 811 # parse/expand nodes group: get group string and namespace 812 ns_str_ext, ns_nsp_ext = self.parse_group_string(nodegroup, 813 namespace) 814 if ns_str_ext: # may still contain groups 815 # recursively parse and aggregate result 816 ns_group.update(self.parse_string(ns_str_ext, 817 autostep, 818 ns_nsp_ext)) 819 # perform operation 820 getattr(nodeset, opc)(ns_group) 821 else: 822 getattr(nodeset, opc)(NodeSetBase(pat, rgnd, False)) 823 824 return nodeset
825
826 - def parse_string_single(self, nsstr, autostep):
827 """Parse provided string and return a NodeSetBase object.""" 828 pat, rangesets = self._scan_string_single(_strip_escape(nsstr), 829 autostep) 830 if len(rangesets) > 1: 831 rgobj = RangeSetND([rangesets], None, autostep, copy_rangeset=False) 832 elif len(rangesets) == 1: 833 rgobj = rangesets[0] 834 else: # non-indexed nodename 835 rgobj = None 836 return NodeSetBase(pat, rgobj, False)
837
838 - def parse_group(self, group, namespace=None, autostep=None):
839 """Parse provided single group name (without @ prefix).""" 840 assert self.group_resolver is not None 841 nodestr = self.group_resolver.group_nodes(group, namespace) 842 return self.parse(",".join(nodestr), autostep)
843
844 - def parse_group_string(self, nodegroup, namespace=None):
845 """Parse provided raw nodegroup string in optional namespace. 846 847 Warning: 1 pass only, may still return groups. 848 849 Return a tuple (grp_resolved_string, namespace). 850 """ 851 assert nodegroup[0] == '@' 852 assert self.group_resolver is not None 853 grpstr = group = nodegroup[1:] 854 if grpstr.find(':') >= 0: 855 # specified namespace does always override 856 namespace, group = grpstr.split(':', 1) 857 if group == '*': # @* or @source:* magic 858 reslist = self.all_nodes(namespace) 859 else: 860 reslist = self.group_resolver.group_nodes(group, namespace) 861 return ','.join(reslist), namespace
862
863 - def grouplist(self, namespace=None):
864 """ 865 Return a sorted list of groups from current resolver (in optional 866 group source / namespace). 867 """ 868 grpset = NodeSetBase() 869 for grpstr in self.group_resolver.grouplist(namespace): 870 # We scan each group string to expand any range seen... 871 grpstr = _strip_escape(grpstr) 872 for opc, pat, rgnd in self._scan_string(grpstr, None): 873 getattr(grpset, opc)(NodeSetBase(pat, rgnd, False)) 874 return list(grpset)
875
876 - def all_nodes(self, namespace=None):
877 """Get all nodes from group resolver as a list of strings.""" 878 # namespace is the optional group source 879 assert self.group_resolver is not None 880 alln = [] 881 try: 882 # Ask resolver to provide all nodes. 883 alln = self.group_resolver.all_nodes(namespace) 884 except NodeUtils.GroupSourceNoUpcall: 885 try: 886 # As the resolver is not able to provide all nodes directly, 887 # failback to list + map(s) method: 888 for grp in self.grouplist(namespace): 889 alln += self.group_resolver.group_nodes(grp, namespace) 890 except NodeUtils.GroupSourceNoUpcall: 891 # We are not able to find "all" nodes, definitely. 892 msg = "Not enough working methods (all or map + list) to " \ 893 "get all nodes" 894 raise NodeSetExternalError(msg) 895 except NodeUtils.GroupSourceQueryFailed, exc: 896 raise NodeSetExternalError("Failed to get all nodes: %s" % exc) 897 return alln
898
899 - def _next_op(self, pat):
900 """Opcode parsing subroutine.""" 901 op_idx = -1 902 next_op_code = None 903 for opc, idx in [(k, pat.find(v)) \ 904 for k, v in ParsingEngine.OP_CODES.iteritems()]: 905 if idx >= 0 and (op_idx < 0 or idx <= op_idx): 906 next_op_code = opc 907 op_idx = idx 908 return op_idx, next_op_code
909
910 - def _scan_string_single(self, nsstr, autostep):
911 """Single node scan, returns (pat, list of rangesets)""" 912 # single node parsing 913 pfx_nd = [mobj.groups() for mobj in self.base_node_re.finditer(nsstr)] 914 pfx_nd = pfx_nd[:-1] 915 if not pfx_nd: 916 raise NodeSetParseError(nsstr, "parse error") 917 918 # pfx+sfx cannot be empty 919 if len(pfx_nd) == 1 and len(pfx_nd[0][0]) == 0: 920 raise NodeSetParseError(nsstr, "empty node name") 921 922 pat = "" 923 rangesets = [] 924 for pfx, idx in pfx_nd: 925 if idx: 926 # optimization: process single index padding directly 927 pad = 0 928 if int(idx) != 0: 929 idxs = idx.lstrip("0") 930 if len(idx) - len(idxs) > 0: 931 pad = len(idx) 932 idxint = int(idxs) 933 else: 934 if len(idx) > 1: 935 pad = len(idx) 936 idxint = 0 937 if idxint > 1e100: 938 raise NodeSetParseRangeError( \ 939 RangeSetParseError(idx, "invalid rangeset index")) 940 # optimization: use numerical RangeSet constructor 941 pat += "%s%%s" % pfx 942 rangesets.append(RangeSet.fromone(idxint, pad, autostep)) 943 else: 944 # undefined pad means no node index 945 pat += pfx 946 return pat, rangesets
947
948 - def _scan_string(self, nsstr, autostep):
949 """Parsing engine's string scanner method (iterator).""" 950 next_op_code = 'update' 951 while nsstr: 952 # Ignore whitespace(s) for convenience 953 nsstr = nsstr.lstrip() 954 955 rsets = [] 956 op_code = next_op_code 957 958 op_idx, next_op_code = self._next_op(nsstr) 959 bracket_idx = nsstr.find(self.BRACKET_OPEN) 960 961 # Check if the operator is after the bracket, or if there 962 # is no operator at all but some brackets. 963 if bracket_idx >= 0 and (op_idx > bracket_idx or op_idx < 0): 964 # In this case, we have a pattern of potentially several 965 # nodes. 966 # Fill prefix, range and suffix from pattern 967 # eg. "forbin[3,4-10]-ilo" -> "forbin", "3,4-10", "-ilo" 968 newpat = "" 969 sfx = nsstr 970 while bracket_idx >= 0 and (op_idx > bracket_idx or op_idx < 0): 971 pfx, sfx = sfx.split(self.BRACKET_OPEN, 1) 972 try: 973 rng, sfx = sfx.split(self.BRACKET_CLOSE, 1) 974 except ValueError: 975 raise NodeSetParseError(nsstr, "missing bracket") 976 977 # illegal closing bracket checks 978 if pfx.find(self.BRACKET_CLOSE) > -1: 979 raise NodeSetParseError(pfx, "illegal closing bracket") 980 981 if len(sfx) > 0: 982 bra_end = sfx.find(self.BRACKET_CLOSE) 983 bra_start = sfx.find(self.BRACKET_OPEN) 984 if bra_start == -1: 985 bra_start = bra_end + 1 986 if bra_end >= 0 and bra_end < bra_start: 987 msg = "illegal closing bracket" 988 raise NodeSetParseError(sfx, msg) 989 990 pfxlen, sfxlen = len(pfx), len(sfx) 991 992 if sfxlen > 0: 993 # amending trailing digits generates /steps 994 sfx, rng = self._amend_trailing_digits(sfx, rng) 995 996 if pfxlen > 0: 997 # this method supports /steps 998 pfx, rng = self._amend_leading_digits(pfx, rng) 999 if pfx: 1000 # scan any nonempty pfx as a single node (no bracket) 1001 pfx, pfxrvec = self._scan_string_single(pfx, autostep) 1002 rsets += pfxrvec 1003 1004 # readahead for sanity check 1005 bracket_idx = sfx.find(self.BRACKET_OPEN, 1006 bracket_idx - pfxlen) 1007 op_idx, next_op_code = self._next_op(sfx) 1008 1009 # Check for empty component or sequenced ranges 1010 if len(pfx) == 0 and op_idx == 0: 1011 raise NodeSetParseError(sfx, "empty node name before") 1012 1013 if len(sfx) > 0 and sfx[0] == '[': 1014 msg = "illegal reopening bracket" 1015 raise NodeSetParseError(sfx, msg) 1016 1017 newpat += "%s%%s" % pfx 1018 try: 1019 rsets.append(RangeSet(rng, autostep)) 1020 except RangeSetParseError, ex: 1021 raise NodeSetParseRangeError(ex) 1022 1023 # the following test forbids fully numeric nodeset 1024 if len(pfx) + len(sfx) == 0: 1025 msg = "fully numeric nodeset" 1026 raise NodeSetParseError(nsstr, msg) 1027 1028 # Check if we have a next op-separated node or pattern 1029 op_idx, next_op_code = self._next_op(sfx) 1030 if op_idx < 0: 1031 nsstr = None 1032 else: 1033 opc = self.OP_CODES[next_op_code] 1034 sfx, nsstr = sfx.split(opc, 1) 1035 # Detected character operator so right operand is mandatory 1036 if not nsstr: 1037 msg = "missing nodeset operand with '%s' operator" % opc 1038 raise NodeSetParseError(None, msg) 1039 1040 # Ignore whitespace(s) 1041 sfx = sfx.rstrip() 1042 if sfx: 1043 sfx, sfxrvec = self._scan_string_single(sfx, autostep) 1044 newpat += sfx 1045 rsets += sfxrvec 1046 else: 1047 # In this case, either there is no comma and no bracket, 1048 # or the bracket is after the comma, then just return 1049 # the node. 1050 if op_idx < 0: 1051 node = nsstr 1052 nsstr = None # break next time 1053 else: 1054 opc = self.OP_CODES[next_op_code] 1055 node, nsstr = nsstr.split(opc, 1) 1056 # Detected character operator so both operands are mandatory 1057 if not node or not nsstr: 1058 msg = "missing nodeset operand with '%s' operator" % opc 1059 raise NodeSetParseError(node or nsstr, msg) 1060 1061 # Check for illegal closing bracket 1062 if node.find(self.BRACKET_CLOSE) > -1: 1063 raise NodeSetParseError(node, "illegal closing bracket") 1064 1065 # Ignore whitespace(s) 1066 node = node.rstrip() 1067 newpat, rsets = self._scan_string_single(node, autostep) 1068 1069 if len(rsets) > 1: 1070 yield op_code, newpat, RangeSetND([rsets], None, autostep, 1071 copy_rangeset=False) 1072 elif len(rsets) == 1: 1073 yield op_code, newpat, rsets[0] 1074 else: 1075 yield op_code, newpat, None
1076
1077 - def _amend_leading_digits(self, outer, inner):
1078 """Helper to get rid of leading bracket digits. 1079 1080 Take a bracket outer prefix string and an inner range set string and 1081 return amended strings. 1082 """ 1083 outerstrip = outer.rstrip(string.digits) 1084 outerlen, outerstriplen = len(outer), len(outerstrip) 1085 if outerstriplen < outerlen: 1086 # get outer bracket leading digits 1087 outerdigits = outer[outerstriplen:] 1088 inner = ','.join( 1089 '-'.join(outerdigits + bound for bound in elem.split('-')) 1090 for elem in (str(subrng) 1091 for subrng in RangeSet(inner).contiguous())) 1092 return outerstrip, inner
1093
1094 - def _amend_trailing_digits(self, outer, inner):
1095 """Helper to get rid of trailing bracket digits. 1096 1097 Take a bracket outer suffix string and an inner range set string and 1098 return amended strings. 1099 """ 1100 outerstrip = outer.lstrip(string.digits) 1101 outerlen, outerstriplen = len(outer), len(outerstrip) 1102 if outerstriplen < outerlen: 1103 # step syntax is not compatible with trailing digits 1104 if '/' in inner: 1105 msg = "illegal trailing digits after range with steps" 1106 raise NodeSetParseError(outer, msg) 1107 # get outer bracket trailing digits 1108 outerdigits = outer[0:outerlen-outerstriplen] 1109 outlen = len(outerdigits) 1110 def shiftstep(orig, power): 1111 """Add needed step after shifting range indexes""" 1112 if '-' in orig: 1113 return orig + '/1' + '0' * power 1114 return orig # do not use /step for single index
1115 inner = ','.join(shiftstep(s, outlen) for s in 1116 ('-'.join(bound + outerdigits 1117 for bound in elem.split('-')) 1118 for elem in inner.split(','))) 1119 return outerstrip, inner
1120
1121 -class NodeSet(NodeSetBase):
1122 """ 1123 Iterable class of nodes with node ranges support. 1124 1125 NodeSet creation examples: 1126 1127 >>> nodeset = NodeSet() # empty NodeSet 1128 >>> nodeset = NodeSet("cluster3") # contains only cluster3 1129 >>> nodeset = NodeSet("cluster[5,10-42]") 1130 >>> nodeset = NodeSet("cluster[0-10/2]") 1131 >>> nodeset = NodeSet("cluster[0-10/2],othername[7-9,120-300]") 1132 1133 NodeSet provides methods like update(), intersection_update() or 1134 difference_update() methods, which conform to the Python Set API. 1135 However, unlike RangeSet or standard Set, NodeSet is somewhat not 1136 so strict for convenience, and understands NodeSet instance or 1137 NodeSet string as argument. Also, there is no strict definition of 1138 one element, for example, it IS allowed to do: 1139 1140 >>> nodeset = NodeSet("blue[1-50]") 1141 >>> nodeset.remove("blue[36-40]") 1142 >>> print nodeset 1143 blue[1-35,41-50] 1144 1145 Additionally, the NodeSet class recognizes the "extended string 1146 pattern" which adds support for union (special character ","), 1147 difference ("!"), intersection ("&") and symmetric difference ("^") 1148 operations. String patterns are read from left to right, by 1149 proceeding any character operators accordinately. 1150 1151 Extended string pattern usage examples: 1152 1153 >>> nodeset = NodeSet("node[0-10],node[14-16]") # union 1154 >>> nodeset = NodeSet("node[0-10]!node[8-10]") # difference 1155 >>> nodeset = NodeSet("node[0-10]&node[5-13]") # intersection 1156 >>> nodeset = NodeSet("node[0-10]^node[5-13]") # xor 1157 """ 1158 1159 _VERSION = 2 1160
1161 - def __init__(self, nodes=None, autostep=None, resolver=None, 1162 fold_axis=None):
1163 """Initialize a NodeSet object. 1164 1165 The `nodes` argument may be a valid nodeset string or a NodeSet 1166 object. If no nodes are specified, an empty NodeSet is created. 1167 1168 The optional `autostep` argument is passed to underlying 1169 :class:`.RangeSet.RangeSet` objects and aims to enable and make use of 1170 the range/step syntax (eg. ``node[1-9/2]``) when converting NodeSet to 1171 string (using folding). To enable this feature, autostep must be set 1172 there to the min number of indexes that are found at equal distance of 1173 each other inside a range before NodeSet starts to use this syntax. For 1174 example, `autostep=3` (or less) will pack ``n[2,4,6]`` into 1175 ``n[2-6/2]``. Default autostep value is None which means "inherit 1176 whenever possible", ie. do not enable it unless set in NodeSet objects 1177 passed as `nodes` here or during arithmetic operations. 1178 You may however use the special ``AUTOSTEP_DISABLED`` constant to force 1179 turning off autostep feature. 1180 1181 The optional `resolver` argument may be used to override the group 1182 resolving behavior for this NodeSet object. It can either be set to a 1183 :class:`.NodeUtils.GroupResolver` object, to the ``RESOLVER_NOGROUP`` 1184 constant to disable any group resolution, or to None (default) to use 1185 standard NodeSet group resolver (see :func:`.set_std_group_resolver()` 1186 at the module level to change it if needed). 1187 1188 nD nodeset only: the optional `fold_axis` parameter, if specified, set 1189 the public instance member `fold_axis` to an iterable over nD 0-indexed 1190 axis integers. This parameter may be used to disengage some nD folding. 1191 That may be useful as all cluster tools don't support folded-nD nodeset 1192 syntax. Pass ``[0]``, for example, to only fold along first axis (that 1193 is, to fold first dimension using ``[a-b]`` rangeset syntax whenever 1194 possible). Using `fold_axis` ensures that rangeset won't be folded on 1195 unspecified axis, but please note however, that using `fold_axis` may 1196 lead to suboptimial folding, this is because NodeSet algorithms are 1197 optimized for folding along all axis (default behavior). 1198 """ 1199 NodeSetBase.__init__(self, autostep=autostep, fold_axis=fold_axis) 1200 1201 # Set group resolver. 1202 if resolver in (RESOLVER_NOGROUP, RESOLVER_NOINIT): 1203 self._resolver = None 1204 else: 1205 self._resolver = resolver or RESOLVER_STD_GROUP 1206 1207 # Initialize default parser. 1208 if resolver == RESOLVER_NOINIT: 1209 self._parser = None 1210 else: 1211 self._parser = ParsingEngine(self._resolver) 1212 self.update(nodes)
1213 1214 @classmethod
1215 - def _fromlist1(cls, nodelist, autostep=None, resolver=None):
1216 """Class method that returns a new NodeSet with single nodes from 1217 provided list (optimized constructor).""" 1218 inst = NodeSet(autostep=autostep, resolver=resolver) 1219 for single in nodelist: 1220 inst.update(inst._parser.parse_string_single(single, autostep)) 1221 return inst
1222 1223 @classmethod
1224 - def fromlist(cls, nodelist, autostep=None, resolver=None):
1225 """Class method that returns a new NodeSet with nodes from provided 1226 list.""" 1227 inst = NodeSet(autostep=autostep, resolver=resolver) 1228 inst.updaten(nodelist) 1229 return inst
1230 1231 @classmethod
1232 - def fromall(cls, groupsource=None, autostep=None, resolver=None):
1233 """Class method that returns a new NodeSet with all nodes from optional 1234 groupsource.""" 1235 inst = NodeSet(autostep=autostep, resolver=resolver) 1236 try: 1237 if not inst._resolver: 1238 raise NodeSetExternalError("Group resolver is not defined") 1239 else: 1240 # fill this nodeset with all nodes found by resolver 1241 inst.updaten(inst._parser.all_nodes(groupsource)) 1242 except NodeUtils.GroupResolverError, exc: 1243 errmsg = "Group source error (%s: %s)" % (exc.__class__.__name__, 1244 exc) 1245 raise NodeSetExternalError(errmsg) 1246 return inst
1247
1248 - def __getstate__(self):
1249 """Called when pickling: remove references to group resolver.""" 1250 odict = self.__dict__.copy() 1251 odict['_version'] = NodeSet._VERSION 1252 del odict['_resolver'] 1253 del odict['_parser'] 1254 return odict
1255
1256 - def __setstate__(self, dic):
1257 """Called when unpickling: restore parser using non group 1258 resolver.""" 1259 self.__dict__.update(dic) 1260 self._resolver = None 1261 self._parser = ParsingEngine(None) 1262 if getattr(self, '_version', 1) <= 1: 1263 self.fold_axis = None 1264 # if setting state from first version, a conversion is needed to 1265 # support native RangeSetND 1266 old_patterns = self._patterns 1267 self._patterns = {} 1268 for pat, rangeset in sorted(old_patterns.iteritems()): 1269 if rangeset: 1270 assert isinstance(rangeset, RangeSet) 1271 rgs = str(rangeset) 1272 if len(rangeset) > 1: 1273 rgs = "[%s]" % rgs 1274 self.update(pat % rgs) 1275 else: 1276 self.update(pat)
1277
1278 - def copy(self):
1279 """Return a shallow copy of a NodeSet.""" 1280 cpy = self.__class__(resolver=RESOLVER_NOINIT) 1281 dic = {} 1282 for pat, rangeset in self._patterns.iteritems(): 1283 if rangeset is None: 1284 dic[pat] = None 1285 else: 1286 dic[pat] = rangeset.copy() 1287 cpy._patterns = dic 1288 cpy.fold_axis = self.fold_axis 1289 cpy._autostep = self._autostep 1290 cpy._resolver = self._resolver 1291 cpy._parser = self._parser 1292 return cpy
1293 1294 __copy__ = copy # For the copy module 1295
1296 - def _find_groups(self, node, namespace, allgroups):
1297 """Find groups of node by namespace.""" 1298 if allgroups: 1299 # find node groups using in-memory allgroups 1300 for grp, nodeset in allgroups.iteritems(): 1301 if node in nodeset: 1302 yield grp 1303 else: 1304 # find node groups using resolver 1305 try: 1306 for group in self._resolver.node_groups(node, namespace): 1307 yield group 1308 except NodeUtils.GroupSourceQueryFailed, exc: 1309 msg = "Group source query failed: %s" % exc 1310 raise NodeSetExternalError(msg)
1311
1312 - def _groups2(self, groupsource=None, autostep=None):
1313 """Find node groups this nodeset belongs to. [private]""" 1314 if not self._resolver: 1315 raise NodeSetExternalError("No node group resolver") 1316 try: 1317 # Get all groups in specified group source. 1318 allgrplist = self._parser.grouplist(groupsource) 1319 except NodeUtils.GroupSourceError: 1320 # If list query failed, we still might be able to regroup 1321 # using reverse. 1322 allgrplist = None 1323 groups_info = {} 1324 allgroups = {} 1325 # Check for external reverse presence, and also use the 1326 # following heuristic: external reverse is used only when number 1327 # of groups is greater than the NodeSet size. 1328 if self._resolver.has_node_groups(groupsource) and \ 1329 (not allgrplist or len(allgrplist) >= len(self)): 1330 # use external reverse 1331 pass 1332 else: 1333 if not allgrplist: # list query failed and no way to reverse! 1334 return groups_info # empty 1335 try: 1336 # use internal reverse: populate allgroups 1337 for grp in allgrplist: 1338 nodelist = self._resolver.group_nodes(grp, groupsource) 1339 allgroups[grp] = NodeSet(",".join(nodelist), 1340 resolver=self._resolver) 1341 except NodeUtils.GroupSourceQueryFailed, exc: 1342 # External result inconsistency 1343 raise NodeSetExternalError("Unable to map a group " \ 1344 "previously listed\n\tFailed command: %s" % exc) 1345 1346 # For each NodeSetBase in self, find its groups. 1347 for node in self._iterbase(): 1348 for grp in self._find_groups(node, groupsource, allgroups): 1349 if grp not in groups_info: 1350 nodes = self._parser.parse_group(grp, groupsource, autostep) 1351 groups_info[grp] = (1, nodes) 1352 else: 1353 i, nodes = groups_info[grp] 1354 groups_info[grp] = (i + 1, nodes) 1355 return groups_info
1356
1357 - def groups(self, groupsource=None, noprefix=False):
1358 """Find node groups this nodeset belongs to. 1359 1360 Return a dictionary of the form: 1361 group_name => (group_nodeset, contained_nodeset) 1362 1363 Group names are always prefixed with "@". If groupsource is provided, 1364 they are prefixed with "@groupsource:", unless noprefix is True. 1365 """ 1366 groups = self._groups2(groupsource, self._autostep) 1367 result = {} 1368 for grp, (_, nsb) in groups.iteritems(): 1369 if groupsource and not noprefix: 1370 key = "@%s:%s" % (groupsource, grp) 1371 else: 1372 key = "@" + grp 1373 result[key] = (NodeSet(nsb, resolver=self._resolver), 1374 self.intersection(nsb)) 1375 return result
1376
1377 - def regroup(self, groupsource=None, autostep=None, overlap=False, 1378 noprefix=False):
1379 """Regroup nodeset using node groups. 1380 1381 Try to find fully matching node groups (within specified groupsource) 1382 and return a string that represents this node set (containing these 1383 potential node groups). When no matching node groups are found, this 1384 method returns the same result as str().""" 1385 groups = self._groups2(groupsource, autostep) 1386 if not groups: 1387 return str(self) 1388 1389 # Keep only groups that are full. 1390 fulls = [] 1391 for k, (i, nodes) in groups.iteritems(): 1392 assert i <= len(nodes) 1393 if i == len(nodes): 1394 fulls.append((i, k)) 1395 1396 rest = NodeSet(self, resolver=RESOLVER_NOGROUP) 1397 regrouped = NodeSet(resolver=RESOLVER_NOGROUP) 1398 1399 bigalpha = lambda x, y: cmp(y[0], x[0]) or cmp(x[1], y[1]) 1400 1401 # Build regrouped NodeSet by selecting largest groups first. 1402 for _, grp in sorted(fulls, cmp=bigalpha): 1403 if not overlap and groups[grp][1] not in rest: 1404 continue 1405 if groupsource and not noprefix: 1406 regrouped.update("@%s:%s" % (groupsource, grp)) 1407 else: 1408 regrouped.update("@" + grp) 1409 rest.difference_update(groups[grp][1]) 1410 if not rest: 1411 return str(regrouped) 1412 1413 if regrouped: 1414 return "%s,%s" % (regrouped, rest) 1415 1416 return str(rest)
1417
1418 - def issubset(self, other):
1419 """ 1420 Report whether another nodeset contains this nodeset. 1421 """ 1422 nodeset = self._parser.parse(other, self._autostep) 1423 return NodeSetBase.issuperset(nodeset, self)
1424
1425 - def issuperset(self, other):
1426 """ 1427 Report whether this nodeset contains another nodeset. 1428 """ 1429 nodeset = self._parser.parse(other, self._autostep) 1430 return NodeSetBase.issuperset(self, nodeset)
1431
1432 - def __getitem__(self, index):
1433 """ 1434 Return the node at specified index or a subnodeset when a slice 1435 is specified. 1436 """ 1437 base = NodeSetBase.__getitem__(self, index) 1438 if not isinstance(base, NodeSetBase): 1439 return base 1440 # return a real NodeSet 1441 inst = NodeSet(autostep=self._autostep, resolver=self._resolver) 1442 inst._patterns = base._patterns 1443 return inst
1444
1445 - def split(self, nbr):
1446 """ 1447 Split the nodeset into nbr sub-nodesets (at most). Each 1448 sub-nodeset will have the same number of elements more or 1449 less 1. Current nodeset remains unmodified. 1450 1451 >>> for nodeset in NodeSet("foo[1-5]").split(3): 1452 ... print nodeset 1453 foo[1-2] 1454 foo[3-4] 1455 foo5 1456 """ 1457 assert(nbr > 0) 1458 1459 # We put the same number of element in each sub-nodeset. 1460 slice_size = len(self) / nbr 1461 left = len(self) % nbr 1462 1463 begin = 0 1464 for i in range(0, min(nbr, len(self))): 1465 length = slice_size + int(i < left) 1466 yield self[begin:begin + length] 1467 begin += length
1468
1469 - def update(self, other):
1470 """ 1471 s.update(t) returns nodeset s with elements added from t. 1472 """ 1473 nodeset = self._parser.parse(other, self._autostep) 1474 NodeSetBase.update(self, nodeset)
1475
1476 - def intersection_update(self, other):
1477 """ 1478 s.intersection_update(t) returns nodeset s keeping only 1479 elements also found in t. 1480 """ 1481 nodeset = self._parser.parse(other, self._autostep) 1482 NodeSetBase.intersection_update(self, nodeset)
1483
1484 - def difference_update(self, other, strict=False):
1485 """ 1486 s.difference_update(t) removes from s all the elements 1487 found in t. If strict is True, raise KeyError if an 1488 element in t cannot be removed from s. 1489 """ 1490 nodeset = self._parser.parse(other, self._autostep) 1491 NodeSetBase.difference_update(self, nodeset, strict)
1492
1493 - def symmetric_difference_update(self, other):
1494 """ 1495 s.symmetric_difference_update(t) returns nodeset s keeping all 1496 nodes that are in exactly one of the nodesets. 1497 """ 1498 nodeset = self._parser.parse(other, self._autostep) 1499 NodeSetBase.symmetric_difference_update(self, nodeset)
1500
1501 1502 -def expand(pat):
1503 """ 1504 Commodity function that expands a nodeset pattern into a list of nodes. 1505 """ 1506 return list(NodeSet(pat))
1507
1508 -def fold(pat):
1509 """ 1510 Commodity function that clean dups and fold provided pattern with ranges 1511 and "/step" support. 1512 """ 1513 return str(NodeSet(pat))
1514
1515 -def grouplist(namespace=None, resolver=None):
1516 """ 1517 Commodity function that retrieves the list of raw groups for a specified 1518 group namespace (or use default namespace). 1519 Group names are not prefixed with "@". 1520 """ 1521 return ParsingEngine(resolver or RESOLVER_STD_GROUP).grouplist(namespace)
1522
1523 -def std_group_resolver():
1524 """ 1525 Get the current resolver used for standard "@" group resolution. 1526 """ 1527 return RESOLVER_STD_GROUP
1528
1529 -def set_std_group_resolver(new_resolver):
1530 """ 1531 Override the resolver used for standard "@" group resolution. The 1532 new resolver should be either an instance of 1533 NodeUtils.GroupResolver or None. In the latter case, the group 1534 resolver is restored to the default one. 1535 """ 1536 global RESOLVER_STD_GROUP 1537 RESOLVER_STD_GROUP = new_resolver or _DEF_RESOLVER_STD_GROUP
1538