10000 Exit with a status code if Fire encounters an error (#27) · PythonExpert/python-fire@c399562 · GitHub
[go: up one dir, main page]

Skip to content

Commit c399562

Browse files
jtratnerdbieber
authored andcommitted
Exit with a status code if Fire encounters an error (google#27)
Checked in by David Bieber, authored by David and Jeff. * Exit with a status code if Fire encounters an error * adds FireExit and assertRaisesFireExit * does not expose FireExit publicly yet
1 parent c3f158a commit c399562

File tree

8 files changed

+198
-70
lines changed

8 files changed

+198
-70
lines changed

fire/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
"""Python Fire module for third_party."""
15+
"""The Python Fire module."""
1616

1717
from __future__ import absolute_import
1818
from __future__ import division

fire/core.py

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ def main(argv):
7070

7171

7272
def Fire(component=None, command=None, name=None):
73-
"""This function, Fire, is the main entrypoint for Fire.
73+
"""This function, Fire, is the main entrypoint for Python Fire.
7474
7575
Executes a command either from the `command` argument or from sys.argv by
7676
recursively traversing the target object `component`'s members consuming
@@ -92,9 +92,10 @@ def Fire(component=None, command=None, name=None):
9292
it's a class). When all arguments are consumed and there's no function left
9393
to call or class left to instantiate, the resulting current component is
9494
the final result.
95-
If a Fire error is encountered, the Fire Trace is displayed to stdout and
96-
None is returned.
97-
If the trace command line argument is supplied, the FireTrace is returned.
95+
Raises:
96+
FireExit: When Fire encounters a FireError, Fire will raise a FireExit with
97+
code 2. When used with the help or trace flags, Fire will raise a
98+
FireExit with code 0 if successful.
9899
"""
99100
# Get args as a list.
100101
if command is None:
@@ -127,21 +128,21 @@ def Fire(component=None, command=None, name=None):
127128
result = component_trace.GetResult()
128129
print(
129130
helputils.HelpString(result, component_trace, component_trace.verbose))
130-
return None
131+
raise FireExit(2, component_trace)
131132
elif component_trace.show_trace and component_trace.show_help:
132133
print('Fire trace:\n{trace}\n'.format(trace=component_trace))
133134
result = component_trace.GetResult()
134135
print(
135136
helputils.HelpString(result, component_trace, component_trace.verbose))
136-
return component_trace
137+
raise FireExit(0, component_trace)
137138
elif component_trace.show_trace:
138139
print('Fire trace:\n{trace}'.format(trace=component_trace))
139-
return component_trace
140+
raise FireExit(0, component_trace)
140141
elif component_trace.show_help:
141142
result = component_trace.GetResult()
142143
print(
143144
helputils.HelpString(result, component_trace, component_trace.verbose))
144-
return None
145+
raise FireExit(0, component_trace)
145146
else:
146147
_PrintResult(component_trace, verbose=component_trace.verbose)
147148
result = component_trace.GetResult()
@@ -161,6 +162,27 @@ class FireError(Exception):
161162
"""
162163

163164

165+
class FireExit(SystemExit):
166+
"""An exception raised by Fire to the client in the case of a FireError.
167+
168+
The trace of the Fire program is available on the `trace` property.
169+
170+
This exception inherits from SystemExit, so clients may explicitly catch it
171+
with `except SystemExit` or `except FireExit`. If not caught, this exception
172+
will cause the client program to exit without a stacktrace.
173+
"""
174+
175+
def __init__(self, code, component_trace):
176+
"""Constructs a FireExit exception.
177+
178+
Args:
179+
code: (int) Exit code for the Fire CLI.
180+
component_trace: (FireTrace) The trace for the Fire command.
181+
"""
182+
super(FireExit, self).__init__(code)
183+
self.trace = component_trace
184+
185+
164186
def _PrintResult(component_trace, verbose=False):
165187
"""Prints the result of the Fire call to stdout in a human readable way."""
166188
# TODO: Design human readable deserializable serialization method
@@ -558,8 +580,8 @@ def _ParseFn(args):
558580

559581
# Note: _ParseArgs modifies kwargs.
560582
parsed_args, kwargs, remaining_args, capacity = _ParseArgs(
561-
fn_spec.args, fn_spec.defaults, num_required_args, kwargs, remaining_args,
562-
metadata)
583+
fn_spec.args, fn_spec.defaults, num_required_args, kwargs,
584+
remaining_args, metadata)
563585

564586
if fn_spec.varargs or fn_spec.varkw:
565587
# If we're allowed *varargs or **kwargs, there's always capacity.

fire/core_test.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,14 @@
1818

1919
from fire import core
2020
from fire import test_components as tc
21+
from fire import testutils
2122
from fire import trace
2223
import mock
2324

2425
import unittest
2526

2627

27-
class CoreTest(unittest.TestCase):
28+
class CoreTest(testutils.BaseTestCase):
2829

2930
def testOneLineResult(self):
3031
self.assertEqual(core._OneLineResult(1), '1')
@@ -66,11 +67,21 @@ def testInteractiveModeVariablesWithName(self, mock_embed):
6667
self.assertIsInstance(variables['trace'], trace.FireTrace)
6768

6869
def testImproperUseOfHelp(self):
69-
# This should produce a warning and return None.
70-
self.assertIsNone(core.Fire(tc.TypedProperties, 'alpha --help'))
70+
# This should produce a warning explaining the proper use of help.
71+
with self.assertRaisesFireExit(2, 'The proper way to show help.*Usage:'):
72+
core.Fire(tc.TypedProperties, 'alpha --help')
73+
74+
def testProperUseOfHelp(self):
75+
with self.assertRaisesFireExit(0, 'Usage:.*upper'):
76+
core.Fire(tc.TypedProperties, 'gamma -- --help')
77+
78+
def testInvalidParameterRaisesFireExit(self):
79+
with self.assertRaisesFireExit(2, 'runmisspelled'):
80+
core.Fire(tc.Kwargs, 'props --a=1 --b=2 runmisspelled')
7181

7282
def testErrorRaising(self):
7383
# Errors in user code should not be caught; they should surface as normal.
84+
# This will lead to exit status code 1 for the client program.
7485
with self.assertRaises(ValueError):
7586
core.Fire(tc.ErrorRaiser, 'fail')
7687

fire/fire_import_test.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,19 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
import fire
16-
15+
import mock
16+
import sys
1717
import unittest
1818

19+
import fire
20+
1921

2022
class FireImportTest(unittest.TestCase):
2123
"""Tests importing Fire."""
2224

2325
def testFire(self):
24-
fire.Fire()
26+
with mock.patch.object(sys, 'argv', ['commandname']):
27+
fire.Fire()
2528

2629
def testFireMethods(self):
2730
self.assertIsNotNone(fire.Fire)

fire/fire_test.py

Lines changed: 77 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,24 @@
1616
from __future__ import division
1717
from __future__ import print_function
1818

19+
import sys
20+
import unittest
21+
1922
import fire
2023
from fire import test_components as tc
21-
from fire import trace
24+
from fire import testutils
2225

26+
import mock
2327
import six
24-
import unittest
2528

2629

27-
class FireTest(unittest.TestCase):
30+
class FireTest(testutils.BaseTestCase):
2831

2932
def testFire(self):
30-
fire.Fire(tc.Empty)
31-
fire.Fire(tc.OldStyleEmpty)
32-
fire.Fire(tc.WithInit)
33+
with mock.patch.object(sys, 'argv', ['progname']):
34+
fire.Fire(tc.Empty)
35+
fire.Fire(tc.OldStyleEmpty)
36+
fire.Fire(tc.WithInit)
3337
self.assertEqual(fire.Fire(tc.NoDefaults, 'double 2'), 4)
3438
self.assertEqual(fire.Fire(tc.NoDefaults, 'triple 4'), 12)
3539
self.assertEqual(fire.Fire(tc.WithDefaults, 'double 2'), 4)
@@ -41,10 +45,13 @@ def testFireNoArgs(self):
4145
self.assertEqual(fire.Fire(tc.MixedDefaults, 'ten'), 10)
4246

4347
def testFireExceptions(self):
44-
# Exceptions of Fire are printed to stderr and None is returned.
45-
self.assertIsNone(fire.Fire(tc.Empty, 'nomethod')) # Member doesn't exist.
46-
self.assertIsNone(fire.Fire(tc.NoDefaults, 'double')) # Missing argument.
47-
self.assertIsNone(fire.Fire(tc.TypedProperties, 'delta x')) # Missing key.
48+
# Exceptions of Fire are printed to stderr and a FireExit is raised.
49+
with self.assertRaisesFireExit(2):
50+
fire.Fire(tc.Empty, 'nomethod') # Member doesn't exist.
51+
with self.assertRaisesFireExit(2):
52+
fire.Fire(tc.NoDefaults, 'double') # Missing argument.
53+
with self.assertRaisesFireExit(2):
54+
fire.Fire(tc.TypedProperties, 'delta x') # Missing key.
4855

4956
# Exceptions of the target components are still raised.
5057
with self.assertRaises(ZeroDivisionError):
@@ -89,11 +96,13 @@ def testFirePartialNamedArgs(self):
8996
fire.Fire(tc.MixedDefaults, 'identity --beta 1 --alpha 2'), (2, 1))
9097

