mirror of https://github.com/jab/bidict.git
629 lines
24 KiB
Python
629 lines
24 KiB
Python
# Copyright 2009-2022 Joshua Bronson. All rights reserved.
|
|
#
|
|
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
|
"""Property-based tests using https://hypothesis.readthedocs.io."""
|
|
|
|
import gc
|
|
import operator as op
|
|
import pickle
|
|
import sys
|
|
import unittest.mock
|
|
import weakref
|
|
|
|
from copy import deepcopy
|
|
from collections import OrderedDict, UserDict
|
|
from collections.abc import Iterable, KeysView, ValuesView, ItemsView
|
|
|
|
from itertools import tee
|
|
|
|
import pytest
|
|
from hypothesis import assume, example, given
|
|
|
|
from bidict import (
|
|
BidictException,
|
|
DROP_NEW, DROP_OLD, RAISE, OnDup,
|
|
BidirectionalMapping, MutableBidirectionalMapping,
|
|
OrderedBidict, bidict, namedbidict,
|
|
inverted,
|
|
DuplicationError, KeyDuplicationError, ValueDuplicationError, KeyAndValueDuplicationError,
|
|
)
|
|
from bidict._iter import iteritems
|
|
|
|
from . import _strategies as st
|
|
from ._types import (
|
|
ORDER_PRESERVING_BIDICT_TYPES,
|
|
BIDICT_TYPE_WHOSE_MODULE_HAS_REF_TO_INV_CLS,
|
|
BIDICT_TYPE_WHOSE_MODULE_HAS_NO_REF_TO_INV_CLS,
|
|
)
|
|
|
|
|
|
require_cpython_refcounting = pytest.mark.skipif(
|
|
sys.implementation.name != 'cpython',
|
|
reason='Requires CPython refcounting behavior',
|
|
)
|
|
|
|
|
|
@given(st.BIDICTS, st.NON_MAPPINGS)
|
|
def test_unequal_to_non_mapping(bi, not_a_mapping):
|
|
"""Bidicts and their inverses should compare unequal to bools, ints, and other typical non-mappings."""
|
|
assert bi != not_a_mapping
|
|
assert bi.inv != not_a_mapping
|
|
assert not bi == not_a_mapping
|
|
assert not bi.inv == not_a_mapping
|
|
|
|
|
|
@given(st.BIDICTS)
|
|
def test_eq_correctly_defers_to_eq_of_non_mapping(bi):
|
|
"""Bidicts' __eq__ does not defeat non-mapping objects' __eq__, when implemented."""
|
|
assert bi == unittest.mock.ANY
|
|
assert unittest.mock.ANY == bi
|
|
|
|
|
|
@given(st.BI_AND_MAP_FROM_DIFF_ITEMS)
|
|
def test_unequal_to_mapping_with_different_items(bi_and_map_from_diff_items):
|
|
"""Bidicts should be unequal to mappings containing different items."""
|
|
bi, mapping = bi_and_map_from_diff_items
|
|
assert bi != mapping
|
|
assert not bi == mapping
|
|
|
|
|
|
@given(st.BI_AND_MAP_FROM_SAME_ND_ITEMS)
|
|
def test_equal_to_mapping_with_same_items(bi_and_map_from_same_items):
|
|
"""Bidicts should be equal to mappings created from the same non-duplicating items.
|
|
|
|
The bidict's inverse and the mapping's inverse should also be equal.
|
|
"""
|
|
bi, mapping = bi_and_map_from_same_items
|
|
assert bi == mapping
|
|
assert mapping == bi
|
|
assert not bi != mapping
|
|
assert not mapping != bi
|
|
mapping_inv = OrderedDict((v, k) for (k, v) in mapping.items())
|
|
assert bi.inv == mapping_inv
|
|
assert mapping_inv == bi.inv
|
|
assert not bi.inv != mapping_inv
|
|
assert not mapping_inv != bi.inv
|
|
|
|
|
|
@given(st.HBI_AND_HMAP_FROM_SAME_ND_ITEMS)
|
|
def test_equal_hashables_have_same_hash(hashable_bidict_and_mapping):
|
|
"""Hashable bidicts and hashable mappings that are equal should hash to the same value."""
|
|
bi, mapping = hashable_bidict_and_mapping
|
|
assert bi == mapping
|
|
assert mapping == bi
|
|
assert hash(bi) == hash(mapping)
|
|
|
|
|
|
@given(st.BIDICTS, st.NON_BI_MAPPINGS)
|
|
@example(OrderedBidict([(1, 1), (2, 2)]), OrderedDict([(1, 1), (2, 2)]))
|
|
@example(OrderedBidict([(1, 1), (2, 2)]), OrderedDict([(2, 2), (1, 1)]))
|
|
@example(OrderedBidict({None: None}), {False: None, None: None})
|
|
def test_equals_matches_equals_order_sensitive(bi, mapping):
|
|
"""Bidict equals_order_sensitive should agree with __eq__."""
|
|
mapping_inv = OrderedDict((v, k) for (k, v) in mapping.items())
|
|
if bi.equals_order_sensitive(mapping):
|
|
assert bi == mapping
|
|
assert mapping == bi
|
|
assert list(bi.inv.items()) == list(mapping_inv.items())
|
|
else:
|
|
assert list(bi.items()) != list(mapping.items())
|
|
if bi == mapping:
|
|
assert mapping == bi
|
|
assert bi.items() == mapping.items() # should use (unordered) set comparison
|
|
assert bi.inv.items() == mapping_inv.items() # ditto
|
|
assert mapping_inv.items() == bi.inv.items() # ditto
|
|
else:
|
|
assert mapping != bi
|
|
assert bi.items() != mapping.items()
|
|
assert bi.inv.items() != mapping_inv.items() or len(mapping_inv) != len(mapping)
|
|
assert mapping_inv.items() != bi.inv.items() or len(mapping_inv) != len(mapping)
|
|
|
|
|
|
@given(st.BI_AND_MAP_FROM_SAME_ND_ITEMS)
|
|
def test_equals_order_sensitive_same_items(bi_and_map_from_same_items):
|
|
"""Bidicts should be order-sensitive-equal to mappings with the same items in the same order.
|
|
|
|
The bidict's inverse and the ordered mapping's inverse should also be order-sensitive-equal.
|
|
"""
|
|
bi, mapping = bi_and_map_from_same_items
|
|
assert bi.equals_order_sensitive(mapping)
|
|
mapping_inv = OrderedDict((v, k) for (k, v) in mapping.items())
|
|
assert bi.inv.equals_order_sensitive(mapping_inv)
|
|
|
|
|
|
@given(st.OBI_AND_OMAP_FROM_SAME_ITEMS_DIFF_ORDER)
|
|
def test_unequal_order_sensitive_same_items_different_order(ob_and_om):
|
|
"""Ordered bidicts should be order-sensitive-unequal to ordered mappings of diff-ordered items.
|
|
|
|
Where both were created from the same items where no key or value was duplicated,
|
|
but the items were ordered differently.
|
|
|
|
The bidict's inverse and the ordered mapping's inverse should also be order-sensitive-unequal.
|
|
"""
|
|
ob, om = ob_and_om
|
|
assert not ob.equals_order_sensitive(om)
|
|
om_inv = OrderedDict((v, k) for (k, v) in om.items())
|
|
assert not ob.inv.equals_order_sensitive(om_inv)
|
|
|
|
|
|
@given(st.ORDERED_BIDICTS, st.NON_MAPPINGS)
|
|
def test_unequal_order_sensitive_non_mapping(ob, not_a_mapping):
|
|
"""Ordered bidicts should be order-sensitive-unequal to ordered mappings of diff-ordered items.
|
|
|
|
Where both were created from the same items where no key or value was duplicated,
|
|
but the items were ordered differently.
|
|
|
|
The bidict's inverse and the ordered mapping's inverse should also be order-sensitive-unequal.
|
|
"""
|
|
assert not ob.equals_order_sensitive(not_a_mapping)
|
|
assert not ob.inv.equals_order_sensitive(not_a_mapping)
|
|
|
|
|
|
@given(st.BIDICTS, st.NON_BI_MAPPINGS)
|
|
def test_merge_operators(bi, mapping):
|
|
"""PEP 584-style dict merge operators should work as expected."""
|
|
try:
|
|
merged = bi | mapping
|
|
except DuplicationError as exc:
|
|
with pytest.raises(exc.__class__):
|
|
bidict(bi).update(mapping)
|
|
with pytest.raises(exc.__class__):
|
|
bi |= mapping
|
|
else:
|
|
assert merged == bidict({**bi, **mapping})
|
|
tmp = bidict(bi)
|
|
tmp |= mapping
|
|
assert merged == tmp
|
|
|
|
try:
|
|
merged = mapping | bi
|
|
except DuplicationError as exc:
|
|
with pytest.raises(exc.__class__):
|
|
bidict(mapping).update(bi)
|
|
else:
|
|
assert merged == bidict({**mapping, **bi})
|
|
mapping |= bi
|
|
assert merged == mapping
|
|
|
|
|
|
@given(st.MUTABLE_BIDICTS, st.DIFF_ATOMS, st.RANDOMS)
|
|
def test_setitem_with_dup_val_raises(bi, new_key, rand):
|
|
"""Setting an item whose value duplicates that of an existing item should raise ValueDuplicationError."""
|
|
ln = len(bi)
|
|
assume(ln > 2)
|
|
for b in (bi, bi.inv):
|
|
existing_val = rand.choice(list(b.inv))
|
|
with pytest.raises(ValueDuplicationError):
|
|
b[new_key] = existing_val
|
|
assert len(b) == len(b.inv) == ln
|
|
|
|
|
|
@given(st.MUTABLE_BIDICTS, st.RANDOMS)
|
|
def test_setitem_with_dup_key_val_raises(bi, rand):
|
|
"""Setting an item whose key and val duplicate two different existing items raises KeyAndValueDuplicationError."""
|
|
ln = len(bi)
|
|
assume(ln > 2)
|
|
for b in (bi, bi.inv):
|
|
existing_items = rand.sample(list(b.items()), 2)
|
|
existing_key = existing_items[0][0]
|
|
existing_val = existing_items[1][1]
|
|
with pytest.raises(KeyAndValueDuplicationError):
|
|
b[existing_key] = existing_val
|
|
assert len(b) == len(b.inv) == ln
|
|
|
|
|
|
@given(st.MUTABLE_BIDICTS, st.DIFF_ATOMS, st.RANDOMS)
|
|
def test_put_with_dup_key_raises(bi, new_val, rand):
|
|
"""Putting an item whose key duplicates that of an existing item should raise KeyDuplicationError."""
|
|
ln = len(bi)
|
|
assume(ln > 2)
|
|
for b in (bi, bi.inv):
|
|
existing_key = rand.choice(list(b))
|
|
with pytest.raises(KeyDuplicationError):
|
|
b.put(existing_key, new_val)
|
|
assert len(b) == len(b.inv) == ln
|
|
|
|
|
|
@given(st.BIDICTS)
|
|
def test_bijectivity(bi):
|
|
"""b[k] == v <==> b.inv[v] == k"""
|
|
for b in (bi, bi.inv):
|
|
assert all(b.inv[v] == k for (k, v) in b.items())
|
|
|
|
|
|
@given(st.MUTABLE_BIDICTS)
|
|
def test_cleared_bidicts_have_no_items(bi):
|
|
bi.clear()
|
|
assert not bi
|
|
assert len(bi) == 0
|
|
|
|
|
|
@given(st.BI_AND_CMPDICT_FROM_SAME_ITEMS, st.DATA)
|
|
def test_consistency_after_method_call(bi_and_cmp_dict, data):
|
|
"""A bidict should be left in a consistent state after calling any method, even if it raises."""
|
|
bi_orig, cmp_dict_orig = bi_and_cmp_dict
|
|
for methodname, args_strat in st.METHOD_ARGS_PAIRS:
|
|
if not hasattr(bi_orig, methodname):
|
|
continue
|
|
bi = bi_orig.copy()
|
|
collect = list if isinstance(bi, ORDER_PRESERVING_BIDICT_TYPES) else set
|
|
method = getattr(bi, methodname)
|
|
args = data.draw(args_strat) if args_strat is not None else ()
|
|
try:
|
|
result = method(*args)
|
|
except TypeError:
|
|
assert methodname == 'popitem', 'popitem should be the only method that can raise TypeError here (we sometimes pass in the wrong number of args)'
|
|
continue
|
|
except (KeyError, BidictException) as exc:
|
|
# Call should fail clean, i.e. bi should be in the same state it was before the call.
|
|
assertmsg = f'{method!r} did not fail clean: {exc!r}'
|
|
assert bi == bi_orig, assertmsg
|
|
assert bi.inv == bi_orig.inv, assertmsg
|
|
assert collect(bi.keys()) == collect(bi_orig.keys()), assertmsg
|
|
assert collect(bi.values()) == collect(bi_orig.values()), assertmsg
|
|
assert collect(bi.items()) == collect(bi_orig.items()), assertmsg
|
|
assert collect(reversed(bi.keys())) == collect(reversed(bi_orig.keys())), assertmsg
|
|
assert collect(reversed(bi.values())) == collect(reversed(bi_orig.values())), assertmsg
|
|
assert collect(reversed(bi.items())) == collect(reversed(bi_orig.items())), assertmsg
|
|
else:
|
|
# Should get the same result as calling the same method on the compare-to dict.
|
|
cmp_dict = cmp_dict_orig.copy()
|
|
cmp_dict_meth = getattr(cmp_dict, methodname, None)
|
|
if cmp_dict_meth:
|
|
cmp_result = cmp_dict_meth(*args)
|
|
if isinstance(cmp_result, Iterable):
|
|
result = collect(result)
|
|
cmp_result = collect(cmp_result)
|
|
assert result == cmp_result, f'methodname={methodname} args={args!r}'
|
|
# Whether the call failed or succeeded, bi should pass consistency checks.
|
|
keys = collect(bi.keys())
|
|
assert keys == collect(bi)
|
|
vals = collect(bi.values())
|
|
assert vals == collect(bi[k] for k in bi)
|
|
items = collect(bi.items())
|
|
assert items == collect((k, bi[k]) for k in bi)
|
|
assert collect(bi.inv.keys()) == collect(bi.inv) == vals
|
|
assert collect(bi.inv.values()) == collect(bi.inv[k] for k in bi.inv) == keys
|
|
assert collect(bi.inv.items()) == collect((k, bi.inv[k]) for k in bi.inv)
|
|
if not getattr(bi.keys(), '__reversed__', None): # Python < 3.8
|
|
return
|
|
assert collect(reversed(bi.keys())) == collect(reversed(bi.inv.values()))
|
|
assert collect(reversed(bi.values())) == collect(reversed(bi.inv.keys()))
|
|
assert collect(reversed(bi.items())) == collect((k, v) for (v, k) in reversed(bi.inv.items()))
|
|
|
|
|
|
@given(st.MUTABLE_BIDICTS, st.L_PAIRS, st.ON_DUP)
|
|
# These test cases ensure coverage of all branches in [Ordered]BidictBase._undo_write
|
|
# (Hypothesis doesn't always generate examples that cover all the branches otherwise).
|
|
@example(bidict({1: 1, 2: 2}), [(1, 3), (1, 2)], OnDup(DROP_OLD, RAISE))
|
|
@example(bidict({1: 1, 2: 2}), [(3, 1), (2, 4)], OnDup(RAISE, DROP_OLD))
|
|
@example(bidict({1: 1, 2: 2}), [(1, 2), (1, 1)], OnDup(RAISE, RAISE, DROP_OLD))
|
|
@example(OrderedBidict({1: 1, 2: 2}), [(1, 3), (1, 2)], OnDup(DROP_OLD, RAISE))
|
|
@example(OrderedBidict({1: 1, 2: 2}), [(3, 1), (2, 4)], OnDup(RAISE, DROP_OLD))
|
|
@example(OrderedBidict({1: 1, 2: 2}), [(1, 2), (1, 1)], OnDup(RAISE, RAISE, DROP_OLD))
|
|
@example(OrderedBidict(), [(1, 1), (2, 2), (1, 2), (1, 1), (2, 1)], OnDup(DROP_OLD, RAISE, DROP_OLD))
|
|
@example(OrderedBidict(), [(1, 2), (2, 1), (1, 1), (1, 2)], OnDup(RAISE, DROP_NEW, DROP_OLD))
|
|
@example(OrderedBidict(), [(1, 1), (2, 1), (1, 1)], OnDup(DROP_NEW, DROP_OLD, DROP_NEW))
|
|
def test_putall_same_as_put_for_each_item(bi, items, on_dup):
|
|
"""*bi.putall(items) <==> for i in items: bi.put(i)* for all values of OnDup."""
|
|
check = bi.copy()
|
|
expect = bi.copy()
|
|
checkexc = None
|
|
expectexc = None
|
|
for (key, val) in items:
|
|
try:
|
|
expect.put(key, val, on_dup)
|
|
except BidictException as exc:
|
|
expectexc = type(exc)
|
|
expect = bi # Bulk updates fail clean -> roll back to original state.
|
|
break
|
|
try:
|
|
check.putall(items, on_dup)
|
|
except BidictException as exc:
|
|
checkexc = type(exc)
|
|
assert checkexc == expectexc
|
|
assert check == expect
|
|
assert check.inv == expect.inv
|
|
|
|
|
|
@given(st.BI_AND_MAP_FROM_SAME_ND_ITEMS)
|
|
def test_bidict_iter(bi_and_mapping):
|
|
"""iter(bi) should yield the keys in a bidict in insertion order."""
|
|
bi, mapping = bi_and_mapping
|
|
assert all(i == j for (i, j) in zip(bi, mapping))
|
|
|
|
|
|
@given(st.RBI_AND_RMAP_FROM_SAME_ND_ITEMS)
|
|
def test_bidict_reversed(rb_and_rd):
|
|
"""reversed(bi) should yield the keys in a bidict in reverse insertion order."""
|
|
rb, rd = rb_and_rd
|
|
assert all(i == j for (i, j) in zip(reversed(rb), reversed(rd)))
|
|
|
|
|
|
@given(st.FROZEN_BIDICTS)
|
|
def test_frozenbidicts_hashable(bi):
|
|
"""Frozen bidicts can be hashed and inserted into sets and mappings."""
|
|
assert hash(bi)
|
|
assert {bi}
|
|
assert {bi: bi}
|
|
|
|
|
|
@given(st.NAMEDBIDICT_NAMES_SOME_INVALID)
|
|
def test_namedbidict_raises_on_invalid_name(names):
|
|
""":func:`bidict.namedbidict` should raise if given invalid names."""
|
|
typename, keyname, valname = names
|
|
with pytest.raises(ValueError):
|
|
namedbidict(typename, keyname, valname)
|
|
|
|
|
|
@given(st.NAMEDBIDICT_NAMES_ALL_VALID)
|
|
def test_namedbidict_raises_on_same_keyname_as_valname(names):
|
|
""":func:`bidict.namedbidict` should raise if given same keyname as valname."""
|
|
typename, keyname, _ = names
|
|
with pytest.raises(ValueError):
|
|
namedbidict(typename, keyname, keyname)
|
|
|
|
|
|
@given(st.NAMEDBIDICT_NAMES_ALL_VALID, st.NON_BI_MAPPING_TYPES)
|
|
def test_namedbidict_raises_on_invalid_base_type(names, invalid_base_type):
|
|
""":func:`bidict.namedbidict` should raise if given a non-bidict base_type."""
|
|
with pytest.raises(TypeError):
|
|
namedbidict(*names, base_type=invalid_base_type)
|
|
|
|
|
|
@given(st.NAMEDBIDICTS)
|
|
def test_namedbidict(nb):
|
|
"""Test :func:`bidict.namedbidict` custom accessors."""
|
|
valfor = getattr(nb, nb.valname + '_for')
|
|
keyfor = getattr(nb, nb.keyname + '_for')
|
|
assert all(valfor[key] == val for (key, val) in nb.items())
|
|
assert all(keyfor[val] == key for (key, val) in nb.items())
|
|
# The same custom accessors should work on the inverse.
|
|
inv = nb.inv
|
|
valfor = getattr(inv, nb.valname + '_for')
|
|
keyfor = getattr(inv, nb.keyname + '_for')
|
|
assert all(valfor[key] == val for (key, val) in nb.items())
|
|
assert all(keyfor[val] == key for (key, val) in nb.items())
|
|
|
|
|
|
@require_cpython_refcounting
|
|
@given(st.BIDICT_TYPES)
|
|
def test_bidicts_freed_on_zero_refcount(bi_cls):
|
|
"""On CPython, the moment you have no more (strong) references to a bidict,
|
|
there are no remaining (internal) strong references to it
|
|
(i.e. no reference cycle was created between it and its inverse),
|
|
allowing the memory to be reclaimed immediately, even with GC disabled.
|
|
"""
|
|
gc.disable()
|
|
try:
|
|
bi = bi_cls()
|
|
weak = weakref.ref(bi)
|
|
assert weak() is not None
|
|
del bi
|
|
assert weak() is None
|
|
finally:
|
|
gc.enable()
|
|
|
|
|
|
@require_cpython_refcounting
|
|
@given(st.ORDERED_BIDICTS)
|
|
def test_orderedbidict_nodes_freed_on_zero_refcount(ob):
|
|
"""On CPython, the moment you have no more references to an ordered bidict,
|
|
the refcount of each of its internal nodes drops to 0
|
|
(i.e. the linked list of nodes does not create a reference cycle),
|
|
allowing the memory to be reclaimed immediately.
|
|
"""
|
|
gc.disable()
|
|
try:
|
|
# Make a local copy of the bidict passed in by hypothesis
|
|
# so that its refcount actually drops to 0 when we del it below.
|
|
ob = ob.copy()
|
|
nodes = weakref.WeakSet(ob._sntl.iternodes())
|
|
assert len(nodes) == len(ob)
|
|
del ob
|
|
assert len(nodes) == 0
|
|
finally:
|
|
gc.enable()
|
|
|
|
|
|
@given(st.ORDERED_BIDICTS)
|
|
def test_orderedbidict_nodes_consistent(ob):
|
|
"""The nodes in an ordered bidict's backing linked list should be the same as those in its backing mapping."""
|
|
mapnodes = set(ob._node_by_korv.inverse)
|
|
listnodes = set(ob._sntl.iternodes())
|
|
assert mapnodes == listnodes
|
|
|
|
|
|
def test_abc_slots():
|
|
"""Bidict ABCs should define __slots__.
|
|
|
|
Ref: https://docs.python.org/3/reference/datamodel.html#notes-on-using-slots
|
|
"""
|
|
assert BidirectionalMapping.__dict__['__slots__'] == ()
|
|
assert MutableBidirectionalMapping.__dict__['__slots__'] == ()
|
|
|
|
|
|
@given(st.BIDICTS)
|
|
def test_inv_aliases_inverse(bi):
|
|
"""bi.inv should alias bi.inverse."""
|
|
assert bi.inverse is bi.inv
|
|
assert bi.inv.inverse is bi.inverse.inv
|
|
|
|
|
|
@given(st.BIDICTS)
|
|
def test_inverse_readonly(bi):
|
|
"""Attempting to set the .inverse attribute should raise AttributeError."""
|
|
with pytest.raises(AttributeError):
|
|
bi.inverse = bi.__class__(inverted(bi))
|
|
with pytest.raises(AttributeError):
|
|
bi.inv = bi.__class__(inverted(bi))
|
|
|
|
|
|
@given(st.BIDICTS)
|
|
@example(BIDICT_TYPE_WHOSE_MODULE_HAS_REF_TO_INV_CLS({1: 'one'}).inverse)
|
|
@example(BIDICT_TYPE_WHOSE_MODULE_HAS_NO_REF_TO_INV_CLS({1: 'one'}).inverse)
|
|
def test_pickle(bi):
|
|
"""All bidicts should work with pickle."""
|
|
pickled = pickle.dumps(bi)
|
|
roundtripped = pickle.loads(pickled)
|
|
assert roundtripped is roundtripped.inv.inv
|
|
assert roundtripped.equals_order_sensitive(bi)
|
|
assert roundtripped.inv.equals_order_sensitive(bi.inv)
|
|
assert roundtripped.inv.inv.equals_order_sensitive(bi.inv.inv)
|
|
assert dict(roundtripped) == dict(bi)
|
|
roundtripped_inv = pickle.loads(pickle.dumps(bi.inv))
|
|
assert roundtripped_inv.equals_order_sensitive(bi.inv)
|
|
assert roundtripped_inv.inv.equals_order_sensitive(bi)
|
|
assert roundtripped_inv.inv.inv.equals_order_sensitive(bi.inv)
|
|
assert dict(roundtripped_inv) == dict(bi.inv)
|
|
|
|
|
|
def test_pickle_orderedbi_whose_order_disagrees_w_fwdm():
|
|
"""An OrderedBidict whose order does not match its _fwdm's should pickle with the correct order."""
|
|
ob = OrderedBidict({0: 1, 2: 3})
|
|
# First get ob._fwdm's order to disagree with ob's, and confirm:
|
|
ob.inverse[1] = 4
|
|
assert next(iter(ob.items())) == (4, 1)
|
|
assert next(iter(ob.inverse.items())) == (1, 4)
|
|
assert next(iter(ob._fwdm.items())) == (2, 3)
|
|
# Now check that its order is preserved after pickling and unpickling:
|
|
roundtripped = pickle.loads(pickle.dumps(ob))
|
|
assert roundtripped.equals_order_sensitive(ob)
|
|
|
|
|
|
class _UserBidict(bidict):
|
|
"""See :func:`test_pickle_dynamically_generated_inverse_bidict` below."""
|
|
_invm_cls = UserDict
|
|
|
|
|
|
def test_pickle_dynamically_generated_inverse_bidict():
|
|
"""Even instances of dynamically-generated inverse bidict classes should be pickleable."""
|
|
# The @example(BIDICT_TYPE_WHOSE_MODULE_HAS_NO_REF_TO_INV_CLS...) in test_pickle above
|
|
# covers this, but this is an even more explicit test for clarity.
|
|
|
|
# First pickle a non-inverse instance (whose class we have a direct reference to).
|
|
ub = _UserBidict(one=1, two=2)
|
|
roundtripped = pickle.loads(pickle.dumps(ub))
|
|
assert roundtripped == ub == _UserBidict({'one': 1, 'two': 2})
|
|
assert dict(roundtripped) == dict(ub)
|
|
|
|
# Now for the inverse:
|
|
assert repr(ub.inverse) == "_UserBidictInv({1: 'one', 2: 'two'})"
|
|
# We can still pickle the inverse, even though its class, _UserBidictInv, was
|
|
# dynamically generated, and we didn't save a reference to it named "_UserBidictInv"
|
|
# anywhere that pickle could find it in sys.modules:
|
|
ubinv = pickle.loads(pickle.dumps(ub.inverse))
|
|
assert repr(ubinv) == "_UserBidictInv({1: 'one', 2: 'two'})"
|
|
inv_cls = ub._inv_cls
|
|
assert inv_cls not in globals().values()
|
|
inv_cls_name = ub._inv_cls.__name__
|
|
assert inv_cls_name not in (name for m in sys.modules for name in dir(m))
|
|
|
|
|
|
@given(st.BIDICTS)
|
|
def test_copy(bi):
|
|
"""A bidict should equal its copy."""
|
|
cp = bi.copy()
|
|
assert cp is not bi
|
|
assert cp.inv is not bi.inv
|
|
assert bi == cp
|
|
assert bi.inv == cp.inv
|
|
collect = list if isinstance(bi, ORDER_PRESERVING_BIDICT_TYPES) else set
|
|
assert collect(bi.items()) == collect(cp.items())
|
|
assert collect(bi.inv.items()) == collect(cp.inv.items())
|
|
|
|
|
|
@given(st.BIDICTS)
|
|
def test_deepcopy(bi):
|
|
"""A bidict should equal its deepcopy."""
|
|
cp = deepcopy(bi)
|
|
assert cp is not bi
|
|
assert cp.inv is not bi.inv
|
|
assert cp.inv.inv is cp
|
|
assert cp.inv.inv is not bi
|
|
assert bi == cp
|
|
assert bi.inv == cp.inv
|
|
collect = list if isinstance(bi, ORDER_PRESERVING_BIDICT_TYPES) else set
|
|
assert collect(bi.items()) == collect(cp.items())
|
|
assert collect(bi.inv.items()) == collect(cp.inv.items())
|
|
|
|
|
|
def test_iteritems_raises_on_too_many_args():
|
|
""":func:`iteritems` should raise if given too many arguments."""
|
|
with pytest.raises(TypeError):
|
|
iteritems('too', 'many', 'args') # pylint: disable=too-many-function-args
|
|
|
|
|
|
@given(st.I_PAIRS, st.DICTS_KW_PAIRS)
|
|
def test_iteritems(arg0, kw):
|
|
""":func:`iteritems` should work correctly."""
|
|
arg0_1, arg0_2 = tee(arg0)
|
|
it = iteritems(arg0_1, **kw)
|
|
# Consume the first `len(arg0)` pairs, checking that they match `arg0`.
|
|
assert all(check == expect for (check, expect) in zip(it, arg0_2))
|
|
with pytest.raises(StopIteration):
|
|
next(arg0_1) # Iterating `it` should have consumed all of `arg0_1`.
|
|
# Consume the remaining pairs, checking that they match `kw`.
|
|
# Once min PY version required is higher, can check that the order matches `kw` too.
|
|
assert all(kw[k] == v for (k, v) in it)
|
|
with pytest.raises(StopIteration):
|
|
next(it)
|
|
|
|
|
|
@given(st.L_PAIRS)
|
|
def test_inverted_pairs(pairs):
|
|
""":func:`bidict.inverted` should yield the inverses of a list of pairs."""
|
|
inv = [(v, k) for (k, v) in pairs]
|
|
assert list(inverted(pairs)) == inv
|
|
assert list(inverted(inverted(pairs))) == pairs
|
|
|
|
|
|
@given(st.BI_AND_MAP_FROM_SAME_ND_ITEMS)
|
|
def test_inverted_bidict(bi_and_mapping):
|
|
""":func:`bidict.inverted` should yield the inverse items of an ordered bidict."""
|
|
bi, mapping = bi_and_mapping
|
|
mapping_inv = {v: k for (k, v) in mapping.items()}
|
|
assert all(i == j for (i, j) in zip(inverted(bi), mapping_inv.items()))
|
|
assert all(i == j for (i, j) in zip(inverted(inverted(bi)), mapping.items()))
|
|
|
|
|
|
_SET_OPS = (
|
|
op.le, op.lt, op.gt, op.ge, op.eq, op.ne, op.and_, op.or_, op.sub, op.xor, (lambda x, y: x.isdisjoint(y)),
|
|
)
|
|
|
|
|
|
@given(st.BIDICTS, st.DATA)
|
|
def test_views(bi, data):
|
|
"""Optimized view APIs should be equivalent to using the corresponding MappingViews from :mod:`collections.abc`."""
|
|
for check, oracle in (bi.keys(), KeysView(bi)), (bi.values(), ValuesView(bi)), (bi.items(), ItemsView(bi)):
|
|
# 0-arity methods: __len__, __iter__
|
|
assert check.__len__() == oracle.__len__()
|
|
assert list(check.__iter__()) == list(oracle.__iter__())
|
|
# 1-arity methods: __contains__
|
|
arg = data.draw(st.PAIRS if isinstance(oracle, ItemsView) else st.ATOMS)
|
|
assert check.__contains__(arg) == oracle.__contains__(arg)
|
|
# Methods of set-like views
|
|
if isinstance(oracle, ValuesView):
|
|
continue
|
|
arg = data.draw(st.KEYSVIEW_SET_OP_ARGS if isinstance(oracle, KeysView) else st.ITEMSVIEW_SET_OP_ARGS)
|
|
for so in _SET_OPS:
|
|
try:
|
|
expect = so(oracle, arg)
|
|
except TypeError:
|
|
with pytest.raises(TypeError):
|
|
so(check, arg)
|
|
else:
|
|
check_ = so(check, arg)
|
|
assert check_ == expect, (check, so, arg)
|
|
try:
|
|
expect = so(arg, oracle)
|
|
except TypeError:
|
|
with pytest.raises(TypeError):
|
|
so(arg, check)
|
|
else:
|
|
check_ = so(arg, check)
|
|
assert check_ == expect, (check, so, arg)
|