1

I have a chain of methods in my python program of the TestClass object like below:-

TestClass().method1(args).method2(args).method3(args)

I want to build a utility which will scan the above chain and then it can build an execution sequence. (similar to the way the data processing frameworks build their execution graph or dag). Also, the above sequence won't run until I call run() at the end.

so, in my case, it will run only when run() method is added to the end of the chain.

TestClass().method1(args).method2(args).method3(args).run()

How to achieve this?

Einzig7
  • 543
  • 1
  • 6
  • 22
dks551
  • 1,113
  • 1
  • 15
  • 39
  • just use a chain of generator functions, `run` would just loop on the outside one, triggering the others – Jean-François Fabre Jul 14 '18 at 07:41
  • It's not clear what you mean by "a utility". Do you mean something you build in to `TestClass`, or some external tool that reads your Python source code and somehow does something with it (either writing out different code, or modifying the source in place)? – Blckknght Jul 14 '18 at 08:37
  • Take a look at the `F` functor from package [fn](https://github.com/kachayev/fn.py) (section *High-level operations with functions* in the README) – Eli Korvigo Jul 14 '18 at 08:41
  • I don't understand why it is necessary for this to be a chain of method calls. Why not just a sequence of calls? I think it would be useful to explain that requirement a little more. Having said that, if you want to scan and transform code you should look at https://docs.python.org/3.5/library/ast.html – jdowner Jul 14 '18 at 09:00
  • @Blckknght By utility I mean "some external command line tool written using python". So that I can directly run that tool my CLI like >mypytool my_file.py. – dks551 Jul 14 '18 at 09:25
  • Making an external tool to interpret your Python code is likely to be much harder than just writing the Python code to do what you want in the first place. You could probably write a class decorator to add the sort of behavior that acushner's answer shows (as they suggest in the answer itself). I'd strongly recommend doing that instead of trying to create your own Python interpreter. – Blckknght Jul 14 '18 at 18:50

2 Answers2

3

what you want is something like a fluent interface.

i'll give you a simple example, which could definitely be made better with some sort of decorator

class A:
    def __init__(self):
        self.call_chain = []

    def m1(self, arg):
        def _actual_logic():
            print(arg)
        self.call_chain.append(actual_logic)
        return self

    def run(self):
        for f in self.call_chain:
            f()

a = A().m1('snth')
a.run()
acushner
  • 9,595
  • 1
  • 34
  • 34
1

I got inspired by this answer in order to elaborate mine.

So, i'll use inspect and re modules alongside with a decorator in order to check if run method is called or not:

import re
import inspect

def check_run_is_called(func):
    def wrapper(*args, **kwargs):
        # Here the interesting part
        # We're searching if the string `.run()` exists in the code context
        run_exist = re.search(r'\.run\(.*?\)', inspect.stack()[-1].code_context[0])
        if not run_exist:
            raise Exception('Run must be called !')
        f = func(*args, **kwargs)
        return f
    return wrapper


class TestClass():
    @check_run_is_called
    def method1(self, arg):
        print(arg)
        return self

    @check_run_is_called
    def method2(self, arg):
        print(arg)
        return self

    @check_run_is_called
    def method3(self, arg):
        print(arg)
        return self

    def run(self):
        print('Run')


# test
if __name__ == '__main__':
    app = TestClass()
    app.method1('method1').method2('method2').method3('method3').run()

Output:

method1
method2
method3
Run

Otherwise, if we call the method chain without including run method:

app.method1('method1').method2('method2').method3('method3')

We'll get an Exception:

Exception: Run must be called !

Besides of this, you can also create a combination of chains like this example:

app.method1('method1').run()
# method1
# Run
app.method1('method1').method2('method2').run()
# method1
# method2
# Run
app.method2('method2').method3('method3').method1('method1').run()
# method2
# method3
# method1
# Run
Chiheb Nexus
  • 9,104
  • 4
  • 30
  • 43
  • 1
    I'm under the impression, the OP doesn't want to enforce `.run` calls at the end of each chain. The method is supposed to initiate a 'precompiled' computation. – Eli Korvigo Jul 14 '18 at 08:53
  • @EliKorvigo He/She said `it will run only when run() method is added to the end of the chain` doesn't this mean that he/she needs to add `run()` at the end of the chain ? if not, i've missed the point. – Chiheb Nexus Jul 14 '18 at 08:55
  • My guess is that the OP wants to create a chain and call it later on, but I might be wrong. – Eli Korvigo Jul 14 '18 at 16:27