Skip to content

Commit 83748a4

Browse files
Michael0x2agvanrossum
authored andcommitted
Generalize the 'open' plugin for 'pathlib.Path.open' (#7643)
This pull request adds a plugin to infer a more precise return type for the `pathlib.Path.open(...)` method. This method is actually nearly identical to the builtin `open(...)` method, with the only difference being that `pathlib.Path.open(...)` method doesn't have the `file` parameter. So, I refactored the logic in both plugins into a shared helper method.
1 parent c6efeab commit 83748a4

File tree

2 files changed

+74
-13
lines changed

2 files changed

+74
-13
lines changed

mypy/plugins/default.py

Lines changed: 48 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
from typing import Callable, Optional, List
33

44
from mypy import message_registry
5-
from mypy.nodes import StrExpr, IntExpr, DictExpr, UnaryExpr
5+
from mypy.nodes import Expression, StrExpr, IntExpr, DictExpr, UnaryExpr
66
from mypy.plugin import (
7-
Plugin, FunctionContext, MethodContext, MethodSigContext, AttributeContext, ClassDefContext
7+
Plugin, FunctionContext, MethodContext, MethodSigContext, AttributeContext, ClassDefContext,
8+
CheckerPluginInterface,
89
)
910
from mypy.plugins.common import try_getting_str_literals
1011
from mypy.types import (
@@ -66,6 +67,8 @@ def get_method_hook(self, fullname: str
6667
return ctypes.array_getitem_callback
6768
elif fullname == 'ctypes.Array.__iter__':
6869
return ctypes.array_iter_callback
70+
elif fullname == 'pathlib.Path.open':
71+
return path_open_callback
6972
return None
7073

7174
def get_attribute_hook(self, fullname: str
@@ -101,23 +104,55 @@ def get_class_decorator_hook(self, fullname: str
101104

102105

103106
def open_callback(ctx: FunctionContext) -> Type:
104-
"""Infer a better return type for 'open'.
105-
106-
Infer TextIO or BinaryIO as the return value if the mode argument is not
107-
given or is a literal.
107+
"""Infer a better return type for 'open'."""
108+
return _analyze_open_signature(
109+
arg_types=ctx.arg_types,
110+
args=ctx.args,
111+
mode_arg_index=1,
112+
default_return_type=ctx.default_return_type,
113+
api=ctx.api,
114+
)
115+
116+
117+
def path_open_callback(ctx: MethodContext) -> Type:
118+
"""Infer a better return type for 'pathlib.Path.open'."""
119+
return _analyze_open_signature(
120+
arg_types=ctx.arg_types,
121+
args=ctx.args,
122+
mode_arg_index=0,
123+
default_return_type=ctx.default_return_type,
124+
api=ctx.api,
125+
)
126+
127+
128+
def _analyze_open_signature(arg_types: List[List[Type]],
129+
args: List[List[Expression]],
130+
mode_arg_index: int,
131+
default_return_type: Type,
132+
api: CheckerPluginInterface,
133+
) -> Type:
134+
"""A helper for analyzing any function that has approximately
135+
the same signature as the builtin 'open(...)' function.
136+
137+
Currently, the only thing the caller can customize is the index
138+
of the 'mode' argument. If the mode argument is omitted or is a
139+
string literal, we refine the return type to either 'TextIO' or
140+
'BinaryIO' as appropriate.
108141
"""
109142
mode = None
110-
if not ctx.arg_types or len(ctx.arg_types[1]) != 1:
143+
if not arg_types or len(arg_types[mode_arg_index]) != 1:
111144
mode = 'r'
112-
elif isinstance(ctx.args[1][0], StrExpr):
113-
mode = ctx.args[1][0].value
145+
else:
146+
mode_expr = args[mode_arg_index][0]
147+
if isinstance(mode_expr, StrExpr):
148+
mode = mode_expr.value
114149
if mode is not None:
115-
assert isinstance(ctx.default_return_type, Instance) # type: ignore
150+
assert isinstance(default_return_type, Instance) # type: ignore
116151
if 'b' in mode:
117-
return ctx.api.named_generic_type('typing.BinaryIO', [])
152+
return api.named_generic_type('typing.BinaryIO', [])
118153
else:
119-
return ctx.api.named_generic_type('typing.TextIO', [])
120-
return ctx.default_return_type
154+
return api.named_generic_type('typing.TextIO', [])
155+
return default_return_type
121156

122157

123158
def contextmanager_callback(ctx: FunctionContext) -> Type:

test-data/unit/pythoneval.test

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,32 @@ _testOpenReturnTypeInferenceSpecialCases.py:2: note: Revealed type is 'typing.Bi
299299
_testOpenReturnTypeInferenceSpecialCases.py:3: note: Revealed type is 'typing.BinaryIO'
300300
_testOpenReturnTypeInferenceSpecialCases.py:5: note: Revealed type is 'typing.IO[Any]'
301301

302+
[case testPathOpenReturnTypeInference]
303+
from pathlib import Path
304+
p = Path("x")
305+
reveal_type(p.open())
306+
reveal_type(p.open('r'))
307+
reveal_type(p.open('rb'))
308+
mode = 'rb'
309+
reveal_type(p.open(mode))
310+
[out]
311+
_program.py:3: note: Revealed type is 'typing.TextIO'
312+
_program.py:4: note: Revealed type is 'typing.TextIO'
313+
_program.py:5: note: Revealed type is 'typing.BinaryIO'
314+
_program.py:7: note: Revealed type is 'typing.IO[Any]'
315+
316+
[case testPathOpenReturnTypeInferenceSpecialCases]
317+
from pathlib import Path
318+
p = Path("x")
319+
reveal_type(p.open(mode='rb', errors='replace'))
320+
reveal_type(p.open(errors='replace', mode='rb'))
321+
mode = 'rb'
322+
reveal_type(p.open(mode=mode, errors='replace'))
323+
[out]
324+
_program.py:3: note: Revealed type is 'typing.BinaryIO'
325+
_program.py:4: note: Revealed type is 'typing.BinaryIO'
326+
_program.py:6: note: Revealed type is 'typing.IO[Any]'
327+
302328
[case testGenericPatterns]
303329
from typing import Pattern
304330
import re

0 commit comments

Comments
 (0)