From 27ea8bfb7ca794b880114d48b45a2be935d31df8 Mon Sep 17 00:00:00 2001 From: Sebastian Spreizer Date: Mon, 21 Oct 2024 10:38:46 +0200 Subject: [PATCH 1/4] Add line number to error message --- pynest/nest/server/hl_api_server.py | 142 ++++++++++++++++++++-------- 1 file changed, 101 insertions(+), 41 deletions(-) diff --git a/pynest/nest/server/hl_api_server.py b/pynest/nest/server/hl_api_server.py index ff0bd2f361..07728a9e0f 100644 --- a/pynest/nest/server/hl_api_server.py +++ b/pynest/nest/server/hl_api_server.py @@ -36,8 +36,7 @@ from flask import Flask, jsonify, request from flask.logging import default_handler from flask_cors import CORS -from werkzeug.exceptions import abort -from werkzeug.wrappers import Response +from nest.lib.hl_api_exceptions import NESTError # This ensures that the logging information shows up in the console running the server, # even when Flask's event loop is running. @@ -189,41 +188,36 @@ def index(): def do_exec(args, kwargs): - try: - source_code = kwargs.get("source", "") - source_cleaned = clean_code(source_code) - - locals_ = dict() - response = dict() - if RESTRICTION_DISABLED: - with Capturing() as stdout: - globals_ = globals().copy() - globals_.update(get_modules_from_env()) - exec(source_cleaned, globals_, locals_) - if len(stdout) > 0: - response["stdout"] = "\n".join(stdout) - else: - code = RestrictedPython.compile_restricted(source_cleaned, "", "exec") # noqa - globals_ = get_restricted_globals() + source_code = kwargs.get("source", "") + source_cleaned = clean_code(source_code) + + locals_ = dict() + response = dict() + if RESTRICTION_DISABLED: + with Capturing() as stdout: + globals_ = globals().copy() globals_.update(get_modules_from_env()) - exec(code, globals_, locals_) - if "_print" in locals_: - response["stdout"] = "".join(locals_["_print"].txt) - - if "return" in kwargs: - if isinstance(kwargs["return"], list): - data = dict() - for variable in kwargs["return"]: - data[variable] = locals_.get(variable, None) - else: - data = locals_.get(kwargs["return"], None) - response["data"] = nest.serialize_data(data) - return response + get_or_error(exec)(source_cleaned, globals_, locals_) + if len(stdout) > 0: + response["stdout"] = "\n".join(stdout) + else: + code = RestrictedPython.compile_restricted(source_cleaned, "", "exec") # noqa + globals_ = get_restricted_globals() + globals_.update(get_modules_from_env()) + get_or_error(exec)(code, globals_, locals_) + if "_print" in locals_: + response["stdout"] = "".join(locals_["_print"].txt) + + if "return" in kwargs: + if isinstance(kwargs["return"], list): + data = dict() + for variable in kwargs["return"]: + data[variable] = locals_.get(variable, None) + else: + data = locals_.get(kwargs["return"], None) - except Exception as e: - for line in traceback.format_exception(*sys.exc_info()): - print(line, flush=True) - flask.abort(EXCEPTION_ERROR_STATUS, str(e)) + response["data"] = get_or_error(nest.serialize_data)(data) + return response def log(call_name, msg): @@ -336,10 +330,42 @@ def __exit__(self, *args): sys.stdout = self._stdout +class ErrorHandler(Exception): + status_code = 400 + lineno = -1 + + def __init__(self, message: str, lineno: int = None, status_code: int = None, payload=None): + super().__init__() + self.message = message + if status_code is not None: + self.status_code = status_code + if lineno is not None: + self.lineno = lineno + self.payload = payload + + def to_dict(self): + rv = dict(self.payload or ()) + rv["message"] = self.message + if self.lineno != -1: + rv["lineNumber"] = self.lineno + return rv + + +# https://flask.palletsprojects.com/en/2.3.x/errorhandling/ +@app.errorhandler(ErrorHandler) +def error_handler(e): + return jsonify(e.to_dict()), e.status_code + + def clean_code(source): codes = source.split("\n") - code_cleaned = filter(lambda code: not (code.startswith("import") or code.startswith("from")), codes) # noqa - return "\n".join(code_cleaned) + codes_cleaned = [] # noqa + for code in codes: + if code.startswith("import") or code.startswith("from"): + codes_cleaned.append("#" + code) + else: + codes_cleaned.append(code) + return "\n".join(codes_cleaned) def get_arguments(request): @@ -368,6 +394,19 @@ def get_arguments(request): return list(args), kwargs +def get_lineno(err, tb_idx): + lineno = -1 + if hasattr(err, "lineno") and err.lineno is not None: + lineno = err.lineno + else: + tb = sys.exc_info()[2] + # if hasattr(tb, "tb_lineno") and tb.tb_lineno is not None: + # lineno = tb.tb_lineno + # else: + lineno = traceback.extract_tb(tb)[tb_idx][1] + return lineno + + def get_modules_from_env(): """Get modules from environment variable NEST_SERVER_MODULES. @@ -400,10 +439,31 @@ def get_or_error(func): def func_wrapper(call, args, kwargs): try: return func(call, args, kwargs) - except Exception as e: - for line in traceback.format_exception(*sys.exc_info()): - print(line, flush=True) - flask.abort(EXCEPTION_ERROR_STATUS, str(e)) + + except NESTError as err: + error_class = err.errorname + " (NESTError)" + detail = err.errormessage + lineno = get_lineno(err, 1) + + except (KeyError, SyntaxError, TypeError, ValueError) as err: + error_class = err.__class__.__name__ + detail = err.args[0] + lineno = get_lineno(err, 1) + + except Exception as err: + error_class = err.__class__.__name__ + detail = err.args[0] + lineno = get_lineno(err, -1) + + for line in traceback.format_exception(*sys.exc_info()): + print(line, flush=True) + + if lineno == -1: + message = "%s: %s" % (error_class, detail) + else: + message = "%s at line %d: %s" % (error_class, lineno, detail) + + raise ErrorHandler(message, lineno) return func_wrapper From 16101aeaf0f9bf6a0f2b36b63345705bd0e52652 Mon Sep 17 00:00:00 2001 From: Sebastian Spreizer Date: Mon, 21 Oct 2024 10:56:01 +0200 Subject: [PATCH 2/4] Fix get_or_eror --- pynest/nest/server/hl_api_server.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pynest/nest/server/hl_api_server.py b/pynest/nest/server/hl_api_server.py index 07728a9e0f..224d76b134 100644 --- a/pynest/nest/server/hl_api_server.py +++ b/pynest/nest/server/hl_api_server.py @@ -436,9 +436,9 @@ def get_modules_from_env(): def get_or_error(func): """Wrapper to get data and status.""" - def func_wrapper(call, args, kwargs): + def func_wrapper(call, *args, **kwargs): try: - return func(call, args, kwargs) + return func(call, *args, **kwargs) except NESTError as err: error_class = err.errorname + " (NESTError)" From 0d1265f3395e85d2db69dd56b06ad39571d3809a Mon Sep 17 00:00:00 2001 From: Sebastian Spreizer Date: Wed, 11 Dec 2024 14:07:22 +0100 Subject: [PATCH 3/4] Add docstring for `clean_code` --- pynest/nest/server/hl_api_server.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pynest/nest/server/hl_api_server.py b/pynest/nest/server/hl_api_server.py index 224d76b134..752c4e25e7 100644 --- a/pynest/nest/server/hl_api_server.py +++ b/pynest/nest/server/hl_api_server.py @@ -357,6 +357,7 @@ def error_handler(e): return jsonify(e.to_dict()), e.status_code +# It comments lines starting with 'import' or 'from' otherwise the line number of error would be wrong. def clean_code(source): codes = source.split("\n") codes_cleaned = [] # noqa From 91eea2715cb9fe2ada5f30082d50a4be8a68ee9d Mon Sep 17 00:00:00 2001 From: Sebastian Spreizer Date: Wed, 11 Dec 2024 14:07:52 +0100 Subject: [PATCH 4/4] Remove commented lines in `get_lineno` --- pynest/nest/server/hl_api_server.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/pynest/nest/server/hl_api_server.py b/pynest/nest/server/hl_api_server.py index 752c4e25e7..eb75763757 100644 --- a/pynest/nest/server/hl_api_server.py +++ b/pynest/nest/server/hl_api_server.py @@ -401,9 +401,6 @@ def get_lineno(err, tb_idx): lineno = err.lineno else: tb = sys.exc_info()[2] - # if hasattr(tb, "tb_lineno") and tb.tb_lineno is not None: - # lineno = tb.tb_lineno - # else: lineno = traceback.extract_tb(tb)[tb_idx][1] return lineno