Repository navigation
[unittest] Allow assertIsNone() and assertIsNotNone() to narrow types - #16767
SpangleLabs wants to merge 1 commit into
Conversation
|
Diff from mypy_primer, showing the effect of this PR on open source code: zulip (https://github.com/zulip/zulip)
- zerver/tests/test_digest.py:630: error: "UserProfile" has no attribute "realm_id"; maybe "realm"? [attr-defined]
- zerver/tests/test_custom_profile_data.py:1045: error: "CustomProfileField" has no attribute "id" [attr-defined]
mkdocs (https://github.com/mkdocs/mkdocs)
+ mkdocs/tests/structure/file_tests.py:317: error: Statement is unreachable [unreachable]
|
| @overload | ||
| def assertIsNone(self, obj: None, msg: Any = None) -> None: ... | ||
| @overload | ||
| def assertIsNone(self, obj: object, msg: Any = None) -> Never: ... |
There was a problem hiding this comment.
Hmm, I don't think this is correct. None is a subtype of object. If you pass an object of type object, Any, Any | None, or similar into the function, then a type checker will take the second overload and infer Never, but the passed-in object could still easily be None. Here's a demo using ty with your typeshed branch checked out at ../typeshed locally:
% cat foo.py
import unittest
def any_object() -> object:
return None
reveal_type(unittest.TestCase().assertIsNone(any_object()))
% uvx ty check foo.py --custom-typeshed-dir ../typeshed --ignore=undefined-reveal
info[revealed-type]: Revealed type
--> foo.py:6:13
|
6 | reveal_type(unittest.TestCase().assertIsNone(any_object()))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `Never`
Found 1 diagnostic
There was a problem hiding this comment.
Also note that while it might lead to a type checker making inferences about reachability and control flow if you have an overload returning Never, it won't lead to the type checker successfully narrowing the type. Here, ty infers the type as Never after the call because it deems everything after the call unreachable, rather than narrowing the type to None. Meanwhile, mypy doesn't even see the reveal_type call, because it doesn't emit any diagnostics (including reveal_type diagnostics) in regions of code it deems to be unreachable:
% cat foo.py
import unittest
def any_object() -> object:
return None
def narrow_please(x: object):
unittest.TestCase().assertIsNone(x)
reveal_type(x)
% uvx ty check foo.py --custom-typeshed-dir ../typeshed --ignore=undefined-reveal
info[revealed-type]: Revealed type
--> foo.py:8:17
|
8 | reveal_type(x)
| ^ `Never`
Found 1 diagnostic
% uvx mypy foo.py --custom-typeshed-dir ../typeshed
Success: no issues found in 1 source file
I'm surprised to find that this is reported as a mypy issue 5 years ago with this suggested fix, but no one made a PR here to actually fix it?
python/mypy#5088
So, here's a PR to fix it. Overloading the assertIsNone and assertIsNotNone definitions to ensure that None values result in Never when appropriate. This avoids having to double up
self.assertIsNotNone(obj)withassert obj is not Noneto appease mypy