-
-
Notifications
You must be signed in to change notification settings - Fork 349
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
Enforce mypy-cleanliness on trio._core #1610
Conversation
@@ -19,7 +19,7 @@ class _KqueueStatistics: | |||
|
|||
@attr.s(slots=True, eq=False) | |||
class KqueueIOManager: | |||
_kqueue = attr.ib(factory=select.kqueue) | |||
_kqueue = attr.ib(factory=select.kqueue) # type: ignore |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This works because we require Linux when running mypy. But if I don't specify the platform, I get the following errors on macOS:
trio/_core/_io_kqueue.py:22: error: unused 'type: ignore' comment
trio/_core/_io_epoll.py:186: error: Module has no attribute "epoll"; maybe "poll"?
I plan to fix that in a further pull request by adding assert sys.platform.startswith("freebsd") or sys.platform.startswith("darwin")
at the top of the file and running mypy on the various platforms we support.
It depends on typed-ast.
Codecov Report
@@ Coverage Diff @@
## master #1610 +/- ##
=======================================
Coverage 99.69% 99.69%
=======================================
Files 110 110
Lines 13955 13957 +2
Branches 1074 1076 +2
=======================================
+ Hits 13913 13915 +2
Misses 27 27
Partials 15 15
|
"if not TYPE_CHECKING" is always True when running the code.
Closing/reopening because the FreeBSD build failed with #1604 |
Coverage is OK, next up is understanding the unrolled_run_next_send issue. |
mypy thought the lambda was returning "object".
|
trio/_core/_run.py
Outdated
@@ -1151,7 +1151,8 @@ class GuestState: | |||
run_sync_soon_not_threadsafe = attr.ib() | |||
done_callback = attr.ib() | |||
unrolled_run_gen = attr.ib() | |||
unrolled_run_next_send = attr.ib(factory=lambda: Value(None), type=object) | |||
_value_factory: Callable[[], Value] = lambda: Value(None) | |||
unrolled_run_next_send = attr.ib(factory=_value_factory, type=Value) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the type here should be outcome.Outcome
, not outcome.Value
? E.g. if get_events
raises an exception, then unrolled_run_next_send
will be an outcome.Error
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! Fixed.
The value can be an Error too, so we should use Outcome, not Value.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@oremanj You definitely know this typing stuff better than I do, so if you have a chance to give feedback here it'd be great :-). If you're busy though no worries, we can still move ahead and fix things later.
@@ -4,6 +4,9 @@ | |||
are publicly available in either trio, trio.lowlevel, or trio.testing. | |||
""" | |||
|
|||
import typing as _t | |||
import sys |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess technically this should be import sys as _sys
, though in practice it doesn't do much harm here...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately, importing sys
as _sys
breaks mypy...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
del sys
at the end will clean up sufficiently.
Yeah, mypy's platform guards are rather brittle. It's strange since, for example, the magic from typing import TYPE_CHECKING
seems to work under any name...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, added del sys
@@ -415,7 +415,7 @@ def traceback_exception_init( | |||
self.embedded = [] | |||
|
|||
|
|||
traceback.TracebackException.__init__ = traceback_exception_init | |||
traceback.TracebackException.__init__ = traceback_exception_init # type: ignore |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not too worried about # type: ignore
comments inside this horrible blob of monkeypatching, but I am curious why you need them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error is trio/_core/_multierror.py:418: error: Cannot assign to a method
. It appears to be a known mypy bug: python/mypy#2427. It's unclear to me if setattr
is a better workaround or not.
trio/_core/_run.py
Outdated
sys.platform == "darwin" | ||
or sys.platform.startswith("freebsd") | ||
or (not TYPE_CHECKING and hasattr(select, "epoll")) | ||
): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well... this check is gross and we should probably complain about it to typing-sig.
But having written it, I guess we might as well copy-paste it into trio/_core/__init__.py
too?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh heh and there's a copy/paste error here – you accidentally replaced hasattr(select, "kqueue")
with hasattr(select, "epoll")
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well... this check is gross and we should probably complain about it to typing-sig.
Do you mean that mypy should be able to tell wich platforms support epoll/kqueue?
But having written it, I guess we might as well copy-paste it into
trio/_core/__init__.py
too?
Do you mean that this hack is better than the current try/except ImportError
way in trio/_core/__init__.py
?
Oh heh and there's a copy/paste error here – you accidentally replaced
hasattr(select, "kqueue")
withhasattr(select, "epoll")
.
Thanks, fixed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you mean that mypy should be able to tell wich platforms support epoll/kqueue?
Maybe? I mean that having to copy-paste this complicated and not-even-terribly-correct code into multiple places is suboptimal, and maybe it could be avoided somehow. But yeah, teaching mypy to evaluate hasattr
statements seems like one good way to do that... it already has a model of which attributes the select
module has!
And heh, on further investigation, it looks like the way mypy's understanding of select
works is:
if sys.platform != "linux" and sys.platform != "win32":
That is probably nicer than trying to enumerate all the BSD variations... so I guess maybe for now we should do:
if sys.platform == "win32":
...
elif sys.platform == "linux" or hasattr(select, "epoll"):
...
elif TYPE_CHECKING or hasattr(select, "kqueue"):
...
else:
raise NotImplementedError
Do you mean that this hack is better than the current try/except ImportError way in trio/_core/init.py?
Yeah, b/c the way that's written right now, if you try to run mypy on macOS then it'll complain that current_kqueue
and friends are undefined.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, done. (Btw, note that you can use "Quote reply" to preserve the markdown formatting of the text you're quoting.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any arm of the if that might be true (according to mypy's very limited static analysis) must type-check. That's the reason for all the not TYPE_CHECKING and
in the original -- it basically is saying "I know you can't tell this hasattr() result when type-checking, so just ignore it". With @njsmith's suggestion, you're requiring the linux
arm to type-check on kqueue platforms too (since as far as mypy knows they might have select.epoll
), which is probably ultimately responsible for the need to invoke mypy with --platform linux
in this diff.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So I guess that means we should do something like this?
if sys.platform == "win32":
...
elif sys.platform == "linux" or (not TYPE_CHECKING and hasattr(select, "epoll")):
...
elif TYPE_CHECKING or (not TYPE_CHECKING and hasattr(select, "kqueue")):
...
else:
raise NotImplementedError
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also raised this upstream here: python/mypy#9042
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think you need the not TYPE_CHECKING
guard in the kqueue arm, because mypy can tell that TYPE_CHECKING or ...
is true regardless of the ...
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, switched to this. Will probably help with multiplatform support even if right now I get the exact same errors on macOS
An added benefit is that we currently don't need to look at TYPE_CHECKING or add ugly pragmas. This might change when we run mypy on more platforms.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry this took me so long to review. Looks good overall!
@@ -23,6 +23,12 @@ flake8 trio/ \ | |||
--ignore=D,E,W,F401,F403,F405,F821,F822\ | |||
|| EXIT_STATUS=$? | |||
|
|||
# Run mypy | |||
# We specify Linux so that we get the same results on all platforms. Without |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you edit this comment to mention your future plans listed below? (run mypy on all platforms, use assert guards to avoid typechecking the wrong IOManagers)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, done
@@ -4,6 +4,9 @@ | |||
are publicly available in either trio, trio.lowlevel, or trio.testing. | |||
""" | |||
|
|||
import typing as _t | |||
import sys |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
del sys
at the end will clean up sufficiently.
Yeah, mypy's platform guards are rather brittle. It's strange since, for example, the magic from typing import TYPE_CHECKING
seems to work under any name...
trio/_core/_io_epoll.py
Outdated
@@ -1,6 +1,7 @@ | |||
import select | |||
import attr | |||
from collections import defaultdict | |||
from typing import DefaultDict |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I generally just use Dict
for defaultdicts, since you rarely wind up accessing the .factory
member
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Switched to Dict
trio/_core/_io_epoll.py
Outdated
_registered = attr.ib( | ||
factory=lambda: defaultdict(EpollWaiters), type=DefaultDict[int, EpollWaiters] | ||
) | ||
_force_wakeup = attr.ib(factory=WakeupSocketpair, type=WakeupSocketpair) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you need the type= here? I would expect mypy to be able to infer it from the factory return type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right, that was not needed! Thanks, fixed.
@@ -1223,7 +1224,8 @@ class GuestState: | |||
run_sync_soon_not_threadsafe = attr.ib() | |||
done_callback = attr.ib() | |||
unrolled_run_gen = attr.ib() | |||
unrolled_run_next_send = attr.ib(factory=lambda: Value(None)) | |||
_value_factory: Callable[[], Value] = lambda: Value(None) | |||
unrolled_run_next_send = attr.ib(factory=_value_factory, type=Outcome) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can use cast()
to write this without introducing an additional class-level name. (cast(Callable[[], Value], lambda: Value(None))
). I'm also curious what breaks if you write it on one line without the cast.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error message without the lambda type hint is Returning Any from function declared to return "object"
. Indeed according to reveal_type
the type of lambda: Value(None)
is 'def () -> Any'
.
So far so good, but if I switch to your inline cast which specifies the Value
return type, I get trio/_core/_run.py:1227: error: Redundant cast to "Callable[[], Any]"
! It's maybe a variance issue?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, this is probably because outcome
is not typed, so mypy is replacing everything imported from it with Any
.
We should add type hints to outcome
. In the meantime, your workaround here seems fine, just note that we're not actually getting anything from declaring the type as Outcome
as opposed to Any
.
mypy can infer the type from the factory.
Thanks for the reviews! I adressed all comments, please take another look. |
This is a less ambitious version of #1254, the idea being that we can introduce mypy cleanliness gradually. I did copy relevant code from #1254, and fixed other issues too.
The relevant message in CI can be found in the check formatting check: