Skip to content

Support narrowing unions that include type[None].#16315

Merged
hauntsaninja merged 7 commits into
python:masterfrom
tyralla:fix/narrow_nonetype
Jan 14, 2024
Merged

Support narrowing unions that include type[None].#16315
hauntsaninja merged 7 commits into
python:masterfrom
tyralla:fix/narrow_nonetype

Conversation

@tyralla

@tyralla tyralla commented Oct 23, 2023

Copy link
Copy Markdown
Collaborator

Fixes #16279

See my comment in the referenced issue.

@github-actions

This comment has been minimized.

@tyralla

tyralla commented Oct 23, 2023

Copy link
Copy Markdown
Collaborator Author

The primer's results show that the proposed changes also have other benefits. However, I have no idea what the failed mypyc runtime test is about. (I have no experience with Mypyc, so any hint is welcome.)

@JelleZijlstra

Copy link
Copy Markdown
Member

The mypyc failure is likely spurious; it's been failing randomly for some time. I retriggered the job.

@tyralla

tyralla commented Oct 23, 2023

Copy link
Copy Markdown
Collaborator Author

Yes, you were right, thanks!

NoneType_ = type(None)

def f(cls: Type[Union[None, int]]) -> None:
if issubclass(cls, NoneType_):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

One case is still broken, I believe: (python3.10+)

if issubclass(cls, types.Nonetype)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

You are right. The definition in types.pyi is not helpful. I assumed something similar to mentioned test:

NoneType = type(None)

But it is:

    @final
    class NoneType:
        def __bool__(self) -> Literal[False]: ...

My suggestion is to let ExpressionChecker.analyze_ref_expr directly return TypeType(NoneType()) when it encounters the full name "types.NoneType". (already pushed)

@github-actions

This comment has been minimized.

@tyralla

tyralla commented Oct 24, 2023

Copy link
Copy Markdown
Collaborator Author

The new unreachable error for clinic.py also seems correct because neither bool nor NoneType is an acceptable base type.

@AlexWaygood

AlexWaygood commented Oct 25, 2023

Copy link
Copy Markdown
Member

The new unreachable error for clinic.py also seems correct because neither bool nor NoneType is an acceptable base type.

The clinic code in question is here: https://github.com/python/cpython/blob/1262e41842957c3b402fc0cf0a6eb2ea230c828f/Tools/clinic/clinic.py#L5828-L5831

I agree that the clinic error is fine, but for a different reason to the one you gave ;)

It looks to me like there's a bug in the annotations for clinic.py. If I apply this diff to clinic.py:

diff --git a/Tools/clinic/clinic.py b/Tools/clinic/clinic.py
index 5dd7900851..51964fb38e 100755
--- a/Tools/clinic/clinic.py
+++ b/Tools/clinic/clinic.py
@@ -5840,6 +5840,7 @@ def bad_node(self, node: ast.AST) -> None:
                         value = unknown
                 else:
                     value = ast.literal_eval(expr)
+                    reveal_type(value)
                     py_default = repr(value)
                     if isinstance(value, (bool, NoneType)):
                         c_default = "Py_" + py_default

And then run mypy --config-file Tools/clinic/mypy.ini, mypy reports (both using mypy 1.6 and using your PR branch):

Tools\clinic\clinic.py:5843: note: Revealed type is "Union[clinic.Sentinels, clinic.Null]"

That's because of an incorrect type declaration in clinic.py here: https://github.com/python/cpython/blob/1262e41842957c3b402fc0cf0a6eb2ea230c828f/Tools/clinic/clinic.py#L5719. The type for that variable should probably be declared as object or Any, not Sentinels | Null. So your PR is improving things there: mypy should be reporting an error on that line for unreachable code with mypy 1.6, but isn't.

@tyralla

tyralla commented Oct 25, 2023

Copy link
Copy Markdown
Collaborator Author

I agree that the clinic error is fine, but for a different reason to the one you gave ;)

Thanks for checking and confirming the error report is "true positive". But I see no contradiction between your and mine opinion (except that I definitely should have used more words; sorry for that).

Hm, I just wrote the following example to explain my thinking (the note and error messages are generated by Mypy 1.6):

from typing import final

class Null: ...

@final
class NoneType1: ...

class NoneType2: ...

class Impossible(Null, NoneType1): ...  # error: Cannot inherit from final class "NoneType"  [misc]  (right)

class Possible(Null, NoneType2): ...

x: Null

if isinstance(x, NoneType1):
    reveal_type(x)  # note: Revealed type is "temp.<subclass of "Null" and "NoneType1">"  (wrong)

if isinstance(x, NoneType2):
    reveal_type(x)  # note: Revealed type is "temp.<subclass of "Null" and "NoneType2">"  (right)

I intended to show that the unreachable warning is only correct for final types. (I somehow defined NoneType to be final in my pull request by setting a upper_bound attribute to False somewhere.) But now I see that Mypy understands that combining Null and NoneType1 is impossible due to NoneType1 being final, but still assumes such an impossible subtype can exist when doing type narrowing.

So, either I am confused, or this example reveals another Mypy limitation (that could possibly be easily fixed in this pull request as well).

@tyralla

tyralla commented Oct 25, 2023

Copy link
Copy Markdown
Collaborator Author

So, either I am confused, or this example reveals another Mypy limitation (that could possibly be easily fixed in this pull request as well).

already reported here: #15148

@tyralla

tyralla commented Oct 26, 2023

Copy link
Copy Markdown
Collaborator Author

So, either I am confused, or this example reveals another Mypy limitation (that could possibly be easily fixed in this pull request as well).

