Skip to content

Commit 7d810b6

Browse files
authored
bpo-46838: Syntax error improvements for function definitions (GH-31590)
1 parent deeaac4 commit 7d810b6

File tree

4 files changed

+3569
-1044
lines changed

4 files changed

+3569
-1044
lines changed

Grammar/python.gram

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -306,14 +306,16 @@ slash_with_default[SlashWithDefault*]:
306306
| a=param_no_default* b=param_with_default+ '/' &')' { _PyPegen_slash_with_default(p, (asdl_arg_seq *)a, b) }
307307

308308
star_etc[StarEtc*]:
309+
| invalid_star_etc
309310
| '*' a=param_no_default b=param_maybe_default* c=[kwds] {
310311
_PyPegen_star_etc(p, a, b, c) }
311312
| '*' ',' b=param_maybe_default+ c=[kwds] {
312313
_PyPegen_star_etc(p, NULL, b, c) }
313314
| a=kwds { _PyPegen_star_etc(p, NULL, NULL, a) }
314-
| invalid_star_etc
315315

316-
kwds[arg_ty]: '**' a=param_no_default { a }
316+
kwds[arg_ty]:
317+
| invalid_kwds
318+
| '**' a=param_no_default { a }
317319

318320
# One parameter. This *includes* a following comma and type comment.
319321
#
@@ -339,7 +341,7 @@ param_maybe_default[NameDefaultPair*]:
339341
| a=param c=default? tc=TYPE_COMMENT? &')' { _PyPegen_name_default_pair(p, a, c, tc) }
340342
param[arg_ty]: a=NAME b=annotation? { _PyAST_arg(a->v.Name.id, b, NULL, EXTRA) }
341343
annotation[expr_ty]: ':' a=expression { a }
342-
default[expr_ty]: '=' a=expression { a }
344+
default[expr_ty]: '=' a=expression { a } | invalid_default
343345

344346
# If statement
345347
# ------------
@@ -836,14 +838,16 @@ lambda_slash_with_default[SlashWithDefault*]:
836838
| a=lambda_param_no_default* b=lambda_param_with_default+ '/' &':' { _PyPegen_slash_with_default(p, (asdl_arg_seq *)a, b) }
837839

838840
lambda_star_etc[StarEtc*]:
841+
| invalid_lambda_star_etc
839842
| '*' a=lambda_param_no_default b=lambda_param_maybe_default* c=[lambda_kwds] {
840843
_PyPegen_star_etc(p, a, b, c) }
841844
| '*' ',' b=lambda_param_maybe_default+ c=[lambda_kwds] {
842845
_PyPegen_star_etc(p, NULL, b, c) }
843846
| a=lambda_kwds { _PyPegen_star_etc(p, NULL, NULL, a) }
844-
| invalid_lambda_star_etc
845847

846-
lambda_kwds[arg_ty]: '**' a=lambda_param_no_default { a }
848+
lambda_kwds[arg_ty]:
849+
| invalid_lambda_kwds
850+
| '**' a=lambda_param_no_default { a }
847851

