Checking warnings: pytest.warns
APIs also evolve. New and better alternatives to old functions are provided, arguments are removed, old ways of using a certain functionality evolve into better ways, and so on.
API writers have to strike a balance between keeping old code working to avoid breaking clients and providing better ways of doing things, while all the while keeping their own API code maintainable. For this reason, a solution often adopted is to start to issue warnings when API clients use the old behavior, in the hope that they update their code to the new constructs. Warning messages are shown in situations where the current usage is not wrong to warrant an exception, it just happens that there are new and better ways of doing it. Often, warning messages are shown during a grace period for this update to take place, and afterward the old way is no longer supported.
Python provides the standard warnings module exactly for this purpose, making it easy to warn developers about forthcoming changes in APIs. For more details, go to: https://docs.python.org/3/library/warnings.html. It lets you choose from a number of warning classes, for example:
- UserWarning: user warnings (user here means developers, not software users)
- DeprecationWarning: features that will be removed in the future
- ResourcesWarning: related to resource usage
(This list is not exhaustive. Consult the warnings documentation for the full listing. For more details, go to: https://docs.python.org/3/library/warnings.html).
Warning classes help users control which warnings should be shown and which ones should be suppressed.
For example, suppose an API for a computer game provides this handy function to obtain the starting hit points of player characters given their class name:
def get_initial_hit_points(player_class: str) -> int:
...
Time moves forward and the developers decide to use an enum instead of class names in the next release. For more details, go to: https://docs.python.org/3/library/enum.html, which is more adequate to represent a limited set of values:
class PlayerClass(Enum):
WARRIOR = 1
KNIGHT = 2
SORCERER = 3
CLERIC = 4
But changing this suddenly would break all clients, so they wisely decide to support both forms for the next release: str and the PlayerClass enum. They don't want to keep supporting this forever, so they start showing a warning whenever a class is passed as a str:
def get_initial_hit_points(player_class: Union[PlayerClass, str]) -> int:
if isinstance(player_class, str):
msg = 'Using player_class as str has been deprecated' \
'and will be removed in the future'
warnings.warn(DeprecationWarning(msg))
player_class = get_player_enum_from_string(player_class)
...
In the same vein as pytest.raises from the previous section, the pytest.warns function lets you test whether your API code is producing the warnings you expect:
def test_get_initial_hit_points_warning():
with pytest.warns(DeprecationWarning):
get_initial_hit_points('warrior')
As with pytest.raises, pytest.warns can receive an optional match argument, which is a regular expression string. For more details, go to: https://docs.python.org/3/howto/regex.html, which will be matched against the exception message:
def test_get_initial_hit_points_warning():
with pytest.warns(DeprecationWarning,
match='.*str has been deprecated.*'):
get_initial_hit_points('warrior')