Skip to content

Commit 83748a4

Browse files
Michael0x2agvanrossum
authored andcommitted
Generalize the 'open' plugin for 'pathlib.Path.open' (python#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)
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