Skip to content

Commit 80fb6d0

Browse files
Support positional and keyword-only arguments
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 b1ac028 commit 80fb6d0

File tree

2 files changed

+105
-0
lines changed

2 files changed

+105
-0
lines changed

mypy/stubdoc.py

Lines changed: 32 additions & 0 deletions
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

@@ -261,6 +263,15 @@ def add_token(self, token: tokenize.TokenInfo) -> None:
261263
return
262264

263265
if token.string == ")":
266+
if (
267+
self.state[-1] == STATE_ARGUMENT_LIST
268+
and self.keyword_only is not None
269+
and self.keyword_only == len(self.args)
270+
and not self.arg_name
271+
):
272+
# Error condition: * must be followed by arguments
273+
self.reset()
274+
return
264275
self.state.pop()
265276

266277
# arg_name is empty when there are no args. e.g. func()
@@ -280,6 +291,27 @@ def add_token(self, token: tokenize.TokenInfo) -> None:
280291
self.arg_type = None
281292
self.arg_default = None
282293
self.accumulator = ""
294+
elif (
295+
token.type == tokenize.OP
296+
and (token.string in {"*", "/"})
297+
and self.state[-1] == STATE_ARGUMENT_LIST
298+
):
299+
if token.string == "/":
300+
if self.pos_only is not None or self.keyword_only is not None or not self.args:
301+
# Error cases:
302+
# - / shows up more than once
303+
# - / shows up after *
304+
# - / shows up before any arguments
305+
self.reset()
306+
return
307+
self.pos_only = len(self.args)
308+
else:
309+
if self.keyword_only is not None:
310+
# * is not allowed after *
311+
self.reset()
312+
return
313+
self.keyword_only = len(self.args)
314+
self.state.append(STATE_ARGUMENT_TYPE)
283315

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

mypy/test/teststubgen.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,79 @@ def test_infer_sig_from_docstring_bad_indentation(self) -> None:
399399
None,
400400
)
401401

402+
def test_infer_sig_from_docstring_positional_only_arguments(self) -> None:
403+
assert_equal(
404+
infer_sig_from_docstring("func(self, /) -> str", "func"),
405+
[FunctionSig(name="func", args=[ArgSig(name="self")], ret_type="str")],
406+
)
407+
408+
assert_equal(
409+
infer_sig_from_docstring("func(self, x, /) -> str", "func"),
410+
[
411+
FunctionSig(
412+
name="func", args=[ArgSig(name="self"), ArgSig(name="x")], ret_type="str"
413+
)
414+
],
415+
)
416+
417+
assert_equal(
418+
infer_sig_from_docstring("func(x, /, y) -> int", "func"),
419+
[FunctionSig(name="func", args=[ArgSig(name="x"), ArgSig(name="y")], ret_type="int")],
420+
)
421+
422+
def test_infer_sig_from_docstring_keyword_only_arguments(self) -> None:
423+
assert_equal(
424+
infer_sig_from_docstring("func(*, x) -> str", "func"),
425+
[FunctionSig(name="func", args=[ArgSig(name="x")], ret_type="str")],
426+
)
427+
428+
assert_equal(
429+
infer_sig_from_docstring("func(x, *, y) -> str", "func"),
430+
[FunctionSig(name="func", args=[ArgSig(name="x"), ArgSig(name="y")], ret_type="str")],
431+
)
432+
433+
assert_equal(
434+
infer_sig_from_docstring("func(*, x, y) -> str", "func"),
435+
[FunctionSig(name="func", args=[ArgSig(name="x"), ArgSig(name="y")], ret_type="str")],
436+
)
437+
438+
def test_infer_sig_from_docstring_pos_only_and_keyword_only_arguments(self) -> None:
439+
assert_equal(
440+
infer_sig_from_docstring("func(x, /, *, y) -> str", "func"),
441+
[FunctionSig(name="func", args=[ArgSig(name="x"), ArgSig(name="y")], ret_type="str")],
442+
)
443+
444+
assert_equal(
445+
infer_sig_from_docstring("func(x, /, y, *, z) -> str", "func"),
446+
[
447+
FunctionSig(
448+
name="func",
449+
args=[ArgSig(name="x"), ArgSig(name="y"), ArgSig(name="z")],
450+
ret_type="str",
451+
)
452+
],
453+
)
454+
455+
def test_infer_sig_from_docstring_pos_only_and_keyword_only_arguments_errors(self) -> None:
456+
# / as first argument
457+
assert_equal(infer_sig_from_docstring("func(/, x) -> str", "func"), [])
458+
459+
# * as last argument
460+
assert_equal(infer_sig_from_docstring("func(x, *) -> str", "func"), [])
461+
462+
# / after *
463+
assert_equal(infer_sig_from_docstring("func(x, *, /, y) -> str", "func"), [])
464+
465+
# Two /
466+
assert_equal(infer_sig_from_docstring("func(x, /, /, *, y) -> str", "func"), [])
467+
468+
assert_equal(infer_sig_from_docstring("func(x, /, y, /, *, z) -> str", "func"), [])
469+
470+
# Two *
471+
assert_equal(infer_sig_from_docstring("func(x, /, *, *, y) -> str", "func"), [])
472+
473+
assert_equal(infer_sig_from_docstring("func(x, /, *, y, *, z) -> str", "func"), [])
474+
402475
def test_infer_arg_sig_from_anon_docstring(self) -> None:
403476
assert_equal(
404477
infer_arg_sig_from_anon_docstring("(*args, **kwargs)"),

0 commit comments

Comments
 (0)