Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use Pyright in CI #242

Merged
merged 9 commits into from
Mar 20, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -16,7 +16,7 @@ repos:
- id: codespell

- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.931
rev: v0.940
hooks:
- id: mypy
exclude: setup.py|docs/conf.py|tests
Expand All @@ -28,7 +28,7 @@ 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

Expand All @@ -48,7 +48,7 @@ repos:
exclude: setup.py|run_tests.py|docs/conf.py
Copy link
Owner Author

@jab jab Mar 10, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More just noting for the record than to take away from more important discussion:

I tried following the docs at https://github.com/microsoft/pyright/blob/main/docs/ci-integration.md#running-pyright-as-a-pre-commit-hook but found:

  • I had to remove pass_filenames: false, or else node would just hang
  • I removed additional_dependencies: ['[email protected]'] because it would duplicate the pyright version from requirements/lint.txt, and the local environment gets set up with the requirements/lint.txt dependencies already


- repo: https://github.com/Lucas-C/pre-commit-hooks
rev: v1.1.10
rev: v1.1.13
hooks:
- id: forbid-crlf
- id: remove-crlf
Expand Down
2 changes: 1 addition & 1 deletion bidict/_bidict.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ def popitem(self) -> t.Tuple[KT, VT]:
del self._invm[val]
return key, val

@t.overload
@t.overload # type: ignore [override]
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just noticed a new version of mypy was released 2 days ago, so upgraded it in this PR in case it would reduce the number of places where mypy and pyright are disagreeing.

But it ended up adding a new place they're disagreeing, namely this line: without this 'type: ignore' comment, mypy errors as follows:

bidict/_bidict.py: note: In member "update" of class "MutableBidict":
bidict/_bidict.py:138:6: error: Signature of "update" incompatible with
supertype "MutableMapping"  [override]
        @t.overload
         ^
bidict/_bidict.py:138:6: note:      Superclass:
bidict/_bidict.py:138:6: note:          @overload
bidict/_bidict.py:138:6: note:          def update(self, SupportsKeysAndGetItem[KT, VT], **kwargs: VT) -> None
bidict/_bidict.py:138:6: note:          @overload
bidict/_bidict.py:138:6: note:          def update(self, Iterable[Tuple[KT, VT]], **kwargs: VT) -> None
bidict/_bidict.py:138:6: note:          @overload
bidict/_bidict.py:138:6: note:          def update(self, **kwargs: VT) -> None
bidict/_bidict.py:138:6: note:      Subclass:
bidict/_bidict.py:138:6: note:          @overload
bidict/_bidict.py:138:6: note:          def update(self, Mapping[KT, VT], **kw: VT) -> None
bidict/_bidict.py:138:6: note:          @overload
bidict/_bidict.py:138:6: note:          def update(self, Iterable[Tuple[KT, VT]], **kw: VT) -> None
bidict/_bidict.py:138:6: note:          @overload
bidict/_bidict.py:138:6: note:          def update(self, **kw: VT) -> None
Found 1 error in 1 file (checked 14 source files)

