test_dictutils.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521
  1. # -*- coding: utf-8 -*-
  2. import sys
  3. import pytest
  4. from boltons.dictutils import OMD, OneToOne, ManyToMany, FrozenDict, subdict, FrozenHashError
  5. _ITEMSETS = [[],
  6. [('a', 1), ('b', 2), ('c', 3)],
  7. [('A', 'One'), ('A', 'One'), ('A', 'One')],
  8. [('Z', -1), ('Y', -2), ('Y', -2)],
  9. [('a', 1), ('b', 2), ('a', 3), ('c', 4)]]
  10. def test_dict_init():
  11. d = dict(_ITEMSETS[1])
  12. omd = OMD(d)
  13. assert omd['a'] == 1
  14. assert omd['b'] == 2
  15. assert omd['c'] == 3
  16. assert len(omd) == 3
  17. assert omd.getlist('a') == [1]
  18. assert omd == d
  19. def test_todict():
  20. omd = OMD(_ITEMSETS[2])
  21. assert len(omd) == 1
  22. assert omd['A'] == 'One'
  23. d = omd.todict(multi=True)
  24. assert len(d) == 1
  25. assert d['A'] == ['One', 'One', 'One']
  26. flat = omd.todict()
  27. assert flat['A'] == 'One'
  28. for itemset in _ITEMSETS:
  29. omd = OMD(itemset)
  30. d = dict(itemset)
  31. flat = omd.todict()
  32. assert flat == d
  33. return
  34. def test_eq():
  35. omd = OMD(_ITEMSETS[3])
  36. assert omd == omd
  37. assert not (omd != omd)
  38. omd2 = OMD(_ITEMSETS[3])
  39. assert omd == omd2
  40. assert omd2 == omd
  41. assert not (omd != omd2)
  42. d = dict(_ITEMSETS[3])
  43. assert d == omd
  44. omd3 = OMD(d)
  45. assert omd != omd3
  46. def test_copy():
  47. for itemset in _ITEMSETS:
  48. omd = OMD(itemset)
  49. omd_c = omd.copy()
  50. assert omd == omd_c
  51. if omd_c:
  52. omd_c.pop(itemset[0][0])
  53. assert omd != omd_c
  54. return
  55. def test_clear():
  56. for itemset in _ITEMSETS:
  57. omd = OMD(itemset)
  58. omd.clear()
  59. assert len(omd) == 0
  60. assert not omd
  61. omd.clear()
  62. assert not omd
  63. omd['a'] = 22
  64. assert omd
  65. omd.clear()
  66. assert not omd
  67. def test_types():
  68. try:
  69. from collections.abc import MutableMapping
  70. except ImportError:
  71. from collections import MutableMapping
  72. omd = OMD()
  73. assert isinstance(omd, dict)
  74. assert isinstance(omd, MutableMapping)
  75. def test_multi_correctness():
  76. size = 100
  77. redun = 5
  78. _rng = range(size)
  79. _rng_redun = list(range(size//redun)) * redun
  80. _pairs = zip(_rng_redun, _rng)
  81. omd = OMD(_pairs)
  82. for multi in (True, False):
  83. vals = [x[1] for x in omd.iteritems(multi=multi)]
  84. strictly_ascending = all([x < y for x, y in zip(vals, vals[1:])])
  85. assert strictly_ascending
  86. return
  87. def test_kv_consistency():
  88. for itemset in _ITEMSETS:
  89. omd = OMD(itemset)
  90. for multi in (True, False):
  91. items = omd.items(multi=multi)
  92. keys = omd.keys(multi=multi)
  93. values = omd.values(multi=multi)
  94. assert keys == [x[0] for x in items]
  95. assert values == [x[1] for x in items]
  96. return
  97. def test_update_basic():
  98. omd = OMD(_ITEMSETS[1])
  99. omd2 = OMD({'a': 10})
  100. omd.update(omd2)
  101. assert omd['a'] == 10
  102. assert omd.getlist('a') == [10]
  103. omd2_c = omd2.copy()
  104. omd2_c.pop('a')
  105. assert omd2 != omd2_c
  106. def test_update():
  107. for first, second in zip(_ITEMSETS, _ITEMSETS[1:]):
  108. omd1 = OMD(first)
  109. omd2 = OMD(second)
  110. ref1 = dict(first)
  111. ref2 = dict(second)
  112. omd1.update(omd2)
  113. ref1.update(ref2)
  114. assert omd1.todict() == ref1
  115. omd1_repr = repr(omd1)
  116. omd1.update(omd1)
  117. assert omd1_repr == repr(omd1)
  118. def test_update_extend():
  119. for first, second in zip(_ITEMSETS, _ITEMSETS[1:] + [[]]):
  120. omd1 = OMD(first)
  121. omd2 = OMD(second)
  122. ref = dict(first)
  123. orig_keys = set(omd1)
  124. ref.update(second)
  125. omd1.update_extend(omd2)
  126. for k in omd2:
  127. assert len(omd1.getlist(k)) >= len(omd2.getlist(k))
  128. assert omd1.todict() == ref
  129. assert orig_keys <= set(omd1)
  130. def test_invert():
  131. for items in _ITEMSETS:
  132. omd = OMD(items)
  133. iomd = omd.inverted()
  134. # first, test all items made the jump
  135. assert len(omd.items(multi=True)) == len(iomd.items(multi=True))
  136. for val in omd.values():
  137. assert val in iomd # all values present as keys
  138. def test_poplast():
  139. for items in _ITEMSETS[1:]:
  140. omd = OMD(items)
  141. assert omd.poplast() == items[-1][-1]
  142. def test_pop():
  143. omd = OMD()
  144. omd.add('even', 0)
  145. omd.add('odd', 1)
  146. omd.add('even', 2)
  147. assert omd.pop('odd') == 1
  148. assert omd.pop('odd', 99) == 99
  149. try:
  150. omd.pop('odd')
  151. assert False
  152. except KeyError:
  153. pass
  154. assert len(omd) == 1
  155. assert len(omd.items(multi=True)) == 2
  156. def test_addlist():
  157. omd = OMD()
  158. omd.addlist('a', [1, 2, 3])
  159. omd.addlist('b', [4, 5])
  160. assert omd.keys() == ['a', 'b']
  161. assert len(list(omd.iteritems(multi=True))) == 5
  162. e_omd = OMD()
  163. e_omd.addlist('a', [])
  164. assert e_omd.keys() == []
  165. assert len(list(e_omd.iteritems(multi=True))) == 0
  166. def test_pop_all():
  167. omd = OMD()
  168. omd.add('even', 0)
  169. omd.add('odd', 1)
  170. omd.add('even', 2)
  171. assert omd.popall('odd') == [1]
  172. assert len(omd) == 1
  173. try:
  174. omd.popall('odd')
  175. assert False
  176. except KeyError:
  177. pass
  178. assert omd.popall('odd', None) is None
  179. assert omd.popall('even') == [0, 2]
  180. assert len(omd) == 0
  181. assert omd.popall('nope', None) is None
  182. assert OMD().popall('', None) is None
  183. def test_reversed():
  184. try:
  185. from collections import OrderedDict
  186. except:
  187. # skip on python 2.6
  188. return
  189. for items in _ITEMSETS:
  190. omd = OMD(items)
  191. od = OrderedDict(items)
  192. for ik, ok in zip(reversed(od), reversed(omd)):
  193. assert ik == ok
  194. r100 = range(100)
  195. omd = OMD(zip(r100, r100))
  196. for i in r100:
  197. omd.add(i, i)
  198. r100 = list(reversed(r100))
  199. assert list(reversed(omd)) == r100
  200. omd = OMD()
  201. assert list(reversed(omd)) == list(reversed(omd.keys()))
  202. for i in range(20):
  203. for j in range(i):
  204. omd.add(i, i)
  205. assert list(reversed(omd)) == list(reversed(omd.keys()))
  206. def test_setdefault():
  207. omd = OMD()
  208. empty_list = []
  209. x = omd.setdefault('1', empty_list)
  210. assert x is empty_list
  211. y = omd.setdefault('2')
  212. assert y is None
  213. assert omd.setdefault('1', None) is empty_list
  214. e_omd = OMD()
  215. e_omd.addlist(1, [])
  216. assert e_omd.popall(1, None) is None
  217. assert len(e_omd) == 0
  218. ## END OMD TESTS
  219. import string
  220. def test_subdict():
  221. cap_map = dict([(x, x.upper()) for x in string.hexdigits])
  222. assert len(cap_map) == 22
  223. assert len(subdict(cap_map, drop=['a'])) == 21
  224. assert 'a' not in subdict(cap_map, drop=['a'])
  225. assert len(subdict(cap_map, keep=['a', 'b'])) == 2
  226. def test_subdict_keep_type():
  227. omd = OMD({'a': 'A'})
  228. assert subdict(omd) == omd
  229. assert type(subdict(omd)) is OMD
  230. def test_one_to_one():
  231. e = OneToOne({1:2})
  232. def ck(val, inv):
  233. assert (e, e.inv) == (val, inv)
  234. ck({1:2}, {2:1})
  235. e[2] = 3
  236. ck({1:2, 2:3}, {3:2, 2:1})
  237. e.clear()
  238. ck({}, {})
  239. e[1] = 1
  240. ck({1:1}, {1:1})
  241. e[1] = 2
  242. ck({1:2}, {2:1})
  243. e[3] = 2
  244. ck({3:2}, {2:3})
  245. del e[3]
  246. ck({}, {})
  247. e[1] = 2
  248. e.inv[2] = 3
  249. ck({3:2}, {2:3})
  250. del e.inv[2]
  251. ck({}, {})
  252. assert OneToOne({1:2, 3:4}).copy().inv == {2:1, 4:3}
  253. e[1] = 2
  254. e.pop(1)
  255. ck({}, {})
  256. e[1] = 2
  257. e.inv.pop(2)
  258. ck({}, {})
  259. e[1] = 2
  260. e.popitem()
  261. ck({}, {})
  262. e.setdefault(1)
  263. ck({1: None}, {None: 1})
  264. e.inv.setdefault(2)
  265. ck({1: None, None: 2}, {None: 1, 2: None})
  266. e.clear()
  267. e.update({1:2}, cat="dog")
  268. ck({1:2, "cat":"dog"}, {2:1, "dog":"cat"})
  269. # try various overlapping values
  270. oto = OneToOne({'a': 0, 'b': 0})
  271. assert len(oto) == len(oto.inv) == 1
  272. oto['c'] = 0
  273. assert len(oto) == len(oto.inv) == 1
  274. assert oto.inv[0] == 'c'
  275. oto.update({'z': 0, 'y': 0})
  276. assert len(oto) == len(oto.inv) == 1
  277. # test out unique classmethod
  278. with pytest.raises(ValueError):
  279. OneToOne.unique({'a': 0, 'b': 0})
  280. return
  281. def test_many_to_many():
  282. m2m = ManyToMany()
  283. assert len(m2m) == 0
  284. assert not m2m
  285. m2m.add(1, 'a')
  286. assert m2m
  287. m2m.add(1, 'b')
  288. assert len(m2m) == 1
  289. assert m2m[1] == frozenset(['a', 'b'])
  290. assert m2m.inv['a'] == frozenset([1])
  291. del m2m.inv['a']
  292. assert m2m[1] == frozenset(['b'])
  293. assert 1 in m2m
  294. del m2m.inv['b']
  295. assert 1 not in m2m
  296. m2m[1] = ('a', 'b')
  297. assert set(m2m.iteritems()) == set([(1, 'a'), (1, 'b')])
  298. m2m.remove(1, 'a')
  299. m2m.remove(1, 'b')
  300. assert 1 not in m2m
  301. m2m.update([(1, 'a'), (2, 'b')])
  302. assert m2m.get(2) == frozenset(('b',))
  303. assert m2m.get(3) == frozenset(())
  304. assert ManyToMany(['ab', 'cd']) == ManyToMany(['ba', 'dc']).inv
  305. assert ManyToMany(ManyToMany(['ab', 'cd'])) == ManyToMany(['ab', 'cd'])
  306. m2m = ManyToMany({'a': 'b'})
  307. m2m.replace('a', 'B')
  308. # also test the repr while we're at it
  309. assert repr(m2m) == repr(ManyToMany([("B", "b")]))
  310. assert repr(m2m).startswith('ManyToMany(') and 'B' in repr(m2m)
  311. def test_frozendict():
  312. efd = FrozenDict()
  313. assert isinstance(efd, dict)
  314. assert len(efd) == 0
  315. assert not efd
  316. assert repr(efd) == "FrozenDict({})"
  317. data = {'a': 'A', 'b': 'B'}
  318. fd = FrozenDict(data)
  319. assert bool(fd)
  320. assert len(fd) == 2
  321. assert fd['a'] == 'A'
  322. assert fd['b'] == 'B'
  323. assert sorted(fd.keys()) == ['a', 'b']
  324. assert sorted(fd.values()) == ['A', 'B']
  325. assert sorted(fd.items()) == [('a', 'A'), ('b', 'B')]
  326. assert 'a' in fd
  327. assert 'c' not in fd
  328. assert hash(fd)
  329. fd_map = {'fd': fd}
  330. assert fd_map['fd'] is fd
  331. with pytest.raises(TypeError):
  332. fd['c'] = 'C'
  333. with pytest.raises(TypeError):
  334. del fd['a']
  335. with pytest.raises(TypeError):
  336. fd.update(x='X')
  337. with pytest.raises(TypeError):
  338. fd.setdefault('x', [])
  339. with pytest.raises(TypeError):
  340. fd.pop('c')
  341. with pytest.raises(TypeError):
  342. fd.popitem()
  343. with pytest.raises(TypeError):
  344. fd.clear()
  345. import pickle
  346. fkfd = FrozenDict.fromkeys([2, 4, 6], value=0)
  347. assert pickle.loads(pickle.dumps(fkfd)) == fkfd
  348. assert sorted(fkfd.updated({8: 0}).keys()) == [2, 4, 6, 8]
  349. # try something with an unhashable value
  350. unfd = FrozenDict({'a': ['A']})
  351. with pytest.raises(TypeError) as excinfo:
  352. {unfd: 'val'}
  353. assert excinfo.type is FrozenHashError
  354. with pytest.raises(TypeError) as excinfo2:
  355. {unfd: 'val'}
  356. assert excinfo.value is excinfo2.value # test cached exception
  357. return
  358. @pytest.mark.skipif(sys.version_info < (3, 9), reason="requires python3.9 or higher")
  359. def test_frozendict_ior():
  360. data = {'a': 'A', 'b': 'B'}
  361. fd = FrozenDict(data)
  362. with pytest.raises(TypeError, match=".*FrozenDict.*immutable.*"):
  363. fd |= fd
  364. def test_frozendict_api():
  365. # all the read-only methods that are fine
  366. through_methods = ['__class__',
  367. '__cmp__',
  368. '__contains__',
  369. '__delattr__',
  370. '__dir__',
  371. '__eq__',
  372. '__format__',
  373. '__ge__',
  374. '__getattribute__',
  375. '__getstate__',
  376. '__getitem__',
  377. '__getstate__',
  378. '__gt__',
  379. '__init__',
  380. '__iter__',
  381. '__le__',
  382. '__len__',
  383. '__lt__',
  384. '__ne__',
  385. '__new__',
  386. '__or__',
  387. '__reduce__',
  388. '__reversed__',
  389. '__ror__',
  390. '__setattr__',
  391. '__sizeof__',
  392. '__str__',
  393. 'copy',
  394. 'get',
  395. 'has_key',
  396. 'items',
  397. 'iteritems',
  398. 'iterkeys',
  399. 'itervalues',
  400. 'keys',
  401. 'values',
  402. 'viewitems',
  403. 'viewkeys',
  404. 'viewvalues']
  405. fd = FrozenDict()
  406. ret = []
  407. for attrname in dir(fd):
  408. if attrname == '_hash': # in the dir, even before it's set
  409. continue
  410. attr = getattr(fd, attrname)
  411. if not callable(attr):
  412. continue
  413. if getattr(FrozenDict, attrname) == getattr(dict, attrname, None) and attrname not in through_methods:
  414. assert attrname == False
  415. ret.append(attrname)
  416. import copy
  417. assert copy.copy(fd) is fd