From cc18c72f527dab1fa1cb3df51ef967da1add73d3 Mon Sep 17 00:00:00 2001 From: vickyhuo Date: Wed, 17 Mar 2021 23:56:42 -0400 Subject: [PATCH 01/14] check for literal subtypes --- mypy/checkexpr.py | 23 ++++++++++++++++------- mypy/messages.py | 1 + test-data/unit/check-kwargs.test | 9 +++++++++ 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 4a924d643676..7eadce353c9e 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -6,7 +6,7 @@ from typing import ( Any, cast, Dict, Set, List, Tuple, Callable, Union, Optional, Sequence, Iterator ) -from typing_extensions import ClassVar, Final, overload +from typing_extensions import ClassVar, Final, overload, Literal from mypy.errors import report_internal_error from mypy.typeanal import ( @@ -1472,7 +1472,7 @@ def check_argument_types(self, not self.is_valid_var_arg(actual_type)): messages.invalid_var_arg(actual_type, context) if (actual_kind == nodes.ARG_STAR2 and - not self.is_valid_keyword_var_arg(actual_type)): + not self.is_valid_keyword_var_arg(actual_type, args[actual])): is_mapping = is_subtype(actual_type, self.chk.named_type('typing.Mapping')) messages.invalid_keyword_var_arg(actual_type, is_mapping, context) expanded_actual = mapper.expand_actual_type( @@ -3935,23 +3935,32 @@ def is_valid_var_arg(self, typ: Type) -> bool: [AnyType(TypeOfAny.special_form)])) or isinstance(typ, AnyType)) - def is_valid_keyword_var_arg(self, typ: Type) -> bool: + def is_valid_keyword_var_arg(self, typ: Type, arg: Any) -> bool: """Is a type valid as a **kwargs argument?""" + import pdb; pdb.set_trace() + str_type = self.named_type('builtins.str') if self.chk.options.python_version[0] >= 3: - return is_subtype(typ, self.chk.named_generic_type( - 'typing.Mapping', [self.named_type('builtins.str'), + return (is_subtype(typ, self.chk.named_generic_type( + 'typing.Mapping', [str_type, AnyType(TypeOfAny.special_form)])) + or + is_subtype(typ, self.chk.named_generic_type( + 'typing.Mapping', [LiteralType(value=arg, fallback=str_type), AnyType(TypeOfAny.special_form)]))) else: return ( is_subtype(typ, self.chk.named_generic_type( 'typing.Mapping', - [self.named_type('builtins.str'), + [str_type, AnyType(TypeOfAny.special_form)])) or is_subtype(typ, self.chk.named_generic_type( 'typing.Mapping', [self.named_type('builtins.unicode'), - AnyType(TypeOfAny.special_form)]))) + AnyType(TypeOfAny.special_form)])) + or + is_subtype(typ, self.chk.named_generic_type( + 'typing.Mapping', + [LiteralType(value=arg, fallback=str_type), AnyType(TypeOfAny.special_form)]))) def has_member(self, typ: Type, member: str) -> bool: """Does type have member with the given name?""" diff --git a/mypy/messages.py b/mypy/messages.py index da9f783a61c1..d2537066dc9a 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -849,6 +849,7 @@ def invalid_var_arg(self, typ: Type, context: Context) -> None: def invalid_keyword_var_arg(self, typ: Type, is_mapping: bool, context: Context) -> None: typ = get_proper_type(typ) if isinstance(typ, Instance) and is_mapping: + import pdb; pdb.set_trace() self.fail('Keywords must be strings', context) else: suffix = '' diff --git a/test-data/unit/check-kwargs.test b/test-data/unit/check-kwargs.test index 96669e7eea36..11bd1ad642fb 100644 --- a/test-data/unit/check-kwargs.test +++ b/test-data/unit/check-kwargs.test @@ -491,3 +491,12 @@ m = {} # type: Mapping[str, object] f(**m) g(**m) # TODO: Should be an error [builtins fixtures/dict.pyi] + + +[case testGivingArgumentAsPositionalAndKeywordArg3] +from typing import Mapping, Any +from typing_extensions import Literal +kw: Mapping[Literal["b"], int] = {"b": 2} +def f(a=None, b=None, **kwargs) -> None: pass +f(**kw) +[builtins fixtures/dict.pyi] \ No newline at end of file From 9ba8f19a90a648378cfb68883995b93980de1f27 Mon Sep 17 00:00:00 2001 From: Vicky Huo Date: Fri, 19 Mar 2021 17:57:07 -0400 Subject: [PATCH 02/14] check mapping and key type --- mypy/checkexpr.py | 16 +++++++++------- test-data/unit/check-kwargs.test | 23 +++++++++++++++++++---- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 7eadce353c9e..1e38d65f49ce 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -1472,7 +1472,7 @@ def check_argument_types(self, not self.is_valid_var_arg(actual_type)): messages.invalid_var_arg(actual_type, context) if (actual_kind == nodes.ARG_STAR2 and - not self.is_valid_keyword_var_arg(actual_type, args[actual])): + not self.is_valid_keyword_var_arg(actual_type)): is_mapping = is_subtype(actual_type, self.chk.named_type('typing.Mapping')) messages.invalid_keyword_var_arg(actual_type, is_mapping, context) expanded_actual = mapper.expand_actual_type( @@ -3935,7 +3935,7 @@ def is_valid_var_arg(self, typ: Type) -> bool: [AnyType(TypeOfAny.special_form)])) or isinstance(typ, AnyType)) - def is_valid_keyword_var_arg(self, typ: Type, arg: Any) -> bool: + def is_valid_keyword_var_arg(self, typ: Type) -> bool: """Is a type valid as a **kwargs argument?""" import pdb; pdb.set_trace() str_type = self.named_type('builtins.str') @@ -3944,8 +3944,10 @@ def is_valid_keyword_var_arg(self, typ: Type, arg: Any) -> bool: 'typing.Mapping', [str_type, AnyType(TypeOfAny.special_form)])) or - is_subtype(typ, self.chk.named_generic_type( - 'typing.Mapping', [LiteralType(value=arg, fallback=str_type), AnyType(TypeOfAny.special_form)]))) + (is_subtype(typ, self.chk.named_type('typing.Mapping')) and isinstance(typ.args[0], LiteralType) + and isinstance(typ.args[0].value, str))) + # try_getting_literal(typ.args[0]) + # try_getting_str_literals(typ.args[0], typ.args[0]) else: return ( is_subtype(typ, self.chk.named_generic_type( @@ -3958,9 +3960,9 @@ def is_valid_keyword_var_arg(self, typ: Type, arg: Any) -> bool: [self.named_type('builtins.unicode'), AnyType(TypeOfAny.special_form)])) or - is_subtype(typ, self.chk.named_generic_type( - 'typing.Mapping', - [LiteralType(value=arg, fallback=str_type), AnyType(TypeOfAny.special_form)]))) + (is_subtype(typ, self.chk.named_type('typing.Mapping')) + and isinstance(typ.args[0], LiteralType) + and isinstance(typ.args[0].value, str))) def has_member(self, typ: Type, member: str) -> bool: """Does type have member with the given name?""" diff --git a/test-data/unit/check-kwargs.test b/test-data/unit/check-kwargs.test index 11bd1ad642fb..6326d018de68 100644 --- a/test-data/unit/check-kwargs.test +++ b/test-data/unit/check-kwargs.test @@ -494,9 +494,24 @@ g(**m) # TODO: Should be an error [case testGivingArgumentAsPositionalAndKeywordArg3] -from typing import Mapping, Any +from typing import Mapping, Any, Union from typing_extensions import Literal -kw: Mapping[Literal["b"], int] = {"b": 2} def f(a=None, b=None, **kwargs) -> None: pass -f(**kw) -[builtins fixtures/dict.pyi] \ No newline at end of file + +--s: Mapping[Literal[3], int] = {3: 2} +--f(**s) # E: Keywords must be strings + +a: Mapping[Literal['b'], int] = {'b':2} +--f(**a) + +b: Mapping[Literal['c'], int] = {'b':2} +f(**b) + +c: Mapping[Literal['a','b'], Any] = {'a':2, 'b':1} +f(**c) + +d: Mapping[Literal['d'], int] = {'c':2} + +[builtins fixtures/dict.pyi] + +-- check for union literal, \ No newline at end of file From 8559ee225835acb073fc39cc2ec0f70f168b6f10 Mon Sep 17 00:00:00 2001 From: vickyhuo Date: Sun, 21 Mar 2021 23:18:51 -0400 Subject: [PATCH 03/14] use try_getting_str_literals_from_type method --- mypy/checkexpr.py | 16 +++++++--------- test-data/unit/check-kwargs.test | 19 ++++++++++--------- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 1e38d65f49ce..47ef33d0ea51 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -6,7 +6,7 @@ from typing import ( Any, cast, Dict, Set, List, Tuple, Callable, Union, Optional, Sequence, Iterator ) -from typing_extensions import ClassVar, Final, overload, Literal +from typing_extensions import ClassVar, Final, overload from mypy.errors import report_internal_error from mypy.typeanal import ( @@ -64,7 +64,7 @@ from mypy.typeops import ( tuple_fallback, make_simplified_union, true_only, false_only, erase_to_union_or_bound, function_type, callable_type, try_getting_str_literals, custom_special_method, - is_literal_type_like, + is_literal_type_like, try_getting_str_literals_from_type, ) import mypy.errorcodes as codes @@ -3944,10 +3944,9 @@ def is_valid_keyword_var_arg(self, typ: Type) -> bool: 'typing.Mapping', [str_type, AnyType(TypeOfAny.special_form)])) or - (is_subtype(typ, self.chk.named_type('typing.Mapping')) and isinstance(typ.args[0], LiteralType) - and isinstance(typ.args[0].value, str))) - # try_getting_literal(typ.args[0]) - # try_getting_str_literals(typ.args[0], typ.args[0]) + (is_subtype(typ, self.chk.named_type('typing.Mapping')) and + try_getting_str_literals_from_type(typ.args[0]) is not None)) + else: return ( is_subtype(typ, self.chk.named_generic_type( @@ -3960,9 +3959,8 @@ def is_valid_keyword_var_arg(self, typ: Type) -> bool: [self.named_type('builtins.unicode'), AnyType(TypeOfAny.special_form)])) or - (is_subtype(typ, self.chk.named_type('typing.Mapping')) - and isinstance(typ.args[0], LiteralType) - and isinstance(typ.args[0].value, str))) + (is_subtype(typ, self.chk.named_type('typing.Mapping')) and + try_getting_str_literals_from_type(typ.args[0]) is not None)) def has_member(self, typ: Type, member: str) -> bool: """Does type have member with the given name?""" diff --git a/test-data/unit/check-kwargs.test b/test-data/unit/check-kwargs.test index 6326d018de68..dcb3a21ed6a8 100644 --- a/test-data/unit/check-kwargs.test +++ b/test-data/unit/check-kwargs.test @@ -498,20 +498,21 @@ from typing import Mapping, Any, Union from typing_extensions import Literal def f(a=None, b=None, **kwargs) -> None: pass ---s: Mapping[Literal[3], int] = {3: 2} ---f(**s) # E: Keywords must be strings +s = {3: 2} # type: Mapping[Literal[3], int] +f(**s) # E: Keywords must be strings -a: Mapping[Literal['b'], int] = {'b':2} ---f(**a) +a = {'b':2} # type: Mapping[Literal['b'], int] +f(**a) -b: Mapping[Literal['c'], int] = {'b':2} +b = {'b':2} # type: Mapping[Literal['c'], int] \ +# E: Dict entry 0 has incompatible type "Literal['b']": "int"; expected "Literal['c']": "int" f(**b) -c: Mapping[Literal['a','b'], Any] = {'a':2, 'b':1} +c = {'a':2, 'b':1} # type: Mapping[Literal['a','b'], int] f(**c) -d: Mapping[Literal['d'], int] = {'c':2} +d = {'c':2} # type: Mapping[Literal['d'], int] \ +# E: Dict entry 0 has incompatible type "Literal['c']": "int"; expected "Literal['d']": "int" +f(**d) [builtins fixtures/dict.pyi] - --- check for union literal, \ No newline at end of file From 056925ee7835fd4ed7ef8798c0e1f5fc683378f7 Mon Sep 17 00:00:00 2001 From: vickyhuo Date: Sun, 21 Mar 2021 23:23:56 -0400 Subject: [PATCH 04/14] remove debug code --- mypy/checkexpr.py | 6 ++---- mypy/messages.py | 1 - 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 47ef33d0ea51..e3fa4822688a 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -3937,11 +3937,9 @@ def is_valid_var_arg(self, typ: Type) -> bool: def is_valid_keyword_var_arg(self, typ: Type) -> bool: """Is a type valid as a **kwargs argument?""" - import pdb; pdb.set_trace() - str_type = self.named_type('builtins.str') if self.chk.options.python_version[0] >= 3: return (is_subtype(typ, self.chk.named_generic_type( - 'typing.Mapping', [str_type, + 'typing.Mapping', [self.named_type('builtins.str'), AnyType(TypeOfAny.special_form)])) or (is_subtype(typ, self.chk.named_type('typing.Mapping')) and @@ -3951,7 +3949,7 @@ def is_valid_keyword_var_arg(self, typ: Type) -> bool: return ( is_subtype(typ, self.chk.named_generic_type( 'typing.Mapping', - [str_type, + [self.named_type('builtins.str'), AnyType(TypeOfAny.special_form)])) or is_subtype(typ, self.chk.named_generic_type( diff --git a/mypy/messages.py b/mypy/messages.py index d2537066dc9a..da9f783a61c1 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -849,7 +849,6 @@ def invalid_var_arg(self, typ: Type, context: Context) -> None: def invalid_keyword_var_arg(self, typ: Type, is_mapping: bool, context: Context) -> None: typ = get_proper_type(typ) if isinstance(typ, Instance) and is_mapping: - import pdb; pdb.set_trace() self.fail('Keywords must be strings', context) else: suffix = '' From bacade72ddaca0cf97067a15fab2cb46a12b3a1a Mon Sep 17 00:00:00 2001 From: vickyhuo Date: Sun, 21 Mar 2021 23:39:47 -0400 Subject: [PATCH 05/14] clean up --- mypy/checkexpr.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index e3fa4822688a..b7392038f8dd 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -3943,7 +3943,7 @@ def is_valid_keyword_var_arg(self, typ: Type) -> bool: AnyType(TypeOfAny.special_form)])) or (is_subtype(typ, self.chk.named_type('typing.Mapping')) and - try_getting_str_literals_from_type(typ.args[0]) is not None)) + try_getting_str_literals_from_type(typ.args[0]))) else: return ( @@ -3958,7 +3958,7 @@ def is_valid_keyword_var_arg(self, typ: Type) -> bool: AnyType(TypeOfAny.special_form)])) or (is_subtype(typ, self.chk.named_type('typing.Mapping')) and - try_getting_str_literals_from_type(typ.args[0]) is not None)) + try_getting_str_literals_from_type(typ.args[0]))) def has_member(self, typ: Type, member: str) -> bool: """Does type have member with the given name?""" From 042653c32809c13576c3868005e10a863bbbf411 Mon Sep 17 00:00:00 2001 From: vickyhuo Date: Mon, 22 Mar 2021 23:37:04 -0400 Subject: [PATCH 06/14] additional test cases --- mypy/checkexpr.py | 11 +++++++++++ test-data/unit/check-kwargs.test | 22 ++++++++++++++-------- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index b7392038f8dd..4f3436f94e1e 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -1439,6 +1439,17 @@ def check_for_extra_actual_arguments(self, context) is_unexpected_arg_error = True ok = False + elif actual_type.type.has_base('typing.Mapping'): + if messages: + args = try_getting_str_literals_from_type(actual_type.args[0]) + if nodes.ARG_STAR2 not in callee.arg_kinds: + messages.unexpected_keyword_argument(callee, args[0], actual_type.args[0], context) + is_unexpected_arg_error = True + elif args and nodes.ARG_POS in callee.arg_kinds and not all(arg in callee.arg_names for arg in args): + act_name = [name for name, kind in zip(actual_names, actual_kinds) if kind != nodes.ARG_STAR2] + messages.too_few_arguments(callee, context, act_name) + ok = False + # *args/**kwargs can be applied even if the function takes a fixed # number of positional arguments. This may succeed at runtime. diff --git a/test-data/unit/check-kwargs.test b/test-data/unit/check-kwargs.test index dcb3a21ed6a8..db3b194c903a 100644 --- a/test-data/unit/check-kwargs.test +++ b/test-data/unit/check-kwargs.test @@ -497,22 +497,28 @@ g(**m) # TODO: Should be an error from typing import Mapping, Any, Union from typing_extensions import Literal def f(a=None, b=None, **kwargs) -> None: pass +def g(a: int, b: int) -> None: pass # N: "g" defined here +def h(a: int, b: int, **kwargs) -> None: pass s = {3: 2} # type: Mapping[Literal[3], int] f(**s) # E: Keywords must be strings -a = {'b':2} # type: Mapping[Literal['b'], int] -f(**a) +t = {'b':2} # type: Mapping[Literal['b'], int] +f(**t) -b = {'b':2} # type: Mapping[Literal['c'], int] \ +u = {'b':2} # type: Mapping[Literal['c'], int] \ # E: Dict entry 0 has incompatible type "Literal['b']": "int"; expected "Literal['c']": "int" -f(**b) +f(**u) -c = {'a':2, 'b':1} # type: Mapping[Literal['a','b'], int] -f(**c) +v = {'a':2, 'b':1} # type: Mapping[Literal['a','b'], int] +f(**v) -d = {'c':2} # type: Mapping[Literal['d'], int] \ +w = {'c':2} # type: Mapping[Literal['d'], int] \ # E: Dict entry 0 has incompatible type "Literal['c']": "int"; expected "Literal['d']": "int" -f(**d) +f(**w) + +x = {'c':1, 'd': 2} # type: Mapping[Literal['c','d'], int] +g(**x) # E: Unexpected keyword argument "c" for "g" +h(**x) # E: Missing positional arguments "a", "b" in call to "h" [builtins fixtures/dict.pyi] From be86dbf7d1fee618d9d36d2ebe69b28ae93a8bc8 Mon Sep 17 00:00:00 2001 From: vickyhuo Date: Tue, 23 Mar 2021 22:48:12 -0400 Subject: [PATCH 07/14] args check --- mypy/checkexpr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 4f3436f94e1e..16a263e1385b 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -1442,7 +1442,7 @@ def check_for_extra_actual_arguments(self, elif actual_type.type.has_base('typing.Mapping'): if messages: args = try_getting_str_literals_from_type(actual_type.args[0]) - if nodes.ARG_STAR2 not in callee.arg_kinds: + if args and nodes.ARG_STAR2 not in callee.arg_kinds: messages.unexpected_keyword_argument(callee, args[0], actual_type.args[0], context) is_unexpected_arg_error = True elif args and nodes.ARG_POS in callee.arg_kinds and not all(arg in callee.arg_names for arg in args): From 49b2599a1cae8c4fe26fd5b0d4f2efe2bd2b1684 Mon Sep 17 00:00:00 2001 From: vickyhuo Date: Tue, 30 Mar 2021 00:00:42 -0400 Subject: [PATCH 08/14] fix tests --- mypy/checkexpr.py | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 16a263e1385b..649e33a3c60b 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -4,7 +4,7 @@ from contextlib import contextmanager import itertools from typing import ( - Any, cast, Dict, Set, List, Tuple, Callable, Union, Optional, Sequence, Iterator + Any, cast, Dict, Set, List, Tuple, Callable, Union, Optional, Sequence, Iterator, Iterable ) from typing_extensions import ClassVar, Final, overload @@ -1439,15 +1439,21 @@ def check_for_extra_actual_arguments(self, context) is_unexpected_arg_error = True ok = False - elif actual_type.type.has_base('typing.Mapping'): - if messages: + elif isinstance(actual_type, Instance) and \ + actual_type.type.has_base('typing.Mapping'): + if messages and actual_type.args: args = try_getting_str_literals_from_type(actual_type.args[0]) if args and nodes.ARG_STAR2 not in callee.arg_kinds: - messages.unexpected_keyword_argument(callee, args[0], actual_type.args[0], context) + messages.unexpected_keyword_argument( + callee, args[0], actual_type.args[0], context) is_unexpected_arg_error = True - elif args and nodes.ARG_POS in callee.arg_kinds and not all(arg in callee.arg_names for arg in args): - act_name = [name for name, kind in zip(actual_names, actual_kinds) if kind != nodes.ARG_STAR2] - messages.too_few_arguments(callee, context, act_name) + elif args and nodes.ARG_POS in callee.arg_kinds and \ + not all(arg in callee.arg_names for arg in args) and \ + isinstance(actual_names, Iterable): + act_names = [name for name, kind in + zip(iter(actual_names), actual_kinds) + if kind != nodes.ARG_STAR2] + messages.too_few_arguments(callee, context, act_names) ok = False # *args/**kwargs can be applied even if the function takes a fixed @@ -3948,28 +3954,32 @@ def is_valid_var_arg(self, typ: Type) -> bool: def is_valid_keyword_var_arg(self, typ: Type) -> bool: """Is a type valid as a **kwargs argument?""" + key_args = None + if hasattr(typ, 'args') and typ.args: + key_args = try_getting_str_literals_from_type(typ.args[0]) # type: ignore + if self.chk.options.python_version[0] >= 3: return (is_subtype(typ, self.chk.named_generic_type( 'typing.Mapping', [self.named_type('builtins.str'), AnyType(TypeOfAny.special_form)])) or (is_subtype(typ, self.chk.named_type('typing.Mapping')) and - try_getting_str_literals_from_type(typ.args[0]))) + key_args is not None)) else: return ( - is_subtype(typ, self.chk.named_generic_type( + (is_subtype(typ, self.chk.named_generic_type( 'typing.Mapping', [self.named_type('builtins.str'), - AnyType(TypeOfAny.special_form)])) + AnyType(TypeOfAny.special_form)]))) or - is_subtype(typ, self.chk.named_generic_type( + (is_subtype(typ, self.chk.named_generic_type( 'typing.Mapping', [self.named_type('builtins.unicode'), - AnyType(TypeOfAny.special_form)])) + AnyType(TypeOfAny.special_form)]))) or (is_subtype(typ, self.chk.named_type('typing.Mapping')) and - try_getting_str_literals_from_type(typ.args[0]))) + key_args is not None)) def has_member(self, typ: Type, member: str) -> bool: """Does type have member with the given name?""" From b45ad909f38ed67ea1a3bcc53eb61e80ef54f5f0 Mon Sep 17 00:00:00 2001 From: vickyhuo Date: Tue, 30 Mar 2021 00:58:33 -0400 Subject: [PATCH 09/14] fix build --- mypy/checkexpr.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 649e33a3c60b..6cb263a9b75e 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -3954,17 +3954,18 @@ def is_valid_var_arg(self, typ: Type) -> bool: def is_valid_keyword_var_arg(self, typ: Type) -> bool: """Is a type valid as a **kwargs argument?""" - key_args = None - if hasattr(typ, 'args') and typ.args: - key_args = try_getting_str_literals_from_type(typ.args[0]) # type: ignore if self.chk.options.python_version[0] >= 3: - return (is_subtype(typ, self.chk.named_generic_type( - 'typing.Mapping', [self.named_type('builtins.str'), - AnyType(TypeOfAny.special_form)])) - or - (is_subtype(typ, self.chk.named_type('typing.Mapping')) and - key_args is not None)) + return ( + (is_subtype(typ, self.chk.named_generic_type( + 'typing.Mapping', + [self.named_type('builtins.str'), + AnyType(TypeOfAny.special_form)]))) + or + (is_subtype(typ, self.chk.named_type( + 'typing.Mapping')) and + typ.args and # type: ignore + try_getting_str_literals_from_type(typ.args[0]) is not None)) # type: ignore else: return ( @@ -3979,7 +3980,8 @@ def is_valid_keyword_var_arg(self, typ: Type) -> bool: AnyType(TypeOfAny.special_form)]))) or (is_subtype(typ, self.chk.named_type('typing.Mapping')) and - key_args is not None)) + typ.args and # type: ignore + try_getting_str_literals_from_type(typ.args[0]) is not None)) # type: ignore def has_member(self, typ: Type, member: str) -> bool: """Does type have member with the given name?""" From 4df3f9925835adb900e2be66660bc75b74f6398f Mon Sep 17 00:00:00 2001 From: vickyhuo Date: Fri, 16 Apr 2021 00:24:48 -0400 Subject: [PATCH 10/14] use map_instance_to_supertype --- mypy/checkexpr.py | 46 +++++++++++++++++++------------- test-data/unit/check-kwargs.test | 1 + 2 files changed, 28 insertions(+), 19 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 6cb263a9b75e..14d63114f0fb 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -1439,17 +1439,21 @@ def check_for_extra_actual_arguments(self, context) is_unexpected_arg_error = True ok = False - elif isinstance(actual_type, Instance) and \ - actual_type.type.has_base('typing.Mapping'): - if messages and actual_type.args: - args = try_getting_str_literals_from_type(actual_type.args[0]) + elif (isinstance(actual_type, Instance) and + actual_type.type.has_base('typing.Mapping')): + any_type = AnyType(TypeOfAny.special_form) + mapping_info = self.chk.named_generic_type('typing.Mapping', + [any_type, any_type]).type + supertype = map_instance_to_supertype(actual_type, mapping_info) + if messages and supertype.args: + args = try_getting_str_literals_from_type(supertype.args[0]) if args and nodes.ARG_STAR2 not in callee.arg_kinds: messages.unexpected_keyword_argument( - callee, args[0], actual_type.args[0], context) + callee, args[0], supertype.args[0], context) is_unexpected_arg_error = True - elif args and nodes.ARG_POS in callee.arg_kinds and \ - not all(arg in callee.arg_names for arg in args) and \ - isinstance(actual_names, Iterable): + elif (args and nodes.ARG_POS in callee.arg_kinds and + not all(arg in callee.arg_names for arg in args) and + isinstance(actual_names, Iterable)): act_names = [name for name, kind in zip(iter(actual_names), actual_kinds) if kind != nodes.ARG_STAR2] @@ -3954,18 +3958,20 @@ def is_valid_var_arg(self, typ: Type) -> bool: def is_valid_keyword_var_arg(self, typ: Type) -> bool: """Is a type valid as a **kwargs argument?""" + mapping_type = self.chk.named_generic_type('typing.Mapping', + [self.named_type('builtins.str'), + AnyType(TypeOfAny.special_form)]) + typ = get_proper_type(typ) if self.chk.options.python_version[0] >= 3: return ( - (is_subtype(typ, self.chk.named_generic_type( - 'typing.Mapping', - [self.named_type('builtins.str'), - AnyType(TypeOfAny.special_form)]))) + (is_subtype(typ, mapping_type)) or - (is_subtype(typ, self.chk.named_type( - 'typing.Mapping')) and - typ.args and # type: ignore - try_getting_str_literals_from_type(typ.args[0]) is not None)) # type: ignore + (isinstance(typ, Instance) and + is_subtype(typ, self.chk.named_type('typing.Mapping')) and + (try_getting_str_literals_from_type( + map_instance_to_supertype(typ, + mapping_type.type).args[0]) is not None))) else: return ( @@ -3979,9 +3985,11 @@ def is_valid_keyword_var_arg(self, typ: Type) -> bool: [self.named_type('builtins.unicode'), AnyType(TypeOfAny.special_form)]))) or - (is_subtype(typ, self.chk.named_type('typing.Mapping')) and - typ.args and # type: ignore - try_getting_str_literals_from_type(typ.args[0]) is not None)) # type: ignore + (isinstance(typ, Instance) and + is_subtype(typ, self.chk.named_type('typing.Mapping')) and + try_getting_str_literals_from_type( + map_instance_to_supertype(typ, + mapping_type.type).args[0]) is not None)) def has_member(self, typ: Type, member: str) -> bool: """Does type have member with the given name?""" diff --git a/test-data/unit/check-kwargs.test b/test-data/unit/check-kwargs.test index db3b194c903a..897829df6e21 100644 --- a/test-data/unit/check-kwargs.test +++ b/test-data/unit/check-kwargs.test @@ -505,6 +505,7 @@ f(**s) # E: Keywords must be strings t = {'b':2} # type: Mapping[Literal['b'], int] f(**t) +h(**t) u = {'b':2} # type: Mapping[Literal['c'], int] \ # E: Dict entry 0 has incompatible type "Literal['b']": "int"; expected "Literal['c']": "int" From 0152d30bbb1efe225bb3bd89a899253b5d5bcf9d Mon Sep 17 00:00:00 2001 From: Jingchen Ye <97littleleaf11@gmail.com> Date: Fri, 26 Nov 2021 19:28:49 +0800 Subject: [PATCH 11/14] Merge branch 'master' of https://github.com/python/mypy into HEAD # Conflicts: # mypy/checkexpr.py --- mypy/checkexpr.py | 15 ++++--- test-data/unit/check-kwargs.test | 74 ++++++++++++++++---------------- 2 files changed, 44 insertions(+), 45 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 93873162bf34..461105904eaa 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -4027,18 +4027,19 @@ def is_valid_var_arg(self, typ: Type) -> bool: def is_valid_keyword_var_arg(self, typ: Type) -> bool: """Is a type valid as a **kwargs argument?""" - mapping_type = self.chk.named_generic_type( + mapping_type = self.chk.named_generic_type( 'typing.Mapping', [self.named_type('builtins.str'), AnyType(TypeOfAny.special_form)]) typ = get_proper_type(typ) ret = ( - is_subtype(typ, mapping_type) or - (isinstance(typ, Instance) and - is_subtype(typ, self.chk.named_generic_type('typing.Mapping', - [UninhabitedType(), UninhabitedType()])) and + is_subtype(typ, mapping_type) or + is_subtype(typ, self.chk.named_generic_type('typing.Mapping', + [UninhabitedType(), UninhabitedType()])) or + (isinstance(typ, Instance) and + is_subtype(typ, self.chk.named_type('typing.Mapping')) and try_getting_str_literals_from_type(map_instance_to_supertype( - typ, mapping_type.type).arg[0]) is not None - ) or + typ, mapping_type.type).args[0]) is not None + ) or isinstance(typ, ParamSpecType) ) if self.chk.options.python_version[0] < 3: diff --git a/test-data/unit/check-kwargs.test b/test-data/unit/check-kwargs.test index 5ba99f2e5910..8b45f1bdc781 100644 --- a/test-data/unit/check-kwargs.test +++ b/test-data/unit/check-kwargs.test @@ -135,14 +135,14 @@ class A: pass [case testKeywordArgumentsWithDynamicallyTypedCallable] from typing import Any -f = None # type: Any +f: Any = None f(x=f(), z=None()) # E: "None" not callable f(f, zz=None()) # E: "None" not callable f(x=None) [case testKeywordArgumentWithFunctionObject] from typing import Callable -f = None # type: Callable[[A, B], None] +f: Callable[[A, B], None] = None f(a=A(), b=B()) f(A(), b=B()) class A: pass @@ -212,8 +212,8 @@ class B: pass [case testKwargsAfterBareArgs] from typing import Tuple, Any def f(a, *, b=None) -> None: pass -a = None # type: Any -b = None # type: Any +a: Any = None +b: Any = None f(a, **b) [builtins fixtures/dict.pyi] @@ -237,7 +237,7 @@ class B: pass [case testKeywordArgAfterVarArgsWithBothCallerAndCalleeVarArgs] from typing import List def f(*a: 'A', b: 'B' = None) -> None: pass -a = None # type: List[A] +a: List[A] = None f(*a) f(A(), *a) f(b=B()) @@ -262,22 +262,20 @@ class A: pass [case testKwargsArgumentInFunctionBody] from typing import Dict, Any def f( **kwargs: 'A') -> None: - d1 = kwargs # type: Dict[str, A] - d2 = kwargs # type: Dict[A, Any] # E: Incompatible types in assignment (expression has type "Dict[str, A]", variable has type "Dict[A, Any]") - d3 = kwargs # type: Dict[Any, str] # E: Incompatible types in assignment (expression has type "Dict[str, A]", variable has type "Dict[Any, str]") + d1: Dict[str, A] = kwargs + d2: Dict[A, Any] = kwargs # E: Incompatible types in assignment (expression has type "Dict[str, A]", variable has type "Dict[A, Any]") + d3: Dict[Any, str] = kwargs # E: Incompatible types in assignment (expression has type "Dict[str, A]", variable has type "Dict[Any, str]") class A: pass [builtins fixtures/dict.pyi] -[out] [case testKwargsArgumentInFunctionBodyWithImplicitAny] from typing import Dict, Any def f(**kwargs) -> None: - d1 = kwargs # type: Dict[str, A] - d2 = kwargs # type: Dict[str, str] - d3 = kwargs # type: Dict[A, Any] # E: Incompatible types in assignment (expression has type "Dict[str, Any]", variable has type "Dict[A, Any]") + d1: Dict[str, A] = kwargs + d2: Dict[str, str] = kwargs + d3: Dict[A, Any] = kwargs # E: Incompatible types in assignment (expression has type "Dict[str, Any]", variable has type "Dict[A, Any]") class A: pass [builtins fixtures/dict.pyi] -[out] [case testCallingFunctionThatAcceptsVarKwargs] import typing @@ -295,10 +293,10 @@ class B: pass [case testCallingFunctionWithKeywordVarArgs] from typing import Dict def f( **kwargs: 'A') -> None: pass -d = None # type: Dict[str, A] +d: Dict[str, A] = None f(**d) f(x=A(), **d) -d2 = None # type: Dict[str, B] +d2: Dict[str, B] = None 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" @@ -331,9 +329,9 @@ reveal_type(formatter.__call__) # N: Revealed type is "def (message: builtins.s [case testPassingMappingForKeywordVarArg] from typing import Mapping def f(**kwargs: 'A') -> None: pass -b = None # type: Mapping -d = None # type: Mapping[A, A] -m = None # type: Mapping[str, A] +b: Mapping = None +d: Mapping[A, A] = None +m: Mapping[str, A] = None f(**d) # E: Keywords must be strings f(**m) f(**b) @@ -344,7 +342,7 @@ class A: pass from typing import Mapping class MappingSubclass(Mapping[str, str]): pass def f(**kwargs: 'A') -> None: pass -d = None # type: MappingSubclass +d: MappingSubclass = None f(**d) class A: pass [builtins fixtures/dict.pyi] @@ -352,7 +350,7 @@ class A: pass [case testInvalidTypeForKeywordVarArg] from typing import Dict def f(**kwargs: 'A') -> None: pass -d = None # type: Dict[A, A] +d: Dict[A, A] = None f(**d) # E: Keywords must be strings f(**A()) # E: Argument after ** must be a mapping, not "A" class A: pass @@ -361,9 +359,9 @@ class A: pass [case testPassingKeywordVarArgsToNonVarArgsFunction] from typing import Any, Dict def f(a: 'A', b: 'B') -> None: pass -d = None # type: Dict[str, Any] +d: Dict[str, Any] = None f(**d) -d2 = None # type: Dict[str, A] +d2: Dict[str, A] = None f(**d2) # E: Argument 1 to "f" has incompatible type "**Dict[str, A]"; expected "B" class A: pass class B: pass @@ -372,8 +370,8 @@ class B: pass [case testBothKindsOfVarArgs] from typing import Any, List, Dict def f(a: 'A', b: 'A') -> None: pass -l = None # type: List[Any] -d = None # type: Dict[Any, Any] +l: List[Any] = None +d: Dict[Any, Any] = None f(*l, **d) class A: pass [builtins fixtures/dict.pyi] @@ -384,8 +382,8 @@ def f1(a: 'A', b: 'A') -> None: pass def f2(a: 'A') -> None: pass def f3(a: 'A', **kwargs: 'A') -> None: pass def f4(**kwargs: 'A') -> None: pass -d = None # type: Dict[Any, Any] -d2 = None # type: Dict[Any, Any] +d: Dict[Any, Any] = None +d2: Dict[Any, Any] = None f1(**d, **d2) f2(**d, **d2) f3(**d, **d2) @@ -396,14 +394,14 @@ class A: pass [case testPassingKeywordVarArgsToVarArgsOnlyFunction] from typing import Any, Dict def f(*args: 'A') -> None: pass -d = None # type: Dict[Any, Any] +d: Dict[Any, Any] = None f(**d) class A: pass [builtins fixtures/dict.pyi] [case testKeywordArgumentAndCommentSignature] import typing -def f(x): # type: (int) -> str # N: "f" defined here +def f(x: int) -> str: # N: "f" defined here pass f(x='') # E: Argument "x" to "f" has incompatible type "str"; expected "int" f(x=0) @@ -412,7 +410,7 @@ f(y=0) # E: Unexpected keyword argument "y" for "f" [case testKeywordArgumentAndCommentSignature2] import typing class A: - def f(self, x): # type: (int) -> str # N: "f" of "A" defined here + def f(self, x: int) -> str: # N: "f" of "A" defined here pass A().f(x='') # E: Argument "x" to "f" of "A" has incompatible type "str"; expected "int" A().f(x=0) @@ -420,7 +418,7 @@ A().f(y=0) # E: Unexpected keyword argument "y" for "f" of "A" [case testKeywordVarArgsAndCommentSignature] import typing -def f(**kwargs): # type: (**int) -> None +def f(**kwargs: int): pass f(z=1) f(x=1, y=1) @@ -484,11 +482,11 @@ def f(*vargs: int, **kwargs: object) -> None: def g(arg: int = 0, **kwargs: object) -> None: pass -d = {} # type: Dict[str, object] +d: Dict[str, object] = {} f(**d) g(**d) # E: Argument 1 to "g" has incompatible type "**Dict[str, object]"; expected "int" -m = {} # type: Mapping[str, object] +m: Mapping[str, object] = {} f(**m) g(**m) # E: Argument 1 to "g" has incompatible type "**Mapping[str, object]"; expected "int" [builtins fixtures/dict.pyi] @@ -563,25 +561,25 @@ def f(a=None, b=None, **kwargs) -> None: pass def g(a: int, b: int) -> None: pass # N: "g" defined here def h(a: int, b: int, **kwargs) -> None: pass -s = {3: 2} # type: Mapping[Literal[3], int] +s: Mapping[Literal[3], int] = {3: 2} f(**s) # E: Keywords must be strings -t = {'b':2} # type: Mapping[Literal['b'], int] +t: Mapping[Literal['b'], int] = {'b':2} f(**t) h(**t) -u = {'b':2} # type: Mapping[Literal['c'], int] \ +u: Mapping[Literal['c'], int] = {'b':2} \ # E: Dict entry 0 has incompatible type "Literal['b']": "int"; expected "Literal['c']": "int" f(**u) -v = {'a':2, 'b':1} # type: Mapping[Literal['a','b'], int] +v: Mapping[Literal['a','b'], int] = {'a':2, 'b':1} f(**v) -w = {'c':2} # type: Mapping[Literal['d'], int] \ +w: Mapping[Literal['d'], int] = {'c':2} \ # E: Dict entry 0 has incompatible type "Literal['c']": "int"; expected "Literal['d']": "int" f(**w) -x = {'c':1, 'd': 2} # type: Mapping[Literal['c','d'], int] +x: Mapping[Literal['c','d'], int] = {'c':1, 'd': 2} g(**x) # E: Unexpected keyword argument "c" for "g" h(**x) # E: Missing positional arguments "a", "b" in call to "h" From 7838906b3152425637a6cadbbd34e1bc532c0265 Mon Sep 17 00:00:00 2001 From: Jingchen Ye <97littleleaf11@gmail.com> Date: Fri, 26 Nov 2021 20:58:24 +0800 Subject: [PATCH 12/14] Rename test case --- test-data/unit/check-kwargs.test | 63 ++++++++++++++++---------------- 1 file changed, 31 insertions(+), 32 deletions(-) diff --git a/test-data/unit/check-kwargs.test b/test-data/unit/check-kwargs.test index 8b45f1bdc781..46544ae08199 100644 --- a/test-data/unit/check-kwargs.test +++ b/test-data/unit/check-kwargs.test @@ -347,6 +347,37 @@ f(**d) class A: pass [builtins fixtures/dict.pyi] +[case testPassingMappingLiteralsForKeywordVarArg] +from typing import Mapping, Any, Union +from typing_extensions import Literal +def f(a=None, b=None, **kwargs) -> None: pass +def g(a: int, b: int) -> None: pass # N: "g" defined here +def h(a: int, b: int, **kwargs) -> None: pass + +s: Mapping[Literal[3], int] = {3: 2} +f(**s) # E: Keywords must be strings + +t: Mapping[Literal['b'], int] = {'b':2} +f(**t) +h(**t) + +u: Mapping[Literal['c'], int] = {'b':2} \ +# E: Dict entry 0 has incompatible type "Literal['b']": "int"; expected "Literal['c']": "int" +f(**u) + +v: Mapping[Literal['a','b'], int] = {'a':2, 'b':1} +f(**v) + +w: Mapping[Literal['d'], int] = {'c':2} \ +# E: Dict entry 0 has incompatible type "Literal['c']": "int"; expected "Literal['d']": "int" +f(**w) + +x: Mapping[Literal['c','d'], int] = {'c':1, 'd': 2} +g(**x) # E: Unexpected keyword argument "c" for "g" +h(**x) # E: Missing positional arguments "a", "b" in call to "h" + +[builtins fixtures/dict.pyi] + [case testInvalidTypeForKeywordVarArg] from typing import Dict def f(**kwargs: 'A') -> None: pass @@ -552,35 +583,3 @@ main:31: error: Argument after ** must be a mapping, not "C[str, float]" main:32: error: Argument after ** must be a mapping, not "D" main:33: error: Argument 1 to "foo" has incompatible type "**Dict[str, str]"; expected "float" [builtins fixtures/dict.pyi] - - -[case testGivingArgumentAsPositionalAndKeywordArg3] -from typing import Mapping, Any, Union -from typing_extensions import Literal -def f(a=None, b=None, **kwargs) -> None: pass -def g(a: int, b: int) -> None: pass # N: "g" defined here -def h(a: int, b: int, **kwargs) -> None: pass - -s: Mapping[Literal[3], int] = {3: 2} -f(**s) # E: Keywords must be strings - -t: Mapping[Literal['b'], int] = {'b':2} -f(**t) -h(**t) - -u: Mapping[Literal['c'], int] = {'b':2} \ -# E: Dict entry 0 has incompatible type "Literal['b']": "int"; expected "Literal['c']": "int" -f(**u) - -v: Mapping[Literal['a','b'], int] = {'a':2, 'b':1} -f(**v) - -w: Mapping[Literal['d'], int] = {'c':2} \ -# E: Dict entry 0 has incompatible type "Literal['c']": "int"; expected "Literal['d']": "int" -f(**w) - -x: Mapping[Literal['c','d'], int] = {'c':1, 'd': 2} -g(**x) # E: Unexpected keyword argument "c" for "g" -h(**x) # E: Missing positional arguments "a", "b" in call to "h" - -[builtins fixtures/dict.pyi] From 80359a1c4ac3554c9a3a65da95ee2c70ca91ea66 Mon Sep 17 00:00:00 2001 From: Jingchen Ye <97littleleaf11@gmail.com> Date: Fri, 26 Nov 2021 20:58:42 +0800 Subject: [PATCH 13/14] Fix indent error --- mypy/checkexpr.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 461105904eaa..4fd822755222 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -4028,18 +4028,17 @@ def is_valid_var_arg(self, typ: Type) -> bool: def is_valid_keyword_var_arg(self, typ: Type) -> bool: """Is a type valid as a **kwargs argument?""" mapping_type = self.chk.named_generic_type( - 'typing.Mapping', [self.named_type('builtins.str'), AnyType(TypeOfAny.special_form)]) + 'typing.Mapping', [self.named_type('builtins.str'), AnyType(TypeOfAny.special_form)]) typ = get_proper_type(typ) ret = ( is_subtype(typ, mapping_type) or - is_subtype(typ, self.chk.named_generic_type('typing.Mapping', - [UninhabitedType(), UninhabitedType()])) or (isinstance(typ, Instance) and is_subtype(typ, self.chk.named_type('typing.Mapping')) and try_getting_str_literals_from_type(map_instance_to_supertype( - typ, mapping_type.type).args[0]) is not None - ) or + typ, mapping_type.type).args[0]) is not None) or + is_subtype(typ, self.chk.named_generic_type('typing.Mapping', + [UninhabitedType(), UninhabitedType()])) or isinstance(typ, ParamSpecType) ) if self.chk.options.python_version[0] < 3: From 837d6a0e72cbda443ed9e901b3ff4772014df4c0 Mon Sep 17 00:00:00 2001 From: Jingchen Ye <97littleleaf11@gmail.com> Date: Wed, 1 Dec 2021 02:36:42 +0800 Subject: [PATCH 14/14] Add comments --- mypy/checkexpr.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 4fd822755222..3c1403c33b88 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -4037,6 +4037,9 @@ def is_valid_keyword_var_arg(self, typ: Type) -> bool: is_subtype(typ, self.chk.named_type('typing.Mapping')) and try_getting_str_literals_from_type(map_instance_to_supertype( typ, mapping_type.type).args[0]) is not None) or + # This condition is to avoid false-positive errors when empty dictionaries are + # passed with double-stars (e.g., **{})。The type of empty dicts is inferred to be + # dict[, ], which is not a subtype of Mapping[str, Any]。 is_subtype(typ, self.chk.named_generic_type('typing.Mapping', [UninhabitedType(), UninhabitedType()])) or isinstance(typ, ParamSpecType)