Skip to content

Understandable error message for incompatible AnyStr arguments #3433

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 31 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
2bc24f3
New message for anystr errors
quartox May 24, 2017
36e66de
Fix logic path of anystr args
quartox May 24, 2017
7816c4f
Add tests
quartox May 24, 2017
6c3e822
Add docstring
quartox May 24, 2017
4c408e7
Fix tests
quartox May 24, 2017
9ee35b7
Get all AnyStr failures
quartox May 24, 2017
066fde6
Clean up error message
quartox May 24, 2017
c108a58
Fix tests
quartox May 24, 2017
4a133bd
Check for any constrained type
quartox Jun 4, 2017
982f4b7
Revert "Check for any constrained type"
quartox Jun 4, 2017
436925e
Revert "Revert "Check for any constrained type""
quartox Jun 4, 2017
bcf86dd
Revert "Merge with mypy master"
quartox Jun 4, 2017
13dcb81
Fix typeshed
quartox Jun 4, 2017
079bff6
Fix spacing
quartox Jun 4, 2017
9aaec96
Merge branch 'master' of https://github.com/python/mypy into anystr
quartox Jun 11, 2017
5c2b024
Test checkout upstream/master
quartox Jun 11, 2017
2bf23ca
Fix inappropriate updates to files
quartox Jun 11, 2017
d280d91
Finish cleanup of inappropriate additions
quartox Jun 11, 2017
36e3de1
Merge branch 'master' of https://github.com/python/mypy into anystr
quartox Jul 15, 2017
533260d
Improve checking
quartox Jul 15, 2017
93a7d58
Sparser testing
quartox Jul 15, 2017
7465cf8
Use MessageBuilder to get type strings
quartox Jul 16, 2017
e88b7b5
Add indeces to AnyStr error message
quartox Aug 5, 2017
2495e33
Merge branch 'master' of https://github.com/python/mypy into anystr
quartox Aug 5, 2017
7c3dd7b
Small change to error message
quartox Aug 5, 2017
5d77828
Fix some test messages
quartox Aug 5, 2017
24bdbe0
Fix more error messages
quartox Aug 5, 2017
30f98ab
Remove brittle indeces of inferred object
quartox Aug 18, 2017
092f7a7
Merge branch 'master' of https://github.com/python/mypy into anystr
quartox Aug 18, 2017
36fdd0f
Remove mutliple indeces from tests
quartox Aug 19, 2017
b746fe8
Fix test typo
quartox Aug 19, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 47 additions & 3 deletions mypy/applytype.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
from typing import List, Dict
from typing import List, Dict, Sequence, Tuple

import mypy.subtypes
from mypy.sametypes import is_same_type
from mypy.expandtype import expand_type
from mypy.types import Type, TypeVarId, TypeVarType, CallableType, AnyType, PartialType
from mypy.types import (
Type, TypeVarId, TypeVarType, TypeVisitor, CallableType, AnyType, PartialType,
Instance, UnionType
)
from mypy.messages import MessageBuilder
from mypy.nodes import Context

