From 9af28854f84e46763dbe8297b284cdcdc22ef704 Mon Sep 17 00:00:00 2001 From: Jam Date: Fri, 26 Apr 2024 18:28:30 -0400 Subject: [PATCH 1/6] Finally found the relevant code, added temporary test file to monitor message changes --- balls.py | 5 +++++ mypy/messages.py | 5 +++-- mypy/test/kwargstest.py | 5 +++++ 3 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 balls.py create mode 100644 mypy/test/kwargstest.py diff --git a/balls.py b/balls.py new file mode 100644 index 0000000000000..25e53d1183cd8 --- /dev/null +++ b/balls.py @@ -0,0 +1,5 @@ +import json + +json_kwargs = dict(indent=2) + +json.dumps({}, **json_kwargs) \ No newline at end of file diff --git a/mypy/messages.py b/mypy/messages.py index 199b7c42b11ba..77a90b02d8535 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -565,9 +565,10 @@ def unsupported_operand_types( code: ErrorCode = codes.OPERATOR, ) -> None: """Report unsupported operand types for a binary operation. - + Types can be Type objects or strings. """ + print(op) left_str = "" if isinstance(left_type, str): left_str = left_type @@ -811,7 +812,7 @@ def incompatible_argument( quote_type_string(expected_type_str), ) else: - msg = "Argument {} {}has incompatible type {}; expected {}".format( + msg = "(Modified) Argument {} {}has incompatible type {}; expected {}".format( arg_label, target, quote_type_string(arg_type_str), diff --git a/mypy/test/kwargstest.py b/mypy/test/kwargstest.py new file mode 100644 index 0000000000000..25e53d1183cd8 --- /dev/null +++ b/mypy/test/kwargstest.py @@ -0,0 +1,5 @@ +import json + +json_kwargs = dict(indent=2) + +json.dumps({}, **json_kwargs) \ No newline at end of file From 806184f3ae2cf19443d72321a48d7b116e4ac913 Mon Sep 17 00:00:00 2001 From: Jam Date: Fri, 26 Apr 2024 19:38:37 -0400 Subject: [PATCH 2/6] Added unconditional note for incompatible argument type --- mypy/messages.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mypy/messages.py b/mypy/messages.py index 77a90b02d8535..fb105df59b30a 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -818,6 +818,8 @@ def incompatible_argument( quote_type_string(arg_type_str), quote_type_string(expected_type_str), ) + note_msg = 'Consider using a TypedDict type or "Dict[str, any]" for the ** argument' + notes.append(note_msg) expected_type = get_proper_type(expected_type) if isinstance(expected_type, UnionType): expected_types = list(expected_type.items) From fc4b3e2d3838dad25304038ae3b63fbb7d213480 Mon Sep 17 00:00:00 2001 From: Jam Date: Fri, 26 Apr 2024 23:37:52 -0400 Subject: [PATCH 3/6] check for dictionary **dictionary type now functional --- mypy/messages.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/mypy/messages.py b/mypy/messages.py index fb105df59b30a..fabac7f4dad68 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -23,6 +23,7 @@ from mypy.erasetype import erase_type from mypy.errorcodes import ErrorCode from mypy.errors import ErrorInfo, Errors, ErrorWatcher +import mypy.nodes from mypy.nodes import ( ARG_NAMED, ARG_NAMED_OPT, @@ -812,14 +813,27 @@ def incompatible_argument( quote_type_string(expected_type_str), ) else: - msg = "(Modified) Argument {} {}has incompatible type {}; expected {}".format( + #callee name is not none + #callee name is not (list, dict, list-comprehension, set-comprehension, dictionary-copmrehension, generator) + #user prefers detailed messages + #outer context is not an instance of Index expressiion or String expression + print(context) + print(outer_context) + proper_arg_type = get_proper_type(arg_type) + print(proper_arg_type.type.fullname) + print(proper_arg_type) + msg = """(Modified) Argument {} {}has incompatible type {}; expected {} + argy type:{}""".format( arg_label, target, quote_type_string(arg_type_str), quote_type_string(expected_type_str), + arg_type ) note_msg = 'Consider using a TypedDict type or "Dict[str, any]" for the ** argument' notes.append(note_msg) + if (arg_kind == ARG_STAR2) and (proper_arg_type.type.fullname == 'builtins.dict'): + print("Finally Working") expected_type = get_proper_type(expected_type) if isinstance(expected_type, UnionType): expected_types = list(expected_type.items) From 0a3403df7b9613b15f51eff8df43c067b9e0a7a3 Mon Sep 17 00:00:00 2001 From: Jam Date: Fri, 26 Apr 2024 23:40:59 -0400 Subject: [PATCH 4/6] Note with argument type checking now functional. More testing is required to see if the conditions are strict enough --- mypy/messages.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/mypy/messages.py b/mypy/messages.py index fabac7f4dad68..736218edb854b 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -817,11 +817,10 @@ def incompatible_argument( #callee name is not (list, dict, list-comprehension, set-comprehension, dictionary-copmrehension, generator) #user prefers detailed messages #outer context is not an instance of Index expressiion or String expression - print(context) - print(outer_context) proper_arg_type = get_proper_type(arg_type) - print(proper_arg_type.type.fullname) - print(proper_arg_type) + if (arg_kind == ARG_STAR2) and (proper_arg_type.type.fullname == 'builtins.dict'): + note_msg = 'Consider using a TypedDict type or "Dict[str, any]" for the ** argument' + notes.append(note_msg) msg = """(Modified) Argument {} {}has incompatible type {}; expected {} argy type:{}""".format( arg_label, @@ -830,10 +829,6 @@ def incompatible_argument( quote_type_string(expected_type_str), arg_type ) - note_msg = 'Consider using a TypedDict type or "Dict[str, any]" for the ** argument' - notes.append(note_msg) - if (arg_kind == ARG_STAR2) and (proper_arg_type.type.fullname == 'builtins.dict'): - print("Finally Working") expected_type = get_proper_type(expected_type) if isinstance(expected_type, UnionType): expected_types = list(expected_type.items) From 7460115657caa7dc9e122f9bee68ad42ac013ee1 Mon Sep 17 00:00:00 2001 From: Jam Date: Sat, 27 Apr 2024 15:55:11 -0400 Subject: [PATCH 5/6] Removed unnecesary test annotations and modifications --- mypy/messages.py | 8 +------- mypy/test/kwargstest.py | 9 +++++++-- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/mypy/messages.py b/mypy/messages.py index 736218edb854b..393fd87561c34 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -813,21 +813,15 @@ def incompatible_argument( quote_type_string(expected_type_str), ) else: - #callee name is not none - #callee name is not (list, dict, list-comprehension, set-comprehension, dictionary-copmrehension, generator) - #user prefers detailed messages - #outer context is not an instance of Index expressiion or String expression proper_arg_type = get_proper_type(arg_type) if (arg_kind == ARG_STAR2) and (proper_arg_type.type.fullname == 'builtins.dict'): note_msg = 'Consider using a TypedDict type or "Dict[str, any]" for the ** argument' notes.append(note_msg) - msg = """(Modified) Argument {} {}has incompatible type {}; expected {} - argy type:{}""".format( + msg = "Argument {} {}has incompatible type {}; expected {}".format( arg_label, target, quote_type_string(arg_type_str), quote_type_string(expected_type_str), - arg_type ) expected_type = get_proper_type(expected_type) if isinstance(expected_type, UnionType): diff --git a/mypy/test/kwargstest.py b/mypy/test/kwargstest.py index 25e53d1183cd8..f768086d8eb61 100644 --- a/mypy/test/kwargstest.py +++ b/mypy/test/kwargstest.py @@ -1,5 +1,10 @@ import json +from typing import Dict, Any -json_kwargs = dict(indent=2) +json_kwargs : Dict[str, Any] = dict(indent = 2) -json.dumps({}, **json_kwargs) \ No newline at end of file +json.dumps({}, **json_kwargs) + +json_kwargs_error = dict(indent = 0) + +json.dumps({}, **json_kwargs_error) \ No newline at end of file From 0ad5c3b88810ccef84b219ecdc74e1fa5aab06bc Mon Sep 17 00:00:00 2001 From: Andy Zhu Date: Sun, 28 Apr 2024 05:02:52 -0400 Subject: [PATCH 6/6] Added support for error messages and test cases --- balls.py | 5 --- mypy/messages.py | 17 ++++--- mypy/test/kwargstest.py | 10 ----- test-data/unit/check-columns.test | 3 +- test-data/unit/check-expressions.test | 3 +- test-data/unit/check-kwargs.test | 45 ++++++++++++++++--- .../unit/check-parameter-specification.test | 6 ++- test-data/unit/check-varargs.test | 3 +- 8 files changed, 59 insertions(+), 33 deletions(-) delete mode 100644 balls.py delete mode 100644 mypy/test/kwargstest.py diff --git a/balls.py b/balls.py deleted file mode 100644 index 25e53d1183cd8..0000000000000 --- a/balls.py +++ /dev/null @@ -1,5 +0,0 @@ -import json - -json_kwargs = dict(indent=2) - -json.dumps({}, **json_kwargs) \ No newline at end of file diff --git a/mypy/messages.py b/mypy/messages.py index 393fd87561c34..3291d7292e9b4 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -23,7 +23,6 @@ from mypy.erasetype import erase_type from mypy.errorcodes import ErrorCode from mypy.errors import ErrorInfo, Errors, ErrorWatcher -import mypy.nodes from mypy.nodes import ( ARG_NAMED, ARG_NAMED_OPT, @@ -566,10 +565,9 @@ def unsupported_operand_types( code: ErrorCode = codes.OPERATOR, ) -> None: """Report unsupported operand types for a binary operation. - + Types can be Type objects or strings. """ - print(op) left_str = "" if isinstance(left_type, str): left_str = left_type @@ -813,16 +811,14 @@ def incompatible_argument( quote_type_string(expected_type_str), ) else: - proper_arg_type = get_proper_type(arg_type) - if (arg_kind == ARG_STAR2) and (proper_arg_type.type.fullname == 'builtins.dict'): - note_msg = 'Consider using a TypedDict type or "Dict[str, any]" for the ** argument' - notes.append(note_msg) msg = "Argument {} {}has incompatible type {}; expected {}".format( arg_label, target, quote_type_string(arg_type_str), quote_type_string(expected_type_str), ) + if isinstance(arg_type, Instance): + notes = append_kwargs_notes(notes, arg_type, arg_kind) expected_type = get_proper_type(expected_type) if isinstance(expected_type, UnionType): expected_types = list(expected_type.items) @@ -3105,6 +3101,13 @@ def pretty_seq(args: Sequence[str], conjunction: str) -> str: return ", ".join(quoted[:-1]) + last_sep + quoted[-1] +def append_kwargs_notes(notes: list[str], arg_type: Instance, arg_kind: ArgKind) -> list[str]: + """Explain that annotating keyword arguments may resolve incompatible type issues.""" + if arg_kind == ARG_STAR2 and arg_type.type.fullname == "builtins.dict": + notes.append('Consider using a TypedDict type or "Dict[str, any]" for the ** argument') + return notes + + def append_invariance_notes( notes: list[str], arg_type: Instance, expected_type: Instance ) -> list[str]: diff --git a/mypy/test/kwargstest.py b/mypy/test/kwargstest.py deleted file mode 100644 index f768086d8eb61..0000000000000 --- a/mypy/test/kwargstest.py +++ /dev/null @@ -1,10 +0,0 @@ -import json -from typing import Dict, Any - -json_kwargs : Dict[str, Any] = dict(indent = 2) - -json.dumps({}, **json_kwargs) - -json_kwargs_error = dict(indent = 0) - -json.dumps({}, **json_kwargs_error) \ No newline at end of file diff --git a/test-data/unit/check-columns.test b/test-data/unit/check-columns.test index 44524b9df9435..1a386205154a1 100644 --- a/test-data/unit/check-columns.test +++ b/test-data/unit/check-columns.test @@ -71,7 +71,8 @@ def g(**x: int) -> None: pass a = [''] f(*a) # E:4: Argument 1 to "f" has incompatible type "*List[str]"; expected "int" b = {'x': 'y'} -g(**b) # E:5: Argument 1 to "g" has incompatible type "**Dict[str, str]"; expected "int" +g(**b) # E:5: Argument 1 to "g" has incompatible type "**Dict[str, str]"; expected "int" \ + # N:5: Consider using a TypedDict type or "Dict[str, any]" for the ** argument [builtins fixtures/dict.pyi] [case testColumnsMultipleStatementsPerLine] diff --git a/test-data/unit/check-expressions.test b/test-data/unit/check-expressions.test index 04b3f7a131cc9..8dda33da3efa4 100644 --- a/test-data/unit/check-expressions.test +++ b/test-data/unit/check-expressions.test @@ -1712,7 +1712,8 @@ kw2 = {'x': ''} d2 = dict(it, **kw2) d2() # E: "Dict[str, object]" not callable -d3 = dict(it, **kw2) # type: Dict[str, int] # E: Argument 2 to "dict" has incompatible type "**Dict[str, str]"; expected "int" +d3 = dict(it, **kw2) # type: Dict[str, int] # E: Argument 2 to "dict" has incompatible type "**Dict[str, str]"; expected "int" \ + # N: Consider using a TypedDict type or "Dict[str, any]" for the ** argument [builtins fixtures/dict.pyi] [case testDictFromIterableAndStarStarArgs2] diff --git a/test-data/unit/check-kwargs.test b/test-data/unit/check-kwargs.test index 4beac047e278b..f4154df2c42bb 100644 --- a/test-data/unit/check-kwargs.test +++ b/test-data/unit/check-kwargs.test @@ -75,6 +75,32 @@ def f(a: int): pass def g(): f(0, a=1) [out] +[case testIncompatibleKeywordArgumentsClass] +from typing import Dict + +class A: + def __init__(self, a: str, b: str, c: str, d: int = 2) -> None: + self.a: str = a + self.b: str = b + self.c: str = c + self.d: int = d + +argument_a: str = "argument_a" +arguments_b_c: Dict[str, str] = {"b": "argument_b", "c": "argument_c"} +object_a: A = A(a=argument_a, **arguments_b_c) # E: Argument 2 to "A" has incompatible type "**Dict[str, str]"; expected "int" \ + # N: Consider using a TypedDict type or "Dict[str, any]" for the ** argument +[builtins fixtures/dict.pyi] + +[case testIncompatibleKeywordArgumentsJSON] +import json +json_kwargs = dict(indent=2) +json.dumps({}, **json_kwargs) # E: Argument 2 to "dumps" has incompatible type "**Dict[str, int]"; expected "bool" \ + # N: Consider using a TypedDict type or "Dict[str, any]" for the ** argument \ + # E: Argument 2 to "dumps" has incompatible type "**Dict[str, int]"; expected "Optional[Type[JSONEncoder]]" \ + # E: Argument 2 to "dumps" has incompatible type "**Dict[str, int]"; expected "Optional[Tuple[str, str]]" \ + # E: Argument 2 to "dumps" has incompatible type "**Dict[str, int]"; expected "Optional[Callable[[Any], Any]]" +[builtins fixtures/dict.pyi] + [case testInvalidKeywordArgument] import typing def f(a: 'A') -> None: pass # N: "f" defined here @@ -301,9 +327,12 @@ d: Dict[str, A] f(**d) f(x=A(), **d) d2: Dict[str, B] -f(**d2) # E: Argument 1 to "f" has incompatible type "**Dict[str, B]"; expected "A" -f(x=A(), **d2) # E: Argument 2 to "f" has incompatible type "**Dict[str, B]"; expected "A" -f(**{'x': B()}) # E: Argument 1 to "f" has incompatible type "**Dict[str, B]"; expected "A" +f(**d2) # E: Argument 1 to "f" has incompatible type "**Dict[str, B]"; expected "A" \ + # N: Consider using a TypedDict type or "Dict[str, any]" for the ** argument +f(x=A(), **d2) # E: Argument 2 to "f" has incompatible type "**Dict[str, B]"; expected "A" \ + # N: Consider using a TypedDict type or "Dict[str, any]" for the ** argument +f(**{'x': B()}) # E: Argument 1 to "f" has incompatible type "**Dict[str, B]"; expected "A" \ + # N: Consider using a TypedDict type or "Dict[str, any]" for the ** argument [builtins fixtures/dict.pyi] [case testKwargsAllowedInDunderCall] @@ -366,7 +395,8 @@ def f(a: 'A', b: 'B') -> None: pass d: Dict[str, Any] f(**d) d2: Dict[str, A] -f(**d2) # E: Argument 1 to "f" has incompatible type "**Dict[str, A]"; expected "B" +f(**d2) # E: Argument 1 to "f" has incompatible type "**Dict[str, A]"; expected "B" \ + # N: Consider using a TypedDict type or "Dict[str, any]" for the ** argument class A: pass class B: pass [builtins fixtures/dict.pyi] @@ -443,7 +473,8 @@ f(**a) # okay b = {'': ''} f(b) # E: Argument 1 to "f" has incompatible type "Dict[str, str]"; expected "int" -f(**b) # E: Argument 1 to "f" has incompatible type "**Dict[str, str]"; expected "int" +f(**b) # E: Argument 1 to "f" has incompatible type "**Dict[str, str]"; expected "int" \ + # N: Consider using a TypedDict type or "Dict[str, any]" for the ** argument c = {0: 0} f(**c) # E: Keywords must be strings @@ -488,7 +519,8 @@ def g(arg: int = 0, **kwargs: object) -> None: d = {} # type: Dict[str, object] f(**d) -g(**d) # E: Argument 1 to "g" has incompatible type "**Dict[str, object]"; expected "int" +g(**d) # E: Argument 1 to "g" has incompatible type "**Dict[str, object]"; expected "int" \ + # N: Consider using a TypedDict type or "Dict[str, any]" for the ** argument m = {} # type: Mapping[str, object] f(**m) @@ -563,4 +595,5 @@ main:37: error: Argument 1 to "foo" has incompatible type "**B[str, str]"; expec main:38: error: Argument after ** must be a mapping, not "C[str, float]" main:39: error: Argument after ** must be a mapping, not "D" main:41: error: Argument 1 to "foo" has incompatible type "**Dict[str, str]"; expected "float" +main:41: note: Consider using a TypedDict type or "Dict[str, any]" for the ** argument [builtins fixtures/dict.pyi] diff --git a/test-data/unit/check-parameter-specification.test b/test-data/unit/check-parameter-specification.test index 8fd9abcb97526..168130a05e67a 100644 --- a/test-data/unit/check-parameter-specification.test +++ b/test-data/unit/check-parameter-specification.test @@ -1257,7 +1257,8 @@ def c3(f: Callable[P, int], *args, **kwargs) -> int: ... def c4(f: Callable[P, int], *args: int, **kwargs: str) -> int: # but not ok to call: f(*args, **kwargs) # E: Argument 1 has incompatible type "*Tuple[int, ...]"; expected "P.args" \ - # E: Argument 2 has incompatible type "**Dict[str, str]"; expected "P.kwargs" + # E: Argument 2 has incompatible type "**Dict[str, str]"; expected "P.kwargs" \ + # N: Consider using a TypedDict type or "Dict[str, any]" for the ** argument return 1 def f1(f: Callable[P, int], *args, **kwargs: P.kwargs) -> int: ... # E: ParamSpec must have "*args" typed as "P.args" and "**kwargs" typed as "P.kwargs" @@ -1287,7 +1288,8 @@ def c3(f: Callable[Concatenate[int, P], int], *args, **kwargs) -> int: ... def c4(f: Callable[Concatenate[int, P], int], *args: int, **kwargs: str) -> int: # but not ok to call: f(1, *args, **kwargs) # E: Argument 2 has incompatible type "*Tuple[int, ...]"; expected "P.args" \ - # E: Argument 3 has incompatible type "**Dict[str, str]"; expected "P.kwargs" + # E: Argument 3 has incompatible type "**Dict[str, str]"; expected "P.kwargs" \ + # N: Consider using a TypedDict type or "Dict[str, any]" for the ** argument return 1 def f1(f: Callable[Concatenate[int, P], int], *args, **kwargs: P.kwargs) -> int: ... # E: ParamSpec must have "*args" typed as "P.args" and "**kwargs" typed as "P.kwargs" diff --git a/test-data/unit/check-varargs.test b/test-data/unit/check-varargs.test index 2495a883aa715..434fbf450dfde 100644 --- a/test-data/unit/check-varargs.test +++ b/test-data/unit/check-varargs.test @@ -804,7 +804,8 @@ Weird = TypedDict("Weird", {"@": int}) def foo(**kwargs: Unpack[Weird]) -> None: reveal_type(kwargs["@"]) # N: Revealed type is "builtins.int" foo(**{"@": 42}) -foo(**{"no": "way"}) # E: Argument 1 to "foo" has incompatible type "**Dict[str, str]"; expected "int" +foo(**{"no": "way"}) # E: Argument 1 to "foo" has incompatible type "**Dict[str, str]"; expected "int" \ + # N: Consider using a TypedDict type or "Dict[str, any]" for the ** argument [builtins fixtures/dict.pyi] [case testUnpackKwargsEmpty]