-
-
Notifications
You must be signed in to change notification settings - Fork 3k
[match-case] Fix narrowing of class pattern with union-argument. #19473
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
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7996,20 +7996,47 @@ def conditional_types( | |
) -> tuple[Type | None, Type | None]: | ||
"""Takes in the current type and a proposed type of an expression. | ||
|
||
Returns a 2-tuple: The first element is the proposed type, if the expression | ||
can be the proposed type. The second element is the type it would hold | ||
if it was not the proposed type, if any. UninhabitedType means unreachable. | ||
None means no new information can be inferred. If default is set it is returned | ||
instead.""" | ||
Returns a 2-tuple: | ||
The first element is the proposed type, if the expression can be the proposed type. | ||
The second element is the type it would hold if it was not the proposed type, if any. | ||
UninhabitedType means unreachable. | ||
None means no new information can be inferred. | ||
If default is set it is returned instead. | ||
""" | ||
if proposed_type_ranges and len(proposed_type_ranges) == 1: | ||
# expand e.g. bool -> Literal[True] | Literal[False] | ||
target = proposed_type_ranges[0].item | ||
target = get_proper_type(target) | ||
if isinstance(target, LiteralType) and ( | ||
target.is_enum_literal() or isinstance(target.value, bool) | ||
): | ||
enum_name = target.fallback.type.fullname | ||
current_type = try_expanding_sum_type_to_union(current_type, enum_name) | ||
|
||
current_type = get_proper_type(current_type) | ||
if isinstance(current_type, UnionType) and (default == current_type): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If I'm not mistaken, this only works by coincidence and is incorrect in general case. The problem is that you try to union and restrict multiple results here. This is only reasonable when none of the ranges are upper bounds, otherwise this is unsound:
Applying the logic from this PR, you get I can suggest only performing this expansion when There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I ran your test example and it gives the expected results.
I believe it depends on from where this is called.
So, we can see for the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If I try to replicate your example with match-case we get these results: class Base: pass
class A1(Base): ...
class A2(Base): ...
class A3(Base): ...
ref: type[Base] = A1
x: Base | int
match x:
case ref() as y: # E: expected type in class pattern; found "type[tmp.Base]"
reveal_type(y) # E: Statement is unreachable
case other:
reveal_type(other) # Base | int
match x:
case Base() as y:
reveal_type(y) # Base
case other:
reveal_type(other) # int
match x:
case A1() as y:
reveal_type(y) # A1
case other:
reveal_type(other) # Base | int There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @sterliakov I integrated your suggestion into a separate branch #19517, and was able to relax the condition to if (
isinstance(current_type, UnionType)
and not any(tr.is_upper_bound for tr in proposed_type_ranges)
and (default in (current_type, None))
): so that it also is applied with regular isinstance branches when |
||
# factorize over union types | ||
# if we try to narrow A|B to C, we instead narrow A to C and B to C, and | ||
# return the union of the results | ||
result: list[tuple[Type | None, Type | None]] = [ | ||
conditional_types( | ||
union_item, | ||
proposed_type_ranges, | ||
default=union_item, | ||
consider_runtime_isinstance=consider_runtime_isinstance, | ||
) | ||
for union_item in get_proper_types(current_type.items) | ||
] | ||
# separate list of tuples into two lists | ||
yes_types, no_types = zip(*result) | ||
yes_type = make_simplified_union([t for t in yes_types if t is not None]) | ||
no_type = restrict_subtype_away( | ||
current_type, yes_type, consider_runtime_isinstance=consider_runtime_isinstance | ||
) | ||
|
||
return yes_type, no_type | ||
|
||
if proposed_type_ranges: | ||
if len(proposed_type_ranges) == 1: | ||
target = proposed_type_ranges[0].item | ||
target = get_proper_type(target) | ||
if isinstance(target, LiteralType) and ( | ||
target.is_enum_literal() or isinstance(target.value, bool) | ||
): | ||
enum_name = target.fallback.type.fullname | ||
current_type = try_expanding_sum_type_to_union(current_type, enum_name) | ||
proposed_items = [type_range.item for type_range in proposed_type_ranges] | ||
proposed_type = make_simplified_union(proposed_items) | ||
if isinstance(proposed_type, AnyType): | ||
|
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.
Same question as in #19471, but this will be conflicted anyway:)