Search code examples
pythoncontextmanager

Is it possible to access enclosing context manager?


There are essentially three ways to use the with statement:

Use an existing context manager:

with manager:
    pass

Create a context manager and bind its result to a variable:

with Manager() as result:
    pass

Create an context manager and discard its return value:

with Manager():
    pass

If we have place a function get_manager() inside the three with blocks above, is there any implementation that can return the enclosing context manager, or at least their __exit__ function?

It's obviously easy in the first case, but I can't think of a way to make it work in the other two. I doubt it's possible to get the entire context manager, since the value stack is popped immediately after the SETUP_WITH opcode. However, since the __exit__ function is stored on the block stack by SETUP_WITH, is there some way to access it?


Solution

  • Unfortunately, as discussed in the comments, this is not possible in all cases. When a context manager is created, the following code is run (in cPython 2.7, at least. I can't comment on other implementations):

        case SETUP_WITH:
        {
            static PyObject *exit, *enter;
            w = TOP();
            x = special_lookup(w, "__exit__", &exit);
            if (!x)
                break;
            SET_TOP(x);
            /* more code follows... */
        }
    

    The __exit__ method is pushed onto a stack with the SET_TOP macro, which is defined as:

    #define SET_TOP(v)        (stack_pointer[-1] = (v))
    

    The stack pointer, in turn, is set to the top of the frame's value stack at the start of frame eval:

    stack_pointer = f->f_stacktop;
    

    Where f is a frame object defined in frameobject.h. Unfortunately for us, this is where the trail stops. The python accessible frame object is defined with the following methods only:

    static PyMemberDef frame_memberlist[] = {
        {"f_back",          T_OBJECT,       OFF(f_back),    RO},
        {"f_code",          T_OBJECT,       OFF(f_code),    RO},
        {"f_builtins",      T_OBJECT,       OFF(f_builtins),RO},
        {"f_globals",       T_OBJECT,       OFF(f_globals), RO},
        {"f_lasti",         T_INT,          OFF(f_lasti),   RO},
        {NULL}      /* Sentinel */
    };
    

    Which, unfortunaltey, does not include the f_valuestack that we would need. This makes sense, since f_valuestack is of the type PyObject **, which would need to be wrapped in an object to be accessible from python any way.

    TL;DR: The __exit__ method we're looking for is only located in one place, the value stack of a frame object, and cPython doesn't make the value stack accessible to python code.