Alex Deft
Alex Deft

Reputation: 2787

`globals` not working inside a nested function

I'm making a decorator that allows me to make functions run as if their lines of code are written in main. To achieve this, I'm using globals().update(vars()) inside the function (works), but now inside the decorator it fails.

class Debugger:
    @staticmethod
    def in_main(func):  # a decorator
        def wrapper():  # a wrapper that remembers the function func because it was in the closure when construced.
            exec(Debugger.convert_to_global(func))  # run the function here.
            globals().update(vars())  # update the global variables with whatever was defined as a result of the above.
        return wrapper


    @staticmethod
    def convert_to_global(name):
        """Takes in a function name, reads it source code and returns a new version of it that can be run in the main.
        """
        import inspect
        import textwrap

        codelines = inspect.getsource(name)
        # remove def func_name() line from the list
        idx = codelines.find("):\n")
        header = codelines[:idx]
        codelines = codelines[idx + 3:]

        # remove any indentation (4 for funcs and 8 for classes methods, etc)
        codelines = textwrap.dedent(codelines)

        # remove return statements
        codelines = codelines.split("\n")
        codelines = [code + "\n" for code in codelines if not code.startswith("return ")]

        code_string = ''.join(codelines)  # convert list to string.

        temp = inspect.getfullargspec(name)
        arg_string = """"""
        # if isinstance(type(name), types.MethodType) else tmp.args
        if temp.defaults:  # not None
            for key, val in zip(temp.args[1:], temp.defaults):
                arg_string += f"{key} = {val}\n"
        if "*args" in header:
            arg_string += "args = (,)\n"
        if "**kwargs" in header:
            arg_string += "kwargs = {}\n"
        result = arg_string + code_string
        return result  # ready to be run with exec()

example of reproducible failure:


@Debugger.in_main
def func():
    a = 2
    b = 22

func()

print(a)

Gives

NameError: name 'a' is not defined

Upvotes: 1

Views: 72

Answers (1)

Lior Cohen
Lior Cohen

Reputation: 5745

  1. I am not sure what you are trying to do. But messing with variables scope and encapsulation may yield very bad and unpredictable and undebuggable behavior. I STRINGLY NOT RECOMMNED IT.

  2. Now to your problem:

globals() is a tricky one as there is no one globals() dictionary.

You can verify it easily by print(id(globals()) and see.

What is working for your example is mutating the __main__ module __dict__ with the exec results.

This is done by exec(the_code_you_want_to_run, sys.modules['__main__'].__dict__. The globals().update(vars()) is not needed.

Here is the code:

import sys
class Debugger:
    @staticmethod
    def in_main(func):  # a decorator
        def wrapper():  # a wrapper that remembers the function func because it was in the closure when construced.
            exec(Debugger.convert_to_global(func), sys.modules['__main__'].__dict__)  # run the function here on the scope of __main__ __dict__
            # globals().update(vars())  # NOT NEEDED
        return wrapper


    @staticmethod
    def convert_to_global(name):
        """Takes in a function name, reads it source code and returns a new version of it that can be run in the main.
        """
        import inspect
        import textwrap

        codelines = inspect.getsource(name)
        # remove def func_name() line from the list
        idx = codelines.find("):\n")
        header = codelines[:idx]
        codelines = codelines[idx + 3:]

        # remove any indentation (4 for funcs and 8 for classes methods, etc)
        codelines = textwrap.dedent(codelines)

        # remove return statements
        codelines = codelines.split("\n")
        codelines = [code + "\n" for code in codelines if not code.startswith("return ")]

        code_string = ''.join(codelines)  # convert list to string.

        temp = inspect.getfullargspec(name)
        arg_string = """"""
        # if isinstance(type(name), types.MethodType) else tmp.args
        if temp.defaults:  # not None
            for key, val in zip(temp.args[1:], temp.defaults):
                arg_string += f"{key} = {val}\n"
        if "*args" in header:
            arg_string += "args = (,)\n"
        if "**kwargs" in header:
            arg_string += "kwargs = {}\n"
        result = arg_string + code_string
        return result  # ready to be run with exec()

Upvotes: 1

Related Questions