- Thu 22 November 2018
- General
- #python, #superuser
(Original notebook can be found in this gist)
Motivation
Assume you want to share across your application a smart list of constants.
For example, a list of colors supported by your application.
One way probably could be a list
in some module along with some replated logic implemented.
In this post, you will learn how I tackled this issue.
Enums in Python
There are a lot of resources about Enums in Python and probably the best place to start are the docs. Let us start with a simple example.
from enum import Enum
class Color(Enum):
RED = 1
GREEN = 2
BLUE = 3
This is great; you can check the value of RED
:
Color.RED
<Color.RED: 1>
Recall, that the objective is to come up with a solution enabling a little more logic. In particular, you want to have two functionalities:
- Nice printout of the supported colors
- Checking whether a given color is supported
Customizing Color
Here is the definition of the customized class.
from enum import EnumMeta
class MyEnumMeta(EnumMeta):
def __str__(cls):
lines = [f"Members of `{cls.__name__}` are:"]
for member in cls:
lines.append(f"- {member}")
return '\n'.join(lines)
def _contains(self, member):
return member in self._member_map_ \
or member in set(
map(lambda x: x.value, self._member_map_.values()))
def is_valid(self, member):
if self._contains(member):
return True
else:
return False
class Color(Enum, metaclass=MyEnumMeta):
RED = 1
GREEN = 2
BLUE = 3
Now, your Color
can do much more:
print(Color)
Members of `Color` are:
- Color.RED
- Color.GREEN
- Color.BLUE
or, alternatively:
Color.is_valid('RED'), Color.is_valid('YELLOW')
(True, False)
I am sure you can take it from here, improve and modify the implementation as per your needs.
But, before doing so, you might want to see how the custom Color
can be used, and, more importantly, tested.
Using the custom Color
Here is an example of how we can use the custom Color
:
def foo(type):
if Color.is_valid(type):
print(f"Wonderful! we will use the wonderful {type} color")
return "valid"
else:
print(f"{type} is not supported color. The supported colors are:\n{str(Color)}")
return "invalid"
And here are some examples:
foo('RED')
Wonderful! we will use the wonderful RED color
'valid'
foo('BLUE')
Wonderful! we will use the wonderful BLUE color
'valid'
foo('YELLOW')
YELLOW is not supported color. The supported colors are:
Members of `Color` are:
- Color.RED
- Color.GREEN
- Color.BLUE
'invalid'
Testing foo
Next, you want to test foo
.
But, as you want it to be a proper unit test, you don't want to refer to the values of Color
.
That is, you want to patch Color
and use some value defined within the scope of the test.
Here is the way to do that.
import unittest
from unittest.mock import patch
class DummyColor(Enum, metaclass=MyEnumMeta):
TEST_TYPE1 = 1
TEST_TYPE2 = 10
class MyTest(unittest.TestCase):
def test_a(self):
# Without any patch, you are depending on the
# values of `Color`
assert foo('RED') == 'valid'
assert foo('YELLOW') == 'invalid'
@patch('__main__.Color', DummyColor) # Make sure you patch the right `Color`
# it should be within the class you want to test
def test_b(self):
assert foo('TEST_TYPE1') == 'valid'
assert foo('TEST_TYPE10') == 'invalid'
if __name__ == '__main__':
unittest.main(argv=['first-arg-is-ignored'], exit=False)
..
Wonderful! we will use the wonderful RED color
YELLOW is not supported color. The supported colors are:
Members of `Color` are:
- Color.RED
- Color.GREEN
- Color.BLUE
Wonderful! we will use the wonderful TEST_TYPE1 color
TEST_TYPE10 is not supported color. The supported colors are:
Members of `DummyColor` are:
- DummyColor.TEST_TYPE1
- DummyColor.TEST_TYPE2
----------------------------------------------------------------------
Ran 2 tests in 0.002s
OK
Summary
I hope you found it useful.
As a bonus, here is more or less the same example, given in scripts.
You can save the three of theme somewhere and run pytest
to see how everything works together.
special_enum.py
# Implementation of the customized enum
from enum import Enum, EnumMeta
class MyEnumMeta(EnumMeta):
def __str__(cls):
lines = [f"Members of `{cls.__name__}` are:"]
for member in cls:
lines.append(f"- {member}")
return '\n'.join(lines)
def _contains(self, member):
return member in self._member_map_ \
or member in set(
map(lambda x: x.value, self._member_map_.values()))
def is_valid(self, member):
if self._contains(member):
return True
else:
return False
class SpecialEnum(Enum, metaclass=MyEnumMeta):
TYPE1 = 1
TYPE2 = 10
foo.py
# Your amazing logic!
from special_enum import SpecialEnum
def bar(type):
print(SpecialEnum) # Just for debugging
if SpecialEnum.is_valid(type):
return "valid"
else:
return "invalid"
test_foo.py
# The testing script
import unittest
from unittest.mock import patch
import foo
from special_enum import MyEnumMeta
from enum import Enum
class SpecialEnumTest(Enum, metaclass=MyEnumMeta):
TEST_TYPE1 = 1
TEST_TYPE2 = 10
class MyTest(unittest.TestCase):
def test_a(self):
# Passes as it is ussing the original SpecialEnum
assert foo.bar('TYPE1') == 'valid'
assert foo.bar('TYPE10') == 'invalid'
@patch('foo.SpecialEnum', SpecialEnumTest)
def test_b(self):
assert foo.bar('TEST_TYPE1') == 'valid'
assert foo.bar('TEST_TYPE10') == 'invalid'