Exec() with 'return' keyword

Python ‘exec’ is great for short evaluation of code. However, i want to make this into ‘step by step’, REPL like function.

generally, if with the described ability, the following code should run fine:

def function():
    value = exec('return 1')

print(function()) # 1

If anyone wants to know how I use this in real world (not yet since python has not added this feature yet), here’s my code: (ps: skipException is a decorator)

from contextlib import suppress
import traceback


def skipException(
    debug_flag=False, breakpoint_flag=False, delayAfterException: int = 3, defaultReturn=None, global_variables:dict={}, local_variables:dict={}
):
    def wrapper(func):
        globals().update(global_variables)
        locals().update(local_variables)
        # myExec = lambda command, myGlobals, myLocals: exec(command) # new way of merging dicts in python 3.9, more 'functional'?
        def space_counter(line):
            counter = 0
            for x in line:
                if x == " ":
                    counter += 1
                else:
                    break
            return counter

        def remove_extra_return(code):
            while True:
                if "\n\n" in code:
                    code = code.replace("\n\n", "\n")
                else:
                    break
            return code

        def isEmptyLine(line):
            emptyChars = ["\n", "\t", "\r", " "]
            length = len(line)
            emptyCounts = 0
            for char in line:
                if char in emptyChars:
                    emptyCounts += 1
            return emptyCounts == length

        def getCodeBlocks(lines):
            mBlocks = []
            current_block = lines[0]
            lines = lines + [""]
            keywords = [" ", "def ", "async ", "with ", "class ", "@"]
            # keywords = [" ", "def", "async def", "with", "async with","class", "@"]
            for line in lines[1:]:
                if sum([line.startswith(keyword) for keyword in keywords]):
                    current_block += "\n"
                    current_block += line
                else:
                    mBlocks.append(current_block)
                    current_block = line
            return mBlocks

        def getExtendedLines(splited_code):
            splited_code = [x.rstrip() for x in splited_code]
            splited_code = "\n".join(splited_code).replace("\\\n", "")
            splited_code = remove_extra_return(splited_code)
            splited_code = splited_code.split("\n")
            return splited_code

        def reformatCode(func_code, MAXINT=10000000000, debug=False):
            # with open("test.py", "r") as f:
            code = func_code

            # need binary data.
            code_encoded = code.encode("utf-8")

            import subprocess

            command = (
                "autopep8 --max-line-length {MAXINT} - | black -l {MAXINT} -C -".format(
                    MAXINT=MAXINT
                )
            )
            commandLine = ["bash", "-c", command]
            result = subprocess.run(commandLine, input=code_encoded, capture_output=True)
            try:
                assert result.returncode == 0
                code_formatted = result.stdout.decode("utf-8")
            except:
                if debug:
                    import traceback

                    traceback.print_exc()
                    print("STDOUT", result.stdout)
                    print("STDERR", result.stderr)
                code_formatted = code
            if debug:
                print(code_formatted)
            return code_formatted

        def new_func(*args, **kwargs):
            func_name = func.__name__
            func_code = dill.source.getsource(func)
            # reformat the func code via our dearly autopep8-black formatter.
            func_code = reformatCode(func_code, debug=debug_flag)
            if debug_flag:
                print("########## FUNCTION CODE #########")
                print(
                    func_code
                )  # do not use chained decorator since doing so will definitely fail everything?
                print("########## FUNCTION CODE #########")
                print("########## FUNCTION #########")
            # print(func_code)
            func_code = remove_extra_return(func_code)
            splited_code = func_code.split("\n")
            splited_code = getExtendedLines(splited_code)
            # index 0: decorator
            # index 1: function name
            # no recursion support. may work inside another undecorated function.
            try:
                assert splited_code[0].strip().startswith("@skipException")
            except:
                raise Exception("Do not nesting the use of @skipException decorator")
            function_definition = splited_code[1]
            function_args = function_definition[:-1].replace("def {}".format(func_name), "")
            if debug_flag:
                print("FUNCTION ARGS:", function_args)
            kwdefaults = func.__defaults__
            pass_kwargs = {}

            if "=" in function_args:
                assert kwdefaults != None
                arg_remains = function_args.split("=")[0]
                kwarg_remains = function_args.replace(arg_remains, "")
                kwarg_extra_names = [
                    content.split(",")[-1].strip()
                    for index, content in enumerate(kwarg_remains.split("="))
                    if index % 2 == 1
                ]
                mfunctionArgsPrimitive = arg_remains.replace("(", "").split(",")
                kwarg_names = [mfunctionArgsPrimitive[-1].strip()] + kwarg_extra_names
                mfunctionArgs = mfunctionArgsPrimitive[:-1]
                if debug_flag:
                    print("PASSED KEYWORD ARGS:", kwargs)
                    print("KWARG NAMES:", kwarg_names)
                for key, value in zip(kwarg_names, kwdefaults):
                    pass_kwargs.update({key: value})
                for key in kwargs.keys():
                    assert key in kwarg_names
                    pass_kwargs[key] = kwargs[key]
            else:
                assert kwdefaults == None
                mfunctionArgs = function_args.replace("(", "").replace(")", "").split(",")
            mfunctionArgs = [x.strip() for x in mfunctionArgs]
            mfunctionArgs = [x for x in mfunctionArgs if not isEmptyLine(x)]
            if debug_flag:
                print("POSITIONAL ARGS:", mfunctionArgs)
            assert len(args) == len(mfunctionArgs)

            for key, value in zip(mfunctionArgs, args):
                exec("{} = {}".format(key, value))
            if kwdefaults is not None:
                for key, value in pass_kwargs.items():
                    exec("{} = {}".format(key, value))
            actualCode = splited_code[2:]
            actualCode = [x for x in actualCode if not isEmptyLine(x)]
            minIndent = min([space_counter(line) for line in actualCode])
            # split the code into different sections.
            if debug_flag:
                print(minIndent)
            newLines = [line[minIndent:] for line in actualCode]
            codeBlocks = getCodeBlocks(newLines)
            for block in codeBlocks:
                no_exception = False
                if debug_flag:
                    print("##########CODEBLOCK##########")
                    print(block)
                    print("##########CODEBLOCK##########")
                if block.startswith('return '):
                    returnName = "var_"+str(uuid.uuid4()).replace("-",'_')
                    block = "{} = {}".format(returnName,block[len('return '):])
                    exec(block)
                    value = locals().get(returnName)
                    return value
                elif block == "return":
                    return
                elif not debug_flag:
                    with suppress(Exception):
                        exec(block)
                        no_exception = True
                else:
                    try:
                        exec(block) #return outside of function?
                        no_exception = True
                    except:
                        traceback.print_exc()
                        if breakpoint_flag:
                            breakpoint()
                if not no_exception:
                    print("##########DELAY AFTER EXCEPTION##########")
                    import time

                    time.sleep(delayAfterException)
                    print("##########DELAY AFTER EXCEPTION##########")
            if debug_flag:
                print("########## FUNCTION #########")
            return defaultReturn
        return new_func
    return wrapper


