12dbeadc800c04105847409e588ef16a0f8650b4
[scons.git] / src / engine / SCons / compat / _scons_sets.py
1 """Classes to represent arbitrary sets (including sets of sets).
2
3 This module implements sets using dictionaries whose values are
4 ignored.  The usual operations (union, intersection, deletion, etc.)
5 are provided as both methods and operators.
6
7 Important: sets are not sequences!  While they support 'x in s',
8 'len(s)', and 'for x in s', none of those operations are unique for
9 sequences; for example, mappings support all three as well.  The
10 characteristic operation for sequences is subscripting with small
11 integers: s[i], for i in range(len(s)).  Sets don't support
12 subscripting at all.  Also, sequences allow multiple occurrences and
13 their elements have a definite order; sets on the other hand don't
14 record multiple occurrences and don't remember the order of element
15 insertion (which is why they don't support s[i]).
16
17 The following classes are provided:
18
19 BaseSet -- All the operations common to both mutable and immutable
20     sets. This is an abstract class, not meant to be directly
21     instantiated.
22
23 Set -- Mutable sets, subclass of BaseSet; not hashable.
24
25 ImmutableSet -- Immutable sets, subclass of BaseSet; hashable.
26     An iterable argument is mandatory to create an ImmutableSet.
27
28 _TemporarilyImmutableSet -- A wrapper around a Set, hashable,
29     giving the same hash value as the immutable set equivalent
30     would have.  Do not use this class directly.
31
32 Only hashable objects can be added to a Set. In particular, you cannot
33 really add a Set as an element to another Set; if you try, what is
34 actually added is an ImmutableSet built from it (it compares equal to
35 the one you tried adding).
36
37 When you ask if `x in y' where x is a Set and y is a Set or
38 ImmutableSet, x is wrapped into a _TemporarilyImmutableSet z, and
39 what's tested is actually `z in y'.
40
41 """
42
43 # Code history:
44 #
45 # - Greg V. Wilson wrote the first version, using a different approach
46 #   to the mutable/immutable problem, and inheriting from dict.
47 #
48 # - Alex Martelli modified Greg's version to implement the current
49 #   Set/ImmutableSet approach, and make the data an attribute.
50 #
51 # - Guido van Rossum rewrote much of the code, made some API changes,
52 #   and cleaned up the docstrings.
53 #
54 # - Raymond Hettinger added a number of speedups and other
55 #   improvements.
56
57 from __future__ import generators
58 try:
59     from itertools import ifilter, ifilterfalse
60 except ImportError:
61     # Code to make the module run under Py2.2
62     def ifilter(predicate, iterable):
63         if predicate is None:
64             def predicate(x):
65                 return x
66         for x in iterable:
67             if predicate(x):
68                 yield x
69     def ifilterfalse(predicate, iterable):
70         if predicate is None:
71             def predicate(x):
72                 return x
73         for x in iterable:
74             if not predicate(x):
75                 yield x
76     try:
77         True, False
78     except NameError:
79         True, False = (0==0, 0!=0)
80
81 __all__ = ['BaseSet', 'Set', 'ImmutableSet']
82
83 class BaseSet(object):
84     """Common base class for mutable and immutable sets."""
85
86     __slots__ = ['_data']
87
88     # Constructor
89
90     def __init__(self):
91         """This is an abstract class."""
92         # Don't call this from a concrete subclass!
93         if self.__class__ is BaseSet:
94             raise TypeError, ("BaseSet is an abstract class.  "
95                               "Use Set or ImmutableSet.")
96
97     # Standard protocols: __len__, __repr__, __str__, __iter__
98
99     def __len__(self):
100         """Return the number of elements of a set."""
101         return len(self._data)
102
103     def __repr__(self):
104         """Return string representation of a set.
105
106         This looks like 'Set([<list of elements>])'.
107         """
108         return self._repr()
109
110     # __str__ is the same as __repr__
111     __str__ = __repr__
112
113     def _repr(self, sorted=False):
114         elements = self._data.keys()
115         if sorted:
116             elements.sort()
117         return '%s(%r)' % (self.__class__.__name__, elements)
118
119     def __iter__(self):
120         """Return an iterator over the elements or a set.
121
122         This is the keys iterator for the underlying dict.
123         """
124         return self._data.iterkeys()
125
126     # Three-way comparison is not supported.  However, because __eq__ is
127     # tried before __cmp__, if Set x == Set y, x.__eq__(y) returns True and
128     # then cmp(x, y) returns 0 (Python doesn't actually call __cmp__ in this
129     # case).
130
131     def __cmp__(self, other):
132         raise TypeError, "can't compare sets using cmp()"
133
134     # Equality comparisons using the underlying dicts.  Mixed-type comparisons
135     # are allowed here, where Set == z for non-Set z always returns False,
136     # and Set != z always True.  This allows expressions like "x in y" to
137     # give the expected result when y is a sequence of mixed types, not
138     # raising a pointless TypeError just because y contains a Set, or x is
139     # a Set and y contain's a non-set ("in" invokes only __eq__).
140     # Subtle:  it would be nicer if __eq__ and __ne__ could return
141     # NotImplemented instead of True or False.  Then the other comparand
142     # would get a chance to determine the result, and if the other comparand
143     # also returned NotImplemented then it would fall back to object address
144     # comparison (which would always return False for __eq__ and always
145     # True for __ne__).  However, that doesn't work, because this type
146     # *also* implements __cmp__:  if, e.g., __eq__ returns NotImplemented,
147     # Python tries __cmp__ next, and the __cmp__ here then raises TypeError.
148
149     def __eq__(self, other):
150         if isinstance(other, BaseSet):
151             return self._data == other._data
152         else:
153             return False
154
155     def __ne__(self, other):
156         if isinstance(other, BaseSet):
157             return self._data != other._data
158         else:
159             return True
160
161     # Copying operations
162
163     def copy(self):
164         """Return a shallow copy of a set."""
165         result = self.__class__()
166         result._data.update(self._data)
167         return result
168
169     __copy__ = copy # For the copy module
170
171     def __deepcopy__(self, memo):
172         """Return a deep copy of a set; used by copy module."""
173         # This pre-creates the result and inserts it in the memo
174         # early, in case the deep copy recurses into another reference
175         # to this same set.  A set can't be an element of itself, but
176         # it can certainly contain an object that has a reference to
177         # itself.
178         from copy import deepcopy
179         result = self.__class__()
180         memo[id(self)] = result
181         data = result._data
182         value = True
183         for elt in self:
184             data[deepcopy(elt, memo)] = value
185         return result
186
187     # Standard set operations: union, intersection, both differences.
188     # Each has an operator version (e.g. __or__, invoked with |) and a
189     # method version (e.g. union).
190     # Subtle:  Each pair requires distinct code so that the outcome is
191     # correct when the type of other isn't suitable.  For example, if
192     # we did "union = __or__" instead, then Set().union(3) would return
193     # NotImplemented instead of raising TypeError (albeit that *why* it
194     # raises TypeError as-is is also a bit subtle).
195
196     def __or__(self, other):
197         """Return the union of two sets as a new set.
198
199         (I.e. all elements that are in either set.)
200         """
201         if not isinstance(other, BaseSet):
202             return NotImplemented
203         return self.union(other)
204
205     def union(self, other):
206         """Return the union of two sets as a new set.
207
208         (I.e. all elements that are in either set.)
209         """
210         result = self.__class__(self)
211         result._update(other)
212         return result
213
214     def __and__(self, other):
215         """Return the intersection of two sets as a new set.
216
217         (I.e. all elements that are in both sets.)
218         """
219         if not isinstance(other, BaseSet):
220             return NotImplemented
221         return self.intersection(other)
222
223     def intersection(self, other):
224         """Return the intersection of two sets as a new set.
225
226         (I.e. all elements that are in both sets.)
227         """
228         if not isinstance(other, BaseSet):
229             other = Set(other)
230         if len(self) <= len(other):
231             little, big = self, other
232         else:
233             little, big = other, self
234         common = ifilter(big._data.has_key, little)
235         return self.__class__(common)
236
237     def __xor__(self, other):
238         """Return the symmetric difference of two sets as a new set.
239
240         (I.e. all elements that are in exactly one of the sets.)
241         """
242         if not isinstance(other, BaseSet):
243             return NotImplemented
244         return self.symmetric_difference(other)
245
246     def symmetric_difference(self, other):
247         """Return the symmetric difference of two sets as a new set.
248
249         (I.e. all elements that are in exactly one of the sets.)
250         """
251         result = self.__class__()
252         data = result._data
253         value = True
254         selfdata = self._data
255         try:
256             otherdata = other._data
257         except AttributeError:
258             otherdata = Set(other)._data
259         for elt in ifilterfalse(otherdata.has_key, selfdata):
260             data[elt] = value
261         for elt in ifilterfalse(selfdata.has_key, otherdata):
262             data[elt] = value
263         return result
264
265     def  __sub__(self, other):
266         """Return the difference of two sets as a new Set.
267
268         (I.e. all elements that are in this set and not in the other.)
269         """
270         if not isinstance(other, BaseSet):
271             return NotImplemented
272         return self.difference(other)
273
274     def difference(self, other):
275         """Return the difference of two sets as a new Set.
276
277         (I.e. all elements that are in this set and not in the other.)
278         """
279         result = self.__class__()
280         data = result._data
281         try:
282             otherdata = other._data
283         except AttributeError:
284             otherdata = Set(other)._data
285         value = True
286         for elt in ifilterfalse(otherdata.has_key, self):
287             data[elt] = value
288         return result
289
290     # Membership test
291
292     def __contains__(self, element):
293         """Report whether an element is a member of a set.
294
295         (Called in response to the expression `element in self'.)
296         """
297         try:
298             return element in self._data
299         except TypeError:
300             transform = getattr(element, "__as_temporarily_immutable__", None)
301             if transform is None:
302                 raise # re-raise the TypeError exception we caught
303             return transform() in self._data
304
305     # Subset and superset test
306
307     def issubset(self, other):
308         """Report whether another set contains this set."""
309         self._binary_sanity_check(other)
310         if len(self) > len(other):  # Fast check for obvious cases
311             return False
312         for elt in ifilterfalse(other._data.has_key, self):
313             return False
314         return True
315
316     def issuperset(self, other):
317         """Report whether this set contains another set."""
318         self._binary_sanity_check(other)
319         if len(self) < len(other):  # Fast check for obvious cases
320             return False
321         for elt in ifilterfalse(self._data.has_key, other):
322             return False
323         return True
324
325     # Inequality comparisons using the is-subset relation.
326     __le__ = issubset
327     __ge__ = issuperset
328
329     def __lt__(self, other):
330         self._binary_sanity_check(other)
331         return len(self) < len(other) and self.issubset(other)
332
333     def __gt__(self, other):
334         self._binary_sanity_check(other)
335         return len(self) > len(other) and self.issuperset(other)
336
337     # Assorted helpers
338
339     def _binary_sanity_check(self, other):
340         # Check that the other argument to a binary operation is also
341         # a set, raising a TypeError otherwise.
342         if not isinstance(other, BaseSet):
343             raise TypeError, "Binary operation only permitted between sets"
344
345     def _compute_hash(self):
346         # Calculate hash code for a set by xor'ing the hash codes of
347         # the elements.  This ensures that the hash code does not depend
348         # on the order in which elements are added to the set.  This is
349         # not called __hash__ because a BaseSet should not be hashable;
350         # only an ImmutableSet is hashable.
351         result = 0
352         for elt in self:
353             result ^= hash(elt)
354         return result
355
356     def _update(self, iterable):
357         # The main loop for update() and the subclass __init__() methods.
358         data = self._data
359
360         # Use the fast update() method when a dictionary is available.
361         if isinstance(iterable, BaseSet):
362             data.update(iterable._data)
363             return
364
365         value = True
366
367         if type(iterable) in (list, tuple, xrange):
368             # Optimized: we know that __iter__() and next() can't
369             # raise TypeError, so we can move 'try:' out of the loop.
370             it = iter(iterable)
371             while True:
372                 try:
373                     for element in it:
374                         data[element] = value
375                     return
376                 except TypeError:
377                     transform = getattr(element, "__as_immutable__", None)
378                     if transform is None:
379                         raise # re-raise the TypeError exception we caught
380                     data[transform()] = value
381         else:
382             # Safe: only catch TypeError where intended
383             for element in iterable:
384                 try:
385                     data[element] = value
386                 except TypeError:
387                     transform = getattr(element, "__as_immutable__", None)
388                     if transform is None:
389                         raise # re-raise the TypeError exception we caught
390                     data[transform()] = value
391
392
393 class ImmutableSet(BaseSet):
394     """Immutable set class."""
395
396     __slots__ = ['_hashcode']
397
398     # BaseSet + hashing
399
400     def __init__(self, iterable=None):
401         """Construct an immutable set from an optional iterable."""
402         self._hashcode = None
403         self._data = {}
404         if iterable is not None:
405             self._update(iterable)
406
407     def __hash__(self):
408         if self._hashcode is None:
409             self._hashcode = self._compute_hash()
410         return self._hashcode
411
412     def __getstate__(self):
413         return self._data, self._hashcode
414
415     def __setstate__(self, state):
416         self._data, self._hashcode = state
417
418 class Set(BaseSet):
419     """ Mutable set class."""
420
421     __slots__ = []
422
423     # BaseSet + operations requiring mutability; no hashing
424
425     def __init__(self, iterable=None):
426         """Construct a set from an optional iterable."""
427         self._data = {}
428         if iterable is not None:
429             self._update(iterable)
430
431     def __getstate__(self):
432         # getstate's results are ignored if it is not
433         return self._data,
434
435     def __setstate__(self, data):
436         self._data, = data
437
438     def __hash__(self):
439         """A Set cannot be hashed."""
440         # We inherit object.__hash__, so we must deny this explicitly
441         raise TypeError, "Can't hash a Set, only an ImmutableSet."
442
443     # In-place union, intersection, differences.
444     # Subtle:  The xyz_update() functions deliberately return None,
445     # as do all mutating operations on built-in container types.
446     # The __xyz__ spellings have to return self, though.
447
448     def __ior__(self, other):
449         """Update a set with the union of itself and another."""
450         self._binary_sanity_check(other)
451         self._data.update(other._data)
452         return self
453
454     def union_update(self, other):
455         """Update a set with the union of itself and another."""
456         self._update(other)
457
458     def __iand__(self, other):
459         """Update a set with the intersection of itself and another."""
460         self._binary_sanity_check(other)
461         self._data = (self & other)._data
462         return self
463
464     def intersection_update(self, other):
465         """Update a set with the intersection of itself and another."""
466         if isinstance(other, BaseSet):
467             self &= other
468         else:
469             self._data = (self.intersection(other))._data
470
471     def __ixor__(self, other):
472         """Update a set with the symmetric difference of itself and another."""
473         self._binary_sanity_check(other)
474         self.symmetric_difference_update(other)
475         return self
476
477     def symmetric_difference_update(self, other):
478         """Update a set with the symmetric difference of itself and another."""
479         data = self._data
480         value = True
481         if not isinstance(other, BaseSet):
482             other = Set(other)
483         if self is other:
484             self.clear()
485         for elt in other:
486             if elt in data:
487                 del data[elt]
488             else:
489                 data[elt] = value
490
491     def __isub__(self, other):
492         """Remove all elements of another set from this set."""
493         self._binary_sanity_check(other)
494         self.difference_update(other)
495         return self
496
497     def difference_update(self, other):
498         """Remove all elements of another set from this set."""
499         data = self._data
500         if not isinstance(other, BaseSet):
501             other = Set(other)
502         if self is other:
503             self.clear()
504         for elt in ifilter(data.has_key, other):
505             del data[elt]
506
507     # Python dict-like mass mutations: update, clear
508
509     def update(self, iterable):
510         """Add all values from an iterable (such as a list or file)."""
511         self._update(iterable)
512
513     def clear(self):
514         """Remove all elements from this set."""
515         self._data.clear()
516
517     # Single-element mutations: add, remove, discard
518
519     def add(self, element):
520         """Add an element to a set.
521
522         This has no effect if the element is already present.
523         """
524         try:
525             self._data[element] = True
526         except TypeError:
527             transform = getattr(element, "__as_immutable__", None)
528             if transform is None:
529                 raise # re-raise the TypeError exception we caught
530             self._data[transform()] = True
531
532     def remove(self, element):
533         """Remove an element from a set; it must be a member.
534
535         If the element is not a member, raise a KeyError.
536         """
537         try:
538             del self._data[element]
539         except TypeError:
540             transform = getattr(element, "__as_temporarily_immutable__", None)
541             if transform is None:
542                 raise # re-raise the TypeError exception we caught
543             del self._data[transform()]
544
545     def discard(self, element):
546         """Remove an element from a set if it is a member.
547
548         If the element is not a member, do nothing.
549         """
550         try:
551             self.remove(element)
552         except KeyError:
553             pass
554
555     def pop(self):
556         """Remove and return an arbitrary set element."""
557         return self._data.popitem()[0]
558
559     def __as_immutable__(self):
560         # Return a copy of self as an immutable set
561         return ImmutableSet(self)
562
563     def __as_temporarily_immutable__(self):
564         # Return self wrapped in a temporarily immutable set
565         return _TemporarilyImmutableSet(self)
566
567
568 class _TemporarilyImmutableSet(BaseSet):
569     # Wrap a mutable set as if it was temporarily immutable.
570     # This only supplies hashing and equality comparisons.
571
572     def __init__(self, set):
573         self._set = set
574         self._data = set._data  # Needed by ImmutableSet.__eq__()
575
576     def __hash__(self):
577         return self._set._compute_hash()
578
579 # Local Variables:
580 # tab-width:4
581 # indent-tabs-mode:nil
582 # End:
583 # vim: set expandtab tabstop=4 shiftwidth=4: