Skip to content

Commit f15d677

Browse files
authored
Make warn-unreachable understand exception-swallowing contextmanagers (#7317)
This pull request fixes #7214: it makes mypy treat any context managers where the `__exit__` returns `bool` or `Literal[True]` as ones that can potentially swallow exceptions. Context managers that return `Optional[bool]`, None, or `Literal[False]` continue to be treated as non-exception-swallowing ones. This distinction helps the `--warn-unreachable` flag do the right thing in this example program: ```python from contextlib import suppress def should_warn() -> str: with contextlib.suppress(IndexError): return ["a", "b", "c"][0] def should_not_warn() -> str: with open("foo.txt") as f: return "blah" ``` This behavior is partially disabled when strict-optional is disabled: we can't necessarily distinguish between `Optional[bool]` vs `bool` in that mode, so we conservatively treat the latter in the same way we treat the former.
1 parent eb5f4a4 commit f15d677

File tree

5 files changed

+414
-10
lines changed

5 files changed

+414
-10
lines changed

mypy/checker.py

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
UnionType, TypeVarId, TypeVarType, PartialType, DeletedType, UninhabitedType, TypeVarDef,
3737
true_only, false_only, function_type, is_named_instance, union_items, TypeQuery, LiteralType,
3838
is_optional, remove_optional, TypeTranslator, StarType, get_proper_type, ProperType,
39-
get_proper_types
39+
get_proper_types, is_literal_type
4040
)
4141
from mypy.sametypes import is_same_type
4242
from mypy.messages import (
@@ -3341,12 +3341,40 @@ def check_incompatible_property_override(self, e: Decorator) -> None:
33413341
self.fail(message_registry.READ_ONLY_PROPERTY_OVERRIDES_READ_WRITE, e)
33423342

33433343
def visit_with_stmt(self, s: WithStmt) -> None:
3344+
exceptions_maybe_suppressed = False
33443345
for expr, target in zip(s.expr, s.target):
33453346
if s.is_async:
3346-
self.check_async_with_item(expr, target, s.unanalyzed_type is None)
3347+
exit_ret_type = self.check_async_with_item(expr, target, s.unanalyzed_type is None)
33473348
else:
3348-
self.check_with_item(expr, target, s.unanalyzed_type is None)
3349-
self.accept(s.body)
3349+
exit_ret_type = self.check_with_item(expr, target, s.unanalyzed_type is None)
3350+
3351+
# Based on the return type, determine if this context manager 'swallows'
3352+
# exceptions or not. We determine this using a heuristic based on the
3353+
# return type of the __exit__ method -- see the discussion in
3354+
# https://github.com/python/mypy/issues/7214 and the section about context managers
3355+
# in https://github.com/python/typeshed/blob/master/CONTRIBUTING.md#conventions
3356+
# for more details.
3357+
3358+
exit_ret_type = get_proper_type(exit_ret_type)
3359+
if is_literal_type(exit_ret_type, "builtins.bool", False):
3360+
continue
3361+
3362+
if (is_literal_type(exit_ret_type, "builtins.bool", True)
3363+
or (isinstance(exit_ret_type, Instance)
3364+
and exit_ret_type.type.fullname() == 'builtins.bool'
3365+
and state.strict_optional)):
3366+
# Note: if strict-optional is disabled, this bool instance
3367+
# could actually be an Optional[bool].
3368+
exceptions_maybe_suppressed = True
3369+
3370+
if exceptions_maybe_suppressed:
3371+
# Treat this 'with' block in the same way we'd treat a 'try: BODY; except: pass'
3372+
# block. This means control flow can continue after the 'with' even if the 'with'
3373+
# block immediately returns.
3374+
with self.binder.frame_context(can_skip=True, try_frame=True):
3375+
self.accept(s.body)
3376+
else:
3377+
self.accept(s.body)
33503378

33513379
def check_untyped_after_decorator(self, typ: Type, func: FuncDef) -> None:
33523380
if not self.options.disallow_any_decorated or self.is_stub:
@@ -3356,7 +3384,7 @@ def check_untyped_after_decorator(self, typ: Type, func: FuncDef) -> None:
33563384
self.msg.untyped_decorated_function(typ, func)
33573385

33583386
def check_async_with_item(self, expr: Expression, target: Optional[Expression],
3359-
infer_lvalue_type: bool) -> None:
3387+
infer_lvalue_type: bool) -> Type:
33603388
echk = self.expr_checker
33613389
ctx = echk.accept(expr)
33623390
obj = echk.check_method_call_by_name('__aenter__', ctx, [], [], expr)[0]
@@ -3365,20 +3393,22 @@ def check_async_with_item(self, expr: Expression, target: Optional[Expression],
33653393
if target:
33663394
self.check_assignment(target, self.temp_node(obj, expr), infer_lvalue_type)
33673395
arg = self.temp_node(AnyType(TypeOfAny.special_form), expr)
3368-
res = echk.check_method_call_by_name(
3369-
'__aexit__', ctx, [arg] * 3, [nodes.ARG_POS] * 3, expr)[0]
3370-
echk.check_awaitable_expr(
3396+
res, _ = echk.check_method_call_by_name(
3397+
'__aexit__', ctx, [arg] * 3, [nodes.ARG_POS] * 3, expr)
3398+
return echk.check_awaitable_expr(
33713399
res, expr, message_registry.INCOMPATIBLE_TYPES_IN_ASYNC_WITH_AEXIT)
33723400

33733401
def check_with_item(self, expr: Expression, target: Optional[Expression],
3374-
infer_lvalue_type: bool) -> None:
3402+
infer_lvalue_type: bool) -> Type:
33753403
echk = self.expr_checker
33763404
ctx = echk.accept(expr)
33773405
obj = echk.check_method_call_by_name('__enter__', ctx, [], [], expr)[0]
33783406
if target:
33793407
self.check_assignment(target, self.temp_node(obj, expr), infer_lvalue_type)
33803408
arg = self.temp_node(AnyType(TypeOfAny.special_form), expr)
3381-
echk.check_method_call_by_name('__exit__', ctx, [arg] * 3, [nodes.ARG_POS] * 3, expr)
3409+
res, _ = echk.check_method_call_by_name(
3410+
'__exit__', ctx, [arg] * 3, [nodes.ARG_POS] * 3, expr)
3411+
return res
33823412

33833413
def visit_print_stmt(self, s: PrintStmt) -> None:
33843414
for arg in s.args:

mypy/types.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2406,6 +2406,17 @@ def remove_optional(typ: Type) -> ProperType:
24062406
return typ
24072407

24082408

2409+
def is_literal_type(typ: ProperType, fallback_fullname: str, value: LiteralValue) -> bool:
2410+
"""Check if this type is a LiteralType with the given fallback type and value."""
2411+
if isinstance(typ, Instance) and typ.last_known_value:
2412+
typ = typ.last_known_value
2413+
if not isinstance(typ, LiteralType):
2414+
return False
2415+
if typ.fallback.type.fullname() != fallback_fullname:
2416+
return False
2417+
return typ.value == value
2418+
2419+
24092420
@overload
24102421
def get_proper_type(typ: None) -> None: ...
24112422
@overload # noqa

0 commit comments

Comments
 (0)