def skipExceptionVerbose(func):
    return skipException(debug_flag=True)(func)


def skipExceptionBreakpoint(func):
    return skipException(breakpoint_flag=True)(func)


def skipExceptionDebug(func):
    return skipException(breakpoint_flag=True, debug_flag=True)(func)

No, it shouldn’t run fine. return outside of a function is illegal so it should be illegal when passed to exec as well. Otherwise exec runs a superset of Python: code that “works” in exec doesn’t work outside of exec. And that would be bad.

I’m both horrified and impressed by your hack to get it working. But what’s the purpose of it? Why not just write return 1 in your top level function instead of hacking the traceback to grab the source and then exec’ing it again without the return?

If you want “REPL like functionality”, you can use the code or cmd modules.

5 Likes

The hack above is the potential use case, not the solution to the problem.

The use case can be explained as skipping statements which cause trouble and continue executing the following statements.

Example:


def troubleFunction():
    a = 0
    print(b) # skipped
    return b # also skipped
    return a # successfully returned with value of a

val = troubleFunction()
print(val) # 0

c = 0
print(d) # skipped
print(c) # printing value of c

In addition to skipping exceptions, one can set breakpoints on exceptions and fix problems by entering code to replace the erroneous code on the fly (coding in the debugger), or showing the location and execution time where the code is currently running, and decide whether some code is blocking the program, automatically or manually timeout specific line of code.

