2017-11-20 03:24:08 +00:00
|
|
|
# -*- coding: utf-8 -*-
|
2018-01-01 03:51:19 +00:00
|
|
|
# Copyright 2018 Joshua Bronson. All Rights Reserved.
|
2017-11-20 03:24:08 +00:00
|
|
|
#
|
|
|
|
# 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/.
|
|
|
|
|
2017-11-21 15:35:51 +00:00
|
|
|
"""Property-based tests using https://hypothesis.readthedocs.io."""
|
2015-03-22 18:21:15 +00:00
|
|
|
|
2017-12-18 02:42:46 +00:00
|
|
|
import gc
|
2018-02-19 07:53:03 +00:00
|
|
|
import pickle
|
2018-02-24 05:35:44 +00:00
|
|
|
from collections import Mapping, MutableMapping, OrderedDict
|
2016-01-13 16:54:29 +00:00
|
|
|
from os import getenv
|
2017-12-18 02:42:46 +00:00
|
|
|
from weakref import ref
|
2017-11-16 20:44:51 +00:00
|
|
|
|
2016-06-28 04:05:22 +00:00
|
|
|
import pytest
|
2018-02-25 23:44:21 +00:00
|
|
|
from hypothesis import assume, given, settings, strategies as strat
|
2017-11-16 20:44:51 +00:00
|
|
|
from bidict import (
|
2018-02-25 23:44:21 +00:00
|
|
|
BidictException,
|
2017-11-16 20:44:51 +00:00
|
|
|
IGNORE, OVERWRITE, RAISE,
|
2017-12-06 19:22:32 +00:00
|
|
|
bidict, namedbidict, OrderedBidict,
|
2017-11-20 03:24:08 +00:00
|
|
|
frozenbidict, FrozenOrderedBidict)
|
2018-02-25 23:44:21 +00:00
|
|
|
from bidict.compat import PY2, PYPY, iterkeys, itervalues, iteritems
|
2016-01-13 16:54:29 +00:00
|
|
|
|
|
|
|
|
2017-11-20 03:24:08 +00:00
|
|
|
settings.register_profile('default', settings(max_examples=200, deadline=None))
|
2016-01-13 16:54:29 +00:00
|
|
|
settings.load_profile(getenv('HYPOTHESIS_PROFILE', 'default'))
|
2015-03-22 18:21:15 +00:00
|
|
|
|
|
|
|
|
2018-02-25 23:44:21 +00:00
|
|
|
def inv_od(items):
|
|
|
|
"""An OrderedDict containing the inverse of each item in *items*."""
|
2016-06-28 04:05:22 +00:00
|
|
|
return OrderedDict((v, k) for (k, v) in items)
|
2015-04-29 18:29:33 +00:00
|
|
|
|
2015-12-21 03:05:20 +00:00
|
|
|
|
2018-02-25 23:44:21 +00:00
|
|
|
def ensure_no_dup(items):
|
|
|
|
"""Given some hypothesis-generated items, prune any with duplicated keys or values."""
|
|
|
|
pruned = list(iteritems(inv_od(iteritems(inv_od(items)))))
|
2016-07-03 15:34:32 +00:00
|
|
|
assume(len(pruned) >= len(items) // 2)
|
|
|
|
return pruned
|
2015-12-21 03:05:20 +00:00
|
|
|
|
|
|
|
|
2018-02-25 23:44:21 +00:00
|
|
|
def ensure_dup(key=False, val=False):
|
|
|
|
"""Return a function that takes some hypothesis-generated items
|
|
|
|
and ensures they contain the specified type of duplication."""
|
|
|
|
assert key or val
|
|
|
|
def _wrapped(items): # noqa: E306 (expected 1 blank line before a nested definition)
|
|
|
|
fwd = dict(items)
|
|
|
|
if key:
|
|
|
|
assume(len(fwd) < len(items))
|
|
|
|
if val:
|
|
|
|
inv = dict((v, k) for (k, v) in items)
|
|
|
|
assume(len(inv) < len(items))
|
|
|
|
if key and val:
|
|
|
|
invinv = dict((v, k) for (k, v) in iteritems(inv))
|
|
|
|
# If an item has a duplicate key and val, they must duplicate two other distinct items.
|
|
|
|
assume(len(invinv) < len(fwd))
|
|
|
|
return items
|
|
|
|
return _wrapped
|
|
|
|
|
|
|
|
|
2017-12-06 19:22:32 +00:00
|
|
|
MyNamedBidict = namedbidict('MyNamedBidict', 'key', 'val')
|
2018-02-25 23:44:21 +00:00
|
|
|
MyNamedFrozenBidict = namedbidict('MyNamedBidict', 'key', 'val', base_type=frozenbidict)
|
|
|
|
MUTABLE_BIDICT_TYPES = (bidict, OrderedBidict, MyNamedBidict)
|
|
|
|
IMMUTABLE_BIDICT_TYPES = (frozenbidict, FrozenOrderedBidict, MyNamedFrozenBidict)
|
|
|
|
BIDICT_TYPES = MUTABLE_BIDICT_TYPES + IMMUTABLE_BIDICT_TYPES
|
|
|
|
HS_BIDICT_TYPES = strat.sampled_from(BIDICT_TYPES)
|
|
|
|
HS_MUTABLE_BIDICT_TYPES = strat.sampled_from(MUTABLE_BIDICT_TYPES)
|
2018-02-24 05:35:44 +00:00
|
|
|
|
2018-02-25 23:44:21 +00:00
|
|
|
HS_DUP_POLICIES = strat.sampled_from((IGNORE, OVERWRITE, RAISE))
|
|
|
|
HS_BOOLEANS = strat.booleans()
|
|
|
|
HS_IMMUTABLES = HS_BOOLEANS | strat.none() | strat.integers()
|
|
|
|
HS_PAIRS = strat.tuples(HS_IMMUTABLES, HS_IMMUTABLES)
|
|
|
|
HS_LISTS_PAIRS = strat.lists(HS_PAIRS)
|
|
|
|
HS_LISTS_PAIRS_NODUP = HS_LISTS_PAIRS.map(ensure_no_dup)
|
|
|
|
HS_LISTS_PAIRS_DUP = (
|
|
|
|
HS_LISTS_PAIRS.map(ensure_dup(key=True)) |
|
|
|
|
HS_LISTS_PAIRS.map(ensure_dup(val=True)) |
|
|
|
|
HS_LISTS_PAIRS.map(ensure_dup(key=True, val=True)))
|
|
|
|
HS_METHOD_ARGS = strat.sampled_from((
|
|
|
|
# 0-arity
|
|
|
|
('clear', ()),
|
|
|
|
('popitem', ()),
|
|
|
|
# 1-arity
|
|
|
|
('__delitem__', (HS_IMMUTABLES,)),
|
|
|
|
('pop', (HS_IMMUTABLES,)),
|
|
|
|
('setdefault', (HS_IMMUTABLES,)),
|
|
|
|
('move_to_end', (HS_IMMUTABLES,)),
|
|
|
|
('update', (HS_LISTS_PAIRS,)),
|
|
|
|
('forceupdate', (HS_LISTS_PAIRS,)),
|
|
|
|
# 2-arity
|
|
|
|
('pop', (HS_IMMUTABLES, HS_IMMUTABLES)),
|
|
|
|
('setdefault', (HS_IMMUTABLES, HS_IMMUTABLES)),
|
|
|
|
('move_to_end', (HS_IMMUTABLES, HS_BOOLEANS)),
|
|
|
|
('__setitem__', (HS_IMMUTABLES, HS_IMMUTABLES)),
|
|
|
|
('put', (HS_IMMUTABLES, HS_IMMUTABLES)),
|
|
|
|
('forceput', (HS_IMMUTABLES, HS_IMMUTABLES)),
|
|
|
|
))
|
2018-02-24 05:35:44 +00:00
|
|
|
|
2018-02-25 23:44:21 +00:00
|
|
|
|
|
|
|
def assert_items_match(map1, map2, assertmsg=None):
|
|
|
|
"""Ensure map1 and map2 contain the same items (and in the same order, if they're ordered)."""
|
|
|
|
if assertmsg is None:
|
|
|
|
assertmsg = repr((map1, map2))
|
|
|
|
both_ordered = all(isinstance(m, (OrderedDict, FrozenOrderedBidict)) for m in (map1, map2))
|
|
|
|
canon = list if both_ordered else set
|
|
|
|
assert canon(iteritems(map1)) == canon(iteritems(map2)), assertmsg
|
|
|
|
|
|
|
|
|
|
|
|
@given(bi_cls=HS_BIDICT_TYPES, init=HS_LISTS_PAIRS_NODUP)
|
|
|
|
def test_eq_ne(bi_cls, init):
|
|
|
|
"""Test == and != comparison between bidicts and other objects."""
|
|
|
|
some_bidict = bi_cls(init)
|
|
|
|
equal_dict = dict(init)
|
|
|
|
equal_odict = OrderedDict(init)
|
|
|
|
inv_odict = inv_od(iteritems(equal_odict))
|
|
|
|
inv_dict = dict(inv_odict)
|
|
|
|
assert some_bidict == equal_dict
|
|
|
|
assert some_bidict == equal_odict
|
|
|
|
assert not some_bidict != equal_dict
|
|
|
|
assert not some_bidict != equal_odict
|
|
|
|
assert some_bidict.inv == inv_dict
|
|
|
|
assert some_bidict.inv == inv_odict
|
|
|
|
assert not some_bidict.inv != inv_dict
|
|
|
|
assert not some_bidict.inv != inv_odict
|
|
|
|
unequal_odict = OrderedDict(equal_odict, new_key='new_val')
|
|
|
|
unequal_dict = dict(unequal_odict)
|
|
|
|
assert some_bidict != unequal_dict
|
|
|
|
assert some_bidict != unequal_odict
|
|
|
|
assert not some_bidict == unequal_dict
|
|
|
|
assert not some_bidict == unequal_odict
|
|
|
|
# Test comparison with a non-Mapping too.
|
|
|
|
assert not some_bidict == 'not a mapping'
|
|
|
|
assert not some_bidict.inv == 'not a mapping'
|
|
|
|
assert some_bidict != 'not a mapping'
|
|
|
|
assert some_bidict.inv != 'not a mapping'
|
|
|
|
|
|
|
|
|
|
|
|
@given(bi_cls=HS_BIDICT_TYPES, init=HS_LISTS_PAIRS_NODUP)
|
|
|
|
def test_bijectivity(bi_cls, init):
|
|
|
|
"""*b[k] == v <==> b.inv[v] == k*"""
|
|
|
|
some_bidict = bi_cls(init)
|
|
|
|
ordered = issubclass(bi_cls, FrozenOrderedBidict)
|
|
|
|
canon = list if ordered else set
|
|
|
|
keys = canon(iterkeys(some_bidict))
|
|
|
|
vals = canon(itervalues(some_bidict))
|
|
|
|
fwd_by_keys = canon(some_bidict[k] for k in iterkeys(some_bidict))
|
|
|
|
inv_by_vals = canon(some_bidict.inv[v] for v in itervalues(some_bidict))
|
|
|
|
assert keys == inv_by_vals
|
|
|
|
assert vals == fwd_by_keys
|
|
|
|
inv = some_bidict.inv
|
|
|
|
inv_keys = canon(iterkeys(inv))
|
|
|
|
inv_vals = canon(itervalues(inv))
|
|
|
|
inv_fwd_by_keys = canon(inv[k] for k in iterkeys(inv))
|
|
|
|
inv_inv_by_vals = canon(inv.inv[v] for v in itervalues(inv))
|
|
|
|
assert inv_keys == inv_inv_by_vals
|
|
|
|
assert inv_vals == inv_fwd_by_keys
|
|
|
|
|
|
|
|
|
|
|
|
@given(bi_cls=HS_MUTABLE_BIDICT_TYPES, init=HS_LISTS_PAIRS_NODUP,
|
|
|
|
method_args=HS_METHOD_ARGS, data=strat.data())
|
|
|
|
def test_consistency_after_mutation(bi_cls, init, method_args, data):
|
|
|
|
"""Call every mutating method on every bidict that implements it,
|
2017-11-21 15:35:51 +00:00
|
|
|
and ensure the bidict is left in a consistent state afterward.
|
|
|
|
"""
|
2018-02-25 23:44:21 +00:00
|
|
|
methodname, hs_args = method_args
|
|
|
|
method = getattr(bi_cls, methodname, None)
|
2016-06-28 04:05:22 +00:00
|
|
|
if not method:
|
|
|
|
return
|
2018-02-25 23:44:21 +00:00
|
|
|
args = tuple(data.draw(i) for i in hs_args)
|
|
|
|
bi_init = bi_cls(init)
|
|
|
|
bi_clone = bi_init.copy()
|
|
|
|
assert_items_match(bi_init, bi_clone)
|
2016-06-28 04:05:22 +00:00
|
|
|
try:
|
2018-02-25 23:44:21 +00:00
|
|
|
method(bi_clone, *args)
|
|
|
|
except (KeyError, BidictException) as exc:
|
|
|
|
# Call should fail clean, i.e. bi_clone should be in the same state it was before the call.
|
|
|
|
assertmsg = '%r did not fail clean: %r' % (method, exc)
|
|
|
|
assert_items_match(bi_clone, bi_init, assertmsg)
|
|
|
|
assert_items_match(bi_clone.inv, bi_init.inv, assertmsg)
|
|
|
|
# Whether the call failed or succeeded, bi_clone should pass consistency checks.
|
|
|
|
assert len(bi_clone) == sum(1 for _ in iteritems(bi_clone))
|
|
|
|
assert len(bi_clone) == sum(1 for _ in iteritems(bi_clone.inv))
|
|
|
|
assert_items_match(bi_clone, dict(bi_clone))
|
|
|
|
assert_items_match(bi_clone.inv, dict(bi_clone.inv))
|
|
|
|
assert_items_match(bi_clone, inv_od(iteritems(bi_clone.inv)))
|
|
|
|
assert_items_match(bi_clone.inv, inv_od(iteritems(bi_clone)))
|
|
|
|
|
|
|
|
|
|
|
|
@given(bi_cls=HS_MUTABLE_BIDICT_TYPES, init=HS_LISTS_PAIRS_NODUP, items=HS_LISTS_PAIRS_DUP,
|
|
|
|
on_dup_key=HS_DUP_POLICIES, on_dup_val=HS_DUP_POLICIES, on_dup_kv=HS_DUP_POLICIES)
|
|
|
|
def test_dup_policies_bulk(bi_cls, init, items, on_dup_key, on_dup_val, on_dup_kv):
|
|
|
|
"""Attempting a bulk update with *items* should yield the same result as
|
|
|
|
attempting to set each of the items sequentially
|
|
|
|
while respecting the duplication policies that are in effect.
|
|
|
|
"""
|
|
|
|
bi_init = bi_cls(init)
|
|
|
|
expect = bi_init.copy()
|
2016-06-28 04:05:22 +00:00
|
|
|
expectexc = None
|
2018-02-25 23:44:21 +00:00
|
|
|
for (key, val) in items:
|
2016-06-28 04:05:22 +00:00
|
|
|
try:
|
2018-02-25 23:44:21 +00:00
|
|
|
expect.put(key, val, on_dup_key=on_dup_key, on_dup_val=on_dup_val, on_dup_kv=on_dup_kv)
|
|
|
|
except BidictException as exc:
|
2017-11-16 20:44:51 +00:00
|
|
|
expectexc = exc
|
2018-02-25 23:44:21 +00:00
|
|
|
expect = bi_init # bulk updates fail clean
|
2016-06-28 04:05:22 +00:00
|
|
|
break
|
2018-02-25 23:44:21 +00:00
|
|
|
check = bi_init.copy()
|
2016-06-28 04:05:22 +00:00
|
|
|
checkexc = None
|
|
|
|
try:
|
|
|
|
check.putall(items, on_dup_key=on_dup_key, on_dup_val=on_dup_val, on_dup_kv=on_dup_kv)
|
2018-02-25 23:44:21 +00:00
|
|
|
except BidictException as exc:
|
2017-11-16 20:44:51 +00:00
|
|
|
checkexc = exc
|
2018-02-25 23:44:21 +00:00
|
|
|
assert type(checkexc) == type(expectexc) # pylint: disable=unidiomatic-typecheck
|
|
|
|
assert_items_match(check, expect)
|
|
|
|
assert_items_match(check.inv, expect.inv)
|
2017-12-18 02:42:46 +00:00
|
|
|
|
|
|
|
|
2018-02-25 23:44:21 +00:00
|
|
|
# Skip this test on PyPy where reference counting isn't used to free objects immediately. See:
|
|
|
|
# http://doc.pypy.org/en/latest/cpython_differences.html#differences-related-to-garbage-collection-strategies
|
2017-12-18 02:42:46 +00:00
|
|
|
# "It also means that weak references may stay alive for a bit longer than expected."
|
2018-02-25 23:44:21 +00:00
|
|
|
@pytest.mark.skipif(PYPY, reason='objects with 0 refcount not freed immediately on PyPy')
|
|
|
|
@given(bi_cls=HS_BIDICT_TYPES)
|
|
|
|
def test_no_reference_cycles(bi_cls):
|
|
|
|
"""When you delete your last strong reference to a bidict,
|
|
|
|
there are no remaining strong references to it
|
|
|
|
(e.g. no reference cycle was created between it and its inverse)
|
|
|
|
so its memory can be reclaimed immediately.
|
|
|
|
"""
|
2017-12-18 02:42:46 +00:00
|
|
|
gc.disable()
|
2018-02-25 23:44:21 +00:00
|
|
|
some_bidict = bi_cls()
|
|
|
|
weak = ref(some_bidict)
|
2017-12-18 02:42:46 +00:00
|
|
|
assert weak() is not None
|
2018-02-25 23:44:21 +00:00
|
|
|
del some_bidict
|
2017-12-18 02:42:46 +00:00
|
|
|
assert weak() is None
|
|
|
|
gc.enable()
|
2018-02-25 23:44:21 +00:00
|
|
|
|
|
|
|
|
|
|
|
@given(bi_cls=HS_BIDICT_TYPES)
|
|
|
|
def test_slots(bi_cls):
|
|
|
|
"""See https://docs.python.org/3/reference/datamodel.html#notes-on-using-slots."""
|
|
|
|
stop_at = {object}
|
|
|
|
if PY2:
|
|
|
|
stop_at.update({Mapping, MutableMapping}) # These don't define __slots__ in Python 2.
|
|
|
|
cls_by_slot = {}
|
|
|
|
for cls in bi_cls.__mro__:
|
|
|
|
if cls in stop_at:
|
|
|
|
break
|
|
|
|
slots = cls.__dict__.get('__slots__')
|
|
|
|
assert slots is not None, 'Expected %r to define __slots__' % cls
|
|
|
|
for slot in slots:
|
|
|
|
seen_at = cls_by_slot.get(slot)
|
|
|
|
assert not seen_at, '%r repeats slot %r declared first by %r' % (seen_at, slot, cls)
|
|
|
|
cls_by_slot[slot] = cls
|
|
|
|
|
|
|
|
|
|
|
|
@given(bi_cls=HS_BIDICT_TYPES, init=HS_LISTS_PAIRS_NODUP)
|
|
|
|
def test_pickle_roundtrips(bi_cls, init):
|
|
|
|
"""A bidict should equal the result of unpickling its pickle."""
|
|
|
|
some_bidict = bi_cls(init)
|
|
|
|
dumps_args = {}
|
|
|
|
# Pickling ordered bidicts in Python 2 requires a higher (non-default) protocol version.
|
|
|
|
if PY2 and issubclass(bi_cls, FrozenOrderedBidict):
|
|
|
|
dumps_args['protocol'] = 2
|
|
|
|
pickled = pickle.dumps(some_bidict, **dumps_args)
|
|
|
|
roundtripped = pickle.loads(pickled)
|
|
|
|
assert roundtripped == some_bidict
|