Skip to content

Commit

Permalink
Finish first pass at adding type hints.
Browse files Browse the repository at this point in the history
Fixes #93.
  • Loading branch information
jab committed Aug 1, 2020
1 parent 8a8e58b commit 5a960e7
Show file tree
Hide file tree
Showing 22 changed files with 260 additions and 210 deletions.
12 changes: 4 additions & 8 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
35 changes: 20 additions & 15 deletions .pylintrc
Original file line number Diff line number Diff line change
@@ -1,24 +1,29 @@
# https://docs.pylint.org/en/latest/technical_reference/features.html

[MESSAGES CONTROL]
disable =
invalid-name,
no-member,
too-few-public-methods,
protected-access,
isinstance-second-argument-not-valid-type, # https://github.com/PyCQA/pylint/issues/3507

# bidict/_version.py is generated by setuptools_scm
ignore = _version.py

# Maximum number of parents for a class.
# The default of 7 results in "too-many-ancestors" for all bidict classes.
max-parents = 13
[MASTER]
ignore=_version.py conf.py

# Maximum number of arguments for a function.
# The default of 5 only leaves room for 4 args besides self for methods.
max-args=6

jobs=0

[MESSAGES CONTROL]
disable=
abstract-method,
arguments-differ,
attribute-defined-outside-init,
bad-continuation,
invalid-name,
isinstance-second-argument-not-valid-type, # https://github.com/PyCQA/pylint/issues/3507
no-init,
no-member,
not-callable,
protected-access,
too-few-public-methods,
too-many-ancestors,


[FORMAT]
max-line-length=125
max-line-length=140
1 change: 1 addition & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 20 additions & 9 deletions bidict/_abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,28 @@

"""Provide the :class:`BidirectionalMapping` abstract base class."""

import typing as _t
from abc import abstractmethod
from typing import AbstractSet, Iterator, Mapping, MutableMapping, Tuple, TypeVar


KT = TypeVar('KT')
VT = TypeVar('VT')
class _SntlMeta(type):
def __repr__(cls):
return f'<{cls.__name__}>'


# pylint: disable=abstract-method,no-init
class _NONE(metaclass=_SntlMeta):
"""Private sentinel type, used to represent e.g. missing values."""

class BidirectionalMapping(Mapping[KT, VT]):

KT = _t.TypeVar('KT') # key type
VT = _t.TypeVar('VT') # value type
OKT = _t.Union[KT, _NONE] # optional key type
OVT = _t.Union[VT, _NONE] # optional value type
IterItems = _t.Iterable[_t.Tuple[KT, VT]]
MapOrIterItems = _t.Union[_t.Mapping[KT, VT], IterItems[KT, VT]]


class BidirectionalMapping(_t.Mapping[KT, VT]):
"""Abstract base class (ABC) for bidirectional mapping types.
Extends :class:`collections.abc.Mapping` primarily by adding the
Expand Down Expand Up @@ -66,7 +77,7 @@ def inverse(self) -> 'BidirectionalMapping[VT, KT]':
# clear there's no reason to call this implementation (e.g. via super() after overriding).
raise NotImplementedError

def __inverted__(self) -> Iterator[Tuple[VT, KT]]:
def __inverted__(self) -> _t.Iterator[_t.Tuple[VT, KT]]:
"""Get an iterator over the items in :attr:`inverse`.
This is functionally equivalent to iterating over the items in the
Expand All @@ -82,7 +93,7 @@ def __inverted__(self) -> Iterator[Tuple[VT, KT]]:
"""
return iter(self.inverse.items())

def values(self) -> AbstractSet[VT]: # type: ignore[override]
def values(self) -> _t.KeysView[VT]: # type: ignore
"""A set-like object providing a view on the contained values.
Override the implementation inherited from
Expand All @@ -94,10 +105,10 @@ def values(self) -> AbstractSet[VT]: # type: ignore[override]
which has the advantages of constant-time containment checks
and supporting set operations.
"""
return self.inverse.keys()
return self.inverse.keys() # type: ignore


class MutableBidirectionalMapping(BidirectionalMapping[KT, VT], MutableMapping[KT, VT]):
class MutableBidirectionalMapping(BidirectionalMapping[KT, VT], _t.MutableMapping[KT, VT]):
"""Abstract base class (ABC) for mutable bidirectional mapping types."""

__slots__ = ()
Expand Down
69 changes: 34 additions & 35 deletions bidict/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,25 +28,24 @@

"""Provide :class:`BidictBase`."""

import typing as _t
from collections import namedtuple
from copy import copy
from typing import Any, Iterator, List, Mapping, Optional, Tuple, TypeVar
from weakref import ref

from ._abc import KT, VT, BidirectionalMapping
from ._abc import _NONE, KT, VT, OKT, OVT, BidirectionalMapping, MapOrIterItems
from ._dup import ON_DUP_DEFAULT, RAISE, DROP_OLD, DROP_NEW, OnDup
from ._exc import (
DuplicationError, KeyDuplicationError, ValueDuplicationError, KeyAndValueDuplicationError)
from ._sntl import _MISS
from ._util import _iteritems_args_kw


_DedupResult = namedtuple('_DedupResult', 'isdupkey isdupval invbyval fwdbykey')
_WriteResult = namedtuple('_WriteResult', 'key val oldkey oldval')
_NODUP = _DedupResult(False, False, _MISS, _MISS)
_NODUP = _DedupResult(False, False, _NONE, _NONE)


T = TypeVar('T', bound='BidictBase')
T = _t.TypeVar('T', bound='BidictBase')


class BidictBase(BidirectionalMapping[KT, VT]):
Expand All @@ -67,18 +66,18 @@ class BidictBase(BidirectionalMapping[KT, VT]):
#: The object used by :meth:`__repr__` for printing the contained items.
_repr_delegate = dict

def __init__(self, *args, **kw) -> None: # pylint: disable=super-init-not-called
def __init__(self, *args: MapOrIterItems[KT, VT], **kw: VT) -> None: # pylint: disable=super-init-not-called
"""Make a new bidirectional dictionary.
The signature behaves like that of :class:`dict`.
Items passed in are added in the order they are passed,
respecting the :attr:`on_dup` class attribute in the process.
"""
#: 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)
Expand All @@ -102,21 +101,21 @@ def _init_inv(self):
self._invweak = None

@classmethod
def _inv_cls(cls):
def _inv_cls(cls) -> '_t.Type[BidictBase[VT, KT]]':
"""The inverse of this bidict type, i.e. one with *_fwdm_cls* and *_invm_cls* swapped."""
if cls._fwdm_cls is cls._invm_cls:
return cls
return cls # type: ignore
if not getattr(cls, '_inv_cls_', None):
class _Inv(cls):
class _Inv(cls): # type: ignore
_fwdm_cls = cls._invm_cls
_invm_cls = cls._fwdm_cls
_inv_cls_ = cls
_Inv.__name__ = cls.__name__ + 'Inv'
cls._inv_cls_ = _Inv
return cls._inv_cls_
cls._inv_cls_ = _Inv # type: ignore
return cls._inv_cls_ # type: ignore

@property
def _isinv(self):
def _isinv(self) -> bool:
return self._inv is None

@property
Expand All @@ -125,14 +124,14 @@ def inverse(self) -> 'BidictBase[VT, KT]':
# Resolve and return a strong reference to the inverse bidict.
# One may be stored in self._inv already.
if self._inv is not None:
return self._inv
return self._inv # type: ignore
# Otherwise a weakref is stored in self._invweak. Try to get a strong ref from it.
inv = self._invweak() # pylint: disable=not-callable
inv = self._invweak()
if inv is not None:
return inv
return inv # type: ignore
# Refcount of referent must have dropped to zero, as in `bidict().inv.inv`. Init a new one.
self._init_inv() # Now this bidict will retain a strong ref to its inverse.
return self._inv
return self._inv # type: ignore

#: Alias for :attr:`inverse`.
inv = inverse
Expand Down Expand Up @@ -172,7 +171,7 @@ def __repr__(self) -> str:
# The inherited Mapping.__eq__ implementation would work, but it's implemented in terms of an
# inefficient ``dict(self.items()) == dict(other.items())`` comparison, so override it with a
# more efficient implementation.
def __eq__(self, other: Any) -> bool:
def __eq__(self, other: _t.Any) -> bool:
"""*x.__eq__(other) ⟺ x == other*
Equivalent to *dict(x.items()) == dict(other.items())*
Expand All @@ -186,10 +185,10 @@ def __eq__(self, other: Any) -> bool:
*See also* :meth:`bidict.FrozenOrderedBidict.equals_order_sensitive`
"""
if not isinstance(other, Mapping) or len(self) != len(other):
if not isinstance(other, _t.Mapping) or len(self) != len(other):
return False
selfget = self.get
return all(selfget(k, _MISS) == v for (k, v) in other.items())
return all(selfget(k, _NONE) == v for (k, v) in other.items()) # type: ignore

# The following methods are mutating and so are not public. But they are implemented in this
# non-mutable base class (rather than the mutable `bidict` subclass) because they are used here
Expand All @@ -206,7 +205,7 @@ def _put(self, key: KT, val: VT, on_dup: OnDup) -> None:
if dedup_result is not None:
self._write_item(key, val, dedup_result)

def _dedup_item(self, key: KT, val: VT, on_dup: OnDup) -> Optional[_DedupResult]:
def _dedup_item(self, key: KT, val: VT, on_dup: OnDup) -> _t.Optional[_DedupResult]:
"""Check *key* and *val* for any duplication in self.
Handle any duplication as per the passed in *on_dup*.
Expand All @@ -227,10 +226,10 @@ def _dedup_item(self, key: KT, val: VT, on_dup: OnDup) -> Optional[_DedupResult]
# pylint: disable=too-many-branches
fwdm = self._fwdm
invm = self._invm
oldval = fwdm.get(key, _MISS)
oldkey = invm.get(val, _MISS)
isdupkey = oldval is not _MISS
isdupval = oldkey is not _MISS
oldval: OVT = fwdm.get(key, _NONE)
oldkey: OKT = invm.get(val, _NONE)
isdupkey = oldval is not _NONE
isdupval = oldkey is not _NONE
dedup_result = _DedupResult(isdupkey, isdupval, oldkey, oldval)
if isdupkey and isdupval:
if self._already_have(key, val, oldkey, oldval):
Expand Down Expand Up @@ -264,7 +263,7 @@ def _dedup_item(self, key: KT, val: VT, on_dup: OnDup) -> Optional[_DedupResult]
return dedup_result

