diff --git a/cm/CHANGES.md b/cm/CHANGES.md index f39ad0dc2..cd843fef2 100644 --- a/cm/CHANGES.md +++ b/cm/CHANGES.md @@ -1,3 +1,7 @@ +## V3.2.4 + - CMX: improved logging + - CMX: improved error handling (show module path and line number) + ## V3.2.3 - added --new_branch to `cm pull repo` and `cm checkout repo` - fixed a bug in `cm show repo` (removed dependency on cm4mlops diff --git a/cm/cmind/__init__.py b/cm/cmind/__init__.py index d1f4e1a27..8319cb86c 100644 --- a/cm/cmind/__init__.py +++ b/cm/cmind/__init__.py @@ -2,10 +2,11 @@ # # Written by Grigori Fursin -__version__ = "3.2.3" +__version__ = "3.2.4" from cmind.core import access from cmind.core import x from cmind.core import error +from cmind.core import errorx from cmind.core import halt from cmind.core import CM diff --git a/cm/cmind/cli.py b/cm/cmind/cli.py index d62fbc90e..7e03d2347 100644 --- a/cm/cmind/cli.py +++ b/cm/cmind/cli.py @@ -84,7 +84,7 @@ def runx(argv = None): r = cm.x(argv, out='con') if r['return']>0 and (cm.output is None or cm.output == 'con'): - cm.error(r) + cm.errorx(r) sys.exit(r['return']) diff --git a/cm/cmind/config.py b/cm/cmind/config.py index bf18f7832..7b5644224 100644 --- a/cm/cmind/config.py +++ b/cm/cmind/config.py @@ -37,6 +37,7 @@ def __init__(self, config_file = None): "flag_help2": "help", "error_prefix": "CM error:", + "error_prefix2": "CMX detected an issue", "info_cli": "cm {action} {automation} {artifact(s)} {flags} @input.yaml @input.json", "info_clix": "cmx {action} {automation} {artifact(s)} {CMX control flags (-)} {CMX automation flags (--)}", diff --git a/cm/cmind/core.py b/cm/cmind/core.py index e903174ad..f563fbfa1 100644 --- a/cm/cmind/core.py +++ b/cm/cmind/core.py @@ -140,6 +140,116 @@ def error(self, r): return r + ############################################################ + def errorx(self, r): + """ + If r['return']>0: print CM error and raise error if in debugging mode + + Args: + r (dict): output from CM function with "return" and "error" + + Returns: + (dict): r + + """ + + import os + + if r['return']>0: + if self.debug: + raise Exception(r['error']) + + module_path = r.get('module_path', '') + lineno = r.get('lineno', '') + + message = '' + + if not self.logger == None or (module_path != '' and lineno != ''): + call_stack = self.state.get('call_stack', []) + + if not self.logger == None: + + self.log(f"x error call stack: {call_stack}", "debug") + self.log(f"x error: {r}", "debug") + + sys.stderr.write('='*60 + '\n') + + if not self.logger == None: + sys.stderr.write('CMX call stack:\n') + + for cs in call_stack: + sys.stderr.write(f' * {cs}\n') + + message += '\n' + else: + message += '\n' + + message += self.cfg['error_prefix2'] + + if module_path != '' and lineno !='': + message += f' in {module_path} ({lineno}):\n\n' + else: + message += ': ' + + message += r['error'] + '\n' + + sys.stderr.write(message) + + return r + + ############################################################ + def prepare_error(self, returncode, error): + """ + Prepare error dictionary with the module and line number of an error + + Args: + returncode (int): CMX returncode + error (str): error message + + Returns: + (dict): r + return (int) + error (str) + module_path (str): path to module + lineno (int): line number + + """ + + from inspect import getframeinfo, stack + + caller = getframeinfo(stack()[1][0]) + + return {'return': returncode, + 'error': error, + 'module_path': caller.filename, + 'lineno': caller.lineno} + + ############################################################ + def embed_error(self, r): + """ + Embed module and line number to an error + + Args: + r (dict): CM return dict + + Returns: + (dict): r + return (int) + error (str) + module_path (str): path to module + lineno (int): line number + + """ + + from inspect import getframeinfo, stack + + caller = getframeinfo(stack()[1][0]) + + r['module_path'] = caller.filename + r['lineno'] = caller.lineno + + return r + ############################################################ def halt(self, r): """ @@ -849,18 +959,6 @@ def x(self, i, out = None): meta = r) if r['return'] >0: - if r['return'] > 32: - print ('') - print ('CM Error Call Stack:') - - call_stack = self.state['call_stack'] - - for cs in call_stack: - print (f' {cs}') - - self.log(f"x error call stack: {call_stack}", "debug") - self.log(f"x error: {r}", "debug") - if use_raise: raise Exception(r['error']) @@ -1518,6 +1616,23 @@ def error(i): return cm.error(i) +############################################################ +def errorx(i): + """ + Automatically initialize CM and print error if needed + without the need to initialize and customize CM class. + Useful for Python automation scripts. + + See CM.error function for more details. + """ + + global cm + + if cm is None: + cm=CM() + + return cm.errorx(i) + ############################################################ def halt(i): """ diff --git a/cm/cmind/utils.py b/cm/cmind/utils.py index 56c19f2c9..c8a964ed5 100644 --- a/cm/cmind/utils.py +++ b/cm/cmind/utils.py @@ -1928,7 +1928,7 @@ def convert_dictionary(d, key, sub = True): return dd ############################################################################## -def test_input(i, module): +def test_input(i): """ Test if input has keys and report them as error """ @@ -1939,9 +1939,10 @@ def test_input(i, module): unknown_keys = i.keys() unknown_keys_str = ', '.join(unknown_keys) + x = '' if len(unknown_keys) == 1 else 's' + r = {'return': 1, - 'error': 'unknown input key(s) "{}" in module {}'.format(unknown_keys_str, module), - 'module': module, + 'error': f'unknown input key{x}: {unknown_keys_str}', 'unknown_keys': unknown_keys, 'unknown_keys_str': unknown_keys_str}