That’s PHP’s mentality. Not Python’s.

For better debugging it is necessary to borrow some language features.

BASIC has it too. It is called ON ERROR RESUME NEXT.

Implementing ON ERROR RESUME NEXT in Python

google for ‘resumable exceptions’

reversible debugging

Not sure that that’s relevant; I can’t find anything about reversible debugging that says that buggy lines of code should be skipped so that execution can arbitrarily continue.

If you really think that this will help you to debug your code, go ahead, but I would never allow this sort of thing to pass code review.

1 Like

Allowing to skip the buggy code is just an option. There are many ways to fix a bug, and many more if a reversible debugger is avaliable.

Skipping the buggy code in accurate words is to resume execution even if a bug is encountered. This allows hot-patching of current code. Aside from that, many more features can be added to debugger to help better understanding of the code, saving time from rerunning the entire program.

I have done a LOT with hot-patching of live code, and one thing I can assure you is: you always need a way to get back to a clean state. Usually, that means returning to a well-known point of execution (a main loop of some sort) before continuing. For a very common example, consider a web server that, whenever it encounters a problem, reports an HTTP 500 error and goes back to handling more queries. That’s what a try/except block does well - it takes you back to a known location, it cleans up state, and it does NOT just go “well, let’s go to one line after where we were”.

After all - how far after the problem should you resume? One line? One statement? One CPython bytecode operation? How do you reason about code when parts of it could be skipped and then you keep on going?

Neither BASIC’s “resume next” nor PHP’s “let’s just keep going and spit information out into the web page” actually help with debugging code. Both of them have gotten thoroughly in the way of my debugging work, although BASIC hasn’t had a chance to since the early 2000s when I stopped working at a job where VB was in use.

There are many ways to fix a bug. They usually involve FINDING the bug first. In fact, most of the work of debugging is actually finding bugs. So which one would you rather have - an exception traceback that guarantees you that the error is on this line or earlier in execution, or undefined and inexplicable behaviour far down the line and, ultimately, just a wrong result?

1 Like

The default behavior is to stop on the bug, and if after review you think it is viable to continue to next line without changing/inserting new code at the spot, it is called skipping.

Debugging without rerunning is not always an option for everyone, even with reversible debugger. However, it is a time saver and its use cases are being recognized, especially among unknown and large projects.

Python already has that. It’s called try...except.

I don’t know what this has to do with REPL-like functionality.

Your troubleFunction would be written in Python as:


def troubleFunction():
    a = 0
    try:
        # Look for a global 'b'.
        print(b)
        return b
    except NameError:
        # If it doesn't exist, return 'a' instead.
        return a

Generally speaking, must errors are bugs that should be fixed, not “trouble” that should be worked around. Why doesn’t global variable b exist? 99 times out of 100, you should fix that bug.

Anyway, the pros and cons of exception handling, Basic’s “ON ERROR” handler, etc are independent of whether it makes sense to exec a bare return. A bare return is not executing inside a function, so there is nothing to return from or to return to.

As stated, reversible debuggers are commonly used in unknown and large projects. You could imagine how much time would be saved if errors can be live-patched instead of rerunning the whole program till next bug and again.

