Skip to content

Commit 1da9046

Browse files
Merge branch 'master' into patch-3
2 parents a15838c + 9fd55aa commit 1da9046

21 files changed

+347
-10
lines changed

.github/workflows/mypy_primer.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,6 @@ jobs:
6767
--debug \
6868
--additional-flags="--debug-serialize" \
6969
--output concise \
70-
--show-speed-regression \
7170
| tee diff_${{ matrix.shard-index }}.txt
7271
) || [ $? -eq 1 ]
7372
- if: ${{ matrix.shard-index == 0 }}

docs/source/command_line.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -845,6 +845,7 @@ of the above sections.
845845
x = 'a string'
846846
x.trim() # error: "str" has no attribute "trim" [attr-defined]
847847
848+
848849
.. _configuring-error-messages:
849850

850851
Configuring error messages

docs/source/error_code_list2.rst

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -612,3 +612,44 @@ Example:
612612
# mypy: disallow-any-explicit
613613
from typing import Any
614614
x: Any = 1 # Error: Explicit "Any" type annotation [explicit-any]
615+
616+
617+
.. _code-exhaustive-match:
618+
619+
Check that match statements match exhaustively [match-exhaustive]
620+
-----------------------------------------------------------------------
621+
622+
If enabled with :option:`--enable-error-code exhaustive-match <mypy --enable-error-code>`,
623+
mypy generates an error if a match statement does not match all possible cases/types.
624+
625+
626+
Example:
627+
628+
.. code-block:: python
629+
630+
import enum
631+
632+
633+
class Color(enum.Enum):
634+
RED = 1
635+
BLUE = 2
636+
637+
val: Color = Color.RED
638+
639+
# OK without --enable-error-code exhaustive-match
640+
match val:
641+
case Color.RED:
642+
print("red")
643+
644+
# With --enable-error-code exhaustive-match
645+
# Error: Match statement has unhandled case for values of type "Literal[Color.BLUE]"
646+
match val:
647+
case Color.RED:
648+
print("red")
649+
650+
# OK with or without --enable-error-code exhaustive-match, since all cases are handled
651+
match val:
652+
case Color.RED:
653+
print("red")
654+
case _:
655+
print("other")

docs/source/literal_types.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,10 @@ If we forget to handle one of the cases, mypy will generate an error:
468468
assert_never(direction) # E: Argument 1 to "assert_never" has incompatible type "Direction"; expected "NoReturn"
469469
470470
Exhaustiveness checking is also supported for match statements (Python 3.10 and later).
471+
For match statements specifically, inexhaustive matches can be caught
472+
without needing to use ``assert_never`` by using
473+
:option:`--enable-error-code exhaustive-match <mypy --enable-error-code>`.
474+
471475

472476
Extra Enum checks
473477
*****************

mypy/checker.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5455,6 +5455,7 @@ def visit_match_stmt(self, s: MatchStmt) -> None:
54555455
inferred_types = self.infer_variable_types_from_type_maps(type_maps)
54565456

