diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index f54bbd0c..04c739f1 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -10,10 +10,10 @@ repos:
# - id: fix-encoding-pragma
- id: check-yaml
-#- repo: https://github.com/pre-commit/mirrors-mypy
-# rev: '0.782'
-# hooks:
-# - id: mypy
+- repo: https://github.com/pre-commit/mirrors-mypy
+ rev: v0.782
+ hooks:
+ - id: mypy
- repo: https://github.com/pycqa/pydocstyle
rev: 5.0.2
@@ -53,10 +53,6 @@ repos:
- hypothesis < 6
- pytest < 7
- Sphinx < 4
- args:
- # http://pylint.pycqa.org/en/latest/user_guide/run.html#parallel-execution
- # "If the provided number is 0, then the total number of CPUs will be used."
- - --jobs=0
- repo: https://github.com/jumanjihouse/pre-commit-hooks
rev: 2.1.4
diff --git a/.pylintrc b/.pylintrc
index a41310b1..326f1333 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -1,24 +1,32 @@
# https://docs.pylint.org/en/latest/technical_reference/features.html
+[MASTER]
+ignore=_version.py conf.py
+jobs=0
+
[MESSAGES CONTROL]
-disable =
+disable=
+ abstract-method,
+ arguments-differ,
+ attribute-defined-outside-init,
+ bad-continuation,
+ broad-except,
invalid-name,
+ isinstance-second-argument-not-valid-type, # https://github.com/PyCQA/pylint/issues/3507
+ line-too-long,
+ missing-function-docstring, # prevents idiomatic type hints
+ multiple-statements, # prevents idiomatic type hints
+ no-init,
no-member,
- too-few-public-methods,
+ no-self-use, # prevents idiomatic type hints
+ not-callable,
protected-access,
- isinstance-second-argument-not-valid-type, # https://github.com/PyCQA/pylint/issues/3507
-
-# bidict/_version.py is generated by setuptools_scm
-ignore = _version.py
-
-# Maximum number of parents for a class.
-# The default of 7 results in "too-many-ancestors" for all bidict classes.
-max-parents = 13
-
-# Maximum number of arguments for a function.
-# The default of 5 only leaves room for 4 args besides self for methods.
-max-args=6
-
-
-[FORMAT]
-max-line-length=125
+ redefined-builtin,
+ signature-differs,
+ super-init-not-called,
+ too-few-public-methods,
+ too-many-ancestors,
+ too-many-branches,
+ too-many-locals,
+ wrong-import-order,
+ wrong-import-position,
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 328cafc7..62fbbf16 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -24,16 +24,55 @@ Tip: Subscribe to releases
to be notified when new versions of ``bidict`` are released.
-0.21.0 not yet released
------------------------
+0.21.0 (not yet released)
+-------------------------
+
+- First release of ``bidict`` to provide
+ `type hints `__! ⌨️ ✅
+
+ Adding static type hints to ``bidict`` poses a particularly interesting challenge
+ due to the combination of generic types,
+ dynamically-generated inverse types and :func:`namedbidicts `,
+ optimizations such as the use of slots and weakrefs,
+ and several other factors.
+
+ It didn't take long to hit up against current limitations
+ in the :mod:`typing` module
+ (e.g. `#548 `__
+ and `BPO-4151 `__, among others).
+
+ That said, this release should provide a solid foundation
+ for type hinted programs that use ``bidict``.
+ But if you spot any opportunities to improve type hints in ``bidict``,
+ please submit a PR!
+
+- Add :class:`bidict.MutableBidirectionalMapping` ABC.
+
+ The :ref:`other-bidict-types:Bidict Types Diagram` has been updated accordingly.
+
+- Drop support for Python 3.5,
+ which lacks support for
+ `variable type hint syntax `__,
+ `ordered dicts `__,
+ and which the Python core developers no longer maintain
+ `as of 2020-09-13 `__.
-- Add :class:`bidict.MutableBidirectionalMapping`.
+- Remove the no-longer-needed ``bidict.compat`` module.
-- Add type hints.
+- :meth:`bidict.OrderedBidictBase.__iter__` no longer accepts
+ a ``reverse`` keyword argument so that it matches the signature of
+ :meth:`container.__iter__`.
-- Drop support for Python 3.5.
+- Set the ``__module__`` attribute of everything
+ that should be imported from the :mod:`bidict` module to ``'bidict'``,
+ so that private, internal submodules are not exposed to users
+ e.g. in repr strings.
-- Remove the ``bidict.compat`` module.
+- :func:`~bidict.namedbidict` now immediately raises :class:`TypeError`
+ if the provided ``base_type`` does not provide
+ ``_isinv`` or :meth:`~object.__getstate__`,
+ rather than succeeding with a class whose instances may raise
+ :class:`AttributeError` when these attributes are accessed.
0.20.0 (2020-07-23)
diff --git a/README.rst b/README.rst
index 3c25acce..537a150e 100644
--- a/README.rst
+++ b/README.rst
@@ -76,6 +76,7 @@ Status
safety, simplicity, flexibility, and ergonomics
- is fast, lightweight, and has no runtime dependencies other than Python's standard library
- integrates natively with Python’s collections interfaces
+- provides type hints for all public APIs
- is implemented in concise, well-factored, pure (PyPy-compatible) Python code
optimized both for reading and learning from [#fn-learning]_
as well as for running efficiently
diff --git a/bidict/__init__.py b/bidict/__init__.py
index 20e0a25a..9efdb89e 100644
--- a/bidict/__init__.py
+++ b/bidict/__init__.py
@@ -58,10 +58,10 @@
# The rest of this file only collects functionality implemented in the rest of the
# source for the purposes of exporting it under the `bidict` module namespace.
-# pylint: disable=wrong-import-position
# flake8: noqa: F401 (imported but unused)
+from ._typing import KT, VT, DT, OKT, OVT, ODT, _NONE, IterItems, MapOrIterItems
from ._abc import BidirectionalMapping, MutableBidirectionalMapping
-from ._base import BidictBase
+from ._base import BidictBase, BT
from ._mut import MutableBidict
from ._bidict import bidict
from ._frozenbidict import frozenbidict
@@ -69,20 +69,25 @@
from ._named import namedbidict
from ._orderedbase import OrderedBidictBase
from ._orderedbidict import OrderedBidict
-from ._dup import (
- ON_DUP_DEFAULT, ON_DUP_RAISE, ON_DUP_DROP_OLD,
- RAISE, DROP_OLD, DROP_NEW, OnDup, OnDupAction,
-)
-from ._exc import (
- BidictException,
- DuplicationError, KeyDuplicationError, ValueDuplicationError, KeyAndValueDuplicationError,
-)
+from ._dup import ON_DUP_DEFAULT, ON_DUP_RAISE, ON_DUP_DROP_OLD, RAISE, DROP_OLD, DROP_NEW, OnDup, OnDupAction
+from ._exc import BidictException, DuplicationError, KeyDuplicationError, ValueDuplicationError, KeyAndValueDuplicationError
from ._util import inverted
from .metadata import (
__author__, __maintainer__, __copyright__, __email__, __credits__, __url__,
__license__, __status__, __description__, __keywords__, __version__, __version_info__,
)
+# Set __module__ of re-exported classes to the 'bidict' top-level module name
+# so that private/internal submodules are not exposed to users e.g. in repr strings.
+_locals = tuple(locals().items())
+for _name, _obj in _locals: # pragma: no cover
+ if not getattr(_obj, '__module__', '').startswith('bidict.'):
+ continue
+ try:
+ _obj.__module__ = 'bidict'
+ except AttributeError as exc: # raised when __module__ is read-only (as in OnDup)
+ pass
+
# * Code review nav *
#==============================================================================
diff --git a/bidict/_abc.py b/bidict/_abc.py
index 1945807b..0accbbd7 100644
--- a/bidict/_abc.py
+++ b/bidict/_abc.py
@@ -28,17 +28,13 @@
"""Provide the :class:`BidirectionalMapping` abstract base class."""
+import typing as _t
from abc import abstractmethod
-from typing import AbstractSet, Iterator, Mapping, MutableMapping, Tuple, TypeVar
+from ._typing import KT, VT
-KT = TypeVar('KT')
-VT = TypeVar('VT')
-
-# pylint: disable=abstract-method,no-init
-
-class BidirectionalMapping(Mapping[KT, VT]):
+class BidirectionalMapping(_t.Mapping[KT, VT]):
"""Abstract base class (ABC) for bidirectional mapping types.
Extends :class:`collections.abc.Mapping` primarily by adding the
@@ -66,7 +62,7 @@ def inverse(self) -> 'BidirectionalMapping[VT, KT]':
# clear there's no reason to call this implementation (e.g. via super() after overriding).
raise NotImplementedError
- def __inverted__(self) -> Iterator[Tuple[VT, KT]]:
+ def __inverted__(self) -> _t.Iterator[_t.Tuple[VT, KT]]:
"""Get an iterator over the items in :attr:`inverse`.
This is functionally equivalent to iterating over the items in the
@@ -82,7 +78,7 @@ def __inverted__(self) -> Iterator[Tuple[VT, KT]]:
"""
return iter(self.inverse.items())
- def values(self) -> AbstractSet[VT]: # type: ignore[override]
+ def values(self) -> _t.KeysView[VT]: # type: ignore
"""A set-like object providing a view on the contained values.
Override the implementation inherited from
@@ -94,10 +90,10 @@ def values(self) -> AbstractSet[VT]: # type: ignore[override]
which has the advantages of constant-time containment checks
and supporting set operations.
"""
- return self.inverse.keys()
+ return self.inverse.keys() # type: ignore
-class MutableBidirectionalMapping(BidirectionalMapping[KT, VT], MutableMapping[KT, VT]):
+class MutableBidirectionalMapping(BidirectionalMapping[KT, VT], _t.MutableMapping[KT, VT]):
"""Abstract base class (ABC) for mutable bidirectional mapping types."""
__slots__ = ()
diff --git a/bidict/_base.py b/bidict/_base.py
index 4942acd7..08a79b0b 100644
--- a/bidict/_base.py
+++ b/bidict/_base.py
@@ -28,25 +28,23 @@
"""Provide :class:`BidictBase`."""
+import typing as _t
from collections import namedtuple
from copy import copy
-from typing import Any, Iterator, List, Mapping, Optional, Tuple, TypeVar
from weakref import ref
-from ._abc import KT, VT, BidirectionalMapping
+from ._abc import BidirectionalMapping
from ._dup import ON_DUP_DEFAULT, RAISE, DROP_OLD, DROP_NEW, OnDup
-from ._exc import (
- DuplicationError, KeyDuplicationError, ValueDuplicationError, KeyAndValueDuplicationError)
-from ._sntl import _MISS
+from ._exc import DuplicationError, KeyDuplicationError, ValueDuplicationError, KeyAndValueDuplicationError
+from ._typing import _NONE, KT, VT, OKT, OVT, IterItems, MapOrIterItems
from ._util import _iteritems_args_kw
-_DedupResult = namedtuple('_DedupResult', 'isdupkey isdupval invbyval fwdbykey')
_WriteResult = namedtuple('_WriteResult', 'key val oldkey oldval')
-_NODUP = _DedupResult(False, False, _MISS, _MISS)
-
+_DedupResult = namedtuple('_DedupResult', 'isdupkey isdupval invbyval fwdbykey')
+_NODUP = _DedupResult(False, False, _NONE, _NONE)
-T = TypeVar('T', bound='BidictBase')
+BT = _t.TypeVar('BT', bound='BidictBase') # typevar for BidictBase.copy
class BidictBase(BidirectionalMapping[KT, VT]):
@@ -67,7 +65,13 @@ class BidictBase(BidirectionalMapping[KT, VT]):
#: The object used by :meth:`__repr__` for printing the contained items.
_repr_delegate = dict
- def __init__(self, *args, **kw) -> None: # pylint: disable=super-init-not-called
+ @_t.overload
+ def __init__(self, __arg: _t.Mapping[KT, VT], **kw: VT) -> None: ...
+ @_t.overload
+ def __init__(self, __arg: IterItems[KT, VT], **kw: VT) -> None: ...
+ @_t.overload
+ def __init__(self, **kw: VT) -> None: ...
+ def __init__(self, *args: MapOrIterItems[KT, VT], **kw: VT) -> None:
"""Make a new bidirectional dictionary.
The signature behaves like that of :class:`dict`.
Items passed in are added in the order they are passed,
@@ -75,15 +79,15 @@ def __init__(self, *args, **kw) -> None: # pylint: disable=super-init-not-calle
"""
#: The backing :class:`~collections.abc.Mapping`
#: storing the forward mapping data (*key* → *value*).
- self._fwdm = self._fwdm_cls()
+ self._fwdm: _t.Dict[KT, VT] = self._fwdm_cls()
#: The backing :class:`~collections.abc.Mapping`
#: storing the inverse mapping data (*value* → *key*).
- self._invm = self._invm_cls()
+ self._invm: _t.Dict[VT, KT] = self._invm_cls()
self._init_inv()
if args or kw:
self._update(True, self.on_dup, *args, **kw)
- def _init_inv(self):
+ def _init_inv(self) -> None:
# Compute the type for this bidict's inverse bidict (will be different from this
# bidict's type if _fwdm_cls and _invm_cls are different).
inv_cls = self._inv_cls()
@@ -102,21 +106,21 @@ def _init_inv(self):
self._invweak = None
@classmethod
- def _inv_cls(cls):
+ def _inv_cls(cls) -> '_t.Type[BidictBase[VT, KT]]':
"""The inverse of this bidict type, i.e. one with *_fwdm_cls* and *_invm_cls* swapped."""
if cls._fwdm_cls is cls._invm_cls:
- return cls
+ return cls # type: ignore
if not getattr(cls, '_inv_cls_', None):
- class _Inv(cls):
+ class _Inv(cls): # type: ignore
_fwdm_cls = cls._invm_cls
_invm_cls = cls._fwdm_cls
_inv_cls_ = cls
_Inv.__name__ = cls.__name__ + 'Inv'
- cls._inv_cls_ = _Inv
- return cls._inv_cls_
+ cls._inv_cls_ = _Inv # type: ignore
+ return cls._inv_cls_ # type: ignore
@property
- def _isinv(self):
+ def _isinv(self) -> bool:
return self._inv is None
@property
@@ -125,9 +129,10 @@ def inverse(self) -> 'BidictBase[VT, KT]':
# Resolve and return a strong reference to the inverse bidict.
# One may be stored in self._inv already.
if self._inv is not None:
- return self._inv
+ return self._inv # type: ignore
# Otherwise a weakref is stored in self._invweak. Try to get a strong ref from it.
- inv = self._invweak() # pylint: disable=not-callable
+ assert self._invweak is not None
+ inv = self._invweak()
if inv is not None:
return inv
# Refcount of referent must have dropped to zero, as in `bidict().inv.inv`. Init a new one.
@@ -137,7 +142,7 @@ def inverse(self) -> 'BidictBase[VT, KT]':
#: Alias for :attr:`inverse`.
inv = inverse
- def __getstate__(self):
+ def __getstate__(self) -> dict:
"""Needed to enable pickling due to use of :attr:`__slots__` and weakrefs.
*See also* :meth:`object.__getstate__`
@@ -153,7 +158,7 @@ def __getstate__(self):
state.pop('__weakref__', None) # Not added back in __setstate__. Python manages this one.
return state
- def __setstate__(self, state):
+ def __setstate__(self, state: dict) -> None:
"""Implemented because use of :attr:`__slots__` would prevent unpickling otherwise.
*See also* :meth:`object.__setstate__`
@@ -172,7 +177,7 @@ def __repr__(self) -> str:
# The inherited Mapping.__eq__ implementation would work, but it's implemented in terms of an
# inefficient ``dict(self.items()) == dict(other.items())`` comparison, so override it with a
# more efficient implementation.
- def __eq__(self, other: Any) -> bool:
+ def __eq__(self, other: _t.Any) -> bool:
"""*x.__eq__(other) ⟺ x == other*
Equivalent to *dict(x.items()) == dict(other.items())*
@@ -186,10 +191,10 @@ def __eq__(self, other: Any) -> bool:
*See also* :meth:`bidict.FrozenOrderedBidict.equals_order_sensitive`
"""
- if not isinstance(other, Mapping) or len(self) != len(other):
+ if not isinstance(other, _t.Mapping) or len(self) != len(other):
return False
selfget = self.get
- return all(selfget(k, _MISS) == v for (k, v) in other.items())
+ return all(selfget(k, _NONE) == v for (k, v) in other.items()) # type: ignore
# The following methods are mutating and so are not public. But they are implemented in this
# non-mutable base class (rather than the mutable `bidict` subclass) because they are used here
@@ -206,7 +211,7 @@ def _put(self, key: KT, val: VT, on_dup: OnDup) -> None:
if dedup_result is not None:
self._write_item(key, val, dedup_result)
- def _dedup_item(self, key: KT, val: VT, on_dup: OnDup) -> Optional[_DedupResult]:
+ def _dedup_item(self, key: KT, val: VT, on_dup: OnDup) -> _t.Optional[_DedupResult]:
"""Check *key* and *val* for any duplication in self.
Handle any duplication as per the passed in *on_dup*.
@@ -224,13 +229,12 @@ def _dedup_item(self, key: KT, val: VT, on_dup: OnDup) -> Optional[_DedupResult]
or if no duplication is found,
return the :class:`_DedupResult` *(isdupkey, isdupval, oldkey, oldval)*.
"""
- # pylint: disable=too-many-branches
fwdm = self._fwdm
invm = self._invm
- oldval = fwdm.get(key, _MISS)
- oldkey = invm.get(val, _MISS)
- isdupkey = oldval is not _MISS
- isdupval = oldkey is not _MISS
+ oldval: OVT = fwdm.get(key, _NONE)
+ oldkey: OKT = invm.get(val, _NONE)
+ isdupkey = oldval is not _NONE
+ isdupval = oldkey is not _NONE
dedup_result = _DedupResult(isdupkey, isdupval, oldkey, oldval)
if isdupkey and isdupval:
if self._already_have(key, val, oldkey, oldval):
@@ -264,7 +268,7 @@ def _dedup_item(self, key: KT, val: VT, on_dup: OnDup) -> Optional[_DedupResult]
return dedup_result
@staticmethod
- def _already_have(key: KT, val: VT, oldkey: KT, oldval: VT) -> bool:
+ def _already_have(key: KT, val: VT, oldkey: OKT, oldval: OVT) -> bool:
# Overridden by _orderedbase.OrderedBidictBase.
isdup = oldkey == key
assert isdup == (oldval == val), f'{key} {val} {oldkey} {oldval}'
@@ -283,13 +287,13 @@ def _write_item(self, key: KT, val: VT, dedup_result: _DedupResult) -> _WriteRes
del fwdm[oldkey]
return _WriteResult(key, val, oldkey, oldval)
- def _update(self, init: bool, on_dup: OnDup, *args, **kw) -> None:
+ def _update(self, init: bool, on_dup: OnDup, *args: MapOrIterItems[KT, VT], **kw: VT) -> None:
# args[0] may be a generator that yields many items, so process input in a single pass.
if not args and not kw:
return
can_skip_dup_check = not self and not kw and isinstance(args[0], BidirectionalMapping)
if can_skip_dup_check:
- self._update_no_dup_check(args[0])
+ self._update_no_dup_check(args[0]) # type: ignore
return
can_skip_rollback = init or RAISE not in on_dup
if can_skip_rollback:
@@ -297,19 +301,19 @@ def _update(self, init: bool, on_dup: OnDup, *args, **kw) -> None:
else:
self._update_with_rollback(on_dup, *args, **kw)
- def _update_no_dup_check(self, other: Mapping) -> None:
+ def _update_no_dup_check(self, other: BidirectionalMapping[KT, VT]) -> None:
write_item = self._write_item
for (key, val) in other.items():
write_item(key, val, _NODUP)
- def _update_no_rollback(self, on_dup: OnDup, *args, **kw) -> None:
+ def _update_no_rollback(self, on_dup: OnDup, *args: MapOrIterItems[KT, VT], **kw: VT) -> None:
put = self._put
for (key, val) in _iteritems_args_kw(*args, **kw):
put(key, val, on_dup)
- def _update_with_rollback(self, on_dup: OnDup, *args, **kw) -> None:
+ def _update_with_rollback(self, on_dup: OnDup, *args: MapOrIterItems[KT, VT], **kw: VT) -> None:
"""Update, rolling back on failure."""
- writes: List[Tuple[_DedupResult, _WriteResult]] = []
+ writes: _t.List[_t.Tuple[_DedupResult, _WriteResult]] = []
append_write = writes.append
dedup_item = self._dedup_item
write_item = self._write_item
@@ -344,7 +348,7 @@ def _undo_write(self, dedup_result: _DedupResult, write_result: _WriteResult) ->
if not isdupkey:
del fwdm[key]
- def copy(self: T) -> T:
+ def copy(self: BT) -> BT:
"""A shallow copy."""
# Could just ``return self.__class__(self)`` here instead, but the below is faster. It uses
# __new__ to create a copy instance while bypassing its __init__, which would result
@@ -355,9 +359,9 @@ def copy(self: T) -> T:
cp._fwdm = copy(self._fwdm)
cp._invm = copy(self._invm)
cp._init_inv()
- return cp
+ return cp # type: ignore
- def __copy__(self: T) -> T:
+ def __copy__(self: BT) -> BT:
"""Used for the copy protocol.
*See also* the :mod:`copy` module
@@ -368,7 +372,7 @@ def __len__(self) -> int:
"""The number of contained items."""
return len(self._fwdm)
- def __iter__(self) -> Iterator[KT]:
+ def __iter__(self) -> _t.Iterator[KT]:
"""Iterator over the contained keys."""
return iter(self._fwdm)
@@ -377,7 +381,7 @@ def __getitem__(self, key: KT) -> VT:
return self._fwdm[key]
-# Work around https://bugs.python.org/issue41451
+# Work around weakref slot with Generics bug on Python 3.6 (https://bugs.python.org/issue41451):
BidictBase.__slots__.remove('__weakref__')
# * Code review nav *
diff --git a/bidict/_bidict.py b/bidict/_bidict.py
index 625416cb..4b123f56 100644
--- a/bidict/_bidict.py
+++ b/bidict/_bidict.py
@@ -28,9 +28,11 @@
"""Provide :class:`bidict`."""
-from ._base import KT, VT
-from ._mut import MutableBidict
+import typing as _t
+
from ._delegating import _DelegatingBidict
+from ._mut import MutableBidict
+from ._typing import KT, VT
class bidict(_DelegatingBidict[KT, VT], MutableBidict[KT, VT]):
@@ -38,6 +40,10 @@ class bidict(_DelegatingBidict[KT, VT], MutableBidict[KT, VT]):
__slots__ = ()
+ if _t.TYPE_CHECKING: # pragma: no cover
+ @property
+ def inverse(self) -> 'bidict[VT, KT]': ...
+
# * Code review nav *
#==============================================================================
diff --git a/bidict/_compat.py b/bidict/_compat.py
deleted file mode 100644
index cdb9237b..00000000
--- a/bidict/_compat.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright 2009-2020 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/.
-
-
-"""Compatibility helpers."""
-
-import typing
-
-
-# Without this guard, we get "TypeError: __weakref__ slot disallowed: either we already got one, or __itemsize__ != 0"
-# errors on Python 3.6. Apparently this is due to no PEP560 support:
-# https://www.python.org/dev/peps/pep-0560/#hacks-and-bugs-that-will-be-removed-by-this-proposal
-# > thus allowing generics with __slots__
-TYPED_GENERICS_SUPPORT_SLOTS = not hasattr(typing, 'GenericMeta')
diff --git a/bidict/_delegating.py b/bidict/_delegating.py
index 8618f4bf..916fe1bc 100644
--- a/bidict/_delegating.py
+++ b/bidict/_delegating.py
@@ -8,9 +8,10 @@
"""Provide :class:`_DelegatingBidict`."""
-from typing import Iterator, KeysView, ItemsView
+import typing as _t
-from ._base import BidictBase, KT, VT
+from ._base import BidictBase
+from ._typing import KT, VT
class _DelegatingBidict(BidictBase[KT, VT]):
@@ -21,18 +22,18 @@ class _DelegatingBidict(BidictBase[KT, VT]):
__slots__ = ()
- def __iter__(self) -> Iterator[KT]:
+ def __iter__(self) -> _t.Iterator[KT]:
"""Iterator over the contained keys."""
return iter(self._fwdm)
- def keys(self) -> KeysView[KT]:
+ def keys(self) -> _t.KeysView[KT]:
"""A set-like object providing a view on the contained keys."""
return self._fwdm.keys()
- def values(self) -> KeysView[VT]:
+ def values(self) -> _t.KeysView[VT]: # type: ignore
"""A set-like object providing a view on the contained values."""
return self._invm.keys()
- def items(self) -> ItemsView[KT, VT]:
+ def items(self) -> _t.ItemsView[KT, VT]:
"""A set-like object providing a view on the contained items."""
return self._fwdm.items()
diff --git a/bidict/_dup.py b/bidict/_dup.py
index 2c31dc6c..056fb007 100644
--- a/bidict/_dup.py
+++ b/bidict/_dup.py
@@ -23,8 +23,8 @@ class OnDupAction(Enum):
#: Keep existing items and drop new items.
DROP_NEW = 'DROP_NEW'
- def __repr__(self):
- return f'' # pragma: no cover
+ def __repr__(self) -> str:
+ return f'<{self.name}>' # pragma: no cover
RAISE = OnDupAction.RAISE
@@ -42,7 +42,7 @@ class OnDup(namedtuple('_OnDup', 'key val kv')):
__slots__ = ()
- def __new__(cls, key=DROP_OLD, val=RAISE, kv=RAISE):
+ def __new__(cls, key: OnDupAction = DROP_OLD, val: OnDupAction = RAISE, kv: OnDupAction = RAISE) -> 'OnDup':
"""Override to provide user-friendly default values."""
return super().__new__(cls, key, val, kv or val)
diff --git a/bidict/_frozenbidict.py b/bidict/_frozenbidict.py
index b97bc6c8..0fecf27c 100644
--- a/bidict/_frozenbidict.py
+++ b/bidict/_frozenbidict.py
@@ -27,7 +27,10 @@
"""Provide :class:`frozenbidict`, an immutable, hashable bidirectional mapping type."""
-from ._delegating import _DelegatingBidict, KT, VT, ItemsView
+import typing as _t
+
+from ._delegating import _DelegatingBidict
+from ._typing import KT, VT
class frozenbidict(_DelegatingBidict[KT, VT]):
@@ -35,11 +38,18 @@ class frozenbidict(_DelegatingBidict[KT, VT]):
__slots__ = ()
+ # Work around lack of support for higher-kinded types in mypy.
+ # Ref: https://github.com/python/typing/issues/548#issuecomment-621571821
+ # Remove this and similar type stubs from other classes if support is ever added.
+ if _t.TYPE_CHECKING: # pragma: no cover
+ @property
+ def inverse(self) -> 'frozenbidict[VT, KT]': ...
+
def __hash__(self) -> int:
"""The hash of this bidict as determined by its items."""
if getattr(self, '_hash', None) is None:
- self._hash = ItemsView(self)._hash() # pylint: disable=attribute-defined-outside-init
- return self._hash
+ self._hash = _t.ItemsView(self)._hash() # type: ignore
+ return self._hash # type: ignore
# * Code review nav *
diff --git a/bidict/_frozenordered.py b/bidict/_frozenordered.py
index 0f5652c1..8f7291dc 100644
--- a/bidict/_frozenordered.py
+++ b/bidict/_frozenordered.py
@@ -27,32 +27,42 @@
"""Provide :class:`FrozenOrderedBidict`, an immutable, hashable, ordered bidict."""
+import typing as _t
+
from ._frozenbidict import frozenbidict
from ._orderedbase import OrderedBidictBase
+from ._typing import KT, VT
-class FrozenOrderedBidict(OrderedBidictBase):
+class FrozenOrderedBidict(OrderedBidictBase[KT, VT]):
"""Hashable, immutable, ordered bidict type."""
__slots__ = ()
__hash__ = frozenbidict.__hash__
+ if _t.TYPE_CHECKING: # pragma: no cover
+ @property
+ def inverse(self) -> 'FrozenOrderedBidict[VT, KT]': ...
+
# Assume the Python implementation's dict type is ordered (e.g. PyPy or CPython >= 3.6), so we
- # can # delegate to `_fwdm` and `_invm` for faster implementations of several methods. Both
+ # can delegate to `_fwdm` and `_invm` for faster implementations of several methods. Both
# `_fwdm` and `_invm` will always be initialized with the provided items in the correct order,
# and since `FrozenOrderedBidict` is immutable, their respective orders can't get out of sync
# after a mutation.
- def __iter__(self, reverse=False): # noqa: N802
+ def __iter__(self) -> _t.Iterator[KT]:
"""Iterator over the contained keys in insertion order."""
+ return self._iter()
+
+ def _iter(self, *, reverse: bool = False) -> _t.Iterator[KT]:
if reverse:
- return super().__iter__(reverse=True)
+ return super()._iter(reverse=True)
return iter(self._fwdm._fwdm)
- def keys(self):
+ def keys(self) -> _t.KeysView[KT]:
"""A set-like object providing a view on the contained keys."""
return self._fwdm._fwdm.keys()
- def values(self):
+ def values(self) -> _t.KeysView[VT]: # type: ignore
"""A set-like object providing a view on the contained values."""
return self._invm._fwdm.keys()
diff --git a/bidict/_mut.py b/bidict/_mut.py
index 44a3de92..d5a819bc 100644
--- a/bidict/_mut.py
+++ b/bidict/_mut.py
@@ -28,12 +28,12 @@
"""Provide :class:`bidict`."""
-from typing import Any, Tuple
+import typing as _t
-from ._abc import MutableBidirectionalMapping, KT, VT
+from ._abc import MutableBidirectionalMapping
from ._base import BidictBase
-from ._dup import ON_DUP_RAISE, ON_DUP_DROP_OLD
-from ._sntl import _MISS
+from ._dup import OnDup, ON_DUP_RAISE, ON_DUP_DROP_OLD
+from ._typing import _NONE, KT, VT, ODT, IterItems, MapOrIterItems
class MutableBidict(BidictBase[KT, VT], MutableBidirectionalMapping[KT, VT]):
@@ -41,6 +41,10 @@ class MutableBidict(BidictBase[KT, VT], MutableBidirectionalMapping[KT, VT]):
__slots__ = ()
+ if _t.TYPE_CHECKING: # pragma: no cover
+ @property
+ def inverse(self) -> 'MutableBidict[VT, KT]': ...
+
def __delitem__(self, key: KT) -> None:
"""*x.__delitem__(y) ⟺ del x[y]*"""
self._pop(key)
@@ -73,7 +77,7 @@ def __setitem__(self, key: KT, val: VT) -> None:
"""
self._put(key, val, self.on_dup)
- def put(self, key: KT, val: VT, on_dup=ON_DUP_RAISE) -> None:
+ def put(self, key: KT, val: VT, on_dup: OnDup = ON_DUP_RAISE) -> None:
"""Associate *key* with *val*, honoring the :class:`OnDup` given in *on_dup*.
For example, if *on_dup* is :attr:`~bidict.ON_DUP_RAISE`,
@@ -112,7 +116,11 @@ def clear(self) -> None:
self._fwdm.clear()
self._invm.clear()
- def pop(self, key: KT, default=_MISS) -> Any:
+ @_t.overload # type: ignore
+ def pop(self, key: KT) -> VT: ... # pragma: no cover
+ @_t.overload
+ def pop(self, key: KT, default: ODT = _NONE) -> _t.Union[VT, ODT]: ... # pragma: no cover
+ def pop(self, key: KT, default: ODT = _NONE) -> _t.Union[VT, ODT]:
"""*x.pop(k[, d]) → v*
Remove specified key and return the corresponding value.
@@ -122,11 +130,11 @@ def pop(self, key: KT, default=_MISS) -> Any:
try:
return self._pop(key)
except KeyError:
- if default is _MISS:
+ if default is _NONE:
raise
return default
- def popitem(self) -> Tuple[KT, VT]:
+ def popitem(self) -> _t.Tuple[KT, VT]:
"""*x.popitem() → (k, v)*
Remove and return some item as a (key, value) pair.
@@ -139,16 +147,22 @@ def popitem(self) -> Tuple[KT, VT]:
del self._invm[val]
return key, val
- def update(self, *args, **kw) -> None: # pylint: disable=signature-differs
+ @_t.overload
+ def update(self, __arg: _t.Mapping[KT, VT], **kw: VT) -> None: ...
+ @_t.overload
+ def update(self, __arg: IterItems[KT, VT], **kw: VT) -> None: ...
+ @_t.overload
+ def update(self, **kw: VT) -> None: ...
+ def update(self, *args: MapOrIterItems[KT, VT], **kw: VT) -> None:
"""Like calling :meth:`putall` with *self.on_dup* passed for *on_dup*."""
if args or kw:
self._update(False, self.on_dup, *args, **kw)
- def forceupdate(self, *args, **kw) -> None:
+ def forceupdate(self, *args: MapOrIterItems[KT, VT], **kw: VT) -> None:
"""Like a bulk :meth:`forceput`."""
self._update(False, ON_DUP_DROP_OLD, *args, **kw)
- def putall(self, items, on_dup=ON_DUP_RAISE) -> None:
+ def putall(self, items: MapOrIterItems[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/_named.py b/bidict/_named.py
index 14300a65..7a227222 100644
--- a/bidict/_named.py
+++ b/bidict/_named.py
@@ -7,20 +7,29 @@
"""Provide :func:`bidict.namedbidict`."""
-from ._abc import BidirectionalMapping
+import typing as _t
+from sys import _getframe
+
+from ._abc import BidirectionalMapping, KT, VT
from ._bidict import bidict
-def namedbidict(typename, keyname, valname, base_type=bidict):
+def namedbidict(
+ typename: str,
+ keyname: str,
+ valname: str,
+ *,
+ base_type: _t.Type[BidirectionalMapping[KT, VT]] = bidict,
+) -> _t.Type[BidirectionalMapping[KT, VT]]:
r"""Create a new subclass of *base_type* with custom accessors.
- Analagous to :func:`collections.namedtuple`.
+ Like :func:`collections.namedtuple` for bidicts.
- The new class's ``__name__`` and ``__qualname__``
- will be set based on *typename*.
+ The new class's ``__name__`` and ``__qualname__`` will be set to *typename*,
+ and its ``__module__`` will be set to the caller's module.
- Instances of it will provide access to their
- :attr:`inverse `\s
+ Instances of the new class will provide access to their
+ :attr:`inverse ` instances
via the custom *keyname*\_for property,
and access to themselves
via the custom *valname*\_for property.
@@ -31,62 +40,58 @@ def namedbidict(typename, keyname, valname, base_type=bidict):
:raises ValueError: if any of the *typename*, *keyname*, or *valname*
strings is not a valid Python identifier, or if *keyname == valname*.
- :raises TypeError: if *base_type* is not a subclass of
- :class:`BidirectionalMapping`.
- (This function requires slightly more of *base_type*,
- e.g. the availability of an ``_isinv`` attribute,
- but all the :ref:`concrete bidict types
- `
- that the :mod:`bidict` module provides can be passed in.
- Check out the code if you actually need to pass in something else.)
+ :raises TypeError: if *base_type* is not a :class:`BidirectionalMapping` subclass
+ that provides ``_isinv`` and :meth:`~object.__getstate__` attributes.
+ (Any :class:`~bidict.BidictBase` subclass can be passed in, including all the
+ concrete bidict types pictured in the :ref:`other-bidict-types:Bidict Types Diagram`.
"""
- # Re the `base_type` docs above:
- # The additional requirements (providing _isinv and __getstate__) do not belong in the
- # BidirectionalMapping interface, and it's overkill to create additional interface(s) for this.
- # On the other hand, it's overkill to require that base_type be a subclass of BidictBase, since
- # that's too specific. The BidirectionalMapping check along with the docs above should suffice.
- if not issubclass(base_type, BidirectionalMapping):
+ if not issubclass(base_type, BidirectionalMapping) or not all(hasattr(base_type, i) for i in ('_isinv', '__getstate__')):
raise TypeError(base_type)
names = (typename, keyname, valname)
if not all(map(str.isidentifier, names)) or keyname == valname:
raise ValueError(names)
- class _Named(base_type): # pylint: disable=too-many-ancestors
+ class _Named(base_type): # type: ignore
__slots__ = ()
- def _getfwd(self):
- return self.inverse if self._isinv else self
+ def _getfwd(self) -> '_Named':
+ return self.inverse if self._isinv else self # type: ignore
- def _getinv(self):
- return self if self._isinv else self.inverse
+ def _getinv(self) -> '_Named':
+ return self if self._isinv else self.inverse # type: ignore
@property
- def _keyname(self):
+ def _keyname(self) -> str:
return valname if self._isinv else keyname
@property
- def _valname(self):
+ def _valname(self) -> str:
return keyname if self._isinv else valname
- def __reduce__(self):
+ def __reduce__(self) -> '_t.Tuple[_t.Callable[[str, str, str, _t.Type[BidirectionalMapping]], BidirectionalMapping], _t.Tuple[str, str, str, _t.Type[BidirectionalMapping]], dict]':
return (_make_empty, (typename, keyname, valname, base_type), self.__getstate__())
bname = base_type.__name__
fname = valname + '_for'
iname = keyname + '_for'
- names = dict(typename=typename, bname=bname, keyname=keyname, valname=valname)
- fdoc = '{typename} forward {bname}: {keyname} → {valname}'.format(**names)
- idoc = '{typename} inverse {bname}: {valname} → {keyname}'.format(**names)
+ fdoc = f'{typename} forward {bname}: {keyname} → {valname}'
+ idoc = f'{typename} inverse {bname}: {valname} → {keyname}'
setattr(_Named, fname, property(_Named._getfwd, doc=fdoc))
setattr(_Named, iname, property(_Named._getinv, doc=idoc))
- _Named.__qualname__ = _Named.__qualname__[:-len(_Named.__name__)] + typename
_Named.__name__ = typename
+ _Named.__qualname__ = typename
+ _Named.__module__ = _getframe(1).f_globals.get('__name__') # type: ignore
return _Named
-def _make_empty(typename, keyname, valname, base_type):
+def _make_empty(
+ typename: str,
+ keyname: str,
+ valname: str,
+ base_type: _t.Type[BidirectionalMapping] = bidict,
+) -> BidirectionalMapping:
"""Create a named bidict with the indicated arguments and return an empty instance.
Used to make :func:`bidict.namedbidict` instances picklable.
"""
diff --git a/bidict/_orderedbase.py b/bidict/_orderedbase.py
index c0cd4807..335f7705 100644
--- a/bidict/_orderedbase.py
+++ b/bidict/_orderedbase.py
@@ -28,13 +28,13 @@
"""Provide :class:`OrderedBidictBase`."""
+import typing as _t
from copy import copy
-from typing import Any, Iterator, Mapping
from weakref import ref
-from ._base import _DedupResult, _WriteResult, BidictBase, KT, VT, T
+from ._base import _NONE, _DedupResult, _WriteResult, BidictBase, BT
from ._bidict import bidict
-from ._sntl import _MISS
+from ._typing import KT, VT, MapOrIterItems
class _Node:
@@ -56,33 +56,33 @@ class _Node:
__slots__ = ('_prv', '_nxt', '__weakref__')
- def __init__(self, prv=None, nxt=None):
+ def __init__(self, prv: '_Node' = None, nxt: '_Node' = None) -> None:
self._setprv(prv)
self._setnxt(nxt)
- def __repr__(self): # pragma: no cover
+ def __repr__(self) -> str: # pragma: no cover
clsname = self.__class__.__name__
prv = id(self.prv)
nxt = id(self.nxt)
return f'{clsname}(prv={prv}, self={id(self)}, nxt={nxt})'
- def _getprv(self):
+ def _getprv(self) -> '_t.Optional[_Node]':
return self._prv() if isinstance(self._prv, ref) else self._prv
- def _setprv(self, prv):
+ def _setprv(self, prv: '_t.Optional[_Node]') -> None:
self._prv = prv and ref(prv)
prv = property(_getprv, _setprv)
- def _getnxt(self):
+ def _getnxt(self) -> '_t.Optional[_Node]':
return self._nxt() if isinstance(self._nxt, ref) else self._nxt
- def _setnxt(self, nxt):
+ def _setnxt(self, nxt: '_t.Optional[_Node]') -> None:
self._nxt = nxt and ref(nxt)
nxt = property(_getnxt, _setnxt)
- def __getstate__(self):
+ def __getstate__(self) -> dict:
"""Return the instance state dictionary
but with weakrefs converted to strong refs
so that it can be pickled.
@@ -91,7 +91,7 @@ def __getstate__(self):
"""
return dict(_prv=self.prv, _nxt=self.nxt)
- def __setstate__(self, state):
+ def __setstate__(self, state: dict) -> None:
"""Set the instance state from *state*."""
self._setprv(state['_prv'])
self._setnxt(state['_nxt'])
@@ -106,16 +106,16 @@ class _SentinelNode(_Node):
__slots__ = ()
- def __init__(self, prv=None, nxt=None):
+ def __init__(self, prv: _Node = None, nxt: _Node = None) -> None:
super().__init__(prv or self, nxt or self)
- def __repr__(self): # pragma: no cover
+ def __repr__(self) -> str: # pragma: no cover
return ''
- def __bool__(self):
+ def __bool__(self) -> bool:
return False
- def __iter__(self, reverse=False):
+ def _iter(self, *, reverse: bool = False) -> _t.Iterator[_Node]:
"""Iterator yielding nodes in the requested order,
i.e. traverse the linked list via :attr:`nxt`
(or :attr:`prv` if *reverse* is truthy)
@@ -133,13 +133,13 @@ class OrderedBidictBase(BidictBase[KT, VT]):
__slots__ = ('_sntl',)
- _fwdm_cls = bidict
- _invm_cls = bidict
+ _fwdm_cls = bidict # type: ignore
+ _invm_cls = bidict # type: ignore
#: The object used by :meth:`__repr__` for printing the contained items.
- _repr_delegate = list
+ _repr_delegate = list # type: ignore
- def __init__(self, *args, **kw) -> None:
+ def __init__(self, *args: MapOrIterItems[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,
@@ -160,12 +160,18 @@ def __init__(self, *args, **kw) -> None:
# are inherited and are able to be reused without modification.
super().__init__(*args, **kw)
+ if _t.TYPE_CHECKING: # pragma: no cover
+ @property
+ def inverse(self) -> 'OrderedBidictBase[VT, KT]': ...
+ _fwdm: bidict[KT, _Node] # type: ignore
+ _invm: bidict[VT, _Node] # type: ignore
+
def _init_inv(self) -> None:
super()._init_inv()
self.inverse._sntl = self._sntl
# Can't reuse BidictBase.copy since ordered bidicts have different internal structure.
- def copy(self: T) -> T:
+ def copy(self: BT) -> BT:
"""A shallow copy of this ordered bidict."""
# Fast copy implementation bypassing __init__. See comments in :meth:`BidictBase.copy`.
cp = self.__class__.__new__(self.__class__)
@@ -183,7 +189,7 @@ def copy(self: T) -> T:
cp._fwdm = fwdm
cp._invm = invm
cp._init_inv()
- return cp
+ return cp # type: ignore
def __getitem__(self, key: KT) -> VT:
nodefwd = self._fwdm[key]
@@ -198,11 +204,11 @@ def _pop(self, key: KT) -> VT:
return val
@staticmethod
- def _already_have(key: KT, val: VT, nodeinv: _Node, nodefwd: _Node) -> bool: # pylint: disable=arguments-differ
+ def _already_have(key: KT, val: VT, nodeinv: _Node, nodefwd: _Node) -> bool: # type: ignore
# Overrides _base.BidictBase.
return nodeinv is nodefwd
- def _write_item(self, key: KT, val: VT, dedup_result: _DedupResult) -> None: # pylint: disable=too-many-locals
+ def _write_item(self, key: KT, val: VT, dedup_result: _DedupResult) -> _WriteResult:
# Overrides _base.BidictBase.
fwdm = self._fwdm # bidict mapping keys to nodes
invm = self._invm # bidict mapping vals to nodes
@@ -213,12 +219,12 @@ def _write_item(self, key: KT, val: VT, dedup_result: _DedupResult) -> None: #
last = sntl.prv
node = _Node(last, sntl)
last.nxt = sntl.prv = fwdm[key] = invm[val] = node
- oldkey = oldval = _MISS
+ oldkey = oldval = _NONE
elif isdupkey and isdupval:
# Key and value duplication across two different nodes.
assert nodefwd is not nodeinv
- oldval = invm.inverse[nodefwd]
- oldkey = fwdm.inverse[nodeinv]
+ oldval = invm.inverse[nodefwd] # type: ignore
+ oldkey = fwdm.inverse[nodeinv] # type: ignore
assert oldkey != key
assert oldval != val
# We have to collapse nodefwd and nodeinv into a single node, i.e. drop one of them.
@@ -228,26 +234,26 @@ def _write_item(self, key: KT, val: VT, dedup_result: _DedupResult) -> None: #
# Don't remove nodeinv's references to its neighbors since
# if the update fails, we'll need them to undo this write.
# Update fwdm and invm.
- tmp = fwdm.pop(oldkey)
+ tmp = fwdm.pop(oldkey) # type: ignore
assert tmp is nodeinv
- tmp = invm.pop(oldval)
+ tmp = invm.pop(oldval) # type: ignore
assert tmp is nodefwd
fwdm[key] = invm[val] = nodefwd
elif isdupkey:
- oldval = invm.inverse[nodefwd]
- oldkey = _MISS
- oldnodeinv = invm.pop(oldval)
+ oldval = invm.inverse[nodefwd] # type: ignore
+ oldkey = _NONE
+ oldnodeinv = invm.pop(oldval) # type: ignore
assert oldnodeinv is nodefwd
invm[val] = nodefwd
else: # isdupval
- oldkey = fwdm.inverse[nodeinv]
- oldval = _MISS
- oldnodefwd = fwdm.pop(oldkey)
+ oldkey = fwdm.inverse[nodeinv] # type: ignore
+ oldval = _NONE
+ oldnodefwd = fwdm.pop(oldkey) # type: ignore
assert oldnodefwd is nodeinv
fwdm[key] = nodeinv
return _WriteResult(key, val, oldkey, oldval)
- def _undo_write(self, dedup_result: _DedupResult, write_result: _DedupResult) -> None: # pylint: disable=too-many-locals
+ def _undo_write(self, dedup_result: _DedupResult, write_result: _WriteResult) -> None:
fwdm = self._fwdm
invm = self._invm
isdupkey, isdupval, nodeinv, nodefwd = dedup_result
@@ -270,24 +276,26 @@ def _undo_write(self, dedup_result: _DedupResult, write_result: _DedupResult) ->
fwdm[oldkey] = nodeinv
assert invm[val] is nodeinv
- def __iter__(self, reverse=False) -> Iterator[KT]:
+ def __iter__(self) -> _t.Iterator[KT]:
"""Iterator over the contained keys in insertion order."""
+ return self._iter()
+
+ def _iter(self, *, reverse: bool = False) -> _t.Iterator[KT]:
fwdm_inv = self._fwdm.inverse
- for node in self._sntl.__iter__(reverse=reverse):
+ for node in self._sntl._iter(reverse=reverse):
yield fwdm_inv[node]
- def __reversed__(self) -> Iterator[KT]:
+ def __reversed__(self) -> _t.Iterator[KT]:
"""Iterator over the contained keys in reverse insertion order."""
- for key in self.__iter__(reverse=True):
- yield key
+ yield from self._iter(reverse=True)
- def equals_order_sensitive(self, other: Any) -> bool:
+ def equals_order_sensitive(self, other: _t.Any) -> bool:
"""Order-sensitive equality check.
*See also* :ref:`eq-order-insensitive`
"""
# Same short-circuit as BidictBase.__eq__. Factoring out not worth function call overhead.
- if not isinstance(other, Mapping) or len(self) != len(other):
+ if not isinstance(other, _t.Mapping) or len(self) != len(other):
return False
return all(i == j for (i, j) in zip(self.items(), other.items()))
diff --git a/bidict/_orderedbidict.py b/bidict/_orderedbidict.py
index d919aed7..7f5d8efa 100644
--- a/bidict/_orderedbidict.py
+++ b/bidict/_orderedbidict.py
@@ -28,11 +28,11 @@
"""Provide :class:`OrderedBidict`."""
-from typing import Tuple
+import typing as _t
-from ._abc import KT, VT
from ._mut import MutableBidict
from ._orderedbase import OrderedBidictBase
+from ._typing import KT, VT
class OrderedBidict(OrderedBidictBase[KT, VT], MutableBidict[KT, VT]):
@@ -40,13 +40,17 @@ class OrderedBidict(OrderedBidictBase[KT, VT], MutableBidict[KT, VT]):
__slots__ = ()
+ if _t.TYPE_CHECKING: # pragma: no cover
+ @property
+ def inverse(self) -> 'OrderedBidict[VT, KT]': ...
+
def clear(self) -> None:
"""Remove all items."""
self._fwdm.clear()
self._invm.clear()
self._sntl.nxt = self._sntl.prv = self._sntl
- def popitem(self, last=True) -> Tuple[KT, VT]: # pylint: disable=arguments-differ
+ def popitem(self, last: bool = True) -> _t.Tuple[KT, VT]:
"""*x.popitem() → (k, v)*
Remove and return the most recently added item as a (key, value) pair
@@ -56,11 +60,11 @@ def popitem(self, last=True) -> Tuple[KT, VT]: # pylint: disable=arguments-diff
"""
if not self:
raise KeyError('mapping is empty')
- key = next((reversed if last else iter)(self))
+ key = next((reversed if last else iter)(self)) # type: ignore
val = self._pop(key)
return key, val
- def move_to_end(self, key: KT, last=True) -> None:
+ def move_to_end(self, key: KT, last: bool = True) -> None:
"""Move an existing key to the beginning or end of this ordered bidict.
The item is moved to the end if *last* is True, else to the beginning.
@@ -72,15 +76,15 @@ def move_to_end(self, key: KT, last=True) -> None:
node.nxt.prv = node.prv
sntl = self._sntl
if last:
- last = sntl.prv
- node.prv = last
+ lastnode = sntl.prv
+ node.prv = lastnode
node.nxt = sntl
- sntl.prv = last.nxt = node
+ sntl.prv = lastnode.nxt = node
else:
- first = sntl.nxt
+ firstnode = sntl.nxt
node.prv = sntl
- node.nxt = first
- sntl.nxt = first.prv = node
+ node.nxt = firstnode
+ sntl.nxt = firstnode.prv = node
# * Code review nav *
diff --git a/bidict/_sntl.py b/bidict/_sntl.py
deleted file mode 100644
index 56cec7ae..00000000
--- a/bidict/_sntl.py
+++ /dev/null
@@ -1,22 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright 2009-2020 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/.
-
-
-"""Provide sentinels used internally in bidict."""
-
-from enum import Enum
-
-
-class _Sentinel(Enum):
- #: The result of looking up a missing key (or inverse key).
- MISS = 'MISS'
-
- def __repr__(self):
- return f'<{self.name}>' # pragma: no cover
-
-
-_MISS = _Sentinel.MISS
diff --git a/bidict/_typing.py b/bidict/_typing.py
new file mode 100644
index 00000000..cfd385ff
--- /dev/null
+++ b/bidict/_typing.py
@@ -0,0 +1,33 @@
+# -*- coding: utf-8 -*-
+# Copyright 2009-2020 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/.
+
+
+"""Provide typing-related objects."""
+
+import typing as _t
+
+
+KT = _t.TypeVar('KT') # key type
+VT = _t.TypeVar('VT') # value type
+IterItems = _t.Iterable[_t.Tuple[KT, VT]]
+MapOrIterItems = _t.Union[_t.Mapping[KT, VT], IterItems[KT, VT]]
+
+DT = _t.TypeVar('DT') # typevar for 'default' arguments
+
+
+class _BareReprMeta(type):
+ def __repr__(cls) -> str: # pragma: no cover
+ return f'<{cls.__name__}>'
+
+
+class _NONE(metaclass=_BareReprMeta):
+ """Sentinel type used to represent 'missing'."""
+
+
+OKT = _t.Union[KT, _NONE] # optional key type
+OVT = _t.Union[VT, _NONE] # optional value type
+ODT = _t.Union[DT, _NONE] # optional default type
diff --git a/bidict/_util.py b/bidict/_util.py
index 09357487..f3100892 100644
--- a/bidict/_util.py
+++ b/bidict/_util.py
@@ -11,11 +11,13 @@
from collections.abc import Mapping
from itertools import chain, repeat
+from ._typing import KT, VT, IterItems, MapOrIterItems
+
_NULL_IT = repeat(None, 0) # repeat 0 times -> raise StopIteration from the start
-def _iteritems_mapping_or_iterable(arg):
+def _iteritems_mapping_or_iterable(arg: MapOrIterItems[KT, VT]) -> IterItems[KT, VT]:
"""Yield the items in *arg*.
If *arg* is a :class:`~collections.abc.Mapping`, return an iterator over its items.
@@ -24,7 +26,7 @@ def _iteritems_mapping_or_iterable(arg):
return iter(arg.items() if isinstance(arg, Mapping) else arg)
-def _iteritems_args_kw(*args, **kw):
+def _iteritems_args_kw(*args: MapOrIterItems[KT, VT], **kw: VT) -> IterItems[KT, VT]:
"""Yield the items from the positional argument (if given) and then any from *kw*.
:raises TypeError: if more than one positional argument is given.
@@ -39,11 +41,11 @@ def _iteritems_args_kw(*args, **kw):
itemchain = _iteritems_mapping_or_iterable(arg)
if kw:
iterkw = iter(kw.items())
- itemchain = chain(itemchain, iterkw) if itemchain else iterkw
- return itemchain or _NULL_IT
+ itemchain = chain(itemchain, iterkw) if itemchain else iterkw # type: ignore
+ return itemchain or _NULL_IT # type: ignore
-def inverted(arg):
+def inverted(arg: MapOrIterItems[KT, VT]) -> IterItems[VT, KT]:
"""Yield the inverse items of the provided object.
If *arg* has a :func:`callable` ``__inverted__`` attribute,
@@ -56,5 +58,5 @@ def inverted(arg):
"""
inv = getattr(arg, '__inverted__', None)
if callable(inv):
- return inv()
+ return inv() # type: ignore
return ((val, key) for (key, val) in _iteritems_mapping_or_iterable(arg))
diff --git a/bidict/metadata.py b/bidict/metadata.py
index afcf9a97..894dfefb 100644
--- a/bidict/metadata.py
+++ b/bidict/metadata.py
@@ -8,25 +8,25 @@
"""Define bidict package metadata."""
-__version__ = '0.0.0.VERSION_NOT_FOUND'
-
# _version.py is generated by setuptools_scm (via its `write_to` param, see setup.py)
try:
- from ._version import version as __version__ # pylint: disable=unused-import
+ from ._version import version
except (ImportError, ValueError, SystemError): # pragma: no cover
try:
import pkg_resources
except ImportError:
- pass
+ __version__ = '0.0.0.VERSION_NOT_FOUND'
else:
try:
__version__ = pkg_resources.get_distribution('bidict').version
except pkg_resources.DistributionNotFound:
- pass
+ __version__ = '0.0.0.VERSION_NOT_FOUND'
+else: # pragma: no cover
+ __version__ = version
try:
__version_info__ = tuple(int(p) if i < 3 else p for (i, p) in enumerate(__version__.split('.')))
-except Exception: # noqa: E722; pragma: no cover; pylint: disable=broad-except
+except Exception: # pragma: no cover
__vesion_info__ = (0, 0, 0, f'PARSE FAILURE: __version__={__version__!r}')
__author__ = 'Joshua Bronson'
diff --git a/docs/api.rst b/docs/api.rst
index 9da858e3..2a4a21bf 100644
--- a/docs/api.rst
+++ b/docs/api.rst
@@ -34,12 +34,3 @@ bidict
.. attribute:: __version_info__
The version of bidict represented as a tuple.
-
-
-bidict.compat
--------------
-
-.. automodule:: bidict.compat
- :members:
- :member-order: bysource
- :undoc-members:
diff --git a/docs/conf.py b/docs/conf.py
index dbe8deba..4fab8ecd 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -23,15 +23,12 @@
import os
-# pylint: disable=invalid-name
-
-
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
sys.path.insert(0, os.path.abspath('..'))
-import bidict # noqa: E402; pylint: disable=wrong-import-position
+import bidict
# -- General configuration ------------------------------------------------
@@ -41,8 +38,7 @@
#needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
-# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
-# ones.
+# extensions coming with Sphinx (named 'sphinx.ext.*') or custom ones.
extensions = [
'alabaster',
'sphinx.ext.autodoc',
@@ -52,6 +48,7 @@
'sphinx.ext.intersphinx',
'sphinx.ext.viewcode',
'sphinx.ext.todo',
+ 'sphinx_autodoc_typehints',
]
intersphinx_mapping = {'python': ('https://docs.python.org/3', None)}
@@ -72,7 +69,7 @@
# General information about the project.
project = 'bidict'
author = bidict.__author__
-copyright = bidict.__copyright__ # pylint: disable=redefined-builtin
+copyright = bidict.__copyright__
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
@@ -144,7 +141,7 @@
show_powered_by=False,
show_relbar_bottom=True,
donate_url='https://gumroad.com/l/bidict',
- tidelift_url='https://tidelift.com/subscription/pkg/pypi-bidict?utm_source=pypi-bidict&utm_medium=referral&utm_campaign=docs', # noqa: E501; pylint: disable=line-too-long
+ tidelift_url='https://tidelift.com/subscription/pkg/pypi-bidict?utm_source=pypi-bidict&utm_medium=referral&utm_campaign=docs',
)
# Add any paths that contain custom themes here, relative to this directory.
@@ -309,6 +306,7 @@
# Ignore urls matching these regex strings when doing "make linkcheck"
linkcheck_ignore = [
+ r'http://ignore\.com/.*'
]
linkcheck_timeout = 30 # 5s default too low
@@ -321,5 +319,5 @@
def setup(app):
- """https://docs.readthedocs.io/en/latest/guides/adding-custom-css.html#adding-custom-css-or-javascript-to-a-sphinx-project""" # noqa: E501; pylint: disable=line-too-long
- app.add_javascript('custom.js')
+ """https://docs.readthedocs.io/en/latest/guides/adding-custom-css.html#adding-custom-css-or-javascript-to-a-sphinx-project"""
+ app.add_js_file('custom.js')
diff --git a/mypy.ini b/mypy.ini
new file mode 100644
index 00000000..ac8797fe
--- /dev/null
+++ b/mypy.ini
@@ -0,0 +1,22 @@
+[mypy]
+# Be flexible about dependencies that don't have stubs yet (like pytest)
+ignore_missing_imports = True
+
+# Be strict about use of Mypy
+warn_unused_ignores = True
+warn_unused_configs = True
+warn_redundant_casts = True
+warn_return_any = True
+
+# Avoid subtle backsliding
+# disallow_any_decorated = True
+disallow_incomplete_defs = True
+disallow_subclassing_any = True
+
+# Enable gradually / for new modules
+check_untyped_defs = False
+disallow_untyped_calls = False
+disallow_untyped_defs = False
+
+# DO NOT use `ignore_errors`; it doesn't apply
+# downstream and users have to deal with them.
diff --git a/setup.cfg b/setup.cfg
index e6799915..360abd84 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -12,18 +12,34 @@ universal = 1
# http://flake8.pycqa.org/en/latest/user/configuration.html
[flake8]
-ignore =
- # too many leading '#' for block comment
- E266,
- # block comment should start with '# '
- E265,
+exclude =
+ __pycache__,
+ .git,
+ .tox
+ bidict/_version.py,
+ docs/conf.py,
-max-line-length = 125
+ignore =
+ E265, # block comment should start with '# '
+ E266, # too many leading '#' for block comment
+ E301, # expect 1 blank line, found 0 (prevents idiomatic type hints)
+ E402, # import not at top of file
+ E501, # line too long
+ E704, # more than one statement on a single line (prevents idiomatic type hints)
+ E722, # broad except
+ F811, # redefinition of unused ... (prevents idiomatic type hints)
# https://pydocstyle.readthedocs.io/en/latest/snippets/config.html
[pydocstyle]
-add_ignore = D105,D205,D400,D401,D402
+add_ignore =
+ D102, # missing docstring in public method (prevents idiomatic type hints)
+ D107, # missing docstring in __init__ (prevents idiomatic type hints)
+ D105,
+ D205,
+ D400,
+ D401,
+ D402
# pylint config is in .pylintrc
diff --git a/setup.py b/setup.py
index 0bffbeee..9f8ad9e3 100644
--- a/setup.py
+++ b/setup.py
@@ -34,7 +34,7 @@
elif sys.version_info < (3, 6):
warn('This version of bidict is untested on Python < 3.6 and may not work.')
-from importlib.util import module_from_spec, spec_from_file_location # noqa: E402,E501; pylint: disable=wrong-import-order,wrong-import-position
+from importlib.util import module_from_spec, spec_from_file_location
CWD = abspath(dirname(__file__))
@@ -42,7 +42,7 @@
METADATA_PATH = join(CWD, 'bidict', 'metadata.py')
SPEC = spec_from_file_location('metadata', METADATA_PATH)
METADATA = module_from_spec(SPEC)
-SPEC.loader.exec_module(METADATA)
+SPEC.loader.exec_module(METADATA) # type: ignore
with c_open(join(CWD, 'README.rst'), encoding='utf-8') as f:
@@ -57,6 +57,7 @@
DOCS_REQS = [
'Sphinx < 4',
+ 'sphinx-autodoc-typehints < 2',
]
TEST_REQS = [
@@ -69,7 +70,7 @@
# pytest's doctest support doesn't support Sphinx extensions
# (https://www.sphinx-doc.org/en/latest/usage/extensions/doctest.html)
# so †est the code in the Sphinx docs using Sphinx's own doctest support.
- DOCS_REQS,
+ *DOCS_REQS,
]
# Split out coverage from test requirements since it slows down the tests.
@@ -80,7 +81,7 @@
PRECOMMIT_REQS = ['pre-commit < 3']
-DEV_REQS = SETUP_REQS + DOCS_REQS + TEST_REQS + COVERAGE_REQS + PRECOMMIT_REQS + ['tox < 4']
+DEV_REQS = SETUP_REQS + TEST_REQS + COVERAGE_REQS + PRECOMMIT_REQS + ['tox < 4']
EXTRAS_REQS = dict(
docs=DOCS_REQS,
@@ -97,16 +98,17 @@
'local_scheme': 'dirty-tag',
'write_to': 'bidict/_version.py',
},
- author=METADATA.__author__,
- author_email=METADATA.__email__,
- description=METADATA.__description__,
+ author=METADATA.__author__, # type: ignore
+ author_email=METADATA.__email__, # type: ignore
+ description=METADATA.__description__, # type: ignore
long_description=LONG_DESCRIPTION,
- keywords=METADATA.__keywords__,
- url=METADATA.__url__,
- license=METADATA.__license__,
+ long_description_content_type='text/x-rst',
+ keywords=METADATA.__keywords__, # type: ignore
+ url=METADATA.__url__, # type: ignore
+ license=METADATA.__license__, # type: ignore
packages=['bidict'],
zip_safe=False, # Don't zip. (We're zip-safe but prefer not to.)
- python_requires='>=3',
+ python_requires='>=3.6',
classifiers=[
'Development Status :: 4 - Beta',
'Intended Audience :: Developers',
diff --git a/tests/properties/_types.py b/tests/properties/_types.py
index ba2311a5..3a7459a3 100644
--- a/tests/properties/_types.py
+++ b/tests/properties/_types.py
@@ -8,12 +8,12 @@
"""Types for Hypothoses tests."""
from collections import OrderedDict
-from collections.abc import ItemsView, KeysView, Mapping
+from collections.abc import KeysView, ItemsView, Mapping
-from bidict import bidict, OrderedBidict, frozenbidict, FrozenOrderedBidict, namedbidict
+from bidict import FrozenOrderedBidict, OrderedBidict, bidict, frozenbidict, namedbidict
-MyNamedBidict = namedbidict('MyNamedBidict', 'key', 'val')
+MyNamedBidict = namedbidict('MyNamedBidict', 'key', 'val', base_type=bidict)
MyNamedFrozenBidict = namedbidict('MyNamedFrozenBidict', 'key', 'val', base_type=frozenbidict)
MyNamedOrderedBidict = namedbidict('MyNamedOrderedBidict', 'key', 'val', base_type=OrderedBidict)
MUTABLE_BIDICT_TYPES = (bidict, OrderedBidict, MyNamedBidict)
@@ -24,14 +24,14 @@
class _FrozenDict(KeysView, Mapping):
- def __init__(self, *args, **kw): # pylint: disable=super-init-not-called
+ def __init__(self, *args, **kw):
self._mapping = dict(*args, **kw)
def __getitem__(self, key):
return self._mapping[key]
def __hash__(self):
- return ItemsView(self._mapping)._hash() # pylint: disable=protected-access
+ return ItemsView(self._mapping)._hash()
NON_BIDICT_MAPPING_TYPES = (dict, OrderedDict, _FrozenDict)
diff --git a/tests/properties/test_properties.py b/tests/properties/test_properties.py
index e4fbbf48..bef0ef7c 100644
--- a/tests/properties/test_properties.py
+++ b/tests/properties/test_properties.py
@@ -123,7 +123,6 @@ def test_bijectivity(bi):
@given(st.BI_AND_CMPDICT_FROM_SAME_ITEMS, st.ARGS_BY_METHOD)
def test_consistency_after_method_call(bi_and_cmp_dict, args_by_method):
"""A bidict should be left in a consistent state after calling any method, even if it raises."""
- # pylint: disable=too-many-locals
bi_orig, cmp_dict_orig = bi_and_cmp_dict
for (_, methodname), args in args_by_method.items():
if not hasattr(bi_orig, methodname):
@@ -214,11 +213,9 @@ def test_orderedbidict_reversed(ob_and_od):
@given(st.FROZEN_BIDICTS)
def test_frozenbidicts_hashable(bi):
"""Frozen bidicts can be hashed and inserted into sets and mappings."""
- # Nothing to assert; making sure these calls don't raise is sufficient.
- # pylint: disable=pointless-statement
- hash(bi)
- {bi}
- {bi: bi}
+ assert hash(bi)
+ assert {bi}
+ assert {bi: bi}
@given(st.NAMEDBIDICT_NAMES_SOME_INVALID)
@@ -264,10 +261,8 @@ def test_bidict_isinv_getstate(bi):
"""All bidicts should provide ``_isinv`` and ``__getstate__``
(or else they won't fully work as a *base_type* for :func:`namedbidict`).
"""
- # Nothing to assert; making sure these calls don't raise is sufficient.
- # pylint: disable=pointless-statement
- bi._isinv
- bi.__getstate__()
+ bi._isinv # pylint: disable=pointless-statement
+ assert bi.__getstate__()
# Skip this test on PyPy where reference counting isn't used to free objects immediately. See: