diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index f462048d..6a1af7ce 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -10,6 +10,8 @@ }, // Set *default* container specific settings.json values on container create. "settings": { + // Enable pylance's pyright integration (disabled by default): + "python.analysis.typeCheckingMode": "basic", "python.defaultInterpreterPath": "/usr/local/bin/python", "python.linting.enabled": true, "python.linting.mypyEnabled": true, diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 437cc792..9b948325 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.0.1 + rev: v4.1.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -16,7 +16,7 @@ repos: - id: codespell - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.931 + rev: v0.941 hooks: - id: mypy exclude: setup.py|docs/conf.py|tests @@ -28,13 +28,18 @@ repos: exclude: setup.py|docs/conf.py|tests - repo: https://github.com/pycqa/flake8 - rev: 3.9.2 + rev: 4.0.1 hooks: - id: flake8 # https://pre-commit.com/#repository-local-hooks - repo: local hooks: + - id: pyright + name: pyright + entry: pyright + language: node + types: [python] - id: pylint name: pylint entry: pylint @@ -43,7 +48,7 @@ repos: exclude: setup.py|run_tests.py|docs/conf.py - repo: https://github.com/Lucas-C/pre-commit-hooks - rev: v1.1.10 + rev: v1.1.13 hooks: - id: forbid-crlf - id: remove-crlf diff --git a/bidict/_base.py b/bidict/_base.py index 9c241114..90826018 100644 --- a/bidict/_base.py +++ b/bidict/_base.py @@ -28,6 +28,10 @@ from ._typing import KT, VT, MISSING, OKT, OVT, IterItems, MapOrIterItems +# Disable pyright strict diagnostics that are causing many false positives or are just not helpful in this file: +# pyright: reportPrivateUsage=false, reportUnknownArgumentType=false, reportUnknownMemberType=false, reportUnknownVariableType=false, reportUnnecessaryIsInstance=false + + OldKV = t.Tuple[OKT[KT], OVT[VT]] DedupResult = t.Optional[OldKV[KT, VT]] Write = t.List[t.Callable[[], None]] @@ -158,6 +162,9 @@ def __init__(self, *args: MapOrIterItems[KT, VT], **kw: VT) -> None: if args or kw: self._update(get_arg(*args), kw, rbof=False) + # If Python ever adds support for higher-kinded types, `inverse` could use them, e.g. + # def inverse(self: BT[KT, VT]) -> BT[VT, KT]: + # Ref: https://github.com/python/typing/issues/548#issuecomment-621571821 @property def inverse(self) -> 'BidictBase[VT, KT]': """The inverse of this bidirectional mapping instance.""" @@ -184,7 +191,8 @@ def inverse(self) -> 'BidictBase[VT, KT]': self._invweak: 't.Optional[weakref.ReferenceType[BidictBase[VT, KT]]]' = None # Also store a weak reference back to `instance` on its inverse instance, so that # the second `.inverse` access in `bi.inverse.inverse` hits the cached weakref. - inv._inv, inv._invweak = None, weakref.ref(self) + inv._inv = None + inv._invweak = weakref.ref(self) # In e.g. `bidict().inverse.inverse`, this design ensures that a strong reference # back to the original instance is retained before its refcount drops to zero, # avoiding an unintended potential deallocation. @@ -487,7 +495,7 @@ def _init_from(self, other: MapOrIterItems[KT, VT]) -> None: # If other is a bidict, use its existing backing inverse mapping, otherwise # other could be a generator that's now exhausted, so invert self._fwdm on the fly. inv = other.inverse if isinstance(other, BidictBase) else inverted(self._fwdm) - self._invm.update(inv) + self._invm.update(inv) # pyright: ignore # https://github.com/jab/bidict/pull/242#discussion_r824223403 #: Used for the copy protocol. #: *See also* the :mod:`copy` module @@ -528,7 +536,7 @@ def __reduce__(self) -> t.Tuple[t.Any, ...]: # somewhere in sys.modules that pickle can discover. should_invert = isinstance(self, GeneratedBidictInverse) cls, init_from = (self._inv_cls, self.inverse) if should_invert else (self.__class__, self) - return self._from_other, (cls, dict(init_from), should_invert) # type: ignore [call-overload] # https://github.com/python/mypy/issues/4975 + return self._from_other, (cls, dict(init_from), should_invert) # type: ignore [call-overload] # See BidictBase._set_reversed() above. diff --git a/bidict/_bidict.py b/bidict/_bidict.py index efa26fbc..db6b8be0 100644 --- a/bidict/_bidict.py +++ b/bidict/_bidict.py @@ -105,12 +105,10 @@ def clear(self) -> None: self._fwdm.clear() self._invm.clear() - @t.overload - def pop(self, __key: KT, __default: DT) -> t.Union[VT, DT]: ... - @t.overload + @t.overload # type: ignore [override] # https://github.com/python/mypy/issues/12390 def pop(self, __key: KT) -> VT: ... @t.overload - def pop(self, __key: KT, __default: t.Union[VT, DT] = ...) -> t.Union[VT, DT]: ... + def pop(self, __key: KT, __default: DT) -> t.Union[VT, DT]: ... def pop(self, key: KT, default: ODT[DT] = MISSING) -> t.Union[VT, DT]: """*x.pop(k[, d]) → v* @@ -137,7 +135,7 @@ def popitem(self) -> t.Tuple[KT, VT]: del self._invm[val] return key, val - @t.overload + @t.overload # type: ignore [override] # https://github.com/jab/bidict/pull/242#discussion_r825464731 def update(self, __m: t.Mapping[KT, VT], **kw: VT) -> None: ... @t.overload def update(self, __i: IterItems[KT, VT], **kw: VT) -> None: ... diff --git a/bidict/_frozenbidict.py b/bidict/_frozenbidict.py index 8280d19a..863eff7c 100644 --- a/bidict/_frozenbidict.py +++ b/bidict/_frozenbidict.py @@ -24,7 +24,7 @@ class frozenbidict(BidictBase[KT, VT]): _hash: int - # Work around lack of support for higher-kinded types in mypy. + # Work around lack of support for higher-kinded types in Python. # Ref: https://github.com/python/typing/issues/548#issuecomment-621571821 if t.TYPE_CHECKING: @property diff --git a/bidict/_frozenordered.py b/bidict/_frozenordered.py index 2ab2e420..de1b74e5 100644 --- a/bidict/_frozenordered.py +++ b/bidict/_frozenordered.py @@ -36,7 +36,7 @@ class FrozenOrderedBidict(OrderedBidictBase[KT, VT]): the ordering of the items can make the ordering dependence more explicit. """ - __hash__: t.Callable[[t.Any], int] = frozenbidict.__hash__ + __hash__: t.Callable[[t.Any], int] = frozenbidict.__hash__ # pyright: ignore if t.TYPE_CHECKING: @property diff --git a/bidict/_iter.py b/bidict/_iter.py index e1d082b7..ee10063d 100644 --- a/bidict/_iter.py +++ b/bidict/_iter.py @@ -7,15 +7,15 @@ """Functions for iterating over items in a mapping.""" -from collections.abc import Mapping from operator import itemgetter +import typing as t from ._typing import KT, VT, IterItems, MapOrIterItems def iteritems_mapping_or_iterable(arg: MapOrIterItems[KT, VT]) -> IterItems[KT, VT]: """Yield the items in *arg* based on whether it's a mapping.""" - yield from arg.items() if isinstance(arg, Mapping) else arg + yield from arg.items() if isinstance(arg, t.Mapping) else arg # pyright: ignore def iteritems(__arg: MapOrIterItems[KT, VT], **kw: VT) -> IterItems[KT, VT]: diff --git a/bidict/_named.py b/bidict/_named.py index 0228eebd..538addf6 100644 --- a/bidict/_named.py +++ b/bidict/_named.py @@ -14,6 +14,9 @@ from ._typing import KT, VT +# pyright: reportPrivateUsage=false, reportUnnecessaryIsInstance=false + + class NamedBidictBase: """Base class that namedbidicts derive from.""" @@ -93,4 +96,4 @@ def _inv_cls_dict_diff(cls) -> t.Dict[str, t.Any]: NamedInv.__doc__ = f'NamedBidictInv({basename}) {typename!r}: {valname} -> {keyname}' caller_module = _getframe(1).f_globals.get('__name__', '__main__') NamedBidict.__module__ = NamedInv.__module__ = caller_module - return NamedBidict + return NamedBidict # pyright: ignore [reportUnknownVariableType] diff --git a/bidict/_orderedbase.py b/bidict/_orderedbase.py index 1c273a5d..94308f55 100644 --- a/bidict/_orderedbase.py +++ b/bidict/_orderedbase.py @@ -98,7 +98,7 @@ def new_last_node(self) -> Node: """Create and return a new terminal node.""" old_last = self.prv new_last = Node(old_last, self) - old_last.nxt = self.prv = new_last + old_last.nxt = self.prv = new_last # pyright: ignore # https://github.com/microsoft/pyright/issues/3183 return new_last diff --git a/bidict/_orderedbidict.py b/bidict/_orderedbidict.py index 3047620e..58e72857 100644 --- a/bidict/_orderedbidict.py +++ b/bidict/_orderedbidict.py @@ -23,6 +23,9 @@ from ._typing import KT, VT +# pyright: reportPrivateUsage=false + + class OrderedBidict(OrderedBidictBase[KT, VT], MutableBidict[KT, VT]): """Mutable bidict type that maintains items in insertion order.""" @@ -57,7 +60,7 @@ def popitem(self, last: bool = True) -> t.Tuple[KT, VT]: korv = self._node_by_korv.inverse[node] if self._bykey: return korv, self._pop(korv) - return self.inverse._pop(korv), korv + return self.inverse._pop(korv), korv # pyright: ignore [reportGeneralTypeIssues] def move_to_end(self, key: KT, last: bool = True) -> None: """Move the item with the given key to the end if *last* is true, else to the beginning. diff --git a/pyrightconfig.json b/pyrightconfig.json new file mode 100644 index 00000000..dc5a1433 --- /dev/null +++ b/pyrightconfig.json @@ -0,0 +1,14 @@ +{ + "include": [ + "bidict", + ], + "exclude": [ + "docs", + "tests", + "run_tests.py", + ], + "strict": [ + "bidict", + ], + "reportUnnecessaryTypeIgnoreComment": "warning", +} diff --git a/requirements/dev.txt b/requirements/dev.txt index 72e73e4b..e1092bd9 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -67,7 +67,9 @@ markupsafe==2.1.0 mccabe==0.6.1 # via pylint nodeenv==1.6.0 - # via pre-commit + # via + # pre-commit + # pyright packaging==21.3 # via # build @@ -113,6 +115,8 @@ pylint==2.12.2 # via -r lint.in pyparsing==3.0.7 # via packaging +pyright==1.1.231 + # via -r lint.in pytest==7.0.1 # via # -r lint.in diff --git a/requirements/lint.in b/requirements/lint.in index 42a5e43b..b9e36dae 100644 --- a/requirements/lint.in +++ b/requirements/lint.in @@ -1,4 +1,7 @@ pre-commit +# pre-commit would ideally be the only direct dependency needed for linting, but the +# following pre-commit hooks require "repo: local", so we add them to our dependencies: +pyright pylint # pylint runs against tests/*.py which import pytest and hypothesis pytest diff --git a/requirements/lint.txt b/requirements/lint.txt index b540ebf6..96d4cd82 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -29,7 +29,9 @@ lazy-object-proxy==1.7.1 mccabe==0.6.1 # via pylint nodeenv==1.6.0 - # via pre-commit + # via + # pre-commit + # pyright packaging==21.3 # via pytest platformdirs==2.5.1 @@ -46,6 +48,8 @@ pylint==2.12.2 # via -r lint.in pyparsing==3.0.7 # via packaging +pyright==1.1.231 + # via -r lint.in pytest==7.0.1 # via -r lint.in pyyaml==6.0