@staticmethod
def _already_have(key: KT, val: VT, oldkey: KT, oldval: VT) -> bool:
def _already_have(key: KT, val: VT, oldkey: OKT, oldval: OVT) -> bool:
# Overridden by _orderedbase.OrderedBidictBase.
isdup = oldkey == key
assert isdup == (oldval == val), f'{key} {val} {oldkey} {oldval}'
Expand All @@ -283,33 +282,33 @@ 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:
self._update_no_rollback(on_dup, *args, **kw)
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
Expand Down Expand Up @@ -355,7 +354,7 @@ def copy(self: T) -> T:
cp._fwdm = copy(self._fwdm)
cp._invm = copy(self._invm)
cp._init_inv()
return cp
return cp # type: ignore

def __copy__(self: T) -> T:
"""Used for the copy protocol.
Expand All @@ -368,7 +367,7 @@ def __len__(self) -> int:
"""The number of contained items."""
return len(self._fwdm)

def __iter__(self) -> Iterator[KT]:
def __iter__(self) -> _t.Iterator[KT]:
"""Iterator over the contained keys."""
return iter(self._fwdm)

Expand Down
7 changes: 7 additions & 0 deletions bidict/_bidict.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@

"""Provide :class:`bidict`."""

import typing as _t

from ._base import KT, VT
from ._mut import MutableBidict
from ._delegating import _DelegatingBidict
Expand All @@ -38,6 +40,11 @@ class bidict(_DelegatingBidict[KT, VT], MutableBidict[KT, VT]):

__slots__ = ()

if _t.TYPE_CHECKING:
@property
def inverse(self) -> 'bidict[VT, KT]':
"""..."""


# * Code review nav *
#==============================================================================
Expand Down
18 changes: 0 additions & 18 deletions bidict/_compat.py

This file was deleted.

Loading

0 comments on commit 5a960e7

Please sign in to comment.