Skip to content

Commit bbd7a6c

Browse files
Support positional and keyword-only arguments in stubdoc (#18762)
Currently the signature parsing logic fails when confronted with a `/` or a `*`, rather than recognizing them as demarcating positional-only and keyword-only arguments. This patch supports parsing signatures with these features, but doesn't pass this information along to the `ArgSig` or `FunctionSig` classes, since the information would not be used anyway.
1 parent 6e21887 commit bbd7a6c

File tree

2 files changed

+202
-7
lines changed

2 files changed

+202
-7
lines changed

mypy/stubdoc.py

+44-7
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,8 @@ def __init__(self, function_name: str) -> None:
175175
self.ret_type = "Any"
176176
self.found = False
177177
self.args: list[ArgSig] = []
178+
self.pos_only: int | None = None
179+
self.keyword_only: int | None = None
178180
# Valid signatures found so far.
179181
self.signatures: list[FunctionSig] = []
180182

@@ -252,15 +254,34 @@ def add_token(self, token: tokenize.TokenInfo) -> None:
252254
self.arg_type = self.accumulator
253255
self.state.pop()
254256
elif self.state[-1] == STATE_ARGUMENT_LIST:
255-
self.arg_name = self.accumulator
256-
if not (
257-
token.string == ")" and self.accumulator.strip() == ""
258-
) and not _ARG_NAME_RE.match(self.arg_name):
259-
# Invalid argument name.
260-
self.reset()
261-
return
257+
if self.accumulator == "*":
258+
if self.keyword_only is not None:
259+
# Error condition: cannot have * twice
260+
self.reset()
261+
return
262+
self.keyword_only = len(self.args)
263+
self.accumulator = ""
264+
else:
265+
if self.accumulator.startswith("*"):
266+
self.keyword_only = len(self.args) + 1
267+
self.arg_name = self.accumulator
268+
if not (
269+
token.string == ")" and self.accumulator.strip() == ""
270+
) and not _ARG_NAME_RE.match(self.arg_name):
271+
# Invalid argument name.
272+
self.reset()
273+
return
262274

263275
if token.string == ")":
276+
if (
277+
self.state[-1] == STATE_ARGUMENT_LIST
278+
and self.keyword_only is not None
279+
and self.keyword_only == len(self.args)
280+
and not self.arg_name
281+
):
282+
# Error condition: * must be followed by arguments
283+
self.reset()
284+
return
264285
self.state.pop()
265286

266287
# arg_name is empty when there are no args. e.g. func()
@@ -280,6 +301,22 @@ def add_token(self, token: tokenize.TokenInfo) -> None:
280301
self.arg_type = None
281302
self.arg_default = None
282303
self.accumulator = ""
304+
elif (
305+
token.type == tokenize.OP
306+
and token.string == "/"
307+
and self.state[-1] == STATE_ARGUMENT_LIST
308+
):
309+
if token.string == "/":
310+
if self.pos_only is not None or self.keyword_only is not None or not self.args:
311+
# Error cases:
312+
# - / shows up more than once
313+
# - / shows up after *
314+
# - / shows up before any arguments
315+
self.reset()
316+
return
317+
self.pos_only = len(self.args)
318+
self.state.append(STATE_ARGUMENT_TYPE)
319+
self.accumulator = ""
283320

284321
elif token.type == tokenize.OP and token.string == "->" and self.state[-1] == STATE_INIT:
285322
self.accumulator = ""

mypy/test/teststubgen.py

+158
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,164 @@ def test_infer_sig_from_docstring_bad_indentation(self) -> None:
399399
None,
400400
)
401401

