1# -*- coding: utf-8 -*- 2"""Decorators for labeling test objects. 3 4Decorators that merely return a modified version of the original function 5object are straightforward. Decorators that return a new function object need 6to use nose.tools.make_decorator(original_function)(decorator) in returning the 7decorator, in order to preserve metadata such as function name, setup and 8teardown functions and so on - see nose.tools for more information. 9 10This module provides a set of useful decorators meant to be ready to use in 11your own tests. See the bottom of the file for the ready-made ones, and if you 12find yourself writing a new one that may be of generic use, add it here. 13 14Included decorators: 15 16 17Lightweight testing that remains unittest-compatible. 18 19- An @as_unittest decorator can be used to tag any normal parameter-less 20 function as a unittest TestCase. Then, both nose and normal unittest will 21 recognize it as such. This will make it easier to migrate away from Nose if 22 we ever need/want to while maintaining very lightweight tests. 23 24NOTE: This file contains IPython-specific decorators. Using the machinery in 25IPython.external.decorators, we import either numpy.testing.decorators if numpy is 26available, OR use equivalent code in IPython.external._decorators, which 27we've copied verbatim from numpy. 28 29""" 30 31# Copyright (c) IPython Development Team. 32# Distributed under the terms of the Modified BSD License. 33 34import os 35import shutil 36import sys 37import tempfile 38import unittest 39import warnings 40from importlib import import_module 41 42from decorator import decorator 43 44# Expose the unittest-driven decorators 45from .ipunittest import ipdoctest, ipdocstring 46 47# Grab the numpy-specific decorators which we keep in a file that we 48# occasionally update from upstream: decorators.py is a copy of 49# numpy.testing.decorators, we expose all of it here. 50from IPython.external.decorators import knownfailureif 51 52#----------------------------------------------------------------------------- 53# Classes and functions 54#----------------------------------------------------------------------------- 55 56# Simple example of the basic idea 57def as_unittest(func): 58 """Decorator to make a simple function into a normal test via unittest.""" 59 class Tester(unittest.TestCase): 60 def test(self): 61 func() 62 63 Tester.__name__ = func.__name__ 64 65 return Tester 66 67# Utility functions 68 69def apply_wrapper(wrapper, func): 70 """Apply a wrapper to a function for decoration. 71 72 This mixes Michele Simionato's decorator tool with nose's make_decorator, 73 to apply a wrapper in a decorator so that all nose attributes, as well as 74 function signature and other properties, survive the decoration cleanly. 75 This will ensure that wrapped functions can still be well introspected via 76 IPython, for example. 77 """ 78 warnings.warn("The function `apply_wrapper` is deprecated since IPython 4.0", 79 DeprecationWarning, stacklevel=2) 80 import nose.tools 81 82 return decorator(wrapper,nose.tools.make_decorator(func)(wrapper)) 83 84 85def make_label_dec(label, ds=None): 86 """Factory function to create a decorator that applies one or more labels. 87 88 Parameters 89 ---------- 90 label : string or sequence 91 One or more labels that will be applied by the decorator to the functions 92 it decorates. Labels are attributes of the decorated function with their 93 value set to True. 94 95 ds : string 96 An optional docstring for the resulting decorator. If not given, a 97 default docstring is auto-generated. 98 99 Returns 100 ------- 101 A decorator. 102 103 Examples 104 -------- 105 106 A simple labeling decorator: 107 108 >>> slow = make_label_dec('slow') 109 >>> slow.__doc__ 110 "Labels a test as 'slow'." 111 112 And one that uses multiple labels and a custom docstring: 113 114 >>> rare = make_label_dec(['slow','hard'], 115 ... "Mix labels 'slow' and 'hard' for rare tests.") 116 >>> rare.__doc__ 117 "Mix labels 'slow' and 'hard' for rare tests." 118 119 Now, let's test using this one: 120 >>> @rare 121 ... def f(): pass 122 ... 123 >>> 124 >>> f.slow 125 True 126 >>> f.hard 127 True 128 """ 129 130 warnings.warn("The function `make_label_dec` is deprecated since IPython 4.0", 131 DeprecationWarning, stacklevel=2) 132 if isinstance(label, str): 133 labels = [label] 134 else: 135 labels = label 136 137 # Validate that the given label(s) are OK for use in setattr() by doing a 138 # dry run on a dummy function. 139 tmp = lambda : None 140 for label in labels: 141 setattr(tmp,label,True) 142 143 # This is the actual decorator we'll return 144 def decor(f): 145 for label in labels: 146 setattr(f,label,True) 147 return f 148 149 # Apply the user's docstring, or autogenerate a basic one 150 if ds is None: 151 ds = "Labels a test as %r." % label 152 decor.__doc__ = ds 153 154 return decor 155 156 157# Inspired by numpy's skipif, but uses the full apply_wrapper utility to 158# preserve function metadata better and allows the skip condition to be a 159# callable. 160def skipif(skip_condition, msg=None): 161 ''' Make function raise SkipTest exception if skip_condition is true 162 163 Parameters 164 ---------- 165 166 skip_condition : bool or callable 167 Flag to determine whether to skip test. If the condition is a 168 callable, it is used at runtime to dynamically make the decision. This 169 is useful for tests that may require costly imports, to delay the cost 170 until the test suite is actually executed. 171 msg : string 172 Message to give on raising a SkipTest exception. 173 174 Returns 175 ------- 176 decorator : function 177 Decorator, which, when applied to a function, causes SkipTest 178 to be raised when the skip_condition was True, and the function 179 to be called normally otherwise. 180 181 Notes 182 ----- 183 You will see from the code that we had to further decorate the 184 decorator with the nose.tools.make_decorator function in order to 185 transmit function name, and various other metadata. 186 ''' 187 188 def skip_decorator(f): 189 # Local import to avoid a hard nose dependency and only incur the 190 # import time overhead at actual test-time. 191 import nose 192 193 # Allow for both boolean or callable skip conditions. 194 if callable(skip_condition): 195 skip_val = skip_condition 196 else: 197 skip_val = lambda : skip_condition 198 199 def get_msg(func,msg=None): 200 """Skip message with information about function being skipped.""" 201 if msg is None: out = 'Test skipped due to test condition.' 202 else: out = msg 203 return "Skipping test: %s. %s" % (func.__name__,out) 204 205 # We need to define *two* skippers because Python doesn't allow both 206 # return with value and yield inside the same function. 207 def skipper_func(*args, **kwargs): 208 """Skipper for normal test functions.""" 209 if skip_val(): 210 raise nose.SkipTest(get_msg(f,msg)) 211 else: 212 return f(*args, **kwargs) 213 214 def skipper_gen(*args, **kwargs): 215 """Skipper for test generators.""" 216 if skip_val(): 217 raise nose.SkipTest(get_msg(f,msg)) 218 else: 219 for x in f(*args, **kwargs): 220 yield x 221 222 # Choose the right skipper to use when building the actual generator. 223 if nose.util.isgenerator(f): 224 skipper = skipper_gen 225 else: 226 skipper = skipper_func 227 228 return nose.tools.make_decorator(f)(skipper) 229 230 return skip_decorator 231 232# A version with the condition set to true, common case just to attach a message 233# to a skip decorator 234def skip(msg=None): 235 """Decorator factory - mark a test function for skipping from test suite. 236 237 Parameters 238 ---------- 239 msg : string 240 Optional message to be added. 241 242 Returns 243 ------- 244 decorator : function 245 Decorator, which, when applied to a function, causes SkipTest 246 to be raised, with the optional message added. 247 """ 248 if msg and not isinstance(msg, str): 249 raise ValueError('invalid object passed to `@skip` decorator, did you ' 250 'meant `@skip()` with brackets ?') 251 return skipif(True, msg) 252 253 254def onlyif(condition, msg): 255 """The reverse from skipif, see skipif for details.""" 256 257 if callable(condition): 258 skip_condition = lambda : not condition() 259 else: 260 skip_condition = lambda : not condition 261 262 return skipif(skip_condition, msg) 263 264#----------------------------------------------------------------------------- 265# Utility functions for decorators 266def module_not_available(module): 267 """Can module be imported? Returns true if module does NOT import. 268 269 This is used to make a decorator to skip tests that require module to be 270 available, but delay the 'import numpy' to test execution time. 271 """ 272 try: 273 mod = import_module(module) 274 mod_not_avail = False 275 except ImportError: 276 mod_not_avail = True 277 278 return mod_not_avail 279 280 281def decorated_dummy(dec, name): 282 """Return a dummy function decorated with dec, with the given name. 283 284 Examples 285 -------- 286 import IPython.testing.decorators as dec 287 setup = dec.decorated_dummy(dec.skip_if_no_x11, __name__) 288 """ 289 warnings.warn("The function `decorated_dummy` is deprecated since IPython 4.0", 290 DeprecationWarning, stacklevel=2) 291 dummy = lambda: None 292 dummy.__name__ = name 293 return dec(dummy) 294 295#----------------------------------------------------------------------------- 296# Decorators for public use 297 298# Decorators to skip certain tests on specific platforms. 299skip_win32 = skipif(sys.platform == 'win32', 300 "This test does not run under Windows") 301skip_linux = skipif(sys.platform.startswith('linux'), 302 "This test does not run under Linux") 303skip_osx = skipif(sys.platform == 'darwin',"This test does not run under OS X") 304 305 306# Decorators to skip tests if not on specific platforms. 307skip_if_not_win32 = skipif(sys.platform != 'win32', 308 "This test only runs under Windows") 309skip_if_not_linux = skipif(not sys.platform.startswith('linux'), 310 "This test only runs under Linux") 311skip_if_not_osx = skipif(sys.platform != 'darwin', 312 "This test only runs under OSX") 313 314 315_x11_skip_cond = (sys.platform not in ('darwin', 'win32') and 316 os.environ.get('DISPLAY', '') == '') 317_x11_skip_msg = "Skipped under *nix when X11/XOrg not available" 318 319skip_if_no_x11 = skipif(_x11_skip_cond, _x11_skip_msg) 320 321 322# Decorators to skip certain tests on specific platform/python combinations 323skip_win32_py38 = skipif(sys.version_info > (3,8) and os.name == 'nt') 324 325 326# not a decorator itself, returns a dummy function to be used as setup 327def skip_file_no_x11(name): 328 warnings.warn("The function `skip_file_no_x11` is deprecated since IPython 4.0", 329 DeprecationWarning, stacklevel=2) 330 return decorated_dummy(skip_if_no_x11, name) if _x11_skip_cond else None 331 332# Other skip decorators 333 334# generic skip without module 335skip_without = lambda mod: skipif(module_not_available(mod), "This test requires %s" % mod) 336 337skipif_not_numpy = skip_without('numpy') 338 339skipif_not_matplotlib = skip_without('matplotlib') 340 341skipif_not_sympy = skip_without('sympy') 342 343skip_known_failure = knownfailureif(True,'This test is known to fail') 344 345# A null 'decorator', useful to make more readable code that needs to pick 346# between different decorators based on OS or other conditions 347null_deco = lambda f: f 348 349# Some tests only run where we can use unicode paths. Note that we can't just 350# check os.path.supports_unicode_filenames, which is always False on Linux. 351try: 352 f = tempfile.NamedTemporaryFile(prefix=u"tmp€") 353except UnicodeEncodeError: 354 unicode_paths = False 355else: 356 unicode_paths = True 357 f.close() 358 359onlyif_unicode_paths = onlyif(unicode_paths, ("This test is only applicable " 360 "where we can use unicode in filenames.")) 361 362 363def onlyif_cmds_exist(*commands): 364 """ 365 Decorator to skip test when at least one of `commands` is not found. 366 """ 367 for cmd in commands: 368 if not shutil.which(cmd): 369 return skip("This test runs only if command '{0}' " 370 "is installed".format(cmd)) 371 return null_deco 372 373def onlyif_any_cmd_exists(*commands): 374 """ 375 Decorator to skip test unless at least one of `commands` is found. 376 """ 377 warnings.warn("The function `onlyif_any_cmd_exists` is deprecated since IPython 4.0", 378 DeprecationWarning, stacklevel=2) 379 for cmd in commands: 380 if shutil.which(cmd): 381 return null_deco 382 return skip("This test runs only if one of the commands {0} " 383 "is installed".format(commands)) 384