Lynn Rees avatar Lynn Rees committed 2e802b7

- add counterstuf
- bump version

Comments (0)

Files changed (13)

-A collection of Python dictionaries supporting attribute-style access. Includes
-*defaultdict*,  *OrderedDict*, restricted, *ChainMap*, and frozen
-implementations plus miscellaneous useful utilities for writing Python software.
+A collection of Python dictionary types that support attribute-style access.
+Includes *defaultdict*,  *OrderedDict*, restricted, *ChainMap*, *Counter*, and
+frozen implementations plus miscellaneous utilities for writing Python software.
         if line.startswith('__version__'):
             return '%s.%s.%s' % eval(line[13:].rstrip())
 
+
 if float('%d.%d' % sys.version_info[:2]) < 2.7:
     reqs = 'reqs/requires-2.6.txt'
 else:
 setup(
     name='stuf',
     version=getversion('stuf/__init__.py'),
-    description='Normal, default, ordered, chained, restricted, and frozen '
-    'dictionaries with attribute-style access.',
+    description='Normal, default, ordered, chained, restricted, counter, and '
+    'frozen dictionaries with attribute-style access.',
     long_description=open(join(getcwd(), 'README.rst'), 'r').read(),
     keywords='dict attribute collection mapping dot notation access bunch',
     license='BSD',
 
 from stuf.iterable import (
     exhaustmap as exhaustitems, exhaustcall as exhaustmap, exhauststar)
-from stuf.core import defaultstuf, fixedstuf, frozenstuf, orderedstuf, stuf
+from stuf.core import (
+    defaultstuf, fixedstuf, frozenstuf, orderedstuf, stuf, chainstuf, countstuf)
 
-__version__ = (0, 9, 9)
-__all__ = 'defaultstuf fixedstuf frozenstuf orderedstuf stuf'.split()
+__version__ = (0, 9, 10)
+__all__ = (
+    'defaultstuf fixedstuf frozenstuf orderedstuf stuf chainstuf countstuf'
+).split()
+# -*- coding: utf-8 -*-
+'''some core stuf.'''
+
+from itertools import chain
+from collections import MutableMapping
+from operator import methodcaller, attrgetter
+
+from .desc import lazy_class, lazy
+from .collects import recursive_repr
+from .deep import clsname, getcls, clsdict
+from .six import getvalues, getitems, getkeys
+from .iterable import exhaustcall, exhaustmap
+
+wraps = attrgetter('_wrapped')
+delitem = attrgetter('_wrapped.__delitem__')
+getitem = attrgetter('_wrapped.__getitem__')
+setitem = attrgetter('_wrapped.__setitem__')
+length = attrgetter('_wrapped.__len__')
+_iter = attrgetter('_wrapped.__iter__')
+asdict = attrgetter('_wrapped._asdict')
+_reserved = 'allowed _wrapped _map'.split()
+
+
+class baseread(object):
+
+    def __getattr__(self, key, _getter=object.__getattribute__):
+        if key == 'iteritems':
+            return getitems(self)
+        elif key == 'iterkeys':
+            return getkeys(self)
+        elif key == 'itervalues':
+            return getvalues(self)
+        try:
+            return self[key]
+        except KeyError:
+            return _getter(self, key)
+
+    @recursive_repr()
+    def __repr__(self):
+        return '{0}({1})'.format(clsname(self), methodcaller('items')(self))
+
+    @lazy_class
+    def _classkeys(self):
+        # protected keywords
+        return frozenset(chain(
+            iter(vars(self)), iter(vars(getcls(self))), _reserved,
+        ))
+
+
+class basewrite(baseread):
+
+    def __setattr__(self, key, value):
+        # handle normal object attributes
+        if key == '_classkeys' or key in self._classkeys:
+            clsdict(self)[key] = value
+        # handle special attributes
+        else:
+            try:
+                self[key] = value
+            except KeyError:
+                raise AttributeError(key)
+
+    def __delattr__(self, key):
+        # allow deletion of key-value pairs only
+        if not key == '_classkeys' or key in self._classkeys:
+            try:
+                del self[key]
+            except KeyError:
+                raise AttributeError(key)
+
+
+class corestuf(baseread):
+
+    _map = dict
+
+    def _build(self, iterable):
+        # add class to handle potential nested objects of the same class
+        try:
+            kw = self._map()
+            # extract appropriate key-values from sequence
+            exhaustcall(kw.update, iterable)
+        except ValueError:
+            kw.update(iterable)
+        return kw
+
+    def _mapping(self, iterable):
+        return self._map(iterable)
+
+    def _new(self, iterable):
+        return getcls(self)(self._build(iterable))
+
+    def _prepop(self, *args, **kw):
+        kw.update(self._build(args))
+        return kw
+
+    def _pop(self, past, future):
+        def closure(key, value, new=self._new):
+            try:
+                if not hasattr(value, 'capitalize'):
+                    # see if stuf can be converted to nested stuf
+                    trial = new(value)
+                    value = trial if trial else value
+            except (TypeError, IOError):
+                pass
+            future[key] = value
+        exhaustmap(closure, past)
+        return self._postpop(future)
+
+    def _postpop(self, future):
+        return future
+
+    def copy(self):
+        return self._new(dict(self))
+
+
+class writestuf(corestuf, basewrite):
+
+    def update(self, *args, **kw):
+        self._pop(self._prepop(*args, **kw), self)
+
+
+class wrapstuf(corestuf):
+
+    def __init__(self, *args, **kw):
+        super(wrapstuf, self).__init__()
+        self._wrapped = self._pop(self._prepop(*args, **kw), self._map())
+
+    def _postpop(self, future):
+        return self._mapping(future)
+
+
+class writewrapstuf(wrapstuf, writestuf, MutableMapping):
+
+    @lazy
+    def __getitem__(self):
+        return getitem(self)
+
+    @lazy
+    def __setitem__(self):
+        return setitem(self)
+
+    @lazy
+    def __delitem__(self):
+        return delitem(self)
+
+    @lazy
+    def __iter__(self):
+        return _iter(self)
+
+    @lazy
+    def __len__(self):
+        return length(self)
+
+    def __reduce__(self):
+        return (getcls(self), (wraps(self).copy(),))
 
 import sys
 
-from stuf.deep import getcls
-from stuf.base import second, first
-from stuf.six import OrderedDict, items
+from .deep import getcls
+from .base import second, first
+from .six import OrderedDict, items
 
 try:
     from reprlib import recursive_repr  # @UnusedImport
 except ImportError:
-    from stuf.six import get_ident, getdoc, getmod, docit
+    from .six import get_ident, getdoc, getmod, docit
 
     def recursive_repr(fillvalue='...'):
         def decorating_function(user_function):
             repr_running = set()
-
             def wrapper(self):  # @IgnorePep8
                 key = id(self), get_ident()
                 if key in repr_running:
     from collections import Counter
 else:
     from heapq import nlargest
-    from stuf.deep import clsname
-    from stuf.base import ismapping
     from itertools import chain, starmap, repeat
 
+    from .deep import clsname
+    from .base import ismapping
+
     class Counter(dict):
 
         '''dict subclass for counting hashable items'''
             if elem in self:
                 super(Counter, self).__delitem__(elem)
 
-        def __repr__(self):
+        def __repr__(self): # pragma: no coverage
             if not self:
                 return '%s()' % clsname(self)
             try:
         def __add__(self, other):
             '''Add counts from two counters.'''
             if not isinstance(other, getcls(self)):
-                return NotImplemented
+                return NotImplemented()
             result = getcls(self)()
             for elem, count in items(self):
                 newcount = count + other[elem]
         def __sub__(self, other):
             '''Subtract count, but keep only results with positive counts.'''
             if not isinstance(other, getcls(self)):
-                return NotImplemented
+                return NotImplemented()
             result = getcls(self)()
             for elem, count in items(self):
                 newcount = count - other[elem]
         def __or__(self, other):
             '''Union is the maximum of value in either of the input counters.'''
             if not isinstance(other, getcls(self)):
-                return NotImplemented
+                return NotImplemented()
             result = getcls(self)()
             for elem, count in items(self):
                 other_count = other[elem]
         def __and__(self, other):
             '''Intersection is the minimum of corresponding counts.'''
             if not isinstance(other, getcls(self)):
-                return NotImplemented
+                return NotImplemented()
             result = getcls(self)()
             for elem, count in items(self):
                 other_count = other[elem]
             '''
             return getcls(self)() - self
 
-        def _keep_positive(self):
-            '''
-            Internal method to strip elements with a negative or zero count
-            '''
-            for elem in (e for e, c in items(self) if not c > 0):
-                del self[elem]
-            return self
-
-        def __iadd__(self, other):
-            '''
-            Inplace add from another counter, keeping only positive counts.
-            '''
-            for elem, count in items(other):
-                self[elem] += count
-            return self._keep_positive()
-
-        def __isub__(self, other):
-            '''
-            Inplace subtract counter, but keep only results with positive
-            counts.
-            '''
-            for elem, count in items(other):
-                self[elem] -= count
-            return self._keep_positive()
-
-        def __ior__(self, other):
-            '''Inplace union is the maximum of value from either counter.'''
-            for elem, other_count in items(other):
-                count = self[elem]
-                if other_count > count:
-                    self[elem] = other_count
-            return self._keep_positive()
-
-        def __iand__(self, other):
-            '''Inplace intersection is the minimum of corresponding counts.'''
-            for elem, count in items(self):
-                other_count = other[elem]
-                if other_count < count:
-                    self[elem] = other_count
-            return self._keep_positive()
-
         def most_common(self, n=None, nl=nlargest, i=items, g=second):
             '''
             List the n most common elements and their counts from the most
 '''core stuf.'''
 
 from itertools import chain
-from operator import methodcaller, attrgetter
-from collections import Mapping, MutableMapping, defaultdict, namedtuple
+from collections import Mapping, defaultdict, namedtuple
 
-from stuf import exhaustitems
-from stuf.iterable import exhaustcall
-from stuf.desc import lazy_class, lazy
-from stuf.deep import clsname, getcls, clsdict
-from stuf.six import getvalues, getitems, getkeys
-from stuf.collects import ChainMap, OrderedDict, recursive_repr
+from ._core import basewrite, writestuf, writewrapstuf, wraps, wrapstuf, asdict
+
+from .deep import getcls
+from .iterable import exhaustcall
+from .desc import lazy_class, lazy
+from .collects import ChainMap, Counter, OrderedDict
 
 __all__ = 'defaultstuf fixedstuf frozenstuf orderedstuf stuf'.split()
 
-wraps = attrgetter('_wrapped')
-delitem = attrgetter('_wrapped.__delitem__')
-getitem = attrgetter('_wrapped.__getitem__')
-setitem = attrgetter('_wrapped.__setitem__')
-length = attrgetter('_wrapped.__len__')
-_iter = attrgetter('_wrapped.__iter__')
-asdict = attrgetter('_wrapped._asdict')
-_reserved = 'allowed _wrapped _map'.split()
 
-
-class corestuf(object):
-
-    _map = dict
-
-    def __getattr__(self, key, _getter=object.__getattribute__):
-        if key == 'iteritems':
-            return getitems(self)
-        elif key == 'iterkeys':
-            return getkeys(self)
-        elif key == 'itervalues':
-            return getvalues(self)
-        try:
-            return self[key]
-        except KeyError:
-            return _getter(self, key)
-
-    @recursive_repr()
-    def __repr__(self):
-        return '{0}({1})'.format(clsname(self), methodcaller('items')(self))
-
-    @lazy_class
-    def _classkeys(self):
-        # protected keywords
-        return frozenset(chain(
-            iter(vars(self)), iter(vars(getcls(self))), _reserved,
-        ))
-
-    def _build(self, iterable):
-        # add class to handle potential nested objects of the same class
-        try:
-            kw = self._map()
-            # extract appropriate key-values from sequence
-            exhaustcall(kw.update, iterable)
-        except ValueError:
-            kw.update(iterable)
-        return kw
-
-    def _mapping(self, iterable):
-        return self._map(iterable)
-
-    def _new(self, iterable):
-        return getcls(self)(self._build(iterable))
-
-    def _prepop(self, *args, **kw):
-        kw.update(self._build(args))
-        return kw
-
-    def _pop(self, past, future):
-        def closure(key, value, new=self._new):
-            try:
-                if not hasattr(value, 'capitalize'):
-                    # see if stuf can be converted to nested stuf
-                    trial = new(value)
-                    value = trial if trial else value
-            except (TypeError, IOError):
-                pass
-            future[key] = value
-        exhaustitems(closure, past)
-        return self._postpop(future)
-
-    def _postpop(self, future):
-        return future
-
-    def copy(self):
-        return self._new(dict(self))
-
-
-class writestuf(corestuf):
-
-    def __setattr__(self, key, value):
-        # handle normal object attributes
-        if key == '_classkeys' or key in self._classkeys:
-            clsdict(self)[key] = value
-        # handle special attributes
-        else:
-            try:
-                self[key] = value
-            except KeyError:
-                raise AttributeError(key)
-
-    def __delattr__(self, key):
-        # allow deletion of key-value pairs only
-        if not key == '_classkeys' or key in self._classkeys:
-            try:
-                del self[key]
-            except KeyError:
-                raise AttributeError(key)
-
-    def update(self, *args, **kw):
-        self._pop(self._prepop(*args, **kw), self)
-
-
-class wrapstuf(corestuf):
-
-    def __init__(self, *args, **kw):
-        super(wrapstuf, self).__init__()
-        self._wrapped = self._pop(self._prepop(*args, **kw), self._map())
-
-    def _postpop(self, future):
-        return self._mapping(future)
-
-
-class writewrapstuf(wrapstuf, writestuf, MutableMapping):
-
-    @lazy
-    def __getitem__(self):
-        return getitem(self)
-
-    @lazy
-    def __setitem__(self):
-        return setitem(self)
-
-    @lazy
-    def __delitem__(self):
-        return delitem(self)
-
-    @lazy
-    def __iter__(self):
-        return _iter(self)
-
-    @lazy
-    def __len__(self):
-        return length(self)
-
-    def __reduce__(self):
-        return (getcls(self), (wraps(self).copy(),))
-
-
-class chainstuf(writestuf, ChainMap):
+class chainstuf(basewrite, ChainMap):
 
     '''stuf chained together.'''
 
     update = ChainMap.update
 
 
+class countstuf(basewrite, Counter):
+
+    '''stuf that counts.'''
+
+
 class defaultstuf(writestuf, defaultdict):
 
     '''
 from threading import local
 from functools import update_wrapper, partial
 
-from stuf.six import items
-from stuf import exhaustitems
-from stuf.deep import selfname, setter, getcls, setpart
+from .six import items
+from .iterable import exhaustmap
+from .deep import selfname, setter, getcls, setpart
 
 
 class lazybase(object):
     def reset(self):
         '''Reset previously accessed :class:`lazybase` attributes.'''
         attrs = set(vars(self))
-        exhaustitems(
+        exhaustmap(
             delattr,
             items(vars(getcls(self))),
             lambda x, y: x in attrs and isinstance(y, lazybase),
 from functools import partial
 from itertools import starmap
 
-from stuf.six import items, map, next
+from .six import items, map, next
 
 
 def _xhaust(mapfunc, call, iterable, exception=StopIteration, n=next):
 from os import sep
 from functools import partial
 
-from stuf.utils import lru
-from stuf.base import first
 from parse import compile as pcompile
-from stuf.six.moves import filterfalse  # @UnresolvedImport
-from stuf.six import isstring, filter, map, rcompile, rescape, rsub
+
+from .utils import lru
+from .base import first
+from .six.moves import filterfalse  # @UnresolvedImport
+from .six import isstring, filter, map, rcompile, rescape, rsub
 
 
 def globpattern(expr):
 from importlib import import_module
 from operator import attrgetter, methodcaller, lt, gt
 
-from stuf.base import isfactory
+from .base import isfactory
 
 intern = backport('__builtin__.intern', 'sys.intern')
 OrderedDict = backport('collections.OrderedDict', 'ordereddict.OrderedDict')
 from itertools import count, repeat
 from functools import update_wrapper, partial
 
-from stuf.base import importer, first, norm
-from stuf.six import (
+from .base import importer, first, norm
+from .six import (
     PY3, items, isstring, func_code, b, next, intern, rcompile, pickle, u)
 
 # first slug pattern
 from marshal import load
 from inspect import getdoc, isclass, isfunction, ismethod
 
-from stuf.deep import clsdict, selfname
-from stuf.six import (
+from .deep import clsdict, selfname
+from .six import (
     method_func, func_code, _func_code, _func_defaults, func_defaults)
 
 

tests/test_stuf.py

         self.assertEquals(
             stuffed.maps,
             [stuf(), stuf([('test1', 'test1')]), stuf(test2='test2'),
-            stuf(test3=stuf(e=1))],
+             stuf(test3=stuf(e=1))],
         )
 
 
+class TestCounter(unittest.TestCase):
+
+    @property
+    def _impone(self):
+        from stuf.core import countstuf
+        return countstuf
+
+    @property
+    def _makeone(self):
+        return self._impone(['test1', 'test2', 'test3'])
+
+    def setUp(self):
+        self.stuf = self._makeone
+
+    def test__getattr__(self):
+        self.assertEqual(self.stuf.test1, 1)
+        self.assertEqual(self.stuf.test2, 1)
+        self.assertEqual(self.stuf.test3, 1)
+
+    def test__getitem__(self):
+        self.assertEqual(self.stuf['test1'], 1)
+        self.assertEqual(self.stuf['test2'], 1)
+        self.assertEqual(self.stuf['test3'], 1)
+
+    def test_get(self):
+        self.assertEqual(self.stuf.get('test1'), 1)
+        self.assertEqual(self.stuf.get('test2'), 1)
+        self.assertIsNone(self.stuf.get('test4'), 1)
+        self.assertEqual(self.stuf.get('test3'), 1)
+
+    def test__setattr__(self):
+        self.stuf.max = 3
+        self.stuf.test1 = 'test1again'
+        self.stuf.test2 = 'test2again'
+        self.stuf.test3 = 5
+        self.assertEqual(self.stuf.max, 3)
+        self.assertEqual(self.stuf.test1, 'test1again')
+        self.assertEqual(self.stuf.test2, 'test2again')
+        self.assertEqual(self.stuf.test3, 5)
+
+    def test__setitem__(self):
+        self.stuf['max'] = 3
+        self.stuf['test1'] = 'test1again'
+        self.stuf['test2'] = 'test2again'
+        self.stuf['test3'] = 5
+        self.assertEqual(self.stuf['max'], 3)
+        self.assertEqual(self.stuf['test1'], 'test1again')
+        self.assertEqual(self.stuf['test2'], 'test2again')
+        self.assertEqual(self.stuf['test3'], 5)
+
+    def test__delattr__(self):
+        del self.stuf.test1
+        del self.stuf.test2
+        del self.stuf.test3
+        self.assertEqual(len(self.stuf), 0)
+
+    def test__delitem__(self):
+        del self.stuf['test1']
+        del self.stuf['test2']
+        del self.stuf['test3']
+        self.assertEqual(len(self.stuf), 0)
+        self.assertNotIn('test1', self.stuf)
+        self.assertNotIn('test2', self.stuf)
+        self.assertNotIn('test3', self.stuf)
+
+    def test__cmp__(self):
+        tstuff = self._makeone
+        self.assertEqual(self.stuf, tstuff)
+
+    def test__len__(self):
+        self.assertEqual(len(self.stuf), 3)
+
+    def test_repr(self):
+        from stuf.six import strings
+        self.assertIsInstance(repr(self._makeone), strings)
+        self.assertIsInstance(repr(self.stuf), strings)
+
+    def test_items(self):
+        slist = list(self.stuf.items())
+        self.assertIn(('test1', 1), slist)
+        self.assertIn(('test2', 1), slist)
+        self.assertIn(('test3', 1), slist)
+
+    def test_iteritems(self):
+        slist = list(self.stuf.iteritems())
+        self.assertIn(('test1', 1), slist)
+        self.assertIn(('test2', 1), slist)
+        self.assertIn(('test3', 1), slist)
+
+    def test_iter(self):
+        slist = list(self.stuf)
+        self.assertIn('test1', slist)
+        self.assertIn('test2', slist)
+        self.assertIn('test3', slist)
+
+    def test_iterkeys(self):
+        slist = list(self.stuf.iterkeys())
+        self.assertIn('test1', slist)
+        self.assertIn('test2', slist)
+        self.assertIn('test3', slist)
+
+    def test_itervalues(self):
+        slist = list(self.stuf.itervalues())
+        self.assertIn(1, slist)
+        self.assertIn(1, slist)
+        self.assertIn(1, slist)
+
+    def test_values(self):
+        slist2 = self.stuf.values()
+        self.assertIn(1, slist2)
+        self.assertIn(1, slist2)
+        self.assertIn(1, slist2)
+
+    def test_keys(self):
+        slist2 = self.stuf.keys()
+        self.assertIn('test1', slist2)
+        self.assertIn('test2', slist2)
+        self.assertIn('test3', slist2)
+
+    def test_pickle(self):
+        import pickle
+        tstuf = self._makeone
+        pkle = pickle.dumps(tstuf)
+        nstuf = pickle.loads(pkle)
+        self.assertIsInstance(nstuf, self._impone)
+        self.assertEqual(tstuf, nstuf)
+
+    def test_clear(self):
+        self.stuf.clear()
+        self.assertEqual(len(self.stuf), 0)
+
+    def test_pop(self):
+        self.assertEqual(self.stuf.pop('test1'), 1)
+        self.assertEqual(self.stuf.pop('test2'), 1)
+        self.assertEqual(self.stuf.pop('test3'), 1)
+
+    def test_copy(self):
+        tstuf = self._makeone
+        nstuf = tstuf.copy()
+        self.assertIsInstance(nstuf, self._impone)
+        self.assertIsInstance(tstuf, self._impone)
+        self.assertEqual(tstuf, nstuf)
+
+    def test_popitem(self):
+        item = self.stuf.popitem()
+        self.assertEqual(len(item) + len(self.stuf), 4, item)
+
+    def test_setdefault(self):
+        self.assertEqual(self.stuf.setdefault('test1', 8), 1)
+        self.assertEqual(self.stuf.setdefault('pow', 8), 8)
+
+    def test_update(self):
+        tstuff = self._makeone
+        tstuff['test1'] = 3
+        tstuff['test2'] = 6
+        tstuff['test3'] = 2
+        self.stuf.update(tstuff)
+        self.assertEqual(self.stuf['test1'], 4)
+        self.assertEqual(self.stuf['test2'], 7)
+        self.assertEqual(self.stuf['test3'], 3)
+
+    def test_basics(self):
+        from stuf.utils import lrange
+        c = self._impone('abcaba')
+        self.assertEqual(c.most_common(), [('a', 3), ('b', 2), ('c', 1)])
+        for i in lrange(5):
+            self.assertEqual(
+                c.most_common(i), [('a', 3), ('b', 2), ('c', 1)][:i]
+            )
+        self.assertEqual(''.join(sorted(c.elements())), 'aaabbc')
+        c.a += 1         # increment an existing value
+        c.b -= 2         # sub existing value to zero
+        del c.c          # remove an entry
+        del c.c          # make sure that del doesn't raise KeyError
+        c.d -= 2         # sub from a missing value
+        c.e = -5         # directly assign a missing value
+        c.f += 4         # add to a missing value
+
+    def test_copy_subclass(self):
+        class MyCounter(self._impone):
+            pass
+        c = MyCounter('slartibartfast')
+        d = c.copy()
+        self.assertEqual(d, c)
+        self.assertEqual(len(d), len(c))
+        self.assertEqual(type(d), type(c))
+
+    def test_invariant_for_the_in_operator(self):
+        c = self._impone(a=10, b=-2, c=0)
+        for elem in c:
+            self.assertTrue(elem in c)
+            self.assertIn(elem, c)
+
+    def test_multiset_operations(self):
+        from stuf.utils import lrange
+        from random import randrange
+        # Verify that adding a zero counter will strip zeros and negatives
+        c = self._impone(a=10, b=-2, c=0) + self._impone()
+        self.assertEqual(dict(c), dict(a=10))
+        elements = 'abcd'
+        for _ in lrange(1000):
+            # test random pairs of multisets
+            p = self._impone(
+                dict((elem, randrange(-2, 4)) for elem in elements))
+            p.update(e=1, f=-1, g=0)
+            q = self._impone(
+                dict((elem, randrange(-2, 4)) for elem in elements))
+            q.update(h=1, i=-1, j=0)
+            for counterop, numberop in [
+                (self._impone.__add__, lambda x, y: max(0, x + y)),
+                (self._impone.__sub__, lambda x, y: max(0, x - y)),
+                (self._impone.__or__, lambda x, y: max(0, x, y)),
+                (self._impone.__and__, lambda x, y: max(0, min(x, y))),
+            ]:
+                result = counterop(p, q)
+                for x in elements:
+                    self.assertEqual(numberop(p[x], q[x]), result[x],
+                                     (counterop, x, p, q))
+                # verify that results exclude non-positive counts
+                self.assertTrue(x > 0 for x in result.values())
+        elements = 'abcdef'
+        for _ in lrange(100):
+            # verify that random multisets with no repeats are exactly like sets
+            p = self._impone(
+                dict((elem, randrange(0, 2)) for elem in elements))
+            q = self._impone(
+                dict((elem, randrange(0, 2)) for elem in elements))
+            for counterop, setop in [
+                (self._impone.__sub__, set.__sub__),
+                (self._impone.__or__, set.__or__),
+                (self._impone.__and__, set.__and__),
+            ]:
+                counter_result = counterop(p, q)
+                set_result = setop(set(p.elements()), set(q.elements()))
+                self.assertEqual(counter_result, dict.fromkeys(set_result, 1))
+
+    def test_unary(self):
+        c = self._impone(a=-5, b=0, c=5, d=10, e=15, g=40)
+        self.assertEqual(dict(+c), dict(c=5, d=10, e=15, g=40))
+        self.assertEqual(dict(-c), dict(a=5))
+
+    def test_repr_nonsortable(self):
+        c = self._impone(a=2, b=None)
+        r = repr(c)
+        self.assertIn("('a', 2)", r)
+        self.assertIn("('b', None)", r)
+
+    def test_subtract(self):
+        c = self._impone(a=-5, b=0, c=5, d=10, e=15, g=40)
+        c.subtract(a=1, b=2, c=-3, d=10, e=20, f=30, h=-50)
+        self.assertEqual(
+            c, self._impone(a=-6, b=-2, c=8, d=0, e=-5, f=-30, g=40, h=50))
+        c = self._impone(a=-5, b=0, c=5, d=10, e=15, g=40)
+        c.subtract(self._impone(a=1, b=2, c=-3, d=10, e=20, f=30, h=-50))
+        self.assertEqual(
+            c, self._impone(a=-6, b=-2, c=8, d=0, e=-5, f=-30, g=40, h=50))
+        c = self._impone('aaabbcd')
+        c.subtract('aaaabbcce')
+        self.assertEqual(c, self._impone(a=-1, b=0, c=-1, d=1, e=-1))
+
+
 if __name__ == '__main__':
     unittest.main()
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.