79211089

Date: 2024-11-21 12:17:29
Score: 0.5
Natty:
Report link

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]
Reasons:
  • Blacklisted phrase (1): stackoverflow
  • Long answer (-1):
  • Has code block (-0.5):
  • Low reputation (1):
Posted by: Ben Tonks