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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy