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