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..1dcb44e9 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1,24 +1,29 @@ # https://docs.pylint.org/en/latest/technical_reference/features.html -[MESSAGES CONTROL] -disable = - invalid-name, - no-member, - too-few-public-methods, - 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 +[MASTER] +ignore=_version.py conf.py # Maximum number of arguments for a function. # The default of 5 only leaves room for 4 args besides self for methods. max-args=6 +jobs=0 + +[MESSAGES CONTROL] +disable= + abstract-method, + arguments-differ, + attribute-defined-outside-init, + bad-continuation, + invalid-name, + isinstance-second-argument-not-valid-type, # https://github.com/PyCQA/pylint/issues/3507 + no-init, + no-member, + not-callable, + protected-access, + too-few-public-methods, + too-many-ancestors, + [FORMAT] -max-line-length=125 +max-line-length=140 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/_abc.py b/bidict/_abc.py index 1945807b..0e891572 100644 --- a/bidict/_abc.py +++ b/bidict/_abc.py @@ -28,17 +28,28 @@ """Provide the :class:`BidirectionalMapping` abstract base class.""" +import typing as _t from abc import abstractmethod -from typing import AbstractSet, Iterator, Mapping, MutableMapping, Tuple, TypeVar -KT = TypeVar('KT') -VT = TypeVar('VT') +class _SntlMeta(type): + def __repr__(cls): + return f'<{cls.__name__}>' -# pylint: disable=abstract-method,no-init +class _NONE(metaclass=_SntlMeta): + """Private sentinel type, used to represent e.g. missing values.""" -class BidirectionalMapping(Mapping[KT, VT]): + +KT = _t.TypeVar('KT') # key type +VT = _t.TypeVar('VT') # value type +OKT = _t.Union[KT, _NONE] # optional key type +OVT = _t.Union[VT, _NONE] # optional value type +IterItems = _t.Iterable[_t.Tuple[KT, VT]] +MapOrIterItems = _t.Union[_t.Mapping[KT, VT], IterItems[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 +77,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 +93,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 +105,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..c8f20e04 100644 --- a/bidict/_base.py +++ b/bidict/_base.py @@ -28,25 +28,24 @@ """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 _NONE, KT, VT, OKT, OVT, BidirectionalMapping, MapOrIterItems from ._dup import ON_DUP_DEFAULT, RAISE, DROP_OLD, DROP_NEW, OnDup from ._exc import ( DuplicationError, KeyDuplicationError, ValueDuplicationError, KeyAndValueDuplicationError) -from ._sntl import _MISS 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) +_NODUP = _DedupResult(False, False, _NONE, _NONE) -T = TypeVar('T', bound='BidictBase') +T = _t.TypeVar('T', bound='BidictBase') class BidictBase(BidirectionalMapping[KT, VT]): @@ -67,7 +66,7 @@ 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 + def __init__(self, *args: MapOrIterItems[KT, VT], **kw: VT) -> None: # pylint: disable=super-init-not-called """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,10 +74,10 @@ 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) @@ -102,21 +101,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,14 +124,14 @@ 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 + inv = self._invweak() if inv is not None: - return inv + return inv # type: ignore # Refcount of referent must have dropped to zero, as in `bidict().inv.inv`. Init a new one. self._init_inv() # Now this bidict will retain a strong ref to its inverse. - return self._inv + return self._inv # type: ignore #: Alias for :attr:`inverse`. inv = inverse @@ -172,7 +171,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 +185,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 +205,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*. @@ -227,10 +226,10 @@ def _dedup_item(self, key: KT, val: VT, on_dup: OnDup) -> Optional[_DedupResult] # 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 +263,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 +282,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 +296,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 @@ -355,7 +354,7 @@ 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: """Used for the copy protocol. @@ -368,7 +367,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) diff --git a/bidict/_bidict.py b/bidict/_bidict.py index 625416cb..8864e222 100644 --- a/bidict/_bidict.py +++ b/bidict/_bidict.py @@ -28,6 +28,8 @@ """Provide :class:`bidict`.""" +import typing as _t + from ._base import KT, VT from ._mut import MutableBidict from ._delegating import _DelegatingBidict @@ -38,6 +40,11 @@ class bidict(_DelegatingBidict[KT, VT], MutableBidict[KT, VT]): __slots__ = () + if _t.TYPE_CHECKING: + @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..aa2a0645 100644 --- a/bidict/_delegating.py +++ b/bidict/_delegating.py @@ -8,7 +8,7 @@ """Provide :class:`_DelegatingBidict`.""" -from typing import Iterator, KeysView, ItemsView +import typing as _t from ._base import BidictBase, KT, VT @@ -21,18 +21,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/_frozenbidict.py b/bidict/_frozenbidict.py index b97bc6c8..2efdc344 100644 --- a/bidict/_frozenbidict.py +++ b/bidict/_frozenbidict.py @@ -27,7 +27,9 @@ """Provide :class:`frozenbidict`, an immutable, hashable bidirectional mapping type.""" -from ._delegating import _DelegatingBidict, KT, VT, ItemsView +import typing as _t + +from ._delegating import _DelegatingBidict, KT, VT class frozenbidict(_DelegatingBidict[KT, VT]): @@ -35,11 +37,19 @@ 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: + @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..4dd389f9 100644 --- a/bidict/_frozenordered.py +++ b/bidict/_frozenordered.py @@ -27,32 +27,43 @@ """Provide :class:`FrozenOrderedBidict`, an immutable, hashable, ordered bidict.""" +import typing as _t + +from ._abc import KT, VT from ._frozenbidict import frozenbidict from ._orderedbase import OrderedBidictBase -class FrozenOrderedBidict(OrderedBidictBase): +class FrozenOrderedBidict(OrderedBidictBase[KT, VT]): """Hashable, immutable, ordered bidict type.""" __slots__ = () __hash__ = frozenbidict.__hash__ + if _t.TYPE_CHECKING: + @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..cbcb8070 100644 --- a/bidict/_mut.py +++ b/bidict/_mut.py @@ -28,12 +28,11 @@ """Provide :class:`bidict`.""" -from typing import Any, Tuple +import typing as _t -from ._abc import MutableBidirectionalMapping, KT, VT +from ._abc import _NONE, MapOrIterItems, MutableBidirectionalMapping, KT, VT 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 class MutableBidict(BidictBase[KT, VT], MutableBidirectionalMapping[KT, VT]): @@ -41,6 +40,11 @@ class MutableBidict(BidictBase[KT, VT], MutableBidirectionalMapping[KT, VT]): __slots__ = () + if _t.TYPE_CHECKING: + @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,7 @@ def clear(self) -> None: self._fwdm.clear() self._invm.clear() - def pop(self, key: KT, default=_MISS) -> Any: + def pop(self, key: KT, default: _t.Any = _NONE) -> _t.Any: """*x.pop(k[, d]) → v* Remove specified key and return the corresponding value. @@ -122,11 +126,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 +143,16 @@ def popitem(self) -> Tuple[KT, VT]: del self._invm[val] return key, val - def update(self, *args, **kw) -> None: # pylint: disable=signature-differs + def update(self, *args: MapOrIterItems[KT, VT], **kw: VT) -> None: # pylint: disable=signature-differs """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..c0ea0443 100644 --- a/bidict/_named.py +++ b/bidict/_named.py @@ -7,11 +7,20 @@ """Provide :func:`bidict.namedbidict`.""" +import typing as _t +from sys import _getframe + from ._abc import BidirectionalMapping 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] = bidict, +) -> _t.Type[BidirectionalMapping]: r"""Create a new subclass of *base_type* with custom accessors. Analagous to :func:`collections.namedtuple`. @@ -51,7 +60,7 @@ def namedbidict(typename, keyname, valname, base_type=bidict): 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__ = () @@ -75,18 +84,23 @@ def __reduce__(self): 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.__module__ = _getframe(1).f_globals.get('__name__', '__unknown_module__') 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..64f5fd97 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 ._abc import _NONE, KT, VT, MapOrIterItems +from ._base import _DedupResult, _WriteResult, BidictBase, T from ._bidict import bidict -from ._sntl import _MISS class _Node: @@ -115,7 +115,7 @@ def __repr__(self): # pragma: no cover def __bool__(self): return False - def __iter__(self, reverse=False): + def _iter(self, *, reverse=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,6 +160,14 @@ def __init__(self, *args, **kw) -> None: # are inherited and are able to be reused without modification. super().__init__(*args, **kw) + if _t.TYPE_CHECKING: + @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 @@ -183,7 +191,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] @@ -195,14 +203,14 @@ def _pop(self, key: KT) -> VT: val = self._invm.inverse.pop(nodefwd) nodefwd.prv.nxt = nodefwd.nxt nodefwd.nxt.prv = nodefwd.prv - return val + return val # type: ignore @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: # pylint: disable=too-many-locals # Overrides _base.BidictBase. fwdm = self._fwdm # bidict mapping keys to nodes invm = self._invm # bidict mapping vals to nodes @@ -213,12 +221,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 +236,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: # pylint: disable=too-many-locals fwdm = self._fwdm invm = self._invm isdupkey, isdupval, nodeinv, nodefwd = dedup_result @@ -270,24 +278,27 @@ 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): + for key in self._iter(reverse=True): yield key - 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..b9c8eb89 100644 --- a/bidict/_orderedbidict.py +++ b/bidict/_orderedbidict.py @@ -28,7 +28,7 @@ """Provide :class:`OrderedBidict`.""" -from typing import Tuple +import typing as _t from ._abc import KT, VT from ._mut import MutableBidict @@ -40,13 +40,18 @@ class OrderedBidict(OrderedBidictBase[KT, VT], MutableBidict[KT, VT]): __slots__ = () + if _t.TYPE_CHECKING: + @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 +61,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 +77,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/_util.py b/bidict/_util.py index 09357487..b1d7cbd3 100644 --- a/bidict/_util.py +++ b/bidict/_util.py @@ -11,11 +11,13 @@ from collections.abc import Mapping from itertools import chain, repeat +from ._abc 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..1f93d1c2 100644 --- a/bidict/metadata.py +++ b/bidict/metadata.py @@ -8,8 +8,6 @@ """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 @@ -17,12 +15,12 @@ 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' try: __version_info__ = tuple(int(p) if i < 3 else p for (i, p) in enumerate(__version__.split('.'))) 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..0ae73133 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -41,8 +41,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 +51,7 @@ 'sphinx.ext.intersphinx', 'sphinx.ext.viewcode', 'sphinx.ext.todo', + 'sphinx_autodoc_typehints', ] intersphinx_mapping = {'python': ('https://docs.python.org/3', None)} @@ -309,6 +309,7 @@ # Ignore urls matching these regex strings when doing "make linkcheck" linkcheck_ignore = [ + r'http://ignore\.com/.*' ] linkcheck_timeout = 30 # 5s default too low 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..dcfdb68a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -18,7 +18,7 @@ ignore = # block comment should start with '# ' E265, -max-line-length = 125 +max-line-length = 140 # https://pydocstyle.readthedocs.io/en/latest/snippets/config.html diff --git a/setup.py b/setup.py index 0bffbeee..b5a9bd95 100644 --- a/setup.py +++ b/setup.py @@ -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',