-
-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Stubtest: Fix __mypy-replace false positives
#15689
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
|
Seems reasonable, maybe I'd pick a name that's either closer to the aspect that interests stubtest ("exists_at_runtime") or closer to the reason it's used in the plugin (a way to serialize/cache types across phases). Another option is to add an |
This comment has been minimized.
This comment has been minimized.
I considered this, but felt like it could raise more questions than it answered. If I'm reading
Not hugely keen on this idea either — what if we start generating fictional methods/attributes in a plugin in the future for something that isn't specifically related to this?
Yeah, I wondered about just doing: to_check = {name for name in stub.names if name.isidentifier()}But it felt slightly less principled/relying on an implementation detail of a separate, unrelated part of mypy.
As in, you prefer what I have currently or you'd prefer it if I changed it? :) |
|
Ok, I agree with you now. Maybe a name that also suggests the use case of a symbol used internally? How about I'm starting to think we should've added a separate list of "internal symbols" and modified the serializer. This is becoming more bothersome. Another case in point: having to make the symbols class-private because otherwise I'd have a child class make an "incompatible override" of a |
Yes, that sounds like it could be a better approach... Even if we make sure that we fix all bits of mypy now that assume all nodes in the symbol table actually exist at runtime, there's a risk we might introduce other bits of mypy in the future that accidentally make that assumption. So if we just don't add it to the symbol table at all, and store it somewhere else, that's probably much more future-proof |
Renamed the flag to Are you going to look at the alternative broader-refactoring solution you mentioned? I can try to take a look tomorrow, if not. |
|
According to mypy_primer, this change doesn't affect type check results on a corpus of open source code. ✅ |
|
sgtm, we'll see how much free time I'll have next week. Starting a new job, so perhaps not much. |
|
For potentially no reason at all, I'm a little nervous about adding fields to SymbolTableNode. I think the alternative of just filtering invalid identifiers in stubtest is quite sane and would be very safe to cherry pick to 1.5 |
What's mypy's internal representation of a call-based typeddict? Would Foo = TypedDict("Foo", {"x-y": int}) |
|
Good question! I think all the typeddict related stuff is in the |
|
Okay, I verified that's the case. I also ran all over typeshed seeing when we'd have invalid identifiers, and the thing that came up was #10545 where I think this would be desirable |
Okay, great! I'll go with that heuristic for now then |
|
Okay, this is now a purely stubtest-specific workaround! |
hauntsaninja
left a comment
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! We can still pursue the SymbolTableNode change if we want it for other reasons, but I can release this in the 1.5 series
Running stubtest on typeshed's stdlib stubs with the mypy
masterbranch (or the mypy1.5branch) produces the following output currently:This is due to recent changes @ikonst made to mypy's dataclasses plugin, where mypy now generates internal
__mypy-replacemethods so as to perform more accurate typechecking ofdataclasses.replace().To fix this false positive, I've added a new slot to
SymbolTableNodeto indicate whether plugin-generated methods actually exist at runtime or not. If we don't want to do this, there are other approaches we could take, such as hardcoding instubtest.pya list of problematic plugin-generated method names to avoid checking. I went for this approach as it felt more principled, but I'm happy to change the approach if adding a new slot toSymbolTableNodeis problematic.I tried adding this test to
mypy/test/teststubtest.py:Details
Unfortunately the test fails due to (I think) the fact that mypy doesn't currently generate
__eq__and__repr__methods for dataclasses (see #12186). There's also something to do with_DTthat I don't really understand:Details
======================================================================== FAILURES ========================================================================= ______________________________________________________________ StubtestUnit.test_dataclasses ______________________________________________________________ [gw3] win32 -- Python 3.11.2 C:\Users\alexw\coding\mypy\venv\Scripts\python.exe args = (<mypy.test.teststubtest.StubtestUnit testMethod=test_dataclasses>,), kwargs = {} cases = [<mypy.test.teststubtest.Case object at 0x0000027188E74210>, <mypy.test.teststubtest.Case object at 0x0000027188E74250>], expected_errors = set() c = <mypy.test.teststubtest.Case object at 0x0000027188E74250>, @py_assert1 = False @py_format3 = "{'test_module...Foo.__repr__'} == set()\n~Extra items in the left set:\n~'test_module.Foo._DT'\n~'test_module.Foo.__eq__'\n~'test_module.Foo.__repr__'\n~Use -v to get more diff" @py_format5 = "test_module.Foo._DT\n~test_module.Foo.__eq__\n~test_module.Foo.__repr__\n~\n>assert {'test_module...Foo.__repr__'} ==...he left set:\n~'test_module.Foo._DT'\n~'test_module.Foo.__eq__'\n~'test_module.Foo.__repr__'\n~Use -v to get more diff" output = 'test_module.Foo._DT\ntest_module.Foo.__eq__\ntest_module.Foo.__repr__\n' actual_errors = {'test_module.Foo._DT', 'test_module.Foo.__eq__', 'test_module.Foo.__repr__'} def test(*args: Any, **kwargs: Any) -> None: cases = list(fn(*args, **kwargs)) expected_errors = set() for c in cases: if c.error is None: continue expected_error = c.error if expected_error == "": expected_error = TEST_MODULE_NAME elif not expected_error.startswith(f"{TEST_MODULE_NAME}."): expected_error = f"{TEST_MODULE_NAME}.{expected_error}" assert expected_error not in expected_errors, ( "collect_cases merges cases into a single stubtest invocation; we already " "expect an error for {}".format(expected_error) ) expected_errors.add(expected_error) output = run_stubtest( stub="\n\n".join(textwrap.dedent(c.stub.lstrip("\n")) for c in cases), runtime="\n\n".join(textwrap.dedent(c.runtime.lstrip("\n")) for c in cases), options=["--generate-allowlist"], ) actual_errors = set(output.splitlines()) > assert actual_errors == expected_errors, output E AssertionError: test_module.Foo._DT E test_module.Foo.__eq__ E test_module.Foo.__repr__ E E assert {'test_module...Foo.__repr__'} == set() E Extra items in the left set: E 'test_module.Foo._DT' E 'test_module.Foo.__eq__' E 'test_module.Foo.__repr__' E Use -v to get more diff mypy\test\teststubtest.py:171: AssertionError ================================================================= short test summary info ================================================================= FAILED mypy/test/teststubtest.py::StubtestUnit::test_dataclasses - AssertionError: test_module.Foo._DT ============================================================== 1 failed, 48 passed in 6.78s ===============================================================I verified manually, however, that this gets rid of all the new false positives when running stubtest on typeshed's stdlib.