Creating custom decorators in Python 3.6

In the hips package, often data has to be fetched from remote servers, especially HiPS tiles. One way to cut back on the queries was by introducing the hips-extra repository. This contains HiPS tiles from various HiPS surveys. This allows us to quickly fetch tiles from local storage, which makes the testing process less time-consuming.

As hips-extra repository does not come with the standard hips package, user has to manually clone it. The availability of the package is checked using an environment variable. This can be set using:

$export HIPS_EXTRA=\path\to\hips-extra

In Python, the path can be retrieved using the os module: os.environ['HIPS_EXTRA']. Now, what if the user does not have hips-extra repository on their system. In this case, we don’t want to raise any errors, but to simply skip the test case. In order to do this, Python decorators become useful.

A brief overview on Python Decorators

Decorators provide a simple syntax for calling higher-order functions. By definition, a decorator is a function that takes another function and extends the behavior of the latter function without explicitly modifying it. Some of the most commonly used decorators are @classmethod, @staticmethod, @property, etc. The decorators are written above a function header, like this:

@property
def name(self):
    return self.name

Writing your own decorators

To write our decorators, we create a wrapper function around pytest.mark.skipif. The documentation of this function can be viewed to get detailed information on how it works. The function below checks if the HIPS_EXTRA environment variable is set on the user’s system, and if it is, it returns a True boolean .

import os
from pathlib import Path

def has_hips_extra():
    """Is hips-extra available? (`bool`)"""
    if 'HIPS_EXTRA' in os.environ:
        path = Path(os.environ['HIPS_EXTRA']) / 'datasets/samples/DSS2Red/properties'
        if path.is_file():
            return True
    return False

A separate function below makes use of the above function to check whether the package is available or not:

import pytest

def requires_hips_extra():
    """Decorator to mark tests requiring ``hips-extra`` data."""
    skip_it = not has_hips_extra()
    reason = 'No hips-extra data available.'
    return pytest.mark.skipif(skip_it, reason=reason)

This decorator can now be used. An example illustrating its use is shown below. First we import the function from its location and then apply it to another function that uses data from hips-extra repository:

from hips.utils.testing import requires_hips_extra

@requires_hips_extra()
def test_draw_sky_image():
    ...

If the user’s HIPS_EXTRA environment variable does not have a valid path, the above test will be skipped.

links

social