Skip to content

Commit 336df0d

Browse files
committed
Make ArgumentSpec iterable so that it yields ArgInfo objects
ArgInfo objects are easy to use for Libdoc when it needs to work with each argument. Helps implementing robotframework#3578 and robotframework#3586.
1 parent c5b4eca commit 336df0d

File tree

3 files changed

+200
-49
lines changed

3 files changed

+200
-49
lines changed

src/robot/libdocpkg/robotbuilder.py

Lines changed: 2 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,13 @@
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
1515

16-
from inspect import isclass
1716
import os
1817
import sys
19-
try:
20-
from enum import Enum
21-
except ImportError: # Standard in Py 3.4+ but can be separately installed
22-
class Enum(object):
23-
pass
2418

2519
from robot.errors import DataError
2620
from robot.running import (TestLibrary, UserLibrary, UserErrorHandler,
2721
ResourceFileBuilder)
28-
from robot.utils import split_tags_from_doc, unescape, unic
22+
from robot.utils import split_tags_from_doc, unescape, unicode
2923

3024
from .model import LibraryDoc, KeywordDoc
3125

@@ -111,7 +105,7 @@ def build_keywords(self, lib):
111105
def build_keyword(self, kw):
112106
doc, tags = self._get_doc_and_tags(kw)
113107
return KeywordDoc(name=kw.name,
114-
args=self._get_args(kw.arguments),
108+
args=[unicode(arg) for arg in kw.arguments],
115109
doc=doc,
116110
tags=tags,
117111
source=kw.source,
@@ -126,43 +120,3 @@ def _get_doc(self, kw):
126120
if self._resource and not isinstance(kw, UserErrorHandler):
127121
return unescape(kw.doc)
128122
return kw.doc
129-
130-
def _get_args(self, argspec):
131-
""":type argspec: :py:class:`robot.running.arguments.ArgumentSpec`"""
132-
args = [self._format_arg(arg, argspec) for arg in argspec.positional]
133-
if argspec.varargs:
134-
args.append('*%s' % self._format_arg(argspec.varargs, argspec))
135-
if argspec.kwonlyargs:
136-
if not argspec.varargs:
137-
args.append('*')
138-
args.extend(self._format_arg(arg, argspec)
139-
for arg in argspec.kwonlyargs)
140-
if argspec.kwargs:
141-
args.append('**%s' % self._format_arg(argspec.kwargs, argspec))
142-
return args
143-
144-
def _format_arg(self, arg, argspec):
145-
result = arg
146-
if argspec.types is not None and arg in argspec.types:
147-
type_info = argspec.types[arg]
148-
result = '%s: %s' % (result, self._format_type(type_info))
149-
if isclass(type_info) and issubclass(type_info, Enum):
150-
result = '%s { %s }' % (result, self._format_enum(type_info))
151-
default_format = '%s = %s'
152-
else:
153-
default_format = '%s=%s'
154-
if arg in argspec.defaults:
155-
result = default_format % (result, unic(argspec.defaults[arg]))
156-
return result
157-
158-
def _format_type(self, type_info):
159-
return type_info.__name__ if isclass(type_info) else type_info
160-
161-
def _format_enum(self, enum):
162-
try:
163-
members = list(enum.__members__)
164-
except AttributeError: # old enum module
165-
members = [attr for attr in dir(enum) if not attr.startswith('_')]
166-
while len(members) > 3 and len(' | '.join(members)) > 42:
167-
members[-2:] = ['...']
168-
return ' | '.join(members)

src/robot/running/arguments/argumentspec.py

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,23 @@
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
1515

16+
from inspect import isclass
1617
import sys
18+
try:
19+
from enum import Enum
20+
except ImportError: # Standard in Py 3.4+ but can be separately installed
21+
class Enum(object):
22+
pass
1723

18-
from robot.utils import setter
24+
from robot.utils import setter, py2to3, unicode, unic
1925

2026
from .argumentconverter import ArgumentConverter
2127
from .argumentmapper import ArgumentMapper
2228
from .argumentresolver import ArgumentResolver
2329
from .typevalidator import TypeValidator
2430

2531

32+
@py2to3
2633
class ArgumentSpec(object):
2734

2835
def __init__(self, name=None, type='Keyword', positional=None,
@@ -69,3 +76,83 @@ def resolve(self, arguments, variables=None, resolve_named=True,
6976
def map(self, positional, named, replace_defaults=True):
7077
mapper = ArgumentMapper(self)
7178
return mapper.map(positional, named, replace_defaults)
79+
80+
def __iter__(self):
81+
notset = ArgInfo.NOTSET
82+
get_type = (self.types or {}).get
83+
get_default = self.defaults.get
84+
for arg in self.positional:
85+
yield ArgInfo(ArgInfo.POSITIONAL_OR_KEYWORD, arg,
86+
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,
94+
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))
98+
99+
def __unicode__(self):
100+
return ', '.join(unicode(arg) for arg in self)
101+
102+
103+
@py2to3
104+
class ArgInfo(object):
105+
NOTSET = object()
106+
POSITIONAL_OR_KEYWORD = 'POSITIONAL_OR_KEYWORD'
107+
VAR_POSITIONAL = 'VAR_POSITIONAL'
108+
KEYWORD_ONLY_MARKER = 'KEYWORD_ONLY_MARKER'
109+
KEYWORD_ONLY = 'KEYWORD_ONLY'
110+
VAR_KEYWORD = 'VAR_KEYWORD'
111+
112+
def __init__(self, kind, name='', type=NOTSET, default=NOTSET):
113+
self.kind = kind
114+
self.name = name
115+
self.type = type
116+
self.default = default
117+
118+
@property
119+
def required(self):
120+
if self.kind in (self.POSITIONAL_OR_KEYWORD, self.KEYWORD_ONLY):
121+
return self.default is self.NOTSET
122+
return False
123+
124+
@property
125+
def type_string(self):
126+
if self.type is self.NOTSET:
127+
return 'NOTSET'
128+
if not isclass(self.type):
129+
return self.type
130+
if issubclass(self.type, Enum):
131+
return self._format_enum(self.type)
132+
return self.type.__name__
133+
134+
def _format_enum(self, enum):
135+
try:
136+
members = list(enum.__members__)
137+
except AttributeError: # old enum module
138+
members = [attr for attr in dir(enum) if not attr.startswith('_')]
139+
while len(members) > 3 and len(' | '.join(members)) > 42:
140+
members[-2:] = ['...']
141+
return '%s { %s }' % (enum.__name__, ' | '.join(members))
142+
143+
def __unicode__(self):
144+
if self.kind == self.KEYWORD_ONLY_MARKER:
145+
return '*'
146+
ret = self.name
147+
if self.kind == self.VAR_POSITIONAL:
148+
ret = '*' + ret
149+
elif self.kind == self.VAR_KEYWORD:
150+
ret = '**' + ret
151+
if self.type is not self.NOTSET:
152+
ret = '%s: %s' % (ret, self.type_string)
153+
default_sep = ' = '
154+
else:
155+
default_sep = '='
156+
if self.default is not self.NOTSET:
157+
ret = '%s%s%s' % (ret, default_sep, unic(self.default))
158+
return ret

utest/running/test_argumentspec.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# encoding=utf-8
2+
3+
import unittest
4+
from enum import Enum
5+
6+
from robot.running.arguments.argumentspec import ArgumentSpec, ArgInfo
7+
from robot.utils.asserts import assert_equal
8+
from robot.utils import unicode
9+
10+
11+
class TestStringRepr(unittest.TestCase):
12+
13+
def test_empty(self):
14+
self._verify('')
15+
16+
def test_normal_only(self):
17+
self._verify('a, b', positional=['a', 'b'])
18+
19+
def test_non_ascii_names(self):
20+
self._verify(u'nön, äscii', positional=[u'nön', u'äscii'])
21+
22+
def test_default(self):
23+
self._verify('a, b=c', positional=['a', 'b'], defaults={'b': 'c'})
24+
self._verify(u'nön=äscii', positional=[u'nön'], defaults={u'nön': u'äscii'})
25+
self._verify('i=42', positional=['i'], defaults={'i': 42})
26+
27+
def test_default_as_bytes(self):
28+
self._verify('b=ytes', positional=['b'], defaults={'b': b'ytes'})
29+
self._verify(u'ä=\\xe4', positional=[u'ä'], defaults={u'ä': b'\xe4'})
30+
31+
def test_type_as_class(self):
32+
self._verify('a: int, b: bool', positional=['a', 'b'],
33+
types={'a': int, 'b': bool})
34+
35+
def test_type_as_string(self):
36+
self._verify('a: Integer, b: Boolean', positional=['a', 'b'],
37+
types={'a': 'Integer', 'b': 'Boolean'})
38+
39+
def test_type_and_default(self):
40+
self._verify('arg: int = 1', positional=['arg'], types=[int],
41+
defaults={'arg': 1})
42+
43+
def test_varargs(self):
44+
self._verify('*varargs', varargs='varargs')
45+
self._verify('a, *b', positional=['a'], varargs='b')
46+
47+
def test_kwonly_without_varargs(self):
48+
self._verify('*, kwo', kwonlyargs=['kwo'])
49+
50+
def test_kwonly_with_varargs(self):
51+
self._verify('*varargs, k1, k2', varargs='varargs', kwonlyargs=['k1', 'k2'])
52+
53+
def test_kwonly_with_default(self):
54+
self._verify('*, k=1, w, o=3', kwonlyargs=['k', 'w', 'o'], defaults={'k': 1, 'o': 3})
55+
56+
def test_kwargs(self):
57+
self._verify('**kws', kwargs='kws')
58+
self._verify('a, b=c, *d, e=f, g, **h', positional=['a', 'b'], varargs='d',
59+
kwonlyargs=['e', 'g'], kwargs='h', defaults={'b': 'c', 'e': 'f'})
60+
61+
def test_enum_with_few_members(self):
62+
class Small(Enum):
63+
ONLY_FEW_MEMBERS = 1
64+
SO_THEY_CAN = 2
65+
BE_PRETTY_LONG = 3
66+
self._verify('e: Small { ONLY_FEW_MEMBERS | SO_THEY_CAN | BE_PRETTY_LONG }',
67+
positional=['e'], types=[Small])
68+
69+
def test_enum_with_many_short_members(self):
70+
class ManyShort(Enum):
71+
ONE = 1
72+
TWO = 2
73+
THREE = 3
74+
FOUR = 4
75+
FIVE = 5
76+
SIX = 6
77+
self._verify('e: ManyShort { ONE | TWO | THREE | FOUR | FIVE | SIX }',
78+
positional=['e'], types=[ManyShort])
79+
80+
def test_enum_with_many_long_members(self):
81+
class Big(Enum):
82+
MANY_MEMBERS = 1
83+
THAT_ARE_LONGISH = 2
84+
MEANS_THEY_ALL_DO_NOT_FIT = 3
85+
AND_SOME_ARE_OMITTED = 4
86+
FROM_THE_END = 5
87+
self._verify('e: Big { MANY_MEMBERS | THAT_ARE_LONGISH | ... }',
88+
positional=['e'], types=[Big])
89+
90+
def _verify(self, expected, **spec):
91+
assert_equal(unicode(ArgumentSpec(**spec)), expected)
92+
93+
94+
class TestArgInfo(unittest.TestCase):
95+
96+
def test_required_without_default(self):
97+
for kind in (ArgInfo.POSITIONAL_OR_KEYWORD,
98+
ArgInfo.KEYWORD_ONLY):
99+
assert_equal(ArgInfo(kind).required, True)
100+
assert_equal(ArgInfo(kind, default=None).required, False)
101+
102+
def test_never_required(self):
103+
for kind in (ArgInfo.VAR_POSITIONAL,
104+
ArgInfo.VAR_KEYWORD,
105+
ArgInfo.KEYWORD_ONLY_MARKER):
106+
assert_equal(ArgInfo(kind).required, False)
107+
108+
109+
if __name__ == '__main__':
110+
unittest.main()

0 commit comments

Comments
 (0)