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

Enforce mypy-cleanliness on trio._core #1610

Merged
merged 15 commits into from
Jun 25, 2020

Conversation

pquentin
Copy link
Member

@pquentin pquentin commented Jun 12, 2020

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:

++ mypy -p trio._core --platform linux
Success: no issues found in 45 source files

@@ -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
Copy link
Member Author

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.

trio/_core/_run.py Outdated Show resolved Hide resolved
It depends on typed-ast.
@codecov
Copy link

codecov bot commented Jun 12, 2020

Codecov Report

Merging #1610 into master will increase coverage by 0.00%.
The diff coverage is 100.00%.

@@           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           
Impacted Files Coverage Δ
trio/_core/__init__.py 100.00% <100.00%> (ø)
trio/_core/_io_epoll.py 100.00% <100.00%> (ø)
trio/_core/_io_kqueue.py 85.12% <100.00%> (ø)
trio/_core/_multierror.py 100.00% <100.00%> (ø)
trio/_core/_run.py 99.77% <100.00%> (+<0.01%) ⬆️
trio/_core/tests/test_multierror.py 100.00% <100.00%> (ø)

"if not TYPE_CHECKING" is always True when running the code.
@pquentin
Copy link
Member Author

Closing/reopening because the FreeBSD build failed with #1604

@pquentin pquentin closed this Jun 12, 2020
@pquentin pquentin reopened this Jun 12, 2020
@pquentin
Copy link
Member Author

Coverage is OK, next up is understanding the unrolled_run_next_send issue.

@pquentin
Copy link
Member Author

unrolled_run_next_send fixed, please take another look.

@pquentin pquentin requested review from oremanj and njsmith June 22, 2020 06:17
@@ -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)
Copy link
Member

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.

Copy link
Member Author

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.
Copy link
Member

@njsmith njsmith left a 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
Copy link
Member

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...

Copy link
Member Author

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...

Copy link
Member

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...

Copy link
Member Author

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
Copy link
Member

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.

Copy link
Member Author

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.

sys.platform == "darwin"
or sys.platform.startswith("freebsd")
or (not TYPE_CHECKING and hasattr(select, "epoll"))
):
Copy link
Member

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?

Copy link
Member

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").

Copy link
Member Author

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") with hasattr(select, "epoll").

Thanks, fixed.

Copy link
Member

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":

https://github.com/python/typeshed/blob/66a9a4b5ce72aaf022b24706bf58254602ef02f8/stdlib/2and3/select.pyi#L36-L38

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.

Copy link
Member Author

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.)

Copy link
Member

@oremanj oremanj Jun 24, 2020

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.

Copy link
Member

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

?

Copy link
Member

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

Copy link
Member

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 ....

Copy link
Member Author

@pquentin pquentin Jun 25, 2020

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

pquentin added 2 commits June 22, 2020 13:55
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.
Copy link
Member

@oremanj oremanj left a 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
Copy link
Member

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)

Copy link
Member Author

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
Copy link
Member

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...

@@ -1,6 +1,7 @@
import select
import attr
from collections import defaultdict
from typing import DefaultDict
Copy link
Member

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

Copy link
Member Author

Choose a reason for hiding this comment

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

Switched to Dict

_registered = attr.ib(
factory=lambda: defaultdict(EpollWaiters), type=DefaultDict[int, EpollWaiters]
)
_force_wakeup = attr.ib(factory=WakeupSocketpair, type=WakeupSocketpair)
Copy link
Member

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.

Copy link
Member Author

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)
Copy link
Member

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.

Copy link
Member Author

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?

Copy link
Member

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.

@pquentin
Copy link
Member Author

pquentin commented Jun 25, 2020

Thanks for the reviews! I adressed all comments, please take another look.

@oremanj oremanj merged commit 5e81f47 into python-trio:master Jun 25, 2020
@pquentin pquentin deleted the mypy-trio-core branch June 25, 2020 06:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants