map_test.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551
  1. from collections import namedtuple
  2. from collections.abc import Mapping, Hashable
  3. from operator import add
  4. import pytest
  5. from pyrsistent import pmap, m
  6. import pickle
  7. def test_instance_of_hashable():
  8. assert isinstance(m(), Hashable)
  9. def test_instance_of_map():
  10. assert isinstance(m(), Mapping)
  11. def test_literalish_works():
  12. assert m() is pmap()
  13. assert m(a=1, b=2) == pmap({'a': 1, 'b': 2})
  14. def test_empty_initialization():
  15. a_map = pmap()
  16. assert len(a_map) == 0
  17. def test_initialization_with_one_element():
  18. the_map = pmap({'a': 2})
  19. assert len(the_map) == 1
  20. assert the_map['a'] == 2
  21. assert the_map.a == 2
  22. assert 'a' in the_map
  23. assert the_map is the_map.discard('b')
  24. empty_map = the_map.remove('a')
  25. assert len(empty_map) == 0
  26. assert 'a' not in empty_map
  27. def test_get_non_existing_raises_key_error():
  28. m1 = m()
  29. with pytest.raises(KeyError) as error:
  30. m1['foo']
  31. assert str(error.value) == "'foo'"
  32. def test_remove_non_existing_element_raises_key_error():
  33. m1 = m(a=1)
  34. with pytest.raises(KeyError) as error:
  35. m1.remove('b')
  36. assert str(error.value) == "'b'"
  37. def test_various_iterations():
  38. assert {'a', 'b'} == set(m(a=1, b=2))
  39. assert ['a', 'b'] == sorted(m(a=1, b=2).keys())
  40. assert {1, 2} == set(m(a=1, b=2).itervalues())
  41. assert [1, 2] == sorted(m(a=1, b=2).values())
  42. assert {('a', 1), ('b', 2)} == set(m(a=1, b=2).iteritems())
  43. assert {('a', 1), ('b', 2)} == set(m(a=1, b=2).items())
  44. pm = pmap({k: k for k in range(100)})
  45. assert len(pm) == len(pm.keys())
  46. assert len(pm) == len(pm.values())
  47. assert len(pm) == len(pm.items())
  48. ks = pm.keys()
  49. assert all(k in pm for k in ks)
  50. assert all(k in ks for k in ks)
  51. us = pm.items()
  52. assert all(pm[k] == v for (k, v) in us)
  53. vs = pm.values()
  54. assert all(v in vs for v in vs)
  55. def test_initialization_with_two_elements():
  56. map1 = pmap({'a': 2, 'b': 3})
  57. assert len(map1) == 2
  58. assert map1['a'] == 2
  59. assert map1['b'] == 3
  60. map2 = map1.remove('a')
  61. assert 'a' not in map2
  62. assert map2['b'] == 3
  63. def test_initialization_with_many_elements():
  64. init_dict = dict([(str(x), x) for x in range(1700)])
  65. the_map = pmap(init_dict)
  66. assert len(the_map) == 1700
  67. assert the_map['16'] == 16
  68. assert the_map['1699'] == 1699
  69. assert the_map.set('256', 256) is the_map
  70. new_map = the_map.remove('1600')
  71. assert len(new_map) == 1699
  72. assert '1600' not in new_map
  73. assert new_map['1601'] == 1601
  74. # Some NOP properties
  75. assert new_map.discard('18888') is new_map
  76. assert '19999' not in new_map
  77. assert new_map['1500'] == 1500
  78. assert new_map.set('1500', new_map['1500']) is new_map
  79. def test_access_non_existing_element():
  80. map1 = pmap()
  81. assert len(map1) == 0
  82. map2 = map1.set('1', 1)
  83. assert '1' not in map1
  84. assert map2['1'] == 1
  85. assert '2' not in map2
  86. def test_overwrite_existing_element():
  87. map1 = pmap({'a': 2})
  88. map2 = map1.set('a', 3)
  89. assert len(map2) == 1
  90. assert map2['a'] == 3
  91. def test_hash():
  92. x = m(a=1, b=2, c=3)
  93. y = m(a=1, b=2, c=3)
  94. assert hash(x) == hash(y)
  95. def test_same_hash_when_content_the_same_but_underlying_vector_size_differs():
  96. x = pmap(dict((x, x) for x in range(1000)))
  97. y = pmap({10: 10, 200: 200, 700: 700})
  98. for z in x:
  99. if z not in y:
  100. x = x.remove(z)
  101. assert x == y
  102. assert hash(x) == hash(y)
  103. class HashabilityControlled(object):
  104. hashable = True
  105. def __hash__(self):
  106. if self.hashable:
  107. return 4 # Proven random
  108. raise ValueError("I am not currently hashable.")
  109. def test_map_does_not_hash_values_on_second_hash_invocation():
  110. hashable = HashabilityControlled()
  111. x = pmap(dict(el=hashable))
  112. hash(x)
  113. hashable.hashable = False
  114. hash(x)
  115. def test_equal():
  116. x = m(a=1, b=2, c=3)
  117. y = m(a=1, b=2, c=3)
  118. assert x == y
  119. assert not (x != y)
  120. assert y == x
  121. assert not (y != x)
  122. def test_equal_to_dict():
  123. x = m(a=1, b=2, c=3)
  124. y = dict(a=1, b=2, c=3)
  125. assert x == y
  126. assert not (x != y)
  127. assert y == x
  128. assert not (y != x)
  129. def test_equal_with_different_bucket_sizes():
  130. x = pmap({'a': 1, 'b': 2}, 50)
  131. y = pmap({'a': 1, 'b': 2}, 10)
  132. assert x == y
  133. assert not (x != y)
  134. assert y == x
  135. assert not (y != x)
  136. def test_equal_with_different_insertion_order():
  137. x = pmap([(i, i) for i in range(50)], 10)
  138. y = pmap([(i, i) for i in range(49, -1, -1)], 10)
  139. assert x == y
  140. assert not (x != y)
  141. assert y == x
  142. assert not (y != x)
  143. def test_not_equal():
  144. x = m(a=1, b=2, c=3)
  145. y = m(a=1, b=2)
  146. assert x != y
  147. assert not (x == y)
  148. assert y != x
  149. assert not (y == x)
  150. def test_not_equal_to_dict():
  151. x = m(a=1, b=2, c=3)
  152. y = dict(a=1, b=2, d=4)
  153. assert x != y
  154. assert not (x == y)
  155. assert y != x
  156. assert not (y == x)
  157. def test_update_with_multiple_arguments():
  158. # If same value is present in multiple sources, the rightmost is used.
  159. x = m(a=1, b=2, c=3)
  160. y = x.update(m(b=4, c=5), {'c': 6})
  161. assert y == m(a=1, b=4, c=6)
  162. def test_update_one_argument():
  163. x = m(a=1)
  164. assert x.update(m(b=2)) == m(a=1, b=2)
  165. def test_update_no_arguments():
  166. x = m(a=1)
  167. assert x.update() is x
  168. def test_addition():
  169. assert m(x=1, y=2) + m(y=3, z=4) == m(x=1, y=3, z=4)
  170. def test_union_operator():
  171. assert m(x=1, y=2) | m(y=3, z=4) == m(x=1, y=3, z=4)
  172. def test_transform_base_case():
  173. # Works as set when called with only one key
  174. x = m(a=1, b=2)
  175. assert x.transform(['a'], 3) == m(a=3, b=2)
  176. def test_transform_nested_maps():
  177. x = m(a=1, b=m(c=3, d=m(e=6, f=7)))
  178. assert x.transform(['b', 'd', 'e'], 999) == m(a=1, b=m(c=3, d=m(e=999, f=7)))
  179. def test_transform_levels_missing():
  180. x = m(a=1, b=m(c=3))
  181. assert x.transform(['b', 'd', 'e'], 999) == m(a=1, b=m(c=3, d=m(e=999)))
  182. class HashDummy(object):
  183. def __hash__(self):
  184. return 6528039219058920 # Hash of '33'
  185. def __eq__(self, other):
  186. return self is other
  187. def test_hash_collision_is_correctly_resolved():
  188. dummy1 = HashDummy()
  189. dummy2 = HashDummy()
  190. dummy3 = HashDummy()
  191. dummy4 = HashDummy()
  192. map1 = pmap({dummy1: 1, dummy2: 2, dummy3: 3})
  193. assert map1[dummy1] == 1
  194. assert map1[dummy2] == 2
  195. assert map1[dummy3] == 3
  196. assert dummy4 not in map1
  197. keys = set()
  198. values = set()
  199. for k, v in map1.iteritems():
  200. keys.add(k)
  201. values.add(v)
  202. assert keys == {dummy1, dummy2, dummy3}
  203. assert values == {1, 2, 3}
  204. map2 = map1.set(dummy1, 11)
  205. assert map2[dummy1] == 11
  206. # Re-use existing structure when inserted element is the same
  207. assert map2.set(dummy1, 11) is map2
  208. map3 = map1.set('a', 22)
  209. assert map3['a'] == 22
  210. assert map3[dummy3] == 3
  211. # Remove elements
  212. map4 = map1.discard(dummy2)
  213. assert len(map4) == 2
  214. assert map4[dummy1] == 1
  215. assert dummy2 not in map4
  216. assert map4[dummy3] == 3
  217. assert map1.discard(dummy4) is map1
  218. # Empty map handling
  219. empty_map = map4.remove(dummy1).remove(dummy3)
  220. assert len(empty_map) == 0
  221. assert empty_map.discard(dummy1) is empty_map
  222. def test_bitmap_indexed_iteration():
  223. a_map = pmap({'a': 2, 'b': 1})
  224. keys = set()
  225. values = set()
  226. count = 0
  227. for k, v in a_map.iteritems():
  228. count += 1
  229. keys.add(k)
  230. values.add(v)
  231. assert count == 2
  232. assert keys == {'a', 'b'}
  233. assert values == {2, 1}
  234. def test_iteration_with_many_elements():
  235. values = list(range(0, 2000))
  236. keys = [str(x) for x in values]
  237. init_dict = dict(zip(keys, values))
  238. hash_dummy1 = HashDummy()
  239. hash_dummy2 = HashDummy()
  240. # Throw in a couple of hash collision nodes to tests
  241. # those properly as well
  242. init_dict[hash_dummy1] = 12345
  243. init_dict[hash_dummy2] = 54321
  244. a_map = pmap(init_dict)
  245. actual_values = set()
  246. actual_keys = set()
  247. for k, v in a_map.iteritems():
  248. actual_values.add(v)
  249. actual_keys.add(k)
  250. assert actual_keys == set(keys + [hash_dummy1, hash_dummy2])
  251. assert actual_values == set(values + [12345, 54321])
  252. def test_str():
  253. assert str(pmap({1: 2, 3: 4})) == "pmap({1: 2, 3: 4})"
  254. def test_empty_truthiness():
  255. assert m(a=1)
  256. assert not m()
  257. def test_update_with():
  258. assert m(a=1).update_with(add, m(a=2, b=4)) == m(a=3, b=4)
  259. assert m(a=1).update_with(lambda l, r: l, m(a=2, b=4)) == m(a=1, b=4)
  260. def map_add(l, r):
  261. return dict(list(l.items()) + list(r.items()))
  262. assert m(a={'c': 3}).update_with(map_add, m(a={'d': 4})) == m(a={'c': 3, 'd': 4})
  263. def test_pickling_empty_map():
  264. assert pickle.loads(pickle.dumps(m(), -1)) == m()
  265. def test_pickling_non_empty_map():
  266. assert pickle.loads(pickle.dumps(m(a=1, b=2), -1)) == m(a=1, b=2)
  267. def test_set_with_relocation():
  268. x = pmap({'a': 1000}, pre_size=1)
  269. x = x.set('b', 3000)
  270. x = x.set('c', 4000)
  271. x = x.set('d', 5000)
  272. x = x.set('d', 6000)
  273. assert len(x) == 4
  274. assert x == pmap({'a': 1000, 'b': 3000, 'c': 4000, 'd': 6000})
  275. def test_evolver_simple_update():
  276. x = m(a=1000, b=2000)
  277. e = x.evolver()
  278. e['b'] = 3000
  279. assert e['b'] == 3000
  280. assert e.persistent()['b'] == 3000
  281. assert x['b'] == 2000
  282. def test_evolver_update_with_relocation():
  283. x = pmap({'a': 1000}, pre_size=1)
  284. e = x.evolver()
  285. e['b'] = 3000
  286. e['c'] = 4000
  287. e['d'] = 5000
  288. e['d'] = 6000
  289. assert len(e) == 4
  290. assert e.persistent() == pmap({'a': 1000, 'b': 3000, 'c': 4000, 'd': 6000})
  291. def test_evolver_set_with_reallocation_edge_case():
  292. # Demonstrates a bug in evolver that also affects updates. Under certain
  293. # circumstances, the result of `x.update(y)` will **not** have all the
  294. # keys from `y`.
  295. foo = object()
  296. x = pmap({'a': foo}, pre_size=1)
  297. e = x.evolver()
  298. e['b'] = 3000
  299. # Bug is triggered when we do a reallocation and the new value is
  300. # identical to the old one.
  301. e['a'] = foo
  302. y = e.persistent()
  303. assert 'b' in y
  304. assert y is e.persistent()
  305. def test_evolver_remove_element():
  306. e = m(a=1000, b=2000).evolver()
  307. assert 'a' in e
  308. del e['a']
  309. assert 'a' not in e
  310. def test_evolver_remove_element_not_present():
  311. e = m(a=1000, b=2000).evolver()
  312. with pytest.raises(KeyError) as error:
  313. del e['c']
  314. assert str(error.value) == "'c'"
  315. def test_copy_returns_reference_to_self():
  316. m1 = m(a=10)
  317. assert m1.copy() is m1
  318. def test_dot_access_of_non_existing_element_raises_attribute_error():
  319. m1 = m(a=10)
  320. with pytest.raises(AttributeError) as error:
  321. m1.b
  322. error_message = str(error.value)
  323. assert "'b'" in error_message
  324. assert type(m1).__name__ in error_message
  325. def test_pmap_unorderable():
  326. with pytest.raises(TypeError):
  327. _ = m(a=1) < m(b=2)
  328. with pytest.raises(TypeError):
  329. _ = m(a=1) <= m(b=2)
  330. with pytest.raises(TypeError):
  331. _ = m(a=1) > m(b=2)
  332. with pytest.raises(TypeError):
  333. _ = m(a=1) >= m(b=2)
  334. def test_supports_weakref():
  335. import weakref
  336. weakref.ref(m(a=1))
  337. def test_insert_and_get_many_elements():
  338. # This test case triggers reallocation of the underlying bucket structure.
  339. a_map = m()
  340. for x in range(1000):
  341. a_map = a_map.set(str(x), x)
  342. assert len(a_map) == 1000
  343. for x in range(1000):
  344. assert a_map[str(x)] == x, x
  345. def test_iterable():
  346. """
  347. PMaps can be created from iterables even though they can't be len() hinted.
  348. """
  349. assert pmap(iter([("a", "b")])) == pmap([("a", "b")])
  350. class BrokenPerson(namedtuple('Person', 'name')):
  351. def __eq__(self, other):
  352. return self.__class__ == other.__class__ and self.name == other.name
  353. def __hash__(self):
  354. return hash(self.name)
  355. class BrokenItem(namedtuple('Item', 'name')):
  356. def __eq__(self, other):
  357. return self.__class__ == other.__class__ and self.name == other.name
  358. def __hash__(self):
  359. return hash(self.name)
  360. def test_pmap_removal_with_broken_classes_deriving_from_namedtuple():
  361. """
  362. The two classes above implement __eq__ but also would need to implement __ne__ to compare
  363. consistently. See issue https://github.com/tobgu/pyrsistent/issues/268 for details.
  364. """
  365. s = pmap({BrokenPerson('X'): 2, BrokenItem('X'): 3})
  366. s = s.remove(BrokenPerson('X'))
  367. # Both items are removed due to how they are compared for inequality
  368. assert BrokenPerson('X') not in s
  369. assert BrokenItem('X') in s
  370. assert len(s) == 1