1 # -*- coding: utf-8 -*- 2 """Decorators for labeling test objects. 3 4 Decorators that merely return a modified version of the original function 5 object are straightforward. Decorators that return a new function object need 6 to use nose.tools.make_decorator(original_function)(decorator) in returning the 7 decorator, in order to preserve metadata such as function name, setup and 8 teardown functions and so on - see nose.tools for more information. 9 10 This module provides a set of useful decorators meant to be ready to use in 11 your own tests. See the bottom of the file for the ready-made ones, and if you 12 find yourself writing a new one that may be of generic use, add it here. 13 14 Included decorators: 15 16 17 Lightweight 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 24 NOTE: This file contains IPython-specific decorators. Using the machinery in 25 IPython.external.decorators, we import either numpy.testing.decorators if numpy is 26 available, OR use equivalent code in IPython.external._decorators, which 27 we'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 34 import os 35 import shutil 36 import sys 37 import tempfile 38 import unittest 39 from importlib import import_module 40 41 from decorator import decorator 42 43 # Expose the unittest-driven decorators 44 from .ipunittest import ipdoctest, ipdocstring 45 46 #----------------------------------------------------------------------------- 47 # Classes and functions 48 #----------------------------------------------------------------------------- 49 50 # Simple example of the basic idea 51 def as_unittest(func): 52 """Decorator to make a simple function into a normal test via unittest.""" 53 class Tester(unittest.TestCase): 54 def test(self): 55 func() 56 57 Tester.__name__ = func.__name__ 58 59 return Tester 60 61 # Utility functions 62 63 64 def skipif(skip_condition, msg=None): 65 """Make function raise SkipTest exception if skip_condition is true 66 67 Parameters 68 ---------- 69 70 skip_condition : bool or callable 71 Flag to determine whether to skip test. If the condition is a 72 callable, it is used at runtime to dynamically make the decision. This 73 is useful for tests that may require costly imports, to delay the cost 74 until the test suite is actually executed. 75 msg : string 76 Message to give on raising a SkipTest exception. 77 78 Returns 79 ------- 80 decorator : function 81 Decorator, which, when applied to a function, causes SkipTest 82 to be raised when the skip_condition was True, and the function 83 to be called normally otherwise. 84 """ 85 if msg is None: 86 msg = "Test skipped due to test condition." 87 88 import pytest 89 90 assert isinstance(skip_condition, bool) 91 return pytest.mark.skipif(skip_condition, reason=msg) 92 93 94 # A version with the condition set to true, common case just to attach a message 95 # to a skip decorator 96 def skip(msg=None): 97 """Decorator factory - mark a test function for skipping from test suite. 98 99 Parameters 100 ---------- 101 msg : string 102 Optional message to be added. 103 104 Returns 105 ------- 106 decorator : function 107 Decorator, which, when applied to a function, causes SkipTest 108 to be raised, with the optional message added. 109 """ 110 if msg and not isinstance(msg, str): 111 raise ValueError('invalid object passed to `@skip` decorator, did you ' 112 'meant `@skip()` with brackets ?') 113 return skipif(True, msg) 114 115 116 def onlyif(condition, msg): 117 """The reverse from skipif, see skipif for details.""" 118 119 return skipif(not condition, msg) 120 121 #----------------------------------------------------------------------------- 122 # Utility functions for decorators 123 def module_not_available(module): 124 """Can module be imported? Returns true if module does NOT import. 125 126 This is used to make a decorator to skip tests that require module to be 127 available, but delay the 'import numpy' to test execution time. 128 """ 129 try: 130 mod = import_module(module) 131 mod_not_avail = False 132 except ImportError: 133 mod_not_avail = True 134 135 return mod_not_avail 136 137 138 #----------------------------------------------------------------------------- 139 # Decorators for public use 140 141 # Decorators to skip certain tests on specific platforms. 142 skip_win32 = skipif(sys.platform == 'win32', 143 "This test does not run under Windows") 144 skip_linux = skipif(sys.platform.startswith('linux'), 145 "This test does not run under Linux") 146 skip_osx = skipif(sys.platform == 'darwin',"This test does not run under OS X") 147 148 149 # Decorators to skip tests if not on specific platforms. 150 skip_if_not_win32 = skipif(sys.platform != "win32", "This test only runs under Windows") 151 skip_if_not_linux = skipif( 152 not sys.platform.startswith("linux"), "This test only runs under Linux" 153 ) 154 skip_if_not_osx = skipif( 155 not sys.platform.startswith("darwin"), "This test only runs under macOS" 156 ) 157 158 _x11_skip_cond = (sys.platform not in ('darwin', 'win32') and 159 os.environ.get('DISPLAY', '') == '') 160 _x11_skip_msg = "Skipped under *nix when X11/XOrg not available" 161 162 skip_if_no_x11 = skipif(_x11_skip_cond, _x11_skip_msg) 163 164 # Other skip decorators 165 166 # generic skip without module 167 skip_without = lambda mod: skipif(module_not_available(mod), "This test requires %s" % mod) 168 169 skipif_not_numpy = skip_without('numpy') 170 171 skipif_not_matplotlib = skip_without('matplotlib') 172 173 # A null 'decorator', useful to make more readable code that needs to pick 174 # between different decorators based on OS or other conditions 175 null_deco = lambda f: f 176 177 # Some tests only run where we can use unicode paths. Note that we can't just 178 # check os.path.supports_unicode_filenames, which is always False on Linux. 179 try: 180 f = tempfile.NamedTemporaryFile(prefix=u"tmp€") 181 except UnicodeEncodeError: 182 unicode_paths = False 183 else: 184 unicode_paths = True 185 f.close() 186 187 onlyif_unicode_paths = onlyif(unicode_paths, ("This test is only applicable " 188 "where we can use unicode in filenames.")) 189 190 191 def onlyif_cmds_exist(*commands): 192 """ 193 Decorator to skip test when at least one of `commands` is not found. 194 """ 195 assert ( 196 os.environ.get("IPTEST_WORKING_DIR", None) is None 197 ), "iptest deprecated since IPython 8.0" 198 for cmd in commands: 199 reason = f"This test runs only if command '{cmd}' is installed" 200 if not shutil.which(cmd): 201 import pytest 202 203 return pytest.mark.skip(reason=reason) 204 return null_deco 205