Skip to content

Commit babf4f1

Browse files
jgarstdbieber
authored andcommitted
Fix keyword-only arguments (google#18)
Adds support for keyword-only arguments to Fire.
1 parent 6feced7 commit babf4f1

File tree

9 files changed

+194
-114
lines changed

9 files changed

+194
-114
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,4 @@ before_install:
88
- pip install --upgrade setuptools pip
99
install:
1010
- python setup.py develop
11-
script: nosetests --ignore-files=parser_fuzz_test.py
11+
script: nosetests --ignore-files=parser_fuzz_test.py --exclude=test_components_py3.py

fire/completion.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,8 @@ def Completions(component, verbose=False):
157157
A list of completions for a command that would so far return the component.
158158
"""
159159
if inspect.isroutine(component) or inspect.isclass(component):
160-
fn_args = inspectutils.GetArgSpec(component).args
161-
return _CompletionsFromArgs(fn_args)
160+
spec = inspectutils.GetFullArgSpec(component)
161+
return _CompletionsFromArgs(spec.args + spec.kwonlyargs)
162162

163163
elif isinstance(component, (tuple, list)):
164164
return [str(index) for index in range(len(component))]

fire/core.py

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -543,31 +543,38 @@ def _MakeParseFn(fn):
543543
can then be called with fn(*varargs, **kwargs). The remaining_args are
544544
the leftover args from the arguments to the parse function.
545545
"""
546-
fn_args, fn_varargs, fn_keywords, fn_defaults = inspectutils.GetArgSpec(fn)
546+
fn_spec = inspectutils.GetFullArgSpec(fn)
547+
all_args = fn_spec.args + fn_spec.kwonlyargs
547548
metadata = decorators.GetMetadata(fn)
548549

549-
# Note: num_required_args is the number of arguments without default values.
550-
# All of these arguments are required.
551-
num_required_args = len(fn_args) - len(fn_defaults)
550+
# Note: num_required_args is the number of positional arguments without
551+
# default values. All of these arguments are required.
552+
num_required_args = len(fn_spec.args) - len(fn_spec.defaults)
553+
required_kwonly = set(fn_spec.kwonlyargs) - set(fn_spec.kwonlydefaults)
552554

553555
def _ParseFn(args):
554556
"""Parses the list of `args` into (varargs, kwargs), remaining_args."""
555-
kwargs, remaining_args = _ParseKeywordArgs(args, fn_args, fn_keywords)
557+
kwargs, remaining_args = _ParseKeywordArgs(args, all_args, fn_spec.varkw)
556558

557559
# Note: _ParseArgs modifies kwargs.
558560
parsed_args, kwargs, remaining_args, capacity = _ParseArgs(
559-
fn_args, fn_defaults, num_required_args, kwargs, remaining_args,
561+
fn_spec.args, fn_spec.defaults, num_required_args, kwargs, remaining_args,
560562
metadata)
561563

562-
if fn_varargs or fn_keywords:
564+
if fn_spec.varargs or fn_spec.varkw:
563565
# If we're allowed *varargs or **kwargs, there's always capacity.
564566
capacity = True
565567

566-
if fn_keywords is None and kwargs:
567-
raise FireError('Unexpected kwargs present', kwargs)
568+
extra_kw = set(kwargs) - set(fn_spec.kwonlyargs)
569+
if fn_spec.varkw is None and extra_kw:
570+
raise FireError('Unexpected kwargs present:', extra_kw)
571+
572+
missing_kwonly = set(required_kwonly) - set(kwargs)
573+
if missing_kwonly:
574+
raise FireError('Missing required flags:', missing_kwonly)
568575

569576
# If we accept *varargs, then use all remaining arguments for *varargs.
570-
if fn_varargs is not None:
577+
if fn_spec.varargs is not None:
571578
varargs, remaining_args = remaining_args, []
572579
else:
573580
varargs = []
@@ -577,7 +584,7 @@ def _ParseFn(args):
577584

578585
varargs = parsed_args + varargs
579586

580-
consumed_args = args[:len(args)-len(remaining_args)]
587+
consumed_args = args[:len(args) - len(remaining_args)]
581588
return (varargs, kwargs), consumed_args, remaining_args, capacity
582589

583590
return _ParseFn

fire/fire_test.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from fire import test_components as tc
2121
from fire import trace
2222

23+
import six
2324
import unittest
2425

2526

@@ -102,6 +103,13 @@ def testFireAnnotatedArgs(self):
102103
self.assertEqual(fire.Fire(tc.Annotations, 'double 5'), 10)
103104
self.assertEqual(fire.Fire(tc.Annotations, 'triple 5'), 15)
104105

106+
@unittest.skipIf(six.PY2, 'Keyword-only arguments not supported in Python 2')
107+
def testFireKeywordOnlyArgs(self):
108+
self.assertIsNone(fire.Fire(tc.py3.KeywordOnly, 'double 5'))
109+
110+
self.assertEqual(fire.Fire(tc.py3.KeywordOnly, 'double --count 5'), 10)
111+
self.assertEqual(fire.Fire(tc.py3.KeywordOnly, 'triple --count 5'), 15)
112+
105113
def testFireProperties(self):
106114
self.assertEqual(fire.Fire(tc.TypedProperties, 'alpha'), True)
107115
self.assertEqual(fire.Fire(tc.TypedProperties, 'beta'), (1, 2, 3))

fire/helputils.py

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -126,26 +126,23 @@ def HelpString(component, trace=None, verbose=False):
126126
return '\n'.join(lines)
127127

128128

129-
def _UsageStringFromFnDetails(command, args, varargs, keywords, defaults):
130-
"""Get a usage string from the function details for the given command.
129+
def _UsageStringFromFullArgSpec(command, spec):
130+
"""Get a usage string from the FullArgSpec for the given command.
131131
132132
The strings look like:
133133
command --arg ARG [--opt OPT] [VAR ...] [--KWARGS ...]
134134
135135
Args:
136136
command: The command leading up to the function.
137-
args: The args accepted by the function.
138-
varargs: If not None, a string naming the *varargs variable used by the fn.
139-
keywords: If not None, a string naming the **kwargs varargs used by the fn.
140-
defaults: The default values for args accepted by the function.
137+
spec: a FullArgSpec object describing the function.
141138
Returns:
142139
The usage string for the function.
143140
"""
144-
num_required_args = len(args) - len(defaults)
141+
num_required_args = len(spec.args) - len(spec.defaults)
145142

146143
help_flags = []
147144
help_positional = []
148-
for index, arg in enumerate(args):
145+
for index, arg in enumerate(spec.args):
149146
flag = arg.replace('_', '-')
150147
if index < num_required_args:
151148
help_flags.append('--{flag} {value}'.format(flag=flag, value=arg.upper()))
@@ -155,13 +152,21 @@ def _UsageStringFromFnDetails(command, args, varargs, keywords, defaults):
155152
flag=flag, value=arg.upper()))
156153
help_positional.append('[{value}]'.format(value=arg.upper()))
157154

158-
if varargs:
159-
help_flags.append('[{var} ...]'.format(var=varargs.upper()))
160-
help_positional.append('[{var} ...]'.format(var=varargs.upper()))
155+
if spec.varargs:
156+
help_flags.append('[{var} ...]'.format(var=spec.varargs.upper()))
157+
help_positional.append('[{var} ...]'.format(var=spec.varargs.upper()))
161158

162-
if keywords:
163-
help_flags.append('[--{kwarg} ...]'.format(kwarg=keywords.upper()))
164-
help_positional.append('[--{kwarg} ...]'.format(kwarg=keywords.upper()))
159+
for arg in spec.kwonlyargs:
160+
if arg in spec.kwonlydefaults:
161+
arg_str = '[--{flag} {value}]'.format(flag=arg, value=arg.upper())
162+
else:
163+
arg_str = '--{flag} {value}'.format(flag=arg, value=arg.upper())
164+
help_flags.append(arg_str)
165+
help_positional.append(arg_str)
166+
167+
if spec.varkw:
168+
help_flags.append('[--{kwarg} ...]'.format(kwarg=spec.varkw.upper()))
169+
help_positional.append('[--{kwarg} ...]'.format(kwarg=spec.varkw.upper()))
165170

166171
commands_flags = command + ' '.join(help_flags)
167172
commands_positional = command + ' '.join(help_positional)
@@ -178,8 +183,8 @@ def UsageString(component, trace=None, verbose=False):
178183
command = trace.GetCommand() + ' ' if trace else ''
179184

180185
if inspect.isroutine(component) or inspect.isclass(component):
181-
args, varargs, keywords, defaults = inspectutils.GetArgSpec(component)
182-
return _UsageStringFromFnDetails(command, args, varargs, keywords, defaults)
186+
spec = inspectutils.GetFullArgSpec(component)
187+
return _UsageStringFromFullArgSpec(command, spec)
183188

184189
elif isinstance(component, (list, tuple)):
185190
length = len(component)

fire/inspectutils.py

Lines changed: 42 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,32 @@
2424
import six
2525

2626

27-
def _GetArgSpecFnInfo(fn):
27+
class FullArgSpec(object):
28+
"""The arguments of a function, as in Python 3's inspect.FullArgSpec."""
29+
30+
def __init__(self, args=None, varargs=None, varkw=None, defaults=None,
31+
kwonlyargs=None, kwonlydefaults=None, annotations=None):
32+
"""Constructs a FullArgSpec with each provided attribute, or the default.
33+
34+
Args:
35+
args: A list of the argument names accepted by the function.
36+
varargs: The name of the *varargs argument or None if there isn't one.
37+
varkw: The name of the **kwargs argument or None if there isn't one.
38+
defaults: A tuple of the defaults for the arguments that accept defaults.
39+
kwonlyargs: A list of argument names that must be passed with a keyword.
40+
kwonlydefaults: A dictionary of keyword only arguments and their defaults.
41+
annotations: A dictionary of arguments and their annotated types.
42+
"""
43+
self.args = args or []
44+
self.varargs = varargs
45+
self.varkw = varkw
46+
self.defaults = defaults or ()
47+
self.kwonlyargs = kwonlyargs or []
48+
self.kwonlydefaults = kwonlydefaults or {}
49+
self.annotations = annotations or {}
50+
51+
52+
def _GetArgSpecInfo(fn):
2853
"""Gives information pertaining to computing the ArgSpec of fn.
2954
3055
Determines if the first arg is supplied automatically when fn is called.
@@ -57,51 +82,35 @@ class with an __init__ method.
5782
return fn, skip_arg
5883

5984

60-
def GetArgSpec(fn):
61-
"""Returns information about the function signature.
85+
def GetFullArgSpec(fn):
86+
"""Returns a FullArgSpec describing the given callable."""
6287

63-
Args:
64-
fn: The function to analyze.
65-
Returns:
66-
A named tuple of type inspect.ArgSpec with the following fields:
67-
args: A list of the argument names accepted by the function.
68-
varargs: The name of the *varargs argument or None if there isn't one.
69-
keywords: The name of the **kwargs argument or None if there isn't one.
70-
defaults: A tuple of the defaults for the arguments that accept defaults.
71-
"""
72-
fn, skip_arg = _GetArgSpecFnInfo(fn)
88+
fn, skip_arg = _GetArgSpecInfo(fn)
7389

7490
try:
75-
7691
if six.PY2:
77-
argspec = inspect.getargspec(fn)
78-
keywords = argspec.keywords
92+
args, varargs, varkw, defaults = inspect.getargspec(fn) # pylint: disable=deprecated-method
93+
kwonlyargs = kwonlydefaults = None
94+
annotations = getattr(fn, '__annotations__', None)
7995
else:
80-
argspec = inspect.getfullargspec(fn)
81-
keywords = argspec.varkw
82-
83-
args = argspec.args
84-
defaults = argspec.defaults or ()
85-
varargs = argspec.varargs
96+
(args, varargs, varkw, defaults,
97+
kwonlyargs, kwonlydefaults, annotations) = inspect.getfullargspec(fn)
8698

8799
except TypeError:
88-
args = []
89-
defaults = ()
90100
# If we can't get the argspec, how do we know if the fn should take args?
91101
# 1. If it's a builtin, it can take args.
92102
# 2. If it's an implicit __init__ function (a 'slot wrapper'), take no args.
93103
# Are there other cases?
94-
varargs = 'vars' if inspect.isbuiltin(fn) else None
95-
keywords = 'kwargs' if inspect.isbuiltin(fn) else None
104+
if inspect.isbuiltin(fn):
105+
return FullArgSpec(varargs='vars', varkw='kwargs')
106+
else:
107+
return FullArgSpec()
96108

97-
if skip_arg:
98-
args = args[1:] # Remove self.
109+
if skip_arg and args:
110+
args.pop(0) # Remove 'self' or 'cls' from the list of arguments.
99111

100-
return inspect.ArgSpec(
101-
args=args,
102-
varargs=varargs,
103-
keywords=keywords,
104-
defaults=defaults)
112+
return FullArgSpec(args, varargs, varkw, defaults,
113+
kwonlyargs, kwonlydefaults, annotations)
105114

106115

107116
def Info(component):

fire/inspectutils_test.py

Lines changed: 64 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -16,61 +16,76 @@
1616
from __future__ import division
1717
from __future__ import print_function
1818

19-
from fire import inspectutils
20-
from fire import test_components as tc
19+
import unittest
2120
import six
2221

23-
import unittest
22+
from fire import inspectutils
23+
from fire import test_components as tc
2424

2525

2626
class InspectUtilsTest(unittest.TestCase):
2727

28-
def testGetArgSpecReturnType(self):
29-
# Asserts that the named tuple returned by GetArgSpec has the appropriate
30-
# fields.
31-
argspec = inspectutils.GetArgSpec(tc.identity)
32-
args, varargs, keywords, defaults = argspec
33-
self.assertEqual(argspec.args, args)
34-
self.assertEqual(argspec.defaults, defaults)
35-
self.assertEqual(argspec.varargs, varargs)
36-
self.assertEqual(argspec.keywords, keywords)
37-
38-
def testGetArgSpec(self):
39-
args, varargs, keywords, defaults = inspectutils.GetArgSpec(tc.identity)
40-
self.assertEqual(args, ['arg1', 'arg2'])
41-
self.assertEqual(defaults, (10,))
42-
self.assertEqual(varargs, 'arg3')
43-
self.assertEqual(keywords, 'arg4')
44-
45-
def testGetArgSpecBuiltin(self):
46-
args, varargs, keywords, defaults = inspectutils.GetArgSpec('test'.upper)
47-
self.assertEqual(args, [])
48-
self.assertEqual(defaults, ())
49-
self.assertEqual(varargs, 'vars')
50-
self.assertEqual(keywords, 'kwargs')
51-
52-
def testGetArgSpecSlotWrapper(self):
53-
args, varargs, keywords, defaults = inspectutils.GetArgSpec(tc.NoDefaults)
54-
self.assertEqual(args, [])
55-
self.assertEqual(defaults, ())
56-
self.assertEqual(varargs, None)
57-
self.assertEqual(keywords, None)
58-
59-
def testGetArgSpecClassNoInit(self):
60-
args, varargs, keywords, defaults = inspectutils.GetArgSpec(
61-
tc.OldStyleEmpty)
62-
self.assertEqual(args, [])
63-
self.assertEqual(defaults, ())
64-
self.assertEqual(varargs, None)
65-
self.assertEqual(keywords, None)
66-
67-
def testGetArgSpecMethod(self):
68-
args, varargs, keywords, defaults = inspectutils.GetArgSpec(
69-
tc.NoDefaults().double)
70-
self.assertEqual(args, ['count'])
71-
self.assertEqual(defaults, ())
72-
self.assertEqual(varargs, None)
73-
self.assertEqual(keywords, None)
28+
def testGetFullArgSpec(self):
29+
spec = inspectutils.GetFullArgSpec(tc.identity)
30+
self.assertEqual(spec.args, ['arg1', 'arg2', 'arg3', 'arg4'])
31+
self.assertEqual(spec.defaults, (10, 20))
32+
self.assertEqual(spec.varargs, 'arg5')
33+
self.assertEqual(spec.varkw, 'arg6')
34+
self.assertEqual(spec.kwonlyargs, [])
35+
self.assertEqual(spec.kwonlydefaults, {})
36+
self.assertEqual(spec.annotations, {'arg2': int, 'arg4': int})
37+
38+
@unittest.skipIf(six.PY2, 'No keyword arguments in python 2')
39+
def testGetFullArgSpecPy3(self):
40+
spec = inspectutils.GetFullArgSpec(tc.py3.identity)
41+
self.assertEqual(spec.args, ['arg1', 'arg2', 'arg3', 'arg4'])
42+
self.assertEqual(spec.defaults, (10, 20))
43+
self.assertEqual(spec.varargs, 'arg5')
44+
self.assertEqual(spec.varkw, 'arg10')
45+
self.assertEqual(spec.kwonlyargs, ['arg6', 'arg7', 'arg8', 'arg9'])
46+
self.assertEqual(spec.kwonlydefaults, {'arg8': 30, 'arg9': 40})
47+
self.assertEqual(spec.annotations,
48+
{'arg2': int, 'arg4': int, 'arg7': int, 'arg9': int})
49+
50+
def testGetFullArgSpecFromBuiltin(self):
51+
spec = inspectutils.GetFullArgSpec('test'.upper)
52+
self.assertEqual(spec.args, [])
53+
self.assertEqual(spec.defaults, ())
54+
self.assertEqual(spec.varargs, 'vars')
55+
self.assertEqual(spec.varkw, 'kwargs')
56+
self.assertEqual(spec.kwonlyargs, [])
57+
self.assertEqual(spec.kwonlydefaults, {})
58+
self.assertEqual(spec.annotations, {})
59+
60+
def testGetFullArgSpecFromSlotWrapper(self):
61+
spec = inspectutils.GetFullArgSpec(tc.NoDefaults)
62+
self.assertEqual(spec.args, [])
63+
self.assertEqual(spec.defaults, ())
64+
self.assertEqual(spec.varargs, None)
65+
self.assertEqual(spec.varkw, None)
66+
self.assertEqual(spec.kwonlyargs, [])
67+
self.assertEqual(spec.kwonlydefaults, {})
68+
self.assertEqual(spec.annotations, {})
69+
70+
def testGetFullArgSpecFromClassNoInit(self):
71+
spec = inspectutils.GetFullArgSpec(tc.OldStyleEmpty)
72+
self.assertEqual(spec.args, [])
73+
self.assertEqual(spec.defaults, ())
74+
self.assertEqual(spec.varargs, None)
75+
self.assertEqual(spec.varkw, None)
76+
self.assertEqual(spec.kwonlyargs, [])
77+
self.assertEqual(spec.kwonlydefaults, {})
78+
self.assertEqual(spec.annotations, {})
79+
80+
def testGetFullArgSpecFromMethod(self):
81+
spec = inspectutils.GetFullArgSpec(tc.NoDefaults().double)
82+
self.assertEqual(spec.args, ['count'])
83+
self.assertEqual(spec.defaults, ())
84+
self.assertEqual(spec.varargs, None)
85+
self.assertEqual(spec.varkw, None)
86+
self.assertEqual(spec.kwonlyargs, [])
87+
self.assertEqual(spec.kwonlydefaults, {})
88+
self.assertEqual(spec.annotations, {})
7489

7590
def testInfoOne(self):
7691
info = inspectutils.Info(1)

0 commit comments

Comments
 (0)