diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a7427781..8c1925a4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -31,7 +31,9 @@ repos: rev: v0.991 hooks: - id: mypy - exclude: setup.py|docs/conf.py|tests + additional_dependencies: + - hypothesis + - pytest - repo: https://github.com/charliermarsh/ruff-pre-commit rev: v0.0.192 diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 6b6f6d5d..9d5312a8 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -27,6 +27,18 @@ please consider sponsoring bidict on GitHub.` Click the "Watch" dropdown, choose "Custom", and then choose "Releases". +0.22.1 (2022-12-31) +------------------- + +- Only include the source code in the source distribution. + This reduces the size of the source distribution + from ~200kB to ~30kB. + +- Fix the return type hint of :func:`bidict.inverted` + to return an :class:`~collections.abc.Iterator`, + rather than an :class:`~collections.abc.Iterable`. + + 0.22.0 (2022-03-23) ------------------- diff --git a/bidict/_base.py b/bidict/_base.py index ebe1d2ad..97be9d49 100644 --- a/bidict/_base.py +++ b/bidict/_base.py @@ -26,7 +26,7 @@ from ._dup import ON_DUP_DEFAULT, RAISE, DROP_OLD, DROP_NEW, OnDup from ._exc import DuplicationError, KeyDuplicationError, ValueDuplicationError, KeyAndValueDuplicationError from ._iter import iteritems, inverted -from ._typing import KT, VT, MISSING, OKT, OVT, IterItems, MapOrIterItems, TypeAlias +from ._typing import KT, VT, MISSING, OKT, OVT, Items, MapOrItems, TypeAlias OldKV: TypeAlias = 'tuple[OKT[KT], OVT[VT]]' @@ -44,7 +44,7 @@ class BidictKeysView(t.KeysView[KT], t.ValuesView[KT]): """ -def get_arg(*args: MapOrIterItems[KT, VT]) -> MapOrIterItems[KT, VT]: +def get_arg(*args: MapOrItems[KT, VT]) -> MapOrItems[KT, VT]: """Ensure there's only a single arg in *args*, then return it.""" if len(args) > 1: raise TypeError(f'Expected at most 1 positional argument, got {len(args)}') @@ -142,9 +142,9 @@ def __init__(self, **kw: VT) -> None: ... @t.overload def __init__(self, __m: t.Mapping[KT, VT], **kw: VT) -> None: ... @t.overload - def __init__(self, __i: IterItems[KT, VT], **kw: VT) -> None: ... + def __init__(self, __i: Items[KT, VT], **kw: VT) -> None: ... - def __init__(self, *args: MapOrIterItems[KT, VT], **kw: VT) -> None: + def __init__(self, *args: MapOrItems[KT, VT], **kw: VT) -> None: """Make a new bidirectional mapping. The signature behaves like that of :class:`dict`. Items passed in are added in the order they are passed, @@ -405,7 +405,7 @@ def _prep_write(self, newkey: KT, newval: VT, oldkey: OKT[KT], oldval: OVT[VT], def _update( self, - arg: MapOrIterItems[KT, VT], + arg: MapOrItems[KT, VT], kw: t.Mapping[str, VT] = MappingProxyType({}), *, rbof: bool | None = None, @@ -470,7 +470,7 @@ def copy(self: BT) -> BT: return self._from_other(self.__class__, self) @staticmethod - def _from_other(bt: t.Type[BT], other: MapOrIterItems[KT, VT], inv: bool = False) -> BT: + def _from_other(bt: t.Type[BT], other: MapOrItems[KT, VT], inv: bool = False) -> BT: """Fast, private constructor based on :meth:`_init_from`. If *inv* is true, return the inverse of the instance instead of the instance itself. @@ -480,7 +480,7 @@ def _from_other(bt: t.Type[BT], other: MapOrIterItems[KT, VT], inv: bool = False inst._init_from(other) return t.cast(BT, inst.inverse) if inv else inst - def _init_from(self, other: MapOrIterItems[KT, VT]) -> None: + def _init_from(self, other: MapOrItems[KT, VT]) -> None: """Fast init from *other*, bypassing item-by-item duplication checking.""" self._fwdm.clear() self._invm.clear() diff --git a/bidict/_bidict.py b/bidict/_bidict.py index 2d5c5b66..2cd1f54e 100644 --- a/bidict/_bidict.py +++ b/bidict/_bidict.py @@ -20,7 +20,7 @@ from ._abc import MutableBidirectionalMapping from ._base import BidictBase, get_arg from ._dup import OnDup, ON_DUP_RAISE, ON_DUP_DROP_OLD -from ._typing import KT, VT, DT, ODT, MISSING, IterItems, MapOrIterItems +from ._typing import KT, VT, DT, ODT, MISSING, Items, MapOrItems class MutableBidict(BidictBase[KT, VT], MutableBidirectionalMapping[KT, VT]): @@ -139,11 +139,11 @@ def popitem(self) -> tuple[KT, VT]: @t.overload # type: ignore [override] # https://github.com/jab/bidict/pull/242#discussion_r825464731 def update(self, __m: t.Mapping[KT, VT], **kw: VT) -> None: ... @t.overload - def update(self, __i: IterItems[KT, VT], **kw: VT) -> None: ... + def update(self, __i: Items[KT, VT], **kw: VT) -> None: ... @t.overload def update(self, **kw: VT) -> None: ... - def update(self, *args: MapOrIterItems[KT, VT], **kw: VT) -> None: + def update(self, *args: MapOrItems[KT, VT], **kw: VT) -> None: """Like calling :meth:`putall` with *self.on_dup* passed for *on_dup*.""" if args or kw: self._update(get_arg(*args), kw) @@ -151,11 +151,11 @@ def update(self, *args: MapOrIterItems[KT, VT], **kw: VT) -> None: @t.overload def forceupdate(self, __m: t.Mapping[KT, VT], **kw: VT) -> None: ... @t.overload - def forceupdate(self, __i: IterItems[KT, VT], **kw: VT) -> None: ... + def forceupdate(self, __i: Items[KT, VT], **kw: VT) -> None: ... @t.overload def forceupdate(self, **kw: VT) -> None: ... - def forceupdate(self, *args: MapOrIterItems[KT, VT], **kw: VT) -> None: + def forceupdate(self, *args: MapOrItems[KT, VT], **kw: VT) -> None: """Like a bulk :meth:`forceput`.""" if args or kw: self._update(get_arg(*args), kw, on_dup=ON_DUP_DROP_OLD) @@ -168,9 +168,9 @@ def __ior__(self, other: t.Mapping[KT, VT]) -> MutableBidict[KT, VT]: @t.overload def putall(self, items: t.Mapping[KT, VT], on_dup: OnDup) -> None: ... @t.overload - def putall(self, items: IterItems[KT, VT], on_dup: OnDup = ...) -> None: ... + def putall(self, items: Items[KT, VT], on_dup: OnDup = ...) -> None: ... - def putall(self, items: MapOrIterItems[KT, VT], on_dup: OnDup = ON_DUP_RAISE) -> None: + def putall(self, items: MapOrItems[KT, VT], on_dup: OnDup = ON_DUP_RAISE) -> None: """Like a bulk :meth:`put`. If one of the given items causes an exception to be raised, diff --git a/bidict/_iter.py b/bidict/_iter.py index 84392db4..c06dc87e 100644 --- a/bidict/_iter.py +++ b/bidict/_iter.py @@ -11,15 +11,15 @@ from operator import itemgetter import typing as t -from ._typing import KT, VT, IterItems, MapOrIterItems +from ._typing import KT, VT, ItemsIter, MapOrItems -def iteritems_mapping_or_iterable(arg: MapOrIterItems[KT, VT]) -> IterItems[KT, VT]: +def iteritems_mapping_or_iterable(arg: MapOrItems[KT, VT]) -> ItemsIter[KT, VT]: """Yield the items in *arg* based on whether it's a mapping.""" yield from arg.items() if isinstance(arg, t.Mapping) else arg -def iteritems(__arg: MapOrIterItems[KT, VT], **kw: VT) -> IterItems[KT, VT]: +def iteritems(__arg: MapOrItems[KT, VT], **kw: VT) -> ItemsIter[KT, VT]: """Yield the items from *arg* and then any from *kw* in the order given.""" yield from iteritems_mapping_or_iterable(__arg) yield from kw.items() # type: ignore [misc] @@ -28,7 +28,7 @@ def iteritems(__arg: MapOrIterItems[KT, VT], **kw: VT) -> IterItems[KT, VT]: swap = itemgetter(1, 0) -def inverted(arg: MapOrIterItems[KT, VT]) -> IterItems[VT, KT]: +def inverted(arg: MapOrItems[KT, VT]) -> ItemsIter[VT, KT]: """Yield the inverse items of the provided object. If *arg* has a :func:`callable` ``__inverted__`` attribute, @@ -41,6 +41,6 @@ def inverted(arg: MapOrIterItems[KT, VT]) -> IterItems[VT, KT]: """ invattr = getattr(arg, '__inverted__', None) if callable(invattr): - inv: IterItems[VT, KT] = invattr() + inv: ItemsIter[VT, KT] = invattr() return inv return map(swap, iteritems_mapping_or_iterable(arg)) diff --git a/bidict/_orderedbase.py b/bidict/_orderedbase.py index 60b88680..2375e789 100644 --- a/bidict/_orderedbase.py +++ b/bidict/_orderedbase.py @@ -22,7 +22,7 @@ from ._base import BidictBase, PreparedWrite from ._bidict import bidict from ._iter import iteritems -from ._typing import KT, VT, OKT, OVT, MISSING, IterItems, MapOrIterItems +from ._typing import KT, VT, OKT, OVT, MISSING, Items, MapOrItems IT = t.TypeVar('IT') # instance type @@ -114,11 +114,11 @@ class OrderedBidictBase(BidictBase[KT, VT]): @t.overload def __init__(self, __m: t.Mapping[KT, VT], **kw: VT) -> None: ... @t.overload - def __init__(self, __i: IterItems[KT, VT], **kw: VT) -> None: ... + def __init__(self, __i: Items[KT, VT], **kw: VT) -> None: ... @t.overload def __init__(self, **kw: VT) -> None: ... - def __init__(self, *args: MapOrIterItems[KT, VT], **kw: VT) -> None: + def __init__(self, *args: MapOrItems[KT, VT], **kw: VT) -> None: """Make a new ordered bidirectional mapping. The signature behaves like that of :class:`dict`. Items passed in are added in the order they are passed, @@ -151,7 +151,7 @@ def _dissoc_node(self, node: Node) -> None: del self._node_by_korv.inverse[node] node.unlink() - def _init_from(self, other: MapOrIterItems[KT, VT]) -> None: + def _init_from(self, other: MapOrItems[KT, VT]) -> None: """See :meth:`BidictBase._init_from`.""" super()._init_from(other) bykey = self._bykey diff --git a/bidict/_typing.py b/bidict/_typing.py index beb86316..482267ed 100644 --- a/bidict/_typing.py +++ b/bidict/_typing.py @@ -21,8 +21,9 @@ VT = t.TypeVar('VT') -IterItems: TypeAlias = 't.Iterable[tuple[KT, VT]]' -MapOrIterItems: TypeAlias = 't.Mapping[KT, VT] | IterItems[KT, VT]' +Items: TypeAlias = 't.Iterable[tuple[KT, VT]]' +MapOrItems: TypeAlias = 't.Mapping[KT, VT] | Items[KT, VT]' +ItemsIter: TypeAlias = 't.Iterator[tuple[KT, VT]]' class MissingT(Enum): diff --git a/bidict/metadata.py b/bidict/metadata.py index 039ad939..00250fe3 100644 --- a/bidict/metadata.py +++ b/bidict/metadata.py @@ -7,7 +7,7 @@ """Define bidict package metadata.""" -__version__ = '0.22.1.dev0' +__version__ = '0.22.1' __author__ = {'name': 'Joshua Bronson', 'email': 'jabronson@gmail.com'} __copyright__ = '© 2009-2022 Joshua Bronson' __description__ = 'The bidirectional mapping library for Python.' diff --git a/run_tests.py b/run_tests.py index 1d5c8a7e..e2910257 100755 --- a/run_tests.py +++ b/run_tests.py @@ -22,4 +22,4 @@ partial(sphinx_main, '-b doctest -d docs/_build/doctrees docs docs/_build/doctest'.split()), ) -raise SystemExit(sum(bool(f()) for f in TEST_FUNCS)) +raise SystemExit(sum(bool(f()) for f in TEST_FUNCS)) # type: ignore diff --git a/tests/property_tests/_strategies.py b/tests/property_tests/_strategies.py index 717c3e34..9be8c1ee 100644 --- a/tests/property_tests/_strategies.py +++ b/tests/property_tests/_strategies.py @@ -6,31 +6,33 @@ """Strategies for Hypothesis tests.""" +from __future__ import annotations from collections import OrderedDict from operator import attrgetter, itemgetter, methodcaller +import typing as t import hypothesis.strategies as st from bidict import DROP_NEW, DROP_OLD, RAISE, OnDup, OrderedBidictBase, namedbidict -from . import _types as t +from . import _types as _t -def one_of(items): +def one_of(items: t.Any) -> t.Any: """Create a one_of strategy using the given items.""" - return st.one_of((st.just(i) for i in items)) + return st.one_of((st.just(i) for i in items)) # type: ignore [call-overload] DATA = st.data() -BIDICT_TYPES = one_of(t.BIDICT_TYPES) -MUTABLE_BIDICT_TYPES = one_of(t.MUTABLE_BIDICT_TYPES) -FROZEN_BIDICT_TYPES = one_of(t.FROZEN_BIDICT_TYPES) -ORDERED_BIDICT_TYPES = one_of(t.ORDERED_BIDICT_TYPES) -REVERSIBLE_BIDICT_TYPES = one_of(t.REVERSIBLE_BIDICT_TYPES) -MAPPING_TYPES = one_of(t.MAPPING_TYPES) -NON_BI_MAPPING_TYPES = one_of(t.NON_BI_MAPPING_TYPES) -NON_NAMED_BIDICT_TYPES = one_of(t.NON_NAMED_BIDICT_TYPES) -ORDERED_MAPPING_TYPES = one_of(t.ORDERED_MAPPING_TYPES) -HASHABLE_MAPPING_TYPES = one_of(t.HASHABLE_MAPPING_TYPES) +BIDICT_TYPES = one_of(_t.BIDICT_TYPES) +MUTABLE_BIDICT_TYPES = one_of(_t.MUTABLE_BIDICT_TYPES) +FROZEN_BIDICT_TYPES = one_of(_t.FROZEN_BIDICT_TYPES) +ORDERED_BIDICT_TYPES = one_of(_t.ORDERED_BIDICT_TYPES) +REVERSIBLE_BIDICT_TYPES = one_of(_t.REVERSIBLE_BIDICT_TYPES) +MAPPING_TYPES = one_of(_t.MAPPING_TYPES) +NON_BI_MAPPING_TYPES = one_of(_t.NON_BI_MAPPING_TYPES) +NON_NAMED_BIDICT_TYPES = one_of(_t.NON_NAMED_BIDICT_TYPES) +ORDERED_MAPPING_TYPES = one_of(_t.ORDERED_MAPPING_TYPES) +HASHABLE_MAPPING_TYPES = one_of(_t.HASHABLE_MAPPING_TYPES) ON_DUP_ACTIONS = one_of((DROP_NEW, DROP_OLD, RAISE)) ON_DUP = st.tuples(ON_DUP_ACTIONS, ON_DUP_ACTIONS, ON_DUP_ACTIONS).map(OnDup._make) @@ -63,8 +65,8 @@ def one_of(items): ).filter(lambda i: i[0] != i[1]) -def _bidict_strat(bi_types, init_items=I_PAIRS_NODUP, _inv=attrgetter('inverse')): - fwd_bidicts = st.tuples(bi_types, init_items).map(lambda i: i[0](i[1])) +def _bidict_strat(bi_types: t.Any, init_items: t.Any = I_PAIRS_NODUP, _inv: t.Any = attrgetter('inverse')) -> t.Any: + fwd_bidicts = st.tuples(bi_types, init_items).map(lambda i: i[0](i[1])) # type: ignore inv_bidicts = fwd_bidicts.map(_inv) return fwd_bidicts | inv_bidicts @@ -78,7 +80,7 @@ def _bidict_strat(bi_types, init_items=I_PAIRS_NODUP, _inv=attrgetter('inverse') KEYSVIEW_SET_OP_ARGS = st.sets(ATOMS) | st.dictionaries(ATOMS, ATOMS).map(callkeys) | BIDICTS.map(callkeys) ITEMSVIEW_SET_OP_ARGS = st.sets(PAIRS) | st.dictionaries(ATOMS, ATOMS).map(callitems) | BIDICTS.map(callitems) -NON_BI_MAPPINGS = st.tuples(NON_BI_MAPPING_TYPES, L_PAIRS).map(lambda i: i[0](i[1])) +NON_BI_MAPPINGS = st.tuples(NON_BI_MAPPING_TYPES, L_PAIRS).map(lambda i: i[0](i[1])) # type: ignore NAMEDBIDICT_NAMES_ALL_VALID = st.lists(VALID_NAMES, min_size=3, max_size=3, unique=True) @@ -91,7 +93,7 @@ def _bidict_strat(bi_types, init_items=I_PAIRS_NODUP, _inv=attrgetter('inverse') NAMEDBIDICTS = _bidict_strat(NAMEDBIDICT_TYPES) -def _bi_and_map(bi_types, map_types=MAPPING_TYPES, init_items=L_PAIRS_NODUP): +def _bi_and_map(bi_types: t.Any, map_types: t.Any = MAPPING_TYPES, init_items: t.Any = L_PAIRS_NODUP) -> t.Any: """Given bidict types and mapping types, return a pair of each type created from init_items.""" return st.tuples(bi_types, map_types, init_items).map( lambda i: (i[0](i[2]), i[1](i[2])) @@ -110,7 +112,7 @@ def _bi_and_map(bi_types, map_types=MAPPING_TYPES, init_items=L_PAIRS_NODUP): ORDERED_BIDICT_TYPES, ORDERED_MAPPING_TYPES, SAME_ITEMS_DIFF_ORDER ).map(_unpack) -_cmpdict = lambda i: (OrderedDict if isinstance(i, OrderedBidictBase) else dict) # noqa: E731 +_cmpdict: t.Any = lambda i: (OrderedDict if isinstance(i, OrderedBidictBase) else dict) # noqa: E731 BI_AND_CMPDICT_FROM_SAME_ITEMS = L_PAIRS_NODUP.map( lambda items: (lambda b: (b, _cmpdict(b)(items)))(_bidict_strat(BIDICT_TYPES, items)) ) diff --git a/tests/property_tests/_types.py b/tests/property_tests/_types.py index 6012ca5b..00290029 100644 --- a/tests/property_tests/_types.py +++ b/tests/property_tests/_types.py @@ -6,27 +6,32 @@ """Types for Hypothoses tests.""" +from __future__ import annotations from collections import OrderedDict, UserDict -from collections.abc import ItemsView, Mapping, Reversible +import typing as t -from bidict import FrozenOrderedBidict, OrderedBidict, bidict, frozenbidict, namedbidict +from bidict import BidictBase, FrozenOrderedBidict, OrderedBidict, bidict, frozenbidict, namedbidict +from bidict._typing import TypeAlias -class UserBidict(bidict): +BiTypesT: TypeAlias = t.Tuple[t.Type[BidictBase[t.Any, t.Any]], ...] + + +class UserBidict(bidict[t.Any, t.Any]): """Custom bidict subclass.""" _fwdm_cls = UserDict _invm_cls = UserDict -class UserOrderedBidict(OrderedBidict): +class UserOrderedBidict(OrderedBidict[t.Any, t.Any]): """Custom OrderedBidict subclass.""" _fwdm_cls = UserDict _invm_cls = UserDict -class UserBidictNotOwnInverse(bidict): +class UserBidictNotOwnInverse(bidict[t.Any, t.Any]): """Custom bidict subclass that is not its own inverse.""" _fwdm_cls = dict @@ -45,38 +50,38 @@ class UserBidictNotOwnInverse2(UserBidictNotOwnInverse): NamedFrozenBidict = namedbidict('NamedFrozenBidict', 'key', 'val', base_type=frozenbidict) NamedOrderedBidict = namedbidict('NamedOrderedBidict', 'key', 'val', base_type=OrderedBidict) NamedUserBidict = namedbidict('NamedUserBidict', 'key', 'val', base_type=UserBidict) -NAMED_BIDICT_TYPES = (NamedBidict, NamedFrozenBidict, NamedOrderedBidict, NamedUserBidict) - -MUTABLE_BIDICT_TYPES = (bidict, OrderedBidict, NamedBidict, UserBidict, UserOrderedBidict, UserBidictNotOwnInverse) -FROZEN_BIDICT_TYPES = (frozenbidict, FrozenOrderedBidict, NamedFrozenBidict) -ORDERED_BIDICT_TYPES = (OrderedBidict, FrozenOrderedBidict, NamedOrderedBidict, UserOrderedBidict) -ORDER_PRESERVING_BIDICT_TYPES = tuple(set(FROZEN_BIDICT_TYPES + ORDERED_BIDICT_TYPES)) -BIDICT_TYPES = tuple(set(MUTABLE_BIDICT_TYPES + FROZEN_BIDICT_TYPES + ORDERED_BIDICT_TYPES)) -NON_NAMED_BIDICT_TYPES = tuple(set(BIDICT_TYPES) - set(NAMED_BIDICT_TYPES)) +NAMED_BIDICT_TYPES: BiTypesT = (NamedBidict, NamedFrozenBidict, NamedOrderedBidict, NamedUserBidict) + +MUTABLE_BIDICT_TYPES: BiTypesT = (bidict, OrderedBidict, NamedBidict, UserBidict, UserOrderedBidict, UserBidictNotOwnInverse) +FROZEN_BIDICT_TYPES: BiTypesT = (frozenbidict, FrozenOrderedBidict, NamedFrozenBidict) +ORDERED_BIDICT_TYPES: BiTypesT = (OrderedBidict, FrozenOrderedBidict, NamedOrderedBidict, UserOrderedBidict) +ORDER_PRESERVING_BIDICT_TYPES: BiTypesT = tuple(set(FROZEN_BIDICT_TYPES + ORDERED_BIDICT_TYPES)) +BIDICT_TYPES: BiTypesT = tuple(set(MUTABLE_BIDICT_TYPES + FROZEN_BIDICT_TYPES + ORDERED_BIDICT_TYPES)) +NON_NAMED_BIDICT_TYPES: BiTypesT = tuple(set(BIDICT_TYPES) - set(NAMED_BIDICT_TYPES)) # When support is dropped for Python < 3.8, all bidict types will be reversible, # and we can remove the following and just use BIDICT_TYPES instead: -REVERSIBLE_BIDICT_TYPES = tuple(b for b in BIDICT_TYPES if issubclass(b, Reversible)) +REVERSIBLE_BIDICT_TYPES: BiTypesT = tuple(b for b in BIDICT_TYPES if issubclass(b, t.Reversible)) BIDICT_TYPE_WHOSE_MODULE_HAS_REF_TO_INV_CLS = UserBidictNotOwnInverse BIDICT_TYPE_WHOSE_MODULE_HAS_NO_REF_TO_INV_CLS = UserBidictNotOwnInverse2 -class _FrozenMap(Mapping): +class _FrozenMap(t.Mapping[t.Any, t.Any]): - def __init__(self, *args, **kw): + def __init__(self, *args: t.Any, **kw: t.Any) -> None: self._mapping = dict(*args, **kw) - def __iter__(self): + def __iter__(self) -> t.Iterator[t.Any]: return iter(self._mapping) - def __len__(self): + def __len__(self) -> int: return len(self._mapping) - def __getitem__(self, key): + def __getitem__(self, key: t.Any) -> t.Any: return self._mapping[key] - def __hash__(self): - return ItemsView(self._mapping)._hash() + def __hash__(self) -> int: + return t.ItemsView(self._mapping)._hash() NON_BI_MAPPING_TYPES = (dict, OrderedDict, _FrozenMap) diff --git a/tests/property_tests/test_properties.py b/tests/property_tests/test_properties.py index 265db191..8afc9b0f 100644 --- a/tests/property_tests/test_properties.py +++ b/tests/property_tests/test_properties.py @@ -6,31 +6,31 @@ """Property-based tests using https://hypothesis.readthedocs.io.""" +from __future__ import annotations +from copy import deepcopy +from collections import OrderedDict, UserDict +from collections.abc import Iterable, KeysView, ValuesView, ItemsView +from itertools import tee +from unittest.mock import ANY import gc import operator as op import pickle import sys -import unittest.mock +import typing as t 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, + BidirectionalMapping, MutableBidirectionalMapping, BidictBase, MutableBidict, OrderedBidictBase, OrderedBidict, bidict, namedbidict, inverted, DuplicationError, KeyDuplicationError, ValueDuplicationError, KeyAndValueDuplicationError, ) from bidict._iter import iteritems +from bidict._typing import Items, TypeAlias from . import _strategies as st from ._types import ( @@ -46,8 +46,13 @@ ) +Bi: TypeAlias = BidictBase[t.Any, t.Any] +MBi: TypeAlias = MutableBidict[t.Any, t.Any] +OBi: TypeAlias = OrderedBidictBase[t.Any, t.Any] + + @given(st.BIDICTS, st.NON_MAPPINGS) -def test_unequal_to_non_mapping(bi, not_a_mapping): +def test_unequal_to_non_mapping(bi: Bi, not_a_mapping: t.Any) -> None: """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 @@ -56,14 +61,14 @@ def test_unequal_to_non_mapping(bi, not_a_mapping): @given(st.BIDICTS) -def test_eq_correctly_defers_to_eq_of_non_mapping(bi): +def test_eq_correctly_defers_to_eq_of_non_mapping(bi: Bi) -> None: """Bidicts' __eq__ does not defeat non-mapping objects' __eq__, when implemented.""" - assert bi == unittest.mock.ANY - assert unittest.mock.ANY == bi + assert bi == ANY + assert ANY == bi @given(st.BI_AND_MAP_FROM_DIFF_ITEMS) -def test_unequal_to_mapping_with_different_items(bi_and_map_from_diff_items): +def test_unequal_to_mapping_with_different_items(bi_and_map_from_diff_items: t.Any) -> None: """Bidicts should be unequal to mappings containing different items.""" bi, mapping = bi_and_map_from_diff_items assert bi != mapping @@ -71,7 +76,7 @@ def test_unequal_to_mapping_with_different_items(bi_and_map_from_diff_items): @given(st.BI_AND_MAP_FROM_SAME_ND_ITEMS) -def test_equal_to_mapping_with_same_items(bi_and_map_from_same_items): +def test_equal_to_mapping_with_same_items(bi_and_map_from_same_items: t.Any) -> None: """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. @@ -89,7 +94,7 @@ def test_equal_to_mapping_with_same_items(bi_and_map_from_same_items): @given(st.HBI_AND_HMAP_FROM_SAME_ND_ITEMS) -def test_equal_hashables_have_same_hash(hashable_bidict_and_mapping): +def test_equal_hashables_have_same_hash(hashable_bidict_and_mapping: t.Any) -> None: """Hashable bidicts and hashable mappings that are equal should hash to the same value.""" bi, mapping = hashable_bidict_and_mapping assert bi == mapping @@ -101,7 +106,7 @@ def test_equal_hashables_have_same_hash(hashable_bidict_and_mapping): @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): +def test_equals_matches_equals_order_sensitive(bi: Bi, mapping: t.Mapping[t.Any, t.Any]) -> None: """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): @@ -123,7 +128,7 @@ def test_equals_matches_equals_order_sensitive(bi, mapping): @given(st.BI_AND_MAP_FROM_SAME_ND_ITEMS) -def test_equals_order_sensitive_same_items(bi_and_map_from_same_items): +def test_equals_order_sensitive_same_items(bi_and_map_from_same_items: t.Any) -> None: """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. @@ -135,7 +140,7 @@ def test_equals_order_sensitive_same_items(bi_and_map_from_same_items): @given(st.OBI_AND_OMAP_FROM_SAME_ITEMS_DIFF_ORDER) -def test_unequal_order_sensitive_same_items_different_order(ob_and_om): +def test_unequal_order_sensitive_same_items_different_order(ob_and_om: t.Any) -> None: """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, @@ -150,7 +155,7 @@ def test_unequal_order_sensitive_same_items_different_order(ob_and_om): @given(st.ORDERED_BIDICTS, st.NON_MAPPINGS) -def test_unequal_order_sensitive_non_mapping(ob, not_a_mapping): +def test_unequal_order_sensitive_non_mapping(ob: OBi, not_a_mapping: t.Any) -> None: """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, @@ -163,7 +168,7 @@ def test_unequal_order_sensitive_non_mapping(ob, not_a_mapping): @given(st.BIDICTS, st.NON_BI_MAPPINGS) -def test_merge_operators(bi, mapping): +def test_merge_operators(bi: Bi, mapping: t.Mapping[t.Any, t.Any]) -> None: """PEP 584-style dict merge operators should work as expected.""" try: merged = bi | mapping @@ -175,7 +180,7 @@ def test_merge_operators(bi, mapping): else: assert merged == bidict({**bi, **mapping}) tmp = bidict(bi) - tmp |= mapping + tmp |= mapping # type: ignore assert merged == tmp try: @@ -190,19 +195,19 @@ def test_merge_operators(bi, mapping): @given(st.MUTABLE_BIDICTS, st.DIFF_ATOMS, st.RANDOMS) -def test_setitem_with_dup_val_raises(bi, new_key, rand): +def test_setitem_with_dup_val_raises(bi: MBi, new_key: t.Any, rand: t.Any) -> None: """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 + b[new_key] = existing_val # type: ignore assert len(b) == len(b.inv) == ln @given(st.MUTABLE_BIDICTS, st.RANDOMS) -def test_setitem_with_dup_key_val_raises(bi, rand): +def test_setitem_with_dup_key_val_raises(bi: MBi, rand: t.Any) -> None: """Setting an item whose key and val duplicate two different existing items raises KeyAndValueDuplicationError.""" ln = len(bi) assume(ln > 2) @@ -211,31 +216,31 @@ def test_setitem_with_dup_key_val_raises(bi, rand): existing_key = existing_items[0][0] existing_val = existing_items[1][1] with pytest.raises(KeyAndValueDuplicationError): - b[existing_key] = existing_val + b[existing_key] = existing_val # type: ignore 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): +def test_put_with_dup_key_raises(bi: MBi, new_val: t.Any, rand: t.Any) -> None: """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) + b.put(existing_key, new_val) # type: ignore assert len(b) == len(b.inv) == ln @given(st.BIDICTS) -def test_bijectivity(bi): +def test_bijectivity(bi: Bi) -> None: """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): +def test_cleared_bidicts_have_no_items(bi: MBi) -> None: """A cleared bidict should contain no items.""" bi.clear() assert not bi @@ -245,7 +250,7 @@ def test_cleared_bidicts_have_no_items(bi): @given(st.BI_AND_CMPDICT_FROM_SAME_ITEMS, st.DATA) -def test_consistency_after_method_call(bi_and_cmp_dict, data): +def test_consistency_after_method_call(bi_and_cmp_dict: t.Any, data: t.Any) -> None: """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: @@ -257,10 +262,9 @@ def test_consistency_after_method_call(bi_and_cmp_dict, data): 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: + except (KeyError, TypeError, DuplicationError) as exc: + if isinstance(exc, TypeError): + assert methodname == 'popitem', 'popitem should be the only method that can raise TypeError here (we sometimes pass in the wrong number of args)' # 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 @@ -310,7 +314,7 @@ def test_consistency_after_method_call(bi_and_cmp_dict, data): @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): +def test_putall_same_as_put_for_each_item(bi: MBi, items: Items[t.Any, t.Any], on_dup: OnDup) -> None: """*bi.putall(items) <==> for i in items: bi.put(i)* for all values of OnDup.""" check = bi.copy() expect = bi.copy() @@ -319,13 +323,13 @@ def test_putall_same_as_put_for_each_item(bi, items, on_dup): for (key, val) in items: try: expect.put(key, val, on_dup) - except BidictException as exc: + except DuplicationError 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: + except DuplicationError as exc: checkexc = type(exc) assert checkexc == expectexc assert check == expect @@ -333,21 +337,21 @@ def test_putall_same_as_put_for_each_item(bi, items, on_dup): @given(st.BI_AND_MAP_FROM_SAME_ND_ITEMS) -def test_bidict_iter(bi_and_mapping): +def test_bidict_iter(bi_and_mapping: t.Any) -> None: """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): +def test_bidict_reversed(rb_and_rd: t.Any) -> None: """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): +def test_frozenbidicts_hashable(bi: Bi) -> None: """Frozen bidicts can be hashed and inserted into sets and mappings.""" assert hash(bi) assert {bi} @@ -355,7 +359,7 @@ def test_frozenbidicts_hashable(bi): @given(st.NAMEDBIDICT_NAMES_SOME_INVALID) -def test_namedbidict_raises_on_invalid_name(names): +def test_namedbidict_raises_on_invalid_name(names: tuple[str, str, str]) -> None: """:func:`bidict.namedbidict` should raise if given invalid names.""" typename, keyname, valname = names with pytest.raises(ValueError): @@ -363,7 +367,7 @@ def test_namedbidict_raises_on_invalid_name(names): @given(st.NAMEDBIDICT_NAMES_ALL_VALID) -def test_namedbidict_raises_on_same_keyname_as_valname(names): +def test_namedbidict_raises_on_same_keyname_as_valname(names: tuple[str, str, str]) -> None: """:func:`bidict.namedbidict` should raise if given same keyname as valname.""" typename, keyname, _ = names with pytest.raises(ValueError): @@ -371,14 +375,14 @@ def test_namedbidict_raises_on_same_keyname_as_valname(names): @given(st.NAMEDBIDICT_NAMES_ALL_VALID, st.NON_BI_MAPPING_TYPES) -def test_namedbidict_raises_on_invalid_base_type(names, invalid_base_type): +def test_namedbidict_raises_on_invalid_base_type(names: tuple[str, str, str], invalid_base_type: t.Any) -> None: """: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): +def test_namedbidict(nb: t.Any) -> None: """Test :func:`bidict.namedbidict` custom accessors.""" valfor = getattr(nb, nb.valname + '_for') keyfor = getattr(nb, nb.keyname + '_for') @@ -394,7 +398,7 @@ def test_namedbidict(nb): @require_cpython_refcounting @given(st.BIDICT_TYPES) -def test_bidicts_freed_on_zero_refcount(bi_cls): +def test_bidicts_freed_on_zero_refcount(bi_cls: t.Type[Bi]) -> None: """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), @@ -413,7 +417,7 @@ def test_bidicts_freed_on_zero_refcount(bi_cls): @require_cpython_refcounting @given(st.ORDERED_BIDICTS) -def test_orderedbidict_nodes_freed_on_zero_refcount(ob): +def test_orderedbidict_nodes_freed_on_zero_refcount(ob: OBi) -> None: """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), @@ -433,14 +437,14 @@ def test_orderedbidict_nodes_freed_on_zero_refcount(ob): @given(st.ORDERED_BIDICTS) -def test_orderedbidict_nodes_consistent(ob): +def test_orderedbidict_nodes_consistent(ob: OBi) -> None: """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(): +def test_abc_slots() -> None: """Bidict ABCs should define __slots__. Ref: https://docs.python.org/3/reference/datamodel.html#notes-on-using-slots @@ -450,25 +454,25 @@ def test_abc_slots(): @given(st.BIDICTS) -def test_inv_aliases_inverse(bi): +def test_inv_aliases_inverse(bi: Bi) -> None: """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): +def test_inverse_readonly(bi: Bi) -> None: """Attempting to set the .inverse attribute should raise AttributeError.""" with pytest.raises(AttributeError): - bi.inverse = bi.__class__(inverted(bi)) + bi.inverse = bi.__class__(inverted(bi)) # type: ignore with pytest.raises(AttributeError): - bi.inv = bi.__class__(inverted(bi)) + bi.inv = bi.__class__(inverted(bi)) # type: ignore @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): +def test_pickle(bi: Bi) -> None: """All bidicts should work with pickle.""" pickled = pickle.dumps(bi) roundtripped = pickle.loads(pickled) @@ -484,7 +488,7 @@ def test_pickle(bi): assert dict(roundtripped_inv) == dict(bi.inv) -def test_pickle_orderedbi_whose_order_disagrees_w_fwdm(): +def test_pickle_orderedbi_whose_order_disagrees_w_fwdm() -> None: """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: @@ -497,12 +501,12 @@ def test_pickle_orderedbi_whose_order_disagrees_w_fwdm(): assert roundtripped.equals_order_sensitive(ob) -class _UserBidict(bidict): +class _UserBidict(bidict[t.Any, t.Any]): """See :func:`test_pickle_dynamically_generated_inverse_bidict` below.""" _invm_cls = UserDict -def test_pickle_dynamically_generated_inverse_bidict(): +def test_pickle_dynamically_generated_inverse_bidict() -> None: """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. @@ -520,14 +524,11 @@ def test_pickle_dynamically_generated_inverse_bidict(): # 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)) + assert ub._inv_cls.__name__ not in (name for m in sys.modules for name in dir(m)) @given(st.BIDICTS) -def test_copy(bi): +def test_copy(bi: Bi) -> None: """A bidict should equal its copy.""" cp = bi.copy() assert cp is not bi @@ -540,7 +541,7 @@ def test_copy(bi): @given(st.BIDICTS) -def test_deepcopy(bi): +def test_deepcopy(bi: Bi) -> None: """A bidict should equal its deepcopy.""" cp = deepcopy(bi) assert cp is not bi @@ -554,14 +555,14 @@ def test_deepcopy(bi): assert collect(bi.inv.items()) == collect(cp.inv.items()) -def test_iteritems_raises_on_too_many_args(): +def test_iteritems_raises_on_too_many_args() -> None: """:func:`iteritems` should raise if given too many arguments.""" with pytest.raises(TypeError): - iteritems('too', 'many', 'args') + iteritems('too', 'many', 'args') # type: ignore @given(st.I_PAIRS, st.DICTS_KW_PAIRS) -def test_iteritems(arg0, kw): +def test_iteritems(arg0: t.Any, kw: t.Any) -> None: """:func:`iteritems` should work correctly.""" arg0_1, arg0_2 = tee(arg0) it = iteritems(arg0_1, **kw) @@ -577,7 +578,7 @@ def test_iteritems(arg0, kw): @given(st.L_PAIRS) -def test_inverted_pairs(pairs): +def test_inverted_pairs(pairs: t.Any) -> None: """: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 @@ -585,7 +586,7 @@ def test_inverted_pairs(pairs): @given(st.BI_AND_MAP_FROM_SAME_ND_ITEMS) -def test_inverted_bidict(bi_and_mapping): +def test_inverted_bidict(bi_and_mapping: t.Any) -> None: """: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()} @@ -593,15 +594,16 @@ def test_inverted_bidict(bi_and_mapping): assert all(i == j for (i, j) in zip(inverted(inverted(bi)), mapping.items())) -_SET_OPS = ( +_SET_OPS: t.Iterable[t.Callable[[t.Any, t.Any], t.Any]] = ( 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): +def test_views(bi: t.Any, data: t.Any) -> None: """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)): + assert isinstance(oracle, t.Iterable) and isinstance(oracle, t.Container) # appease mypy # 0-arity methods: __len__, __iter__ assert check.__len__() == oracle.__len__() assert list(check.__iter__()) == list(oracle.__iter__()) diff --git a/tests/test_class_relationships.py b/tests/test_class_relationships.py index 1ef53091..66c11ea2 100644 --- a/tests/test_class_relationships.py +++ b/tests/test_class_relationships.py @@ -6,9 +6,11 @@ """Test various issubclass checks.""" -import sys +from __future__ import annotations from collections.abc import Hashable, Mapping, MutableMapping, Reversible from collections import OrderedDict +import sys +import typing as t import pytest @@ -18,15 +20,18 @@ BidictBase, MutableBidict, OrderedBidictBase, NamedBidictBase, GeneratedBidictInverse, ) +from bidict._typing import TypeAlias -class AbstractBimap(BidirectionalMapping): +class AbstractBimap(BidirectionalMapping[t.Any, t.Any]): """Does not override `inverse` and therefore should not be instantiatable.""" -BIDICT_BASE_TYPES = (BidictBase, MutableBidict, OrderedBidictBase) +BiT: TypeAlias = t.Type[BidictBase[t.Any, t.Any]] + +BIDICT_BASE_TYPES: tuple[BiT, ...] = (BidictBase, MutableBidict, OrderedBidictBase) BIDICT_TYPES = BIDICT_BASE_TYPES + (bidict, frozenbidict, FrozenOrderedBidict, OrderedBidict) -MyNamedBidict = namedbidict('MyNamedBidict', 'key', 'val') +MyNamedBidict = namedbidict('MyNamedBidict', 'key', 'val') # type: ignore BIMAP_TYPES = BIDICT_TYPES + (AbstractBimap, MyNamedBidict) NOT_BIMAP_TYPES = (dict, OrderedDict, int, object) MUTABLE_BIDICT_TYPES = (bidict, OrderedBidict) @@ -35,13 +40,13 @@ class AbstractBimap(BidirectionalMapping): @pytest.mark.parametrize('bi_cls', BIMAP_TYPES) -def test_issubclass_bimap(bi_cls): +def test_issubclass_bimap(bi_cls: BiT) -> None: """All bidict types should be considered subclasses of :class:`BidirectionalMapping`.""" assert issubclass(bi_cls, BidirectionalMapping) @pytest.mark.parametrize('not_bi_cls', NOT_BIMAP_TYPES) -def test_not_issubclass_not_bimap(not_bi_cls): +def test_not_issubclass_not_bimap(not_bi_cls: t.Any) -> None: """Classes that do not conform to :class:`BidirectionalMapping` interface should not be considered subclasses of it. """ @@ -49,44 +54,44 @@ def test_not_issubclass_not_bimap(not_bi_cls): @pytest.mark.parametrize('bi_cls', BIDICT_TYPES) -def test_issubclass_mapping(bi_cls): +def test_issubclass_mapping(bi_cls: BiT) -> None: """All bidict types should be :class:`collections.abc.Mapping`s.""" assert issubclass(bi_cls, Mapping) @pytest.mark.parametrize('bi_cls', MUTABLE_BIDICT_TYPES) -def test_issubclass_mutable_and_mutable_bidirectional_mapping(bi_cls): +def test_issubclass_mutable_and_mutable_bidirectional_mapping(bi_cls: BiT) -> None: """All mutable bidict types should be mutable (bidirectional) mappings.""" assert issubclass(bi_cls, MutableMapping) assert issubclass(bi_cls, MutableBidirectionalMapping) -def test_issubclass_namedbidict(): +def test_issubclass_namedbidict() -> None: """Named bidicts should derive from NamedBidictBase and their inverse classes from GeneratedBidictInverse.""" assert issubclass(MyNamedBidict, NamedBidictBase) assert issubclass(MyNamedBidict._inv_cls, GeneratedBidictInverse) @pytest.mark.parametrize('bi_cls', HASHABLE_BIDICT_TYPES) -def test_hashable_not_mutable(bi_cls): +def test_hashable_not_mutable(bi_cls: BiT) -> None: """All hashable bidict types should not be mutable (bidirectional) mappings.""" assert not issubclass(bi_cls, MutableMapping) assert not issubclass(bi_cls, MutableBidirectionalMapping) @pytest.mark.parametrize('bi_cls', HASHABLE_BIDICT_TYPES) -def test_issubclass_hashable(bi_cls): +def test_issubclass_hashable(bi_cls: BiT) -> None: """All hashable bidict types should implement :class:`collections.abc.Hashable`.""" assert issubclass(bi_cls, Hashable) @pytest.mark.parametrize('bi_cls', ORDERED_BIDICT_TYPES) -def test_ordered_reversible(bi_cls): +def test_ordered_reversible(bi_cls: BiT) -> None: """All ordered bidict types should be reversible.""" assert issubclass(bi_cls, Reversible) -def test_issubclass_internal(): +def test_issubclass_internal() -> None: """The docs specifically recommend using ABCs over concrete classes when checking whether an interface is provided (see :ref:`polymorphism`). @@ -116,34 +121,34 @@ def test_issubclass_internal(): assert not issubclass(OnlyHasInverse, Mapping) -def test_abstract_bimap_init_fails(): +def test_abstract_bimap_init_fails() -> None: """Instantiating `AbstractBimap` should fail with expected TypeError.""" excmatch = r"Can't instantiate abstract class AbstractBimap with abstract methods .* inverse" with pytest.raises(TypeError, match=excmatch): - AbstractBimap() + AbstractBimap() # type: ignore -def test_bimap_inverse_notimplemented(): +def test_bimap_inverse_notimplemented() -> None: """Calling .inverse on a BidirectionalMapping should raise :class:`NotImplementedError`.""" with pytest.raises(NotImplementedError): # Can't instantiate a BidirectionalMapping that hasn't overridden the abstract methods of # the interface, so only way to call this implementation is on the class. - BidirectionalMapping.inverse.fget(bidict()) + BidirectionalMapping.inverse.fget(bidict()) # type: ignore @pytest.mark.parametrize('bi_cls', BIDICT_BASE_TYPES) -def test_bidict_bases_init_succeed(bi_cls): +def test_bidict_bases_init_succeed(bi_cls: BiT) -> None: """Bidict base classes should be initializable and have a working .inverse property.""" b = bi_cls(one=1, two=2) assert dict(b.inverse) == {1: 'one', 2: 'two'} -def test_bidict_reversible_matches_dict_reversible(): +def test_bidict_reversible_matches_dict_reversible() -> None: """Reversibility of bidict matches dict's on all supported Python versions.""" assert issubclass(bidict, Reversible) == issubclass(dict, Reversible) @pytest.mark.skipif(sys.version_info < (3, 8), reason='reversible unordered bidicts require Python 3.8+') -def test_bidict_reversible(): +def test_bidict_reversible() -> None: """All bidicts are Reversible on Python 3.8+.""" assert issubclass(bidict, Reversible) diff --git a/tests/test_metadata.py b/tests/test_metadata.py index fb6f45d9..5fa4edc2 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -6,6 +6,8 @@ """Test bidict metadata.""" +from __future__ import annotations + import bidict @@ -19,7 +21,7 @@ """.split() -def test_metadata(): +def test_metadata() -> None: """Ensure bidict has expected metadata attributes.""" for i in METADATA_ATTRS: assert getattr(bidict, i) diff --git a/tests/test_microbenchmarks.py b/tests/test_microbenchmarks.py index 4cf66260..178eca25 100644 --- a/tests/test_microbenchmarks.py +++ b/tests/test_microbenchmarks.py @@ -6,10 +6,11 @@ """Microbenchmarks.""" -import pickle -import typing as t +from __future__ import annotations from collections import deque from functools import partial +import pickle +import typing as t import pytest @@ -54,25 +55,25 @@ @pytest.mark.parametrize('n', LENS) -def test_bi_init_from_dict(n, benchmark): +def test_bi_init_from_dict(n: int, benchmark: t.Any) -> None: """Benchmark initializing a new bidict from a dict.""" other = DICTS_BY_LEN[n] benchmark(b.bidict, other) @pytest.mark.parametrize('n', LENS) -def test_bi_init_from_bi(n, benchmark): +def test_bi_init_from_bi(n: int, benchmark: t.Any) -> None: """Benchmark initializing a bidict from another bidict.""" other = BIDICTS_BY_LEN[n] benchmark(b.bidict, other) @pytest.mark.parametrize('n', LENS) -def test_bi_init_fail_worst_case(n, benchmark): +def test_bi_init_fail_worst_case(n: int, benchmark: t.Any) -> None: """Benchmark initializing a bidict from a dict with a final duplicate value.""" other = DICTS_BY_LEN_LAST_ITEM_DUPVAL[n] - def expect_failing_init(): + def expect_failing_init() -> None: try: b.bidict(other) except b.DuplicationError: @@ -84,21 +85,21 @@ def expect_failing_init(): @pytest.mark.parametrize('n', LENS) -def test_empty_bi_update_from_bi(n, benchmark): +def test_empty_bi_update_from_bi(n: int, benchmark: t.Any) -> None: """Benchmark updating an empty bidict from another bidict.""" - bi = b.bidict() + bi: b.bidict[int, int] = b.bidict() other = BIDICTS_BY_LEN[n] benchmark(bi.update, other) assert bi == other @pytest.mark.parametrize('n', LENS) -def test_small_bi_large_update_fails_worst_case(n, benchmark): +def test_small_bi_large_update_fails_worst_case(n: int, benchmark: t.Any) -> None: """Benchmark updating a small bidict with a large update that fails on the final item and then rolls back.""" bi = b.bidict(zip(range(-9, 0), range(-9, 0))) other = DICTS_BY_LEN_LAST_ITEM_DUPVAL[n] - def apply_failing_update(): + def apply_failing_update() -> None: try: bi.update(other) except b.DuplicationError: @@ -111,21 +112,21 @@ def apply_failing_update(): @pytest.mark.parametrize('n', LENS) -def test_bi_iter(n, benchmark): +def test_bi_iter(n: int, benchmark: t.Any) -> None: """Benchmark iterating over a bidict.""" bi = BIDICTS_BY_LEN[n] benchmark(consume, iter(bi)) @pytest.mark.parametrize('n', LENS) -def test_orderedbi_iter(n, benchmark): +def test_orderedbi_iter(n: int, benchmark: t.Any) -> None: """Benchmark iterating over an OrderedBidict.""" ob = ORDERED_BIDICTS_BY_LEN[n] benchmark(consume, iter(ob)) @pytest.mark.parametrize('n', LENS) -def test_bi_contains_key_present(n, benchmark): +def test_bi_contains_key_present(n: int, benchmark: t.Any) -> None: """Benchmark bidict.__contains__ with a contained key.""" bi = BIDICTS_BY_LEN[n] key = next(iter(bi)) @@ -134,7 +135,7 @@ def test_bi_contains_key_present(n, benchmark): @pytest.mark.parametrize('n', LENS) -def test_bi_contains_key_missing(n, benchmark): +def test_bi_contains_key_missing(n: int, benchmark: t.Any) -> None: """Benchmark bidict.__contains__ with a missing key.""" bi = BIDICTS_BY_LEN[n] result = benchmark(bi.__contains__, object()) @@ -142,7 +143,7 @@ def test_bi_contains_key_missing(n, benchmark): @pytest.mark.parametrize('n', LENS) -def test_bi_equals_with_equal_dict(n, benchmark): +def test_bi_equals_with_equal_dict(n: int, benchmark: t.Any) -> None: """Benchmark bidict.__eq__ with an equivalent dict.""" bi, d = BIDICT_AND_DICT_LAST_TWO_ITEMS_DIFFERENT_ORDER[n] result = benchmark(bi.__eq__, d) @@ -150,7 +151,7 @@ def test_bi_equals_with_equal_dict(n, benchmark): @pytest.mark.parametrize('n', LENS) -def test_orderedbi_equals_with_equal_dict(n, benchmark): +def test_orderedbi_equals_with_equal_dict(n: int, benchmark: t.Any) -> None: """Benchmark OrderedBidict.__eq__ with an equivalent dict.""" ob, d = ORDERED_BIDICT_AND_DICT_LAST_TWO_ITEMS_DIFFERENT_ORDER[n] result = benchmark(ob.__eq__, d) @@ -158,7 +159,7 @@ def test_orderedbi_equals_with_equal_dict(n, benchmark): @pytest.mark.parametrize('n', LENS) -def test_orderedbi_items_equals_with_equal_dict_items(n, benchmark): +def test_orderedbi_items_equals_with_equal_dict_items(n: int, benchmark: t.Any) -> None: """Benchmark OrderedBidict.items().__eq__ with an equivalent dict_items.""" ob, d = ORDERED_BIDICT_AND_DICT_LAST_TWO_ITEMS_DIFFERENT_ORDER[n] obi, di = ob.items(), d.items() @@ -167,7 +168,7 @@ def test_orderedbi_items_equals_with_equal_dict_items(n, benchmark): @pytest.mark.parametrize('n', LENS) -def test_orderedbi_items_equals_with_unequal_dict_items(n, benchmark): +def test_orderedbi_items_equals_with_unequal_dict_items(n: int, benchmark: t.Any) -> None: """Benchmark OrderedBidict.items().__eq__ with an unequal dict_items.""" ob, d = ORDERED_BIDICT_AND_DICT_ONLY_LAST_ITEM_DIFFERENT[n] obi, di = ob.items(), d.items() @@ -176,7 +177,7 @@ def test_orderedbi_items_equals_with_unequal_dict_items(n, benchmark): @pytest.mark.parametrize('n', LENS) -def test_bi_equals_with_unequal_dict(n, benchmark): +def test_bi_equals_with_unequal_dict(n: int, benchmark: t.Any) -> None: """Benchmark bidict.__eq__ with an unequal dict.""" bi, d = BIDICT_AND_DICT_ONLY_LAST_ITEM_DIFFERENT[n] result = benchmark(bi.__eq__, d) @@ -184,7 +185,7 @@ def test_bi_equals_with_unequal_dict(n, benchmark): @pytest.mark.parametrize('n', LENS) -def test_orderedbi_equals_with_unequal_dict(n, benchmark): +def test_orderedbi_equals_with_unequal_dict(n: int, benchmark: t.Any) -> None: """Benchmark OrderedBidict.__eq__ with an unequal dict.""" ob, d = ORDERED_BIDICT_AND_DICT_ONLY_LAST_ITEM_DIFFERENT[n] result = benchmark(ob.__eq__, d) @@ -192,7 +193,7 @@ def test_orderedbi_equals_with_unequal_dict(n, benchmark): @pytest.mark.parametrize('n', LENS) -def test_bi_order_sensitive_equals_dict(n, benchmark): +def test_bi_order_sensitive_equals_dict(n: int, benchmark: t.Any) -> None: """Benchmark bidict.equals_order_sensitive with an order-sensitive-equal dict.""" bi, d = BIDICTS_BY_LEN[n], DICTS_BY_LEN[n] result = benchmark(bi.equals_order_sensitive, d) @@ -200,7 +201,7 @@ def test_bi_order_sensitive_equals_dict(n, benchmark): @pytest.mark.parametrize('n', LENS) -def test_orderedbi_order_sensitive_equals_dict(n, benchmark): +def test_orderedbi_order_sensitive_equals_dict(n: int, benchmark: t.Any) -> None: """Benchmark OrderedBidict.equals_order_sensitive with an order-sensitive-equal dict.""" ob, d = ORDERED_BIDICTS_BY_LEN[n], DICTS_BY_LEN[n] result = benchmark(ob.equals_order_sensitive, d) @@ -208,7 +209,7 @@ def test_orderedbi_order_sensitive_equals_dict(n, benchmark): @pytest.mark.parametrize('n', LENS) -def test_bi_equals_order_sensitive_with_unequal_dict(n, benchmark): +def test_bi_equals_order_sensitive_with_unequal_dict(n: int, benchmark: t.Any) -> None: """Benchmark bidict.equals_order_sensitive with an order-sensitive-unequal dict.""" bi, d = BIDICT_AND_DICT_LAST_TWO_ITEMS_DIFFERENT_ORDER[n] result = benchmark(bi.equals_order_sensitive, d) @@ -216,7 +217,7 @@ def test_bi_equals_order_sensitive_with_unequal_dict(n, benchmark): @pytest.mark.parametrize('n', LENS) -def test_orderedbi_equals_order_sensitive_with_unequal_dict(n, benchmark): +def test_orderedbi_equals_order_sensitive_with_unequal_dict(n: int, benchmark: t.Any) -> None: """Benchmark OrderedBidict.equals_order_sensitive with an order-sensitive-unequal dict.""" ob, d = ORDERED_BIDICT_AND_DICT_LAST_TWO_ITEMS_DIFFERENT_ORDER[n] result = benchmark(ob.equals_order_sensitive, d) @@ -224,21 +225,21 @@ def test_orderedbi_equals_order_sensitive_with_unequal_dict(n, benchmark): @pytest.mark.parametrize('n', LENS) -def test_copy(n, benchmark): +def test_copy(n: int, benchmark: t.Any) -> None: """Benchmark creating a copy of a bidict.""" bi = BIDICTS_BY_LEN[n] benchmark(bi.copy) @pytest.mark.parametrize('n', LENS) -def test_pickle(n, benchmark): +def test_pickle(n: int, benchmark: t.Any) -> None: """Benchmark pickling a bidict.""" bi = BIDICTS_BY_LEN[n] benchmark(pickle.dumps, bi) @pytest.mark.parametrize('n', LENS) -def test_unpickle(n, benchmark): +def test_unpickle(n: int, benchmark: t.Any) -> None: """Benchmark unpickling a bidict.""" bp = pickle.dumps(BIDICTS_BY_LEN[n]) benchmark(pickle.loads, bp)