From 05f2c713556be88b73e8c73159d958642683b5ad Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Mon, 23 Oct 2023 08:50:25 +0200 Subject: [PATCH 1/5] Support narrowing unions that include type[None]. --- mypy/checker.py | 8 +++++-- mypy/checkexpr.py | 16 +++++++------ test-data/unit/check-narrowing.test | 35 +++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 9 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index e1b65a95ae98a..e6df053af17b6 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -6831,7 +6831,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)) @@ -7258,7 +7262,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) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 1d5233170a106..182e08c7cb9f5 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -496,13 +496,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)) @@ -4645,6 +4645,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: diff --git a/test-data/unit/check-narrowing.test b/test-data/unit/check-narrowing.test index c86cffd453dfc..f8750b0fe214a 100644 --- a/test-data/unit/check-narrowing.test +++ b/test-data/unit/check-narrowing.test @@ -1334,3 +1334,38 @@ if isinstance(some, raw): else: reveal_type(some) # N: Revealed type is "Union[builtins.int, __main__.Base]" [builtins fixtures/dict.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] From 32ca5f43a2bf4712794097134dd32a9bf59bfe83 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Tue, 24 Oct 2023 06:50:53 +0200 Subject: [PATCH 2/5] special case `types.NoneType` --- mypy/checkexpr.py | 3 +++ test-data/unit/check-narrowing.test | 14 ++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index c0e90664e8ff7..cc0496bfc35df 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -370,6 +370,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] diff --git a/test-data/unit/check-narrowing.test b/test-data/unit/check-narrowing.test index 4c69360dd08d5..1b83dea58efe0 100644 --- a/test-data/unit/check-narrowing.test +++ b/test-data/unit/check-narrowing.test @@ -1945,3 +1945,17 @@ def f(cls: Type[Union[None, int]]) -> 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] From fe12f7d1a62a66c45ba23c45965e4064117a2fff Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Tue, 5 Dec 2023 08:54:00 +0100 Subject: [PATCH 3/5] Add test case `testNarrowingIsInstanceNoIntersectionWithFinalTypeAndNoneType` and modify (fix?) the second algorithm tried in `conditional_types_with_intersection`. --- mypy/checker.py | 7 ++++--- test-data/unit/check-narrowing.test | 22 ++++++++++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 87faaf57e190a..8bdfcd123bbf8 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -7120,9 +7120,10 @@ 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): + possible_target_types.append(item) + if not possible_target_types: + return yes_type, no_type out = [] errors: list[tuple[str, str]] = [] diff --git a/test-data/unit/check-narrowing.test b/test-data/unit/check-narrowing.test index 771b213613567..5f91262cd0b0b 100644 --- a/test-data/unit/check-narrowing.test +++ b/test-data/unit/check-narrowing.test @@ -2067,3 +2067,25 @@ def f(cls: Type[Union[None, int]]) -> 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__." +if isinstance(x, (Y, NoneType)): + reveal_type(x) # N: Revealed type is "__main__.1" +if isinstance(x, (Y, Z, NoneType)): + reveal_type(x) # N: Revealed type is "__main__.2" + +[builtins fixtures/isinstance.pyi] From 58f0562f34af9c8854b50496a96bb1c3e9300410 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Tue, 5 Dec 2023 10:19:42 +0100 Subject: [PATCH 4/5] A little additional effort for `Subclass of "..." and "NoneType" cannot exist: "NoneType" is final` messages. --- mypy/checker.py | 5 ++++- test-data/unit/check-narrowing.test | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index 8bdfcd123bbf8..0f4600efb0239 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -7120,7 +7120,7 @@ def conditional_types_with_intersection( possible_target_types = [] for tr in type_ranges: item = get_proper_type(tr.item) - if isinstance(item, Instance): + if isinstance(item, (Instance, NoneType)): possible_target_types.append(item) if not possible_target_types: return yes_type, no_type @@ -7131,6 +7131,9 @@ def conditional_types_with_intersection( 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"', f'"NoneType" is final')) + continue intersection = self.intersect_instances((v, t), errors) if intersection is None: continue diff --git a/test-data/unit/check-narrowing.test b/test-data/unit/check-narrowing.test index 5f91262cd0b0b..d50d1f508b85d 100644 --- a/test-data/unit/check-narrowing.test +++ b/test-data/unit/check-narrowing.test @@ -2087,5 +2087,8 @@ if isinstance(x, (Y, NoneType)): reveal_type(x) # N: Revealed type is "__main__.1" if isinstance(x, (Y, Z, NoneType)): reveal_type(x) # N: Revealed type is "__main__.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] From 55b2858e87f041ea463c244cf9d8fddc7a0e0afa Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 5 Dec 2023 09:21:26 +0000 Subject: [PATCH 5/5] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/checker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index 0f4600efb0239..e007c0adcc88e 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -7132,7 +7132,7 @@ def conditional_types_with_intersection( return yes_type, no_type for t in possible_target_types: if isinstance(t, NoneType): - errors.append((f'"{v.type.name}" and "NoneType"', f'"NoneType" is final')) + errors.append((f'"{v.type.name}" and "NoneType"', '"NoneType" is final')) continue intersection = self.intersect_instances((v, t), errors) if intersection is None: