99
1010import argparse
1111from collections import namedtuple
12+ import functools
1213import sys
1314from typing import List , Tuple
1415
1920 from contextlib import redirect_stdout , redirect_stderr
2021
2122from .argparse_completer import _RangeAction
23+ from .utils import namedtuple_with_defaults
2224
2325
24- CommandResult = namedtuple ('FunctionResult' , 'stdout stderr data' )
26+ class CommandResult (namedtuple_with_defaults ('CmdResult' , ['stdout' , 'stderr' , 'data' ])):
27+ """Encapsulates the results from a command.
28+
29+ Named tuple attributes
30+ ----------------------
31+ stdout: str - Output captured from stdout while this command is executing
32+ stderr: str - Output captured from stderr while this command is executing. None if no error captured
33+ data - Data returned by the command.
34+
35+ NOTE: Named tuples are immutable. So the contents are there for access, not for modification.
36+ """
37+ def __bool__ (self ):
38+ """If stderr is None and data is not None the command is considered a success"""
39+ return not self .stderr and self .data is not None
2540
2641
2742class CopyStream (object ):
28- """ Toy class for replacing self.stdout in cmd2.Cmd instances for unit testing. """
29- def __init__ (self , innerStream ):
43+ """Copies all data written to a stream """
44+ def __init__ (self , inner_stream ):
3045 self .buffer = ''
31- self .innerStream = innerStream
46+ self .inner_stream = inner_stream
3247
3348 def write (self , s ):
3449 self .buffer += s
35- self .innerStream .write (s )
50+ self .inner_stream .write (s )
3651
3752 def read (self ):
3853 raise NotImplementedError
3954
4055 def clear (self ):
4156 self .buffer = ''
42- self .innerStream .clear ()
57+
58+
59+ def _exec_cmd (cmd2_app , func ):
60+ """Helper to encapsulate executing a command and capturing the results"""
61+ copy_stdout = CopyStream (sys .stdout )
62+ copy_stderr = CopyStream (sys .stderr )
63+
64+ cmd2_app ._last_result = None
65+
66+ with redirect_stdout (copy_stdout ):
67+ with redirect_stderr (copy_stderr ):
68+ func ()
69+
70+ # if stderr is empty, set it to None
71+ stderr = copy_stderr if copy_stderr .buffer else None
72+
73+ result = CommandResult (stdout = copy_stdout .buffer , stderr = stderr , data = cmd2_app ._last_result )
74+ return result
4375
4476
4577class ArgparseFunctor :
@@ -208,14 +240,7 @@ def traverse_parser(parser):
208240
209241 # print('Command: {}'.format(cmd_str[0]))
210242
211- copyStdOut = CopyStream (sys .stdout )
212- copyStdErr = CopyStream (sys .stderr )
213- with redirect_stdout (copyStdOut ):
214- with redirect_stderr (copyStdErr ):
215- func (cmd_str [0 ])
216- result = CommandResult (stdout = copyStdOut .buffer , stderr = copyStdErr .buffer , data = self ._cmd2_app ._last_result )
217- return result
218-
243+ return _exec_cmd (self ._cmd2_app , functools .partial (func , cmd_str [0 ]))
219244
220245class PyscriptBridge (object ):
221246 """Preserves the legacy 'cmd' interface for pyscript while also providing a new python API wrapper for
@@ -236,8 +261,7 @@ def __getattr__(self, item: str):
236261 except AttributeError :
237262 # Command doesn't, we will accept parameters in the form of a command string
238263 def wrap_func (args = '' ):
239- func (args )
240- return self ._cmd2_app ._last_result
264+ return _exec_cmd (self ._cmd2_app , functools .partial (func , args ))
241265 return wrap_func
242266 else :
243267 # Command does use argparse, return an object that can traverse the argparse subcommands and arguments
@@ -246,6 +270,4 @@ def wrap_func(args=''):
246270 raise AttributeError (item )
247271
248272 def __call__ (self , args ):
249- self ._cmd2_app .onecmd_plus_hooks (args + '\n ' )
250- self ._last_result = self ._cmd2_app ._last_result
251- return self ._cmd2_app ._last_result
273+ return _exec_cmd (self ._cmd2_app , functools .partial (self ._cmd2_app .onecmd_plus_hooks , args + '\n ' ))
0 commit comments