0

I have the following problem. I have multiple unittests, each one having a context manager to open a browser and do some selenium testing.

I want to ensure that I can run the tests in parallel, and that the browser window is closed in case of error, so I use a context manager:

def test_xxx():
    with webapp() as p:                                                                   
        file_loader = FileLoader(p, id="loader").upload(path)

As you can see, I use a class FileLoader, which takes the context manager web application (basically a wrapper of the selenium driver) and uses it to encapsulate the rigmarole required to upload the file.

My objective would be not to have to specify the p parameter to FileLoader(), so that I could write

def test_xxx():
    with webapp():                                                                   
        file_loader = FileLoader(id="loader").upload(path)

I could use a global that is assigned when the context manager is opened, but this would prevent any isolation when tests run in parallel. Suppose that one test connects to site A, and another test connects to site B. I need two drivers, each connected to a different site.

In other words, how can I design FileLoader to be aware of its enclosing context manager without passing the context variable?

Stefano Borini
  • 138,652
  • 96
  • 297
  • 431
  • 3
    "My objective would be..." Why? Is this really worth it? Isn't it much more explicit the way it is? – tobias_k Nov 15 '21 at 09:21
  • 1
    By "parallel", do you mean concurrently (i.e. in different threads)? [*explicit is better than implicit*](https://www.python.org/dev/peps/pep-0020/#the-zen-of-python), so I very much favor the `FileLoader(p, id="loader")` version over `FileLoader(id="loader")`. Imagine reading something like `with open('data.json'): data = json.load()`; it just doesn't look as clear. Alternatively you could design a `FileLoaderWebapp` as a context manager, which wraps the `webapp()` and `FileLoader(...)` stuff, and then use it as `with FileLoaderWebapp() as p: p.upload(path)`. – a_guest Nov 16 '21 at 10:45
  • I agree with @tobias_k that you must have some reasons to do this, but which stay unclear from what you shared, so it's hard to help. Another simple way to hide the argument would be creating a `functools.partial` first: `pFileLoader = partial(FileLoader, p)`. Then `pFileLoader` works without the arg but is contextualised to `p`. – Jeronimo Nov 16 '21 at 12:30

1 Answers1

3

By using the inspect module, a code can read the local variables of its caller. It is a rather unusual if not dangerous use, because it actually boils down to a non standard and non conventional way of passing a parameter to a function. But if you really want to go that way, this other SO question gives a possible way.

Demo:

class ContextAware:
    """ Class that will copy  the x local variable of its caller if any"""
    def __init__(self):
        # uncomment next line for debugging
        # print("ContextAware", inspect.currentframe().f_back.f_locals)
        self.x = inspect.currentframe().f_back.f_locals.get('x')

        
def foo(x):
    # the value of x is not explicitely passed, yet it will be used...
    return ContextAware()

The object created in foo is aware of the x variable of its caller:

>>> a = foo(4)
>>> a.x
4
>>> a = foo(6)
>>> a.x
6

That means you you could write something close to:

def test_xxx():
    with webapp() as ctx_p:
        file_loader = FileLoader(id="loader").upload(path)

provided the __init__ method of FileLoader spies on the ctx_p local variable of its caller.

Serge Ballesta
  • 143,923
  • 11
  • 122
  • 252
  • That's nice. As you said, it's a bit unusual, but I was wondering if you have also some design suggestions to create something that feels more natural. – Stefano Borini Nov 15 '21 at 12:50
  • 1
    AFAIK, the natural way would be to explicitely pass the parameter to the function or the constructor – Serge Ballesta Nov 15 '21 at 13:12