Skip to content

Commit c4e28ce

Browse files
committed
Add positional-only argument support. robotframework#3695
Adding this support allowed removing ArgumentSpec.supports_named and in the end more code was removed than added.
1 parent 618f6ac commit c4e28ce

File tree

11 files changed

+180
-63
lines changed

11 files changed

+180
-63
lines changed

atest/interpreter.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ def _platform_excludes(self):
109109
yield 'require-py3.5'
110110
if self.version_info < (3, 7):
111111
yield 'require-py3.7'
112+
if self.version_info < (3, 8):
113+
yield 'require-py3.8'
112114
if self.is_windows:
113115
yield 'no-windows'
114116
if self.is_jython:
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
*** Settings ***
2+
Suite Setup Run Tests ${EMPTY} keywords/positional_only_args.robot
3+
Force Tags require-py3.8
4+
Resource atest_resource.robot
5+
6+
*** Test Cases ***
7+
Normal usage
8+
Check Test Case ${TESTNAME}
9+
10+
Named syntax is not used
11+
Check Test Case ${TESTNAME}
12+
13+
Default values
14+
Check Test Case ${TESTNAME}
15+
16+
Type conversion
17+
Check Test Case ${TESTNAME}
18+
19+
Too few arguments
20+
Check Test Case ${TESTNAME} 1
21+
Check Test Case ${TESTNAME} 2
22+
23+
Too many arguments
24+
Check Test Case ${TESTNAME} 1
25+
Check Test Case ${TESTNAME} 2
26+
27+
Named argument syntax doesn't work after valid named arguments
28+
Check Test Case ${TESTNAME}
29+
30+
Name can be used with kwargs
31+
Check Test Case ${TESTNAME}
32+
33+
Mandatory positional-only missing with kwargs
34+
Check Test Case ${TESTNAME}

atest/robot/libdoc/python_library.robot

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,14 @@ Keyword-only Arguments
9999
Keyword Arguments Should Be 0 * kwo
100100
Keyword Arguments Should Be 1 *varargs kwo another=default
101101

102+
Positional-only Arguments
103+
[Tags] require-py3.8
104+
Run Libdoc And Parse Output ${DATADIR}/keywords/PositionalOnly.py
105+
Keyword Arguments Should Be 2 arg /
106+
Keyword Arguments Should Be 5 posonly / normal
107+
Keyword Arguments Should Be 0 required optional=default /
108+
Keyword Arguments Should Be 4 first: int second: float /
109+
102110
Decorators
103111
Run Libdoc And Parse Output ${TESTDATADIR}/Decorators.py
104112
Keyword Name Should Be 0 Keyword Using Decorator
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
def one_argument(arg, /):
2+
return arg.upper()
3+
4+
5+
def three_arguments(a, b, c, /):
6+
return '-'.join([a, b, c])
7+
8+
9+
def with_normal(posonly, /, normal):
10+
return posonly + '-' + normal
11+
12+
13+
def defaults(required, optional='default', /):
14+
return required + '-' + optional
15+
16+
17+
def types(first: int, second: float, /):
18+
return first + second
19+
20+
21+
def kwargs(x, /, **y):
22+
return '%s, %s' % (x, ', '.join('%s: %s' % item for item in y.items()))
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
*** Settings ***
2+
Library PositionalOnly.py
3+
Force Tags require-py3.8
4+
5+
*** Test Cases ***
6+
Normal usage
7+
${result} = One argument arg
8+
Should be equal ${result} ARG
9+
${result} = Three arguments 1 2 3
10+
Should be equal ${result} 1-2-3
11+
${result} = With normal foo bar
12+
Should be equal ${result} foo-bar
13+
${result} = With normal foo normal=bar
14+
Should be equal ${result} foo-bar
15+
16+
Named syntax is not used
17+
${result} = One argument what=ever
18+
Should be equal ${result} WHAT=EVER
19+
${result} = One argument arg=arg
20+
Should be equal ${result} ARG=ARG
21+
${result} = With normal posonly=foo bar
22+
Should be equal ${result} posonly=foo-bar
23+
${result} = With normal posonly=foo normal=bar
24+
Should be equal ${result} posonly=foo-bar
25+
26+
Default values
27+
${result} = Defaults first
28+
Should be equal ${result} first-default
29+
${result} = Defaults first second
30+
Should be equal ${result} first-second
31+
32+
Type conversion
33+
${result} = Types 1 2.5
34+
Should be equal ${result} ${3.5}
35+
36+
Too few arguments 1
37+
[Documentation] FAIL Keyword 'PositionalOnly.Three Arguments' expected 3 arguments, got 2.
38+
Three arguments 1 2
39+
40+
Too few arguments 2
41+
[Documentation] FAIL Keyword 'PositionalOnly.Defaults' expected 1 to 2 arguments, got 0.
42+
Defaults
43+
44+
Too many arguments 1
45+
[Documentation] FAIL Keyword 'PositionalOnly.One Argument' expected 1 argument, got 3.
46+
One argument too many args
47+
48+
Too many arguments 2
49+
[Documentation] FAIL Keyword 'PositionalOnly.With Normal' expected 2 arguments, got 3.
50+
With normal too many args
51+
52+
Named argument syntax doesn't work after valid named arguments
53+
[Documentation] FAIL Keyword 'PositionalOnly.With Normal' does not accept argument 'posonly' as named argument.
54+
With normal normal=would work posonly=fails
55+
56+
Name can be used with kwargs
57+
${result} = Kwargs posonly x=1 y=2
58+
Should be equal ${result} posonly, x: 1, y: 2
59+
60+
Mandatory positional-only missing with kwargs
61+
[Documentation] FAIL Keyword 'PositionalOnly.Kwargs' expected 1 non-named argument, got 0.
62+
Kwargs x=1