848852
lambda_param_no_default[arg_ty]:
849853
| a=lambda_param ',' { a }
@@ -1151,6 +1155,26 @@ invalid_parameters:
11511155
RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "non-default argument follows default argument") }
11521156
| param_no_default* a='(' param_no_default+ ','? b=')' {
11531157
RAISE_SYNTAX_ERROR_KNOWN_RANGE(a, b, "Function parameters cannot be parenthesized") }
1158+
| a="/" ',' {
1159+
RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "at least one argument must precede /") }
1160+
| (slash_no_default | slash_with_default) param_maybe_default* a='/' {
1161+
RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "/ may appear only once") }
1162+
| (slash_no_default | slash_with_default)? param_maybe_default* '*' (',' | param_no_default) param_maybe_default* a='/' {
1163+
RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "/ must be ahead of *") }
1164+
| param_maybe_default+ '/' a='*' {
1165+
RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "expected comma between / and *") }
1166+
invalid_default:
1167+
| a='=' &(')'|',') { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "expected default value expression") }
1168+
invalid_star_etc:
1169+
| a='*' (')' | ',' (')' | '**')) { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "named arguments must follow bare *") }
1170+
| '*' ',' TYPE_COMMENT { RAISE_SYNTAX_ERROR("bare * has associated type comment") }
1171+
| '*' param a='=' { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "var-positional argument cannot have default value") }
1172+
| '*' (param_no_default | ',') param_maybe_default* a='*' (param_no_default | ',') {
1173+
RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "* argument may appear only once") }
1174+
invalid_kwds:
1175+
| '**' param a='=' { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "var-keyword argument cannot have default value") }
1176+
| '**' param ',' a=param { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "arguments cannot follow var-keyword argument") }
1177+
| '**' param ',' a[Token*]=('*'|'**'|'/') { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "arguments cannot follow var-keyword argument") }
11541178
invalid_parameters_helper: # This is only there to avoid type errors
11551179
| a=slash_with_default { _PyPegen_singleton_seq(p, a) }
11561180
| param_with_default+
@@ -1159,14 +1183,26 @@ invalid_lambda_parameters:
11591183
RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "non-default argument follows default argument") }
11601184
| lambda_param_no_default* a='(' ','.lambda_param+ ','? b=')' {
11611185
RAISE_SYNTAX_ERROR_KNOWN_RANGE(a, b, "Lambda expression parameters cannot be parenthesized") }
1186+
| a="/" ',' {
1187+
RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "at least one argument must precede /") }
1188+
| (lambda_slash_no_default | lambda_slash_with_default) lambda_param_maybe_default* a='/' {
1189+
RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "/ may appear only once") }
1190+
| (lambda_slash_no_default | lambda_slash_with_default)? lambda_param_maybe_default* '*' (',' | lambda_param_no_default) lambda_param_maybe_default* a='/' {
1191+
RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "/ must be ahead of *") }
1192+
| lambda_param_maybe_default+ '/' a='*' {
1193+
RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "expected comma between / and *") }
11621194
invalid_lambda_parameters_helper:
11631195
| a=lambda_slash_with_default { _PyPegen_singleton_seq(p, a) }
11641196
| lambda_param_with_default+
1165-
invalid_star_etc:
1166-
| a='*' (')' | ',' (')' | '**')) { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "named arguments must follow bare *") }
1167-
| '*' ',' TYPE_COMMENT { RAISE_SYNTAX_ERROR("bare * has associated type comment") }
11681197
invalid_lambda_star_etc:
11691198
| '*' (':' | ',' (':' | '**')) { RAISE_SYNTAX_ERROR("named arguments must follow bare *") }
1199+
| '*' lambda_param a='=' { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "var-positional argument cannot have default value") }
1200+
| '*' (lambda_param_no_default | ',') lambda_param_maybe_default* a='*' (lambda_param_no_default | ',') {
1201+
RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "* argument may appear only once") }
1202+
invalid_lambda_kwds:
1203+
| '**' lambda_param a='=' { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "var-keyword argument cannot have default value") }
1204+
| '**' lambda_param ',' a=lambda_param { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "arguments cannot follow var-keyword argument") }
1205+
| '**' lambda_param ',' a[Token*]=('*'|'**'|'/') { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "arguments cannot follow var-keyword argument") }
11701206
invalid_double_type_comments:
11711207
| TYPE_COMMENT NEWLINE TYPE_COMMENT NEWLINE INDENT {
11721208
RAISE_SYNTAX_ERROR("Cannot have two type comments on def") }
@@ -1269,4 +1305,4 @@ invalid_kvpair:
12691305
| a=expression !(':') {
12701306
RAISE_ERROR_KNOWN_LOCATION(p, PyExc_SyntaxError, a->lineno, a->end_col_offset - 1, a->end_lineno, -1, "':' expected after dictionary key") }
12711307
| expression ':' a='*' bitwise_or { RAISE_SYNTAX_ERROR_STARTING_FROM(a, "cannot use a starred expression in a dictionary value") }
1272-
| expression a=':' {RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "expression expected after dictionary key and ':'") }
1308+
| expression a=':' {RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "expression expected after dictionary key and ':'") }