(This is the same "override" error we're seeing in #242 (comment), which pyright was similarly not flagging, though that case still looks like a false positive bug in mypy. In this case, it looks like mypy is correct and pyright is giving a false negative: MutableMapping.update() does work with any SupportsKeysAndGetItem type, whereas my subclass does require the narrower type Mapping (since it calls items() on it), violating the Liskov substitution principle.)

Strange that mypy only just started flagging this in v0.940, and was not flagging it in v0.931, given that this change was introduced in typeshed in August 2020: python/typeshed@c065982

In any case, I don't think it's worth changing my code to use SupportsKeysAndGetItem at this time. (While this may technically be the minimum interface that my update() method should accept, in all likelihood, not once in the history of this code has anyone ever passed in a SupportsKeysAndGetItems that wasn't also a Mapping.) So I'm going with a type: ignore here for now.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For completeness, here's a sketch of the kind of changes that would be required to support non-Mapping SupportsKeysAndGetItems objects, before I throw this away:

show/hide diff
diff --git a/bidict/_typing.py b/bidict/_typing.py
index 8e3ec60..315b225 100644
--- a/bidict/_typing.py
+++ b/bidict/_typing.py
@@ -10,11 +10,17 @@
 import typing as t
 from enum import Enum

+if t.TYPE_CHECKING:
+    from _typeshed import SupportsKeysAndGetItem
+else:
+    SupportsKeysAndGetItem = t.Mapping
+

 KT = t.TypeVar('KT')
 VT = t.TypeVar('VT')
 IterItems = t.Iterable[t.Tuple[KT, VT]]
 MapOrIterItems = t.Union[t.Mapping[KT, VT], IterItems[KT, VT]]
+MapLikeOrIterItems = t.Union[SupportsKeysAndGetItem[KT, VT], MapOrIterItems[KT, VT]]

diff --git a/bidict/_iter.py b/bidict/_iter.py
index 1e7dd5a..957db83 100644
--- a/bidict/_iter.py
+++ b/bidict/_iter.py
@@ -10,15 +10,21 @@
 from operator import itemgetter
 import typing as t

-from ._typing import KT, VT, IterItems, MapOrIterItems
+from ._typing import KT, VT, IterItems, MapLikeOrIterItems


-def iteritems_mapping_or_iterable(arg: MapOrIterItems[KT, VT]) -> IterItems[KT, VT]:
+def iteritems_mapping_or_iterable(arg: MapLikeOrIterItems[KT, VT]) -> IterItems[KT, VT]:
     """Yield the items in *arg* based on whether it's a mapping."""
-    yield from arg.items() if isinstance(arg, t.Mapping) else arg  # pyright: ignore
+    if isinstance(arg, t.Mapping):
+        items = arg.items()  # pyright: ignore
+    elif hasattr(arg, 'keys'):
+        items = ((k, arg[k]) for k in arg.keys())  # type: ignore
+    else:
+        items = arg  # type: ignore
+    return iter(items)  # pyright: ignore

diff --git a/bidict/_bidict.py b/bidict/_bidict.py
index f713f0b..a18341e 100644
--- a/bidict/_bidict.py
+++ b/bidict/_bidict.py
@@ -19,7 +19,7 @@ import typing as t
 from ._abc import MutableBidirectionalMapping
 from ._base import BidictBase, get_arg
 from ._dup import OnDup, ON_DUP_RAISE, ON_DUP_DROP_OLD
-from ._typing import KT, VT, DT, ODT, MISSING, IterItems, MapOrIterItems
+from ._typing import KT, VT, DT, ODT, MISSING, IterItems, MapLikeOrIterItems, SupportsKeysAndGetItem


 class MutableBidict(BidictBase[KT, VT], MutableBidirectionalMapping[KT, VT]):
@@ -135,26 +135,26 @@ class MutableBidict(BidictBase[KT, VT], MutableBidirectionalMapping[KT, VT]):
         del self._invm[val]
         return key, val

-    @t.overload  # type: ignore [override]
-    def update(self, __m: t.Mapping[KT, VT], **kw: VT) -> None: ...
+    @t.overload
+    def update(self, __m: SupportsKeysAndGetItem[KT, VT], **kw: VT) -> None: ...
     @t.overload
     def update(self, __i: IterItems[KT, VT], **kw: VT) -> None: ...
     @t.overload
     def update(self, **kw: VT) -> None: ...

-    def update(self, *args: MapOrIterItems[KT, VT], **kw: VT) -> None:
+    def update(self, *args: MapLikeOrIterItems[KT, VT], **kw: VT) -> None:
         """Like calling :meth:`putall` with *self.on_dup* passed for *on_dup*."""
         if args or kw:
             self._update(get_arg(*args), kw)

def update(self, __m: t.Mapping[KT, VT], **kw: VT) -> None: ...
@t.overload
def update(self, __i: IterItems[KT, VT], **kw: VT) -> None: ...
Expand Down