402+
def test_infer_sig_from_docstring_args_kwargs(self) -> None:
403+
assert_equal(
404+
infer_sig_from_docstring("func(*args, **kwargs) -> int", "func"),
405+
[
406+
FunctionSig(
407+
name="func",
408+
args=[ArgSig(name="*args"), ArgSig(name="**kwargs")],
409+
ret_type="int",
410+
)
411+
],
412+
)
413+
414+
assert_equal(
415+
infer_sig_from_docstring("func(*args) -> int", "func"),
416+
[FunctionSig(name="func", args=[ArgSig(name="*args")], ret_type="int")],
417+
)
418+
419+
assert_equal(
420+
infer_sig_from_docstring("func(**kwargs) -> int", "func"),
421+
[FunctionSig(name="func", args=[ArgSig(name="**kwargs")], ret_type="int")],
422+
)
423+
424+
@pytest.mark.xfail(
425+
raises=AssertionError, reason="Arg and kwarg signature validation not implemented yet"
426+
)
427+
def test_infer_sig_from_docstring_args_kwargs_errors(self) -> None:
428+
# Double args
429+
assert_equal(infer_sig_from_docstring("func(*args, *args2) -> int", "func"), [])
430+
431+
# Double kwargs
432+
assert_equal(infer_sig_from_docstring("func(**kw, **kw2) -> int", "func"), [])
433+
434+
# args after kwargs
435+
assert_equal(infer_sig_from_docstring("func(**kwargs, *args) -> int", "func"), [])
436+
437+
def test_infer_sig_from_docstring_positional_only_arguments(self) -> None:
438+
assert_equal(
439+
infer_sig_from_docstring("func(self, /) -> str", "func"),
440+
[FunctionSig(name="func", args=[ArgSig(name="self")], ret_type="str")],
441+
)
442+
443+
assert_equal(
444+
infer_sig_from_docstring("func(self, x, /) -> str", "func"),
445+
[
446+
FunctionSig(
447+
name="func", args=[ArgSig(name="self"), ArgSig(name="x")], ret_type="str"
448+
)
449+
],
450+
)
451+
452+
assert_equal(
453+
infer_sig_from_docstring("func(x, /, y) -> int", "func"),
454+
[FunctionSig(name="func", args=[ArgSig(name="x"), ArgSig(name="y")], ret_type="int")],
455+
)
456+
457+
assert_equal(
458+
infer_sig_from_docstring("func(x, /, *args) -> str", "func"),
459+
[
460+
FunctionSig(
461+
name="func", args=[ArgSig(name="x"), ArgSig(name="*args")], ret_type="str"
462+
)
463+
],
464+
)
465+
466+
assert_equal(
467+
infer_sig_from_docstring("func(x, /, *, kwonly, **kwargs) -> str", "func"),
468+
[
469+
FunctionSig(
470+
name="func",
471+
args=[ArgSig(name="x"), ArgSig(name="kwonly"), ArgSig(name="**kwargs")],
472+
ret_type="str",
473+
)
474+
],
475+
)
476+
477+
def test_infer_sig_from_docstring_keyword_only_arguments(self) -> None:
478+
assert_equal(
479+
infer_sig_from_docstring("func(*, x) -> str", "func"),
480+
[FunctionSig(name="func", args=[ArgSig(name="x")], ret_type="str")],
481+
)
482+
483+
assert_equal(
484+
infer_sig_from_docstring("func(x, *, y) -> str", "func"),
485+
[FunctionSig(name="func", args=[ArgSig(name="x"), ArgSig(name="y")], ret_type="str")],
486+
)
487+
488+
assert_equal(
489+
infer_sig_from_docstring("func(*, x, y) -> str", "func"),
490+
[FunctionSig(name="func", args=[ArgSig(name="x"), ArgSig(name="y")], ret_type="str")],
491+
)
492+
493+
assert_equal(
494+
infer_sig_from_docstring("func(x, *, kwonly, **kwargs) -> str", "func"),
495+
[
496+
FunctionSig(
497+
name="func",
498+
args=[ArgSig(name="x"), ArgSig(name="kwonly"), ArgSig("**kwargs")],
499+
ret_type="str",
500+
)
501+
],
502+
)
503+
504+
def test_infer_sig_from_docstring_pos_only_and_keyword_only_arguments(self) -> None:
505+
assert_equal(
506+
infer_sig_from_docstring("func(x, /, *, y) -> str", "func"),
507+
[FunctionSig(name="func", args=[ArgSig(name="x"), ArgSig(name="y")], ret_type="str")],
508+
)
509+
510+
assert_equal(
511+
infer_sig_from_docstring("func(x, /, y, *, z) -> str", "func"),
512+
[
513+
FunctionSig(
514+
name="func",
515+
args=[ArgSig(name="x"), ArgSig(name="y"), ArgSig(name="z")],
516+
ret_type="str",
517+
)
518+
],
519+
)
520+
521+
assert_equal(
522+
infer_sig_from_docstring("func(x, /, y, *, z, **kwargs) -> str", "func"),
523+
[
524+
FunctionSig(
525+
name="func",
526+
args=[
527+
ArgSig(name="x"),
528+
ArgSig(name="y"),
529+
ArgSig(name="z"),
530+
ArgSig("**kwargs"),
531+
],
532+
ret_type="str",
533+
)
534+
],
535+
)
536+
537+
def test_infer_sig_from_docstring_pos_only_and_keyword_only_arguments_errors(self) -> None:
538+
# / as first argument
539+
assert_equal(infer_sig_from_docstring("func(/, x) -> str", "func"), [])
540+
541+
# * as last argument
542+
assert_equal(infer_sig_from_docstring("func(x, *) -> str", "func"), [])
543+
544+
# / after *
545+
assert_equal(infer_sig_from_docstring("func(x, *, /, y) -> str", "func"), [])
546+
547+
# Two /
548+
assert_equal(infer_sig_from_docstring("func(x, /, /, *, y) -> str", "func"), [])
549+
550+
assert_equal(infer_sig_from_docstring("func(x, /, y, /, *, z) -> str", "func"), [])
551+
552+
# Two *
553+
assert_equal(infer_sig_from_docstring("func(x, /, *, *, y) -> str", "func"), [])
554+
555+
assert_equal(infer_sig_from_docstring("func(x, /, *, y, *, z) -> str", "func"), [])
556+
557+
# *args and * are not allowed
558+
assert_equal(infer_sig_from_docstring("func(*args, *, kwonly) -> str", "func"), [])
559+
402560
def test_infer_arg_sig_from_anon_docstring(self) -> None:
403561
assert_equal(
404562
infer_arg_sig_from_anon_docstring("(*args, **kwargs)"),

0 commit comments

Comments
 (0)