Lib/test/test_syntax.py

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,210 @@
351351
Traceback (most recent call last):
352352
SyntaxError: invalid syntax
353353
354+
>>> def foo(/,a,b=,c):
355+
... pass
356+
Traceback (most recent call last):
357+
SyntaxError: at least one argument must precede /
358+
359+
>>> def foo(a,/,/,b,c):
360+
... pass
361+
Traceback (most recent call last):
362+
SyntaxError: / may appear only once
363+
364+
>>> def foo(a,/,a1,/,b,c):
365+
... pass
366+
Traceback (most recent call last):
367+
SyntaxError: / may appear only once
368+
369+
>>> def foo(a=1,/,/,*b,/,c):
370+
... pass
371+
Traceback (most recent call last):
372+
SyntaxError: / may appear only once
373+
374+
>>> def foo(a,/,a1=1,/,b,c):
375+
... pass
376+
Traceback (most recent call last):
377+
SyntaxError: / may appear only once
378+
379+
>>> def foo(a,*b,c,/,d,e):
380+
... pass
381+
Traceback (most recent call last):
382+
SyntaxError: / must be ahead of *
383+
384+
>>> def foo(a=1,*b,c=3,/,d,e):
385+
... pass
386+
Traceback (most recent call last):
387+
SyntaxError: / must be ahead of *
388+
389+
>>> def foo(a,*b=3,c):
390+
... pass
391+
Traceback (most recent call last):
392+
SyntaxError: var-positional argument cannot have default value
393+
394+
>>> def foo(a,*b: int=,c):
395+
... pass
396+
Traceback (most recent call last):
397+
SyntaxError: var-positional argument cannot have default value
398+
399+
>>> def foo(a,**b=3):
400+
... pass
401+
Traceback (most recent call last):
402+
SyntaxError: var-keyword argument cannot have default value
403+
404+
>>> def foo(a,**b: int=3):
405+
... pass
406+
Traceback (most recent call last):
407+
SyntaxError: var-keyword argument cannot have default value
408+
409+
>>> def foo(a,*a, b, **c, d):
410+
... pass
411+
Traceback (most recent call last):
412+
SyntaxError: arguments cannot follow var-keyword argument
413+
414+
>>> def foo(a,*a, b, **c, d=4):
415+
... pass
416+
Traceback (most recent call last):
417+
SyntaxError: arguments cannot follow var-keyword argument
418+
419+
>>> def foo(a,*a, b, **c, *d):
420+
... pass
421+
Traceback (most recent call last):
422+
SyntaxError: arguments cannot follow var-keyword argument
423+
424+
>>> def foo(a,*a, b, **c, **d):
425+
... pass
426+
Traceback (most recent call last):
427+
SyntaxError: arguments cannot follow var-keyword argument
428+
429+
>>> def foo(a=1,/,**b,/,c):
430+
... pass
431+
Traceback (most recent call last):
432+
SyntaxError: arguments cannot follow var-keyword argument
433+
434+
>>> def foo(*b,*d):
435+
... pass
436+
Traceback (most recent call last):
437+
SyntaxError: * argument may appear only once
438+
439+
>>> def foo(a,*b,c,*d,*e,c):
440+
... pass
441+
Traceback (most recent call last):
442+
SyntaxError: * argument may appear only once
443+
444+
>>> def foo(a,b,/,c,*b,c,*d,*e,c):
445+
... pass
446+
Traceback (most recent call last):
447+
SyntaxError: * argument may appear only once
448+
449+
>>> def foo(a,b,/,c,*b,c,*d,**e):
450+
... pass
451+
Traceback (most recent call last):
452+
SyntaxError: * argument may appear only once
453+
454+
>>> def foo(a=1,/*,b,c):
455+
... pass
456+
Traceback (most recent call last):
457+
SyntaxError: expected comma between / and *
458+
459+
>>> def foo(a=1,d=,c):
460+
... pass
461+
Traceback (most recent call last):
462+
SyntaxError: expected default value expression
463+
464+
>>> def foo(a,d=,c):
465+
... pass
466+
Traceback (most recent call last):
467+
SyntaxError: expected default value expression
468+
469+
>>> def foo(a,d: int=,c):
470+
... pass
471+
Traceback (most recent call last):
472+
SyntaxError: expected default value expression
473+
474+
>>> lambda /,a,b,c: None
475+
Traceback (most recent call last):
476+
SyntaxError: at least one argument must precede /
477+
478+
>>> lambda a,/,/,b,c: None
479+
Traceback (most recent call last):
480+
SyntaxError: / may appear only once
481+
482+
>>> lambda a,/,a1,/,b,c: None
483+
Traceback (most recent call last):
484+
SyntaxError: / may appear only once
485+
486+
>>> lambda a=1,/,/,*b,/,c: None
487+
Traceback (most recent call last):
488+
SyntaxError: / may appear only once
489+
490+
>>> lambda a,/,a1=1,/,b,c: None
491+
Traceback (most recent call last):
492+
SyntaxError: / may appear only once
493+
494+
>>> lambda a,*b,c,/,d,e: None
495+
Traceback (most recent call last):
496+
SyntaxError: / must be ahead of *
497+
498+
>>> lambda a=1,*b,c=3,/,d,e: None
499+
Traceback (most recent call last):
500+
SyntaxError: / must be ahead of *
501+
502+
>>> lambda a=1,/*,b,c: None
503+
Traceback (most recent call last):
504+
SyntaxError: expected comma between / and *
505+
506+
>>> lambda a,*b=3,c: None
507+
Traceback (most recent call last):
508+
SyntaxError: var-positional argument cannot have default value
509+
510+
>>> lambda a,**b=3: None
511+
Traceback (most recent call last):
512+
SyntaxError: var-keyword argument cannot have default value
513+
514+
>>> lambda a, *a, b, **c, d: None
515+
Traceback (most recent call last):
516+
SyntaxError: arguments cannot follow var-keyword argument
517+
518+
>>> lambda a,*a, b, **c, d=4: None
519+
Traceback (most recent call last):
520+
SyntaxError: arguments cannot follow var-keyword argument
521+
522+
>>> lambda a,*a, b, **c, *d: None
523+
Traceback (most recent call last):
524+
SyntaxError: arguments cannot follow var-keyword argument
525+
526+
>>> lambda a,*a, b, **c, **d: None
527+
Traceback (most recent call last):
528+
SyntaxError: arguments cannot follow var-keyword argument
529+
530+
>>> lambda a=1,/,**b,/,c: None
531+
Traceback (most recent call last):
532+
SyntaxError: arguments cannot follow var-keyword argument
533+
534+
>>> lambda *b,*d: None
535+
Traceback (most recent call last):
536+
SyntaxError: * argument may appear only once
537+
538+
>>> lambda a,*b,c,*d,*e,c: None
539+
Traceback (most recent call last):
540+
SyntaxError: * argument may appear only once
541+
542+
>>> lambda a,b,/,c,*b,c,*d,*e,c: None
543+
Traceback (most recent call last):
544+
SyntaxError: * argument may appear only once
545+
546+
>>> lambda a,b,/,c,*b,c,*d,**e: None
547+
Traceback (most recent call last):
548+
SyntaxError: * argument may appear only once
549+
550+
>>> lambda a=1,d=,c: None
551+
Traceback (most recent call last):
552+
SyntaxError: expected default value expression
553+
554+
>>> lambda a,d=,c: None
555+
Traceback (most recent call last):
556+
SyntaxError: expected default value expression
557+
354558
>>> import ast; ast.parse('''
355559
... def f(
356560
... *, # type: int
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Improve syntax errors for incorrect function definitions. Patch by Pablo
2+
Galindo

0 commit comments

Comments
 (0)