It’s not about code review, it is about the way to do code review, either by looking into the code itself, using a linter or just running the program. Reversible debugger makes it a lot easier for you to debug by runtime analysis and patching.

Reverse Debugging / Time Travel Debugging: A Brief History of Time

The exec function is designed as such so we can’t put return inside of it. But a reversible debugger must have ways to exec it somehow and make it possible to re-execute or cancel return statements.

Transforming every line of code into try...except statements is exactly what my skipException decorator is doing, but without the ability to exec the return statements. What I want to accomplish or find is just a reversible debugger.

I don’t need to imagine. I have multiple projects built on the model of live patching. And not one of them needs abnormal exception handling.

Live code editing is extremely hard to get right, if you don’t have a good framework to do it in. And that framework could take on a number of forms, but the one I invariably fall back on - due to its simplicity and ease of debugging - is the “core event loop” model. Generally speaking, these sorts of projects consist of a number of components.

  1. A kernel which does not ever get hot-reloaded. It handles basic initialization, then loads up all the other modules, and finally sets up the most basic event loop possible. In a Python project, that would probably end up dropping into an asyncio event loop or equivalent. In C, it’s probably something hand-rolled but would generally involve select()/epoll() and an event dispatch table. In Pike, I simply return -1 from main().
  2. A handful of core modules which provide crucial services to other modules. These modules make certain features available in some form of global registry. Some might set up socket servers or database connections, or even a GUI.
  3. “Leaf” modules that provide most of the user-facing functionality. As much as possible, these do not depend on each other. They use services from the core modules and get called from the central event loop. If something goes wrong, an exception is raised, and execution returns to the event loop.

This general three-tier model works fairly well for most of the projects I’ve built. And crucially, all debugging is done by returning to the main event loop after reporting an exception. In other words, standard try/except behaviour, just like you’d find in a typical Python web server or anything.

The beauty of it is that you don’t have to define the scope of what “caused trouble”; there are only a small handful of places that have exception-guard code (notably the core event loop, and sometimes there’ll be a separate one for services that can do better than the default). Everything else is simply built to keep returning to the main event loop.

Hackery to make statements skippable would not help with hot reloading.

Reversible debuggers, on the other hand, are extremely usable and useful; their main value is that they retain a lot of older state, not that they keep on executing. I don’t understand where you see a connection here, because reversible debuggers ONLY retain prior state, not future. If you can look at a crash report, then unwind the execution to see how things came to be the way they were, that’s effectively the same as recreating the problem but without having to recreate it. It is nothing to do with finding a way to “re-execute or cancel return statements”.

If you truly want a reversible debugger in Python, look into the trace feature and try retaining enough information to reconstruct the state.

People discuss about this along with live-code editing during debugging. Because it is implemented and used by the few, existing reversible debuggers may not explain how it will become in the future, or maybe ‘time-travel debugging/edit and continue’ is a better name.

Rudimentary live-patching is not perfect, but it can be a solution to fix problems in a highly interconnected project.

There is a python library offering ‘edit and continue’ ability during debugging.

I can’t see anything in that thread about skipping execution of anything. Just the exact same thing that everyone else was saying: retain state, which is very costly. If you perfectly retain all state, you can perfectly reconstruct state at the time of the exception and then you can change something and retry. (Or, more likely, you’ll retain state periodically, so you’ll effectively unwind a little bit from the point of the exception.)

Still not seeing anything that would skip over a failing statement. Nothing that requires the shenanigans and hackery with exec that you’re trying to do here.

In that page the author offered an example to input code, replace the buggy code and resume execution. Maybe restoring the state is important, but even if we can’t retain the state automatically, we can re-execute the following code along with user inserted code (manually retaining state or creating bug-free state) to fix problems.

What does all this discussion about reversible debugging have to do with exec("return 1")?

That could be the lead. Maybe it can be done in other ways.