Sample usage of Metaclasses in Python: Taking screenshots automatically
• 6 minute read
python, proxy pattern
Python is a very versatile programming language. So, instead of doing this all the time to measure time execution of a given logic:
class MyPageSample:
def show_my_profile(self):
start = time.perf_counter()
try:
# Main logic
for i in range(1, 100):
pass
# End of main logic
finally:
elapsed = time.perf_counter() - start
logger.info(f"show_my_profile took {elapsed:0.2f} seconds")
It can be translated to:
class MyPageSample:
@measure_it
def show_my_profile(self):
for i in range(1, 100):
pass
Did you see @measure_it
? It's known as a decorator. Let's see its implementation:
import logging
import time
from functools import wraps
logger = logging.getLogger(__name__)
def measure_it(func):
@wraps(func)
def timed(*args, **kw):
start = time.perf_counter()
try:
return func(*args, **kw)
finally:
elapsed = time.perf_counter() - start
logger.info(f"{func.__name__} from {func.__module__} with args {args} took {elapsed:0.2f} seconds")
return timed
The problem
Now imagine we have the following class, and each method that starts with step_
should take a screenshot by the end of its execution. So, according to the example above, we would have the following:
class MyPageXYZSampleBad:
@take_screenshot
def step_access_home_page(self):
pass
@take_screenshot
def step_access_ticker_panel(self):
pass
@take_screenshot
def step_select_ticker(self, ticker_name: str):
pass
@take_screenshot
def step_access_my_profile(self):
pass
How can we avoid decorating methods all the time?
Getting to know proxy design pattern
This article on Wikipedia explains pretty well what it is. By the way, if you know about the same concept in networking, it works pretty much the same. So an implicit or transparent proxy is a great example to grab the idea:
To achieve the same level of experience in our class, it would have to be like this:
class MyPageXYZSample:
def step_access_home_page(self):
pass
def step_access_ticker_panel(self):
pass
def step_select_ticker(self, ticker_name: str):
pass
def step_acess_my_profile(self):
pass
But how do we do it in Python, though? Is there an API for that?
Metaclasses in action
I want to show you how it works in actual code. So you can always look at the official documentation for more details or this lovely answer on Stack Overflow. That said, what we want to do is:
- If a method starts with
step_
, a screenshot should be taken after its execution. - If the screenshot fails for some reason, an exception should inform why.
This is our take_screenshot
decorator:
import logging
from functools import wraps
logger = logging.getLogger(__name__)
def take_screenshot(func):
@wraps(func)
def wrapped(*args, **kw):
result = func(*args, **kw)
try:
logger.info(f"Screenshot has been taken for {func.__name__}")
except Exception as e:
raise FailedToTakeScreenshotException
return result
return wrapped
class FailedToTakeScreenshotException(Exception):
pass
It just logs a message in stdout
. As always, if you know the rules about something upfront, it's a good idea to start with tests even though you don't know how to implement them. So this is our test class:
import unittest
from unittest import mock
from python_metaclass.decorators import FailedToTakeScreenshotException
from python_metaclass.pages.page_xyz_good import MyPageXYZSample
class MyPageXYZSampleTests(unittest.TestCase):
@mock.patch("python_metaclass.decorators.logger")
def test_should_take_screenshots_when_required(self, mocked_logger):
# Arrange
my_page_sample_instance = MyPageXYZSample()
# Act
my_page_sample_instance.step_access_home_page()
my_page_sample_instance.step_access_ticker_panel()
my_page_sample_instance.step_select_ticker("cogn3")
my_page_sample_instance.step_access_my_profile()
my_page_sample_instance.do_not_take_screenshot()
# Assert
mocked_info = mocked_logger.info
self.assertEqual(4, mocked_info.call_count)
messages = []
for call_arg in mocked_logger.info.call_args_list:
first_argument = call_arg[0][0]
messages.append(first_argument)
expected_messages = [
"Screenshot has been taken for step_access_home_page",
"Screenshot has been taken for step_access_ticker_panel",
"Screenshot has been taken for step_select_ticker",
"Screenshot has been taken for step_access_my_profile",
]
self.assertEqual(expected_messages, messages)
@mock.patch("python_metaclass.decorators.logger")
def test_should_raise_exception_given_take_screenshot_fails(self, mocked_logger):
# Arrange
mocked_info = mocked_logger.info
mocked_info.side_effect = [None, Exception]
my_page_sample_instance = MyPageXYZSampleGood()
# Act and assert
with self.assertRaises(FailedToTakeScreenshotException):
my_page_sample_instance.step_access_home_page()
my_page_sample_instance.step_access_my_profile()
self.assertEqual(2, mocked_info.call_count)
So far, so good. Now, let's follow the official documentation to understand what we can do to create our proxy. By the way, I don't know why Python 3 documentation lacks a clear example, as we have in Python 2 one. After a bit of debugging (and I recommend you that), that's how the metaclass
is:
from python_metaclass.decorators import take_screenshot
class BasePageMetaClass(type):
def __new__(cls, subclass_name, bases, dictionary):
for attribute in dictionary:
value = dictionary[attribute]
should_be_decorated_with_screenshot = attribute.startswith("step_") and callable(value)
if should_be_decorated_with_screenshot:
dictionary[attribute] = take_screenshot(value)
return type.__new__(cls, subclass_name, bases, dictionary)
To use it, you should follow a contract, and you can also check it in the documentation. Then that's how it is:
class MyPageXYZSample(object, metaclass=BasePageMetaClass):
def step_access_home_page(self):
pass
def step_access_ticker_panel(self):
pass
def step_select_ticker(self, ticker_name: str):
pass
def step_access_my_profile(self):
pass
def do_not_take_screenshot(self):
pass
If you run the test now, it should pass just fine 🙂.
Conclusion
The issue I described here is quite common and typical in frameworks. If you want to look at this in action, just look at this code. Are you in need of intercepting something and changing the behavior without impacting the user? The proxy pattern is a great choice! I'm used to analyzing something with "lessons learned" or "best practices" on Google, which can also be applied to this problem. So even if you hadn't ever heard of the proxy pattern or metaclasses before this article, sooner or later, you would have heard about it. For example, I had to create a proxy to switch between reader and writer databases in Java a long time ago. My team came up with a solution written natively in the language. Unfortunately, we didn't know the proper name (proxy pattern) until we faced the real problem. Depending on which programming language you're using, there are many possibilities to achieve what we did here, even in Python.
See everything we did here on GitHub.
Posted listening to Donkey Kong Country 2: Hot-Head Bop 🎶.