From d398b430510ba0fa630be368d498446caa7b653c Mon Sep 17 00:00:00 2001 From: JDWarner Date: Wed, 20 Nov 2013 11:20:42 -0600 Subject: [PATCH] ENH: Add `doctest_skip_parser` allowing conditional skipping of doctests --- skimage/_shared/testing.py | 51 ++++++++++++++++ skimage/_shared/tests/test_testing.py | 87 +++++++++++++++++++++++++++ skimage/viewer/plugins/base.py | 23 ++++--- skimage/viewer/viewers/core.py | 13 +++- 4 files changed, 163 insertions(+), 11 deletions(-) create mode 100644 skimage/_shared/tests/test_testing.py diff --git a/skimage/_shared/testing.py b/skimage/_shared/testing.py index eab83a56..9b42c582 100644 --- a/skimage/_shared/testing.py +++ b/skimage/_shared/testing.py @@ -1,6 +1,12 @@ """Testing utilities.""" +import re + + +SKIP_RE = re.compile("(\s*>>>.*?)(\s*)#\s*skip\s+if\s+(.*)$") + + def _assert_less(a, b, msg=None): message = "%r is not lower than %r" % (a, b) if msg is not None: @@ -24,3 +30,48 @@ try: from nose.tools import assert_greater except ImportError: assert_greater = _assert_greater + + +def doctest_skip_parser(func): + """ Decorator replaces custom skip test markup in doctests + + Say a function has a docstring:: + + >>> something # skip if not HAVE_AMODULE + >>> something + else + >>> something # skip if HAVE_BMODULE + + This decorator will evaluate the expresssion after ``skip if``. If this + evaluates to True, then the comment is replaced by ``# doctest: +SKIP``. If + False, then the comment is just removed. The expression is evaluated in the + ``globals`` scope of `func`. + + For example, if the module global ``HAVE_AMODULE`` is False, and module + global ``HAVE_BMODULE`` is False, the returned function will have docstring:: + + >>> something # doctest: +SKIP + >>> something + else + >>> something + + """ + lines = func.__doc__.split('\n') + new_lines = [] + for line in lines: + match = SKIP_RE.match(line) + if match is None: + new_lines.append(line) + continue + code, space, expr = match.groups() + + try: + # Works as a function decorator + if eval(expr, func.__globals__): + code = code + space + "# doctest: +SKIP" + except AttributeError: + # Works as a class decorator + if eval(expr, func.__init__.__globals__): + code = code + space + "# doctest: +SKIP" + + new_lines.append(code) + func.__doc__ = "\n".join(new_lines) + return func diff --git a/skimage/_shared/tests/test_testing.py b/skimage/_shared/tests/test_testing.py new file mode 100644 index 00000000..f563caad --- /dev/null +++ b/skimage/_shared/tests/test_testing.py @@ -0,0 +1,87 @@ +""" Testing decorators module +""" + +import numpy as np +from nose.tools import (assert_true, assert_raises, assert_equal) +from skimage._shared.testing import doctest_skip_parser + + +def test_skipper(): + def f(): + pass + + class c(): + def __init__(self): + self.me = "I think, therefore..." + + docstring = \ + """ Header + + >>> something # skip if not HAVE_AMODULE + >>> something + else + >>> a = 1 # skip if not HAVE_BMODULE + >>> something2 # skip if HAVE_AMODULE + """ + f.__doc__ = docstring + c.__doc__ = docstring + + global HAVE_AMODULE, HAVE_BMODULE + HAVE_AMODULE = False + HAVE_BMODULE = True + + f2 = doctest_skip_parser(f) + c2 = doctest_skip_parser(c) + assert_true(f is f2) + assert_true(c is c2) + + assert_equal(f2.__doc__, + """ Header + + >>> something # doctest: +SKIP + >>> something + else + >>> a = 1 + >>> something2 + """) + assert_equal(c2.__doc__, + """ Header + + >>> something # doctest: +SKIP + >>> something + else + >>> a = 1 + >>> something2 + """) + + HAVE_AMODULE = True + HAVE_BMODULE = False + f.__doc__ = docstring + c.__doc__ = docstring + f2 = doctest_skip_parser(f) + c2 = doctest_skip_parser(c) + + assert_true(f is f2) + assert_equal(f2.__doc__, + """ Header + + >>> something + >>> something + else + >>> a = 1 # doctest: +SKIP + >>> something2 # doctest: +SKIP + """) + assert_equal(c2.__doc__, + """ Header + + >>> something + >>> something + else + >>> a = 1 # doctest: +SKIP + >>> something2 # doctest: +SKIP + """) + + del HAVE_AMODULE + f.__doc__ = docstring + c.__doc__ = docstring + assert_raises(NameError, doctest_skip_parser, f) + assert_raises(NameError, doctest_skip_parser, c) + + +if __name__ == '__main__': + np.testing.run_module_suite() diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index 1a2eedd9..6aa37947 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -5,11 +5,18 @@ from warnings import warn import numpy as np -from ..qt import QtGui +from ..qt import QtGui, qt_api from ..qt.QtCore import Qt, Signal from ..utils import RequiredAttr, init_qtapp +from skimage._shared.testing import doctest_skip_parser + +if qt_api is not None: + has_qt = True +else: + has_qt = False +@doctest_skip_parser class Plugin(QtGui.QDialog): """Base class for plugins that interact with an ImageViewer. @@ -52,13 +59,14 @@ class Plugin(QtGui.QDialog): >>> from skimage.viewer.widgets import Slider >>> from skimage import data >>> - >>> plugin = Plugin(image_filter=lambda img, threshold: img > threshold) - >>> plugin += Slider('threshold', 0, 255) + >>> plugin = Plugin(image_filter=lambda img, + ... threshold: img > threshold) # skip if not has_qt + >>> plugin += Slider('threshold', 0, 255) # skip if not has_qt >>> >>> image = data.coins() - >>> viewer = ImageViewer(image) - >>> viewer += plugin - >>> # viewer.show() + >>> viewer = ImageViewer(image) # skip if not has_qt + >>> viewer += plugin # skip if not has_qt + >>> viewer.show() # skip if not has_qt The plugin will automatically delegate parameters to `image_filter` based on its parameter type, i.e., `ptype` (widgets for required arguments must @@ -99,7 +107,7 @@ class Plugin(QtGui.QDialog): self.row = 0 self.arguments = [] - self.keyword_arguments= {} + self.keyword_arguments = {} self.useblit = useblit self.cids = [] @@ -254,4 +262,3 @@ class Plugin(QtGui.QDialog): you don't want to return a value. """ return (self.image_viewer.image, None) - diff --git a/skimage/viewer/viewers/core.py b/skimage/viewer/viewers/core.py index 781b6877..1bf53f64 100644 --- a/skimage/viewer/viewers/core.py +++ b/skimage/viewer/viewers/core.py @@ -1,12 +1,18 @@ """ ImageViewer class for viewing and interacting with images. """ -from ..qt import QtGui +from ..qt import QtGui, qt_api from ..qt.QtCore import Qt, Signal +if qt_api is not None: + has_qt = True +else: + has_qt = False + from skimage import io, img_as_float from skimage.util.dtype import dtype_range from skimage.exposure import rescale_intensity +from skimage._shared.testing import doctest_skip_parser import numpy as np from .. import utils from ..widgets import Slider @@ -45,6 +51,7 @@ def mpl_image_to_rgba(mpl_image): return img_as_float(image) +@doctest_skip_parser class ImageViewer(QtGui.QMainWindow): """Viewer for displaying images. @@ -72,8 +79,8 @@ class ImageViewer(QtGui.QMainWindow): -------- >>> from skimage import data >>> image = data.coins() - >>> viewer = ImageViewer(image) - >>> viewer.show() # doctest: +SKIP + >>> viewer = ImageViewer(image) # skip if not has_qt + >>> viewer.show() # skip if not has_qt """