diff --git a/mypy/plugins/default.py b/mypy/plugins/default.py index 55a9a469e97b..6dbd48253e8a 100644 --- a/mypy/plugins/default.py +++ b/mypy/plugins/default.py @@ -1,8 +1,8 @@ from functools import partial -from typing import Callable, Optional, List +from typing import Callable, Optional, List, Tuple from mypy import message_registry -from mypy.nodes import Expression, StrExpr, IntExpr, DictExpr, UnaryExpr +from mypy.nodes import Context, Expression, StrExpr, IntExpr, DictExpr, UnaryExpr from mypy.plugin import ( Plugin, FunctionContext, MethodContext, MethodSigContext, AttributeContext, ClassDefContext, CheckerPluginInterface, @@ -107,55 +107,65 @@ def get_class_decorator_hook(self, fullname: str def open_callback(ctx: FunctionContext) -> Type: - """Infer a better return type for 'open'.""" - return _analyze_open_signature( + """Verify argument types for 'open'.""" + _verify_open_signature( arg_types=ctx.arg_types, args=ctx.args, + arg_names=ctx.callee_arg_names, mode_arg_index=1, - default_return_type=ctx.default_return_type, + text_only_arg_indices=(3, 4, 5), api=ctx.api, + context=ctx.context, ) + return ctx.default_return_type def path_open_callback(ctx: MethodContext) -> Type: - """Infer a better return type for 'pathlib.Path.open'.""" - return _analyze_open_signature( + """Verify argument types for 'pathlib.Path.open'.""" + _verify_open_signature( arg_types=ctx.arg_types, args=ctx.args, + arg_names=ctx.callee_arg_names, mode_arg_index=0, - default_return_type=ctx.default_return_type, + text_only_arg_indices=(2, 3, 4), api=ctx.api, + context=ctx.context, ) + return ctx.default_return_type -def _analyze_open_signature(arg_types: List[List[Type]], - args: List[List[Expression]], - mode_arg_index: int, - default_return_type: Type, - api: CheckerPluginInterface, - ) -> Type: - """A helper for analyzing any function that has approximately - the same signature as the builtin 'open(...)' function. - - Currently, the only thing the caller can customize is the index - of the 'mode' argument. If the mode argument is omitted or is a - string literal, we refine the return type to either 'TextIO' or - 'BinaryIO' as appropriate. +def _verify_open_signature( + arg_types: List[List[Type]], + args: List[List[Expression]], + arg_names: List[Optional[str]], + mode_arg_index: int, + text_only_arg_indices: Tuple[int, ...], + api: CheckerPluginInterface, + context: Context, +) -> None: + """A helper for verifying any function that has approximately the same + signature as the builtin 'open(...)' function. + + If mode is detected to be binary, verify that text-only arguments + (encoding, errors, newline) are None. """ - mode = None if not arg_types or len(arg_types[mode_arg_index]) != 1: - mode = 'r' - else: - mode_expr = args[mode_arg_index][0] - if isinstance(mode_expr, StrExpr): - mode = mode_expr.value - if mode is not None: - assert isinstance(default_return_type, Instance) # type: ignore - if 'b' in mode: - return api.named_generic_type('typing.BinaryIO', []) - else: - return api.named_generic_type('typing.TextIO', []) - return default_return_type + return + + mode_str = try_getting_str_literals( + args[mode_arg_index][0], arg_types[mode_arg_index][0] + ) + if mode_str is None or len(mode_str) != 1 or 'b' not in mode_str[0]: + # mode cannot be found, or is not binary + return + + # Verify that text-only arguments are None if mode is binary + for arg_index in text_only_arg_indices: + if (len(arg_types[arg_index]) == 1 + and not is_subtype(arg_types[arg_index][0], NoneType())): + api.fail("Binary mode doesn't take an argument for {}" + .format(arg_names[arg_index]), + context) def contextmanager_callback(ctx: FunctionContext) -> Type: diff --git a/mypy/typeshed b/mypy/typeshed index 00f5240b88f3..529d3b87fd2f 160000 --- a/mypy/typeshed +++ b/mypy/typeshed @@ -1 +1 @@ -Subproject commit 00f5240b88f382411510de434ec575b4ae46bcee +Subproject commit 529d3b87fd2f07ab4912df49ae149c43fb870cf2 diff --git a/test-data/unit/pythoneval.test b/test-data/unit/pythoneval.test index 6a76b3941a5d..d56118af8d2c 100644 --- a/test-data/unit/pythoneval.test +++ b/test-data/unit/pythoneval.test @@ -287,17 +287,32 @@ _program.py:3: note: Revealed type is 'typing.BinaryIO' _program.py:5: note: Revealed type is 'typing.IO[Any]' [case testOpenReturnTypeInferenceSpecialCases] +from typing_extensions import Literal reveal_type(open()) reveal_type(open(mode='rb', file='x')) reveal_type(open(file='x', mode='rb')) mode = 'rb' -reveal_type(open(mode=mode, file='r')) -[out] -_testOpenReturnTypeInferenceSpecialCases.py:1: error: Too few arguments for "open" -_testOpenReturnTypeInferenceSpecialCases.py:1: note: Revealed type is 'typing.TextIO' -_testOpenReturnTypeInferenceSpecialCases.py:2: note: Revealed type is 'typing.BinaryIO' +reveal_type(open(mode=mode, file='r', errors='replace')) +open('x', mode='rb', errors='replace') +open('x', errors='replace', mode='rb') +errors: Literal['strict', 'ignore'] = 'ignore' +open('x', 'rb', -1, 'UTF-8', errors, '\n') +[out] +_testOpenReturnTypeInferenceSpecialCases.py:2: error: All overload variants of "open" require at least one argument +_testOpenReturnTypeInferenceSpecialCases.py:2: note: Possible overload variants: +_testOpenReturnTypeInferenceSpecialCases.py:2: note: def open(file: Union[str, bytes, int, _PathLike[Any]], mode: = ..., buffering: int = ..., encoding: Optional[str] = ..., errors: Optional[str] = ..., newline: Optional[str] = ..., closefd: bool = ..., opener: Optional[Callable[[str, int], int]] = ...) -> TextIO +_testOpenReturnTypeInferenceSpecialCases.py:2: note: def open(file: Union[str, bytes, int, _PathLike[Any]], mode: , buffering: int = ..., encoding: None = ..., errors: None = ..., newline: None = ..., closefd: bool = ..., opener: Optional[Callable[[str, int], int]] = ...) -> BinaryIO +_testOpenReturnTypeInferenceSpecialCases.py:2: note: def open(file: Union[str, bytes, int, _PathLike[Any]], mode: str, buffering: int = ..., encoding: Optional[str] = ..., errors: Optional[str] = ..., newline: Optional[str] = ..., closefd: bool = ..., opener: Optional[Callable[[str, int], int]] = ...) -> IO[Any] +_testOpenReturnTypeInferenceSpecialCases.py:2: note: Revealed type is 'Any' _testOpenReturnTypeInferenceSpecialCases.py:3: note: Revealed type is 'typing.BinaryIO' -_testOpenReturnTypeInferenceSpecialCases.py:5: note: Revealed type is 'typing.IO[Any]' +_testOpenReturnTypeInferenceSpecialCases.py:4: note: Revealed type is 'typing.BinaryIO' +_testOpenReturnTypeInferenceSpecialCases.py:6: note: Revealed type is 'typing.IO[Any]' +_testOpenReturnTypeInferenceSpecialCases.py:7: error: Binary mode doesn't take an argument for errors +_testOpenReturnTypeInferenceSpecialCases.py:8: error: Binary mode doesn't take an argument for errors +_testOpenReturnTypeInferenceSpecialCases.py:10: error: Binary mode doesn't take an argument for encoding +_testOpenReturnTypeInferenceSpecialCases.py:10: error: Binary mode doesn't take an argument for errors +_testOpenReturnTypeInferenceSpecialCases.py:10: error: Binary mode doesn't take an argument for newline + [case testPathOpenReturnTypeInference] from pathlib import Path @@ -315,15 +330,25 @@ _program.py:7: note: Revealed type is 'typing.IO[Any]' [case testPathOpenReturnTypeInferenceSpecialCases] from pathlib import Path +from typing import Optional +from typing_extensions import Literal p = Path("x") -reveal_type(p.open(mode='rb', errors='replace')) -reveal_type(p.open(errors='replace', mode='rb')) +p.open(mode='rb', errors='replace') +p.open(errors='replace', mode='rb') mode = 'rb' reveal_type(p.open(mode=mode, errors='replace')) -[out] -_program.py:3: note: Revealed type is 'typing.BinaryIO' -_program.py:4: note: Revealed type is 'typing.BinaryIO' -_program.py:6: note: Revealed type is 'typing.IO[Any]' +mode2: Literal['rb'] = 'rb' +p.open(mode=mode2, errors='replace') +errors: Optional[Literal['strict', 'ignore']] = None +p.open('rb', -1, 'UTF-8', errors, '\n') +[out] +_program.py:5: error: Binary mode doesn't take an argument for errors +_program.py:6: error: Binary mode doesn't take an argument for errors +_program.py:8: note: Revealed type is 'typing.IO[Any]' +_program.py:10: error: Binary mode doesn't take an argument for errors +_program.py:12: error: Binary mode doesn't take an argument for encoding +_program.py:12: error: Binary mode doesn't take an argument for errors +_program.py:12: error: Binary mode doesn't take an argument for newline [case testGenericPatterns] from typing import Pattern