0

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

D_Asti
  • 59
  • 7
  • I only need to generate/define the functions. I don't need to call them – D_Asti Oct 09 '20 at 07:59
  • What's the reason that you need 50 functions doing the same ? – Maurice Meyer Oct 09 '20 at 08:03
  • I am using Locust load testing frameworks. These functions will basically represent each 'test'. – D_Asti Oct 09 '20 at 08:09
  • 1
    @D_Asti Looks like you should switch from using the decorator to using the [`tasks` attribute](https://docs.locust.io/en/stable/writing-a-locustfile.html#id2). The docs say the decorator is just a convenience for simple cases (which this is not). That would avoid this whole mess. – Arthur Tacca Oct 09 '20 at 08:26

3 Answers3

1

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)

Cyberwiz
  • 11,027
  • 3
  • 20
  • 40
0

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
Maurice Meyer
  • 17,279
  • 4
  • 30
  • 47
  • I think the point is that `t.view_item_with_id_14()` should work without a parameter. – Arthur Tacca Oct 09 '20 at 08:20
  • If you tweak your answer to use `functools.partialmethod(self.itemViewer, item_nr)` instead of `self.itemViewer` (see [`functools.partialmethod`](https://docs.python.org/3/library/functools.html#functools.partialmethod)) then I think it would work. – Arthur Tacca Oct 09 '20 at 08:23
  • Oops, I mean partial, not partialmethod (I was confused because normally methods are unbound attributes of the class, but you've got them as bound attributes of the object). Also, you are only calling the decorator once, rather than 50 times as required. – Arthur Tacca Oct 09 '20 at 08:43
0

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)
Arthur Tacca
  • 8,833
  • 2
  • 31
  • 49