1

The context

I'm trying to create an "environment" context manager. Think of it as choosing to execute some code locally or remotely depending on a parameter of the context manager:

with PythonExecutor(env="local"):
    x = 1
    assert x == 1

would run that code in-process. However, changing the env parameter to "remote" would connect to SSH and execute the code remotely.

Thanks to this StackOverflow question, I managed to extract the code within the with block as a string in the __exit__ method and the SSH part is trivial (and irrelevant for that question).

The question

How can I prevent the code within the with block to run in-process? Context managers always follow:

  1. Calling __enter__
  2. Executing the code within the with block
  3. Calling __exit__

This means that even if I choose "remote" execution, the code will be executed remotely in __enter__ or __exit__, but it will still be executed locally. In other words, is there some way to skip step 2? I started looking into runtime bytecode manipulation but it's getting a bit hairy…

Other solutions to the original issue (running code in different environments in an elegant way) are welcome too

filaton
  • 2,257
  • 17
  • 27
  • What about doing this with a decorator, running a *function* remotely or locally? – 2e0byo Sep 27 '21 at 13:40
  • The general problem here is this: can you "lift" the body of a `with` statement into an implicitly defined function, which can be executed locally or remotely. – chepner Sep 27 '21 at 13:42
  • The with statement is just syntactic sugar. Your code will run according to the function definition, not the with statement. – alec_djinn Sep 27 '21 at 13:55

2 Answers2

1

It's a bit hacky and requires changing the code of the with block slightly but you could make your __enter__ method return a function that raises an error when env == 'remote'. Then on the remote case you'll get a local error and then handle everything else in the __exit__ block.

class PythonExecutor:

    def __init__(self, env):
        self.env = env

    def __enter__(self):
        def switch():
            if self.env == 'remote':
                raise Exception # Probably some custom exception for only this purpose
        return switch

    def __exit__(self, exctype, excinst, exctb):
        # do ssh stuff here depending on exctype
        ...


with PythonExecutor(env='remote') as switch:
    switch()
    print('hello')
Kyle Parsons
  • 1,475
  • 6
  • 14
  • Thanks, hacky indeed but does the trick :) Just a bit sad to have to change the `with` block… – filaton Sep 27 '21 at 16:54
0

What about doing this with a decorator on functions? (You did say other ways of solving the problem were welcome...)

def remote(func):
    def execute():
        print(f"running {func.__name__} remotely")
        return func()

    return execute

@remote
def func():
    print("hello")

func()

Or for the same semantics:

def Env(env="local"):
    if env == "remote":
        return remote
    else:
        return lambda x: x

@Env("remote")
def func2():
    print("hello again")

@Env("local")
def func3():
    print("hello again")

func2()
func3()

I realise you have almost certainly thought of this approach and rejected it for some reason, but on the offchance, it's worth mentioning.

Whether it is possible to get the content of a with block into some kind of function-like thing I don't know. As written this method of course requires you to define your code as functions.

2e0byo
  • 5,305
  • 1
  • 6
  • 26
  • Thanks for your answer. I should have mentioned that it's exactly the approach I have in place right now :) We would like to move away from it as it's not always so clean from the user perspective (although definitely cleaner behind the scenes :D) – filaton Sep 27 '21 at 16:55