Expand Down Expand Up @@ -38,7 +41,13 @@ def apply_generic_arguments(callable: CallableType, types: List[Type],
types[i] = value
break
else:
msg.incompatible_typevar_value(callable, type, callable.variables[i].name, context)
constraints = get_inferred_object_constraints(msg, callable.arg_types, type, i + 1)
if constraints:
msg.incompatible_inferred_object_arguments(
callable, i + 1, constraints, context)
else:
msg.incompatible_typevar_value(
callable, type, callable.variables[i].name, context)
upper_bound = callable.variables[i].upper_bound
if (type and not isinstance(type, PartialType) and
not mypy.subtypes.is_subtype(type, upper_bound)):
Expand All @@ -61,3 +70,38 @@ def apply_generic_arguments(callable: CallableType, types: List[Type],
ret_type=expand_type(callable.ret_type, id_to_type),
variables=remaining_tvars,
)


def get_inferred_object_constraints(msg: MessageBuilder,
arg_types: Sequence[Type],
type: Type,
index: int) -> Dict[str, Tuple[str, ...]]:
"""Gets incompatible function arguments that are inferred as object based on the type
constraints.

An example of a constrained type is AnyStr which must be all str or all byte. When there is a
mismatch of arguments with a constrained type like AnyStr, then the inferred type is object.
"""
constraints = {} # type: Dict[str, Tuple[str, ...]]
if isinstance(type, Instance) and type.type.fullname() == 'builtins.object':
if index == len(arg_types):
# Index is off by one for '*' arguments
constraints = add_inferred_object_arg_constraints(
msg, constraints, arg_types[index - 1])
else:
constraints = add_inferred_object_arg_constraints(msg, constraints, arg_types[index])
return constraints


def add_inferred_object_arg_constraints(msg: MessageBuilder,
constraints: Dict[str, Tuple[str, ...]],
arg_type: Type) -> Dict[str, Tuple[str, ...]]:
if (isinstance(arg_type, TypeVarType) and
arg_type.values and
len(arg_type.values) > 1 and
arg_type.name not in constraints.keys()):
constraints[arg_type.name] = tuple(msg.format(val) for val in arg_type.values)
elif isinstance(arg_type, UnionType):
for item in arg_type.items:
constraints = add_inferred_object_arg_constraints(msg, constraints, item)
return constraints
17 changes: 16 additions & 1 deletion mypy/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import re
import difflib

from typing import cast, List, Dict, Any, Sequence, Iterable, Tuple, Set, Optional, Union
from typing import cast, List, Dict, Any, Sequence, Iterable, Tuple, Set, Optional, Union, Mapping

from mypy.erasetype import erase_type
from mypy.errors import Errors
Expand Down Expand Up @@ -869,6 +869,21 @@ def incompatible_typevar_value(self,
self.fail(INCOMPATIBLE_TYPEVAR_VALUE.format(typevar_name, callable_name(callee),
self.format(typ)), context)

def incompatible_inferred_object_arguments(self,
callee: CallableType,
index: int,
constraints: Mapping[str, Sequence[str]],
context: Context) -> None:
for key, values in constraints.items():
self.fail('Argument {} of {} has incompatible value'.format(
index, callable_name(callee)), context)
if len(values) == 2:
constraint_str = '{} or {}'.format(values[0], values[1])
elif len(values) > 3:
constraint_str = ', '.join(values[:-1]) + ', or ' + values[-1]
self.note('"{}" must be all one type: {}'.format(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This error message is better than existing but still quite confusing. Ca you make it less terse?
Maybe something in the spirit of "Arguments 1, 2, and 4 to 'my_function' should be either all 'str' or all 'bytes'. Got (str, str, bytes)."

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I can probably find the argument indices easily enough, but the types of the other arguments isn't available when constructing the message because the message builder only gets called when the second argument is analyzed (from what I can tell, please let me know if there is something I have overlooked). I can get mypy.nodes.StrExpr and mypy.nodes.BytesExpr from context, but I do not know how to safely get these values as MessageBuilder.format for both of these returns object.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, then again we could postpone this to the next PR (probably the idea is to use types argument for apply_generic_arguments and map_actuals_to_formals from checkexpr.py).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still I think that error message like "Arguments 1, 2, and 4 to 'my_function' should be either all 'str' or all 'bytes'" will look better than the current one, so it is better to change it in this PR.

key, constraint_str), context)

def overloaded_signatures_overlap(self, index1: int, index2: int,
context: Context) -> None:
self.fail('Overloaded function signatures {} and {} overlap with '
Expand Down
69 changes: 69 additions & 0 deletions test-data/unit/check-functions.test
Original file line number Diff line number Diff line change
Expand Up @@ -2162,3 +2162,72 @@ def i() -> List[Union[str, int]]:
return x

[builtins fixtures/dict.pyi]

[case testAnyStrIncompatibleArguments]
from typing import TypeVar
AnyStr = TypeVar('AnyStr', str, bytes)
def f(x: AnyStr, y: AnyStr) -> None: pass
def g(x: AnyStr, y: AnyStr, z: int) -> AnyStr: pass
f('a', 'b')
f(b'a', b'b')
f('a', b'b') # E: Argument 1 of "f" has incompatible value \
# N: "AnyStr" must be all one type: "str" or "bytes"
g('a', 'b', 1)
g(b'a', b'b', 1)
g('a', b'b', 1) # E: Argument 1 of "g" has incompatible value \
# N: "AnyStr" must be all one type: "str" or "bytes"
g('a', b'b', 'c') # E: Argument 1 of "g" has incompatible value \
# N: "AnyStr" must be all one type: "str" or "bytes" \
# E: Argument 3 to "g" has incompatible type "str"; expected "int"

[case testUnionAnyStrIncompatibleArguments]
from typing import TypeVar, Union
AnyStr = TypeVar('AnyStr', str, bytes)
def f(x: Union[AnyStr, int], y: AnyStr) -> None: pass
f('a', 'b')
f(1, 'b')
f('a', b'b') # E: Argument 1 of "f" has incompatible value \
# N: "AnyStr" must be all one type: "str" or "bytes"

[case testStarAnyStrIncompatibleArguments]
from typing import TypeVar, Union
AnyStr = TypeVar('AnyStr', str, bytes)
def f(*x: AnyStr) -> None: pass
def g(x: int, *y: AnyStr) -> None: pass
def h(*x: AnyStr, y: int) -> None: pass
f('a')
f('a', 'b')
f('a', b'b') # E: Argument 1 of "f" has incompatible value \
# N: "AnyStr" must be all one type: "str" or "bytes"
g(1, 'a')
g(1, 'a', b'b') # E: Argument 1 of "g" has incompatible value \
# N: "AnyStr" must be all one type: "str" or "bytes"
h('a', y=1)
h('a', 'b', y=1)
h('a', b'b', y=1) # E: Value of type variable "AnyStr" of "h" cannot be "object"

[case testConstrainedIncompatibleArguments]
from typing import TypeVar
S = TypeVar('S', int, str)
def f(x: S, y: S) -> S: return (x + y)
f('1', '2')
f('1', 2) # E: Argument 1 of "f" has incompatible value \
# N: "S" must be all one type: "int" or "str"

[case testMultipleConstrainedIncompatibleArguments]
from typing import TypeVar
S = TypeVar('S', int, str)
AnyStr = TypeVar('AnyStr', str, bytes)
def f(a: S, b: S, c: AnyStr, d: AnyStr) -> S: return (a + b)
f('1', '2', '3', '4')
f('1', '2', b'3', b'4')
f(1, 2, '3', '4')
f(1, 2, b'3', b'4')
f(1, '2', '3', '4') # E: Argument 1 of "f" has incompatible value \
# N: "S" must be all one type: "int" or "str"
f('1', '2', b'3', '4') # E: Argument 2 of "f" has incompatible value \
# N: "AnyStr" must be all one type: "str" or "bytes"
f('1', 2, b'3', '4') # E: Argument 1 of "f" has incompatible value \
# N: "S" must be all one type: "int" or "str" \
# E: Argument 2 of "f" has incompatible value \
# N: "AnyStr" must be all one type: "str" or "bytes"
6 changes: 4 additions & 2 deletions test-data/unit/check-inference.test
Original file line number Diff line number Diff line change
Expand Up @@ -750,10 +750,12 @@ AnyStr = TypeVar('AnyStr', bytes, str)
def f(x: Union[AnyStr, int], *a: AnyStr) -> None: pass
f('foo')
f('foo', 'bar')
f('foo', b'bar') # E: Value of type variable "AnyStr" of "f" cannot be "object"
f('foo', b'bar') # E: Argument 1 of "f" has incompatible value \
# N: "AnyStr" must be all one type: "bytes" or "str"
f(1)
f(1, 'foo')
f(1, 'foo', b'bar') # E: Value of type variable "AnyStr" of "f" cannot be "object"
f(1, 'foo', b'bar') # E: Argument 1 of "f" has incompatible value \
# N: "AnyStr" must be all one type: "bytes" or "str"
[builtins fixtures/primitives.pyi]


Expand Down
6 changes: 4 additions & 2 deletions test-data/unit/check-overloading.test
Original file line number Diff line number Diff line change
Expand Up @@ -998,10 +998,12 @@ def g(x: int, *a: AnyStr) -> None: pass

g('foo')
g('foo', 'bar')
g('foo', b'bar') # E: Value of type variable "AnyStr" of "g" cannot be "object"
g('foo', b'bar') # E: Argument 1 of "g" has incompatible value \
# N: "AnyStr" must be all one type: "bytes" or "str"
g(1)
g(1, 'foo')
g(1, 'foo', b'bar') # E: Value of type variable "AnyStr" of "g" cannot be "object"
g(1, 'foo', b'bar') # E: Argument 1 of "g" has incompatible value \
# N: "AnyStr" must be all one type: "bytes" or "str"
[builtins fixtures/primitives.pyi]

[case testBadOverlapWithTypeVarsWithValues]
Expand Down
10 changes: 7 additions & 3 deletions test-data/unit/check-typevar-values.test
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ T = TypeVar('T', int, str)
def f(x: T) -> None: pass
f(1)
f('x')
f(object()) # E: Value of type variable "T" of "f" cannot be "object"
f(object()) # E: Argument 1 of "f" has incompatible value \
# N: "T" must be all one type: "int" or "str"


[case testCallGenericFunctionWithTypeVarValueRestrictionUsingContext]
from typing import TypeVar, List
Expand All @@ -18,7 +20,8 @@ s = ['x']
o = [object()]
i = f(1)
s = f('')
o = f(1) # E: Value of type variable "T" of "f" cannot be "object"
o = f(1) # E: Argument 1 of "f" has incompatible value \
# N: "T" must be all one type: "int" or "str"
[builtins fixtures/list.pyi]

[case testCallGenericFunctionWithTypeVarValueRestrictionAndAnyArgs]
Expand Down Expand Up @@ -239,7 +242,8 @@ class A(Generic[X]):
A(1)
A('x')
A(cast(Any, object()))
A(object()) # E: Value of type variable "X" of "A" cannot be "object"
A(object()) # E: Argument 1 of "A" has incompatible value \
# N: "X" must be all one type: "int" or "str"

[case testGenericTypeWithTypevarValuesAndTypevarArgument]
from typing import TypeVar, Generic
Expand Down
6 changes: 4 additions & 2 deletions test-data/unit/pythoneval.test
Original file line number Diff line number Diff line change
Expand Up @@ -1277,7 +1277,8 @@ re.subn(bpat, b'', b'')[0] + b''
re.subn(bre, lambda m: b'', b'')[0] + b''
re.subn(bpat, lambda m: b'', b'')[0] + b''
[out]
_program.py:7: error: Value of type variable "AnyStr" of "search" cannot be "object"
_program.py:7: error: Argument 1 of "search" has incompatible value
_program.py:7: note: "AnyStr" must be all one type: "str" or "bytes"
_program.py:9: error: Cannot infer type argument 1 of "search"

[case testReModuleString]
Expand All @@ -1301,7 +1302,8 @@ re.subn(spat, '', '')[0] + ''
re.subn(sre, lambda m: '', '')[0] + ''
re.subn(spat, lambda m: '', '')[0] + ''
[out]
_program.py:7: error: Value of type variable "AnyStr" of "search" cannot be "object"
_program.py:7: error: Argument 1 of "search" has incompatible value
_program.py:7: note: "AnyStr" must be all one type: "str" or "bytes"
_program.py:9: error: Cannot infer type argument 1 of "search"

[case testListSetitemTuple]
Expand Down