Reputation: 2787
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
Reputation: 5745
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.
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