9198
def testFirePartialNamedArgsOneMissing(self):
92-
# By default, errors are written to standard out and None is returned.
93-
self.assertIsNone( # Identity needs an arg.
94-
fire.Fire(tc.MixedDefaults, 'identity'))
95-
self.assertIsNone( # Identity needs a value for alpha.
96-
fire.Fire(tc.MixedDefaults, 'identity --beta 2'))
99+
# Errors are written to standard out and a FireExit is raised.
100+
with self.assertRaisesFireExit(2):
101+
fire.Fire(tc.MixedDefaults, 'identity') # Identity needs an arg.
102+
103+
with self.assertRaisesFireExit(2):
104+
# Identity needs a value for alpha.
105+
fire.Fire(tc.MixedDefaults, 'identity --beta 2')
97106

98107
self.assertEqual(fire.Fire(tc.MixedDefaults, 'identity 1'), (1, '0'))
99108
self.assertEqual(
@@ -103,9 +112,11 @@ def testFireAnnotatedArgs(self):
103112
self.assertEqual(fire.Fire(tc.Annotations, 'double 5'), 10)
104113
self.assertEqual(fire.Fire(tc.Annotations, 'triple 5'), 15)
105114

106-
@unittest.skipIf(six.PY2, 'Keyword-only arguments not supported in Python 2')
115+
@unittest.skipIf(six.PY2, 'Keyword-only arguments not in Python 2.')
107116
def testFireKeywordOnlyArgs(self):
108-
self.assertIsNone(fire.Fire(tc.py3.KeywordOnly, 'double 5'))
117+
with self.assertRaisesFireExit(2):
118+
# Keyword arguments must be passed with flag syntax.
119+
fire.Fire(tc.py3.KeywordOnly, 'double 5')
109120

110121
self.assertEqual(fire.Fire(tc.py3.KeywordOnly, 'double --count 5'), 10)
111122
self.assertEqual(fire.Fire(tc.py3.KeywordOnly, 'triple --count 5'), 15)
@@ -252,17 +263,19 @@ def fn1(thing, nothing):
252263
self.assertEqual(fire.Fire(fn1, '--thing --nothing'), (True, True))
253264
self.assertEqual(fire.Fire(fn1, '--thing --nonothing'), (True, False))
254265

255-
# In the next example nothing=False (since rightmost setting of a flag gets
256-
# precedence), but it errors because thing has no value.
257-
self.assertEqual(fire.Fire(fn1, '--nothing --nonothing'), None)
266+
with self.assertRaisesFireExit(2):
267+
# In this case nothing=False (since rightmost setting of a flag gets
268+
# precedence), but it errors because thing has no value.
269+
fire.Fire(fn1, '--nothing --nonothing')
258270

259271
# In these examples, --nothing sets thing=False:
260272
def fn2(thing, **kwargs):
261273
return thing, kwargs
262274
self.assertEqual(fire.Fire(fn2, '--thing'), (True, {}))
263275
self.assertEqual(fire.Fire(fn2, '--nothing'), (False, {}))
264-
# In the next one, nothing=True, but it errors because thing has no value.
265-
self.assertEqual(fire.Fire(fn2, '--nothing=True'), None)
276+
with self.assertRaisesFireExit(2):
277+
# In this case, nothing=True, but it errors because thing has no value.
278+
fire.Fire(fn2, '--nothing=True')
266279
self.assertEqual(fire.Fire(fn2, '--nothing --nothing=True'),
267280
(False, {'nothing': True}))
268281

@@ -276,26 +289,28 @@ def fn3(arg, **kwargs):
276289
('value', {'nothing': False}))
277290

278291
def testTraceFlag(self):
279-
self.assertIsInstance(
280-
fire.Fire(tc.BoolConverter, 'as-bool True -- --trace'), trace.FireTrace)
281-
self.assertIsInstance(
282-
fire.Fire(tc.BoolConverter, 'as-bool True -- -t'), trace.FireTrace)
283-
self.assertIsInstance(
284-
fire.Fire(tc.BoolConverter, '-- --trace'), trace.FireTrace)
292+
with self.assertRaisesFireExit(0, 'Fire trace:\n'):
293+
fire.Fire(tc.BoolConverter, 'as-bool True -- --trace')
294+
with self.assertRaisesFireExit(0, 'Fire trace:\n'):
295+
fire.Fire(tc.BoolConverter, 'as-bool True -- -t')
296+
with self.assertRaisesFireExit(0, 'Fire trace:\n'):
297+
fire.Fire(tc.BoolConverter, '-- --trace')
285298

286299
def testHelpFlag(self):
287-
self.assertIsNone(fire.Fire(tc.BoolConverter, 'as-bool True -- --help'))
288-
self.assertIsNone(fire.Fire(tc.BoolConverter, 'as-bool True -- -h'))
289-
self.assertIsNone(fire.Fire(tc.BoolConverter, '-- --help'))
300+
with self.assertRaisesFireExit(0):
301+
fire.Fire(tc.BoolConverter, 'as-bool True -- --help')
302+
with self.assertRaisesFireExit(0):
303+
fire.Fire(tc.BoolConverter, 'as-bool True -- -h')
304+
with self.assertRaisesFireExit(0):
305+
fire.Fire(tc.BoolConverter, '-- --help')
290306

291307
def testHelpFlagAndTraceFlag(self):
292-
self.assertIsInstance(
293-
fire.Fire(tc.BoolConverter, 'as-bool True -- --help --trace'),
294-
10000 trace.FireTrace)
295-
self.assertIsInstance(
296-
fire.Fire(tc.BoolConverter, 'as-bool True -- -h -t'), trace.FireTrace)
297-
self.assertIsInstance(
298-
fire.Fire(tc.BoolConverter, '-- -h --trace'), trace.FireTrace)
308+
with self.assertRaisesFireExit(0, 'Fire trace:\n.*Usage:'):
309+
fire.Fire(tc.BoolConverter, 'as-bool True -- --help --trace')
310+
with self.assertRaisesFireExit(0, 'Fire trace:\n.*Usage:'):
311+
fire.Fire(tc.BoolConverter, 'as-bool True -- -h -t')
312+
with self.assertRaisesFireExit(0, 'Fire trace:\n.*Usage:'):
313+
fire.Fire(tc.BoolConverter, '-- -h --trace')
299314

300315
def testTabCompletionNoName(self):
301316
with self.assertRaises(ValueError):
@@ -323,7 +338,8 @@ def testBasicSeparator(self):
323338
('-', '_'))
324339

325340
# The separator triggers a function call, but there aren't enough arguments.
326-
self.assertEqual(fire.Fire(tc.MixedDefaults, 'identity - _ +'), None)
341+
with self.assertRaisesFireExit(2):
342+
fire.Fire(tc.MixedDefaults, 'identity - _ +')
327343

328344
def testNonComparable(self):
329345
"""Fire should work with classes that disallow comparisons."""
@@ -367,24 +383,34 @@ def testFloatForExpectedInt(self):
367383
def testClassInstantiation(self):
368384
self.assertIsInstance(fire.Fire(tc.InstanceVars, '--arg1=a1 --arg2=a2'),
369385
tc.InstanceVars)
370-
# Cannot instantiate a class with positional args by default.
371-
self.assertIsNone(fire.Fire(tc.InstanceVars, 'a1 a2'))
386+
with self.assertRaisesFireExit(2):
387+
# Cannot instantiate a class with positional args.
388+
fire.Fire(tc.InstanceVars, 'a1 a2')
372389

373390
def testTraceErrors(self):
374391
# Class needs additional value but runs out of args.
375-
self.assertIsNone(fire.Fire(tc.InstanceVars, 'a1'))
376-
self.assertIsNone(fire.Fire(tc.InstanceVars, '--arg1=a1'))
392+
with self.assertRaisesFireExit(2):
393+
fire.Fire(tc.InstanceVars, 'a1')
394+
with self.assertRaisesFireExit(2):
395+
fire.Fire(tc.InstanceVars, '--arg1=a1')
396+
377397
# Routine needs additional value but runs out of args.
378-
self.assertIsNone(fire.Fire(tc.InstanceVars, 'a1 a2 - run b1'))
379-
self.assertIsNone(
380-
fire.Fire(tc.InstanceVars, '--arg1=a1 --arg2=a2 - run b1'))
398+
with self.assertRaisesFireExit(2):
399+
fire.Fire(tc.InstanceVars, 'a1 a2 - run b1')
400+
with self.assertRaisesFireExit(2):
401+
fire.Fire(tc.InstanceVars, '--arg1=a1 --arg2=a2 - run b1')
402+
381403
# Extra args cannot be consumed.
382-
self.assertIsNone(fire.Fire(tc.InstanceVars, 'a1 a2 - run b1 b2 b3'))
383-
self.assertIsNone(
384-
fire.Fire(tc.InstanceVars, '--arg1=a1 --arg2=a2 - run b1 b2 b3'))
404+
with self.assertRaisesFireExit(2):
405+
fire.Fire(tc.InstanceVars, 'a1 a2 - run b1 b2 b3')
406+
with self.assertRaisesFireExit(2):
407+
fire.Fire(tc.InstanceVars, '--arg1=a1 --arg2=a2 - run b1 b2 b3')
408+
385409
# Cannot find member to access.
386-
self.assertIsNone(fire.Fire(tc.InstanceVars, 'a1 a2 - jog'))
387-
self.assertIsNone(fire.Fire(tc.InstanceVars, '--arg1=a1 --arg2=a2 - jog'))
410+
with self.assertRaisesFireExit(2):
411+
fire.Fire(tc.InstanceVars, 'a1 a2 - jog')
412+
with self.assertRaisesFireExit(2):
413+
fire.Fire(tc.InstanceVars, '--arg1=a1 --arg2=a2 - jog')
388414

389415

390416
if __name__ == '__main__':

fire/inspectutils_test.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,14 @@
1616
from __future__ import division
1717
from __future__ import print_function
1818

19-
import unittest
20-
import six
2119
import os
20+
import unittest
2221

2322
from fire import inspectutils
2423
from fire import test_components as tc
2524

25+
import six
26+
2627

2728
class InspectUtilsTest(unittest.TestCase):
2829

fire/test_components.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ def __init__(self):
110110
}
111111
self.echo = ['alex', 'bethany']
112112
self.fox = ('carry', 'divide')
113+
self.gamma = 'myexcitingstring'
113114

114115

115116
class VarArgs(object):

0 commit comments

Comments
 (0)
0