54575457
# The second pass narrows down the types and type checks bodies.
5458+
unmatched_types: TypeMap = None
54585459
for p, g, b in zip(s.patterns, s.guards, s.bodies):
54595460
current_subject_type = self.expr_checker.narrow_type_from_binder(
54605461
named_subject, subject_type
@@ -5511,6 +5512,11 @@ def visit_match_stmt(self, s: MatchStmt) -> None:
55115512
else:
55125513
self.accept(b)
55135514
self.push_type_map(else_map, from_assignment=False)
5515+
unmatched_types = else_map
5516+
5517+
if unmatched_types is not None:
5518+
for typ in list(unmatched_types.values()):
5519+
self.msg.match_statement_inexhaustive_match(typ, s)
55145520

55155521
# This is needed due to a quirk in frame_context. Without it types will stay narrowed
55165522
# after the match.

mypy/config_parser.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,14 @@
2828
_INI_PARSER_CALLABLE: _TypeAlias = Callable[[Any], _CONFIG_VALUE_TYPES]
2929

3030

31+
class VersionTypeError(argparse.ArgumentTypeError):
32+
"""Provide a fallback value if the Python version is unsupported."""
33+
34+
def __init__(self, *args: Any, fallback: tuple[int, int]) -> None:
35+
self.fallback = fallback
36+
super().__init__(*args)
37+
38+
3139
def parse_version(v: str | float) -> tuple[int, int]:
3240
m = re.match(r"\A(\d)\.(\d+)\Z", str(v))
3341
if not m:
@@ -44,7 +52,7 @@ def parse_version(v: str | float) -> tuple[int, int]:
4452
if isinstance(v, float):
4553
msg += ". You may need to put quotes around your Python version"
4654

47-
raise argparse.ArgumentTypeError(msg)
55+
raise VersionTypeError(msg, fallback=defaults.PYTHON3_VERSION_MIN)
4856
else:
4957
raise argparse.ArgumentTypeError(
5058
f"Python major version '{major}' out of range (must be 3)"
@@ -548,6 +556,9 @@ def parse_section(
548556
continue
549557
try:
550558
v = ct(section.get(key))
559+
except VersionTypeError as err_version:
560+
print(f"{prefix}{key}: {err_version}", file=stderr)
561+
v = err_version.fallback
551562
except argparse.ArgumentTypeError as err:
552563
print(f"{prefix}{key}: {err}", file=stderr)
553564
continue

mypy/errorcodes.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,12 @@ def __hash__(self) -> int:
264264
"General",
265265
default_enabled=False,
266266
)
267+
EXHAUSTIVE_MATCH: Final = ErrorCode(
268+
"exhaustive-match",
269+
"Reject match statements that are not exhaustive",
270+
"General",
271+
default_enabled=False,
272+
)
267273

268274
# Syntax errors are often blocking.
269275
SYNTAX: Final[ErrorCode] = ErrorCode("syntax", "Report syntax errors", "General")

mypy/messages.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2491,6 +2491,16 @@ def type_parameters_should_be_declared(self, undeclared: list[str], context: Con
24912491
code=codes.VALID_TYPE,
24922492
)
24932493

2494+
def match_statement_inexhaustive_match(self, typ: Type, context: Context) -> None:
2495+
type_str = format_type(typ, self.options)
2496+
msg = f"Match statement has unhandled case for values of type {type_str}"
2497+
self.fail(msg, context, code=codes.EXHAUSTIVE_MATCH)
2498+
self.note(
2499+
"If match statement is intended to be non-exhaustive, add `case _: pass`",
2500+
context,
2501+
code=codes.EXHAUSTIVE_MATCH,
2502+
)
2503+
24942504

24952505
def quote_type_string(type_string: str) -> str:
24962506
"""Quotes a type representation for use in messages."""

mypy/typeops.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1190,6 +1190,10 @@ def custom_special_method(typ: Type, name: str, check_all: bool = False) -> bool
11901190
if isinstance(typ, FunctionLike) and typ.is_type_obj():
11911191
# Look up __method__ on the metaclass for class objects.
11921192
return custom_special_method(typ.fallback, name, check_all)
1193+
if isinstance(typ, TypeType) and isinstance(typ.item, Instance):
1194+
if typ.item.type.metaclass_type:
1195+
# Look up __method__ on the metaclass for class objects.
1196+
return custom_special_method(typ.item.type.metaclass_type, name, check_all)
11931197
if isinstance(typ, AnyType):
11941198
# Avoid false positives in uncertain cases.
11951199
return True

mypyc/irbuild/builder.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1300,12 +1300,19 @@ def node_type(self, node: Expression) -> RType:
13001300
return self.type_to_rtype(mypy_type)
13011301

13021302
def add_var_to_env_class(
1303-
self, var: SymbolNode, rtype: RType, base: FuncInfo | ImplicitClass, reassign: bool = False
1303+
self,
1304+
var: SymbolNode,
1305+
rtype: RType,
1306+
base: FuncInfo | ImplicitClass,
1307+
reassign: bool = False,
1308+
always_defined: bool = False,
13041309
) -> AssignmentTarget:
13051310
# First, define the variable name as an attribute of the environment class, and then
13061311
# construct a target for that attribute.
13071312
name = remangle_redefinition_name(var.name)
13081313
self.fn_info.env_class.attributes[name] = rtype
1314+
if always_defined:
1315+
self.fn_info.env_class.attrs_with_defaults.add(name)
13091316
attr_target = AssignmentTargetAttr(base.curr_env_reg, name)
13101317

13111318
if reassign:

0 commit comments

Comments
 (0)