1+ import sys
12
23from argparse import ArgumentParser
34from datetime import datetime , timedelta
45from inspect import getdoc , signature
56from io import StringIO
67from pathlib import Path
8+ from shutil import copy as copy_file
79from traceback import format_exception
810
911from robot .api import ExecutionResult , ResultVisitor , ResultWriter
1012from robot .libraries .BuiltIn import BuiltIn
1113from robot .errors import DataError
12- from yaml import load , FullLoader
14+ from yaml import load , FullLoader , dump as dump_yaml
1315
14- from .config import CONFIG_FILE
15- from .errors import OxygenException
16+ from .config import CONFIG_FILE , ORIGINAL_CONFIG_FILE
17+ from .errors import (OxygenException ,
18+ InvalidConfigurationException ,
19+ ResultFileNotFoundException )
1620from .robot_interface import RobotInterface
1721from .version import VERSION
1822
@@ -25,17 +29,35 @@ class OxygenCore(object):
2529
2630
2731 def __init__ (self ):
28- with open (CONFIG_FILE , 'r' ) as infile :
32+ self ._config = None
33+ self ._handlers = None
34+
35+ @property
36+ def config (self ):
37+ if self ._config is None :
38+ self .load_config (CONFIG_FILE )
39+ return self ._config
40+
41+ def load_config (self , config_file ):
42+ with open (config_file , 'r' ) as infile :
2943 self ._config = load (infile , Loader = FullLoader )
30- self ._handlers = {}
31- self ._register_handlers ()
44+
45+ @property
46+ def handlers (self ):
47+ if self ._handlers is None :
48+ self ._handlers = {}
49+ self ._register_handlers ()
50+ return self ._handlers
3251
3352 def _register_handlers (self ):
34- for tool_name , config in self ._config .items ():
35- handler_class = getattr (__import__ (tool_name ,
36- fromlist = [config ['handler' ]]),
37- config ['handler' ])
38- handler = handler_class (config )
53+ for tool_name , handler_config in self .config .items ():
54+ try :
55+ handler_class = getattr (
56+ __import__ (tool_name , fromlist = [handler_config ['handler' ]]),
57+ handler_config ['handler' ])
58+ except ModuleNotFoundError as e :
59+ raise InvalidConfigurationException (e )
60+ handler = handler_class (handler_config )
3961 self ._handlers [tool_name ] = handler
4062
4163
@@ -53,7 +75,7 @@ def __init__(self, data):
5375
5476 def visit_test (self , test ):
5577 failures = []
56- for handler_type , handler in self ._handlers .items ():
78+ for handler_type , handler in self .handlers .items ():
5779 try :
5880 handler .check_for_keyword (test , self .data )
5981 except Exception as e :
@@ -182,12 +204,12 @@ def __init__(self):
182204 def _fetch_handler (self , name ):
183205 try :
184206 return next (filter (lambda h : h .keyword == name ,
185- self ._handlers .values ()))
207+ self .handlers .values ()))
186208 except StopIteration :
187209 raise OxygenException ('No handler for keyword "{}"' .format (name ))
188210
189211 def get_keyword_names (self ):
190- return list (handler .keyword for handler in self ._handlers .values ())
212+ return list (handler .keyword for handler in self .handlers .values ())
191213
192214 def run_keyword (self , name , args , kwargs ):
193215 handler = self ._fetch_handler (name )
@@ -210,31 +232,72 @@ class OxygenCLI(OxygenCore):
210232 OxygenCLI is a command line interface to transform one test result file to
211233 corresponding Robot Framework output.xml
212234 '''
213- def parse_args (self , parser ):
235+ MAIN_LEVEL_CLI_ARGS = {
236+ # we intentionally define `dest` here so we can filter arguments later
237+ '--version' : {'action' : 'version' ,
238+ 'dest' : 'version' },
239+ '--add-config' : {'type' : Path ,
240+ 'metavar' : 'FILE' ,
241+ 'dest' : 'add_config' ,
242+ 'help' : ('path to YAML file whose content is '
243+ 'appended to existing Oxygen handler '
244+ 'configuration' )},
245+ '--reset-config' : {'action' : 'store_true' ,
246+ 'dest' : 'reset_config' ,
247+ 'help' : ('resets the Oxygen handler '
248+ 'configuration to a pristine, '
249+ 'as-freshly-installed version' )},
250+ '--print-config' : {'action' : 'store_true' ,
251+ 'dest' : 'print_config' ,
252+ 'help' : ('prints current Oxygen handler '
253+ 'configuration' )}
254+ }
255+ def add_arguments (self , parser ):
256+ # Add version number here to the arguments as it depends on OxygenCLI
257+ # being initiated already
258+ self .MAIN_LEVEL_CLI_ARGS ['--version' ]['version' ] = \
259+ f'%(prog)s { self .__version__ } '
260+ for flag , params in self .MAIN_LEVEL_CLI_ARGS .items ():
261+ parser .add_argument (flag , ** params )
262+
214263 subcommands = parser .add_subparsers ()
215- for tool_name , tool_handler in self ._handlers .items ():
264+ for tool_name , tool_handler in self .handlers .items ():
216265 subcommand_parser = subcommands .add_parser (tool_name )
217266 for flags , params in tool_handler .cli ().items ():
218267 subcommand_parser .add_argument (* flags , ** params )
219268 subcommand_parser .set_defaults (func = tool_handler .parse_results )
269+
270+ def parse_args (self , parser ):
220271 return vars (parser .parse_args ()) # returns a dictionary
221272
222273 def get_output_filename (self , result_file ):
274+ if result_file is None :
275+ raise ResultFileNotFoundException ('You did not give any result '
276+ 'file to convert' )
223277 filename = Path (result_file )
224278 filename = filename .with_suffix ('.xml' )
225279 robot_name = filename .stem + '_robot_output' + filename .suffix
226280 filename = filename .with_name (robot_name )
227281 return str (filename )
228282
229- def run (self ):
230- parser = ArgumentParser (prog = 'oxygen' )
231- parser .add_argument ('--version' ,
232- action = 'version' ,
233- version = f'%(prog)s { self .__version__ } ' )
234- args = self .parse_args (parser )
235- if not args :
236- parser .error ('No arguments given' )
237- output_filename = self .get_output_filename (args ['result_file' ])
283+ def append_config (self , new_config_path ):
284+ with open (new_config_path , 'r' ) as new_config :
285+ with open (CONFIG_FILE , 'a' ) as old_config :
286+ old_config .write (new_config .read ())
287+ self .load_config (CONFIG_FILE )
288+
289+ @staticmethod
290+ def reset_config ():
291+ copy_file (ORIGINAL_CONFIG_FILE , CONFIG_FILE )
292+ OxygenCLI ().load_config (CONFIG_FILE )
293+ print ('Oxygen handler configuration reset!' )
294+
295+ def print_config (self ):
296+ print (f'Using config file: { CONFIG_FILE } ' )
297+ print (dump_yaml (self .config ))
298+
299+ def convert_to_robot_result (self , args ):
300+ output_filename = self .get_output_filename (args .get ('result_file' ))
238301 parsed_results = args ['func' ](
239302 ** {k : v for (k , v ) in args .items () if not callable (v )})
240303 robot_suite = RobotInterface ().running .build_suite (parsed_results )
@@ -243,6 +306,38 @@ def run(self):
243306 report = None ,
244307 stdout = StringIO ())
245308
309+ def run (self ):
310+ parser = ArgumentParser (prog = 'oxygen' )
311+ self .add_arguments (parser )
312+ args = self .parse_args (parser )
313+ match args :
314+ case {'add_config' : new_config_path } if new_config_path is not None :
315+ return self .append_config (new_config_path )
316+ case {'print_config' : should_print } if should_print :
317+ return self .print_config ()
318+ case {'add_config' : _,
319+ 'reset_config' : _,
320+ 'print_config' : _,
321+ ** rest } if not rest : # user is not trying to invoke main-level arguments, but do not provide other arguments either
322+ parser .error ('No arguments given' )
323+ case _:
324+ # filter out arguments meant for other cases so that downstream
325+ # handler does not need to know about them
326+ filter_list = [v ['dest' ] for v in
327+ self .MAIN_LEVEL_CLI_ARGS .values ()]
328+ filtered_args = {k : v for k , v in args .items ()
329+ if k not in filter_list }
330+ return self .convert_to_robot_result (filtered_args )
331+
332+ def main ():
333+ '''Main CLI entrypoint
334+
335+ Also used in __main__.py
336+ '''
337+ if '--reset-config' in sys .argv :
338+ OxygenCLI .reset_config ()
339+ sys .exit (0 )
340+ OxygenCLI ().run ()
246341
247342if __name__ == '__main__' :
248- OxygenCLI (). run ()
343+ main ()
0 commit comments