Skip to content

Commit fe1d523

Browse files
sixoletJukkaL
authored andcommitted
Ensure required keyword-only arguments are provided. (#2441)
* Introduce kind ARG_NAMED_OPT for optional keyword-only arguments. Before, mypy would assume all keyword-only arguments were optional, and not provide an error if you failed to provide a keyword-only argument at a call site. Now mypy provides you with a helpful error. * Adjust tests to understand NAMED vs NAMED_OPT * More tests for named optionals. Also extends the test runner to allow more than one error per line. * Until the typeshed is fixed ignore `handlers` argument not being provided The relevant typeshed PR is https://github.com/python/typeshed/pull/704/commits * In test data parser, split and strip expected error comments better
1 parent 5f8c447 commit fe1d523

15 files changed

+115
-46
lines changed

mypy/checkexpr.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -666,15 +666,21 @@ def check_argument_count(self, callee: CallableType, actual_types: List[Type],
666666
if messages:
667667
messages.too_few_arguments(callee, context, actual_names)
668668
ok = False
669+
elif kind == nodes.ARG_NAMED and (not formal_to_actual[i] and
670+
not is_unexpected_arg_error):
671+
# No actual for a mandatory named formal
672+
if messages:
673+
messages.missing_named_argument(callee, context, callee.arg_names[i])
674+
ok = False
669675
elif kind in [nodes.ARG_POS, nodes.ARG_OPT,
670-
nodes.ARG_NAMED] and is_duplicate_mapping(
676+
nodes.ARG_NAMED, nodes.ARG_NAMED_OPT] and is_duplicate_mapping(
671677
formal_to_actual[i], actual_kinds):
672678
if (self.chk.in_checked_function() or
673679
isinstance(actual_types[formal_to_actual[i][0]], TupleType)):
674680
if messages:
675681
messages.duplicate_argument_value(callee, i, context)
676682
ok = False
677-
elif (kind == nodes.ARG_NAMED and formal_to_actual[i] and
683+
elif (kind in (nodes.ARG_NAMED, nodes.ARG_NAMED_OPT) and formal_to_actual[i] and
678684
actual_kinds[formal_to_actual[i][0]] not in [nodes.ARG_NAMED, nodes.ARG_STAR2]):
679685
# Positional argument when expecting a keyword argument.
680686
if messages:
@@ -1898,7 +1904,7 @@ def map_actuals_to_formals(caller_kinds: List[int],
18981904
if kind == nodes.ARG_POS:
18991905
if j < ncallee:
19001906
if callee_kinds[j] in [nodes.ARG_POS, nodes.ARG_OPT,
1901-
nodes.ARG_NAMED]:
1907+
nodes.ARG_NAMED, nodes.ARG_NAMED_OPT]:
19021908
map[j].append(i)
19031909
j += 1
19041910
elif callee_kinds[j] == nodes.ARG_STAR:
@@ -1920,14 +1926,14 @@ def map_actuals_to_formals(caller_kinds: List[int],
19201926
# Assume that it is an iterable (if it isn't, there will be
19211927
# an error later).
19221928
while j < ncallee:
1923-
if callee_kinds[j] in (nodes.ARG_NAMED, nodes.ARG_STAR2):
1929+
if callee_kinds[j] in (nodes.ARG_NAMED, nodes.ARG_NAMED_OPT, nodes.ARG_STAR2):
19241930
break
19251931
else:
19261932
map[j].append(i)
19271933
if callee_kinds[j] == nodes.ARG_STAR:
19281934
break
19291935
j += 1
1930-
elif kind == nodes.ARG_NAMED:
1936+
elif kind in (nodes.ARG_NAMED, nodes.ARG_NAMED_OPT):
19311937
name = caller_names[i]
19321938
if name in callee_names:
19331939
map[callee_names.index(name)].append(i)

mypy/fastparse.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
StarExpr, YieldFromExpr, NonlocalDecl, DictionaryComprehension,
1616
SetComprehension, ComplexExpr, EllipsisExpr, YieldExpr, Argument,
1717
AwaitExpr, TempNode, Expression, Statement,
18-
ARG_POS, ARG_OPT, ARG_STAR, ARG_NAMED, ARG_STAR2
18+
ARG_POS, ARG_OPT, ARG_STAR, ARG_NAMED, ARG_NAMED_OPT, ARG_STAR2
1919
)
2020
from mypy.types import (
2121
Type, CallableType, AnyType, UnboundType, TupleType, TypeList, EllipsisType,
@@ -356,14 +356,12 @@ def make_argument(arg: ast35.arg, default: Optional[ast35.expr], kind: int) -> A
356356
if args.vararg is not None:
357357
new_args.append(make_argument(args.vararg, None, ARG_STAR))
358358

359-
num_no_kw_defaults = len(args.kwonlyargs) - len(args.kw_defaults)
360-
# keyword-only arguments without defaults
361-
for a in args.kwonlyargs[:num_no_kw_defaults]:
362-
new_args.append(make_argument(a, None, ARG_NAMED))
363-
364359
# keyword-only arguments with defaults
365-
for a, d in zip(args.kwonlyargs[num_no_kw_defaults:], args.kw_defaults):
366-
new_args.append(make_argument(a, d, ARG_NAMED))
360+
for a, d in zip(args.kwonlyargs, args.kw_defaults):
361+
new_args.append(make_argument(
362+
a,
363+
d,
364+
ARG_NAMED if d is None else ARG_NAMED_OPT))
367365

368366
# **kwarg
369367
if args.kwarg is not None:

mypy/messages.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,12 @@ def too_few_arguments(self, callee: CallableType, context: Context,
508508
msg += ' for {}'.format(callee.name)
509509
self.fail(msg, context)
510510

511+
def missing_named_argument(self, callee: CallableType, context: Context, name: str) -> None:
512+
msg = 'Missing named argument "{}"'.format(name)
513+
if callee.name:
514+
msg += ' for function {}'.format(callee.name)
515+
self.fail(msg, context)
516+
511517
def too_many_arguments(self, callee: CallableType, context: Context) -> None:
512518
msg = 'Too many arguments'
513519
if callee.name:

mypy/nodes.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1214,6 +1214,8 @@ def accept(self, visitor: NodeVisitor[T]) -> T:
12141214
ARG_NAMED = 3 # type: int
12151215
# **arg argument
12161216
ARG_STAR2 = 4 # type: int
1217+
# In an argument list, keyword-only and also optional
1218+
ARG_NAMED_OPT = 5
12171219

12181220

12191221
class CallExpr(Expression):

mypy/parse.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -773,7 +773,7 @@ def parse_normal_arg(self, require_named: bool,
773773
self.expect('=')
774774
initializer = self.parse_expression(precedence[','])
775775
if require_named:
776-
kind = nodes.ARG_NAMED
776+
kind = nodes.ARG_NAMED_OPT
777777
else:
778778
kind = nodes.ARG_OPT
779779
else:

mypy/semanal.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@
5959
SymbolTableNode, BOUND_TVAR, UNBOUND_TVAR, ListComprehension, GeneratorExpr,
6060
FuncExpr, MDEF, FuncBase, Decorator, SetExpr, TypeVarExpr, NewTypeExpr,
6161
StrExpr, BytesExpr, PrintStmt, ConditionalExpr, PromoteExpr,
62-
ComparisonExpr, StarExpr, ARG_POS, ARG_NAMED, MroError, type_aliases,
62+
ComparisonExpr, StarExpr, ARG_POS, ARG_NAMED, ARG_NAMED_OPT, MroError, type_aliases,
6363
YieldFromExpr, NamedTupleExpr, TypedDictExpr, NonlocalDecl, SymbolNode,
6464
SetComprehension, DictionaryComprehension, TYPE_ALIAS, TypeAliasExpr,
6565
YieldExpr, ExecStmt, Argument, BackquoteExpr, ImportBase, AwaitExpr,
@@ -1830,14 +1830,14 @@ def add_method(funcname: str, ret: Type, args: List[Argument], name=None,
18301830
info.names[funcname] = SymbolTableNode(MDEF, func)
18311831

18321832
add_method('_replace', ret=selftype,
1833-
args=[Argument(var, var.type, EllipsisExpr(), ARG_NAMED) for var in vars])
1833+
args=[Argument(var, var.type, EllipsisExpr(), ARG_NAMED_OPT) for var in vars])
18341834
add_method('__init__', ret=NoneTyp(), name=info.name(),
18351835
args=[Argument(var, var.type, None, ARG_POS) for var in vars])
18361836
add_method('_asdict', args=[], ret=ordereddictype)
18371837
add_method('_make', ret=selftype, is_classmethod=True,
18381838
args=[Argument(Var('iterable', iterable_type), iterable_type, None, ARG_POS),
1839-
Argument(Var('new'), AnyType(), EllipsisExpr(), ARG_NAMED),
1840-
Argument(Var('len'), AnyType(), EllipsisExpr(), ARG_NAMED)])
1839+
Argument(Var('new'), AnyType(), EllipsisExpr(), ARG_NAMED_OPT),
1840+
Argument(Var('len'), AnyType(), EllipsisExpr(), ARG_NAMED_OPT)])
18411841
return info
18421842

18431843
def make_argument(self, name: str, type: Type) -> Argument:

mypy/strconv.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,9 @@ def func_helper(self, o: 'mypy.nodes.FuncItem') -> List[object]:
4141
extra = [] # type: List[Tuple[str, List[mypy.nodes.Var]]]
4242
for i, arg in enumerate(o.arguments):
4343
kind = arg.kind # type: int
44-
if kind == mypy.nodes.ARG_POS:
44+
if kind in (mypy.nodes.ARG_POS, mypy.nodes.ARG_NAMED):
4545
args.append(o.arguments[i].variable)
46-
elif kind in (mypy.nodes.ARG_OPT, mypy.nodes.ARG_NAMED):
46+
elif kind in (mypy.nodes.ARG_OPT, mypy.nodes.ARG_NAMED_OPT):
4747
args.append(o.arguments[i].variable)
4848
init.append(o.arguments[i].initialization_statement)
4949
elif kind == mypy.nodes.ARG_STAR:
@@ -108,7 +108,8 @@ def visit_import_all(self, o: 'mypy.nodes.ImportAll') -> str:
108108
def visit_func_def(self, o: 'mypy.nodes.FuncDef') -> str:
109109
a = self.func_helper(o)
110110
a.insert(0, o.name())
111-
if mypy.nodes.ARG_NAMED in [arg.kind for arg in o.arguments]:
111+
arg_kinds = {arg.kind for arg in o.arguments}
112+
if len(arg_kinds & {mypy.nodes.ARG_NAMED, mypy.nodes.ARG_NAMED_OPT}) > 0:
112113
a.insert(1, 'MaxPos({})'.format(o.max_pos))
113114
if o.is_abstract:
114115
a.insert(-1, 'Abstract')

mypy/stubgen.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@
5555
from mypy.nodes import (
5656
Expression, IntExpr, UnaryExpr, StrExpr, BytesExpr, NameExpr, FloatExpr, MemberExpr, TupleExpr,
5757
ListExpr, ComparisonExpr, CallExpr, ClassDef, MypyFile, Decorator, AssignmentStmt,
58-
IfStmt, ImportAll, ImportFrom, Import, FuncDef, FuncBase, ARG_STAR, ARG_STAR2, ARG_NAMED
58+
IfStmt, ImportAll, ImportFrom, Import, FuncDef, FuncBase,
59+
ARG_STAR, ARG_STAR2, ARG_NAMED, ARG_NAMED_OPT,
5960
)
6061
from mypy.stubgenc import parse_all_signatures, find_unique_signatures, generate_stub_for_c_module
6162
from mypy.stubutil import is_c_module, write_header
@@ -261,7 +262,7 @@ def visit_func_def(self, o: FuncDef) -> None:
261262
name = var.name()
262263
init_stmt = arg_.initialization_statement
263264
if init_stmt:
264-
if kind == ARG_NAMED and '*' not in args:
265+
if kind in (ARG_NAMED, ARG_NAMED_OPT) and '*' not in args:
265266
args.append('*')
266267
typename = self.get_str_type_of_node(init_stmt.rvalue, True)
267268
arg = '{}: {} = ...'.format(name, typename)

mypy/test/data.py

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -363,15 +363,20 @@ def expand_errors(input: List[str], output: List[str], fnam: str) -> None:
363363
"""
364364

365365
for i in range(len(input)):
366-
m = re.search('# ([EN]):((?P<col>\d+):)? (?P<message>.*)$', input[i])
367-
if m:
368-
severity = 'error' if m.group(1) == 'E' else 'note'
369-
col = m.group('col')
370-
if col is None:
371-
output.append('{}:{}: {}: {}'.format(fnam, i + 1, severity, m.group('message')))
372-
else:
373-
output.append('{}:{}:{}: {}: {}'.format(
374-
fnam, i + 1, col, severity, m.group('message')))
366+
# The first in the split things isn't a comment
367+
for possible_err_comment in input[i].split('#')[1:]:
368+
m = re.search(
369+
'^([EN]):((?P<col>\d+):)? (?P<message>.*)$',
370+
possible_err_comment.strip())
371+
if m:
372+
severity = 'error' if m.group(1) == 'E' else 'note'
373+
col = m.group('col')
374+
if col is None:
375+
output.append(
376+
'{}:{}: {}: {}'.format(fnam, i + 1, severity, m.group('message')))
377+
else:
378+
output.append('{}:{}:{}: {}: {}'.format(
379+
fnam, i + 1, col, severity, m.group('message')))
375380

376381

377382
def fix_win_path(line: str) -> str:

mypy/types.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@
77
)
88

99
import mypy.nodes
10-
from mypy.nodes import INVARIANT, SymbolNode
10+
from mypy.nodes import (
11+
INVARIANT, SymbolNode,
12+
ARG_POS, ARG_OPT, ARG_STAR, ARG_STAR2, ARG_NAMED, ARG_NAMED_OPT,
13+
)
1114

1215
from mypy import experiments
1316

@@ -539,7 +542,7 @@ class CallableType(FunctionLike):
539542
"""Type of a non-overloaded callable object (function)."""
540543

541544
arg_types = None # type: List[Type] # Types of function arguments
542-
arg_kinds = None # type: List[int] # mypy.nodes.ARG_ constants
545+
arg_kinds = None # type: List[int] # ARG_ constants
543546
arg_names = None # type: List[str] # None if not a keyword argument
544547
min_args = 0 # Minimum number of arguments; derived from arg_kinds
545548
is_var_arg = False # Is it a varargs function? derived from arg_kinds
@@ -581,8 +584,9 @@ def __init__(self,
581584
self.arg_types = arg_types
582585
self.arg_kinds = arg_kinds
583586
self.arg_names = arg_names
584-
self.min_args = arg_kinds.count(mypy.nodes.ARG_POS)
585-
self.is_var_arg = mypy.nodes.ARG_STAR in arg_kinds
587+
self.min_args = arg_kinds.count(ARG_POS)
588+
self.is_var_arg = ARG_STAR in arg_kinds
589+
self.is_kw_arg = ARG_STAR2 in arg_kinds
586590
self.ret_type = ret_type
587591
self.fallback = fallback
588592
assert not name or '<bound method' not in name
@@ -1250,17 +1254,17 @@ def visit_callable_type(self, t: CallableType) -> str:
12501254
for i in range(len(t.arg_types)):
12511255
if s != '':
12521256
s += ', '
1253-
if t.arg_kinds[i] == mypy.nodes.ARG_NAMED and not bare_asterisk:
1257+
if t.arg_kinds[i] in (ARG_NAMED, ARG_NAMED_OPT) and not bare_asterisk:
12541258
s += '*, '
12551259
bare_asterisk = True
1256-
if t.arg_kinds[i] == mypy.nodes.ARG_STAR:
1260+
if t.arg_kinds[i] == ARG_STAR:
12571261
s += '*'
1258-
if t.arg_kinds[i] == mypy.nodes.ARG_STAR2:
1262+
if t.arg_kinds[i] == ARG_STAR2:
12591263
s += '**'
12601264
if t.arg_names[i]:
12611265
s += t.arg_names[i] + ': '
12621266
s += str(t.arg_types[i])
1263-
if t.arg_kinds[i] == mypy.nodes.ARG_OPT:
1267+
if t.arg_kinds[i] in (ARG_OPT, ARG_NAMED_OPT):
12641268
s += ' ='
12651269

12661270
s = '({})'.format(s)

test-data/samples/crawl.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -859,5 +859,5 @@ def main() -> None:
859859

860860

861861
if __name__ == '__main__':
862-
logging.basicConfig(level=logging.INFO)
862+
logging.basicConfig(level=logging.INFO) # type: ignore
863863
main()

test-data/samples/crawl2.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -848,5 +848,5 @@ def main() -> None:
848848

849849

850850
if __name__ == '__main__':
851-
logging.basicConfig(level=logging.INFO)
851+
logging.basicConfig(level=logging.INFO) # type: ignore
852852
main()

test-data/unit/check-kwargs.test

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,10 +115,57 @@ main:4: error: Unexpected keyword argument "b"
115115
[case testKeywordOnlyArguments]
116116
import typing
117117
def f(a: 'A', *, b: 'B' = None) -> None: pass
118+
def g(a: 'A', *, b: 'B') -> None: pass
119+
def h(a: 'A', *, b: 'B', aa: 'A') -> None: pass
120+
def i(a: 'A', *, b: 'B', aa: 'A' = None) -> None: pass
118121
f(A(), b=B())
119122
f(b=B(), a=A())
120123
f(A())
121124
f(A(), B()) # E: Too many positional arguments for "f"
125+
g(A(), b=B())
126+
g(b=B(), a=A())
127+
g(A()) # E: Missing named argument "b" for function "g"
128+
g(A(), B()) # E: Too many positional arguments for "g"
129+
h(A()) # E: Missing named argument "b" for function "h" # E: Missing named argument "aa" for function "h"
130+
h(A(), b=B()) # E: Missing named argument "aa" for function "h"
131+
h(A(), aa=A()) # E: Missing named argument "b" for function "h"
132+
h(A(), b=B(), aa=A())
133+
h(A(), aa=A(), b=B())
134+
i(A()) # E: Missing named argument "b" for function "i"
135+
i(A(), b=B())
136+
i(A(), aa=A()) # E: Missing named argument "b" for function "i"
137+
i(A(), b=B(), aa=A())
138+
i(A(), aa=A(), b=B())
139+
140+
class A: pass
141+
class B: pass
142+
143+
[case testKeywordOnlyArgumentsFastparse]
144+
# flags: --fast-parser
145+
import typing
146+
def f(a: 'A', *, b: 'B' = None) -> None: pass
147+
def g(a: 'A', *, b: 'B') -> None: pass
148+
def h(a: 'A', *, b: 'B', aa: 'A') -> None: pass
149+
def i(a: 'A', *, b: 'B', aa: 'A' = None) -> None: pass
150+
f(A(), b=B())
151+
f(b=B(), a=A())
152+
f(A())
153+
f(A(), B()) # E: Too many positional arguments for "f"
154+
g(A(), b=B())
155+
g(b=B(), a=A())
156+
g(A()) # E: Missing named argument "b" for function "g"
157+
g(A(), B()) # E: Too many positional arguments for "g"
158+
h(A()) # E: Missing named argument "b" for function "h" # E: Missing named argument "aa" for function "h"
159+
h(A(), b=B()) # E: Missing named argument "aa" for function "h"
160+
h(A(), aa=A()) # E: Missing named argument "b" for function "h"
161+
h(A(), b=B(), aa=A())
162+
h(A(), aa=A(), b=B())
163+
i(A()) # E: Missing named argument "b" for function "i"
164+
i(A(), b=B())
165+
i(A(), aa=A()) # E: Missing named argument "b" for function "i"
166+
i(A(), b=B(), aa=A())
167+
i(A(), aa=A(), b=B())
168+
122169
class A: pass
123170
class B: pass
124171

test-data/unit/parse.test

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2340,7 +2340,7 @@ MypyFile:1(
23402340
Args(
23412341
Var(x)
23422342
Var(y))
2343-
def (x: A?, *, y: B?) -> None?
2343+
def (x: A?, *, y: B? =) -> None?
23442344
Init(
23452345
AssignmentStmt:1(
23462346
NameExpr(y)
@@ -2357,7 +2357,7 @@ MypyFile:1(
23572357
MaxPos(0)
23582358
Args(
23592359
Var(y))
2360-
def (*, y: B?) -> None?
2360+
def (*, y: B? =) -> None?
23612361
Init(
23622362
AssignmentStmt:1(
23632363
NameExpr(y)
@@ -2375,7 +2375,6 @@ MypyFile:1(
23752375
Args(
23762376
Var(y))
23772377
def (*, y: B?) -> None?
2378-
Init()
23792378
Block:1(
23802379
PassStmt:1())))
23812380

test-data/unit/semanal-types.test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1005,7 +1005,7 @@ MypyFile:1(
10051005
MaxPos(0)
10061006
Args(
10071007
Var(y))
1008-
def (*x: builtins.int, *, y: builtins.str) -> Any
1008+
def (*x: builtins.int, *, y: builtins.str =) -> Any
10091009
Init(
10101010
AssignmentStmt:1(
10111011
NameExpr(y [l])

0 commit comments

Comments
 (0)