From 274789caece4686606e78c60f9a9e4fd8cfec672 Mon Sep 17 00:00:00 2001 From: STerliakov Date: Mon, 7 Apr 2025 21:20:11 +0200 Subject: [PATCH 1/5] Do not leak TypeGuardedType from `narrow_declared_type` with enums --- mypy/meet.py | 7 ++++++- test-data/unit/check-typeguard.test | 29 +++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/mypy/meet.py b/mypy/meet.py index b5262f87c0bd..add0785f5e71 100644 --- a/mypy/meet.py +++ b/mypy/meet.py @@ -143,7 +143,12 @@ def narrow_declared_type(declared: Type, narrowed: Type) -> Type: ] ) if is_enum_overlapping_union(declared, narrowed): - return original_narrowed + # Quick check before reaching `is_overlapping_types`. If it's enum/literal overlap, + # avoid full expansion and make it faster. + assert isinstance(narrowed, UnionType) + return make_simplified_union( + [narrow_declared_type(declared, x) for x in narrowed.relevant_items()] + ) elif not is_overlapping_types(declared, narrowed, prohibit_none_typevar_overlap=True): if state.strict_optional: return UninhabitedType() diff --git a/test-data/unit/check-typeguard.test b/test-data/unit/check-typeguard.test index 71c4473fbfaa..b3d05ef258db 100644 --- a/test-data/unit/check-typeguard.test +++ b/test-data/unit/check-typeguard.test @@ -803,3 +803,32 @@ def test() -> None: return reveal_type(x) # N: Revealed type is "builtins.list[__main__.C]" [builtins fixtures/tuple.pyi] + +[case testTypeGuardedTypeDoesNotLeak] +# https://github.com/python/mypy/issues/18895 +from enum import Enum +from typing import Union +from typing_extensions import TypeGuard, Literal + +class Model(str, Enum): + MODEL_A1 = 'model_a1' + MODEL_A2 = 'model_a2' + MODEL_B = 'model_b' + +MODEL_A = Literal[Model.MODEL_A1, Model.MODEL_A2] +MODEL_B = Literal[Model.MODEL_B] + +def is_model_a(model: str) -> TypeGuard[MODEL_A]: + return True + +def is_model_b(model: str) -> TypeGuard[MODEL_B]: + return True + +def process_model(model: Union[MODEL_A, MODEL_B]) -> int: + return 42 + +def handle(model: Model) -> int: + if is_model_a(model) or is_model_b(model): + return process_model(model) + return 0 +[builtins fixtures/tuple.pyi] From aa219cff7eea6ffb79d238fb419fb408bdcad5fb Mon Sep 17 00:00:00 2001 From: STerliakov Date: Mon, 7 Apr 2025 21:29:28 +0200 Subject: [PATCH 2/5] Restore error back --- test-data/unit/check-typeguard.test | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test-data/unit/check-typeguard.test b/test-data/unit/check-typeguard.test index b3d05ef258db..f143ab342b85 100644 --- a/test-data/unit/check-typeguard.test +++ b/test-data/unit/check-typeguard.test @@ -829,6 +829,7 @@ def process_model(model: Union[MODEL_A, MODEL_B]) -> int: def handle(model: Model) -> int: if is_model_a(model) or is_model_b(model): - return process_model(model) + # TODO: should start passing after #18896 + return process_model(model) # E: Argument 1 to "process_model" has incompatible type "Model"; expected "Union[Literal[Model.MODEL_A1, Model.MODEL_A2], Literal[Model.MODEL_B]]" return 0 [builtins fixtures/tuple.pyi] From ac0d9cb51ee01852beb9741d466d94080edec66a Mon Sep 17 00:00:00 2001 From: STerliakov Date: Wed, 9 Apr 2025 15:23:57 +0200 Subject: [PATCH 3/5] Remove error from the test again, fixed separately --- test-data/unit/check-typeguard.test | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/test-data/unit/check-typeguard.test b/test-data/unit/check-typeguard.test index f143ab342b85..d4888cfcb271 100644 --- a/test-data/unit/check-typeguard.test +++ b/test-data/unit/check-typeguard.test @@ -807,16 +807,16 @@ def test() -> None: [case testTypeGuardedTypeDoesNotLeak] # https://github.com/python/mypy/issues/18895 from enum import Enum -from typing import Union -from typing_extensions import TypeGuard, Literal +from typing import Literal, Union +from typing_extensions import TypeGuard class Model(str, Enum): - MODEL_A1 = 'model_a1' - MODEL_A2 = 'model_a2' - MODEL_B = 'model_b' + A1 = 'model_a1' + A2 = 'model_a2' + B = 'model_b' -MODEL_A = Literal[Model.MODEL_A1, Model.MODEL_A2] -MODEL_B = Literal[Model.MODEL_B] +MODEL_A = Literal[Model.A1, Model.A2] +MODEL_B = Literal[Model.B] def is_model_a(model: str) -> TypeGuard[MODEL_A]: return True @@ -829,7 +829,6 @@ def process_model(model: Union[MODEL_A, MODEL_B]) -> int: def handle(model: Model) -> int: if is_model_a(model) or is_model_b(model): - # TODO: should start passing after #18896 - return process_model(model) # E: Argument 1 to "process_model" has incompatible type "Model"; expected "Union[Literal[Model.MODEL_A1, Model.MODEL_A2], Literal[Model.MODEL_B]]" + return process_model(model) return 0 [builtins fixtures/tuple.pyi] From b3b5f922dcb673f59f853a6db701b1260aa5d286 Mon Sep 17 00:00:00 2001 From: STerliakov Date: Wed, 9 Apr 2025 15:25:47 +0200 Subject: [PATCH 4/5] Add an explicit test for changed behaviour of enum/union overlap --- test-data/unit/check-typeis.test | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test-data/unit/check-typeis.test b/test-data/unit/check-typeis.test index e70c71a4b62e..4f9d684d40aa 100644 --- a/test-data/unit/check-typeis.test +++ b/test-data/unit/check-typeis.test @@ -936,3 +936,19 @@ def func(arg: Any) -> None: if is_dataclass(arg): reveal_type(arg) # N: Revealed type is "Union[Type[__main__.DataclassInstance], __main__.DataclassInstance]" [builtins fixtures/tuple.pyi] + +[case testTypeIsEnumOverlappingUnionExcludesIrrelevant] +from enum import Enum +from typing import Literal +from typing_extensions import TypeIs + +class Model(str, Enum): + A = 'a' + B = 'a' + +def is_model_a(model: str) -> TypeIs[Literal[Model.A, "foo"]]: + return True +def handle(model: Model) -> None: + if is_model_a(model): + reveal_type(model) # N: Revealed type is "Literal[__main__.Model.A]" +[builtins fixtures/tuple.pyi] From c8bd14800b255d13ef9fca2399cd3448fd7ef699 Mon Sep 17 00:00:00 2001 From: STerliakov Date: Wed, 9 Apr 2025 15:52:23 +0200 Subject: [PATCH 5/5] Address review comments --- test-data/unit/check-typeguard.test | 1 + test-data/unit/check-typeis.test | 1 + 2 files changed, 2 insertions(+) diff --git a/test-data/unit/check-typeguard.test b/test-data/unit/check-typeguard.test index d4888cfcb271..00bf7d211927 100644 --- a/test-data/unit/check-typeguard.test +++ b/test-data/unit/check-typeguard.test @@ -829,6 +829,7 @@ def process_model(model: Union[MODEL_A, MODEL_B]) -> int: def handle(model: Model) -> int: if is_model_a(model) or is_model_b(model): + reveal_type(model) # N: Revealed type is "__main__.Model" return process_model(model) return 0 [builtins fixtures/tuple.pyi] diff --git a/test-data/unit/check-typeis.test b/test-data/unit/check-typeis.test index 4f9d684d40aa..8cdcf8634788 100644 --- a/test-data/unit/check-typeis.test +++ b/test-data/unit/check-typeis.test @@ -948,6 +948,7 @@ class Model(str, Enum): def is_model_a(model: str) -> TypeIs[Literal[Model.A, "foo"]]: return True + def handle(model: Model) -> None: if is_model_a(model): reveal_type(model) # N: Revealed type is "Literal[__main__.Model.A]"