From 1695def04e37dbc14a5170f13dc6c37d39f90e40 Mon Sep 17 00:00:00 2001 From: Joshua Bronson Date: Sat, 1 Aug 2020 14:26:08 -0400 Subject: [PATCH] Finish first pass at adding type hints. Fixes #93. --- .pre-commit-config.yaml | 12 +-- .pylintrc | 44 ++++++---- CHANGELOG.rst | 69 ++++++++++++++-- README.rst | 26 +++--- bidict/__init__.py | 22 +++-- bidict/_abc.py | 18 ++-- bidict/_base.py | 121 ++++++++++++++------------- bidict/_bidict.py | 10 ++- bidict/_compat.py | 18 ---- bidict/_delegating.py | 13 +-- bidict/_dup.py | 6 +- bidict/_frozenbidict.py | 16 +++- bidict/_frozenordered.py | 22 +++-- bidict/_mut.py | 36 +++++--- bidict/_named.py | 73 ++++++++-------- bidict/_orderedbase.py | 92 +++++++++++---------- bidict/_orderedbidict.py | 26 +++--- bidict/_sntl.py | 22 ----- bidict/_typing.py | 33 ++++++++ bidict/_util.py | 14 ++-- bidict/metadata.py | 12 +-- docs/api.rst | 11 +-- docs/conf.py | 28 ++++--- docs/extending.rst | 124 ++++++++++++++++++---------- docs/learning-from-bidict.rst | 44 +++++----- docs/thanks.rst | 11 ++- mypy.ini | 22 +++++ setup.cfg | 30 +++++-- setup.py | 24 +++--- tests/properties/_types.py | 10 +-- tests/properties/test_properties.py | 15 ++-- 31 files changed, 598 insertions(+), 426 deletions(-) delete mode 100644 bidict/_compat.py delete mode 100644 bidict/_sntl.py create mode 100644 bidict/_typing.py create mode 100644 mypy.ini 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..0b36c605 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -24,16 +24,67 @@ Tip: Subscribe to releases to be notified when new versions of ``bidict`` are released. -0.21.0 not yet released ------------------------ - -- Add :class:`bidict.MutableBidirectionalMapping`. - -- Add type hints. - -- Drop support for Python 3.5. +0.21.0 (not yet released) +------------------------- -- Remove the ``bidict.compat`` module. +- :mod:`bidict` now provides + `type hints `__! ⌨️ ✅ + + Adding type hints to :mod:`bidict` poses particularly interesting challenges + due to the combination of generic types, + dynamically-generated types + (such as :ref:`inverse bidict classes ` + and :func:`namedbidicts `), + and complicating optimizations + such as the use of slots and weakrefs. + + It didn't take long to hit bugs and missing features + in the state of the art for type hinting in Python today, + e.g. no support for + `higher-kinded types `__, + a `typing.Generic bug in Python 3.6 `__, etc. + + That said, this release should provide a solid foundation + for code using :mod:`bidict` that enables static type checking. + + As always, if you spot any opportunities to improve :mod:`bidict` + (including its new type hints), + please don't hesitate to 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 reaches end of life on 2020-09-13, + represents a tiny percentage of bidict downloads on + `PyPI Stats `__, + and lacks support for + `variable type hint syntax `__, + `ordered dicts `__, + and :attr:`object.__init_subclass__`. + +- Remove the no-longer-needed ``bidict.compat`` module. + +- Move :ref:`inverse bidict class access ` + from a property to an attribute set in + :attr:`~bidict.BidictBase.__init_subclass__`, + to save function call overhead on repeated access. + +- :meth:`bidict.OrderedBidictBase.__iter__` no longer accepts + a ``reverse`` keyword argument so that it matches the signature of + :meth:`container.__iter__`. + +- Set the ``__module__`` attribute of various :mod:`bidict` types + (using :func:`sys._getframe` when necessary) + so that private, internal modules are not exposed + e.g. in classes' repr strings. + +- :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..a5ec27f8 100644 --- a/README.rst +++ b/README.rst @@ -39,9 +39,6 @@ Status .. image:: https://img.shields.io/lgtm/alerts/github/jab/bidict.svg :target: https://lgtm.com/projects/g/jab/bidict/ :alt: LGTM alerts - .. image:: https://api.codacy.com/project/badge/Grade/6628756a73254cd895656348236833b8 - :target: https://www.codacy.com/app/jab/bidict - :alt: Codacy grade .. image:: https://bestpractices.coreinfrastructure.org/projects/2354/badge :target: https://bestpractices.coreinfrastructure.org/en/projects/2354 :alt: CII best practices badge @@ -62,24 +59,25 @@ Status :target: https://raw.githubusercontent.com/jab/bidict/master/LICENSE :alt: License -.. image:: https://img.shields.io/badge/dynamic/json.svg?label=downloads&url=https%3A%2F%2Fpypistats.org%2Fapi%2Fpackages%2Fbidict%2Frecent%3Fperiod%3Dmonth&query=%24.data.last_month&colorB=blue&suffix=%2fmonth - :target: https://pypistats.org/packages/bidict - :alt: Downloads past month +.. image:: https://static.pepy.tech/badge/bidict + :target: https://pepy.tech/project/bidict + :alt: PyPI Downloads ``bidict``: ^^^^^^^^^^^ - has been used for many years by several teams at - Google, Venmo, CERN, Bank of America Merrill Lynch, Bloomberg, Two Sigma, and others + **Google, Venmo, CERN, Bank of America Merrill Lynch, Bloomberg, Two Sigma,** and many others - has carefully designed APIs for - 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 -- 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 -- has extensive docs and test coverage + **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.abc`` interfaces +- provides **type hints** for all public APIs +- is implemented in **concise, well-factored, pure (PyPy-compatible) Python code** + that is **optimized for running efficiently** + as well as for **reading and learning** [#fn-learning]_ +- has **extensive docs and test coverage** (including property-based tests and benchmarks) run continuously on all supported Python versions and OSes diff --git a/bidict/__init__.py b/bidict/__init__.py index 20e0a25a..58c447e7 100644 --- a/bidict/__init__.py +++ b/bidict/__init__.py @@ -58,7 +58,6 @@ # 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 ._abc import BidirectionalMapping, MutableBidirectionalMapping from ._base import BidictBase @@ -69,20 +68,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..3f7cd23e 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]): @@ -61,13 +59,35 @@ class BidictBase(BidirectionalMapping[KT, VT]): #: *See also* :ref:`basic-usage:Values Must Be Unique`, :doc:`extending` on_dup = ON_DUP_DEFAULT - _fwdm_cls = dict - _invm_cls = dict + _fwdm_cls = dict #: class of the backing forward mapping + _invm_cls = dict #: class of the backing inverse mapping #: 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_subclass__(cls, **kw): + super().__init_subclass__(**kw) + # Compute and set _inv_cls, the inverse of this bidict class. + if '_inv_cls' in cls.__dict__: + return + if cls._fwdm_cls is cls._invm_cls: + cls._inv_cls = cls + return + inv_cls = type(cls.__name__ + 'Inv', cls.__bases__, { + **cls.__dict__, + '_inv_cls': cls, + '_fwdm_cls': cls._invm_cls, + '_invm_cls': cls._fwdm_cls, + }) + cls._inv_cls = inv_cls + + @_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,22 +95,19 @@ 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): - # 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() + def _init_inv(self) -> None: # Create the inverse bidict instance via __new__, bypassing its __init__ so that its # _fwdm and _invm can be assigned to this bidict's _invm and _fwdm. Store it in self._inv, # which holds a strong reference to a bidict's inverse, if one is available. - self._inv = inv = inv_cls.__new__(inv_cls) + self._inv = inv = self._inv_cls.__new__(self._inv_cls) # type: ignore inv._fwdm = self._invm inv._invm = self._fwdm # Only give the inverse a weak reference to this bidict to avoid creating a reference cycle, @@ -101,22 +118,8 @@ def _init_inv(self): # Since this bidict has a strong reference to its inverse already, set its _invweak to None. self._invweak = None - @classmethod - def _inv_cls(cls): - """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 - if not getattr(cls, '_inv_cls_', None): - class _Inv(cls): - _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_ - @property - def _isinv(self): + def _isinv(self) -> bool: return self._inv is None @property @@ -125,9 +128,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 +141,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 +157,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 +176,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 +190,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 +210,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 +228,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 +267,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 +286,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 +300,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 +347,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 +358,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 +371,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 +380,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..292910d6 --- /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..1918626f 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -11,9 +11,9 @@ bidict :imported-members: :member-order: bysource :special-members: + :private-members: :show-inheritance: :undoc-members: - :exclude-members: __abstractmethods__,__dict__,__module__,__weakref__ :inherited-members: .. autodata:: bidict.RAISE @@ -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..4555f105 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,8 +48,12 @@ 'sphinx.ext.intersphinx', 'sphinx.ext.viewcode', 'sphinx.ext.todo', + 'sphinx_autodoc_typehints', ] +# https://github.com/agronholm/sphinx-autodoc-typehints#options +set_type_checking_flag = True + intersphinx_mapping = {'python': ('https://docs.python.org/3', None)} #todo_include_todos = True @@ -72,7 +72,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 +144,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,8 +309,14 @@ # Ignore urls matching these regex strings when doing "make linkcheck" linkcheck_ignore = [ + r'https://codecov\.io/.*', # gives 405 for HEAD requests + r'https://pypistats\.org/.*', # unreliable + # alternative links for readers on GitHub (which don't work on readthedocs.io): + r'CODE_OF_CONDUCT\.rst', + r'docs/intro\.rst', + r'docs/learning-from-bidict\.rst', ] -linkcheck_timeout = 30 # 5s default too low +linkcheck_timeout = 10 # 5s default too low # http://www.sphinx-doc.org/en/stable/ext/autosectionlabel.html#configuration autosectionlabel_prefix_document = True @@ -321,5 +327,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/docs/extending.rst b/docs/extending.rst index 31b8de0e..14f59c56 100644 --- a/docs/extending.rst +++ b/docs/extending.rst @@ -101,21 +101,16 @@ but the excellent `sortedcontainers `__ and `sortedcollections `__ libraries do. -Armed with these along with bidict's -:attr:`~bidict.BidictBase._fwdm_cls` -and -:attr:`~bidict.BidictBase._invm_cls` -attributes, -creating a sorted bidict type is dead simple: + +Armed with these, along with :class:`~bidict.BidictBase`'s +:attr:`~bidict.BidictBase._fwdm_cls` (forward mapping class) and +:attr:`~bidict.BidictBase._invm_cls` (inverse mapping class) attributes, +creating a sorted bidict is simple: .. doctest:: - >>> # As an optimization, bidict.bidict includes a mixin class that - >>> # we can't use here (namely bidict._delegating_mixins._DelegateKeysAndItemsToFwdm), - >>> # so extend the main parent class, bidict.MutableBidict, instead. >>> from bidict import MutableBidict - - >>> import sortedcontainers + >>> from sortedcontainers import SortedDict >>> class SortedBidict(MutableBidict): ... """A sorted bidict whose forward items stay sorted by their keys, @@ -124,8 +119,8 @@ creating a sorted bidict type is dead simple: ... in different orders. ... """ ... __slots__ = () - ... _fwdm_cls = sortedcontainers.SortedDict - ... _invm_cls = sortedcontainers.SortedDict + ... _fwdm_cls = SortedDict + ... _invm_cls = SortedDict ... _repr_delegate = list >>> b = SortedBidict({'Tokyo': 'Japan', 'Cairo': 'Egypt'}) @@ -149,52 +144,88 @@ will yield their items in *the same* order: .. doctest:: - >>> import sortedcollections + >>> from sortedcollections import ValueSortedDict >>> class KeySortedBidict(MutableBidict): ... __slots__ = () - ... _fwdm_cls = sortedcontainers.SortedDict - ... _invm_cls = sortedcollections.ValueSortedDict + ... _fwdm_cls = SortedDict + ... _invm_cls = ValueSortedDict ... _repr_delegate = list - >>> element_by_atomic_number = KeySortedBidict({ + >>> elem_by_atomicnum = KeySortedBidict({ ... 3: 'lithium', 1: 'hydrogen', 2: 'helium'}) >>> # stays sorted by key: - >>> element_by_atomic_number + >>> elem_by_atomicnum KeySortedBidict([(1, 'hydrogen'), (2, 'helium'), (3, 'lithium')]) >>> # .inverse stays sorted by value: - >>> list(element_by_atomic_number.inverse.items()) + >>> list(elem_by_atomicnum.inverse.items()) [('hydrogen', 1), ('helium', 2), ('lithium', 3)] - >>> element_by_atomic_number[4] = 'beryllium' + >>> elem_by_atomicnum[4] = 'beryllium' - >>> list(element_by_atomic_number.inverse.items()) + >>> list(elem_by_atomicnum.inverse.items()) [('hydrogen', 1), ('helium', 2), ('lithium', 3), ('beryllium', 4)] - >>> # This works because a bidict whose _fwdm_cls differs from its _invm_cls computes - >>> # its inverse class -- which (note) is not actually the same class as the original, - >>> # as it needs to have its _fwdm_cls and _invm_cls swapped -- automatically. - >>> # You can see this if you inspect the inverse bidict: - >>> element_by_atomic_number.inverse # Note the different class, which was auto-generated: - KeySortedBidictInv([('hydrogen', 1), ('helium', 2), ('lithium', 3), ('beryllium', 4)]) - >>> ValueSortedBidict = element_by_atomic_number.inverse.__class__ - >>> ValueSortedBidict._fwdm_cls - - >>> ValueSortedBidict._invm_cls - - - >>> # Round trips work as expected: - >>> atomic_number_by_element = ValueSortedBidict(element_by_atomic_number.inverse) - >>> atomic_number_by_element + +Dynamic Inverse Class Generation +:::::::::::::::::::::::::::::::: + +When a bidict class's +:attr:`~bidict.BidictBase._fwdm_cls` and +:attr:`~bidict.BidictBase._invm_cls` +are the same, +the bidict class is its own inverse class. +(This is the case for all the +:ref:`bidict classes ` +that come with :mod:`bidict`.) + +However, when a bidict's +:attr:`~bidict.BidictBase._fwdm_cls` and +:attr:`~bidict.BidictBase._invm_cls` differ, +the inverse class of the bidict +needs to have its +:attr:`~bidict.BidictBase._fwdm_cls` and +:attr:`~bidict.BidictBase._invm_cls` swapped. + +:class:`~bidict.BidictBase` detects this +and dynamically computes the correct inverse class for you automatically. + +You can see this if you inspect the inverse bidict: + + >>> elem_by_atomicnum.__class__.__name__ + 'KeySortedBidict' + >>> elem_by_atomicnum.inverse.__class__.__name__ + 'KeySortedBidictInv' + +Notice that :class:`~bidict.BidictBase` automatically created a +``KeySortedBidictInv`` class and used it for the inverse bidict. + +As expected, ``KeySortedBidictInv``'s +:attr:`~bidict.BidictBase._fwdm_cls` and +:attr:`~bidict.BidictBase._invm_cls` +are the opposite of ``KeySortedBidict``'s: + + >>> elem_by_atomicnum.inverse._fwdm_cls.__name__ + 'ValueSortedDict' + >>> elem_by_atomicnum.inverse._invm_cls.__name__ + 'SortedDict' + +:class:`~bidict.BidictBase` also ensures that round trips work as expected: + + >>> KeySortedBidictInv = elem_by_atomicnum.inverse.__class__ # i.e. a value-sorted bidict + >>> atomicnum_by_elem = KeySortedBidictInv(elem_by_atomicnum.inverse) + >>> atomicnum_by_elem KeySortedBidictInv([('hydrogen', 1), ('helium', 2), ('lithium', 3), ('beryllium', 4)]) - >>> KeySortedBidict(atomic_number_by_element.inverse) == element_by_atomic_number + >>> KeySortedBidict(atomicnum_by_elem.inverse) == elem_by_atomicnum True - >>> # One other useful trick: - >>> # To pass method calls through to the _fwdm SortedDict when not present - >>> # on the bidict instance, provide a custom __getattribute__ method: +You can even play tricks with attribute lookup redirection here too. +For example, to pass attribute access through to the backing ``_fwdm`` mapping +when an attribute is not provided by the bidict class itself, +you can override :meth:`~object.__getattribute__` as follows: + >>> def __getattribute__(self, name): ... try: ... return object.__getattribute__(self, name) @@ -203,11 +234,16 @@ will yield their items in *the same* order: >>> KeySortedBidict.__getattribute__ = __getattribute__ - >>> # bidict has no .peekitem attr, so the call is passed through to _fwdm: - >>> element_by_atomic_number.peekitem() +Now, even though this ``KeySortedBidict`` itself provides no ``peekitem`` attribute, +the following call still succeeds +because it's passed through to the backing ``SortedDict``: + + >>> elem_by_atomicnum.peekitem() (4, 'beryllium') - >>> element_by_atomic_number.inverse.peekitem() - ('beryllium', 4) +This goes to show how simple it can be +to compose your own bidirectional mapping types +out of the building blocks that :mod:`bidict` provides. + Next proceed to :doc:`other-functionality`. diff --git a/docs/learning-from-bidict.rst b/docs/learning-from-bidict.rst index b9b86e4f..faf8ea25 100644 --- a/docs/learning-from-bidict.rst +++ b/docs/learning-from-bidict.rst @@ -3,11 +3,12 @@ Learning from ``bidict`` Below is an outline of some of the more fascinating and lesser-known Python corners I got to explore further -thanks to working on bidict. +thanks to working on :mod:`bidict`. -If you are interested in learning more about any of the following, -I highly encourage you to -`read bidict's code `__. +If you would like to learn more about any of the topics below, +you may find `reading bidict's code +`__ +particularly interesting. I've sought to optimize the code not just for correctness and performance, but also to make for a clear and enjoyable read, @@ -21,7 +22,8 @@ it's brought me. 😊 Python syntax hacks =================== -Bidict used to support (ab)using a specialized form of Python's :ref:`slice ` syntax +:mod:`bidict` used to support +(ab)using a specialized form of Python's :ref:`slice ` syntax for getting and setting keys by value: .. code-block:: python @@ -40,7 +42,8 @@ and `#19 `__ for why this was dropped. Code structure ============== -Bidicts come in every combination of mutable, immutable, ordered, and unordered types, +:class:`~bidict.bidict`\s come in every combination of +mutable, immutable, ordered, and unordered types, implementing Python's various :class:`relevant ` :class:`collections ` @@ -147,7 +150,7 @@ Python surprises, gotchas, regrets that contain the same items but in a different order? What about when comparing with an unordered mapping? - Check out what Python's :class:`~collections.OrderedDict` does, + Check out what Python's :class:`collections.OrderedDict` does, and the surprising results: .. code-block:: python @@ -234,8 +237,7 @@ you can subclass a :func:`~collections.namedtuple` class. Just make sure to include ``__slots__ = ()``, or you'll lose a lot of the performance benefits. -``_marker.py`` contains a small example. -Here's a larger one: +Here's an example: .. doctest:: @@ -298,8 +300,7 @@ How to deeply integrate with Python's :mod:`collections` and other built-in APIs ``issubclass(Foo, Hashable)`` will always be True, no need to explicitly subclass via ``class Foo(Hashable): ...`` -- How to make your own open ABC like :class:`~collections.abc.Hashable`, - i.e. how does :class:`~bidict.BidirectionalMapping` work? +- How to make your own open ABC like :class:`~collections.abc.Hashable`? - Override :meth:`~abc.ABCMeta.__subclasshook__` to check for the interface you require. @@ -315,12 +316,8 @@ How to deeply integrate with Python's :mod:`collections` and other built-in APIs :class:`list` is a subclass of :class:`object`, but :class:`list` is not a subclass of :class:`~collections.abc.Hashable`. -- What if we needed to add a second metaclass - in addition to :class:`~bidict.BidirectionalMapping` - (e.g. to conditionally implement an optimized version of some methods - based on the type of ``_fwmd_cls``, - as ``_delegating.py`` currently does without a metaclass)? - Would have to be careful to avoid +- What if you needed to derive from a second metaclass? + Be careful to avoid "TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases". See the great write-up in @@ -329,14 +326,14 @@ How to deeply integrate with Python's :mod:`collections` and other built-in APIs - :class:`collections.abc.Mapping` and :class:`collections.abc.MutableMapping` don't implement :meth:`~abc.ABCMeta.__subclasshook__`, - so must either explicitly subclass - (if you want to inherit any of their implementations) + so you must either explicitly subclass them + (in which case you inherit their concrete method implementations) or use :meth:`abc.ABCMeta.register` - (to register as a virtual subclass without inheriting any implementation) + (to register as a virtual subclass without inheriting any of the implementation). -- Notice we have :class:`collections.abc.Reversible` +- Notice that Python provides :class:`collections.abc.Reversible` but no ``collections.abc.Ordered`` or ``collections.abc.OrderedMapping``. - Proposed in `bpo-28912 `__ but rejected. + This was proposed in `bpo-28912 `__ but rejected. Would have been useful for bidict's ``__repr__()`` implementation (see ``_base.py``), and potentially for interop with other ordered mapping implementations such as `SortedDict `__. @@ -468,7 +465,7 @@ Portability attribute off the borrowed method to avoid getting ``TypeError: unbound method ...() must be called with ... instance as first argument`` - See the `implementation `__ + See the `old implementation `__ of :class:`~bidict.FrozenOrderedBidict`. - CPython vs. PyPy @@ -491,7 +488,6 @@ Other interesting stuff in the standard library - :mod:`reprlib` and :func:`reprlib.recursive_repr` (but not needed for bidict because there's no way to insert a bidict into itself) - :func:`operator.methodcaller` -- :attr:`platform.python_implementation` - See :ref:`addendum:Missing \`\`bidict\`\`\\s in the Standard Library` diff --git a/docs/thanks.rst b/docs/thanks.rst index 5311ff75..5edf47fd 100644 --- a/docs/thanks.rst +++ b/docs/thanks.rst @@ -44,17 +44,16 @@ People Projects ======== -- `Tidelift `__ +- `Tidelift `__ - `Pytest `__ - `Coverage `__ - `hypothesis `__ - `pytest-benchmark `__ - `Sphinx `__ - `Travis `__ -- `Readthedocs `__ -- `Codecov `__ -- `lgtm `__ +- `Readthedocs `__ +- `Codecov `__ - `Pylint `__ -- `Flake8 `__ -- `Alabaster `__ +- `Flake8 `__ +- `Alabaster `__ - `setuptools_scm `__ 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..2b57e295 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, # expected 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: