decorators.py
366 lines
| 12.0 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. | ||||
- @parametric, for parametric test support that is vastly easier to use than | ||||
nose's for debugging. With ours, if a test fails, the stack under inspection | ||||
is that of the test and not that of the test framework. | ||||
- 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 | |||
Authors | ||||
------- | ||||
- Fernando Perez <Fernando.Perez@berkeley.edu> | ||||
Fernando Perez
|
r1420 | """ | ||
Fernando Perez
|
r2368 | #----------------------------------------------------------------------------- | ||
Matthias BUSSONNIER
|
r5390 | # Copyright (C) 2009-2011 The IPython Development Team | ||
Fernando Perez
|
r2368 | # | ||
# Distributed under the terms of the BSD License. The full license is in | ||||
# the file COPYING, distributed as part of this software. | ||||
#----------------------------------------------------------------------------- | ||||
#----------------------------------------------------------------------------- | ||||
# Imports | ||||
#----------------------------------------------------------------------------- | ||||
Fernando Perez
|
r1420 | # Stdlib imports | ||
Fernando Perez
|
r1721 | import sys | ||
Thomas Kluyver
|
r3903 | import tempfile | ||
Fernando Perez
|
r2368 | import unittest | ||
Fernando Perez
|
r1420 | |||
# Third-party imports | ||||
Fernando Perez
|
r2368 | # This is Michele Simionato's decorator module, kept verbatim. | ||
Thomas Kluyver
|
r4053 | from IPython.external.decorator import decorator | ||
Fernando Perez
|
r1420 | |||
Fernando Perez
|
r2368 | # We already have python3-compliant code for parametric tests | ||
if sys.version[0]=='2': | ||||
Thomas Kluyver
|
r11133 | from _paramtestpy2 import parametric | ||
Fernando Perez
|
r2368 | else: | ||
Thomas Kluyver
|
r11133 | from _paramtestpy3 import parametric | ||
Fernando Perez
|
r2368 | |||
Fernando Perez
|
r2414 | # Expose the unittest-driven decorators | ||
from ipunittest import ipdoctest, ipdocstring | ||||
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 | ||
from IPython.utils.process import is_cmd_found | ||||
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 | ||||
def apply_wrapper(wrapper,func): | ||||
"""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. | ||||
""" | ||||
import nose.tools | ||||
return decorator(wrapper,nose.tools.make_decorator(func)(wrapper)) | ||||
def make_label_dec(label,ds=None): | ||||
"""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 | ||||
""" | ||||
if isinstance(label,basestring): | ||||
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 | ---------- | ||
Bernardo B. Marques
|
r4872 | skip_condition : bool or callable. | ||
Thomas Kluyver
|
r9244 | 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 | ||
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. | ||||
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: | ||||
Paul Ivanov
|
r3504 | mod = __import__(module) | ||
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 | |||
#----------------------------------------------------------------------------- | ||||
# 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") | ||||
# 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 | |||
Thomas Kluyver
|
r5204 | known_failure_py3 = knownfailureif(sys.version_info[0] >= 3, | ||
'This test is known to fail on Python 3.') | ||||
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: | ||||
Paul Ivanov
|
r11456 | try: | ||
if not is_cmd_found(cmd): | ||||
return skip("This test runs only if command '{0}' " | ||||
"is installed".format(cmd)) | ||||
except ImportError as e: | ||||
# is_cmd_found uses pywin32 on windows, which might not be available | ||||
if sys.platform == 'win32' and 'pywin32' in e.message: | ||||
return skip("This test runs only if pywin32 and command '{0}' " | ||||
"is installed".format(cmd)) | ||||
raise e | ||||
Takafumi Arakaki
|
r7858 | return null_deco | ||