-
-
Notifications
You must be signed in to change notification settings - Fork 32k
Run mypy on Lib/test/libregrtest
in CI
#109382
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
Conversation
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.
Most libregrtest/ changes LGTM, but I'm not sure about coding style changes. Can you please create a PR just for this change, and then update this PR to just add the infra to run mypy?
Which coding style changes do you dislike? I tried hard not to make any changes in this PR unless they were strictly required to make the mypy check pass (which is why I think it's good to introduce mypy to CI in the same PR: it validates that the changes in this PR are correct, at least from mypy's perspective). If you highlight the changes you're not sure about, hopefully I can explain why they were necessary to make mypy happy ;-) |
I don't get how replacing self.workers with workers is needed by mypy for example. |
Good point, those changes aren't needed anymore. I think I needed them in an earlier version of this PR, but mypy now seems happy without them :-) I've reverted those, thanks! |
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 change is too big to be reviewed. Please extract non-controversial changes: remove .github/workflows/mypy.yml changes and the lines where I added comments :-) I mean, please create a new PR with the most obvious non-controversial type hint changes.
Lib/test/libregrtest/results.py
Outdated
@@ -1,4 +1,5 @@ | |||
import sys | |||
import xml.etree.ElementTree as ET |
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.
Please don't do that. I want to minimize imports at startup, to minimize how many modules are imported when a test is run. Maybe even add a comment to explain why the import is not done at top level, that it's a deliberate choice.
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.
Okay. The reason why I did this was to accurately add type hints for the TestResults.testsuite_xml
attribute on line 35. The current type hints for that attribute are incorrect, and caused several mypy errors.
A possible workaround is to do the top-level import inside an if TYPE_CHECKING
block.
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.
A possible workaround is to do the top-level import inside an
if TYPE_CHECKING
block.
I made that change in 581ef0d; let me know what you think!
Lib/test/libregrtest/single.py
Outdated
@@ -184,6 +187,11 @@ def _runtest_env_changed_exc(result: TestResult, runtests: RunTests, | |||
result.state = State.PASSED | |||
|
|||
|
|||
class PrintWarningsType(Protocol): | |||
orig_stderr: io.TextIOWrapper | |||
def __call__(self, msg: str) -> None: ... |
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.
If possible, I would prefer avoiding to have to add glue on top of code just to please mypy. If print_warning() API is too weird for mypy, maybe rewrite it with such class directly in test.support, no?
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.
If print_warning() API is too weird for mypy, maybe rewrite it with such class directly in test.support, no?
Unfortunately not. Mypy doesn't know that the test.support
module exists: when it comes to the stdlib, it only knows about the modules we include stubs for in typeshed. Historically, we haven't included stubs for any modules in the Lib/test/
directory, as these aren't available on all builds of Python, and are generally only meant to be used by CPython core devs like us. As a result, everything that is imported from test.support
is inferred by mypy as being of type Any
.
However, that also means that we don't need this protocol here: I only added it to give mypy a little more information, and make the code slightly more type safe. Since you don't like it, I'll remove it!
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.
Mypy doesn't know that the test.support module exists
Can't we make mypy aware of test.support, instead of having to put ducktape to workaround these limitations?
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't we make mypy aware of test.support
This is somewhat tricky due to the fact that regrtest
is in the Lib/test/
directory. Mypy hates being run on stuff inside the Lib/
directory, as it thinks that everything inside the Lib/
directory is "shadowing the stdlib". The only way we're managing to get mypy happy with being run on libregrtest
is by cd
-ing into Lib/test/
, and then running mypy with very specific options so that it doesn't look anywhere else in the Lib/
directory, it only looks in Lib/test/libregrtest
.
There would be some ways of making mypy aware of test.support
, but I'm not sure I like any of them:
- We could write a custom script that copies
libregrtest
andtest.support
into a temporary directory, then runs mypy on them together. Why I don't like this: it's easier for users if they can run mypy just by invokingmypy
on the command line. Having to run mypy via a wrapper script is a bad user experience, and it's more code for us to maintain as well. - We could add stubs for
test.support
in typeshed, so that mypy seestest.support
as "just another part of the stdlib". Why I don't like this: lots of Python users can't accesstest.support
on their installed versions of Python, but if we add stubs fortest.support
in typeshed, mypy will never complain if users importtest.support
in their own code. Also, it will be a lot of work for us typeshed maintainers to maintain stubs fortest.support
, since thetest.support
module tends to change its API a lot more frequently than other things in theLib/
directory. - We could add stubs for
test.support
inside thelibregrtest
directory itself. Why I don't like this: this just feels like it would be really ugly, and really hard to maintain. The stubs would go out of date really quickly, if we weren't careful.
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.
We could write a custom script that copies libregrtest and test.support into a temporary directory
That sounds like a good idea to me.
I would prefer that mypy users need workaround, than libregrtest have to write complicated workarounds in the working code just to please mypy.
@@ -195,7 +203,7 @@ def _runtest(result: TestResult, runtests: RunTests) -> None: | |||
timeout is not None and threading_helper.can_start_thread | |||
) | |||
if use_timeout: | |||
faulthandler.dump_traceback_later(timeout, exit=True) | |||
faulthandler.dump_traceback_later(cast(float, timeout), exit=True) |
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 understand this cast. timeout is a float is use_timeout is True. Why is a cast needed?
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.
Yes, this code is type-safe. The issue is that mypy only understands a limited set of type narrowing operations, and the way timeout
has been narrowed to be a float
here isn't one of them.
Mypy understands this kind of logic:
x: float | None
if x is not None:
# OK, mypy understands `f` has to be a float here
But this kind of thing is too complex for mypy:
x: float | None
x_is_None = x is None
if not x_is_None:
# mypy will not understand that x has been safely narrowed to `None` here
Another way we could make mypy happy would be to add an assert
, like this -- but it feels redundant to me, since the type of timeout
has already been narrowed to be a float
in the lines immediately preceding this:
faulthandler.dump_traceback_later(cast(float, timeout), exit=True) | |
assert timeout is not None | |
faulthandler.dump_traceback_later(timeout, exit=True) |
@@ -67,7 +68,8 @@ def worker_process(worker_json: StrJSON) -> NoReturn: | |||
runtests = RunTests.from_json(worker_json) | |||
test_name = runtests.tests[0] | |||
match_tests: FilterTuple | None = runtests.match_tests | |||
json_file: JsonFile = runtests.json_file | |||
json_file = runtests.json_file | |||
assert isinstance(json_file, JsonFile) |
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 dislike assert, it's removed by python -O. Why is this assertion needed? The attribute is declared as json_file: JsonFile | None
. Is it to remove the None
case?
If mypy doesn't understand the logic, maybe a developer can have the same doubt. Maybe add a regular if json_file is None: raise ...
.
Or maybe, RunTests can have a sub-type which adds the json_file attribute: the class used by create_worker_process() / worker_process(). Would it please 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.
Why is this assertion needed? The attribute is declared as
json_file: JsonFile | None
. Is it to remove theNone
case?
Yes, it's to remove the None
case.
If mypy doesn't understand the logic, maybe a developer can have the same doubt. Maybe add a regular
if json_file is None: raise ...
.
Sure. Should I do the same for all assert
s being proposed in this PR?
Or maybe, RunTests can have a sub-type which adds the json_file attribute: the class used by create_worker_process() / worker_process(). Would it please mypy?
Yes, that might also make mypy happier. Hard to tell without seeing a PR and running mypy on it, though :-)
Okay, I've had a go at doing that in #109405 |
Can you rebase your PR on the main branch? |
I'll close this for now, and continue to break it up into smaller PRs so that it's obvious what I'm doing at each stage 👍 |
Nice! I like this idea :-) |
@vstinner: this is essentially the minimum set of changes that are required to get mypy to pass when run on
libregrtest
. Let me know what you think!I think it might be good to post on Discourse and check that everybody's okay with this idea, before we go through with it. This would be the first time we'd be running mypy on a project inside the
Lib/
directory; all existing mypy-checked projects live insideTools/
.