diff --git a/Lib/collections/__init__.py b/Lib/collections/__init__.py index ace8db113..8a2b22083 100644 --- a/Lib/collections/__init__.py +++ b/Lib/collections/__init__.py @@ -27,7 +27,6 @@ __all__ = [ ] import _collections_abc -import heapq as _heapq import sys as _sys from itertools import chain as _chain @@ -56,22 +55,6 @@ except ImportError: from ._defaultdict import defaultdict -def __getattr__(name): - # For backwards compatibility, continue to make the collections ABCs - # through Python 3.6 available through the collections module. - # Note, no new collections ABCs were added in Python 3.7 - if name in _collections_abc.__all__: - obj = getattr(_collections_abc, name) - import warnings - warnings.warn("Using or importing the ABCs from 'collections' instead " - "of from 'collections.abc' is deprecated since Python 3.3, " - "and in 3.10 it will stop working", - DeprecationWarning, stacklevel=2) - globals()[name] = obj - return obj - raise AttributeError(f'module {__name__!r} has no attribute {name!r}') - - ################################################################################ ### OrderedDict ################################################################################ @@ -111,17 +94,10 @@ class OrderedDict(dict): # Individual links are kept alive by the hard reference in self.__map. # Those hard references disappear when a key is deleted from an OrderedDict. - def __init__(*args, **kwds): + def __init__(self, other=(), /, **kwds): '''Initialize an ordered dictionary. The signature is the same as regular dictionaries. Keyword argument order is preserved. ''' - if not args: - raise TypeError("descriptor '__init__' of 'OrderedDict' object " - "needs an argument") - self, *args = args - if len(args) > 1: - raise TypeError('expected at most 1 arguments, got %d' % len(args)) - try: self.__root except AttributeError: @@ -129,7 +105,7 @@ class OrderedDict(dict): self.__root = root = _proxy(self.__hardroot) root.prev = root.next = root self.__map = {} - self.__update(*args, **kwds) + self.__update(other, **kwds) def __setitem__(self, key, value, dict_setitem=dict.__setitem__, proxy=_proxy, Link=_Link): @@ -455,8 +431,8 @@ def namedtuple(typename, field_names, *, rename=False, defaults=None, module=Non _make.__func__.__doc__ = (f'Make a new {typename} object from a sequence ' 'or iterable') - def _replace(_self, /, **kwds): - result = _self._make(_map(kwds.pop, field_names, _self)) + def _replace(self, /, **kwds): + result = self._make(_map(kwds.pop, field_names, self)) if kwds: raise ValueError(f'Got unexpected field names: {list(kwds)!r}') return result @@ -500,6 +476,7 @@ def namedtuple(typename, field_names, *, rename=False, defaults=None, module=Non '__repr__': __repr__, '_asdict': _asdict, '__getnewargs__': __getnewargs__, + '__match_args__': field_names, } for index, name in enumerate(field_names): doc = _sys.intern(f'Alias for field number {index}') @@ -589,7 +566,7 @@ class Counter(dict): # http://code.activestate.com/recipes/259174/ # Knuth, TAOCP Vol. II section 4.6.3 - def __init__(*args, **kwds): + def __init__(self, iterable=None, /, **kwds): '''Create a new, empty Counter object. And if given, count elements from an input iterable. Or, initialize the count from another mapping of elements to their counts. @@ -600,20 +577,18 @@ class Counter(dict): >>> c = Counter(a=4, b=2) # a new counter from keyword args ''' - if not args: - raise TypeError("descriptor '__init__' of 'Counter' object " - "needs an argument") - self, *args = args - if len(args) > 1: - raise TypeError('expected at most 1 arguments, got %d' % len(args)) - super(Counter, self).__init__() - self.update(*args, **kwds) + super().__init__() + self.update(iterable, **kwds) def __missing__(self, key): 'The count of elements not in the Counter is zero.' # Needed so that self[missing_item] does not raise KeyError return 0 + def total(self): + 'Sum of the counts' + return sum(self.values()) + def most_common(self, n=None): '''List the n most common elements and their counts from the most common to the least. If n is None, then list all element counts. @@ -625,7 +600,10 @@ class Counter(dict): # Emulate Bag.sortedByCount from Smalltalk if n is None: return sorted(self.items(), key=_itemgetter(1), reverse=True) - return _heapq.nlargest(n, self.items(), key=_itemgetter(1)) + + # Lazy import to speedup Python startup time + import heapq + return heapq.nlargest(n, self.items(), key=_itemgetter(1)) def elements(self): '''Iterator over elements repeating each as many times as its count. @@ -663,7 +641,7 @@ class Counter(dict): raise NotImplementedError( 'Counter.fromkeys() is undefined. Use Counter(iterable) instead.') - def update(*args, **kwds): + def update(self, iterable=None, /, **kwds): '''Like dict.update() but add counts instead of replacing them. Source can be an iterable, a dictionary, or another Counter instance. @@ -683,14 +661,6 @@ class Counter(dict): # contexts. Instead, we implement straight-addition. Both the inputs # and outputs are allowed to contain zero and negative counts. - if not args: - raise TypeError("descriptor 'update' of 'Counter' object " - "needs an argument") - self, *args = args - if len(args) > 1: - raise TypeError('expected at most 1 arguments, got %d' % len(args)) - iterable = args[0] if args else None - if iterable is not None: if isinstance(iterable, _collections_abc.Mapping): if self: @@ -699,13 +669,13 @@ class Counter(dict): self[elem] = count + self_get(elem, 0) else: # fast path when counter is empty - super(Counter, self).update(iterable) + super().update(iterable) else: _count_elements(self, iterable) if kwds: self.update(kwds) - def subtract(*args, **kwds): + def subtract(self, iterable=None, /, **kwds): '''Like dict.update() but subtracts counts instead of replacing them. Counts can be reduced below zero. Both the inputs and outputs are allowed to contain zero and negative counts. @@ -721,14 +691,6 @@ class Counter(dict): -1 ''' - if not args: - raise TypeError("descriptor 'subtract' of 'Counter' object " - "needs an argument") - self, *args = args - if len(args) > 1: - raise TypeError('expected at most 1 arguments, got %d' % len(args)) - iterable = args[0] if args else None - if iterable is not None: self_get = self.get if isinstance(iterable, _collections_abc.Mapping): @@ -752,6 +714,42 @@ class Counter(dict): if elem in self: super().__delitem__(elem) + def __eq__(self, other): + 'True if all counts agree. Missing counts are treated as zero.' + if not isinstance(other, Counter): + return NotImplemented + return all(self[e] == other[e] for c in (self, other) for e in c) + + def __ne__(self, other): + 'True if any counts disagree. Missing counts are treated as zero.' + if not isinstance(other, Counter): + return NotImplemented + return not self == other + + def __le__(self, other): + 'True if all counts in self are a subset of those in other.' + if not isinstance(other, Counter): + return NotImplemented + return all(self[e] <= other[e] for c in (self, other) for e in c) + + def __lt__(self, other): + 'True if all counts in self are a proper subset of those in other.' + if not isinstance(other, Counter): + return NotImplemented + return self <= other and self != other + + def __ge__(self, other): + 'True if all counts in self are a superset of those in other.' + if not isinstance(other, Counter): + return NotImplemented + return all(self[e] >= other[e] for c in (self, other) for e in c) + + def __gt__(self, other): + 'True if all counts in self are a proper superset of those in other.' + if not isinstance(other, Counter): + return NotImplemented + return self >= other and self != other + def __repr__(self): if not self: return f'{self.__class__.__name__}()' @@ -772,12 +770,30 @@ class Counter(dict): # To strip negative and zero counts, add-in an empty counter: # c += Counter() # - # Rich comparison operators for multiset subset and superset tests - # are deliberately omitted due to semantic conflicts with the - # existing inherited dict equality method. Subset and superset - # semantics ignore zero counts and require that p≤q ∧ p≥q → p=q; - # however, that would not be the case for p=Counter(a=1, b=0) - # and q=Counter(a=1) where the dictionaries are not equal. + # Results are ordered according to when an element is first + # encountered in the left operand and then by the order + # encountered in the right operand. + # + # When the multiplicities are all zero or one, multiset operations + # are guaranteed to be equivalent to the corresponding operations + # for regular sets. + # Given counter multisets such as: + # cp = Counter(a=1, b=0, c=1) + # cq = Counter(c=1, d=0, e=1) + # The corresponding regular sets would be: + # sp = {'a', 'c'} + # sq = {'c', 'e'} + # All of the following relations would hold: + # set(cp + cq) == sp | sq + # set(cp - cq) == sp - sq + # set(cp | cq) == sp | sq + # set(cp & cq) == sp & sq + # (cp == cq) == (sp == sq) + # (cp != cq) == (sp != sq) + # (cp <= cq) == (sp <= sq) + # (cp < cq) == (sp < sq) + # (cp >= cq) == (sp >= sq) + # (cp > cq) == (sp > sq) def __add__(self, other): '''Add counts from two counters. @@ -1006,12 +1022,15 @@ class ChainMap(_collections_abc.MutableMapping): __copy__ = copy - def new_child(self, m=None): # like Django's Context.push() + def new_child(self, m=None, **kwargs): # like Django's Context.push() '''New ChainMap with a new map followed by all previous maps. If no map is provided, an empty dict is used. + Keyword arguments update the map or new empty dict. ''' if m is None: - m = {} + m = kwargs + elif kwargs: + m.update(kwargs) return self.__class__(m, *self.maps) @property @@ -1073,34 +1092,16 @@ class ChainMap(_collections_abc.MutableMapping): class UserDict(_collections_abc.MutableMapping): # Start by filling-out the abstract methods - def __init__(*args, **kwargs): - if not args: - raise TypeError("descriptor '__init__' of 'UserDict' object " - "needs an argument") - self, *args = args - if len(args) > 1: - raise TypeError('expected at most 1 arguments, got %d' % len(args)) - if args: - dict = args[0] - elif 'dict' in kwargs: - dict = kwargs.pop('dict') - import warnings - warnings.warn("Passing 'dict' as keyword argument is deprecated", - DeprecationWarning, stacklevel=2) - else: - dict = None + def __init__(self, dict=None, /, **kwargs): self.data = {} if dict is not None: self.update(dict) - if len(kwargs): + if kwargs: self.update(kwargs) def __len__(self): return len(self.data) - def __bool__(self): - return bool(self.data) - def __getitem__(self, key): if key in self.data: return self.data[key] @@ -1153,9 +1154,6 @@ class UserDict(_collections_abc.MutableMapping): inst.__dict__["data"] = self.__dict__["data"].copy() return inst - def __sizeof__(self): - return _sys.getsizeof(self.data) - def copy(self): if self.__class__ is UserDict: return UserDict(self.data.copy()) @@ -1222,9 +1220,6 @@ class UserList(_collections_abc.MutableSequence): def __len__(self): return len(self.data) - def __bool__(self): - return bool(self.data) - def __getitem__(self, i): if isinstance(i, slice): return self.__class__(self.data[i]) @@ -1276,9 +1271,6 @@ class UserList(_collections_abc.MutableSequence): inst.__dict__["data"] = self.__dict__["data"][:] return inst - def __sizeof__(self): - return _sys.getsizeof(self.data) - def append(self, item): self.data.append(item) @@ -1381,9 +1373,6 @@ class UserString(_collections_abc.Sequence): char = char.data return char in self.data - def __bool__(self): - return bool(self.data) - def __len__(self): return len(self.data) @@ -1413,9 +1402,6 @@ class UserString(_collections_abc.Sequence): def __rmod__(self, template): return self.__class__(str(template) % self) - def __sizeof__(self): - return _sys.getsizeof(self.data) - # the following methods are defined in alphabetical order: def capitalize(self): return self.__class__(self.data.capitalize()) @@ -1571,4 +1557,4 @@ class UserString(_collections_abc.Sequence): return self.__class__(self.data.upper()) def zfill(self, width): - return self.__class__(self.data.zfill(width)) \ No newline at end of file + return self.__class__(self.data.zfill(width)) diff --git a/Lib/collections/abc.py b/Lib/collections/abc.py index 891600d16..86ca8b8a8 100644 --- a/Lib/collections/abc.py +++ b/Lib/collections/abc.py @@ -1,2 +1,3 @@ from _collections_abc import * from _collections_abc import __all__ +from _collections_abc import _CallableGenericAlias diff --git a/Lib/test/test_collections.py b/Lib/test/test_collections.py index abfcb1514..c98ee55b1 100644 --- a/Lib/test/test_collections.py +++ b/Lib/test/test_collections.py @@ -7,6 +7,7 @@ import inspect import operator import pickle from random import choice, randrange +from itertools import product, chain, combinations import string import sys from test import support @@ -248,6 +249,10 @@ class TestChainMap(unittest.TestCase): for k, v in dict(a=1, B=20, C=30, z=100).items(): # check get self.assertEqual(d.get(k, 100), v) + c = ChainMap({'a': 1, 'b': 2}) + d = c.new_child(b=20, c=30) + self.assertEqual(d.maps, [{'b': 20, 'c': 30}, {'a': 1, 'b': 2}]) + def test_union_operators(self): cm1 = ChainMap(dict(a=1, b=2), dict(c=3, d=4)) cm2 = ChainMap(dict(a=10, e=5), dict(b=20, d=4)) @@ -663,8 +668,7 @@ class TestNamedTuple(unittest.TestCase): a.w = 5 self.assertEqual(a.__dict__, {'w': 5}) - # TODO: RUSTPYTHON - @unittest.expectedFailure + @support.cpython_only def test_field_descriptor(self): Point = namedtuple('Point', 'x y') p = Point(11, 22) @@ -683,9 +687,14 @@ class TestNamedTuple(unittest.TestCase): self.assertEqual(np.y, 2) def test_new_builtins_issue_43102(self): - self.assertEqual( - namedtuple('C', ()).__new__.__globals__['__builtins__'], - {}) + obj = namedtuple('C', ()) + new_func = obj.__new__ + self.assertEqual(new_func.__globals__['__builtins__'], {}) + self.assertEqual(new_func.__builtins__, {}) + + def test_match_args(self): + Point = namedtuple('Point', 'x y') + self.assertEqual(Point.__match_args__, ('x', 'y')) ################################################################################ @@ -1516,7 +1525,7 @@ class TestCollectionABCs(ABCTestCase): items = [5,43,2,1] s = MySet(items) r = s.pop() - self.assertEquals(len(s), len(items) - 1) + self.assertEqual(len(s), len(items) - 1) self.assertNotIn(r, s) self.assertIn(r, items) @@ -1566,10 +1575,6 @@ class TestCollectionABCs(ABCTestCase): def test_issue26915(self): # Container membership test should check identity first - class CustomEqualObject: - def __eq__(self, other): - return False - class CustomSequence(Sequence): def __init__(self, seq): self._seq = seq @@ -1579,7 +1584,7 @@ class TestCollectionABCs(ABCTestCase): return len(self._seq) nan = float('nan') - obj = CustomEqualObject + obj = support.NEVER_EQ seq = CustomSequence([nan, obj, nan]) containers = [ seq, @@ -1599,7 +1604,7 @@ class TestCollectionABCs(ABCTestCase): self.assertSetEqual(set(s1), set(s2)) def test_Set_from_iterable(self): - """Verify _from_iterable overriden to an instance method works.""" + """Verify _from_iterable overridden to an instance method works.""" class SetUsingInstanceFromIterable(MutableSet): def __init__(self, values, created_by): if not created_by: @@ -1994,6 +1999,12 @@ class TestCollectionABCs(ABCTestCase): self.assertEqual(len(mss), len(mss2)) self.assertEqual(list(mss), list(mss2)) + def test_illegal_patma_flags(self): + with self.assertRaises(TypeError): + class Both(Collection): + __abc_tpflags__ = (Sequence.__flags__ | Mapping.__flags__) + + ################################################################################ ### Counter @@ -2087,6 +2098,10 @@ class TestCounter(unittest.TestCase): self.assertRaises(TypeError, Counter, (), ()) self.assertRaises(TypeError, Counter.__init__) + def test_total(self): + c = Counter(a=10, b=5, c=0) + self.assertEqual(c.total(), 15) + def test_order_preservation(self): # Input order dictates items() order self.assertEqual(list(Counter('abracadabra').items()), @@ -2240,31 +2255,6 @@ class TestCounter(unittest.TestCase): set_result = setop(set(p.elements()), set(q.elements())) self.assertEqual(counter_result, dict.fromkeys(set_result, 1)) - # TODO: RUSTPYTHON - @unittest.expectedFailure - def test_subset_superset_not_implemented(self): - # Verify that multiset comparison operations are not implemented. - - # These operations were intentionally omitted because multiset - # comparison semantics conflict with existing dict equality semantics. - - # For multisets, we would expect that if p<=q and p>=q are both true, - # then p==q. However, dict equality semantics require that p!=q when - # one of sets contains an element with a zero count and the other - # doesn't. - - p = Counter(a=1, b=0) - q = Counter(a=1, c=0) - self.assertNotEqual(p, q) - with self.assertRaises(TypeError): - p < q - with self.assertRaises(TypeError): - p <= q - with self.assertRaises(TypeError): - p > q - with self.assertRaises(TypeError): - p >= q - def test_inplace_operations(self): elements = 'abcd' for i in range(1000): @@ -2339,23 +2329,61 @@ class TestCounter(unittest.TestCase): self.assertTrue(c.called) self.assertEqual(dict(c), {'a': 5, 'b': 2, 'c': 1, 'd': 1, 'r':2 }) + def test_multiset_operations_equivalent_to_set_operations(self): + # When the multiplicities are all zero or one, multiset operations + # are guaranteed to be equivalent to the corresponding operations + # for regular sets. + s = list(product(('a', 'b', 'c'), range(2))) + powerset = chain.from_iterable(combinations(s, r) for r in range(len(s)+1)) + counters = [Counter(dict(groups)) for groups in powerset] + for cp, cq in product(counters, repeat=2): + sp = set(cp.elements()) + sq = set(cq.elements()) + self.assertEqual(set(cp + cq), sp | sq) + self.assertEqual(set(cp - cq), sp - sq) + self.assertEqual(set(cp | cq), sp | sq) + self.assertEqual(set(cp & cq), sp & sq) + self.assertEqual(cp == cq, sp == sq) + self.assertEqual(cp != cq, sp != sq) + self.assertEqual(cp <= cq, sp <= sq) + self.assertEqual(cp >= cq, sp >= sq) + self.assertEqual(cp < cq, sp < sq) + self.assertEqual(cp > cq, sp > sq) + + def test_eq(self): + self.assertEqual(Counter(a=3, b=2, c=0), Counter('ababa')) + self.assertNotEqual(Counter(a=3, b=2), Counter('babab')) + + def test_le(self): + self.assertTrue(Counter(a=3, b=2, c=0) <= Counter('ababa')) + self.assertFalse(Counter(a=3, b=2) <= Counter('babab')) + + def test_lt(self): + self.assertTrue(Counter(a=3, b=1, c=0) < Counter('ababa')) + self.assertFalse(Counter(a=3, b=2, c=0) < Counter('ababa')) + + def test_ge(self): + self.assertTrue(Counter(a=2, b=1, c=0) >= Counter('aab')) + self.assertFalse(Counter(a=3, b=2, c=0) >= Counter('aabd')) + + def test_gt(self): + self.assertTrue(Counter(a=3, b=2, c=0) > Counter('aab')) + self.assertFalse(Counter(a=2, b=1, c=0) > Counter('aab')) + ################################################################################ ### Run tests ################################################################################ def test_main(verbose=None): - # TODO: RUSTPYTHON - # NamedTupleDocs = doctest.DocTestSuite(module=collections) - # test_classes = [TestNamedTuple, NamedTupleDocs, TestOneTrickPonyABCs, - test_classes = [TestNamedTuple, TestOneTrickPonyABCs, + NamedTupleDocs = doctest.DocTestSuite(module=collections) + test_classes = [TestNamedTuple, NamedTupleDocs, TestOneTrickPonyABCs, TestCollectionABCs, TestCounter, TestChainMap, TestUserObjects, ] support.run_unittest(*test_classes) - # TODO: RUSTPYTHON - # support.run_doctest(collections, verbose) + support.run_doctest(collections, verbose) if __name__ == "__main__": - test_main(verbose=True) \ No newline at end of file + test_main(verbose=True) diff --git a/Lib/test/test_defaultdict.py b/Lib/test/test_defaultdict.py index b9f1fb9f2..68fc44978 100644 --- a/Lib/test/test_defaultdict.py +++ b/Lib/test/test_defaultdict.py @@ -72,27 +72,6 @@ class TestDefaultDict(unittest.TestCase): d3[13] self.assertEqual(repr(d3), "defaultdict(%s, {13: 43})" % repr(foo)) - def test_print(self): - d1 = defaultdict() - def foo(): return 42 - d2 = defaultdict(foo, {1: 2}) - # NOTE: We can't use tempfile.[Named]TemporaryFile since this - # code must exercise the tp_print C code, which only gets - # invoked for *real* files. - tfn = tempfile.mktemp() - try: - f = open(tfn, "w+") - try: - print(d1, file=f) - print(d2, file=f) - f.seek(0) - self.assertEqual(f.readline(), repr(d1) + "\n") - self.assertEqual(f.readline(), repr(d2) + "\n") - finally: - f.close() - finally: - os.remove(tfn) - def test_copy(self): d1 = defaultdict() d2 = d1.copy() @@ -160,18 +139,6 @@ class TestDefaultDict(unittest.TestCase): r"sub\(, \{\}\)") - # NOTE: printing a subclass of a builtin type does not call its - # tp_print slot. So this part is essentially the same test as above. - tfn = tempfile.mktemp() - try: - f = open(tfn, "w+") - try: - print(d, file=f) - finally: - f.close() - finally: - os.remove(tfn) - def test_callable_arg(self): self.assertRaises(TypeError, defaultdict, {}) @@ -183,5 +150,43 @@ class TestDefaultDict(unittest.TestCase): o = pickle.loads(s) self.assertEqual(d, o) + def test_union(self): + i = defaultdict(int, {1: 1, 2: 2}) + s = defaultdict(str, {0: "zero", 1: "one"}) + + i_s = i | s + self.assertIs(i_s.default_factory, int) + self.assertDictEqual(i_s, {1: "one", 2: 2, 0: "zero"}) + self.assertEqual(list(i_s), [1, 2, 0]) + + s_i = s | i + self.assertIs(s_i.default_factory, str) + self.assertDictEqual(s_i, {0: "zero", 1: 1, 2: 2}) + self.assertEqual(list(s_i), [0, 1, 2]) + + i_ds = i | dict(s) + self.assertIs(i_ds.default_factory, int) + self.assertDictEqual(i_ds, {1: "one", 2: 2, 0: "zero"}) + self.assertEqual(list(i_ds), [1, 2, 0]) + + ds_i = dict(s) | i + self.assertIs(ds_i.default_factory, int) + self.assertDictEqual(ds_i, {0: "zero", 1: 1, 2: 2}) + self.assertEqual(list(ds_i), [0, 1, 2]) + + with self.assertRaises(TypeError): + i | list(s.items()) + with self.assertRaises(TypeError): + list(s.items()) | i + + # We inherit a fine |= from dict, so just a few sanity checks here: + i |= list(s.items()) + self.assertIs(i.default_factory, int) + self.assertDictEqual(i, {1: "one", 2: 2, 0: "zero"}) + self.assertEqual(list(i), [1, 2, 0]) + + with self.assertRaises(TypeError): + i |= None + if __name__ == "__main__": unittest.main()