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: