88"""
99
1010import argparse
11- from collections import namedtuple
1211import functools
1312import sys
14- from typing import List , Tuple
13+ from typing import List , Tuple , Callable
1514
1615# Python 3.4 require contextlib2 for temporarily redirecting stderr and stdout
1716if sys .version_info < (3 , 5 ):
@@ -41,54 +40,78 @@ def __bool__(self):
4140
4241class CopyStream (object ):
4342 """Copies all data written to a stream"""
44- def __init__ (self , inner_stream ):
43+ def __init__ (self , inner_stream , echo : bool = False ):
4544 self .buffer = ''
4645 self .inner_stream = inner_stream
46+ self .echo = echo
4747
4848 def write (self , s ):
4949 self .buffer += s
50- self .inner_stream .write (s )
50+ if self .echo :
51+ self .inner_stream .write (s )
5152
5253 def read (self ):
5354 raise NotImplementedError
5455
5556 def clear (self ):
5657 self .buffer = ''
5758
59+ def __getattr__ (self , item : str ):
60+ if item in self .__dict__ :
61+ return self .__dict__ [item ]
62+ else :
63+ return getattr (self .inner_stream , item )
64+
5865
59- def _exec_cmd (cmd2_app , func ):
66+ def _exec_cmd (cmd2_app , func : Callable , echo : bool ):
6067 """Helper to encapsulate executing a command and capturing the results"""
61- copy_stdout = CopyStream (sys .stdout )
62- copy_stderr = CopyStream (sys .stderr )
68+ copy_stdout = CopyStream (sys .stdout , echo )
69+ copy_stderr = CopyStream (sys .stderr , echo )
70+
71+ copy_cmd_stdout = CopyStream (cmd2_app .stdout , echo )
6372
6473 cmd2_app ._last_result = None
6574
66- with redirect_stdout (copy_stdout ):
67- with redirect_stderr (copy_stderr ):
68- func ()
75+ try :
76+ cmd2_app .stdout = copy_cmd_stdout
77+ with redirect_stdout (copy_stdout ):
78+ with redirect_stderr (copy_stderr ):
79+ func ()
80+ finally :
81+ cmd2_app .stdout = copy_cmd_stdout .inner_stream
6982
7083 # if stderr is empty, set it to None
71- stderr = copy_stderr if copy_stderr .buffer else None
84+ stderr = copy_stderr . buffer if copy_stderr .buffer else None
7285
73- result = CommandResult (stdout = copy_stdout .buffer , stderr = stderr , data = cmd2_app ._last_result )
86+ outbuf = copy_cmd_stdout .buffer if copy_cmd_stdout .buffer else copy_stdout .buffer
87+ result = CommandResult (stdout = outbuf , stderr = stderr , data = cmd2_app ._last_result )
7488 return result
7589
7690
7791class ArgparseFunctor :
7892 """
7993 Encapsulates translating python object traversal
8094 """
81- def __init__ (self , cmd2_app , item , parser ):
95+ def __init__ (self , echo : bool , cmd2_app , command_name : str , parser : argparse .ArgumentParser ):
96+ self ._echo = echo
8297 self ._cmd2_app = cmd2_app
83- self ._item = item
98+ self ._command_name = command_name
8499 self ._parser = parser
85100
86101 # Dictionary mapping command argument name to value
87102 self ._args = {}
88103 # argparse object for the current command layer
89104 self .__current_subcommand_parser = parser
90105
91- def __getattr__ (self , item ):
106+ def __dir__ (self ):
107+ """Returns a custom list of attribute names to match the sub-commands"""
108+ commands = []
109+ for action in self .__current_subcommand_parser ._actions :
110+ if not action .option_strings and isinstance (action , argparse ._SubParsersAction ):
111+ commands .extend (action .choices )
112+ return commands
113+
114+ def __getattr__ (self , item : str ):
92115 """Search for a subcommand matching this item and update internal state to track the traversal"""
93116 # look for sub-command under the current command/sub-command layer
94117 for action in self .__current_subcommand_parser ._actions :
@@ -101,7 +124,6 @@ def __getattr__(self, item):
101124 return self
102125
103126 raise AttributeError (item )
104- # return super().__getattr__(item)
105127
106128 def __call__ (self , * args , ** kwargs ):
107129 """
@@ -182,7 +204,7 @@ def __call__(self, *args, **kwargs):
182204
183205 def _run (self ):
184206 # look up command function
185- func = getattr (self ._cmd2_app , 'do_' + self ._item )
207+ func = getattr (self ._cmd2_app , 'do_' + self ._command_name )
186208
187209 # reconstruct the cmd2 command from the python call
188210 cmd_str = ['' ]
@@ -238,16 +260,16 @@ def traverse_parser(parser):
238260
239261 traverse_parser (self ._parser )
240262
241- # print('Command: {}'.format( cmd_str[0]))
263+ return _exec_cmd ( self . _cmd2_app , functools . partial ( func , cmd_str [0 ]), self . _echo )
242264
243- return _exec_cmd (self ._cmd2_app , functools .partial (func , cmd_str [0 ]))
244265
245266class PyscriptBridge (object ):
246267 """Preserves the legacy 'cmd' interface for pyscript while also providing a new python API wrapper for
247268 application commands."""
248269 def __init__ (self , cmd2_app ):
249270 self ._cmd2_app = cmd2_app
250271 self ._last_result = None
272+ self .cmd_echo = False
251273
252274 def __getattr__ (self , item : str ):
253275 """Check if the attribute is a command. If so, return a callable."""
@@ -261,13 +283,19 @@ def __getattr__(self, item: str):
261283 except AttributeError :
262284 # Command doesn't, we will accept parameters in the form of a command string
263285 def wrap_func (args = '' ):
264- return _exec_cmd (self ._cmd2_app , functools .partial (func , args ))
286+ return _exec_cmd (self ._cmd2_app , functools .partial (func , args ), self . cmd_echo )
265287 return wrap_func
266288 else :
267289 # Command does use argparse, return an object that can traverse the argparse subcommands and arguments
268- return ArgparseFunctor (self ._cmd2_app , item , parser )
290+ return ArgparseFunctor (self .cmd_echo , self . _cmd2_app , item , parser )
269291
270- raise AttributeError (item )
292+ return super ().__getattr__ (item )
293+
294+ def __dir__ (self ):
295+ """Return a custom set of attribute names to match the available commands"""
296+ commands = list (self ._cmd2_app .get_all_commands ())
297+ commands .insert (0 , 'cmd_echo' )
298+ return commands
271299
272- def __call__ (self , args ):
273- return _exec_cmd (self ._cmd2_app , functools .partial (self ._cmd2_app .onecmd_plus_hooks , args + '\n ' ))
300+ def __call__ (self , args : str ):
301+ return _exec_cmd (self ._cmd2_app , functools .partial (self ._cmd2_app .onecmd_plus_hooks , args + '\n ' ), self . cmd_echo )
0 commit comments