src/robot/running/arguments/argumentmapper.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ def fill_positional(self, positional):
4747
def fill_named(self, named):
4848
spec = self._argspec
4949
for name, value in named:
50-
if name in spec.positional and spec.supports_named:
51-
index = spec.positional.index(name)
50+
if name in spec.positional_or_named:
51+
index = spec.positional_or_named.index(name)
5252
self.args[index] = value
5353
elif spec.var_named or name in spec.named_only:
5454
self.kwargs.append((name, value))

src/robot/running/arguments/argumentparser.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,7 @@ def _format_arg_spec(self, name, positional=0, defaults=0, varargs=False,
9595
positional_only=positional,
9696
var_positional='varargs' if varargs else None,
9797
var_named='kwargs' if kwargs else None,
98-
defaults=defaults,
99-
supports_named=False) # FIXME: Shouldn't be needed anymore
98+
defaults=defaults)
10099

101100

102101
class _ArgumentSpecParser(_ArgumentParser):

src/robot/running/arguments/argumentresolver.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,11 @@ def _is_named(self, arg, previous_named, variables=None):
6969
name = variables.replace_scalar(name)
7070
except DataError:
7171
return False
72-
argspec = self._argspec
73-
if previous_named or name in argspec.named_only or argspec.var_named:
74-
return True
75-
return argspec.supports_named and name in argspec.positional
72+
spec = self._argspec
73+
return bool(previous_named or
74+
spec.var_named or
75+
name in spec.positional_or_named or
76+
name in spec.named_only)
7677

7778
def _raise_positional_after_named(self):
7879
raise DataError("%s '%s' got positional argument after named arguments."

src/robot/running/arguments/argumentspec.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ class ArgumentSpec(object):
3434

3535
def __init__(self, name=None, type='Keyword', positional_only=None,
3636
positional_or_named=None, var_positional=None, named_only=None,
37-
var_named=None, defaults=None, types=None, supports_named=True):
37+
var_named=None, defaults=None, types=None):
3838
self.name = name
3939
self.type = type
4040
self.positional_only = positional_only or []
@@ -44,8 +44,6 @@ def __init__(self, name=None, type='Keyword', positional_only=None,
4444
self.var_named = var_named
4545
self.defaults = defaults or {}
4646
self.types = types
47-
# FIXME: Shouldn't be needed anymore when positional-only are fully supported.
48-
self.supports_named = supports_named
4947

5048
@setter
5149
def types(self, types):

src/robot/running/arguments/argumentvalidator.py

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,20 +29,32 @@ def validate(self, positional, named, dryrun=False):
2929
return
3030
named = set(name for name, value in named)
3131
self._validate_no_multiple_values(positional, named, self._argspec)
32+
self._validate_no_positional_only_as_named(named, self._argspec)
3233
self._validate_positional_limits(positional, named, self._argspec)
3334
self._validate_no_mandatory_missing(positional, named, self._argspec)
3435
self._validate_no_named_only_missing(named, self._argspec)
3536
self._validate_no_extra_named(named, self._argspec)
3637

38+
def _validate_no_multiple_values(self, positional, named, spec):
39+
for name in (spec.positional_only + spec.positional_or_named)[:len(positional)]:
40+
if name in spec.positional_or_named and name in named:
41+
raise DataError("%s '%s' got multiple values for argument "
42+
"'%s'." % (spec.type, spec.name, name))
43+
44+
def _validate_no_positional_only_as_named(self, named, spec):
45+
if not spec.var_named:
46+
for name in named:
47+
if name in spec.positional_only:
48+
raise DataError("%s '%s' does not accept argument '%s' as named "
49+
"argument." % (spec.type, spec.name, name))
50+
3751
def _validate_positional_limits(self, positional, named, spec):
3852
count = len(positional) + self._named_positionals(named, spec)
3953
if not spec.minargs <= count <= spec.maxargs:
4054
self._raise_wrong_count(count, spec)
4155

4256
def _named_positionals(self, named, spec):
43-
if not spec.supports_named:
44-
return 0
45-
return sum(1 for n in named if n in spec.positional)
57+
return sum(1 for n in named if n in spec.positional_or_named)
4658

4759
def _raise_wrong_count(self, count, spec):
4860
minend = plural_or_not(spec.minargs)
@@ -57,13 +69,6 @@ def _raise_wrong_count(self, count, spec):
5769
raise DataError("%s '%s' expected %s, got %d."
5870
% (spec.type, spec.name, expected, count))
5971

60-
def _validate_no_multiple_values(self, positional, named, spec):
61-
if named and spec.supports_named:
62-
for name in spec.positional[:len(positional)]:
63-
if name in named:
64-
raise DataError("%s '%s' got multiple values for argument "
65-
"'%s'." % (spec.type, spec.name, name))
66-
6772
def _validate_no_mandatory_missing(self, positional, named, spec):
6873
for name in spec.positional[len(positional):spec.minargs]:
6974
if name not in named:
@@ -80,7 +85,7 @@ def _validate_no_named_only_missing(self, named, spec):
8085

8186
def _validate_no_extra_named(self, named, spec):
8287
if not spec.var_named:
83-
extra = set(named) - set(spec.positional) - set(spec.named_only)
88+
extra = set(named) - set(spec.positional_or_named) - set(spec.named_only)
8489
if extra:
8590
raise DataError("%s '%s' got unexpected named argument%s %s."
8691
% (spec.type, spec.name, plural_or_not(extra),

0 commit comments

Comments
 (0)