Search code examples
pythonpython-3.xmetaclasspython-internals

How to inject code in class and assign values to class fields at runtime?


I have a BatchJob specification file (batch.spec) something like below:

python = <<EOF

def foo():
    return f"foo: I am {self.name}"

def bar():
    return f"bar: I am {self.name}"

EOF

<batch>
  name p&l_calculator
  exec echo %foo()%
</batch>

I am converting this file to a python dict by using https://github.com/etingof/apacheconfig

from apacheconfig import *

with make_loader() as loader:
    config = loader.load('batch.spec')

# Print content of python dict
for key, value in config.items():
    print(key, value)

# output from print statement above
# python 
# def foo():
#     return f"foo: I am {self.name}"
#
# def bar():
#     return f"bar: I am {self.name}"
# batch {'name': 'p&l_calculator', 'exec': 'echo %foo()%'}

Now I am converting this python dict to a BatchJobSpec object like below:

class BatchJobSpec:

    def __init__(self, pythoncode , batch):
        self.pythoncode = pythoncode
        self.name = batch.get("name")
        self.exec = batch.get("exec")

batchjobspec = BatchJobSpec(config.get("python"), config.get("batch"))

If I print batchjobspec fields, then I will get something like below

print(batchjobspec.pythoncode)
print(batchjobspec.name)
print(batchjobspec.exec) 

# Output from above print statements
# def foo():
#     return f"foo: I am {self.name}"
#
# def bar():
#     return f"bar: I am {self.name}"
# p&l_calculator
# echo %foo()%

Problem: I want the value of "batchjobspec.exec" to be interpolated when I try to access it, i.e. it should not be "echo %foo()%" but it should be "echo foo: I am p&l_calculator".

i.e. somehow in the getter of fields, I want to check if there is "% %" syntax. If yes, and if content inside "% %" contains a callable then I want to call that callable and append the value returned from callable to the value of the corresponding field. I guess I will have to make these callables available in the BatchJobSpec dict as well.


Solution

  • Comment: I have foo as well as bar. exec(str) will execute both

    In your self.pythoncode you have definitons of functions like def foo():. Therefore exec doesn't execute but do define these functions in the local namespace. To execute these functions as a class methode, you have to create a attribute, referencing these local functions, in the class itself.

    1. In this example, the function names are known beforhand

      class BatchJobSpec:
          def __init__(self, pythoncode , batch):
              self.pythoncode = pythoncode
              self.name = batch['name']
              self._exec = ['for', 'bar']
              self.exec()
      
    2. To make self visible in the local namespace, define the locals_ dict.
      exec the string self.pythoncode results in insert the reference to the functions into locals_. To use these local references, as a class methode and make it persistent, use self.__setattr__(....

          def exec(self):
              locals_ = {'self': self}
              exec(self.pythoncode, locals_)
              for attrib in self._exec:
                  print('__setattr__({})'.format(attrib))
                  self.__setattr__(attrib, locals_[attrib])
      
    3. Usage: Different python format syntax, as i using python 3.5

      python = """
      def foo():
          return 'foo: I am {name}'.format(name=self.name)
      
      def bar():
          return 'bar: I am {name}'.format(name=self.name)
      """
      
      if __name__ == "__main__":
          batchjobspec = BatchJobSpec(config.get("python"), config.get("batch"))
          print('echo {}'.format(batchjobspec.foo()))
          print('echo {}'.format(batchjobspec.bar()))
      
    4. Output:

      __setattr__(foo)
      __setattr__(bar)
      echo foo: I am p&l_calculator
      echo bar: I am p&l_calculator
      

    Question I want the value of "batchjobspec.exec" to be interpolated when I try to access it


    Change your class BatchJobSpec to:

    class BatchJobSpec:
    
        def __init__(self, pythoncode , batch):
            self.pythoncode = pythoncode
            self.name = batch.get("name")
            self._exec = batch.get("exec")
    
        @property
        def exec(self):
            to_exec = 'foo'
            self.__setattr__(to_exec, lambda : exec('print("Hello world")'))
    
            self.foo()
            return None