I decided to open #16330. So, in my opinion, the pull request here is ready.

@AlexWaygood

Copy link
Copy Markdown
Member

I decided to open #16330. So, in my opinion, the pull request here is ready.

Yeah, wise to defer that to another PR, imo!

@AlexWaygood

Copy link
Copy Markdown
Member

@tyralla, looks like there might be some merge conflicts here now — think you'd be able to merge in master and fix them? :)

# Conflicts:
#	test-data/unit/check-narrowing.test
@tyralla

tyralla commented Dec 4, 2023

Copy link
Copy Markdown
Collaborator Author

@AlexWaygood No big deal; just two tests inserted at the same place.

@github-actions

This comment has been minimized.

@AlexWaygood

Copy link
Copy Markdown
Member
CPython (Argument Clinic) (https://github.com/python/cpython)
- Tools/clinic/clinic.py:5903: error: Subclass of "Sentinels" and "bool" cannot exist: "bool" is final  [unreachable]
- Tools/clinic/clinic.py:5903: error: Subclass of "Sentinels" and "NoneType" cannot exist: "NoneType" is final  [unreachable]
- Tools/clinic/clinic.py:5903: error: Subclass of "Null" and "bool" cannot exist: "bool" is final  [unreachable]
- Tools/clinic/clinic.py:5903: error: Subclass of "Null" and "NoneType" cannot exist: "NoneType" is final  [unreachable]

Hmm, any idea why these errors are going away? I see mypy still emits an error: Statement is unreachable [unreachable] error on line 5904 of clinic.py (following #16330). But the error messages that are going away here are IMO more helpful in figuring out what the actual error is in the clinic.py code

@tyralla

tyralla commented Dec 5, 2023

Copy link
Copy Markdown
Collaborator Author

Yes, something is still missing in this pull request. Smaller repro:

from types import NoneType
from typing import final

class X: ...
class Y: ...
@final
class Z: ...

x: X

if isinstance(x, (Y, Z)):
    reveal_type(x)  # N: note: Revealed type is "__main__.<subclass of "X" and "Y">" (right)
if isinstance(x, (Y, NoneType)):
    reveal_type(x)  # E: Statement is unreachable (wrong)

I will have a look at it later.

…oneType` and modify (fix?) the second algorithm tried in `conditional_types_with_intersection`.
@github-actions

This comment has been minimized.

@github-actions

github-actions Bot commented Dec 5, 2023

Copy link
Copy Markdown
Contributor

Diff from mypy_primer, showing the effect of this PR on open source code:

alerta (https://github.com/alerta/alerta)
+ alerta/models/alert.py:38: error: Unused "type: ignore" comment  [unused-ignore]

discord.py (https://github.com/Rapptz/discord.py)
- discord/app_commands/transformers.py:729: error: Dict entry 0 has incompatible type "AppCommandOptionType": "tuple[type[str], <typing special form>]"; expected "AppCommandOptionType": "tuple[type[Any], ...]"  [dict-item]
- discord/app_commands/transformers.py:730: error: Dict entry 1 has incompatible type "AppCommandOptionType": "tuple[type[int], <typing special form>]"; expected "AppCommandOptionType": "tuple[type[Any], ...]"  [dict-item]
- discord/app_commands/transformers.py:731: error: Dict entry 2 has incompatible type "AppCommandOptionType": "tuple[type[bool], <typing special form>]"; expected "AppCommandOptionType": "tuple[type[Any], ...]"  [dict-item]
- discord/app_commands/transformers.py:732: error: Dict entry 3 has incompatible type "AppCommandOptionType": "tuple[type[float], <typing special form>]"; expected "AppCommandOptionType": "tuple[type[Any], ...]"  [dict-item]
- discord/app_commands/transformers.py:739: error: Incompatible default for argument "_none" (default has type "<typing special form>", argument has type "type")  [assignment]
- discord/app_commands/commands.py:235: error: Incompatible default for argument "_none" (default has type "<typing special form>", argument has type "type")  [assignment]

@tyralla

tyralla commented Jan 9, 2024

Copy link
Copy Markdown
Collaborator Author

@AlexWaygood This has been ready for a while now. Can you merge it, or is there anything left to do?

@AlexWaygood

Copy link
Copy Markdown
Member

@AlexWaygood This has been ready for a while now. Can you merge it, or is there anything left to do?

I'm not a maintainer, just a triager, I'm afraid, so I can review but I can't merge :/ I'm also afraid I'm unusually busy at the moment, so I probably won't be able to take another look at this soon :(

@tyralla

tyralla commented Jan 9, 2024

Copy link
Copy Markdown
Collaborator Author

Ah, okay. I didn't know Mypy has triagers (to be honest, I didn't even know that "triager" is an English word...).

So, this means this pull request has to wait until you find time to finish your review and give the maintainers your okay? (No need for a rush; I just wanted to make sure this won't be forgotten.)

@AlexWaygood

AlexWaygood commented Jan 9, 2024

Copy link
Copy Markdown
Member

So, this means this pull request has to wait until you find time to finish your review and give the maintainers your okay? (No need for a rush; I just wanted to make sure this won't be forgotten.)

Not necessarily; it can be merged as soon as a maintainer approves it. That's maybe slightly more likely to happen if a triager approves it first, but it's very marginal — most PRs at mypy are just reviewed by a maintainer before being merged, as we actually have more maintainers than triagers. But as with most open-source projects, there aren't enough reviewers, so it can unfortunately take some time sometimes :)

@hauntsaninja hauntsaninja left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nice, thank you for the improvement!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

can't narrow a union of type[None]

4 participants