Inspired by this post and others I got to this:
Basically it's building off everything what has been said but there's also the ctypes.pythonapi.PyFrame_LocalsToFast which enables you to modify variables at various scopes which enabled me to make a pretty cool function.
Enjoy:
import ctypes
from inspect import currentframe
from IPython import get_ipython
from types import FrameType
from typing import Any
def has_IPython() -> bool:
"""Checks for IPython"""
return get_ipython() != None
class nonlocals:
"""
Equivalent of nonlocals()
# code reference: jsbueno (2023) https://stackoverflow.com/questions/8968407/where-is-nonlocals,CC BY-SA 4.0
# changes made: condensed the core concept of using a stackframe with getting the keys from the
# locals dict since every nonlocal should be local as well and made a class
"""
def __init__(self,frame: FrameType|None=None) -> None:
self.frame=frame if frame else currentframe().f_back
self.locals=self.frame.f_locals
def __repr__(self) -> str: return repr(self.nonlocals)
@property
def nonlocals(self) -> dict:
names=self.frame.f_code.co_freevars
return {key:value for key,value in self.locals.items() if key in names} if len(names) else {}
def check(self,key: Any) -> None:
if key not in self.nonlocals: raise KeyError(key)
def __getitem__(self,key: Any) -> Any: return self.nonlocals[key]
def update(self,dct) -> None:
for key,value in dct.items(): self[key]=value
def get(self,key,default=None) -> Any: return self.nonlocals.get(key,default=default)
def __setitem__(self,key: Any,value: Any) -> None:
self.check(key)
self.locals[key]=value
# code reference: MariusSiuram (2020). https://stackoverflow.com/questions/34650744/modify-existing-variable-in-locals-or-frame-f-locals,CC BY-SA 4.0
ctypes.pythonapi.PyFrame_LocalsToFast(ctypes.py_object(self.frame), ctypes.c_int(0))
def __delitem__(self,key: Any) -> None:
self.check(key)
del self.locals[key]
# code reference: https://stackoverflow.com/questions/76995970/explicitly-delete-variables-within-a-function-if-the-function-raised-an-error,CC BY-SA 4.0
ctypes.pythonapi.PyFrame_LocalsToFast(ctypes.py_object(self.frame), ctypes.c_int(1))
## Bonus function ##
class scope:
"""
gets the function name at frame-depth and the current scope that's within the main program
Note: if using in jupyter notebook scope.scope will remove jupyter notebook specific attributes
that record in program inputs and outputs. These attributes will still be available just not via
scope.scope because it causes a recursion error from some of the attributes changing while in use
How to use:
def a():
c=3
def b():
c
y=4
print(scope(1).locals)
print(scope().locals)
print(scope().nonlocals)
scope(1)["c"]=7
print(scope(1).locals)
print(scope().locals)
print(scope().nonlocals)
b()
print(c)
a()
## i.e. should print:
{'b': <function a.<locals>.b at 0x000001DD9DFEDB20>, 'c': 3}
{'y': 4, 'c': 3}
{'c': 3}
{'b': <function a.<locals>.b at 0x000001DD9DFEDB20>, 'c': 7}
{'y': 4, 'c': 7}
{'c': 7}
7
This allows us to change variables at any stack frame so long as it's on the stack
"""
def __init__(self,depth: int=0) -> None:
## get the global_frame, local_frame, and name of the call in the stack
global_frame,local_frame,name=currentframe(),{},[]
while global_frame.f_code.co_name!="<module>":
name+=[global_frame.f_code.co_name]
global_frame=global_frame.f_back
if len(name)==depth+1: local_frame=(global_frame,) # to create a copy otherwise it's a pointer
## instantiate
if depth > (temp:=(len(name)-1)): raise ValueError(f"the value of 'depth' exceeds the maximum stack frame depth allowed. Max depth allowed is {temp}")
name=["__main__"]+name[::-1][:-(1+depth)]
self.depth=len(name)-1
self.name=".".join(name)
self.local_frame,self.global_frame=local_frame[0],global_frame
self.locals,self.globals,self.nonlocals=local_frame[0].f_locals,global_frame.f_locals,nonlocals(local_frame[0])
def __repr__(self) -> str:
"""displays the current frames scope"""
return repr(self.scope)
@property
def scope(self) -> dict:
"""The full current scope"""
if has_IPython():
## certain attributes needs to be removed since it's causing recursion errors e.g. it'll be the notebook trying to record inputs and outputs most likely ##
not_allowed,current_scope=["_ih","_oh","_dh","In","Out","_","__","___"],{}
local_keys,global_keys=list(self.locals),list(self.globals)
for key in set(local_keys+global_keys):
if (re.match(r"^_i+$",key) or re.match(r"^_(\d+|i\d+)$",key))==None:
if key in not_allowed: not_allowed.remove(key)
elif key in local_keys:
current_scope[key]=self.locals[key]
local_keys.remove(key)
else: current_scope[key]=self.globals[key]
return current_scope
current_scope=self.globals.copy()
current_scope.update(self.locals)
return current_scope
def __getitem__(self,key: Any) -> Any: return self.locals[key] if key in self.locals else self.globals[key]
def update(self,dct) -> None:
for key,value in dct.items(): self[key]=value
def get(self,key,default=None) -> Any: return self.scope.get(key,default=default)
def __setitem__(self,key: Any,value: Any) -> None:
if key in self.locals:
self.locals[key]=value
# code reference: MariusSiuram (2020). https://stackoverflow.com/questions/34650744/modify-existing-variable-in-locals-or-frame-f-locals,CC BY-SA 4.0
ctypes.pythonapi.PyFrame_LocalsToFast(ctypes.py_object(self.local_frame), ctypes.c_int(0))
else: self.globals[key]=value
def __delitem__(self,key: Any) -> None:
if key in self.locals:
del self.locals[key]
# code reference: https://stackoverflow.com/questions/76995970/explicitly-delete-variables-within-a-function-if-the-function-raised-an-error,CC BY-SA 4.0
ctypes.pythonapi.PyFrame_LocalsToFast(ctypes.py_object(self.frame), ctypes.c_int(1))
else: del self.globals[key]