Skip to content

Add check for comparisons like None in list/set/tuple #17154

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

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 23 additions & 5 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@
RaiseStmt,
RefExpr,
ReturnStmt,
SetExpr,
StarExpr,
Statement,
StrExpr,
Expand Down Expand Up @@ -5928,20 +5929,37 @@ def has_no_custom_eq_checks(t: Type) -> bool:
):
if_map[operands[left_index]] = remove_optional(item_type)

right_iterable_expr = operands[right_index]
if right_index in narrowable_operand_index_to_hash:
if_type, else_type = self.conditional_types_for_iterable(
item_type, iterable_type
)
expr = operands[right_index]
if if_type is None:
if_map = None
else:
if_map[expr] = if_type
if_map[right_iterable_expr] = if_type
if else_type is None:
else_map = None
else:
else_map[expr] = else_type
else_map[right_iterable_expr] = else_type

# check for `None in <some_iterable>`
if (
isinstance(get_proper_type(item_type), NoneType)
and isinstance(right_iterable_expr, (ListExpr, TupleExpr, SetExpr))
# Ensure the iterable does not inherently contain None literals
and not any(
is_literal_none(iterable_item)
for iterable_item in right_iterable_expr.items
)
):
if else_map is None:
else_map = {}
for iterable_item in right_iterable_expr.items:
proper_item_type = self.lookup_type(iterable_item)
# Remove the option of the current item to be `None` for the entire else scope
if is_overlapping_none(proper_item_type):
else_map[iterable_item] = remove_optional(proper_item_type)
else:
if_map = {}
else_map = {}
Expand Down Expand Up @@ -6006,8 +6024,8 @@ def has_no_custom_eq_checks(t: Type) -> bool:
and_conditional_maps(left_else_vars, right_else_vars),
)
elif isinstance(node, UnaryExpr) and node.op == "not":
left, right = self.find_isinstance_check(node.expr)
return right, left
left, iterable_expr = self.find_isinstance_check(node.expr)
return iterable_expr, left
elif (
literal(node) == LITERAL_TYPE
and self.has_type(node)
Expand Down
65 changes: 65 additions & 0 deletions test-data/unit/check-optional.test
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,71 @@ else:
reveal_type(x) # N: Revealed type is "Any"
[builtins fixtures/bool.pyi]

[case testRefinementAfterNoneInCheck]
from typing import Optional, List

def refine_types(a: Optional[int], b: Optional[str], c: Optional[float]) -> str:
if None in [a, b, c]:
return "One or more are None"
else:
reveal_type(a) # N: Revealed type is "builtins.int"
reveal_type(b) # N: Revealed type is "builtins.str"
reveal_type(c) # N: Revealed type is "builtins.float"
return f"{a}, {b}, {c}"

a: Optional[int] = 5
b: Optional[str] = "hello"
c: Optional[float] = None

result = refine_types(a, b, c)
reveal_type(result) # N: Revealed type is "builtins.str"

[case testRefinementWithComplexCondition]
from typing import Optional, List

def refine_complex(a: Optional[int], b: Optional[float], c: Optional[str]) -> str:
# Test with complex conditions mixed with `None in`
if None not in (a, b, c) and b + 3 != 5.5:
reveal_type(a) # N: Revealed type is "builtins.int"
reveal_type(b) # N: Revealed type is "builtins.float"
reveal_type(c) # N: Revealed type is "builtins.str"
return f"{a}, {b}, {c}"
reveal_type(a) # N: Revealed type is "Union[builtins.int, None]"
reveal_type(b) # N: Revealed type is "Union[builtins.float, None]"
reveal_type(c) # N: Revealed type is "Union[builtins.str, None]"
return "All are valid"

a: Optional[int] = 5
b: Optional[float] = 10.3
c: Optional[str] = "cCCCc"

result_complex = refine_complex(a, b, c)
reveal_type(result_complex) # N: Revealed type is "builtins.str"

[case testRefinementFailureWhenNonePresent]
from typing import Optional, List

def check_failure(a: Optional[int], b: Optional[float], c: Optional[str]) -> str:
if None in {a, b, c}:
print(a + 3) # E: Unsupported operand types for + ("None" and "int") [operator]
print(b.is_integer()) # E: Item "None" of "float | None" has no attribute "is_integer" [union-attr]
print(c.upper()) # E: Item "None" of "str | None" has no attribute "upper" [union-attr]
return "None is present"
else:
print(a + 3)
print(b.is_integer())
print(c.upper())
return "All are valid"

[case testNotRefiningWhenNoneIsInTheIterable]
def not_refine_none_in_iterable(a: Optional[int], b: Optional[float], c: Optional[str]) -> str:
# Test with complex conditions mixed with `None in`
if None not in {a, b, c, None} and b + 3 != 5.5: # E: Unsupported operand types for + ("None" and "int")
reveal_type(a) # N: Revealed type is "Union[builtins.int, None]"
reveal_type(b) # N: Revealed type is "Union[builtins.float, None]"
reveal_type(c) # N: Revealed type is "Union[builtins.str, None]"
return "Bla Bla"

[case testOrCases]
from typing import Optional
x = None # type: Optional[str]
Expand Down