Skip to content

Commit 618f6ac

Browse files
committed
Refactor ArgumentSpec to enable positional-only support.
Changes: - Add `positional_only` attribute. - Rename `positional` to `positional_or_named`. - Rename other attributes for consistency. - Enhance ArgInfo to handle positional-only args. Actual positional-only argument support is still to-do. See robotframework#3695 for more details.
1 parent 202ae40 commit 618f6ac

16 files changed

+218
-123
lines changed

src/robot/running/arguments/argumentconverter.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,15 @@ def _convert_positional(self, positional):
3333
names = self._argspec.positional
3434
converted = [self._convert(name, value)
3535
for name, value in zip(names, positional)]
36-
if self._argspec.varargs:
37-
converted.extend(self._convert(self._argspec.varargs, value)
36+
if self._argspec.var_positional:
37+
converted.extend(self._convert(self._argspec.var_positional, value)
3838
for value in positional[len(names):])
3939
return converted
4040

4141
def _convert_named(self, named):
42-
names = set(self._argspec.positional) | set(self._argspec.kwonlyargs)
43-
kwargs = self._argspec.kwargs
44-
return [(name, self._convert(name if name in names else kwargs, value))
42+
names = set(self._argspec.positional) | set(self._argspec.named_only)
43+
var_named = self._argspec.var_named
44+
return [(name, self._convert(name if name in names else var_named, value))
4545
for name, value in named]
4646

4747
def _convert(self, name, value):

src/robot/running/arguments/argumentmapper.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,12 @@ def fill_named(self, named):
5050
if name in spec.positional and spec.supports_named:
5151
index = spec.positional.index(name)
5252
self.args[index] = value
53-
elif spec.kwargs or name in spec.kwonlyargs:
53+
elif spec.var_named or name in spec.named_only:
5454
self.kwargs.append((name, value))
5555
else:
5656
raise DataError("Non-existing named argument '%s'." % name)
5757
named_names = {name for name, _ in named}
58-
for name in spec.kwonlyargs:
58+
for name in spec.named_only:
5959
if name not in named_names:
6060
value = DefaultValue(spec.defaults[name])
6161
self.kwargs.append((name, value))

src/robot/running/arguments/argumentparser.py

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -92,39 +92,39 @@ def _format_arg_spec(self, name, positional=0, defaults=0, varargs=False,
9292
else:
9393
defaults = {}
9494
return ArgumentSpec(name, self._type,
95-
positional=positional,
96-
varargs='varargs' if varargs else None,
97-
kwargs='kwargs' if kwargs else None,
95+
positional_only=positional,
96+
var_positional='varargs' if varargs else None,
97+
var_named='kwargs' if kwargs else None,
9898
defaults=defaults,
99-
supports_named=False)
99+
supports_named=False) # FIXME: Shouldn't be needed anymore
100100

101101

102102
class _ArgumentSpecParser(_ArgumentParser):
103103

104104
def parse(self, argspec, name=None):
105105
spec = ArgumentSpec(name, self._type)
106-
kw_only_args = False
106+
named_only = False
107107
for arg in argspec:
108108
arg = self._validate_arg(arg)
109-
if spec.kwargs:
109+
if spec.var_named:
110110
self._raise_invalid_spec('Only last argument can be kwargs.')
111111
elif isinstance(arg, tuple):
112112
arg, default = arg
113-
arg = self._add_arg(spec, arg, kw_only_args)
113+
arg = self._add_arg(spec, arg, named_only)
114114
spec.defaults[arg] = default
115115
elif self._is_kwargs(arg):
116-
spec.kwargs = self._format_kwargs(arg)
116+
spec.var_named = self._format_kwargs(arg)
117117
elif self._is_varargs(arg):
118-
if kw_only_args:
118+
if named_only:
119119
self._raise_invalid_spec('Cannot have multiple varargs.')
120120
if not self._is_kw_only_separator(arg):
121-
spec.varargs = self._format_varargs(arg)
122-
kw_only_args = True
123-
elif spec.defaults and not kw_only_args:
121+
spec.var_positional = self._format_varargs(arg)
122+
named_only = True
123+
elif spec.defaults and not named_only:
124124
self._raise_invalid_spec('Non-default argument after default '
125125
'arguments.')
126126
else:
127-
self._add_arg(spec, arg, kw_only_args)
127+
self._add_arg(spec, arg, named_only)
128128
return spec
129129

130130
def _validate_arg(self, arg):
@@ -151,9 +151,9 @@ def _format_varargs(self, varargs):
151151
def _format_arg(self, arg):
152152
return arg
153153

154-
def _add_arg(self, spec, arg, kw_only_arg=False):
154+
def _add_arg(self, spec, arg, named_only=False):
155155
arg = self._format_arg(arg)
156-
target = spec.positional if not kw_only_arg else spec.kwonlyargs
156+
target = spec.positional_or_named if not named_only else spec.named_only
157157
target.append(arg)
158158
return arg
159159

src/robot/running/arguments/argumentresolver.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ def resolve(self, arguments, variables=None):
4343
class NamedArgumentResolver(object):
4444

4545
def __init__(self, argspec):
46+
""":type argspec: :py:class:`robot.running.arguments.ArgumentSpec`"""
4647
self._argspec = argspec
4748

4849
def resolve(self, arguments, variables=None):
@@ -69,7 +70,7 @@ def _is_named(self, arg, previous_named, variables=None):
6970
except DataError:
7071
return False
7172
argspec = self._argspec
72-
if previous_named or name in argspec.kwonlyargs or argspec.kwargs:
73+
if previous_named or name in argspec.named_only or argspec.var_named:
7374
return True
7475
return argspec.supports_named and name in argspec.positional
7576

@@ -88,7 +89,7 @@ class DictToKwargs(object):
8889

8990
def __init__(self, argspec, enabled=False):
9091
self._maxargs = argspec.maxargs
91-
self._enabled = enabled and bool(argspec.kwargs)
92+
self._enabled = enabled and bool(argspec.var_named)
9293

9394
def handle(self, positional, named):
9495
if self._enabled and self._extra_arg_has_kwargs(positional, named):

src/robot/running/arguments/argumentspec.py

Lines changed: 49 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -32,36 +32,45 @@ class Enum(object):
3232
@py2to3
3333
class ArgumentSpec(object):
3434

35-
def __init__(self, name=None, type='Keyword', positional=None,
36-
varargs=None, kwonlyargs=None, kwargs=None, defaults=None,
37-
types=None, supports_named=True):
35+
def __init__(self, name=None, type='Keyword', positional_only=None,
36+
positional_or_named=None, var_positional=None, named_only=None,
37+
var_named=None, defaults=None, types=None, supports_named=True):
3838
self.name = name
3939
self.type = type
40-
self.positional = positional or []
41-
self.varargs = varargs
42-
self.kwonlyargs = kwonlyargs or []
43-
self.kwargs = kwargs
40+
self.positional_only = positional_only or []
41+
self.positional_or_named = positional_or_named or []
42+
self.var_positional = var_positional
43+
self.named_only = named_only or []
44+
self.var_named = var_named
4445
self.defaults = defaults or {}
4546
self.types = types
47+
# FIXME: Shouldn't be needed anymore when positional-only are fully supported.
4648
self.supports_named = supports_named
4749

4850
@setter
4951
def types(self, types):
5052
return TypeValidator(self).validate(types)
5153

54+
@property
55+
def positional(self):
56+
return self.positional_only + self.positional_or_named
57+
5258
@property
5359
def minargs(self):
5460
required = [arg for arg in self.positional if arg not in self.defaults]
5561
return len(required)
5662

5763
@property
5864
def maxargs(self):
59-
return len(self.positional) if not self.varargs else sys.maxsize
65+
return len(self.positional) if not self.var_positional else sys.maxsize
6066

6167
@property
6268
def argument_names(self):
63-
return (self.positional + ([self.varargs] if self.varargs else []) +
64-
self.kwonlyargs + ([self.kwargs] if self.kwargs else []))
69+
return (self.positional_only +
70+
self.positional_or_named +
71+
([self.var_positional] if self.var_positional else []) +
72+
self.named_only +
73+
([self.var_named] if self.var_named else []))
6574

6675
def resolve(self, arguments, variables=None, resolve_named=True,
6776
resolve_variables_until=None, dict_to_kwargs=False):
@@ -81,20 +90,25 @@ def __iter__(self):
8190
notset = ArgInfo.NOTSET
8291
get_type = (self.types or {}).get
8392
get_default = self.defaults.get
84-
for arg in self.positional:
85-
yield ArgInfo(ArgInfo.POSITIONAL_OR_KEYWORD, arg,
93+
for arg in self.positional_only:
94+
yield ArgInfo(ArgInfo.POSITIONAL_ONLY, arg,
95+
get_type(arg, notset), get_default(arg, notset))
96+
if self.positional_only:
97+
yield ArgInfo(ArgInfo.POSITIONAL_ONLY_MARKER)
98+
for arg in self.positional_or_named:
99+
yield ArgInfo(ArgInfo.POSITIONAL_OR_NAMED, arg,
86100
get_type(arg, notset), get_default(arg, notset))
87-
if self.varargs:
88-
yield ArgInfo(ArgInfo.VAR_POSITIONAL, self.varargs,
89-
get_type(self.varargs, notset))
90-
elif self.kwonlyargs:
91-
yield ArgInfo(ArgInfo.KEYWORD_ONLY_MARKER)
92-
for arg in self.kwonlyargs:
93-
yield ArgInfo(ArgInfo.KEYWORD_ONLY, arg,
101+
if self.var_positional:
102+
yield ArgInfo(ArgInfo.VAR_POSITIONAL, self.var_positional,
103+
get_type(self.var_positional, notset))
104+
elif self.named_only:
105+
yield ArgInfo(ArgInfo.NAMED_ONLY_MARKER)
106+
for arg in self.named_only:
107+
yield ArgInfo(ArgInfo.NAMED_ONLY, arg,
94108
get_type(arg, notset), get_default(arg, notset))
95-
if self.kwargs:
96-
yield ArgInfo(ArgInfo.VAR_KEYWORD, self.kwargs,
97-
get_type(self.kwargs, notset))
109+
if self.var_named:
110+
yield ArgInfo(ArgInfo.VAR_NAMED, self.var_named,
111+
get_type(self.var_named, notset))
98112

99113
def __unicode__(self):
100114
return ', '.join(unicode(arg) for arg in self)
@@ -103,11 +117,13 @@ def __unicode__(self):
103117
@py2to3
104118
class ArgInfo(object):
105119
NOTSET = object()
106-
POSITIONAL_OR_KEYWORD = 'POSITIONAL_OR_KEYWORD'
120+
POSITIONAL_ONLY = 'POSITIONAL_ONLY'
121+
POSITIONAL_ONLY_MARKER = 'POSITIONAL_ONLY_MARKER'
122+
POSITIONAL_OR_NAMED = 'POSITIONAL_OR_NAMED'
107123
VAR_POSITIONAL = 'VAR_POSITIONAL'
108-
KEYWORD_ONLY_MARKER = 'KEYWORD_ONLY_MARKER'
109-
KEYWORD_ONLY = 'KEYWORD_ONLY'
110-
VAR_KEYWORD = 'VAR_KEYWORD'
124+
NAMED_ONLY_MARKER = 'NAMED_ONLY_MARKER'
125+
NAMED_ONLY = 'NAMED_ONLY'
126+
VAR_NAMED = 'VAR_NAMED'
111127

112128
def __init__(self, kind, name='', type=NOTSET, default=NOTSET):
113129
self.kind = kind
@@ -117,7 +133,9 @@ def __init__(self, kind, name='', type=NOTSET, default=NOTSET):
117133

118134
@property
119135
def required(self):
120-
if self.kind in (self.POSITIONAL_OR_KEYWORD, self.KEYWORD_ONLY):
136+
if self.kind in (self.POSITIONAL_ONLY,
137+
self.POSITIONAL_OR_NAMED,
138+
self.NAMED_ONLY):
121139
return self.default is self.NOTSET
122140
return False
123141

@@ -141,12 +159,14 @@ def _format_enum(self, enum):
141159
return '%s { %s }' % (enum.__name__, ' | '.join(members))
142160

143161
def __unicode__(self):
144-
if self.kind == self.KEYWORD_ONLY_MARKER:
162+
if self.kind == self.POSITIONAL_ONLY_MARKER:
163+
return '/'
164+
if self.kind == self.NAMED_ONLY_MARKER:
145165
return '*'
146166
ret = self.name
147167
if self.kind == self.VAR_POSITIONAL:
148168
ret = '*' + ret
149-
elif self.kind == self.VAR_KEYWORD:
169+
elif self.kind == self.VAR_NAMED:
150170
ret = '**' + ret
151171
if self.type is not self.NOTSET:
152172
ret = '%s: %s' % (ret, self.type_string)

src/robot/running/arguments/argumentvalidator.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,11 @@ def _raise_wrong_count(self, count, spec):
4848
minend = plural_or_not(spec.minargs)
4949
if spec.minargs == spec.maxargs:
5050
expected = '%d argument%s' % (spec.minargs, minend)
51-
elif not spec.varargs:
51+
elif not spec.var_positional:
5252
expected = '%d to %d arguments' % (spec.minargs, spec.maxargs)
5353
else:
5454
expected = 'at least %d argument%s' % (spec.minargs, minend)
55-
if spec.kwargs or spec.kwonlyargs:
55+
if spec.var_named or spec.named_only:
5656
expected = expected.replace('argument', 'non-named argument')
5757
raise DataError("%s '%s' expected %s, got %d."
5858
% (spec.type, spec.name, expected, count))
@@ -72,15 +72,15 @@ def _validate_no_mandatory_missing(self, positional, named, spec):
7272

7373
def _validate_no_named_only_missing(self, named, spec):
7474
defined = set(named) | set(spec.defaults)
75-
missing = [arg for arg in spec.kwonlyargs if arg not in defined]
75+
missing = [arg for arg in spec.named_only if arg not in defined]
7676
if missing:
7777
raise DataError("%s '%s' missing named-only argument%s %s."
7878
% (spec.type, spec.name, plural_or_not(missing),
7979
seq2str(sorted(missing))))
8080

8181
def _validate_no_extra_named(self, named, spec):
82-
if not spec.kwargs:
83-
extra = set(named) - set(spec.positional) - set(spec.kwonlyargs)
82+
if not spec.var_named:
83+
extra = set(named) - set(spec.positional) - set(spec.named_only)
8484
if extra:
8585
raise DataError("%s '%s' got unexpected named argument%s %s."
8686
% (spec.type, spec.name, plural_or_not(extra),

src/robot/running/arguments/javaargumentcoercer.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
class JavaArgumentCoercer(object):
2323

2424
def __init__(self, signatures, argspec):
25+
""":type argspec: :py:class:`robot.running.arguments.ArgumentSpec`"""
2526
self._argspec = argspec
2627
self._coercers = CoercerFinder().find_coercers(signatures)
2728
self._varargs_handler = VarargsHandler(argspec)
@@ -30,7 +31,7 @@ def coerce(self, arguments, named, dryrun=False):
3031
arguments = self._varargs_handler.handle(arguments)
3132
arguments = [c.coerce(a, dryrun)
3233
for c, a in zip(self._coercers, arguments)]
33-
if self._argspec.kwargs:
34+
if self._argspec.var_named:
3435
arguments.append(dict(named))
3536
return arguments
3637

@@ -132,7 +133,7 @@ def _coerce(self, argument):
132133
class VarargsHandler(object):
133134

134135
def __init__(self, argspec):
135-
self._index = argspec.minargs if argspec.varargs else -1
136+
self._index = argspec.minargs if argspec.var_positional else -1
136137

137138
def handle(self, arguments):
138139
if self._index > -1 and not self._passing_list(arguments):

src/robot/running/arguments/py2argumentparser.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,9 @@ def parse(self, handler, name=None):
3333
spec = ArgumentSpec(
3434
name,
3535
self._type,
36-
positional=args,
37-
varargs=varargs,
38-
kwargs=kws,
36+
positional_or_named=args,
37+
var_positional=varargs,
38+
var_named=kws,
3939
defaults=self._get_defaults(args, defaults),
4040
types=getattr(handler, 'robot_types', ())
4141
)

src/robot/running/arguments/py3argumentparser.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,10 @@ def parse(self, handler, name=None):
3131
spec = ArgumentSpec(
3232
name,
3333
self._type,
34-
positional=positional,
35-
varargs=varargs,
36-
kwonlyargs=kwonly,
37-
kwargs=kwargs,
34+
positional_or_named=positional,
35+
var_positional=varargs,
36+
named_only=kwonly,
37+
var_named=kwargs,
3838
defaults=defaults
3939
)
4040
spec.types = self._get_types(handler, spec)

src/robot/running/dynamicmethods.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,10 +125,10 @@ def _supports_java_kwargs(self, method):
125125
self._java_multi_signature_kwargs(spec))
126126

127127
def _java_single_signature_kwargs(self, spec):
128-
return len(spec.positional) == 1 and spec.varargs and spec.kwargs
128+
return len(spec.positional) == 1 and spec.var_positional and spec.var_named
129129

130130
def _java_multi_signature_kwargs(self, spec):
131-
return len(spec.positional) == 3 and not (spec.varargs or spec.kwargs)
131+
return len(spec.positional) == 3 and not (spec.var_positional or spec.var_named)
132132

133133

134134
class GetKeywordDocumentation(_DynamicMethod):

0 commit comments

Comments
 (0)