-1

I have defined a python context class and a Test class in a file:

class Test(object):
  pass

class MyContext(object):
  def __init(self):
     self._vars = []
  def __enter__(self):
    pass
  def __exit(self, ....):
    pass

In another file using that context:

from somewhere import Test, MyContext
with MyContext() as ctx:
  mytest = Test()

So what I want to achieve is that when I exit the context, I want to be aware of the mytest instance created and add it in the ctx._vars = [<instance of Test >].

I don't want to have a ctx.add_var(mytest) method, I want those Test instances to be added automatically to the ctx instance.

jsbueno
  • 99,910
  • 10
  • 151
  • 209
Paul Ochon
  • 11
  • 1
  • Using the context doesn't mean that `ctx` is aware of the code inside your `with` statement. – dspencer Mar 23 '20 at 06:52
  • correct. an internediate solution would be to pass the locals() to ctx with a final line like. `ctx.get_nodes_from_locals(locals())` but i have to filter whats just inside the ctx and get rid of the outside that could exists. not bulletproof at the moment. – Paul Ochon Mar 23 '20 at 13:11
  • The closure is evaluated when you enter context. Changing that context from within suggests that you're using the wrong design tools for whatever you're designing. – Prune Mar 24 '20 at 05:36
  • (I removed the "python-contextvars" tag as those are something not related with the "context" managed by a with-block – jsbueno Apr 21 '20 at 14:21

1 Answers1

1

That is possible of being done, using Python's introspection capabilities, but you have to be aware this is not what the with context block was created for.

I agree it is a useful syntax construction that can be "deviated" to do things like what you want: annotate the objects created inside a code block in a "registry".

Before showing how to do that with a context manager consider if a class body would not suffice you. Using a class body this way also deviates from its primary purpose, but you have your "registry" for free:


from somewhere import Test, MyContext
class ctx:
  mytest = Test()

vars = ctx.__dict__.values()

In order to do that with a context manager, you have to inspect the local variables at the start and at the end of the with block. While that is not hard to do, it wuld not cover all instances of Test created - because if the code is like this:

mytests = []
with Mycontext as ctx:
    mytests.append(Test())

No new variable is created - so code tracking the local variables would not find anything. Code could be written to look recursively into variables with containers, such as dictionaries and lists - but then mytest() instances could be added to a container referenced as a global variable, or a variable in other module.

It turns out that a reliable way to track Test instances would be to instrument the Test class itself to annotate new instances ina registry. That is far easier and less depentend on "local variable introspection" tricks.

The code for that is somewhat like:


class Test(object):
  pass

class MyContext(object):
    def __init(self, *args):
        self.vars = []
        self.track = args
        self.original_new = {}

    def patch(self, cls_to_patch):
        cls_new = getattr(cls_to_patch, "__new__")
        if "__new__" in cls.__dict__:
            self.original_new[cls_to_patch] = cls_new

        def patched_new(cls, *args, **kwargs):
            instance = cls_new(*args, **kwags)
            self.vars.append(instance)
            return instance

        cls_to_patch.__new__ = patched_new        

    def restore(self, cls):
        if cls in self.original_new:
            # class had a very own __new_ prior to patching
            cls.__new__ = self.original_new[cls]
        else:
            # just remove the wrapped new method, restores access to superclass `__new__`
            del cls.__new__

    def __enter__(self):
        for cls in self.track:
            self.patch(cls)
        return self

    def __exit(self, ....):
        for cls in self.track:
            self.restore(cls)
        ...


from somewhere import Test, MyContext
with MyContext(Test) as ctx:
  mytest = Test()

jsbueno
  • 99,910
  • 10
  • 151
  • 209