Add methods to a class in Python - Sat, Mar 5, 2022
Programmatically add methods to a class in Python
Adding methods to a class programmatically
During one of my recent code katas
I wanted to create a python module that automatically validates that the pages linked in my blog posts are not dead and still contain the content they should. Test code was generic so that tests for new blog entries could be added simply by adding them to a json file. See the test data file here
So far so good, the only small imperfection was that the pytest test class
only contained one generic test method which meant that running the test would also result in only a single test being run for all blog posts tested. So the success rate was either 0
or 100
, regardless of the number of blog posts actually being tested.
Wouldn’t it be nice to have individual tests for each test yet keeping the generic json configuration file ?
Methods are first class citizens
After thinking about this for a while I remembered that functions are first class citizens
in python. That is they can be created and treated as any other object. So my idea was to create a bootstrap method that would create a test method for each blog post and adds it to the test class.
So I did some research and tried different ways to do it. And the simplest solution in that case for me was to use the setattr
function. This method can be used to add arbitrary attributes to a python object (which a class is), including functions. This gist
contains an example on how to use this method. Now the particular code in this case is show below:
def add_test(cls, post: BlogPost):
def blog_pots_test_method(self):
blog_post_tester = BlogPostTester(post, RetroBlogTest.driver)
test_result = blog_post_tester.verify_blog_post()
assert not test_result.has_errors()
blog_pots_test_method.__name__ = f"test-{post.name}"
setattr(cls, blog_pots_test_method.__name__, blog_pots_test_method)
for blog_post in load_test_data():
add_test(RetroBlogTest, blog_post)
The loop at the bottom reads the test data and calls add_test
for each page to test. Let’s take a closer look at that method. At the start of the method a inner method is defined that contains the actual test code. Note the parameter self
which is mandatory since the method is going to be an instance method of the test class.
The parameter post
is used inside the inner method and will be still be accessible when the method is invoked during test execution.
After its definition the method can be treated as any other object and is accessible by its name. In the penultimate line the name of the inner method is changed so that it reflects the name of the page being tested. Finally the method is added to the test class.
Running the test now is much more informative:
$ python -m pytest -v ./tests/*.py
==== test session starts ====
tests/retro_blog_tests.py::RetroBlogTest::test-k8s-hardly-readable-configmap PASSED [ 33%]
tests/retro_blog_tests.py::RetroBlogTest::test-prometheus-test-alerts PASSED [ 66%]
tests/retro_blog_tests.py::RetroBlogTest::test-python-selenium-testing PASSED [100%]
==== 3 passed in 29.89s ====
As you can see there are now 3 methods instead of one and each method contains the name of the page being tested. I’m now able to see exactly which page failed the test which in turn will make fixing faulty links easier.
Conclusion
The example here shows how easy and elegant it is in python to change classes and methods. Since methods are just objects they can be created and worked with programmatically.