Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there a way to run a method automatically on the initialization of an instance without using __init__?

I am writing some unit tests with Pytest. If I want them to be collected automatically, I have to avoid the __init__ constructor. (If there's a way to make Pytest collect tests with the __init__ constructor I'd take that as an alternate useful answer.)

My unit tests have some variables and methods in common. Right now I have base test class TestFoo, and child test class TestBar(TestFoo), and grandchild test class TestBaz(TestBar). Since I can't have an init method, right now I'm calling a setup() method that assigns a bunch of variables to the class instance as a part of every single test method.

It looks like:

Class TestBaz(TestBar):
    def setup():
        super().setup()
        # do some other stuff

    def test_that_my_program_works(self):
        self.setup()
        my_program_works = do_stuff()
        assert my_program_works

But this is ugly and I was wondering if there was a way to get around it. One thing I got working -- I made this decorator function to decorate every method:

def setup(cls):
    def inner_function(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            cls.set_up()
            return func(*args, **kwargs)
        return wrapper
    return inner_function

but

@setup
def test_that_my_program_works():

is not that much better. I was sort of in the weeds reading about metaclasses and trying to figure out how I would more silently wrap every method when I realized that fundamentally I don't want or need to wrap every method. I just want a method that executes automatically on class initialization. I want __init__ without __init__.

Is there a way to do this?

like image 965
Katie Avatar asked Dec 23 '25 00:12

Katie


1 Answers

As you had seem, py.test have other means to run a setup for class-scoped methods. You'd probably run those, as they are guaranteed to be run at the right points between each (test) method call - as one won't have control on when py.test instantiate such a class.

For the record, just add a setup method to the class (the method name is all-lower case), like in:

class Test1:
    def setup(self):
        self.a = 1
    def test_blah(self):
        assert self.a == 1

However, as you asked about metaclasses, yes, a metaclass can work to create a "custom method equivalent to __init__".

When a new object is created, that is, when class is instantiated in Python, it is as though the class itself was called. What happens internally is that the __call__ method for the metaclass is called, with the parameters passed to create the instance.

This method then runs the class' __new__ and __init__ methods passing those parameters, and returns the value returned by __new__.

A metaclass inheriting from type can override __call__ to add extra __init__- like calls, and the code for that is just:

class Meta(type):
    def __call__(cls, *args, **kw):
        instance = super().__call__(*args, **kw)
        custom_init = getattr(instance, "__custom_init__", None)
        if callable(custom_init):
            custom_init(*args, **kw)

        return instance

I've tried this with a small class in a file I run with pytest, and it just works:

class Test2(metaclass=Meta):
    def __custom_init__(self):
        self.a = 1
    def test_blah(self):
        assert self.a == 1
like image 52
jsbueno Avatar answered Dec 24 '25 13:12

jsbueno



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!