From 98618ba39a0193ca6ea101994f87592da7ddb742 Mon Sep 17 00:00:00 2001 From: Mark Feit Date: Fri, 15 Mar 2024 11:27:24 -0400 Subject: [PATCH] Make jq errors cleaner for users when a header script is prepended. #1414 (#1418) --- .../pscheduler-core/validate-limits.raw | 4 +-- .../pscheduler/pscheduler/jqfilter.py | 35 +++++++++++++++++-- .../pscheduler/limitprocessor/rewriter.py | 18 ++++++---- 3 files changed, 45 insertions(+), 12 deletions(-) diff --git a/pscheduler-core/pscheduler-core/validate-limits.raw b/pscheduler-core/pscheduler-core/validate-limits.raw index 3839bd3b71..22d7f971c6 100755 --- a/pscheduler-core/pscheduler-core/validate-limits.raw +++ b/pscheduler-core/pscheduler-core/validate-limits.raw @@ -67,11 +67,9 @@ except IOError as ex: try: processor = LimitProcessor(infile) except Exception as ex: - pscheduler.fail(str(ex)) + pscheduler.fail(f'Limit configuration is invalid:\n{str(ex)}') if sys.stdout.isatty() and not options.quiet: print("Limit configuration is valid.") pscheduler.succeed() - - diff --git a/python-pscheduler/pscheduler/pscheduler/jqfilter.py b/python-pscheduler/pscheduler/pscheduler/jqfilter.py index 7f9ed1855f..df4d528b19 100644 --- a/python-pscheduler/pscheduler/pscheduler/jqfilter.py +++ b/python-pscheduler/pscheduler/pscheduler/jqfilter.py @@ -76,9 +76,9 @@ def __init__( args={}, output_raw=False, groom=False, + strip_errors_to=None ): - """ - Construct a filter. Arguments: + """Construct a filter. Arguments: filter_spec - The JQ script to be used for this filter. This may be any subclass of basestring, a list or a dict. Strings @@ -95,6 +95,11 @@ def __init__( top of the script before compiling. This allows filters prepending functions to user-provided scripts that do imports to compile properly. + + strip_errors_to - Strip all lines in error messages up to the + string provided. This is used to weed known-correct, + program-provided code out of errors on the first line so the + user sees the correct line numbers. """ self.output_raw = output_raw @@ -109,6 +114,9 @@ def __init__( if not isinstance(filter_spec, str): raise ValueError("Filter spec must be plain text, list or dict") + if strip_errors_to is not None and not isinstance(strip_errors_to, str): + raise ValueError("Strip string must be a string") + # Passing an args hash into PyJQ causes a memory corruption # problem. As a temporary workaround, turn them into "as" # statements. (See #1059) @@ -125,7 +133,28 @@ def __init__( if groom: filter_spec = _groom(filter_spec) - self.script = pyjq.compile(filter_spec, args, library_paths=_library_path()) + value_error = None + try: + self.script = pyjq.compile(filter_spec, args, library_paths=_library_path()) + except ValueError as ex: + # This is held and thrown seprately because Python will + # produce a confusing nested exception message when it's + # thrown. + value_error = ex + + if value_error is not None: + + if strip_errors_to is not None: + ex_lines = [] + strip_length = len(strip_errors_to) + for num, line in enumerate(str(value_error).split('\n')): + place = line.find(strip_errors_to) + if place != -1: + line = line[place+strip_length:] + ex_lines.append(line.rstrip()) + value_error = ValueError('\n'.join(ex_lines)) + + raise value_error diff --git a/python-pscheduler/pscheduler/pscheduler/limitprocessor/rewriter.py b/python-pscheduler/pscheduler/pscheduler/limitprocessor/rewriter.py index f1acaebfe1..a681259651 100644 --- a/python-pscheduler/pscheduler/pscheduler/limitprocessor/rewriter.py +++ b/python-pscheduler/pscheduler/pscheduler/limitprocessor/rewriter.py @@ -29,8 +29,7 @@ def __init__(self, else: script = transform["script"] - - script_lines = [ + SCRIPT_HEADER = [ "def classifiers:", " ." + self.PRIVATE_KEY + ".classifiers", @@ -59,15 +58,22 @@ def __init__(self, " error(\"Task rejected: \" + ($message | tostring))", ";", - script - ] + "def __END_HEADER__:", + " .", + ";", + "__END_HEADER__ | " + ] - transform["script"] = script_lines + # Stuff the entire header onto the first line so errors + # reflect the line numbers of what the user provided. + + transform["script"] = ' '.join(SCRIPT_HEADER) + script self.transform = JQFilter( filter_spec=transform, args=transform.get("args", {}), - groom=True + groom=True, + strip_errors_to='__END_HEADER__ | ' )