Hello community, here is the log from the commit of package python-immutables for openSUSE:Factory checked in at 2020-10-29 09:47:45 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-immutables (Old) and /work/SRC/openSUSE:Factory/.python-immutables.new.3463 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-immutables" Thu Oct 29 09:47:45 2020 rev:4 rq:841417 version:0.14 Changes: -------- --- /work/SRC/openSUSE:Factory/python-immutables/python-immutables.changes 2020-03-10 09:51:12.984019255 +0100 +++ /work/SRC/openSUSE:Factory/.python-immutables.new.3463/python-immutables.changes 2020-10-29 09:47:47.452139414 +0100 @@ -1,0 +2,16 @@ +Tue Oct 13 07:42:13 UTC 2020 - Dirk Mueller <dmuel...@suse.com> + +- skip tests that fail on 32bit + +------------------------------------------------------------------- +Wed Sep 16 11:27:37 UTC 2020 - Dirk Mueller <dmuel...@suse.com> + +- update to 0.14: + * python 3.8 support + * Various improvements w.r.t. type annotations & typing + * Fix pure-Python implementation to accept keyword argument + * Fix the mutation API to maintain elements count correctly + * Allow None to be used as key in pure-Python implementation. +- remove py38.patch (upstream) + +------------------------------------------------------------------- Old: ---- immutables-0.11.tar.gz py38.patch New: ---- immutables-0.14.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-immutables.spec ++++++ --- /var/tmp/diff_new_pack.TUCfQU/_old 2020-10-29 09:47:47.984139917 +0100 +++ /var/tmp/diff_new_pack.TUCfQU/_new 2020-10-29 09:47:47.988139921 +0100 @@ -19,13 +19,12 @@ %{?!python_module:%define python_module() python-%{**} python3-%{**}} %define skip_python2 1 Name: python-immutables -Version: 0.11 +Version: 0.14 Release: 0 Summary: Immutable collections for Python License: Apache-2.0 URL: https://github.com/MagicStack/immutables Source: https://files.pythonhosted.org/packages/source/i/immutables/immutables-%{version}.tar.gz -Patch0: py38.patch BuildRequires: %{python_module devel} BuildRequires: %{python_module setuptools} BuildRequires: fdupes @@ -37,7 +36,6 @@ %prep %setup -q -n immutables-%{version} -%patch0 -p1 sed -i 's/\.system//' setup.py %build @@ -51,6 +49,11 @@ } %check +# Fails on 32bit for some reason +%ifarch %ix86 +rm -v tests/test_none_keys.py +%endif + %python_exec setup.py test %files %{python_files} ++++++ immutables-0.11.tar.gz -> immutables-0.14.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/immutables-0.11/PKG-INFO new/immutables-0.14/PKG-INFO --- old/immutables-0.11/PKG-INFO 2019-10-15 15:59:44.000000000 +0200 +++ new/immutables-0.14/PKG-INFO 2020-05-18 06:37:31.000000000 +0200 @@ -1,6 +1,6 @@ -Metadata-Version: 1.1 +Metadata-Version: 1.2 Name: immutables -Version: 0.11 +Version: 0.14 Summary: Immutable Collections Home-page: https://github.com/MagicStack/immutables Author: MagicStack Inc @@ -9,11 +9,8 @@ Description: immutables ========== - .. image:: https://travis-ci.org/MagicStack/immutables.svg?branch=master - :target: https://travis-ci.org/MagicStack/immutables - - .. image:: https://ci.appveyor.com/api/projects/status/tgbc6tq56u63qqhf?svg=true - :target: https://ci.appveyor.com/project/MagicStack/immutables + .. image:: https://github.com/MagicStack/immutables/workflows/Tests/badge.svg?branch=master + :target: https://github.com/MagicStack/immutables/actions?query=workflow%3ATests+branch%3Amaster+event%3Apush .. image:: https://img.shields.io/pypi/v/immutables.svg :target: https://pypi.python.org/pypi/immutables @@ -139,7 +136,9 @@ Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 Classifier: Operating System :: POSIX Classifier: Operating System :: MacOS :: MacOS X Classifier: Operating System :: Microsoft :: Windows Provides: immutables +Requires-Python: >=3.5 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/immutables-0.11/README.rst new/immutables-0.14/README.rst --- old/immutables-0.11/README.rst 2019-10-15 15:59:09.000000000 +0200 +++ new/immutables-0.14/README.rst 2020-05-18 06:37:20.000000000 +0200 @@ -1,11 +1,8 @@ immutables ========== -.. image:: https://travis-ci.org/MagicStack/immutables.svg?branch=master - :target: https://travis-ci.org/MagicStack/immutables - -.. image:: https://ci.appveyor.com/api/projects/status/tgbc6tq56u63qqhf?svg=true - :target: https://ci.appveyor.com/project/MagicStack/immutables +.. image:: https://github.com/MagicStack/immutables/workflows/Tests/badge.svg?branch=master + :target: https://github.com/MagicStack/immutables/actions?query=workflow%3ATests+branch%3Amaster+event%3Apush .. image:: https://img.shields.io/pypi/v/immutables.svg :target: https://pypi.python.org/pypi/immutables diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/immutables-0.11/immutables/__init__.py new/immutables-0.14/immutables/__init__.py --- old/immutables-0.11/immutables/__init__.py 2019-10-15 15:59:09.000000000 +0200 +++ new/immutables-0.14/immutables/__init__.py 2020-05-18 06:37:20.000000000 +0200 @@ -1,3 +1,5 @@ +# flake8: noqa + try: from ._map import Map except ImportError: @@ -6,6 +8,6 @@ import collections.abc as _abc _abc.Mapping.register(Map) +from ._version import __version__ __all__ = 'Map', -__version__ = '0.11' diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/immutables-0.11/immutables/_map.c new/immutables-0.14/immutables/_map.c --- old/immutables-0.11/immutables/_map.c 2019-10-15 15:59:09.000000000 +0200 +++ new/immutables-0.14/immutables/_map.c 2020-05-18 06:37:20.000000000 +0200 @@ -393,12 +393,13 @@ map_update(uint64_t mutid, MapObject *o, PyObject *src); -#ifdef NDEBUG +#if !defined(NDEBUG) static void _map_node_array_validate(void *o) { assert(IS_ARRAY_NODE(o)); MapNode_Array *node = (MapNode_Array*)(o); + assert(node->a_count <= HAMT_ARRAY_NODE_SIZE); Py_ssize_t i = 0, count = 0; for (; i < HAMT_ARRAY_NODE_SIZE; i++) { if (node->a_array[i] != NULL) { @@ -1109,7 +1110,7 @@ } } -#ifdef NDEBUG +#if !defined(NDEBUG) /* Ensure that Collision.without implementation converts to Bitmap nodes itself. */ @@ -1744,6 +1745,7 @@ Py_ssize_t i; VALIDATE_ARRAY_NODE(node) + assert(node->a_count <= HAMT_ARRAY_NODE_SIZE); /* Create a new Array node. */ clone = (MapNode_Array *)map_node_array_new(node->a_count, mutid); @@ -1806,6 +1808,7 @@ if (mutid != 0 && self->a_mutid == mutid) { new_node = self; + self->a_count++; Py_INCREF(self); } else { @@ -1940,9 +1943,9 @@ if (target == NULL) { return W_ERROR; } - target->a_count = new_count; } + target->a_count = new_count; Py_CLEAR(target->a_array[idx]); *new_node = (MapNode*)target; /* borrow */ @@ -2006,7 +2009,7 @@ } else { -#ifdef NDEBUG +#if !defined(NDEBUG) if (IS_COLLISION_NODE(node)) { assert( (map_node_collision_count( @@ -2101,7 +2104,9 @@ goto error; } - if (_map_dump_format(writer, "ArrayNode(id=%p):\n", node)) { + if (_map_dump_format(writer, "ArrayNode(id=%p count=%zd):\n", + node, node->a_count) + ) { goto error; } @@ -2298,7 +2303,7 @@ Py_ssize_t pos = iter->i_pos[level]; if (pos + 1 >= Py_SIZE(node)) { -#ifdef NDEBUG +#if !defined(NDEBUG) assert(iter->i_level >= 0); iter->i_nodes[iter->i_level] = NULL; #endif @@ -2335,7 +2340,7 @@ Py_ssize_t pos = iter->i_pos[level]; if (pos + 1 >= Py_SIZE(node)) { -#ifdef NDEBUG +#if !defined(NDEBUG) assert(iter->i_level >= 0); iter->i_nodes[iter->i_level] = NULL; #endif @@ -2359,7 +2364,7 @@ Py_ssize_t pos = iter->i_pos[level]; if (pos >= HAMT_ARRAY_NODE_SIZE) { -#ifdef NDEBUG +#if !defined(NDEBUG) assert(iter->i_level >= 0); iter->i_nodes[iter->i_level] = NULL; #endif @@ -2381,7 +2386,7 @@ } } -#ifdef NDEBUG +#if !defined(NDEBUG) assert(iter->i_level >= 0); iter->i_nodes[iter->i_level] = NULL; #endif diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/immutables-0.11/immutables/_map.pyi new/immutables-0.14/immutables/_map.pyi --- old/immutables-0.11/immutables/_map.pyi 2019-10-15 15:59:09.000000000 +0200 +++ new/immutables-0.14/immutables/_map.pyi 2020-05-18 06:37:20.000000000 +0200 @@ -3,10 +3,10 @@ from typing import Hashable from typing import Iterable from typing import Iterator -from typing import Literal from typing import Mapping from typing import MutableMapping from typing import NoReturn +from typing import overload from typing import Tuple from typing import Type from typing import TypeVar @@ -40,14 +40,20 @@ class Map(Mapping[K, V]): + @overload + def __init__(self, **kw: V) -> None: ... + @overload def __init__( - self, col: Union[Mapping[K, V], Iterable[Tuple[K, V]]] = ..., **kw: V - ): ... - def __reduce__(self) -> NoReturn: ... + self, col: Union[Mapping[K, V], Iterable[Tuple[K, V]]], **kw: V + ) -> None: ... + def __reduce__(self) -> Tuple[Type[Map], Tuple[dict]]: ... def __len__(self) -> int: ... def __eq__(self, other: Any) -> bool: ... + @overload + def update(self, **kw: V) -> Map[str, V]: ... + @overload def update( - self, col: Union[Mapping[K, V], Iterable[Tuple[K, V]]] = ..., **kw: V + self, col: Union[Mapping[K, V], Iterable[Tuple[K, V]]], **kw: V ) -> Map[K, V]: ... def mutate(self) -> MapMutation[K, V]: ... def set(self, key: K, val: V) -> Map[K, V]: ... @@ -71,7 +77,7 @@ def __init__(self, count: int, root: BitmapNode) -> None: ... def set(self, key: K, val: V) -> None: ... def __enter__(self: S) -> S: ... - def __exit__(self, *exc: Any) -> Literal[False]: ... + def __exit__(self, *exc: Any): ... def __iter__(self) -> NoReturn: ... def __delitem__(self, key: K) -> None: ... def __setitem__(self, key: K, val: V) -> None: ... @@ -79,9 +85,12 @@ def get(self, key: K, default: D = ...) -> Union[V, D]: ... def __getitem__(self, key: K) -> V: ... def __contains__(self, key: Any) -> bool: ... + @overload + def update(self, **kw: V) -> None: ... + @overload def update( - self, col: Union[Mapping[K, V], Iterable[Tuple[K, V]]] = ..., **kw: V - ): ... + self, col: Union[Mapping[K, V], Iterable[Tuple[K, V]]], **kw: V + ) -> None: ... def finish(self) -> Map[K, V]: ... def __len__(self) -> int: ... def __eq__(self, other: Any) -> bool: ... diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/immutables-0.11/immutables/_testutils.py new/immutables-0.14/immutables/_testutils.py --- old/immutables-0.11/immutables/_testutils.py 1970-01-01 01:00:00.000000000 +0100 +++ new/immutables-0.14/immutables/_testutils.py 2020-05-18 06:37:20.000000000 +0200 @@ -0,0 +1,80 @@ +class HashKey: + _crasher = None + + def __init__(self, hash, name, *, error_on_eq_to=None): + assert hash != -1 + self.name = name + self.hash = hash + self.error_on_eq_to = error_on_eq_to + + def __repr__(self): + if self._crasher is not None and self._crasher.error_on_repr: + raise ReprError + return '<Key name:{} hash:{}>'.format(self.name, self.hash) + + def __hash__(self): + if self._crasher is not None and self._crasher.error_on_hash: + raise HashingError + + return self.hash + + def __eq__(self, other): + if not isinstance(other, HashKey): + return NotImplemented + + if self._crasher is not None and self._crasher.error_on_eq: + raise EqError + + if self.error_on_eq_to is not None and self.error_on_eq_to is other: + raise ValueError('cannot compare {!r} to {!r}'.format(self, other)) + if other.error_on_eq_to is not None and other.error_on_eq_to is self: + raise ValueError('cannot compare {!r} to {!r}'.format(other, self)) + + return (self.name, self.hash) == (other.name, other.hash) + + +class KeyStr(str): + + def __hash__(self): + if HashKey._crasher is not None and HashKey._crasher.error_on_hash: + raise HashingError + return super().__hash__() + + def __eq__(self, other): + if HashKey._crasher is not None and HashKey._crasher.error_on_eq: + raise EqError + return super().__eq__(other) + + def __repr__(self, other): + if HashKey._crasher is not None and HashKey._crasher.error_on_repr: + raise ReprError + return super().__eq__(other) + + +class HashKeyCrasher: + + def __init__(self, *, error_on_hash=False, error_on_eq=False, + error_on_repr=False): + self.error_on_hash = error_on_hash + self.error_on_eq = error_on_eq + self.error_on_repr = error_on_repr + + def __enter__(self): + if HashKey._crasher is not None: + raise RuntimeError('cannot nest crashers') + HashKey._crasher = self + + def __exit__(self, *exc): + HashKey._crasher = None + + +class HashingError(Exception): + pass + + +class EqError(Exception): + pass + + +class ReprError(Exception): + pass diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/immutables-0.11/immutables/_version.py new/immutables-0.14/immutables/_version.py --- old/immutables-0.11/immutables/_version.py 1970-01-01 01:00:00.000000000 +0100 +++ new/immutables-0.14/immutables/_version.py 2020-05-18 06:37:20.000000000 +0200 @@ -0,0 +1,13 @@ +# This file MUST NOT contain anything but the __version__ assignment. +# +# When making a release, change the value of __version__ +# to an appropriate value, and open a pull request against +# the correct branch (master if making a new feature release). +# The commit message MUST contain a properly formatted release +# log, and the commit must be signed. +# +# The release automation will: build and test the packages for the +# supported platforms, publish the packages on PyPI, merge the PR +# to the target branch, create a Git tag pointing to the commit. + +__version__ = '0.14' diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/immutables-0.11/immutables/map.py new/immutables-0.14/immutables/map.py --- old/immutables-0.11/immutables/map.py 2019-10-15 15:59:09.000000000 +0200 +++ new/immutables-0.14/immutables/map.py 2020-05-18 06:37:20.000000000 +0200 @@ -46,6 +46,13 @@ W_EMPTY, W_NEWNODE, W_NOT_FOUND = range(3) void = object() +class _Unhashable: + __slots__ = () + __hash__ = None + +_NULL = _Unhashable() +del _Unhashable + class BitmapNode: @@ -70,7 +77,7 @@ key_or_null = self.array[key_idx] val_or_node = self.array[val_idx] - if key_or_null is None: + if key_or_null is _NULL: sub_node, added = val_or_node.assoc( shift + 5, hash, key, val, mutid) if val_or_node is sub_node: @@ -111,12 +118,12 @@ mutid) if mutid and mutid == self.mutid: - self.array[key_idx] = None + self.array[key_idx] = _NULL self.array[val_idx] = sub_node return self, True else: ret = self.clone(mutid) - ret.array[key_idx] = None + ret.array[key_idx] = _NULL ret.array[val_idx] = sub_node return ret, True @@ -153,7 +160,7 @@ key_or_null = self.array[key_idx] val_or_node = self.array[val_idx] - if key_or_null is None: + if key_or_null is _NULL: return val_or_node.find(shift + 5, hash, key) if key == key_or_null: @@ -173,7 +180,7 @@ key_or_null = self.array[key_idx] val_or_node = self.array[val_idx] - if key_or_null is None: + if key_or_null is _NULL: res, sub_node = val_or_node.without(shift + 5, hash, key, mutid) if res is W_EMPTY: @@ -182,7 +189,7 @@ elif res is W_NEWNODE: if (type(sub_node) is BitmapNode and sub_node.size == 2 and - sub_node.array[0] is not None): + sub_node.array[0] is not _NULL): if mutid and mutid == self.mutid: self.array[key_idx] = sub_node.array[0] @@ -231,7 +238,7 @@ for i in range(0, self.size, 2): key_or_null = self.array[i] - if key_or_null is None: + if key_or_null is _NULL: val_or_node = self.array[i + 1] yield from val_or_node.keys() else: @@ -242,7 +249,7 @@ key_or_null = self.array[i] val_or_node = self.array[i + 1] - if key_or_null is None: + if key_or_null is _NULL: yield from val_or_node.values() else: yield val_or_node @@ -252,7 +259,7 @@ key_or_null = self.array[i] val_or_node = self.array[i + 1] - if key_or_null is None: + if key_or_null is _NULL: yield from val_or_node.items() else: yield key_or_null, val_or_node @@ -269,8 +276,8 @@ pad = ' ' * (level + 2) - if key_or_null is None: - buf.append(pad + 'None:') + if key_or_null is _NULL: + buf.append(pad + 'NULL:') val_or_node.dump(buf, level + 2) else: buf.append(pad + '{!r}: {!r}'.format(key_or_null, val_or_node)) @@ -328,7 +335,7 @@ else: new_node = BitmapNode( - 2, map_bitpos(self.hash, shift), [None, self], mutid) + 2, map_bitpos(self.hash, shift), [_NULL, self], mutid) return new_node.assoc(shift, hash, key, val, mutid) def without(self, shift, hash, key, mutid): @@ -433,7 +440,17 @@ class Map: - def __init__(self, col=None, **kw): + def __init__(self, *args, **kw): + if not args: + col = None + elif len(args) == 1: + col = args[0] + else: + raise TypeError( + "immutables.Map expected at most 1 arguments, " + "got {}".format(len(args)) + ) + self.__count = 0 self.__root = BitmapNode(0, 0, [], 0) self.__hash = -1 @@ -483,8 +500,18 @@ return True - def update(self, col=None, **kw): + def update(self, *args, **kw): + if not args: + col = None + elif len(args) == 1: + col = args[0] + else: + raise TypeError( + "update expected at most 1 arguments, got {}".format(len(args)) + ) + it = None + if col is not None: if hasattr(col, 'items'): it = iter(col.items()) @@ -721,7 +748,16 @@ else: return True - def update(self, col=None, **kw): + def update(self, *args, **kw): + if not args: + col = None + elif len(args) == 1: + col = args[0] + else: + raise TypeError( + "update expected at most 1 arguments, got {}".format(len(args)) + ) + if self.__mutid == 0: raise ValueError('mutation {!r} has been finished'.format(self)) @@ -740,8 +776,7 @@ it = iter(kw.items()) if it is None: - - return self + return root = self.__root count = self.__count diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/immutables-0.11/immutables.egg-info/PKG-INFO new/immutables-0.14/immutables.egg-info/PKG-INFO --- old/immutables-0.11/immutables.egg-info/PKG-INFO 2019-10-15 15:59:44.000000000 +0200 +++ new/immutables-0.14/immutables.egg-info/PKG-INFO 2020-05-18 06:37:30.000000000 +0200 @@ -1,6 +1,6 @@ -Metadata-Version: 1.1 +Metadata-Version: 1.2 Name: immutables -Version: 0.11 +Version: 0.14 Summary: Immutable Collections Home-page: https://github.com/MagicStack/immutables Author: MagicStack Inc @@ -9,11 +9,8 @@ Description: immutables ========== - .. image:: https://travis-ci.org/MagicStack/immutables.svg?branch=master - :target: https://travis-ci.org/MagicStack/immutables - - .. image:: https://ci.appveyor.com/api/projects/status/tgbc6tq56u63qqhf?svg=true - :target: https://ci.appveyor.com/project/MagicStack/immutables + .. image:: https://github.com/MagicStack/immutables/workflows/Tests/badge.svg?branch=master + :target: https://github.com/MagicStack/immutables/actions?query=workflow%3ATests+branch%3Amaster+event%3Apush .. image:: https://img.shields.io/pypi/v/immutables.svg :target: https://pypi.python.org/pypi/immutables @@ -139,7 +136,9 @@ Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 Classifier: Operating System :: POSIX Classifier: Operating System :: MacOS :: MacOS X Classifier: Operating System :: Microsoft :: Windows Provides: immutables +Requires-Python: >=3.5 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/immutables-0.11/immutables.egg-info/SOURCES.txt new/immutables-0.14/immutables.egg-info/SOURCES.txt --- old/immutables-0.11/immutables.egg-info/SOURCES.txt 2019-10-15 15:59:44.000000000 +0200 +++ new/immutables-0.14/immutables.egg-info/SOURCES.txt 2020-05-18 06:37:31.000000000 +0200 @@ -6,6 +6,8 @@ immutables/_map.c immutables/_map.h immutables/_map.pyi +immutables/_testutils.py +immutables/_version.py immutables/map.py immutables/py.typed immutables.egg-info/PKG-INFO @@ -13,4 +15,6 @@ immutables.egg-info/dependency_links.txt immutables.egg-info/top_level.txt tests/__init__.py -tests/test_map.py \ No newline at end of file +tests/test_issue24.py +tests/test_map.py +tests/test_none_keys.py \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/immutables-0.11/setup.py new/immutables-0.14/setup.py --- old/immutables-0.11/setup.py 2019-10-15 15:59:09.000000000 +0200 +++ new/immutables-0.14/setup.py 2020-05-18 06:37:20.000000000 +0200 @@ -1,4 +1,4 @@ -import os.path +import os import platform import setuptools @@ -10,7 +10,7 @@ with open(os.path.join( - os.path.dirname(__file__), 'immutables', '__init__.py')) as f: + os.path.dirname(__file__), 'immutables', '_version.py')) as f: for line in f: if line.startswith('__version__ ='): _, _, version = line.partition('=') @@ -18,15 +18,24 @@ break else: raise RuntimeError( - 'unable to read the version from immutables/__init__.py') + 'unable to read the version from immutables/_version.py') if platform.python_implementation() == 'CPython': + if os.environ.get("DEBUG_IMMUTABLES") == '1': + define_macros = [] + undef_macros = ['NDEBUG'] + else: + define_macros = [('NDEBUG', '1')] + undef_macros = [] + ext_modules = [ setuptools.Extension( "immutables._map", ["immutables/_map.c"], - extra_compile_args=CFLAGS) + extra_compile_args=CFLAGS, + define_macros=define_macros, + undef_macros=undef_macros) ] else: ext_modules = [] @@ -41,6 +50,7 @@ version=VERSION, description='Immutable Collections', long_description=readme, + python_requires='>=3.5', classifiers=[ 'License :: OSI Approved :: Apache Software License', 'Intended Audience :: Developers', @@ -48,6 +58,7 @@ 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', 'Operating System :: POSIX', 'Operating System :: MacOS :: MacOS X', 'Operating System :: Microsoft :: Windows', diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/immutables-0.11/tests/test_issue24.py new/immutables-0.14/tests/test_issue24.py --- old/immutables-0.11/tests/test_issue24.py 1970-01-01 01:00:00.000000000 +0100 +++ new/immutables-0.14/tests/test_issue24.py 2020-05-18 06:37:20.000000000 +0200 @@ -0,0 +1,155 @@ +import unittest + +from immutables.map import Map as PyMap, map_bitcount + + +class CollisionKey: + def __hash__(self): + return 0 + + +class Issue24Base: + Map = None + + def test_issue24(self): + keys = range(27) + new_entries = dict.fromkeys(keys, True) + m = self.Map(new_entries) + self.assertTrue(17 in m) + with m.mutate() as mm: + for i in keys: + del mm[i] + self.assertEqual(len(mm), 0) + + def dump_check_node_kind(self, header, kind): + header = header.strip() + self.assertTrue(header.strip().startswith(kind)) + + def dump_check_node_size(self, header, size): + node_size = header.split('size=', 1)[1] + node_size = int(node_size.split(maxsplit=1)[0]) + self.assertEqual(node_size, size) + + def dump_check_bitmap_count(self, header, count): + header = header.split('bitmap=')[1] + bitmap = int(header.split(maxsplit=1)[0], 0) + self.assertEqual(map_bitcount(bitmap), count) + + def dump_check_bitmap_node_count(self, header, count): + self.dump_check_node_kind(header, 'Bitmap') + self.dump_check_node_size(header, count * 2) + self.dump_check_bitmap_count(header, count) + + def dump_check_collision_node_count(self, header, count): + self.dump_check_node_kind(header, 'Collision') + self.dump_check_node_size(header, 2 * count) + + def test_bitmap_node_update_in_place_count(self): + keys = range(7) + new_entries = dict.fromkeys(keys, True) + m = self.Map(new_entries) + d = m.__dump__().splitlines() + self.assertTrue(d) + if d[0].startswith('HAMT'): + header = d[1] # skip _map.Map.__dump__() header + else: + header = d[0] + self.dump_check_bitmap_node_count(header, 7) + + def test_bitmap_node_delete_in_place_count(self): + keys = range(7) + new_entries = dict.fromkeys(keys, True) + m = self.Map(new_entries) + with m.mutate() as mm: + del mm[0], mm[2], mm[3] + m2 = mm.finish() + d = m2.__dump__().splitlines() + self.assertTrue(d) + if d[0].startswith('HAMT'): + header = d[1] # skip _map.Map.__dump__() header + else: + header = d[0] + self.dump_check_bitmap_node_count(header, 4) + + def test_collision_node_update_in_place_count(self): + keys = (CollisionKey() for i in range(7)) + new_entries = dict.fromkeys(keys, True) + m = self.Map(new_entries) + d = m.__dump__().splitlines() + self.assertTrue(len(d) > 3) + # get node headers + if d[0].startswith('HAMT'): + h1, h2 = d[1], d[3] # skip _map.Map.__dump__() header + else: + h1, h2 = d[0], d[2] + self.dump_check_node_kind(h1, 'Bitmap') + self.dump_check_collision_node_count(h2, 7) + + def test_collision_node_delete_in_place_count(self): + keys = [CollisionKey() for i in range(7)] + new_entries = dict.fromkeys(keys, True) + m = self.Map(new_entries) + with m.mutate() as mm: + del mm[keys[0]], mm[keys[2]], mm[keys[3]] + m2 = mm.finish() + d = m2.__dump__().splitlines() + self.assertTrue(len(d) > 3) + # get node headers + if d[0].startswith('HAMT'): + h1, h2 = d[1], d[3] # skip _map.Map.__dump__() header + else: + h1, h2 = d[0], d[2] + self.dump_check_node_kind(h1, 'Bitmap') + self.dump_check_collision_node_count(h2, 4) + +try: + from immutables._map import Map as CMap +except ImportError: + CMap = None + + +class Issue24PyTest(Issue24Base, unittest.TestCase): + Map = PyMap + + +@unittest.skipIf(CMap is None, 'C Map is not available') +class Issue24CTest(Issue24Base, unittest.TestCase): + Map = CMap + + def hamt_dump_check_first_return_second(self, m): + d = m.__dump__().splitlines() + self.assertTrue(len(d) > 2) + self.assertTrue(d[0].startswith('HAMT')) + return d[1] + + def test_array_node_update_in_place_count(self): + keys = range(27) + new_entries = dict.fromkeys(keys, True) + m = self.Map(new_entries) + header = self.hamt_dump_check_first_return_second(m) + self.dump_check_node_kind(header, 'Array') + for i in range(2, 18): + m = m.delete(i) + header = self.hamt_dump_check_first_return_second(m) + self.dump_check_bitmap_node_count(header, 11) + + def test_array_node_delete_in_place_count(self): + keys = range(27) + new_entries = dict.fromkeys(keys, True) + m = self.Map(new_entries) + header = self.hamt_dump_check_first_return_second(m) + self.dump_check_node_kind(header, 'Array') + with m.mutate() as mm: + for i in range(5): + del mm[i] + m2 = mm.finish() + header = self.hamt_dump_check_first_return_second(m2) + self.dump_check_node_kind(header, 'Array') + for i in range(6, 17): + m2 = m2.delete(i) + header = self.hamt_dump_check_first_return_second(m2) + self.dump_check_bitmap_node_count(header, 11) + + +if __name__ == '__main__': + unittest.main() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/immutables-0.11/tests/test_map.py new/immutables-0.14/tests/test_map.py --- old/immutables-0.11/tests/test_map.py 2019-10-15 15:59:09.000000000 +0200 +++ new/immutables-0.14/tests/test_map.py 2020-05-18 06:37:20.000000000 +0200 @@ -7,88 +7,7 @@ import weakref from immutables.map import Map as PyMap - - -class HashKey: - _crasher = None - - def __init__(self, hash, name, *, error_on_eq_to=None): - assert hash != -1 - self.name = name - self.hash = hash - self.error_on_eq_to = error_on_eq_to - - def __repr__(self): - if self._crasher is not None and self._crasher.error_on_repr: - raise ReprError - return '<Key name:{} hash:{}>'.format(self.name, self.hash) - - def __hash__(self): - if self._crasher is not None and self._crasher.error_on_hash: - raise HashingError - - return self.hash - - def __eq__(self, other): - if not isinstance(other, HashKey): - return NotImplemented - - if self._crasher is not None and self._crasher.error_on_eq: - raise EqError - - if self.error_on_eq_to is not None and self.error_on_eq_to is other: - raise ValueError('cannot compare {!r} to {!r}'.format(self, other)) - if other.error_on_eq_to is not None and other.error_on_eq_to is self: - raise ValueError('cannot compare {!r} to {!r}'.format(other, self)) - - return (self.name, self.hash) == (other.name, other.hash) - - -class KeyStr(str): - - def __hash__(self): - if HashKey._crasher is not None and HashKey._crasher.error_on_hash: - raise HashingError - return super().__hash__() - - def __eq__(self, other): - if HashKey._crasher is not None and HashKey._crasher.error_on_eq: - raise EqError - return super().__eq__(other) - - def __repr__(self, other): - if HashKey._crasher is not None and HashKey._crasher.error_on_repr: - raise ReprError - return super().__eq__(other) - - -class HashKeyCrasher: - - def __init__(self, *, error_on_hash=False, error_on_eq=False, - error_on_repr=False): - self.error_on_hash = error_on_hash - self.error_on_eq = error_on_eq - self.error_on_repr = error_on_repr - - def __enter__(self): - if HashKey._crasher is not None: - raise RuntimeError('cannot nest crashers') - HashKey._crasher = self - - def __exit__(self, *exc): - HashKey._crasher = None - - -class HashingError(Exception): - pass - - -class EqError(Exception): - pass - - -class ReprError(Exception): - pass +from immutables._testutils import * # NoQA class BaseMapTest: @@ -240,7 +159,9 @@ # <Key name:E hash:362244>: 'e' # <Key name:B hash:101>: 'b' - def test_map_stress(self): + + + def test_map_stress_01(self): COLLECTION_SIZE = 7000 TEST_ITERS_EVERY = 647 CRASH_HASH_EVERY = 97 @@ -330,6 +251,78 @@ self.assertEqual(len(h), 0) self.assertEqual(list(h.items()), []) + def test_map_stress_02(self): + COLLECTION_SIZE = 20000 + TEST_ITERS_EVERY = 647 + CRASH_HASH_EVERY = 97 + DELETE_EVERY = 3 + CRASH_EQ_EVERY = 11 + + h = self.Map() + d = dict() + + for i in range(COLLECTION_SIZE // 2): + key = KeyStr(i) + + if not (i % CRASH_HASH_EVERY): + with HashKeyCrasher(error_on_hash=True): + with self.assertRaises(HashingError): + h.set(key, i) + + h = h.set(key, i) + + if not (i % CRASH_EQ_EVERY): + with HashKeyCrasher(error_on_eq=True): + with self.assertRaises(EqError): + h.get(KeyStr(i)) # really trigger __eq__ + + d[key] = i + self.assertEqual(len(d), len(h)) + + if not (i % TEST_ITERS_EVERY): + self.assertEqual(set(h.items()), set(d.items())) + self.assertEqual(len(h.items()), len(d.items())) + + with h.mutate() as m: + for i in range(COLLECTION_SIZE // 2, COLLECTION_SIZE): + key = KeyStr(i) + + if not (i % CRASH_HASH_EVERY): + with HashKeyCrasher(error_on_hash=True): + with self.assertRaises(HashingError): + m[key] = i + + m[key] = i + + if not (i % CRASH_EQ_EVERY): + with HashKeyCrasher(error_on_eq=True): + with self.assertRaises(EqError): + m[KeyStr(i)] + + d[key] = i + self.assertEqual(len(d), len(m)) + + if not (i % DELETE_EVERY): + del m[key] + del d[key] + + self.assertEqual(len(d), len(m)) + + h = m.finish() + + self.assertEqual(len(h), len(d)) + self.assertEqual(set(h.items()), set(d.items())) + + with h.mutate() as m: + for key in list(d): + del d[key] + del m[key] + self.assertEqual(len(m), len(d)) + h = m.finish() + + self.assertEqual(len(h), len(d)) + self.assertEqual(set(h.items()), set(d.items())) + def test_map_delete_1(self): A = HashKey(100, 'A') B = HashKey(101, 'B') @@ -1235,6 +1228,67 @@ m2 = m.update({'a': 20}) self.assertEqual(len(m2), 2) + def test_map_mut_20(self): + # Issue 24: + + h = self.Map() + + for i in range(19): + # Create more than 16 keys to trigger the root bitmap + # node to be converted into an array node + h = h.set(HashKey(i, i), i) + + + h = h.set(HashKey(18, '18-collision'), 18) + + with h.mutate() as m: + del m[HashKey(18, 18)] + del m[HashKey(18, '18-collision')] + + # The pre-issue-24 code failed to update the number of array + # node element, so at this point it would be greater than it + # actually is. + h = m.finish() + + # Any of the below operations shouldn't crash the debug build. + with h.mutate() as m: + for i in range(18): + del m[HashKey(i, i)] + h = m.finish() + h = h.set(HashKey(21, 21), 21) + h = h.set(HashKey(22, 22), 22) + + def test_map_mut_21(self): + # Issue 24: + # Array nodes, while in mutation, failed to increment the + # internal count of elements when adding a new key to it. + # Because the internal count + + h = self.Map() + + for i in range(18): + # Create more than 16 keys to trigger the root bitmap + # node to be converted into an array node + h = h.set(HashKey(i, i), i) + + with h.mutate() as m: + # Add one new key to the array node + m[HashKey(18, 18)] = 18 + # Add another key -- after this the old code failed + # to increment the number of elements in the mutated + # array node. + m[HashKey(19, 19)] = 19 + h = m.finish() + + for i in range(20): + # Start deleting keys one by one. Because array node + # element count was accounted incorrectly (smaller by 1 + # than it actually is, the mutation for "del h[18]" would + # create an empty array node, clipping the "19" key). + # Before the issue #24 fix, the below line would crash + # on i=19. + h = h.delete(HashKey(i, i)) + def test_map_mut_stress(self): COLLECTION_SIZE = 7000 TEST_ITERS_EVERY = 647 @@ -1294,13 +1348,18 @@ self.assertTrue(isinstance(uh, self.Map)) self.assertEqual(h, uh) - with self.assertRaisesRegex(TypeError, "can't pickle"): + with self.assertRaisesRegex(TypeError, "can('t|not) pickle"): pickle.dumps(h.mutate()) @unittest.skipIf(sys.version_info < (3, 7, 0), "__class_getitem__ is not available") def test_map_is_subscriptable(self): self.assertIs(self.Map[int, str], self.Map) + def test_kwarg_named_col(self): + self.assertEqual(dict(self.Map(col=0)), {"col": 0}) + self.assertEqual(dict(self.Map(a=0, col=1)), {"a": 0, "col": 1}) + self.assertEqual(dict(self.Map({"a": 0}, col=1)), {"a": 0, "col": 1}) + class PyMapTest(BaseMapTest, unittest.TestCase): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/immutables-0.11/tests/test_none_keys.py new/immutables-0.14/tests/test_none_keys.py --- old/immutables-0.11/tests/test_none_keys.py 1970-01-01 01:00:00.000000000 +0100 +++ new/immutables-0.14/tests/test_none_keys.py 2020-05-18 06:37:20.000000000 +0200 @@ -0,0 +1,511 @@ +import unittest + +from immutables.map import map_hash, map_mask, Map as PyMap +from immutables._testutils import * # NoQA + + +none_hash = map_hash(None) +assert(none_hash != 1) +assert((none_hash >> 32) == 0) + +not_collision = 0xffffffff & (~none_hash) + +mask = 0x7ffffffff +none_collisions = [none_hash & (mask >> shift) + for shift in reversed(range(0, 32, 5))] +assert(len(none_collisions) == 7) +none_collisions = [h | (not_collision & (mask << shift)) + for shift, h in zip(range(5, 37, 5), none_collisions)] + + +class NoneCollision(HashKey): + def __init__(self, name, level): + if name is None: + raise ValueError("Can't have a NoneCollision with a None value") + super().__init__(none_collisions[level], name) + + def __eq__(self, other): + if other is None: + return False + return super().__eq__(other) + + __hash__ = HashKey.__hash__ + + +class BaseNoneTest: + Map = None + + def test_none_collisions(self): + collisions = [NoneCollision('a', level) for level in range(7)] + indices = [map_mask(none_hash, shift) for shift in range(0, 32, 5)] + + for i, c in enumerate(collisions[:-1], 1): + self.assertNotEqual(c, None) + c_hash = map_hash(c) + self.assertNotEqual(c_hash, none_hash) + for j, idx in enumerate(indices[:i]): + self.assertEqual(map_mask(c_hash, j*5), idx) + for j, idx in enumerate(indices[i:], i): + self.assertNotEqual(map_mask(c_hash, j*5), idx) + + c = collisions[-1] + self.assertNotEqual(c, None) + c_hash = map_hash(c) + self.assertEqual(c_hash, none_hash) + for i, idx in enumerate(indices): + self.assertEqual(map_mask(c_hash, i*5), idx) + + def test_none_as_key(self): + m = self.Map({None: 1}) + + self.assertEqual(len(m), 1) + self.assertTrue(None in m) + self.assertEqual(m[None], 1) + self.assertTrue(repr(m).startswith('<immutables.Map({None: 1}) at 0x')) + + for level in range(7): + key = NoneCollision('a', level) + self.assertFalse(key in m) + with self.assertRaises(KeyError): + m.delete(key) + + m = m.delete(None) + self.assertEqual(len(m), 0) + self.assertFalse(None in m) + self.assertTrue(repr(m).startswith('<immutables.Map({}) at 0x')) + + self.assertEqual(m, self.Map()) + + with self.assertRaises(KeyError): + m.delete(None) + + def test_none_set(self): + m = self.Map().set(None, 2) + + self.assertEqual(len(m), 1) + self.assertTrue(None in m) + self.assertEqual(m[None], 2) + + m = m.set(None, 1) + + self.assertEqual(len(m), 1) + self.assertTrue(None in m) + self.assertEqual(m[None], 1) + + m = m.delete(None) + + self.assertEqual(len(m), 0) + self.assertEqual(m, self.Map()) + self.assertFalse(None in m) + + with self.assertRaises(KeyError): + m.delete(None) + + def test_none_collision_1(self): + for level in range(7): + key = NoneCollision('a', level) + m = self.Map({None: 1, key: 2}) + + self.assertEqual(len(m), 2) + self.assertTrue(None in m) + self.assertEqual(m[None], 1) + self.assertTrue(key in m) + self.assertEqual(m[key], 2) + + m2 = m.delete(None) + self.assertEqual(len(m2), 1) + self.assertTrue(key in m2) + self.assertEqual(m2[key], 2) + self.assertFalse(None in m2) + with self.assertRaises(KeyError): + m2.delete(None) + + m3 = m2.delete(key) + self.assertEqual(len(m3), 0) + self.assertFalse(None in m3) + self.assertFalse(key in m3) + self.assertEqual(m3, self.Map()) + self.assertTrue(repr(m3).startswith('<immutables.Map({}) at 0x')) + with self.assertRaises(KeyError): + m3.delete(None) + with self.assertRaises(KeyError): + m3.delete(key) + + m2 = m.delete(key) + self.assertEqual(len(m2), 1) + self.assertTrue(None in m2) + self.assertEqual(m2[None], 1) + self.assertFalse(key in m2) + with self.assertRaises(KeyError): + m2.delete(key) + + m4 = m2.delete(None) + self.assertEqual(len(m4), 0) + self.assertFalse(None in m4) + self.assertFalse(key in m4) + self.assertEqual(m4, self.Map()) + self.assertTrue(repr(m4).startswith('<immutables.Map({}) at 0x')) + with self.assertRaises(KeyError): + m4.delete(None) + with self.assertRaises(KeyError): + m4.delete(key) + + self.assertEqual(m3, m4) + + def test_none_collision_2(self): + key = HashKey(not_collision, 'a') + m = self.Map().set(None, 1).set(key, 2) + + self.assertEqual(len(m), 2) + self.assertTrue(key in m) + self.assertTrue(None in m) + self.assertEqual(m[key], 2) + self.assertEqual + + m = m.set(None, 0) + self.assertEqual(len(m), 2) + self.assertTrue(key in m) + self.assertTrue(None in m) + + for level in range(7): + key2 = NoneCollision('b', level) + self.assertFalse(key2 in m) + m2 = m.set(key2, 1) + + self.assertEqual(len(m2), 3) + self.assertTrue(key in m2) + self.assertTrue(None in m2) + self.assertTrue(key2 in m2) + self.assertEqual(m2[key], 2) + self.assertEqual(m2[None], 0) + self.assertEqual(m2[key2], 1) + + m2 = m2.set(None, 1) + self.assertEqual(len(m2), 3) + self.assertTrue(key in m2) + self.assertTrue(None in m2) + self.assertTrue(key2 in m2) + self.assertEqual(m2[key], 2) + self.assertEqual(m2[None], 1) + self.assertEqual(m2[key2], 1) + + m2 = m2.set(None, 2) + self.assertEqual(len(m2), 3) + self.assertTrue(key in m2) + self.assertTrue(None in m2) + self.assertTrue(key2 in m2) + self.assertEqual(m2[key], 2) + self.assertEqual(m2[None], 2) + self.assertEqual(m2[key2], 1) + + m3 = m2.delete(key) + self.assertEqual(len(m3), 2) + self.assertTrue(None in m3) + self.assertTrue(key2 in m3) + self.assertFalse(key in m3) + self.assertEqual(m3[None], 2) + self.assertEqual(m3[key2], 1) + with self.assertRaises(KeyError): + m3.delete(key) + + m3 = m2.delete(key2) + self.assertEqual(len(m3), 2) + self.assertTrue(None in m3) + self.assertTrue(key in m3) + self.assertFalse(key2 in m3) + self.assertEqual(m3[None], 2) + self.assertEqual(m3[key], 2) + with self.assertRaises(KeyError): + m3.delete(key2) + + m3 = m2.delete(None) + self.assertEqual(len(m3), 2) + self.assertTrue(key in m3) + self.assertTrue(key2 in m3) + self.assertFalse(None in m3) + self.assertEqual(m3[key], 2) + self.assertEqual(m3[key2], 1) + with self.assertRaises(KeyError): + m3.delete(None) + + m2 = m.delete(None) + self.assertEqual(len(m2), 1) + self.assertFalse(None in m2) + self.assertTrue(key in m2) + self.assertEqual(m2[key], 2) + with self.assertRaises(KeyError): + m2.delete(None) + + m2 = m.delete(key) + self.assertEqual(len(m2), 1) + self.assertFalse(key in m2) + self.assertTrue(None in m2) + self.assertEqual(m2[None], 0) + with self.assertRaises(KeyError): + m2.delete(key) + + def test_none_collision_3(self): + for level in range(7): + key = NoneCollision('a', level) + m = self.Map({key: 2}) + + self.assertEqual(len(m), 1) + self.assertFalse(None in m) + self.assertTrue(key in m) + self.assertEqual(m[key], 2) + with self.assertRaises(KeyError): + m.delete(None) + + m = m.set(None, 1) + self.assertEqual(len(m), 2) + self.assertTrue(key in m) + self.assertEqual(m[key], 2) + self.assertTrue(None in m) + self.assertEqual(m[None], 1) + + m = m.set(None, 0) + self.assertEqual(len(m), 2) + self.assertTrue(key in m) + self.assertEqual(m[key], 2) + self.assertTrue(None in m) + self.assertEqual(m[None], 0) + + m2 = m.delete(key) + self.assertEqual(len(m2), 1) + self.assertTrue(None in m2) + self.assertEqual(m2[None], 0) + self.assertFalse(key in m2) + with self.assertRaises(KeyError): + m2.delete(key) + + m2 = m.delete(None) + self.assertEqual(len(m2), 1) + self.assertTrue(key in m2) + self.assertEqual(m2[key], 2) + self.assertFalse(None in m2) + with self.assertRaises(KeyError): + m2.delete(None) + + def test_collision_4(self): + key2 = NoneCollision('a', 2) + key4 = NoneCollision('b', 4) + m = self.Map({key2: 2, key4: 4}) + + self.assertEqual(len(m), 2) + self.assertTrue(key2 in m) + self.assertTrue(key4 in m) + self.assertEqual(m[key2], 2) + self.assertEqual(m[key4], 4) + self.assertFalse(None in m) + + m2 = m.set(None, 9) + + self.assertEqual(len(m2), 3) + self.assertTrue(key2 in m2) + self.assertTrue(key4 in m2) + self.assertTrue(None in m2) + self.assertEqual(m2[key2], 2) + self.assertEqual(m2[key4], 4) + self.assertEqual(m2[None], 9) + + m3 = m2.set(None, 0) + self.assertEqual(len(m3), 3) + self.assertTrue(key2 in m3) + self.assertTrue(key4 in m3) + self.assertTrue(None in m3) + self.assertEqual(m3[key2], 2) + self.assertEqual(m3[key4], 4) + self.assertEqual(m3[None], 0) + + m3 = m2.set(key2, 0) + self.assertEqual(len(m3), 3) + self.assertTrue(key2 in m3) + self.assertTrue(key4 in m3) + self.assertTrue(None in m3) + self.assertEqual(m3[key2], 0) + self.assertEqual(m3[key4], 4) + self.assertEqual(m3[None], 9) + + m3 = m2.set(key4, 0) + self.assertEqual(len(m3), 3) + self.assertTrue(key2 in m3) + self.assertTrue(key4 in m3) + self.assertTrue(None in m3) + self.assertEqual(m3[key2], 2) + self.assertEqual(m3[key4], 0) + self.assertEqual(m3[None], 9) + + m3 = m2.delete(None) + self.assertEqual(m3, m) + self.assertEqual(len(m3), 2) + self.assertTrue(key2 in m3) + self.assertTrue(key4 in m3) + self.assertEqual(m3[key2], 2) + self.assertEqual(m3[key4], 4) + self.assertFalse(None in m3) + with self.assertRaises(KeyError): + m3.delete(None) + + m3 = m2.delete(key2) + self.assertEqual(len(m3), 2) + self.assertTrue(None in m3) + self.assertTrue(key4 in m3) + self.assertEqual(m3[None], 9) + self.assertEqual(m3[key4], 4) + self.assertFalse(key2 in m3) + with self.assertRaises(KeyError): + m3.delete(key2) + + m3 = m2.delete(key4) + self.assertEqual(len(m3), 2) + self.assertTrue(None in m3) + self.assertTrue(key2 in m3) + self.assertEqual(m3[None], 9) + self.assertEqual(m3[key2], 2) + self.assertFalse(key4 in m3) + with self.assertRaises(KeyError): + m3.delete(key4) + + def test_none_mutation(self): + key2 = NoneCollision('a', 2) + key4 = NoneCollision('b', 4) + key = NoneCollision('c', -1) + m = self.Map({key: -1, key2: 2, key4: 4, None: 9}) + + with m.mutate() as mm: + self.assertEqual(len(mm), 4) + self.assertTrue(key in mm) + self.assertTrue(key2 in mm) + self.assertTrue(key4 in mm) + self.assertTrue(None in mm) + self.assertEqual(mm[key2], 2) + self.assertEqual(mm[key4], 4) + self.assertEqual(mm[key], -1) + self.assertEqual(mm[None], 9) + + for k in m: + mm[k] = -mm[k] + + self.assertEqual(len(mm), 4) + self.assertTrue(key in mm) + self.assertTrue(key2 in mm) + self.assertTrue(key4 in mm) + self.assertTrue(None in mm) + self.assertEqual(mm[key2], -2) + self.assertEqual(mm[key4], -4) + self.assertEqual(mm[key], 1) + self.assertEqual(mm[None], -9) + + for k in m: + del mm[k] + self.assertEqual(len(mm), 3) + self.assertFalse(k in mm) + for n in m: + if n != k: + self.assertTrue(n in mm) + self.assertEqual(mm[n], -m[n]) + with self.assertRaises(KeyError): + del mm[k] + mm[k] = -m[k] + self.assertEqual(len(mm), 4) + self.assertTrue(k in mm) + self.assertEqual(mm[k], -m[k]) + + for k in m: + mm[k] = -mm[k] + + self.assertEqual(len(mm), 4) + self.assertTrue(key in mm) + self.assertTrue(key2 in mm) + self.assertTrue(key4 in mm) + self.assertTrue(None in mm) + self.assertEqual(mm[key2], 2) + self.assertEqual(mm[key4], 4) + self.assertEqual(mm[key], -1) + self.assertEqual(mm[None], 9) + + for k in m: + mm[k] = -mm[k] + + self.assertEqual(len(mm), 4) + self.assertTrue(key in mm) + self.assertTrue(key2 in mm) + self.assertTrue(key4 in mm) + self.assertTrue(None in mm) + self.assertEqual(mm[key2], -2) + self.assertEqual(mm[key4], -4) + self.assertEqual(mm[key], 1) + self.assertEqual(mm[None], -9) + + m2 = mm.finish() + + self.assertEqual(set(m), set(m2)) + self.assertEqual(len(m2), 4) + self.assertTrue(key in m2) + self.assertTrue(key2 in m2) + self.assertTrue(key4 in m2) + self.assertTrue(None in m2) + self.assertEqual(m2[key2], -2) + self.assertEqual(m2[key4], -4) + self.assertEqual(m2[key], 1) + self.assertEqual(m2[None], -9) + + for k, v in m.items(): + self.assertTrue(k in m2) + self.assertEqual(m2[k], -v) + + def test_iterators(self): + key2 = NoneCollision('a', 2) + key4 = NoneCollision('b', 4) + key = NoneCollision('c', -1) + m = self.Map({key: -1, key2: 2, key4: 4, None: 9}) + + self.assertEqual(len(m), 4) + self.assertTrue(key in m) + self.assertTrue(key2 in m) + self.assertTrue(key4 in m) + self.assertTrue(None in m) + self.assertEqual(m[key2], 2) + self.assertEqual(m[key4], 4) + self.assertEqual(m[key], -1) + self.assertEqual(m[None], 9) + + s = set(m) + self.assertEqual(len(s), 4) + self.assertEqual(s, set([None, key, key2, key4])) + + sk = set(m.keys()) + self.assertEqual(s, sk) + + sv = set(m.values()) + self.assertEqual(len(sv), 4) + self.assertEqual(sv, set([-1, 2, 4, 9])) + + si = set(m.items()) + self.assertEqual(len(si), 4) + self.assertEqual(si, + set([(key, -1), (key2, 2), (key4, 4), (None, 9)])) + + d = {key: -1, key2: 2, key4: 4, None: 9} + self.assertEqual(dict(m.items()), d) + + +class PyMapNoneTest(BaseNoneTest, unittest.TestCase): + + Map = PyMap + + +try: + from immutables._map import Map as CMap +except ImportError: + CMap = None + + +@unittest.skipIf(CMap is None, 'C Map is not available') +class CMapNoneTest(BaseNoneTest, unittest.TestCase): + + Map = CMap + + +if __name__ == "__main__": + unittest.main()