Skip to content

Commit 261e569

Browse files
authored
Support narrowing unions that include type[None] (#16315)
Fixes #16279 See my comment in the referenced issue.
1 parent b1fe23f commit 261e569

File tree

3 files changed

+95
-12
lines changed

3 files changed

+95
-12
lines changed

mypy/checker.py

+13-5
Original file line numberDiff line numberDiff line change
@@ -7121,16 +7121,20 @@ def conditional_types_with_intersection(
71217121
possible_target_types = []
71227122
for tr in type_ranges:
71237123
item = get_proper_type(tr.item)
7124-
if not isinstance(item, Instance) or tr.is_upper_bound:
7125-
return yes_type, no_type
7126-
possible_target_types.append(item)
7124+
if isinstance(item, (Instance, NoneType)):
7125+
possible_target_types.append(item)
7126+
if not possible_target_types:
7127+
return yes_type, no_type
71277128

71287129
out = []
71297130
errors: list[tuple[str, str]] = []
71307131
for v in possible_expr_types:
71317132
if not isinstance(v, Instance):
71327133
return yes_type, no_type
71337134
for t in possible_target_types:
7135+
if isinstance(t, NoneType):
7136+
errors.append((f'"{v.type.name}" and "NoneType"', '"NoneType" is final'))
7137+
continue
71347138
intersection = self.intersect_instances((v, t), errors)
71357139
if intersection is None:
71367140
continue
@@ -7174,7 +7178,11 @@ def get_isinstance_type(self, expr: Expression) -> list[TypeRange] | None:
71747178
elif isinstance(typ, TypeType):
71757179
# Type[A] means "any type that is a subtype of A" rather than "precisely type A"
71767180
# we indicate this by setting is_upper_bound flag
7177-
types.append(TypeRange(typ.item, is_upper_bound=True))
7181+
is_upper_bound = True
7182+
if isinstance(typ.item, NoneType):
7183+
# except for Type[None], because "'NoneType' is not an acceptable base type"
7184+
is_upper_bound = False
7185+
types.append(TypeRange(typ.item, is_upper_bound=is_upper_bound))
71787186
elif isinstance(typ, Instance) and typ.type.fullname == "builtins.type":
71797187
object_type = Instance(typ.type.mro[-1], [])
71807188
types.append(TypeRange(object_type, is_upper_bound=True))
@@ -7627,7 +7635,7 @@ def convert_to_typetype(type_map: TypeMap) -> TypeMap:
76277635
if isinstance(t, TypeVarType):
76287636
t = t.upper_bound
76297637
# TODO: should we only allow unions of instances as per PEP 484?
7630-
if not isinstance(get_proper_type(t), (UnionType, Instance)):
7638+
if not isinstance(get_proper_type(t), (UnionType, Instance, NoneType)):
76317639
# unknown type; error was likely reported earlier
76327640
return {}
76337641
converted_type_map[expr] = TypeType.make_normalized(typ)

mypy/checkexpr.py

+12-7
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,9 @@ def analyze_ref_expr(self, e: RefExpr, lvalue: bool = False) -> Type:
385385
if node.typeddict_type:
386386
# We special-case TypedDict, because they don't define any constructor.
387387
result = self.typeddict_callable(node)
388+
elif node.fullname == "types.NoneType":
389+
# We special case NoneType, because its stub definition is not related to None.
390+
result = TypeType(NoneType())
388391
else:
389392
result = type_object_type(node, self.named_type)
390393
if isinstance(result, CallableType) and isinstance( # type: ignore[misc]
@@ -511,13 +514,13 @@ def visit_call_expr_inner(self, e: CallExpr, allow_none_return: bool = False) ->
511514
if is_expr_literal_type(typ):
512515
self.msg.cannot_use_function_with_type(e.callee.name, "Literal", e)
513516
continue
514-
if (
515-
node
516-
and isinstance(node.node, TypeAlias)
517-
and isinstance(get_proper_type(node.node.target), AnyType)
518-
):
519-
self.msg.cannot_use_function_with_type(e.callee.name, "Any", e)
520-
continue
517+
if node and isinstance(node.node, TypeAlias):
518+
target = get_proper_type(node.node.target)
519+
if isinstance(target, AnyType):
520+
self.msg.cannot_use_function_with_type(e.callee.name, "Any", e)
521+
continue
522+
if isinstance(target, NoneType):
523+
continue
521524
if (
522525
isinstance(typ, IndexExpr)
523526
and isinstance(typ.analyzed, (TypeApplication, TypeAliasExpr))
@@ -4731,6 +4734,8 @@ class LongName(Generic[T]): ...
47314734
return type_object_type(tuple_fallback(item).type, self.named_type)
47324735
elif isinstance(item, TypedDictType):
47334736
return self.typeddict_callable_from_context(item)
4737+
elif isinstance(item, NoneType):
4738+
return TypeType(item, line=item.line, column=item.column)
47344739
elif isinstance(item, AnyType):
47354740
return AnyType(TypeOfAny.from_another_any, source_any=item)
47364741
else:

test-data/unit/check-narrowing.test

+70
Original file line numberDiff line numberDiff line change
@@ -2022,3 +2022,73 @@ def f(x: Union[int, Sequence[int]]) -> None:
20222022
):
20232023
reveal_type(x) # N: Revealed type is "Tuple[builtins.int, builtins.int]"
20242024
[builtins fixtures/len.pyi]
2025+
2026+
[case testNarrowingIsSubclassNoneType1]
2027+
from typing import Type, Union
2028+
2029+
def f(cls: Type[Union[None, int]]) -> None:
2030+
if issubclass(cls, int):
2031+
reveal_type(cls) # N: Revealed type is "Type[builtins.int]"
2032+
else:
2033+
reveal_type(cls) # N: Revealed type is "Type[None]"
2034+
[builtins fixtures/isinstance.pyi]
2035+
2036+
[case testNarrowingIsSubclassNoneType2]
2037+
from typing import Type, Union
2038+
2039+
def f(cls: Type[Union[None, int]]) -> None:
2040+
if issubclass(cls, type(None)):
2041+
reveal_type(cls) # N: Revealed type is "Type[None]"
2042+
else:
2043+
reveal_type(cls) # N: Revealed type is "Type[builtins.int]"
2044+
[builtins fixtures/isinstance.pyi]
2045+
2046+
[case testNarrowingIsSubclassNoneType3]
2047+
from typing import Type, Union
2048+
2049+
NoneType_ = type(None)
2050+
2051+
def f(cls: Type[Union[None, int]]) -> None:
2052+
if issubclass(cls, NoneType_):
2053+
reveal_type(cls) # N: Revealed type is "Type[None]"
2054+
else:
2055+
reveal_type(cls) # N: Revealed type is "Type[builtins.int]"
2056+
[builtins fixtures/isinstance.pyi]
2057+
2058+
[case testNarrowingIsSubclassNoneType4]
2059+
# flags: --python-version 3.10
2060+
2061+
from types import NoneType
2062+
from typing import Type, Union
2063+
2064+
def f(cls: Type[Union[None, int]]) -> None:
2065+
if issubclass(cls, NoneType):
2066+
reveal_type(cls) # N: Revealed type is "Type[None]"
2067+
else:
2068+
reveal_type(cls) # N: Revealed type is "Type[builtins.int]"
2069+
[builtins fixtures/isinstance.pyi]
2070+
2071+
[case testNarrowingIsInstanceNoIntersectionWithFinalTypeAndNoneType]
2072+
# flags: --warn-unreachable --python-version 3.10
2073+
2074+
from types import NoneType
2075+
from typing import final
2076+
2077+
class X: ...
2078+
class Y: ...
2079+
@final
2080+
class Z: ...
2081+
2082+
x: X
2083+
2084+
if isinstance(x, (Y, Z)):
2085+
reveal_type(x) # N: Revealed type is "__main__.<subclass of "X" and "Y">"
2086+
if isinstance(x, (Y, NoneType)):
2087+
reveal_type(x) # N: Revealed type is "__main__.<subclass of "X" and "Y">1"
2088+
if isinstance(x, (Y, Z, NoneType)):
2089+
reveal_type(x) # N: Revealed type is "__main__.<subclass of "X" and "Y">2"
2090+
if isinstance(x, (Z, NoneType)): # E: Subclass of "X" and "Z" cannot exist: "Z" is final \
2091+
# E: Subclass of "X" and "NoneType" cannot exist: "NoneType" is final
2092+
reveal_type(x) # E: Statement is unreachable
2093+
2094+
[builtins fixtures/isinstance.pyi]

0 commit comments

Comments
 (0)