Skip to content

Commit

Permalink
Support narrowing unions that include type[None] (#16315)
Browse files Browse the repository at this point in the history
Fixes #16279

See my comment in the referenced issue.
  • Loading branch information
tyralla authored Jan 14, 2024
1 parent b1fe23f commit 261e569
Show file tree
Hide file tree
Showing 3 changed files with 95 additions and 12 deletions.
18 changes: 13 additions & 5 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -7121,16 +7121,20 @@ def conditional_types_with_intersection(
possible_target_types = []
for tr in type_ranges:
item = get_proper_type(tr.item)
if not isinstance(item, Instance) or tr.is_upper_bound:
return yes_type, no_type
possible_target_types.append(item)
if isinstance(item, (Instance, NoneType)):
possible_target_types.append(item)
if not possible_target_types:
return yes_type, no_type

out = []
errors: list[tuple[str, str]] = []
for v in possible_expr_types:
if not isinstance(v, Instance):
return yes_type, no_type
for t in possible_target_types:
if isinstance(t, NoneType):
errors.append((f'"{v.type.name}" and "NoneType"', '"NoneType" is final'))
continue
intersection = self.intersect_instances((v, t), errors)
if intersection is None:
continue
Expand Down Expand Up @@ -7174,7 +7178,11 @@ def get_isinstance_type(self, expr: Expression) -> list[TypeRange] | None:
elif isinstance(typ, TypeType):
# Type[A] means "any type that is a subtype of A" rather than "precisely type A"
# we indicate this by setting is_upper_bound flag
types.append(TypeRange(typ.item, is_upper_bound=True))
is_upper_bound = True
if isinstance(typ.item, NoneType):
# except for Type[None], because "'NoneType' is not an acceptable base type"
is_upper_bound = False
types.append(TypeRange(typ.item, is_upper_bound=is_upper_bound))
elif isinstance(typ, Instance) and typ.type.fullname == "builtins.type":
object_type = Instance(typ.type.mro[-1], [])
types.append(TypeRange(object_type, is_upper_bound=True))
Expand Down Expand Up @@ -7627,7 +7635,7 @@ def convert_to_typetype(type_map: TypeMap) -> TypeMap:
if isinstance(t, TypeVarType):
t = t.upper_bound
# TODO: should we only allow unions of instances as per PEP 484?
if not isinstance(get_proper_type(t), (UnionType, Instance)):
if not isinstance(get_proper_type(t), (UnionType, Instance, NoneType)):
# unknown type; error was likely reported earlier
return {}
converted_type_map[expr] = TypeType.make_normalized(typ)
Expand Down
19 changes: 12 additions & 7 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,9 @@ def analyze_ref_expr(self, e: RefExpr, lvalue: bool = False) -> Type:
if node.typeddict_type:
# We special-case TypedDict, because they don't define any constructor.
result = self.typeddict_callable(node)
elif node.fullname == "types.NoneType":
# We special case NoneType, because its stub definition is not related to None.
result = TypeType(NoneType())
else:
result = type_object_type(node, self.named_type)
if isinstance(result, CallableType) and isinstance( # type: ignore[misc]
Expand Down Expand Up @@ -511,13 +514,13 @@ def visit_call_expr_inner(self, e: CallExpr, allow_none_return: bool = False) ->
if is_expr_literal_type(typ):
self.msg.cannot_use_function_with_type(e.callee.name, "Literal", e)
continue
if (
node
and isinstance(node.node, TypeAlias)
and isinstance(get_proper_type(node.node.target), AnyType)
):
self.msg.cannot_use_function_with_type(e.callee.name, "Any", e)
continue
if node and isinstance(node.node, TypeAlias):
target = get_proper_type(node.node.target)
if isinstance(target, AnyType):
self.msg.cannot_use_function_with_type(e.callee.name, "Any", e)
continue
if isinstance(target, NoneType):
continue
if (
isinstance(typ, IndexExpr)
and isinstance(typ.analyzed, (TypeApplication, TypeAliasExpr))
Expand Down Expand Up @@ -4731,6 +4734,8 @@ class LongName(Generic[T]): ...
return type_object_type(tuple_fallback(item).type, self.named_type)
elif isinstance(item, TypedDictType):
return self.typeddict_callable_from_context(item)
elif isinstance(item, NoneType):
return TypeType(item, line=item.line, column=item.column)
elif isinstance(item, AnyType):
return AnyType(TypeOfAny.from_another_any, source_any=item)
else:
Expand Down
70 changes: 70 additions & 0 deletions test-data/unit/check-narrowing.test
Original file line number Diff line number Diff line change
Expand Up @@ -2022,3 +2022,73 @@ def f(x: Union[int, Sequence[int]]) -> None:
):
reveal_type(x) # N: Revealed type is "Tuple[builtins.int, builtins.int]"
[builtins fixtures/len.pyi]

[case testNarrowingIsSubclassNoneType1]
from typing import Type, Union

def f(cls: Type[Union[None, int]]) -> None:
if issubclass(cls, int):
reveal_type(cls) # N: Revealed type is "Type[builtins.int]"
else:
reveal_type(cls) # N: Revealed type is "Type[None]"
[builtins fixtures/isinstance.pyi]

[case testNarrowingIsSubclassNoneType2]
from typing import Type, Union

def f(cls: Type[Union[None, int]]) -> None:
if issubclass(cls, type(None)):
reveal_type(cls) # N: Revealed type is "Type[None]"
else:
reveal_type(cls) # N: Revealed type is "Type[builtins.int]"
[builtins fixtures/isinstance.pyi]

[case testNarrowingIsSubclassNoneType3]
from typing import Type, Union

NoneType_ = type(None)

def f(cls: Type[Union[None, int]]) -> None:
if issubclass(cls, NoneType_):
reveal_type(cls) # N: Revealed type is "Type[None]"
else:
reveal_type(cls) # N: Revealed type is "Type[builtins.int]"
[builtins fixtures/isinstance.pyi]

[case testNarrowingIsSubclassNoneType4]
# flags: --python-version 3.10

from types import NoneType
from typing import Type, Union

def f(cls: Type[Union[None, int]]) -> None:
if issubclass(cls, NoneType):
reveal_type(cls) # N: Revealed type is "Type[None]"
else:
reveal_type(cls) # N: Revealed type is "Type[builtins.int]"
[builtins fixtures/isinstance.pyi]

[case testNarrowingIsInstanceNoIntersectionWithFinalTypeAndNoneType]
# flags: --warn-unreachable --python-version 3.10

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: Revealed type is "__main__.<subclass of "X" and "Y">"
if isinstance(x, (Y, NoneType)):
reveal_type(x) # N: Revealed type is "__main__.<subclass of "X" and "Y">1"
if isinstance(x, (Y, Z, NoneType)):
reveal_type(x) # N: Revealed type is "__main__.<subclass of "X" and "Y">2"
if isinstance(x, (Z, NoneType)): # E: Subclass of "X" and "Z" cannot exist: "Z" is final \
# E: Subclass of "X" and "NoneType" cannot exist: "NoneType" is final
reveal_type(x) # E: Statement is unreachable

[builtins fixtures/isinstance.pyi]

0 comments on commit 261e569

Please sign in to comment.