Define a function at runtime taking its code from a string

I need my server-side code to execute a funcion whose code is stored (as a python file) in a media column (or, that is the same, in a string).
This server code is triggered in an HTTP API endpoint.
This is needed because that function is a validation function that must be defined in configuration, and is not know in advance.
Customer A may have its own validation function, customer B its own, etc etc.
Using uplink, I was able to do that with this code:

                evaluation_function_file = row['answers_script'] # db column with media object .py file
                print("Evaluation script:")                
                print(evaluation_function_file.get_bytes().decode('utf-8'))
                with tempfile.TemporaryDirectory() as tmpdirname:
                    print('Running in temporary directory:', tmpdirname)
                    filename = tmpdirname + '\\my_func.py'
                    anvil.media.write_to_file(media=evaluation_function_file,filename=filename)
                    sys.path.append(tmpdirname)
                    import my_func
                    result = my_func.my_func(questions_list=questions_list)
                    sys.path.pop()
                print("result:", result )

This code assumes that the function inside my_func.py is defined as

def my_func():

This code works on Uplink but does not work in Anvil server environment,
I get this app log:

Running in temporary directory:
/tmp/tmpi7_swy8c
ModuleNotFoundError: No module named 'my_func'
at callback, line 102
called from /downlink/anvil/_server.py, line 1288

Then I tried to avoid dynamic python files and keep it all in RAM:

                evaluation_function_file = row['answers_script'] # db column with media object .py file
                function_code_str = evaluation_function_file.get_bytes().decode('utf-8')
                print("Evaluation  script:")                
                print(evaluation_function_file.get_bytes().decode('utf-8'))
                exec(function_code_str)
                result = my_func(questions_list=questions_list)
                print("result:", result )

But that’s not working either.
I get this app log:

NameError: name 'my_func' is not defined
at callback, line 107
called from /downlink/anvil/_server.py, line 1288

BTW this second method doesn’t work in Uplink too.

Is there a way to do what I need?

That is: define a function whose code is determined at run-time getting it from a table-column (or, more in general, from a string) and call that function.

Or, is there some other approach to the problem, that is: how to validate a list of answers against a user-defined, runtime-defined function?

Thanks

Nevermind, I figured it out by myself.
When running on Anvil environment, this line

                    filename = tmpdirname + '\\my_func.py'

must become this line:

                    filename = tmpdirname + '/my_func.py'

Just a path building error.

BR.

1 Like

Careful, because this will write 1 twice:

  with tempfile.TemporaryDirectory() as tmpdirname:
    with open(tmpdirname + '/my_module.py', 'w') as f:
      f.write('def my_func():\n')
      f.write('  return 1\n')
    sys.path.append(tmpdirname)
    import my_module
  print(my_module.my_func())
  
  with tempfile.TemporaryDirectory() as tmpdirname:
    with open(tmpdirname + '/my_module.py', 'w') as f:
      f.write('def my_func():\n')
      f.write('  return 2\n')
    sys.path.append(tmpdirname)
    import my_module
  print(my_module.my_func())

A cleaner version could be this:

  with tempfile.TemporaryDirectory() as tmpdirname:
    with open(tmpdirname + '/my_module.py', 'w') as f:
      f.write('def my_func():\n')
      f.write('  return 1\n')
    sys.path.append(tmpdirname)
    try:
      importlib.reload(my_module)
    except UnboundLocalError:
      import my_module
    sys.path.remove(tmpdirname)
  print(my_module.my_func())
  
  with tempfile.TemporaryDirectory() as tmpdirname:
    with open(tmpdirname + '/my_module.py', 'w') as f:
      f.write('def my_func():\n')
      f.write('  return 2\n')
    sys.path.append(tmpdirname)
    try:
      importlib.reload(my_module)
    except UnboundLocalError:
      print(str(e))
      import my_module
    sys.path.remove(tmpdirname)
  print(my_module.my_func())

Hi @stefano.menci and thanks for your answer.
Doesn’t my sys.path.pop() work as your sys.path.remove(tmpdirname) ?

Yes, the pop and the remove are equivalent, but I forgot to specify that the focus of my answer was about using importlib.reload rather than just import, otherwise the interpreter would not re-import a module already imported.

1 Like

Oh I see now, thanks.