How to create functionas dynamically containing a decorator?

101 views Asked by At

I need to create/declare 50 functions inside my Class. But, I need to do it dynamically with custom function names and cutom urls in the function body. Something as the following:

    for item_nr in range(1, 51):
        @task(1)
        def view_item_with_id_{item_nr}(self, item_nr=item_nr):
            self.client.get(
                url=f"/my-url/id=SEPT24_00{item_nr}",
                verify=False,
                auth=(os.environ['USERNAME'], os.environ['PASSWORD'])
            )

P.S since it's inside a class- I cannot really use another function to generate it as suggested in some other threads, because the 'self' parameter will not be visible then. Example (this will not work):

def function_builder(args):
    def function(more_args):
       #do stuff based on the values of args
    return function

Any help is appreciated

3

There are 3 answers

3
Maurice Meyer On

You could use setattr to define new class methods:

class Test:
    def __init__(self):
        for item_nr in range(1, 51):
            setattr(self, f"view_item_with_id_{item_nr}", self.itemViewer)

    @task(1)
    def itemViewer(self, item_nr=None):
        """
        self.client.get(
            url=f"/my-url/id=SEPT24_00{item_nr}",
            verify=False,
            auth=(os.environ['USERNAME'], os.environ['PASSWORD'])
        )
        """
        print(f"/my-url/id=SEPT24_00{item_nr}")


t = Test()
t.view_item_with_id_14(14)
t.view_item_with_id_44(44)

Out:

/my-url/id=SEPT24_0014
/my-url/id=SEPT24_0044
0
Arthur Tacca On

I have to mention: Fundamentally, wanting to do this in the first place is almost certainly a mistake. You should surely, instead, modify the part of the code that calls these functions to just call one function and pass in a parameter, or otherwise work around the problem.

There are two parts to this question: how to generate the functions, and how to wrap them in decorators.

Create methods programmatically

To create methods programmatically, you need to use setattr(). This allows you to add attributes to an object specified by string rather than explicitly in your code. However, for a function added in this way to be treated as a method (i.e. your object is automatically passed as the self parameter), it must set on the class, not directly on the object.

class Foo:
    pass

def f(self, val):
    self.x = val
setattr(Foo, "f", f)

def g(self):
    print(self.x)
setattr(Foo, "g", g)

obj = Foo()
obj.f(3)
obj.g()  # prints 3

Call decorator manually

The second part is the easy bit. A decorator is just some syntactic sugar on a function that receives and returns another function. So these are equivalent:

@task(1)
def foo():
    pass

def bar():
    pass
bar = task(1)(bar)

In your case, you can simply call the decorator directly rather than having to use anything like the @task(1) notation.

Putting it together

Putting the two ideas together, here's what you want. (By the way, your default parameter technique is fine for creating the function, but functools.partialmethod is a bit cleaner because the result doesn't allow the caller to override the parameter, so I've used that below.)

def MyClass
    pass # Whatever else you need

def fn(self, item_nr):
    self.client.get(
        url=f"/my-url/id=SEPT24_00{item_nr}",
        verify=False,
        auth=(os.environ['USERNAME'], os.environ['PASSWORD'])
    )

for item_nr in range(1, 51):
    this_fn = functools.partialmethod(fn, item_nr)
    decorated_fn = task(1)(this_fn)
    setattr(MyClass, f"view_item_with_id_{item_nr}", decorated_fn)
0
Cyberwiz On

This is an interesting question, but is there some reason you cant just use a single @task with a loop? (this solution is of course specific to Locust)

@task
def view_item_with_id(self):
    for item_nr in range(1, 51):
        self.client.get(
            url=f"/my-url/id=SEPT24_00{item_nr}",
            verify=False,
            auth=(os.environ['USERNAME'], os.environ['PASSWORD'])
        )

The other suggested answers might be closer to your original question, but this is so much simpler (it can of course easily be adjusted to pick items randomly, if that is your preference)