From 02796d944b0144535a7029e18947c49ca45c161b Mon Sep 17 00:00:00 2001 From: Donal Burns Date: Fri, 23 May 2025 19:00:25 +0100 Subject: [PATCH 01/25] Add check for exhaustive match statements --- mypy/checker.py | 7 ++ mypy/messages.py | 8 ++ test-data/unit/check-match-exhaustive.test | 136 +++++++++++++++++++++ 3 files changed, 151 insertions(+) create mode 100644 test-data/unit/check-match-exhaustive.test diff --git a/mypy/checker.py b/mypy/checker.py index aceb0291926a..24f891fc7831 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -5452,6 +5452,7 @@ def visit_match_stmt(self, s: MatchStmt) -> None: inferred_types = self.infer_variable_types_from_type_maps(type_maps) # The second pass narrows down the types and type checks bodies. + unmatched_types: TypeMap = None for p, g, b in zip(s.patterns, s.guards, s.bodies): current_subject_type = self.expr_checker.narrow_type_from_binder( named_subject, subject_type @@ -5508,6 +5509,12 @@ def visit_match_stmt(self, s: MatchStmt) -> None: else: self.accept(b) self.push_type_map(else_map, from_assignment=False) + unmatched_types = else_map + + if unmatched_types is not None: + # for expr, typ in unmatched_types.items(): + for typ in set(unmatched_types.values()): + self.msg.match_statement_unexhaustive_match(typ, s) # This is needed due to a quirk in frame_context. Without it types will stay narrowed # after the match. diff --git a/mypy/messages.py b/mypy/messages.py index 2e07d7f63498..c7f6207272bf 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -2486,6 +2486,14 @@ def type_parameters_should_be_declared(self, undeclared: list[str], context: Con code=codes.VALID_TYPE, ) + def match_statement_unexhaustive_match(self, typ: Type, context: Context) -> None: + type_str = format_type(typ, self.options) + msg = ( + f"Cases within match statement do not exhaustively handle all values: {type_str}." + " If not intended to handle all cases, use `case _: pass`" + ) + self.fail(msg, context) + def quote_type_string(type_string: str) -> str: """Quotes a type representation for use in messages.""" diff --git a/test-data/unit/check-match-exhaustive.test b/test-data/unit/check-match-exhaustive.test new file mode 100644 index 000000000000..cdb995e078de --- /dev/null +++ b/test-data/unit/check-match-exhaustive.test @@ -0,0 +1,136 @@ +[case testExhaustiveMatchNoFlag] +# flags: --python-version 3.12 +a: int = 5 +match a: + case 1: + pass + case _: + pass + +b: str = "hello" +match b: + case "bye": + pass + case _: + pass + +[case testNonExhaustiveMatchNoFlag] +# flags: --python-version 3.12 +a: int = 5 +match a: + case 1: + pass + +b: str = "hello" +match b: + case "bye": + pass + + +[case testExhaustiveMatchWithFlag] +# flags: --python-version 3.12 --only-allow-exhaustive-match-statements +a: int = 5 +match a: + case 1: + pass + case _: + pass + +b: str = "hello" +match b: + case "bye": + pass + case _: + pass + +[case testNonExhaustiveMatchWithFlag] +# flags: --python-version 3.12 --only-allow-exhaustive-match-statements +a: int = 5 +match a: # E: Cases within match statement do not exhaustively handle all values: "int". If not intended to handle all cases, use `case _: pass` + case 1: + pass + +b: str = "hello" +match b: # E: Cases within match statement do not exhaustively handle all values: "str". If not intended to handle all cases, use `case _: pass` + case "bye": + pass + +[case testEnumNonExhaustiveWithFlag] +# flags: --python-version 3.12 --only-allow-exhaustive-match-statements + +import enum + +class Color(enum.Enum): + RED = 1 + BLUE = 2 + GREEN = 3 + +val: Color = Color.RED + +match val: # E: Cases within match statement do not exhaustively handle all values: "Literal[Color.GREEN]". If not intended to handle all cases, use `case _: pass` + case Color.RED: + a = "red" + case Color.BLUE: + a= "blue" + +[builtins fixtures/enum.pyi] + + +[case testEnumExhaustiveWithFlag] +# flags: --python-version 3.12 --only-allow-exhaustive-match-statements + +import enum + +class Color(enum.Enum): + RED = 1 + BLUE = 2 + +val: Color = Color.RED + +match val: + case Color.RED: + a = "red" + case Color.BLUE: + a= "blue" + +[builtins fixtures/enum.pyi] + +[case testEnumMultipleMissingMatchesWithFlag] +# flags: --python-version 3.12 --only-allow-exhaustive-match-statements + +import enum + +class Color(enum.Enum): + RED = 1 + BLUE = 2 + GREEN = 3 + +val: Color = Color.RED + +match val: # E: Cases within match statement do not exhaustively handle all values: "Literal[Color.BLUE, Color.GREEN]". If not intended to handle all cases, use `case _: pass` + case Color.RED: + a = "red" + + +[builtins fixtures/enum.pyi] + +[case testEnumFallbackWithFlag] +# flags: --python-version 3.12 --only-allow-exhaustive-match-statements + +import enum + +class Color(enum.Enum): + RED = 1 + BLUE = 2 + GREEN = 3 + +val: Color = Color.RED + +match val: + case Color.RED: + a = "red" + case _: + a = "other" + + +[builtins fixtures/enum.pyi] From 3aaa98b2f2a6609f82d45172702e63c378085901 Mon Sep 17 00:00:00 2001 From: Donal Burns Date: Fri, 23 May 2025 21:54:02 +0100 Subject: [PATCH 02/25] Add flag for exhaustive match statements --- mypy/checker.py | 6 ++++-- mypy/main.py | 7 +++++++ mypy/options.py | 3 +++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 24f891fc7831..ea4fc72c25bc 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -5511,8 +5511,10 @@ def visit_match_stmt(self, s: MatchStmt) -> None: self.push_type_map(else_map, from_assignment=False) unmatched_types = else_map - if unmatched_types is not None: - # for expr, typ in unmatched_types.items(): + if ( + self.options.only_allow_exhaustive_match_statements is True + and unmatched_types is not None + ): for typ in set(unmatched_types.values()): self.msg.match_statement_unexhaustive_match(typ, s) diff --git a/mypy/main.py b/mypy/main.py index 6ebf32ded6e1..8994bbf9c7f3 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -931,6 +931,13 @@ def add_invertible_flag( "and makes arguments prepended via Concatenate positional-only", group=strictness_group, ) + add_invertible_flag( + "--only-allow-exhaustive-match-statements", + default=False, + strict_flag=False, + help="Raise type error for match statements that do not match exhaustively", + group=strictness_group, + ) strict_help = "Strict mode; enables the following flags: {}".format( ", ".join(strict_flag_names) diff --git a/mypy/options.py b/mypy/options.py index 52afd27211ed..dbcbc9f450c6 100644 --- a/mypy/options.py +++ b/mypy/options.py @@ -349,6 +349,9 @@ def __init__(self) -> None: # Use this sparingly to avoid tests diverging from non-test behavior. self.test_env = False + # Only allow exhaustive match statements + self.only_allow_exhaustive_match_statements = False + # -- experimental options -- self.shadow_file: list[list[str]] | None = None self.show_column_numbers: bool = False From 6b613f5cf33545f70bb89258bef716945f11a592 Mon Sep 17 00:00:00 2001 From: Donal Burns Date: Fri, 23 May 2025 22:25:16 +0100 Subject: [PATCH 03/25] Update docs for exhaustive match statements --- docs/source/command_line.rst | 38 ++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/docs/source/command_line.rst b/docs/source/command_line.rst index b455e287017e..9742eb1b58fa 100644 --- a/docs/source/command_line.rst +++ b/docs/source/command_line.rst @@ -799,6 +799,44 @@ of the above sections. x = 'a string' x.trim() # error: "str" has no attribute "trim" [attr-defined] +.. option:: --only-allow-exhaustive-match-statements + + This flag will cause mypy to report an error whenever it encounters a match statement + that does not cover all possible cases. + + .. code-block:: python + + import enum + + + class Color(enum.Enum): + RED = 1 + BLUE = 2 + + val: Color = Color.RED + + match val: # error: Cases within match statement do not exhaustively handle all values: "Literal[Color.BLUE]". If not intended to handle all cases, use `case _: pass` + case Color.RED: + print("red") + # without --only-allow-exhaustive-match-statements + match val: + case Color.RED: + print("red") + # with --only-allow-exhaustive-match-statements + match val: # error: Cases within match statement do not exhaustively handle all values: "Literal[Color.BLUE]". If not intended to handle all cases, use `case _: pass` + case Color.RED: + print("red") + + + # no error with --only-allow-exhaustive-match-statements since all cases are handled + match val: # error: Cases within match statement do not exhaustively handle all values: "Literal[Color.BLUE]". If not intended to handle all cases, use `case _: pass` + case Color.RED: + print("red") + case _: + print("other") + + + .. _configuring-error-messages: Configuring error messages From 3341b4bbd1b29892590e94d730cc8cd68cc15244 Mon Sep 17 00:00:00 2001 From: Donal Burns Date: Sat, 24 May 2025 00:36:24 +0100 Subject: [PATCH 04/25] Add flag to mypy_primer Add flag to primer as asked in PR --- .github/workflows/mypy_primer.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/mypy_primer.yml b/.github/workflows/mypy_primer.yml index ee868484751e..1a9ff8ee06f7 100644 --- a/.github/workflows/mypy_primer.yml +++ b/.github/workflows/mypy_primer.yml @@ -65,7 +65,7 @@ jobs: --new $GITHUB_SHA --old base_commit \ --num-shards 5 --shard-index ${{ matrix.shard-index }} \ --debug \ - --additional-flags="--debug-serialize" \ + --additional-flags="--debug-serialize --only-allow-exhaustive-match-statements" \ --output concise \ | tee diff_${{ matrix.shard-index }}.txt ) || [ $? -eq 1 ] From a186cc8fdd95a6cc2f55d06de2116447a556466d Mon Sep 17 00:00:00 2001 From: Donal Burns Date: Sat, 24 May 2025 08:39:32 +0100 Subject: [PATCH 05/25] Revert "Add flag to mypy_primer" This reverts commit 3341b4bbd1b29892590e94d730cc8cd68cc15244. --- .github/workflows/mypy_primer.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/mypy_primer.yml b/.github/workflows/mypy_primer.yml index 1a9ff8ee06f7..ee868484751e 100644 --- a/.github/workflows/mypy_primer.yml +++ b/.github/workflows/mypy_primer.yml @@ -65,7 +65,7 @@ jobs: --new $GITHUB_SHA --old base_commit \ --num-shards 5 --shard-index ${{ matrix.shard-index }} \ --debug \ - --additional-flags="--debug-serialize --only-allow-exhaustive-match-statements" \ + --additional-flags="--debug-serialize" \ --output concise \ | tee diff_${{ matrix.shard-index }}.txt ) || [ $? -eq 1 ] From ec55c815f2e1b7ec6f96bd49015e605365b02412 Mon Sep 17 00:00:00 2001 From: Donal Burns Date: Sat, 24 May 2025 08:40:04 +0100 Subject: [PATCH 06/25] Move tests to 3.10 file so only run on >=3.10 --- test-data/unit/check-match-exhaustive.test | 136 -------------------- test-data/unit/check-python310.test | 139 +++++++++++++++++++++ 2 files changed, 139 insertions(+), 136 deletions(-) delete mode 100644 test-data/unit/check-match-exhaustive.test diff --git a/test-data/unit/check-match-exhaustive.test b/test-data/unit/check-match-exhaustive.test deleted file mode 100644 index cdb995e078de..000000000000 --- a/test-data/unit/check-match-exhaustive.test +++ /dev/null @@ -1,136 +0,0 @@ -[case testExhaustiveMatchNoFlag] -# flags: --python-version 3.12 -a: int = 5 -match a: - case 1: - pass - case _: - pass - -b: str = "hello" -match b: - case "bye": - pass - case _: - pass - -[case testNonExhaustiveMatchNoFlag] -# flags: --python-version 3.12 -a: int = 5 -match a: - case 1: - pass - -b: str = "hello" -match b: - case "bye": - pass - - -[case testExhaustiveMatchWithFlag] -# flags: --python-version 3.12 --only-allow-exhaustive-match-statements -a: int = 5 -match a: - case 1: - pass - case _: - pass - -b: str = "hello" -match b: - case "bye": - pass - case _: - pass - -[case testNonExhaustiveMatchWithFlag] -# flags: --python-version 3.12 --only-allow-exhaustive-match-statements -a: int = 5 -match a: # E: Cases within match statement do not exhaustively handle all values: "int". If not intended to handle all cases, use `case _: pass` - case 1: - pass - -b: str = "hello" -match b: # E: Cases within match statement do not exhaustively handle all values: "str". If not intended to handle all cases, use `case _: pass` - case "bye": - pass - -[case testEnumNonExhaustiveWithFlag] -# flags: --python-version 3.12 --only-allow-exhaustive-match-statements - -import enum - -class Color(enum.Enum): - RED = 1 - BLUE = 2 - GREEN = 3 - -val: Color = Color.RED - -match val: # E: Cases within match statement do not exhaustively handle all values: "Literal[Color.GREEN]". If not intended to handle all cases, use `case _: pass` - case Color.RED: - a = "red" - case Color.BLUE: - a= "blue" - -[builtins fixtures/enum.pyi] - - -[case testEnumExhaustiveWithFlag] -# flags: --python-version 3.12 --only-allow-exhaustive-match-statements - -import enum - -class Color(enum.Enum): - RED = 1 - BLUE = 2 - -val: Color = Color.RED - -match val: - case Color.RED: - a = "red" - case Color.BLUE: - a= "blue" - -[builtins fixtures/enum.pyi] - -[case testEnumMultipleMissingMatchesWithFlag] -# flags: --python-version 3.12 --only-allow-exhaustive-match-statements - -import enum - -class Color(enum.Enum): - RED = 1 - BLUE = 2 - GREEN = 3 - -val: Color = Color.RED - -match val: # E: Cases within match statement do not exhaustively handle all values: "Literal[Color.BLUE, Color.GREEN]". If not intended to handle all cases, use `case _: pass` - case Color.RED: - a = "red" - - -[builtins fixtures/enum.pyi] - -[case testEnumFallbackWithFlag] -# flags: --python-version 3.12 --only-allow-exhaustive-match-statements - -import enum - -class Color(enum.Enum): - RED = 1 - BLUE = 2 - GREEN = 3 - -val: Color = Color.RED - -match val: - case Color.RED: - a = "red" - case _: - a = "other" - - -[builtins fixtures/enum.pyi] diff --git a/test-data/unit/check-python310.test b/test-data/unit/check-python310.test index c2e2e5bddb34..055aafd45b48 100644 --- a/test-data/unit/check-python310.test +++ b/test-data/unit/check-python310.test @@ -2638,3 +2638,142 @@ def f2() -> None: return reveal_type(y) # N: Revealed type is "builtins.str" [builtins fixtures/list.pyi] + +[case testExhaustiveMatchNoFlag] + +a: int = 5 +match a: + case 1: + pass + case _: + pass + +b: str = "hello" +match b: + case "bye": + pass + case _: + pass + +[case testNonExhaustiveMatchNoFlag] + +a: int = 5 +match a: + case 1: + pass + +b: str = "hello" +match b: + case "bye": + pass + + +[case testExhaustiveMatchWithFlag] +# flags: --only-allow-exhaustive-match-statements + +a: int = 5 +match a: + case 1: + pass + case _: + pass + +b: str = "hello" +match b: + case "bye": + pass + case _: + pass + +[case testNonExhaustiveMatchWithFlag] +# flags: --only-allow-exhaustive-match-statements + +a: int = 5 +match a: # E: Cases within match statement do not exhaustively handle all values: "int". If not intended to handle all cases, use `case _: pass` + case 1: + pass + +b: str = "hello" +match b: # E: Cases within match statement do not exhaustively handle all values: "str". If not intended to handle all cases, use `case _: pass` + case "bye": + pass + +[case testNonExhaustiveMatchEnumWithFlag] +# flags: --only-allow-exhaustive-match-statements + +import enum + +class Color(enum.Enum): + RED = 1 + BLUE = 2 + GREEN = 3 + +val: Color = Color.RED + +match val: # E: Cases within match statement do not exhaustively handle all values: "Literal[Color.GREEN]". If not intended to handle all cases, use `case _: pass` + case Color.RED: + a = "red" + case Color.BLUE: + a= "blue" + +[builtins fixtures/enum.pyi] + + +[case testExhaustiveMatchEnumWithFlag] +# flags: --only-allow-exhaustive-match-statements + +import enum + +class Color(enum.Enum): + RED = 1 + BLUE = 2 + +val: Color = Color.RED + +match val: + case Color.RED: + a = "red" + case Color.BLUE: + a= "blue" + +[builtins fixtures/enum.pyi] + +[case testNonExhaustiveMatchEnumMultipleMissingMatchesWithFlag] +# flags: --only-allow-exhaustive-match-statements + +import enum + +class Color(enum.Enum): + RED = 1 + BLUE = 2 + GREEN = 3 + +val: Color = Color.RED + +match val: # E: Cases within match statement do not exhaustively handle all values: "Literal[Color.BLUE, Color.GREEN]". If not intended to handle all cases, use `case _: pass` + case Color.RED: + a = "red" + + +[builtins fixtures/enum.pyi] + +[case testExhaustiveMatchEnumFallbackWithFlag] +# flags: --only-allow-exhaustive-match-statements + +import enum + +class Color(enum.Enum): + RED = 1 + BLUE = 2 + GREEN = 3 + +val: Color = Color.RED + +match val: + case Color.RED: + a = "red" + case _: + a = "other" + + +[builtins fixtures/enum.pyi] From 8ce5b6aa265ca9a676d6e59890a8bf79b5a5b64d Mon Sep 17 00:00:00 2001 From: Donal Burns Date: Sat, 24 May 2025 08:55:03 +0100 Subject: [PATCH 07/25] Rename flag to better follow other flags --- docs/source/command_line.rst | 18 +++++++++++------- mypy/checker.py | 2 +- mypy/main.py | 2 +- mypy/options.py | 2 +- test-data/unit/check-python310.test | 12 ++++++------ 5 files changed, 20 insertions(+), 16 deletions(-) diff --git a/docs/source/command_line.rst b/docs/source/command_line.rst index 9742eb1b58fa..8cd033b8b026 100644 --- a/docs/source/command_line.rst +++ b/docs/source/command_line.rst @@ -799,7 +799,7 @@ of the above sections. x = 'a string' x.trim() # error: "str" has no attribute "trim" [attr-defined] -.. option:: --only-allow-exhaustive-match-statements +.. option:: --disallow-inexhaustive-match-statements This flag will cause mypy to report an error whenever it encounters a match statement that does not cover all possible cases. @@ -815,21 +815,25 @@ of the above sections. val: Color = Color.RED - match val: # error: Cases within match statement do not exhaustively handle all values: "Literal[Color.BLUE]". If not intended to handle all cases, use `case _: pass` + # without --disallow-inexhaustive-match-statements + match val: case Color.RED: print("red") - # without --only-allow-exhaustive-match-statements + + # Also no issues without --disallow-inexhaustive-match-statements, but this is exhaustive match val: case Color.RED: print("red") - # with --only-allow-exhaustive-match-statements + case _: + print("other") + + # with --disallow-inexhaustive-match-statements match val: # error: Cases within match statement do not exhaustively handle all values: "Literal[Color.BLUE]". If not intended to handle all cases, use `case _: pass` case Color.RED: print("red") - - # no error with --only-allow-exhaustive-match-statements since all cases are handled - match val: # error: Cases within match statement do not exhaustively handle all values: "Literal[Color.BLUE]". If not intended to handle all cases, use `case _: pass` + # no error with --disallow-inexhaustive-match-statements since all cases are handled + match val: case Color.RED: print("red") case _: diff --git a/mypy/checker.py b/mypy/checker.py index ea4fc72c25bc..b15336aa4dc8 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -5512,7 +5512,7 @@ def visit_match_stmt(self, s: MatchStmt) -> None: unmatched_types = else_map if ( - self.options.only_allow_exhaustive_match_statements is True + self.options.disallow_inexhaustive_match_statements is True and unmatched_types is not None ): for typ in set(unmatched_types.values()): diff --git a/mypy/main.py b/mypy/main.py index 8994bbf9c7f3..c161a0bf8244 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -932,7 +932,7 @@ def add_invertible_flag( group=strictness_group, ) add_invertible_flag( - "--only-allow-exhaustive-match-statements", + "--disallow-inexhaustive-match-statements", default=False, strict_flag=False, help="Raise type error for match statements that do not match exhaustively", diff --git a/mypy/options.py b/mypy/options.py index dbcbc9f450c6..57f04acfd59f 100644 --- a/mypy/options.py +++ b/mypy/options.py @@ -350,7 +350,7 @@ def __init__(self) -> None: self.test_env = False # Only allow exhaustive match statements - self.only_allow_exhaustive_match_statements = False + self.disallow_inexhaustive_match_statements = False # -- experimental options -- self.shadow_file: list[list[str]] | None = None diff --git a/test-data/unit/check-python310.test b/test-data/unit/check-python310.test index 055aafd45b48..abbd366ba07c 100644 --- a/test-data/unit/check-python310.test +++ b/test-data/unit/check-python310.test @@ -2669,7 +2669,7 @@ match b: [case testExhaustiveMatchWithFlag] -# flags: --only-allow-exhaustive-match-statements +# flags: --disallow-inexhaustive-match-statements a: int = 5 match a: @@ -2686,7 +2686,7 @@ match b: pass [case testNonExhaustiveMatchWithFlag] -# flags: --only-allow-exhaustive-match-statements +# flags: --disallow-inexhaustive-match-statements a: int = 5 match a: # E: Cases within match statement do not exhaustively handle all values: "int". If not intended to handle all cases, use `case _: pass` @@ -2699,7 +2699,7 @@ match b: # E: Cases within match statement do not exhaustively handle all values pass [case testNonExhaustiveMatchEnumWithFlag] -# flags: --only-allow-exhaustive-match-statements +# flags: --disallow-inexhaustive-match-statements import enum @@ -2720,7 +2720,7 @@ match val: # E: Cases within match statement do not exhaustively handle all valu [case testExhaustiveMatchEnumWithFlag] -# flags: --only-allow-exhaustive-match-statements +# flags: --disallow-inexhaustive-match-statements import enum @@ -2739,7 +2739,7 @@ match val: [builtins fixtures/enum.pyi] [case testNonExhaustiveMatchEnumMultipleMissingMatchesWithFlag] -# flags: --only-allow-exhaustive-match-statements +# flags: --disallow-inexhaustive-match-statements import enum @@ -2758,7 +2758,7 @@ match val: # E: Cases within match statement do not exhaustively handle all valu [builtins fixtures/enum.pyi] [case testExhaustiveMatchEnumFallbackWithFlag] -# flags: --only-allow-exhaustive-match-statements +# flags: --disallow-inexhaustive-match-statements import enum From 3581ec9e2fd81c81ae6bc134f8c3f499112e0804 Mon Sep 17 00:00:00 2001 From: Donal Burns Date: Sat, 24 May 2025 09:29:38 +0100 Subject: [PATCH 08/25] Fix doc gen error There appears to be a parsing issue in sphyx? For the lines with `match val: # error: ...` the `: # error:` part is being interpreted as a directive I think? Means the # is stripped from the python code and the docs can't build --- docs/source/command_line.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/source/command_line.rst b/docs/source/command_line.rst index 8cd033b8b026..06bf5c148cea 100644 --- a/docs/source/command_line.rst +++ b/docs/source/command_line.rst @@ -828,7 +828,8 @@ of the above sections. print("other") # with --disallow-inexhaustive-match-statements - match val: # error: Cases within match statement do not exhaustively handle all values: "Literal[Color.BLUE]". If not intended to handle all cases, use `case _: pass` + # error: Cases within match statement do not exhaustively handle all values: "Literal[Color.BLUE]". If not intended to handle all cases, use `case _: pass` + match val: case Color.RED: print("red") From 0f9ed6a85d8724fee86445199fc4e26ba174f5ac Mon Sep 17 00:00:00 2001 From: Donal Burns Date: Sat, 24 May 2025 09:50:50 +0100 Subject: [PATCH 09/25] Set default True for CI run, will revert before merge Requested in PR --- mypy/options.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mypy/options.py b/mypy/options.py index 57f04acfd59f..903e238e013f 100644 --- a/mypy/options.py +++ b/mypy/options.py @@ -350,7 +350,8 @@ def __init__(self) -> None: self.test_env = False # Only allow exhaustive match statements - self.disallow_inexhaustive_match_statements = False + # TODO: Set this to default to False once a commit in CI runs mypy_primer + self.disallow_inexhaustive_match_statements = True # -- experimental options -- self.shadow_file: list[list[str]] | None = None From 62e408955ea1cf938b011862340ffe3c46b01317 Mon Sep 17 00:00:00 2001 From: Donal Burns Date: Sat, 24 May 2025 12:09:40 +0100 Subject: [PATCH 10/25] Revert "Set default True for CI run, will revert before merge" This reverts commit 0f9ed6a85d8724fee86445199fc4e26ba174f5ac. --- mypy/options.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mypy/options.py b/mypy/options.py index 903e238e013f..57f04acfd59f 100644 --- a/mypy/options.py +++ b/mypy/options.py @@ -350,8 +350,7 @@ def __init__(self) -> None: self.test_env = False # Only allow exhaustive match statements - # TODO: Set this to default to False once a commit in CI runs mypy_primer - self.disallow_inexhaustive_match_statements = True + self.disallow_inexhaustive_match_statements = False # -- experimental options -- self.shadow_file: list[list[str]] | None = None From abf7e98c98b6a4bb9363ad0aa457157f4e0c13e0 Mon Sep 17 00:00:00 2001 From: Donal Burns Date: Sat, 24 May 2025 15:01:56 +0100 Subject: [PATCH 11/25] Add explicit error code - change [misc] -> [exhaustive-match] --- mypy/checker.py | 2 +- mypy/errorcodes.py | 5 +++++ mypy/messages.py | 4 ++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index b15336aa4dc8..4721876e0a9d 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -5516,7 +5516,7 @@ def visit_match_stmt(self, s: MatchStmt) -> None: and unmatched_types is not None ): for typ in set(unmatched_types.values()): - self.msg.match_statement_unexhaustive_match(typ, s) + self.msg.match_statement_inexhaustive_match(typ, s) # This is needed due to a quirk in frame_context. Without it types will stay narrowed # after the match. diff --git a/mypy/errorcodes.py b/mypy/errorcodes.py index 8f650aa30605..53db9fc5fff8 100644 --- a/mypy/errorcodes.py +++ b/mypy/errorcodes.py @@ -264,6 +264,11 @@ def __hash__(self) -> int: "General", default_enabled=False, ) +EXHAUSTIVE_MATCH: Final = ErrorCode( + "exhaustive-match", + "Reject match statements that are not exhaustive", + "General", +) # Syntax errors are often blocking. SYNTAX: Final[ErrorCode] = ErrorCode("syntax", "Report syntax errors", "General") diff --git a/mypy/messages.py b/mypy/messages.py index c7f6207272bf..86018adc45f1 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -2486,13 +2486,13 @@ def type_parameters_should_be_declared(self, undeclared: list[str], context: Con code=codes.VALID_TYPE, ) - def match_statement_unexhaustive_match(self, typ: Type, context: Context) -> None: + def match_statement_inexhaustive_match(self, typ: Type, context: Context) -> None: type_str = format_type(typ, self.options) msg = ( f"Cases within match statement do not exhaustively handle all values: {type_str}." " If not intended to handle all cases, use `case _: pass`" ) - self.fail(msg, context) + self.fail(msg, context, code=codes.EXHAUSTIVE_MATCH) def quote_type_string(type_string: str) -> str: From 6a525f7a7a2d8cc15991951206c1574118990024 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 24 May 2025 14:03:21 +0000 Subject: [PATCH 12/25] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/errorcodes.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/mypy/errorcodes.py b/mypy/errorcodes.py index 53db9fc5fff8..58c4d1217442 100644 --- a/mypy/errorcodes.py +++ b/mypy/errorcodes.py @@ -265,9 +265,7 @@ def __hash__(self) -> int: default_enabled=False, ) EXHAUSTIVE_MATCH: Final = ErrorCode( - "exhaustive-match", - "Reject match statements that are not exhaustive", - "General", + "exhaustive-match", "Reject match statements that are not exhaustive", "General" ) # Syntax errors are often blocking. From 7dc7c019eae19a3dd78fa1ec1564159e37138916 Mon Sep 17 00:00:00 2001 From: Donal Burns Date: Sat, 24 May 2025 15:33:08 +0100 Subject: [PATCH 13/25] Add missed docs --- docs/source/error_code_list2.rst | 47 ++++++++++++++++++++++++++++++++ docs/source/literal_types.rst | 5 ++++ 2 files changed, 52 insertions(+) diff --git a/docs/source/error_code_list2.rst b/docs/source/error_code_list2.rst index dfe2e30874f7..90adff8876cd 100644 --- a/docs/source/error_code_list2.rst +++ b/docs/source/error_code_list2.rst @@ -612,3 +612,50 @@ Example: # mypy: disallow-any-explicit from typing import Any x: Any = 1 # Error: Explicit "Any" type annotation [explicit-any] + + +.. _code-match-exhaustive: + +Check that match statements match exhaustively [match-exhaustive] +----------------------------------------------------------------------- + +If you use :option:`--disallow-inexhaustive-match-statements `, +mypy generates an error if a match statement does not match all possible cases/types. + + +Example: + +.. code-block:: python + import enum + + + class Color(enum.Enum): + RED = 1 + BLUE = 2 + + val: Color = Color.RED + + # without --disallow-inexhaustive-match-statements + match val: + case Color.RED: + print("red") + + # Also no issues without --disallow-inexhaustive-match-statements, but this is exhaustive + match val: + case Color.RED: + print("red") + case _: + print("other") + + # with --disallow-inexhaustive-match-statements + # error: Cases within match statement do not exhaustively handle all values: "Literal[Color.BLUE]". If not intended to handle all cases, use `case _: pass` + match val: + case Color.RED: + print("red") + + # no error with --disallow-inexhaustive-match-statements since all cases are handled + match val: + case Color.RED: + print("red") + case _: + print("other") diff --git a/docs/source/literal_types.rst b/docs/source/literal_types.rst index 877ab5de9087..e259ec72fca6 100644 --- a/docs/source/literal_types.rst +++ b/docs/source/literal_types.rst @@ -469,6 +469,11 @@ If we forget to handle one of the cases, mypy will generate an error: Exhaustiveness checking is also supported for match statements (Python 3.10 and later). +For match statements specifically, inexhaustive matches can be caught +without needing to use ``assert_never`` by using +:option:`--disallow-inexhaustive-match-statements `. + + Extra Enum checks ***************** From 3ab9848d94a267c90349332527f17b5f7fce5261 Mon Sep 17 00:00:00 2001 From: Donal Burns Date: Sat, 24 May 2025 15:38:08 +0100 Subject: [PATCH 14/25] fixup! Add missed docs --- docs/source/error_code_list2.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/error_code_list2.rst b/docs/source/error_code_list2.rst index 90adff8876cd..fbb0f6b510a8 100644 --- a/docs/source/error_code_list2.rst +++ b/docs/source/error_code_list2.rst @@ -626,6 +626,7 @@ mypy generates an error if a match statement does not match all possible cases/t Example: .. code-block:: python + import enum From 1a3e359e1d0290bc43e7b7a7df0dbb3651fe1f55 Mon Sep 17 00:00:00 2001 From: Donal Burns Date: Sat, 24 May 2025 16:01:15 +0100 Subject: [PATCH 15/25] fixup! fixup! Add missed docs --- docs/source/error_code_list2.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/error_code_list2.rst b/docs/source/error_code_list2.rst index fbb0f6b510a8..d6cd749957d1 100644 --- a/docs/source/error_code_list2.rst +++ b/docs/source/error_code_list2.rst @@ -614,7 +614,7 @@ Example: x: Any = 1 # Error: Explicit "Any" type annotation [explicit-any] -.. _code-match-exhaustive: +.. _code-exhaustive-match: Check that match statements match exhaustively [match-exhaustive] ----------------------------------------------------------------------- From fecd37dda7ecd5cc6caddacfbdc2d5fa29e3e85e Mon Sep 17 00:00:00 2001 From: Donal Burns Date: Sat, 24 May 2025 15:54:31 +0100 Subject: [PATCH 16/25] Change to error code only for exhaustive match --- docs/source/command_line.rst | 42 ----------------------------- docs/source/error_code_list2.rst | 10 +++---- docs/source/literal_types.rst | 2 +- mypy/checker.py | 5 +--- mypy/errorcodes.py | 5 +++- mypy/main.py | 7 ----- mypy/options.py | 3 --- test-data/unit/check-python310.test | 12 ++++----- 8 files changed, 17 insertions(+), 69 deletions(-) diff --git a/docs/source/command_line.rst b/docs/source/command_line.rst index 06bf5c148cea..ddadb8d43a02 100644 --- a/docs/source/command_line.rst +++ b/docs/source/command_line.rst @@ -799,48 +799,6 @@ of the above sections. x = 'a string' x.trim() # error: "str" has no attribute "trim" [attr-defined] -.. option:: --disallow-inexhaustive-match-statements - - This flag will cause mypy to report an error whenever it encounters a match statement - that does not cover all possible cases. - - .. code-block:: python - - import enum - - - class Color(enum.Enum): - RED = 1 - BLUE = 2 - - val: Color = Color.RED - - # without --disallow-inexhaustive-match-statements - match val: - case Color.RED: - print("red") - - # Also no issues without --disallow-inexhaustive-match-statements, but this is exhaustive - match val: - case Color.RED: - print("red") - case _: - print("other") - - # with --disallow-inexhaustive-match-statements - # error: Cases within match statement do not exhaustively handle all values: "Literal[Color.BLUE]". If not intended to handle all cases, use `case _: pass` - match val: - case Color.RED: - print("red") - - # no error with --disallow-inexhaustive-match-statements since all cases are handled - match val: - case Color.RED: - print("red") - case _: - print("other") - - .. _configuring-error-messages: diff --git a/docs/source/error_code_list2.rst b/docs/source/error_code_list2.rst index d6cd749957d1..aa340f1a7b32 100644 --- a/docs/source/error_code_list2.rst +++ b/docs/source/error_code_list2.rst @@ -619,7 +619,7 @@ Example: Check that match statements match exhaustively [match-exhaustive] ----------------------------------------------------------------------- -If you use :option:`--disallow-inexhaustive-match-statements `, +If enabled with :option:`--enable-error-code exhaustive-match `, mypy generates an error if a match statement does not match all possible cases/types. @@ -636,25 +636,25 @@ Example: val: Color = Color.RED - # without --disallow-inexhaustive-match-statements + # without --enable-error-code exhaustive-match match val: case Color.RED: print("red") - # Also no issues without --disallow-inexhaustive-match-statements, but this is exhaustive + # Also no issues without --enable-error-code exhaustive-match, but this is exhaustive match val: case Color.RED: print("red") case _: print("other") - # with --disallow-inexhaustive-match-statements + # with --enable-error-code exhaustive-match # error: Cases within match statement do not exhaustively handle all values: "Literal[Color.BLUE]". If not intended to handle all cases, use `case _: pass` match val: case Color.RED: print("red") - # no error with --disallow-inexhaustive-match-statements since all cases are handled + # no error with --enable-error-code exhaustive-match since all cases are handled match val: case Color.RED: print("red") diff --git a/docs/source/literal_types.rst b/docs/source/literal_types.rst index e259ec72fca6..ec9b03a5a82b 100644 --- a/docs/source/literal_types.rst +++ b/docs/source/literal_types.rst @@ -471,7 +471,7 @@ Exhaustiveness checking is also supported for match statements (Python 3.10 and For match statements specifically, inexhaustive matches can be caught without needing to use ``assert_never`` by using -:option:`--disallow-inexhaustive-match-statements `. +:option:`--enable-error-code exhaustive-match `. Extra Enum checks diff --git a/mypy/checker.py b/mypy/checker.py index 4721876e0a9d..07f8f9207a65 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -5511,10 +5511,7 @@ def visit_match_stmt(self, s: MatchStmt) -> None: self.push_type_map(else_map, from_assignment=False) unmatched_types = else_map - if ( - self.options.disallow_inexhaustive_match_statements is True - and unmatched_types is not None - ): + if unmatched_types is not None: for typ in set(unmatched_types.values()): self.msg.match_statement_inexhaustive_match(typ, s) diff --git a/mypy/errorcodes.py b/mypy/errorcodes.py index 58c4d1217442..c22308e4a754 100644 --- a/mypy/errorcodes.py +++ b/mypy/errorcodes.py @@ -265,7 +265,10 @@ def __hash__(self) -> int: default_enabled=False, ) EXHAUSTIVE_MATCH: Final = ErrorCode( - "exhaustive-match", "Reject match statements that are not exhaustive", "General" + "exhaustive-match", + "Reject match statements that are not exhaustive", + "General", + default_enabled=False, ) # Syntax errors are often blocking. diff --git a/mypy/main.py b/mypy/main.py index c161a0bf8244..6ebf32ded6e1 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -931,13 +931,6 @@ def add_invertible_flag( "and makes arguments prepended via Concatenate positional-only", group=strictness_group, ) - add_invertible_flag( - "--disallow-inexhaustive-match-statements", - default=False, - strict_flag=False, - help="Raise type error for match statements that do not match exhaustively", - group=strictness_group, - ) strict_help = "Strict mode; enables the following flags: {}".format( ", ".join(strict_flag_names) diff --git a/mypy/options.py b/mypy/options.py index 57f04acfd59f..52afd27211ed 100644 --- a/mypy/options.py +++ b/mypy/options.py @@ -349,9 +349,6 @@ def __init__(self) -> None: # Use this sparingly to avoid tests diverging from non-test behavior. self.test_env = False - # Only allow exhaustive match statements - self.disallow_inexhaustive_match_statements = False - # -- experimental options -- self.shadow_file: list[list[str]] | None = None self.show_column_numbers: bool = False diff --git a/test-data/unit/check-python310.test b/test-data/unit/check-python310.test index abbd366ba07c..e2dd7ddf58c5 100644 --- a/test-data/unit/check-python310.test +++ b/test-data/unit/check-python310.test @@ -2669,7 +2669,7 @@ match b: [case testExhaustiveMatchWithFlag] -# flags: --disallow-inexhaustive-match-statements +# flags: --enable-error-code exhaustive-match a: int = 5 match a: @@ -2686,7 +2686,7 @@ match b: pass [case testNonExhaustiveMatchWithFlag] -# flags: --disallow-inexhaustive-match-statements +# flags: --enable-error-code exhaustive-match a: int = 5 match a: # E: Cases within match statement do not exhaustively handle all values: "int". If not intended to handle all cases, use `case _: pass` @@ -2699,7 +2699,7 @@ match b: # E: Cases within match statement do not exhaustively handle all values pass [case testNonExhaustiveMatchEnumWithFlag] -# flags: --disallow-inexhaustive-match-statements +# flags: --enable-error-code exhaustive-match import enum @@ -2720,7 +2720,7 @@ match val: # E: Cases within match statement do not exhaustively handle all valu [case testExhaustiveMatchEnumWithFlag] -# flags: --disallow-inexhaustive-match-statements +# flags: --enable-error-code exhaustive-match import enum @@ -2739,7 +2739,7 @@ match val: [builtins fixtures/enum.pyi] [case testNonExhaustiveMatchEnumMultipleMissingMatchesWithFlag] -# flags: --disallow-inexhaustive-match-statements +# flags: --enable-error-code exhaustive-match import enum @@ -2758,7 +2758,7 @@ match val: # E: Cases within match statement do not exhaustively handle all valu [builtins fixtures/enum.pyi] [case testExhaustiveMatchEnumFallbackWithFlag] -# flags: --disallow-inexhaustive-match-statements +# flags: --enable-error-code exhaustive-match import enum From 525924f0e29939d401b90561d03d1ea10837bf72 Mon Sep 17 00:00:00 2001 From: Donal Burns Date: Mon, 26 May 2025 09:44:59 +0100 Subject: [PATCH 17/25] Merge doc literal paragraph --- docs/source/literal_types.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/source/literal_types.rst b/docs/source/literal_types.rst index ec9b03a5a82b..e449589ddb4d 100644 --- a/docs/source/literal_types.rst +++ b/docs/source/literal_types.rst @@ -468,7 +468,6 @@ If we forget to handle one of the cases, mypy will generate an error: assert_never(direction) # E: Argument 1 to "assert_never" has incompatible type "Direction"; expected "NoReturn" Exhaustiveness checking is also supported for match statements (Python 3.10 and later). - For match statements specifically, inexhaustive matches can be caught without needing to use ``assert_never`` by using :option:`--enable-error-code exhaustive-match `. From f798b43f2830a3fd5dfb204e4e788de55b1af562 Mon Sep 17 00:00:00 2001 From: Donal Burns Date: Tue, 27 May 2025 09:15:56 +0100 Subject: [PATCH 18/25] Empty commit to trigger CI From 9b200ac0b9cf518a85592500b9316061d6b9e03c Mon Sep 17 00:00:00 2001 From: Donal Burns <56016914+Don-Burns@users.noreply.github.com> Date: Wed, 28 May 2025 09:11:19 +0100 Subject: [PATCH 19/25] Apply suggestions to remove whitespace from tests Co-authored-by: Stanislav Terliakov <50529348+sterliakov@users.noreply.github.com> --- test-data/unit/check-python310.test | 4 ---- 1 file changed, 4 deletions(-) diff --git a/test-data/unit/check-python310.test b/test-data/unit/check-python310.test index e2dd7ddf58c5..a38b46342fe6 100644 --- a/test-data/unit/check-python310.test +++ b/test-data/unit/check-python310.test @@ -2753,8 +2753,6 @@ val: Color = Color.RED match val: # E: Cases within match statement do not exhaustively handle all values: "Literal[Color.BLUE, Color.GREEN]". If not intended to handle all cases, use `case _: pass` case Color.RED: a = "red" - - [builtins fixtures/enum.pyi] [case testExhaustiveMatchEnumFallbackWithFlag] @@ -2774,6 +2772,4 @@ match val: a = "red" case _: a = "other" - - [builtins fixtures/enum.pyi] From 681a327d10b755280d124ea077c5b17f2d8e0d4e Mon Sep 17 00:00:00 2001 From: Donal Burns Date: Wed, 28 May 2025 09:24:08 +0100 Subject: [PATCH 20/25] fixup! Apply suggestions to remove whitespace from tests --- test-data/unit/check-python310.test | 3 --- 1 file changed, 3 deletions(-) diff --git a/test-data/unit/check-python310.test b/test-data/unit/check-python310.test index a38b46342fe6..0c75631a7c43 100644 --- a/test-data/unit/check-python310.test +++ b/test-data/unit/check-python310.test @@ -2715,10 +2715,8 @@ match val: # E: Cases within match statement do not exhaustively handle all valu a = "red" case Color.BLUE: a= "blue" - [builtins fixtures/enum.pyi] - [case testExhaustiveMatchEnumWithFlag] # flags: --enable-error-code exhaustive-match @@ -2735,7 +2733,6 @@ match val: a = "red" case Color.BLUE: a= "blue" - [builtins fixtures/enum.pyi] [case testNonExhaustiveMatchEnumMultipleMissingMatchesWithFlag] From d219871ffc78e0b91d0d888efe53035cd2664df3 Mon Sep 17 00:00:00 2001 From: Donal Burns Date: Wed, 28 May 2025 09:24:31 +0100 Subject: [PATCH 21/25] Add test for more complex narrowing use case --- test-data/unit/check-python310.test | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/test-data/unit/check-python310.test b/test-data/unit/check-python310.test index 0c75631a7c43..a863900fa9bd 100644 --- a/test-data/unit/check-python310.test +++ b/test-data/unit/check-python310.test @@ -2770,3 +2770,24 @@ match val: case _: a = "other" [builtins fixtures/enum.pyi] + +# Fork of testMatchNarrowingUnionTypedDictViaIndex to check behaviour with exhaustive match flag +[case testExhaustiveMatchNarrowingUnionTypedDictViaIndex] +# flags: --enable-error-code exhaustive-match + +from typing import Literal, TypedDict + +class A(TypedDict): + tag: Literal["a"] + name: str + +class B(TypedDict): + tag: Literal["b"] + num: int + +d: A | B +match d["tag"]: # E: Cases within match statement do not exhaustively handle all values: "B". If not intended to handle all cases, use `case _: pass` # E: Cases within match statement do not exhaustively handle all values: "Literal['b']". If not intended to handle all cases, use `case _: pass` + case "a": + reveal_type(d) # N: Revealed type is "TypedDict('__main__.A', {'tag': Literal['a'], 'name': builtins.str})" + reveal_type(d["name"]) # N: Revealed type is "builtins.str" +[typing fixtures/typing-typeddict.pyi] From 914bff49e98c348bbac7d089437b7f628e4a2eda Mon Sep 17 00:00:00 2001 From: Donal Burns Date: Wed, 28 May 2025 11:30:16 +0100 Subject: [PATCH 22/25] fixup! Add test for more complex narrowing use case Fix for more deterministic behaviour --- mypy/checker.py | 2 +- test-data/unit/check-python310.test | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 07f8f9207a65..3a58e8c9835c 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -5512,7 +5512,7 @@ def visit_match_stmt(self, s: MatchStmt) -> None: unmatched_types = else_map if unmatched_types is not None: - for typ in set(unmatched_types.values()): + for typ in list(unmatched_types.values()): self.msg.match_statement_inexhaustive_match(typ, s) # This is needed due to a quirk in frame_context. Without it types will stay narrowed diff --git a/test-data/unit/check-python310.test b/test-data/unit/check-python310.test index a863900fa9bd..4de963bff585 100644 --- a/test-data/unit/check-python310.test +++ b/test-data/unit/check-python310.test @@ -2786,7 +2786,7 @@ class B(TypedDict): num: int d: A | B -match d["tag"]: # E: Cases within match statement do not exhaustively handle all values: "B". If not intended to handle all cases, use `case _: pass` # E: Cases within match statement do not exhaustively handle all values: "Literal['b']". If not intended to handle all cases, use `case _: pass` +match d["tag"]: # E: Cases within match statement do not exhaustively handle all values: "Literal['b']". If not intended to handle all cases, use `case _: pass` # E: Cases within match statement do not exhaustively handle all values: "B". If not intended to handle all cases, use `case _: pass` case "a": reveal_type(d) # N: Revealed type is "TypedDict('__main__.A', {'tag': Literal['a'], 'name': builtins.str})" reveal_type(d["name"]) # N: Revealed type is "builtins.str" From da2752a041ec91a495df39e389171386d5ed8858 Mon Sep 17 00:00:00 2001 From: Donal Burns Date: Tue, 3 Jun 2025 21:44:01 +0100 Subject: [PATCH 23/25] Change test for readability --- test-data/unit/check-python310.test | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test-data/unit/check-python310.test b/test-data/unit/check-python310.test index 185aef166801..0ca3a400889a 100644 --- a/test-data/unit/check-python310.test +++ b/test-data/unit/check-python310.test @@ -2786,7 +2786,8 @@ class B(TypedDict): num: int d: A | B -match d["tag"]: # E: Cases within match statement do not exhaustively handle all values: "Literal['b']". If not intended to handle all cases, use `case _: pass` # E: Cases within match statement do not exhaustively handle all values: "B". If not intended to handle all cases, use `case _: pass` +match d["tag"]: # E: Cases within match statement do not exhaustively handle all values: "Literal['b']". If not intended to handle all cases, use `case _: pass` \ + # E: Cases within match statement do not exhaustively handle all values: "B". If not intended to handle all cases, use `case _: pass` case "a": reveal_type(d) # N: Revealed type is "TypedDict('__main__.A', {'tag': Literal['a'], 'name': builtins.str})" reveal_type(d["name"]) # N: Revealed type is "builtins.str" From 3eaa5ad92b1b21a5932b6ac4cf971214294e90cd Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Tue, 3 Jun 2025 14:39:45 -0700 Subject: [PATCH 24/25] error message change, use notes --- mypy/messages.py | 10 ++++++---- test-data/unit/check-python310.test | 18 +++++++++++------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/mypy/messages.py b/mypy/messages.py index 86018adc45f1..d233621c1263 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -2488,11 +2488,13 @@ def type_parameters_should_be_declared(self, undeclared: list[str], context: Con def match_statement_inexhaustive_match(self, typ: Type, context: Context) -> None: type_str = format_type(typ, self.options) - msg = ( - f"Cases within match statement do not exhaustively handle all values: {type_str}." - " If not intended to handle all cases, use `case _: pass`" - ) + msg = f"Match statement has unhandled case for values of type {type_str}" self.fail(msg, context, code=codes.EXHAUSTIVE_MATCH) + self.note( + "If match statement is intended to be non-exhaustive, add `case _: pass`", + context, + code=codes.EXHAUSTIVE_MATCH, + ) def quote_type_string(type_string: str) -> str: diff --git a/test-data/unit/check-python310.test b/test-data/unit/check-python310.test index 0ca3a400889a..6a0906a0fefe 100644 --- a/test-data/unit/check-python310.test +++ b/test-data/unit/check-python310.test @@ -2689,15 +2689,16 @@ match b: # flags: --enable-error-code exhaustive-match a: int = 5 -match a: # E: Cases within match statement do not exhaustively handle all values: "int". If not intended to handle all cases, use `case _: pass` +match a: # E: Match statement has unhandled case for values of type "int" \ + # N: If match statement is intended to be non-exhaustive, add `case _: pass` case 1: pass b: str = "hello" -match b: # E: Cases within match statement do not exhaustively handle all values: "str". If not intended to handle all cases, use `case _: pass` +match b: # E: Match statement has unhandled case for values of type "str" \ + # N: If match statement is intended to be non-exhaustive, add `case _: pass` case "bye": pass - [case testNonExhaustiveMatchEnumWithFlag] # flags: --enable-error-code exhaustive-match @@ -2710,7 +2711,8 @@ class Color(enum.Enum): val: Color = Color.RED -match val: # E: Cases within match statement do not exhaustively handle all values: "Literal[Color.GREEN]". If not intended to handle all cases, use `case _: pass` +match val: # E: Match statement has unhandled case for values of type "Literal[Color.GREEN]" \ + # N: If match statement is intended to be non-exhaustive, add `case _: pass` case Color.RED: a = "red" case Color.BLUE: @@ -2747,7 +2749,8 @@ class Color(enum.Enum): val: Color = Color.RED -match val: # E: Cases within match statement do not exhaustively handle all values: "Literal[Color.BLUE, Color.GREEN]". If not intended to handle all cases, use `case _: pass` +match val: # E: Match statement has unhandled case for values of type "Literal[Color.BLUE, Color.GREEN]" \ + # N: If match statement is intended to be non-exhaustive, add `case _: pass` case Color.RED: a = "red" [builtins fixtures/enum.pyi] @@ -2786,8 +2789,9 @@ class B(TypedDict): num: int d: A | B -match d["tag"]: # E: Cases within match statement do not exhaustively handle all values: "Literal['b']". If not intended to handle all cases, use `case _: pass` \ - # E: Cases within match statement do not exhaustively handle all values: "B". If not intended to handle all cases, use `case _: pass` +match d["tag"]: # E: Match statement has unhandled case for values of type "Literal['b']" \ + # N: If match statement is intended to be non-exhaustive, add `case _: pass` \ + # E: Match statement has unhandled case for values of type "B" case "a": reveal_type(d) # N: Revealed type is "TypedDict('__main__.A', {'tag': Literal['a'], 'name': builtins.str})" reveal_type(d["name"]) # N: Revealed type is "builtins.str" From 23afed22f41ee06ee3ff945b3799344abf162c3c Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Tue, 3 Jun 2025 19:51:38 -0700 Subject: [PATCH 25/25] tweak docs --- docs/source/error_code_list2.rst | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/docs/source/error_code_list2.rst b/docs/source/error_code_list2.rst index aa340f1a7b32..141aa4490c0b 100644 --- a/docs/source/error_code_list2.rst +++ b/docs/source/error_code_list2.rst @@ -636,25 +636,18 @@ Example: val: Color = Color.RED - # without --enable-error-code exhaustive-match + # OK without --enable-error-code exhaustive-match match val: case Color.RED: print("red") - # Also no issues without --enable-error-code exhaustive-match, but this is exhaustive - match val: - case Color.RED: - print("red") - case _: - print("other") - - # with --enable-error-code exhaustive-match - # error: Cases within match statement do not exhaustively handle all values: "Literal[Color.BLUE]". If not intended to handle all cases, use `case _: pass` + # With --enable-error-code exhaustive-match + # Error: Match statement has unhandled case for values of type "Literal[Color.BLUE]" match val: case Color.RED: print("red") - # no error with --enable-error-code exhaustive-match since all cases are handled + # OK with or without --enable-error-code exhaustive-match, since all cases are handled match val: case Color.RED: print("red")