decorators.py
378 lines
| 12.2 KiB
| text/x-python
|
PythonLexer
Thomas Kluyver
|
r3904 | # -*- coding: utf-8 -*- | ||
Fernando Perez
|
r1420 | """Decorators for labeling test objects. | ||
Fernando Perez
|
r1848 | Decorators that merely return a modified version of the original function | ||
object are straightforward. Decorators that return a new function object need | ||||
to use nose.tools.make_decorator(original_function)(decorator) in returning the | ||||
decorator, in order to preserve metadata such as function name, setup and | ||||
teardown functions and so on - see nose.tools for more information. | ||||
Fernando Perez
|
r1420 | |||
Fernando Perez
|
r1721 | This module provides a set of useful decorators meant to be ready to use in | ||
your own tests. See the bottom of the file for the ready-made ones, and if you | ||||
find yourself writing a new one that may be of generic use, add it here. | ||||
Fernando Perez
|
r2368 | Included decorators: | ||
Lightweight testing that remains unittest-compatible. | ||||
- An @as_unittest decorator can be used to tag any normal parameter-less | ||||
function as a unittest TestCase. Then, both nose and normal unittest will | ||||
recognize it as such. This will make it easier to migrate away from Nose if | ||||
we ever need/want to while maintaining very lightweight tests. | ||||
Paul Ivanov
|
r3511 | NOTE: This file contains IPython-specific decorators. Using the machinery in | ||
IPython.external.decorators, we import either numpy.testing.decorators if numpy is | ||||
available, OR use equivalent code in IPython.external._decorators, which | ||||
we've copied verbatim from numpy. | ||||
Fernando Perez
|
r2368 | |||
Fernando Perez
|
r1420 | """ | ||
Min RK
|
r21122 | # Copyright (c) IPython Development Team. | ||
# Distributed under the terms of the Modified BSD License. | ||||
Fernando Perez
|
r2368 | |||
Fernando Perez
|
r1721 | import sys | ||
Paul Ivanov
|
r11970 | import os | ||
Thomas Kluyver
|
r3903 | import tempfile | ||
Fernando Perez
|
r2368 | import unittest | ||
Matthias Bussonnier
|
r21788 | import warnings | ||
Diego Garcia
|
r22954 | from importlib import import_module | ||
Fernando Perez
|
r1420 | |||
MinRK
|
r20813 | from decorator import decorator | ||
Fernando Perez
|
r1420 | |||
Fernando Perez
|
r2414 | # Expose the unittest-driven decorators | ||
Thomas Kluyver
|
r13347 | from .ipunittest import ipdoctest, ipdocstring | ||
Fernando Perez
|
r2414 | |||
Fernando Perez
|
r1420 | # Grab the numpy-specific decorators which we keep in a file that we | ||
Fernando Perez
|
r2368 | # occasionally update from upstream: decorators.py is a copy of | ||
# numpy.testing.decorators, we expose all of it here. | ||||
from IPython.external.decorators import * | ||||
Takafumi Arakaki
|
r7858 | # For onlyif_cmd_exists decorator | ||
Srinivas Reddy Thatiparthy
|
r23091 | from IPython.utils.py3compat import which | ||
Takafumi Arakaki
|
r7858 | |||
Fernando Perez
|
r2368 | #----------------------------------------------------------------------------- | ||
# Classes and functions | ||||
#----------------------------------------------------------------------------- | ||||
Fernando Perez
|
r1420 | |||
Fernando Perez
|
r2368 | # Simple example of the basic idea | ||
def as_unittest(func): | ||||
"""Decorator to make a simple function into a normal test via unittest.""" | ||||
class Tester(unittest.TestCase): | ||||
def test(self): | ||||
func() | ||||
Tester.__name__ = func.__name__ | ||||
return Tester | ||||
Fernando Perez
|
r1420 | |||
# Utility functions | ||||
Matthias Bussonnier
|
r22989 | def apply_wrapper(wrapper, func): | ||
Fernando Perez
|
r1420 | """Apply a wrapper to a function for decoration. | ||
This mixes Michele Simionato's decorator tool with nose's make_decorator, | ||||
to apply a wrapper in a decorator so that all nose attributes, as well as | ||||
function signature and other properties, survive the decoration cleanly. | ||||
This will ensure that wrapped functions can still be well introspected via | ||||
IPython, for example. | ||||
""" | ||||
Matthias Bussonnier
|
r22991 | warnings.warn("The function `apply_wrapper` is deprecated since IPython 4.0", | ||
Matthias Bussonnier
|
r22989 | DeprecationWarning, stacklevel=2) | ||
Fernando Perez
|
r1420 | import nose.tools | ||
return decorator(wrapper,nose.tools.make_decorator(func)(wrapper)) | ||||
Matthias Bussonnier
|
r22989 | def make_label_dec(label, ds=None): | ||
Fernando Perez
|
r1420 | """Factory function to create a decorator that applies one or more labels. | ||
Fernando Perez
|
r2368 | Parameters | ||
---------- | ||||
Fernando Perez
|
r1420 | label : string or sequence | ||
One or more labels that will be applied by the decorator to the functions | ||||
it decorates. Labels are attributes of the decorated function with their | ||||
value set to True. | ||||
ds : string | ||||
An optional docstring for the resulting decorator. If not given, a | ||||
default docstring is auto-generated. | ||||
Fernando Perez
|
r2368 | Returns | ||
------- | ||||
Fernando Perez
|
r1420 | A decorator. | ||
Fernando Perez
|
r2368 | Examples | ||
-------- | ||||
Fernando Perez
|
r1420 | |||
A simple labeling decorator: | ||||
Fernando Perez
|
r11040 | >>> slow = make_label_dec('slow') | ||
>>> slow.__doc__ | ||||
"Labels a test as 'slow'." | ||||
Fernando Perez
|
r1420 | And one that uses multiple labels and a custom docstring: | ||
Fernando Perez
|
r11040 | |||
Fernando Perez
|
r1420 | >>> rare = make_label_dec(['slow','hard'], | ||
... "Mix labels 'slow' and 'hard' for rare tests.") | ||||
Fernando Perez
|
r11040 | >>> rare.__doc__ | ||
"Mix labels 'slow' and 'hard' for rare tests." | ||||
Fernando Perez
|
r1420 | |||
Now, let's test using this one: | ||||
>>> @rare | ||||
... def f(): pass | ||||
... | ||||
>>> | ||||
>>> f.slow | ||||
True | ||||
>>> f.hard | ||||
True | ||||
""" | ||||
Matthias Bussonnier
|
r22991 | warnings.warn("The function `make_label_dec` is deprecated since IPython 4.0", | ||
Matthias Bussonnier
|
r22989 | DeprecationWarning, stacklevel=2) | ||
Srinivas Reddy Thatiparthy
|
r23037 | if isinstance(label, str): | ||
Fernando Perez
|
r1420 | labels = [label] | ||
else: | ||||
labels = label | ||||
Bernardo B. Marques
|
r4872 | |||
Fernando Perez
|
r1420 | # Validate that the given label(s) are OK for use in setattr() by doing a | ||
# dry run on a dummy function. | ||||
tmp = lambda : None | ||||
for label in labels: | ||||
setattr(tmp,label,True) | ||||
# This is the actual decorator we'll return | ||||
def decor(f): | ||||
for label in labels: | ||||
setattr(f,label,True) | ||||
return f | ||||
Bernardo B. Marques
|
r4872 | |||
Fernando Perez
|
r1420 | # Apply the user's docstring, or autogenerate a basic one | ||
if ds is None: | ||||
ds = "Labels a test as %r." % label | ||||
decor.__doc__ = ds | ||||
Bernardo B. Marques
|
r4872 | |||
Fernando Perez
|
r1420 | return decor | ||
Fernando Perez
|
r1848 | # Inspired by numpy's skipif, but uses the full apply_wrapper utility to | ||
# preserve function metadata better and allows the skip condition to be a | ||||
# callable. | ||||
def skipif(skip_condition, msg=None): | ||||
''' Make function raise SkipTest exception if skip_condition is true | ||||
Parameters | ||||
Fernando Perez
|
r1850 | ---------- | ||
Thomas Kluyver
|
r13595 | |||
skip_condition : bool or callable | ||||
Flag to determine whether to skip test. If the condition is a | ||||
callable, it is used at runtime to dynamically make the decision. This | ||||
is useful for tests that may require costly imports, to delay the cost | ||||
until the test suite is actually executed. | ||||
Fernando Perez
|
r1848 | msg : string | ||
Thomas Kluyver
|
r13595 | Message to give on raising a SkipTest exception. | ||
Returns | ||||
------- | ||||
decorator : function | ||||
Decorator, which, when applied to a function, causes SkipTest | ||||
to be raised when the skip_condition was True, and the function | ||||
to be called normally otherwise. | ||||
Fernando Perez
|
r1848 | |||
Notes | ||||
----- | ||||
You will see from the code that we had to further decorate the | ||||
decorator with the nose.tools.make_decorator function in order to | ||||
transmit function name, and various other metadata. | ||||
''' | ||||
def skip_decorator(f): | ||||
Bernardo B. Marques
|
r4872 | # Local import to avoid a hard nose dependency and only incur the | ||
# import time overhead at actual test-time. | ||||
Fernando Perez
|
r1848 | import nose | ||
# Allow for both boolean or callable skip conditions. | ||||
if callable(skip_condition): | ||||
Fernando Perez
|
r2452 | skip_val = skip_condition | ||
Fernando Perez
|
r1848 | else: | ||
skip_val = lambda : skip_condition | ||||
def get_msg(func,msg=None): | ||||
"""Skip message with information about function being skipped.""" | ||||
if msg is None: out = 'Test skipped due to test condition.' | ||||
else: out = msg | ||||
return "Skipping test: %s. %s" % (func.__name__,out) | ||||
# We need to define *two* skippers because Python doesn't allow both | ||||
# return with value and yield inside the same function. | ||||
def skipper_func(*args, **kwargs): | ||||
"""Skipper for normal test functions.""" | ||||
if skip_val(): | ||||
raise nose.SkipTest(get_msg(f,msg)) | ||||
else: | ||||
Bernardo B. Marques
|
r4872 | return f(*args, **kwargs) | ||
Fernando Perez
|
r1848 | |||
def skipper_gen(*args, **kwargs): | ||||
"""Skipper for test generators.""" | ||||
if skip_val(): | ||||
raise nose.SkipTest(get_msg(f,msg)) | ||||
else: | ||||
for x in f(*args, **kwargs): | ||||
yield x | ||||
# Choose the right skipper to use when building the actual generator. | ||||
if nose.util.isgenerator(f): | ||||
skipper = skipper_gen | ||||
else: | ||||
skipper = skipper_func | ||||
Bernardo B. Marques
|
r4872 | |||
Fernando Perez
|
r1848 | return nose.tools.make_decorator(f)(skipper) | ||
return skip_decorator | ||||
Jason Grout
|
r6177 | # A version with the condition set to true, common case just to attach a message | ||
Fernando Perez
|
r1848 | # to a skip decorator | ||
def skip(msg=None): | ||||
"""Decorator factory - mark a test function for skipping from test suite. | ||||
Fernando Perez
|
r1721 | |||
Fernando Perez
|
r2368 | Parameters | ||
---------- | ||||
Fernando Perez
|
r1560 | msg : string | ||
Optional message to be added. | ||||
Fernando Perez
|
r1848 | |||
Fernando Perez
|
r2368 | Returns | ||
------- | ||||
Fernando Perez
|
r1848 | decorator : function | ||
Decorator, which, when applied to a function, causes SkipTest | ||||
to be raised, with the optional message added. | ||||
Fernando Perez
|
r1560 | """ | ||
Fernando Perez
|
r1420 | |||
Fernando Perez
|
r1848 | return skipif(True,msg) | ||
Gael Varoquaux
|
r1505 | |||
Fernando Perez
|
r1577 | |||
Fernando Perez
|
r2452 | def onlyif(condition, msg): | ||
"""The reverse from skipif, see skipif for details.""" | ||||
if callable(condition): | ||||
skip_condition = lambda : not condition() | ||||
else: | ||||
skip_condition = lambda : not condition | ||||
return skipif(skip_condition, msg) | ||||
Fernando Perez
|
r1848 | #----------------------------------------------------------------------------- | ||
# Utility functions for decorators | ||||
Paul Ivanov
|
r3504 | def module_not_available(module): | ||
"""Can module be imported? Returns true if module does NOT import. | ||||
Fernando Perez
|
r1577 | |||
Paul Ivanov
|
r3504 | This is used to make a decorator to skip tests that require module to be | ||
Fernando Perez
|
r1848 | available, but delay the 'import numpy' to test execution time. | ||
""" | ||||
try: | ||||
Diego Garcia
|
r22954 | mod = import_module(module) | ||
Paul Ivanov
|
r3504 | mod_not_avail = False | ||
Fernando Perez
|
r1848 | except ImportError: | ||
Paul Ivanov
|
r3504 | mod_not_avail = True | ||
Fernando Perez
|
r1848 | |||
Paul Ivanov
|
r3505 | return mod_not_avail | ||
Fernando Perez
|
r1848 | |||
Paul Ivanov
|
r11970 | |||
def decorated_dummy(dec, name): | ||||
"""Return a dummy function decorated with dec, with the given name. | ||||
Examples | ||||
-------- | ||||
import IPython.testing.decorators as dec | ||||
setup = dec.decorated_dummy(dec.skip_if_no_x11, __name__) | ||||
""" | ||||
Matthias Bussonnier
|
r22991 | warnings.warn("The function `decorated_dummy` is deprecated since IPython 4.0", | ||
Matthias Bussonnier
|
r22989 | DeprecationWarning, stacklevel=2) | ||
Paul Ivanov
|
r11970 | dummy = lambda: None | ||
dummy.__name__ = name | ||||
return dec(dummy) | ||||
Fernando Perez
|
r1848 | #----------------------------------------------------------------------------- | ||
# Decorators for public use | ||||
Fernando Perez
|
r1721 | # Decorators to skip certain tests on specific platforms. | ||
Fernando Perez
|
r1872 | skip_win32 = skipif(sys.platform == 'win32', | ||
Fernando Perez
|
r1848 | "This test does not run under Windows") | ||
MinRK
|
r4138 | skip_linux = skipif(sys.platform.startswith('linux'), | ||
Fernando Perez
|
r1872 | "This test does not run under Linux") | ||
skip_osx = skipif(sys.platform == 'darwin',"This test does not run under OS X") | ||||
Fernando Perez
|
r1848 | |||
Jorgen Stenarson
|
r1803 | # Decorators to skip tests if not on specific platforms. | ||
Fernando Perez
|
r1872 | skip_if_not_win32 = skipif(sys.platform != 'win32', | ||
"This test only runs under Windows") | ||||
MinRK
|
r4138 | skip_if_not_linux = skipif(not sys.platform.startswith('linux'), | ||
Fernando Perez
|
r1872 | "This test only runs under Linux") | ||
skip_if_not_osx = skipif(sys.platform != 'darwin', | ||||
"This test only runs under OSX") | ||||
Paul Ivanov
|
r11970 | |||
_x11_skip_cond = (sys.platform not in ('darwin', 'win32') and | ||||
Paul Ivanov
|
r12135 | os.environ.get('DISPLAY', '') == '') | ||
Paul Ivanov
|
r11970 | _x11_skip_msg = "Skipped under *nix when X11/XOrg not available" | ||
skip_if_no_x11 = skipif(_x11_skip_cond, _x11_skip_msg) | ||||
# not a decorator itself, returns a dummy function to be used as setup | ||||
def skip_file_no_x11(name): | ||||
Matthias Bussonnier
|
r22991 | warnings.warn("The function `skip_file_no_x11` is deprecated since IPython 4.0", | ||
Matthias Bussonnier
|
r22989 | DeprecationWarning, stacklevel=2) | ||
Paul Ivanov
|
r11970 | return decorated_dummy(skip_if_no_x11, name) if _x11_skip_cond else None | ||
Fernando Perez
|
r1872 | # Other skip decorators | ||
Fernando Perez
|
r1848 | |||
MinRK
|
r5146 | # generic skip without module | ||
skip_without = lambda mod: skipif(module_not_available(mod), "This test requires %s" % mod) | ||||
Jens Hedegaard Nielsen
|
r4788 | |||
MinRK
|
r5146 | skipif_not_numpy = skip_without('numpy') | ||
skipif_not_matplotlib = skip_without('matplotlib') | ||||
skipif_not_sympy = skip_without('sympy') | ||||
Paul Ivanov
|
r3504 | |||
Paul Ivanov
|
r3511 | skip_known_failure = knownfailureif(True,'This test is known to fail') | ||
Fernando Perez
|
r2461 | |||
# A null 'decorator', useful to make more readable code that needs to pick | ||||
# between different decorators based on OS or other conditions | ||||
null_deco = lambda f: f | ||||
Thomas Kluyver
|
r3903 | |||
# Some tests only run where we can use unicode paths. Note that we can't just | ||||
# check os.path.supports_unicode_filenames, which is always False on Linux. | ||||
try: | ||||
Thomas Kluyver
|
r3904 | f = tempfile.NamedTemporaryFile(prefix=u"tmp€") | ||
Thomas Kluyver
|
r3903 | except UnicodeEncodeError: | ||
unicode_paths = False | ||||
else: | ||||
unicode_paths = True | ||||
Thomas Kluyver
|
r3904 | f.close() | ||
Thomas Kluyver
|
r3903 | |||
onlyif_unicode_paths = onlyif(unicode_paths, ("This test is only applicable " | ||||
"where we can use unicode in filenames.")) | ||||
Takafumi Arakaki
|
r7858 | |||
def onlyif_cmds_exist(*commands): | ||||
""" | ||||
Decorator to skip test when at least one of `commands` is not found. | ||||
""" | ||||
for cmd in commands: | ||||
Min RK
|
r21122 | if not which(cmd): | ||
return skip("This test runs only if command '{0}' " | ||||
"is installed".format(cmd)) | ||||
Takafumi Arakaki
|
r7858 | return null_deco | ||
MinRK
|
r15438 | |||
def onlyif_any_cmd_exists(*commands): | ||||
""" | ||||
Decorator to skip test unless at least one of `commands` is found. | ||||
""" | ||||
Matthias Bussonnier
|
r22991 | warnings.warn("The function `onlyif_any_cmd_exists` is deprecated since IPython 4.0", | ||
Matthias Bussonnier
|
r22989 | DeprecationWarning, stacklevel=2) | ||
MinRK
|
r15438 | for cmd in commands: | ||
Min RK
|
r21122 | if which(cmd): | ||
return null_deco | ||||
MinRK
|
r15438 | return skip("This test runs only if one of the commands {0} " | ||
"is installed".format(commands)) | ||||