[py-svn] py-trunk commit 94601d28b6dd: generalize skipping

commits-noreply at bitbucket.org commits-noreply at bitbucket.org
Thu Oct 15 16:24:38 CEST 2009


# HG changeset patch -- Bitbucket.org
# Project py-trunk
# URL http://bitbucket.org/hpk42/py-trunk/overview/
# User holger krekel <holger at merlinux.eu>
# Date 1255616337 -7200
# Node ID 94601d28b6dd85f3afe5bde05d7850c6ae1ce34a
# Parent 6d27121e78c29ae55573e4c8a152f4489a02dae3
generalize skipping
- rename pytest_xfail to pytest_skip
- dynamic "skipif" and "xfail" decorators
- move most skipping code to the plugin

also coming with this commit:
- extend mark keyword to accept positional args + docs
- fix a few documentation related issues
- leave version as "trunk" for now

--- a/testing/pytest/plugin/test_pytest_runner.py
+++ b/testing/pytest/plugin/test_pytest_runner.py
@@ -27,6 +27,12 @@ class TestSetupState:
         ss.teardown_all()
         assert not l 
 
+    def test_teardown_exact_stack_empty(self, testdir):
+        item = testdir.getitem("def test_func(): pass")
+        ss = runner.SetupState()
+        ss.teardown_exact(item)
+        ss.teardown_exact(item)
+        ss.teardown_exact(item)
 
 class BaseFunctionalTests:
     def test_passfunction(self, testdir):

--- a/testing/pytest/test_outcome.py
+++ b/testing/pytest/test_outcome.py
@@ -15,26 +15,6 @@ class TestRaises:
     def test_raises_function(self):
         py.test.raises(ValueError, int, 'hello')
 
-def test_importorskip():
-    from _py.test.outcome import Skipped
-    try:
-        sys = py.test.importorskip("sys")
-        assert sys == py.std.sys
-        #path = py.test.importorskip("os.path")
-        #assert path == py.std.os.path
-        py.test.raises(Skipped, "py.test.importorskip('alskdj')")
-        py.test.raises(SyntaxError, "py.test.importorskip('x y z')")
-        py.test.raises(SyntaxError, "py.test.importorskip('x=y')")
-        path = py.test.importorskip("py", minversion=".".join(py.__version__))
-        mod = py.std.types.ModuleType("hello123")
-        mod.__version__ = "1.3" 
-        py.test.raises(Skipped, """
-            py.test.importorskip("hello123", minversion="5.0")
-        """)
-    except Skipped:
-        print(py.code.ExceptionInfo())
-        py.test.fail("spurious skip")
-
 def test_pytest_exit():
     try:
         py.test.exit("hello")

--- a/doc/test/funcargs.txt
+++ b/doc/test/funcargs.txt
@@ -276,6 +276,7 @@ methods in a convenient way.
 .. _`conftest plugin`: customize.html#conftestplugin
 
 .. _`funcarg factory`:
+.. _factory:
  
 funcarg factories: setting up test function arguments 
 ==============================================================

--- a/testing/pytest/conftest.py
+++ b/testing/pytest/conftest.py
@@ -1,3 +1,3 @@
 
-pytest_plugins = "pytest_xfail", "pytest_pytester", "pytest_tmpdir"
+pytest_plugins = "skipping", "pytester", "tmpdir"
 

--- a/testing/pytest/plugin/test_pytest_xfail.py
+++ /dev/null
@@ -1,21 +0,0 @@
-
-def test_xfail(testdir):
-    p = testdir.makepyfile(test_one="""
-        import py
-        @py.test.mark.xfail
-        def test_this():
-            assert 0
-
-        @py.test.mark.xfail
-        def test_that():
-            assert 1
-    """)
-    result = testdir.runpytest(p)
-    extra = result.stdout.fnmatch_lines([
-        "*expected failures*",
-        "*test_one.test_this*test_one.py:4*",
-        "*UNEXPECTEDLY PASSING*",
-        "*test_that*",
-    ])
-    assert result.ret == 1
-

--- a/doc/test/features.txt
+++ b/doc/test/features.txt
@@ -125,22 +125,11 @@ a PDB `Python debugger`_ when a test fai
 advanced skipping of tests 
 -------------------------------
 
-If you want to skip tests you can use ``py.test.skip`` within
-test or setup functions.  Example::
+py.test has builtin support for skipping tests or expecting
+failures on tests on certain platforms.  Apart from the 
+minimal py.test style also unittest- and nose-style tests 
+can make use of this feature.  
 
-    def test_hello():
-        if sys.platform != "win32":
-            py.test.skip("only win32 supported")
-
-You can also use a helper to skip on a failing import::
-
-    docutils = py.test.importorskip("docutils")
-
-or to skip if a library does not have the right version::
-
-    docutils = py.test.importorskip("docutils", minversion="0.3")
-
-The version will be read from the specified module's ``__version__`` attribute. 
 
 .. _`funcargs mechanism`: funcargs.html
 .. _`unittest.py`: http://docs.python.org/library/unittest.html

--- a/testing/pytest/test_parseopt.py
+++ b/testing/pytest/test_parseopt.py
@@ -10,7 +10,7 @@ class TestParser:
 
     def test_epilog(self):
         parser = parseopt.Parser()
-        assert not parser.epilog 
+        assert not parser.epilog
         parser.epilog += "hello"
         assert parser.epilog == "hello"
 
@@ -76,15 +76,6 @@ class TestParser:
         args = parser.parse_setoption([], option)
         assert option.hello == "x"
 
-    def test_parser_epilog(self, testdir):
-        testdir.makeconftest("""
-            def pytest_addoption(parser):
-                parser.epilog = "hello world"
-        """)
-        result = testdir.runpytest('--help')
-        #assert result.ret != 0
-        assert result.stdout.fnmatch_lines(["*hello world*"])
-
     def test_parse_setoption(self):
         parser = parseopt.Parser()
         parser.addoption("--hello", dest="hello", action="store")
@@ -109,3 +100,14 @@ class TestParser:
         option, args = parser.parse([])
         assert option.hello == "world"
         assert option.this == 42
+
+ at py.test.mark.skipif("sys.version_info < (2,5)")
+def test_addoption_parser_epilog(testdir):
+    testdir.makeconftest("""
+        def pytest_addoption(parser):
+            parser.epilog = "hello world"
+    """)
+    result = testdir.runpytest('--help')
+    #assert result.ret != 0
+    assert result.stdout.fnmatch_lines(["*hello world*"])
+

--- a/doc/test/plugin/xfail.txt
+++ /dev/null
@@ -1,34 +0,0 @@
-
-pytest_xfail plugin
-===================
-
-mark python test functions as expected-to-fail and report them separately.
-
-.. contents::
-  :local:
-
-usage
-------------
-
-Use the generic mark decorator to mark your test functions as 
-'expected to fail':: 
-
-    @py.test.mark.xfail
-    def test_hello():
-        ...
-
-This test will be executed but no traceback will be reported 
-when it fails. Instead terminal reporting will list it in the 
-"expected to fail" section or "unexpectedly passing" section.
-
-Start improving this plugin in 30 seconds
-=========================================
-
-
-1. Download `pytest_xfail.py`_ plugin source code 
-2. put it somewhere as ``pytest_xfail.py`` into your import path 
-3. a subsequent ``py.test`` run will use your local version
-
-Checkout customize_, other plugins_ or `get in contact`_. 
-
-.. include:: links.txt

--- a/_py/test/outcome.py
+++ b/_py/test/outcome.py
@@ -56,25 +56,6 @@ def skip(msg=""):
     __tracebackhide__ = True
     raise Skipped(msg=msg) 
 
-def importorskip(modname, minversion=None):
-    """ return imported module or skip() """
-    compile(modname, '', 'eval') # to catch syntaxerrors
-    try:
-        mod = __import__(modname)
-    except ImportError:
-        py.test.skip("could not import %r" %(modname,))
-    if minversion is None:
-        return mod
-    verattr = getattr(mod, '__version__', None)
-    if isinstance(minversion, str):
-        minver = minversion.split(".")
-    else:
-        minver = list(minversion)
-    if verattr is None or verattr.split(".") < minver:
-        py.test.skip("module %r has __version__ %r, required is: %r" %(
-                     modname, verattr, minversion))
-    return mod
-
 def fail(msg="unknown failure"):
     """ fail with the given Message. """
     __tracebackhide__ = True

--- a/_py/test/plugin/pytest_runner.py
+++ b/_py/test/plugin/pytest_runner.py
@@ -276,7 +276,7 @@ class SetupState(object):
         assert not self._finalizers
 
     def teardown_exact(self, item):
-        if item == self.stack[-1]:
+        if self.stack and item == self.stack[-1]:
             self._pop_and_teardown()
         else:
             self._callfinalizers(item)

--- /dev/null
+++ b/testing/pytest/plugin/test_pytest_skipping.py
@@ -0,0 +1,109 @@
+import py
+
+def test_xfail_decorator(testdir):
+    p = testdir.makepyfile(test_one="""
+        import py
+        @py.test.mark.xfail
+        def test_this():
+            assert 0
+
+        @py.test.mark.xfail
+        def test_that():
+            assert 1
+    """)
+    result = testdir.runpytest(p)
+    extra = result.stdout.fnmatch_lines([
+        "*expected failures*",
+        "*test_one.test_this*test_one.py:4*",
+        "*UNEXPECTEDLY PASSING*",
+        "*test_that*",
+        "*1 xfailed*"
+    ])
+    assert result.ret == 1
+
+def test_skipif_decorator(testdir):
+    p = testdir.makepyfile("""
+        import py
+        @py.test.mark.skipif("hasattr(sys, 'platform')")
+        def test_that():
+            assert 0
+    """)
+    result = testdir.runpytest(p)
+    extra = result.stdout.fnmatch_lines([
+        "*Skipped*platform*",
+        "*1 skipped*"
+    ])
+    assert result.ret == 0
+
+def test_skipif_class(testdir):
+    p = testdir.makepyfile("""
+        import py
+        class TestClass:
+            skipif = "True"
+            def test_that(self):
+                assert 0
+            def test_though(self):
+                assert 0
+    """)
+    result = testdir.runpytest(p)
+    extra = result.stdout.fnmatch_lines([
+        "*2 skipped*"
+    ])
+
+def test_getexpression(testdir):
+    from _py.test.plugin.pytest_skipping import getexpression
+    l = testdir.getitems("""
+        import py
+        mod = 5
+        class TestClass:
+            cls = 4
+            @py.test.mark.func(3)
+            def test_func(self):
+                pass
+            @py.test.mark.just
+            def test_other(self):
+                pass
+    """)
+    item, item2 = l
+    assert getexpression(item, 'xyz') is None
+    assert getexpression(item, 'func') == 3
+    assert getexpression(item, 'cls') == 4
+    assert getexpression(item, 'mod') == 5
+
+    assert getexpression(item2, 'just')
+
+def test_evalexpression_cls_config_example(testdir):
+    from _py.test.plugin.pytest_skipping import evalexpression
+    item, = testdir.getitems("""
+        class TestClass:
+            skipif = "config._hackxyz"
+            def test_func(self):
+                pass
+    """)
+    item.config._hackxyz = 3
+    x, y = evalexpression(item, 'skipif')
+    assert x == 'config._hackxyz'
+    assert y == 3
+
+def test_importorskip():
+    from _py.test.outcome import Skipped
+    from _py.test.plugin.pytest_skipping import importorskip
+    assert importorskip == py.test.importorskip
+    try:
+        sys = importorskip("sys")
+        assert sys == py.std.sys
+        #path = py.test.importorskip("os.path")
+        #assert path == py.std.os.path
+        py.test.raises(Skipped, "py.test.importorskip('alskdj')")
+        py.test.raises(SyntaxError, "py.test.importorskip('x y z')")
+        py.test.raises(SyntaxError, "py.test.importorskip('x=y')")
+        path = importorskip("py", minversion=".".join(py.__version__))
+        mod = py.std.types.ModuleType("hello123")
+        mod.__version__ = "1.3"
+        py.test.raises(Skipped, """
+            py.test.importorskip("hello123", minversion="5.0")
+        """)
+    except Skipped:
+        print(py.code.ExceptionInfo())
+        py.test.fail("spurious skip")
+

--- a/doc/test/plugin/keyword.txt
+++ b/doc/test/plugin/keyword.txt
@@ -14,22 +14,29 @@ By default, all filename parts and class
 function are put into the set of keywords for a given test.  You can
 specify additional kewords like this::
 
-    @py.test.mark.webtest 
+    @py.test.mark.webtest
     def test_send_http():
         ... 
 
-This will set an attribute 'webtest' on the given test function
-and by default all such attributes signal keywords.  You can 
-also set values in this attribute which you could read from
-a hook in order to do something special with respect to
-the test function::
+This will set an attribute 'webtest' to True on the given test function.
+You can read the value 'webtest' from the functions __dict__ later.
 
-    @py.test.mark.timeout(seconds=5)
+You can also set values for an attribute which are put on an empty
+dummy object::
+
+    @py.test.mark.webtest(firefox=30)
     def test_receive():
         ...
 
-This will set the "timeout" attribute with a Marker object 
-that has a 'seconds' attribute.
+after which ``test_receive.webtest.firefox == 30`` holds true. 
+
+In addition to keyword arguments you can also use positional arguments::
+
+    @py.test.mark.webtest("triangular")
+    def test_receive():
+        ...
+
+after which ``test_receive.webtest._1 == 'triangular`` hold true.
 
 Start improving this plugin in 30 seconds
 =========================================

--- a/_py/test/defaultconftest.py
+++ b/_py/test/defaultconftest.py
@@ -10,5 +10,5 @@ Generator = py.test.collect.Generator
 Function = py.test.collect.Function
 Instance = py.test.collect.Instance
 
-pytest_plugins = "default runner capture terminal keyword xfail tmpdir monkeypatch recwarn pdb pastebin unittest helpconfig nose assertion".split()
+pytest_plugins = "default runner capture terminal keyword skipping tmpdir monkeypatch recwarn pdb pastebin unittest helpconfig nose assertion".split()
 

--- a/_py/test/plugin/pytest_keyword.py
+++ b/_py/test/plugin/pytest_keyword.py
@@ -8,22 +8,29 @@ By default, all filename parts and class
 function are put into the set of keywords for a given test.  You can
 specify additional kewords like this::
 
-    @py.test.mark.webtest 
+    @py.test.mark.webtest
     def test_send_http():
         ... 
 
-This will set an attribute 'webtest' on the given test function
-and by default all such attributes signal keywords.  You can 
-also set values in this attribute which you could read from
-a hook in order to do something special with respect to
-the test function::
+This will set an attribute 'webtest' to True on the given test function.
+You can read the value 'webtest' from the functions __dict__ later.
 
-    @py.test.mark.timeout(seconds=5)
+You can also set values for an attribute which are put on an empty
+dummy object::
+
+    @py.test.mark.webtest(firefox=30)
     def test_receive():
         ...
 
-This will set the "timeout" attribute with a Marker object 
-that has a 'seconds' attribute. 
+after which ``test_receive.webtest.firefox == 30`` holds true. 
+
+In addition to keyword arguments you can also use positional arguments::
+
+    @py.test.mark.webtest("triangular")
+    def test_receive():
+        ...
+
+after which ``test_receive.webtest._1 == 'triangular`` hold true.
 
 """
 import py
@@ -49,20 +56,20 @@ class MarkerDecorator:
         return "<MarkerDecorator %r %r>" %(name, d)
 
     def __call__(self, *args, **kwargs):
-        if not args:
-            if hasattr(self, 'kwargs'):
-                raise TypeError("double mark-keywords?") 
-            self.kwargs = kwargs.copy()
-            return self 
-        else:
-            if not len(args) == 1 or not hasattr(args[0], '__dict__'):
-                raise TypeError("need exactly one function to decorate, "
-                                "got %r" %(args,))
-            func = args[0]
-            mh = MarkHolder(getattr(self, 'kwargs', {}))
-            setattr(func, self.markname, mh)
-            return func
-
+        if args:
+            if hasattr(args[0], '__call__'):
+                func = args[0]
+                mh = MarkHolder(getattr(self, 'kwargs', {}))
+                setattr(func, self.markname, mh)
+                return func
+            # not a function so we memorize all args/kwargs settings
+            for i, arg in enumerate(args):
+                kwargs["_" + str(i)] = arg
+        if hasattr(self, 'kwargs'):
+            raise TypeError("double mark-keywords?")
+        self.kwargs = kwargs.copy()
+        return self
+        
 class MarkHolder:
     def __init__(self, kwargs):
         self.__dict__.update(kwargs)

--- a/doc/test/plugin/links.txt
+++ b/doc/test/plugin/links.txt
@@ -5,22 +5,23 @@
 .. _`pytest_monkeypatch.py`: http://bitbucket.org/hpk42/py-trunk/raw/trunk/py/test/plugin/pytest_monkeypatch.py
 .. _`pytest_keyword.py`: http://bitbucket.org/hpk42/py-trunk/raw/trunk/py/test/plugin/pytest_keyword.py
 .. _`pastebin`: pastebin.html
+.. _`skipping`: skipping.html
 .. _`plugins`: index.html
-.. _`pytest_capture.py`: http://bitbucket.org/hpk42/py-trunk/raw/trunk/py/test/plugin/pytest_capture.py
 .. _`pytest_doctest.py`: http://bitbucket.org/hpk42/py-trunk/raw/trunk/py/test/plugin/pytest_doctest.py
 .. _`capture`: capture.html
 .. _`pytest_nose.py`: http://bitbucket.org/hpk42/py-trunk/raw/trunk/py/test/plugin/pytest_nose.py
 .. _`pytest_restdoc.py`: http://bitbucket.org/hpk42/py-trunk/raw/trunk/py/test/plugin/pytest_restdoc.py
-.. _`xfail`: xfail.html
+.. _`restdoc`: restdoc.html
 .. _`pytest_pastebin.py`: http://bitbucket.org/hpk42/py-trunk/raw/trunk/py/test/plugin/pytest_pastebin.py
 .. _`pytest_figleaf.py`: http://bitbucket.org/hpk42/py-trunk/raw/trunk/py/test/plugin/pytest_figleaf.py
 .. _`pytest_hooklog.py`: http://bitbucket.org/hpk42/py-trunk/raw/trunk/py/test/plugin/pytest_hooklog.py
+.. _`pytest_skipping.py`: http://bitbucket.org/hpk42/py-trunk/raw/trunk/py/test/plugin/pytest_skipping.py
 .. _`checkout the py.test development version`: ../../download.html#checkout
 .. _`pytest_helpconfig.py`: http://bitbucket.org/hpk42/py-trunk/raw/trunk/py/test/plugin/pytest_helpconfig.py
 .. _`oejskit`: oejskit.html
 .. _`doctest`: doctest.html
 .. _`get in contact`: ../../contact.html
-.. _`pytest_xfail.py`: http://bitbucket.org/hpk42/py-trunk/raw/trunk/py/test/plugin/pytest_xfail.py
+.. _`pytest_capture.py`: http://bitbucket.org/hpk42/py-trunk/raw/trunk/py/test/plugin/pytest_capture.py
 .. _`figleaf`: figleaf.html
 .. _`customize`: ../customize.html
 .. _`hooklog`: hooklog.html
@@ -30,7 +31,6 @@
 .. _`monkeypatch`: monkeypatch.html
 .. _`resultlog`: resultlog.html
 .. _`keyword`: keyword.html
-.. _`restdoc`: restdoc.html
 .. _`django`: django.html
 .. _`pytest_unittest.py`: http://bitbucket.org/hpk42/py-trunk/raw/trunk/py/test/plugin/pytest_unittest.py
 .. _`nose`: nose.html

--- a/testing/path/test_local.py
+++ b/testing/path/test_local.py
@@ -208,7 +208,7 @@ class TestLocalPath(common.CommonFSTests
         assert l[2] == p3
 
 class TestExecutionOnWindows:
-    disabled = py.std.sys.platform != 'win32'
+    skipif = "sys.platform != 'win32'"
 
     def test_sysfind(self):
         x = py.path.local.sysfind('cmd')
@@ -216,7 +216,7 @@ class TestExecutionOnWindows:
         assert py.path.local.sysfind('jaksdkasldqwe') is None
 
 class TestExecution:
-    disabled = py.std.sys.platform == 'win32'
+    skipif = "sys.platform == 'win32'"
 
     def test_sysfind(self):
         x = py.path.local.sysfind('test')
@@ -346,8 +346,7 @@ def test_homedir():
     assert homedir.check(dir=1)
 
 class TestWINLocalPath:
-    #root = local(TestLocalPath.root)
-    disabled = py.std.sys.platform != 'win32'
+    skipif = "sys.platform != 'win32'"
 
     def test_owner_group_not_implemented(self):
         py.test.raises(NotImplementedError, "path1.stat().owner")
@@ -396,7 +395,7 @@ class TestWINLocalPath:
             old.chdir()    
 
 class TestPOSIXLocalPath:
-    disabled = py.std.sys.platform == 'win32'
+    skipif = "sys.platform == 'win32'"
 
     def test_samefile(self, tmpdir):
         assert tmpdir.samefile(tmpdir)

--- a/_py/test/plugin/pytest_xfail.py
+++ /dev/null
@@ -1,65 +0,0 @@
-"""
-mark python test functions as expected-to-fail and report them separately. 
-
-usage
-------------
-
-Use the generic mark decorator to mark your test functions as 
-'expected to fail':: 
-
-    @py.test.mark.xfail
-    def test_hello():
-        ...
-
-This test will be executed but no traceback will be reported 
-when it fails. Instead terminal reporting will list it in the 
-"expected to fail" section or "unexpectedly passing" section.  
-
-"""
-
-import py
-
-def pytest_runtest_makereport(__multicall__, item, call):
-    if call.when != "call":
-        return
-    if hasattr(item, 'obj') and py.builtin._getfuncdict(item.obj):
-        if 'xfail' in py.builtin._getfuncdict(item.obj):
-            res = __multicall__.execute()
-            if call.excinfo:
-                res.skipped = True
-                res.failed = res.passed = False
-            else:
-                res.skipped = res.passed = False
-                res.failed = True
-            return res 
-
-def pytest_report_teststatus(report):
-    if 'xfail' in report.keywords: 
-        if report.skipped:
-            return "xfailed", "x", "xfail"
-        elif report.failed:
-            return "xpassed", "P", "xpass" 
-
-# called by the terminalreporter instance/plugin
-def pytest_terminal_summary(terminalreporter):
-    tr = terminalreporter
-    xfailed = tr.stats.get("xfailed")
-    if xfailed:
-        tr.write_sep("_", "expected failures")
-        for rep in xfailed:
-            entry = rep.longrepr.reprcrash 
-            modpath = rep.item.getmodpath(includemodule=True)
-            pos = "%s %s:%d: " %(modpath, entry.path, entry.lineno)
-            reason = rep.longrepr.reprcrash.message
-            i = reason.find("\n")
-            if i != -1:
-                reason = reason[:i]
-            tr._tw.line("%s %s" %(pos, reason))
-
-    xpassed = terminalreporter.stats.get("xpassed")
-    if xpassed:
-        tr.write_sep("_", "UNEXPECTEDLY PASSING TESTS")
-        for rep in xpassed:
-            fspath, lineno, modpath = rep.item.reportinfo()
-            pos = "%s %s:%d: unexpectedly passing" %(modpath, fspath, lineno)
-            tr._tw.line(pos)

--- /dev/null
+++ b/doc/test/plugin/skipping.txt
@@ -0,0 +1,115 @@
+
+pytest_skipping plugin
+======================
+
+mark python test functions, classes or modules for conditional
+
+.. contents::
+  :local:
+
+skipping (skipif) or as expected-to-fail (xfail).  Both declarations
+lead to special reporting and both can be systematically associated
+with functions, whole classes or modules. The difference between
+the two is that 'xfail' will still execute test functions
+but it will revert the outcome.  A passing test is now
+a failure and failing test is expected.  All skip conditions
+are reported at the end of test run through the terminal
+reporter.
+
+.. _skipif:
+
+skip a test function conditionally
+-------------------------------------------
+
+Here is an example for skipping a test function on Python3::
+
+    @py.test.mark.skipif("sys.version_info >= (3,0)")
+    def test_function():
+        ...
+
+Conditions are specified as python expressions
+and can access the ``sys`` module.  They can also
+access the config object and thus depend on command
+line or conftest options::
+
+    @py.test.mark.skipif("config.getvalue('db') is None")
+    def test_function(...):
+        ...
+
+conditionally mark a function as "expected to fail"
+-------------------------------------------------------
+
+You can use the ``xfail`` keyword to mark your test functions as
+'expected to fail'::
+
+    @py.test.mark.xfail
+    def test_hello():
+        ...
+
+This test will be executed but no traceback will be reported
+when it fails. Instead terminal reporting will list it in the
+"expected to fail" or "unexpectedly passing" sections.
+As with skipif_ you may selectively expect a failure
+depending on platform::
+
+    @py.test.mark.xfail("sys.version_info >= (3,0)")
+    def test_function():
+        ...
+
+skip/xfail a whole test class or module
+-------------------------------------------
+
+Instead of marking single functions you can skip
+a whole class of tests when runnign on a specific
+platform::
+
+    class TestSomething:
+        skipif = "sys.platform == 'win32'"
+
+Or you can mark all test functions as expected
+to fail for a specific test configuration::
+
+    xfail = "config.getvalue('db') == 'mysql'"
+
+
+skip if a dependency cannot be imported
+---------------------------------------------
+
+You can use a helper to skip on a failing import::
+
+    docutils = py.test.importorskip("docutils")
+
+You can use this helper at module level or within
+a test or setup function.
+
+You can aslo skip if a library does not have the right version::
+
+    docutils = py.test.importorskip("docutils", minversion="0.3")
+
+The version will be read from the specified module's ``__version__`` attribute.
+
+
+dynamically skip from within a test or setup
+-------------------------------------------------
+
+If you want to skip the execution of a test you can call
+``py.test.skip()`` within a test, a setup or from a
+`funcarg factory`_ function.  Example::
+
+    def test_function():
+        if not valid_config():
+            py.test.skip("unsuppored configuration")
+
+.. _`funcarg factory`: ../funcargs.html#factory
+
+Start improving this plugin in 30 seconds
+=========================================
+
+
+1. Download `pytest_skipping.py`_ plugin source code 
+2. put it somewhere as ``pytest_skipping.py`` into your import path 
+3. a subsequent ``py.test`` run will use your local version
+
+Checkout customize_, other plugins_ or `get in contact`_. 
+
+.. include:: links.txt

--- a/doc/test/plugin/index.txt
+++ b/doc/test/plugin/index.txt
@@ -2,7 +2,7 @@
 plugins for Python test functions
 =================================
 
-xfail_ mark python test functions as expected-to-fail and report them separately.
+skipping_ mark python test functions, classes or modules for conditional
 
 figleaf_ write and report coverage data with 'figleaf'.
 

--- a/doc/changelog.txt
+++ b/doc/changelog.txt
@@ -1,6 +1,11 @@
 Changes between 1.0.2 and '1.1.0b1'
 =====================================
 
+* generalized skipping: a new way to mark python functions with skipif or xfail 
+  at function, class and modules level based on platform or sys-module attributes. 
+
+* extend py.test.mark decorator to allow for positional args
+
 * introduce and test "py.cleanup -d" to remove empty directories 
 
 * fix issue #59 - robustify unittest test collection

--- a/bin-for-dist/makepluginlist.py
+++ b/bin-for-dist/makepluginlist.py
@@ -5,7 +5,7 @@ WIDTH = 75
 
 plugins = [
     ('plugins for Python test functions', 
-            'xfail figleaf monkeypatch capture recwarn',),
+            'skipping figleaf monkeypatch capture recwarn',),
     ('plugins for other testing styles and languages', 
             'oejskit unittest nose django doctest restdoc'),
     ('plugins for generic reporting and failure logging', 
@@ -252,7 +252,7 @@ class PluginDoc(RestWriter):
                 warn("missing docstring", func)
 
     def emit_options(self, plugin):
-        from py.__.test.parseopt import Parser
+        from _py.test.parseopt import Parser
         options = []
         parser = Parser(processopt=options.append)
         if hasattr(plugin, 'pytest_addoption'):

--- a/doc/confrest.py
+++ b/doc/confrest.py
@@ -1,5 +1,5 @@
 import py
-from py.__.rest.resthtml import convert_rest_html, strip_html_header 
+from _py.rest.resthtml import convert_rest_html, strip_html_header 
 
 html = py.xml.html 
 

--- a/_py/test/plugin/pytest_restdoc.py
+++ b/_py/test/plugin/pytest_restdoc.py
@@ -175,7 +175,7 @@ class ReSTSyntaxTest(py.test.collect.Ite
                                             'to the py package') % (text,)
             relpath = '/'.join(text.split('/')[1:])
             if check:
-                pkgroot = py.__pkg__.getpath()
+                pkgroot = py.path.local(py._py.__file__).dirpath()
                 abspath = pkgroot.join(relpath)
                 assert pkgroot.join(relpath).check(), (
                         'problem with linkrole :source:`%s`: '

--- a/py/__init__.py
+++ b/py/__init__.py
@@ -15,7 +15,7 @@ For questions please check out http://py
 
 (c) Holger Krekel and others, 2009
 """
-version = "1.1.0b1"
+version = "trunk"
 
 __version__ = version = version or "1.1.x"
 import _py.apipkg
@@ -53,7 +53,6 @@ _py.apipkg.initpkg(__name__, dict(
         '_PluginManager'    : '_py.test.pluginmanager:PluginManager',
         'raises'            : '_py.test.outcome:raises',
         'skip'              : '_py.test.outcome:skip',
-        'importorskip'      : '_py.test.outcome:importorskip',
         'fail'              : '_py.test.outcome:fail',
         'exit'              : '_py.test.outcome:exit',
         # configuration/initialization related test api

--- a/testing/path/test_svnurl.py
+++ b/testing/path/test_svnurl.py
@@ -50,12 +50,11 @@ class TestSvnURLCommandPath(CommonSvnTes
     def test_svnurl_characters_tilde_end(self, path1):
         py.path.svnurl("http://host.com/some/file~")
 
+    @py.test.mark.xfail("sys.platform == 'win32'")
     def test_svnurl_characters_colon_path(self, path1):
-        if py.std.sys.platform == 'win32':
-            # colons are allowed on win32, because they're part of the drive
-            # part of an absolute path... however, they shouldn't be allowed in
-            # other parts, I think
-            py.test.skip('XXX fixme win32')
+        # colons are allowed on win32, because they're part of the drive
+        # part of an absolute path... however, they shouldn't be allowed in
+        # other parts, I think
         py.test.raises(ValueError, 'py.path.svnurl("http://host.com/foo:bar")')
 
     def test_export(self, path1, tmpdir):

--- a/doc/test/plugin/hookspec.txt
+++ b/doc/test/plugin/hookspec.txt
@@ -139,6 +139,15 @@ hook specification sourcecode
     # distributed testing 
     # -------------------------------------------------------------------------
     
+    def pytest_gwmanage_newgateway(gateway, platinfo):
+        """ called on new raw gateway creation. """ 
+    
+    def pytest_gwmanage_rsyncstart(source, gateways):
+        """ called before rsyncing a directory to remote gateways takes place. """
+    
+    def pytest_gwmanage_rsyncfinish(source, gateways):
+        """ called after rsyncing a directory to remote gateways takes place. """
+    
     def pytest_testnodeready(node):
         """ Test Node is ready to operate. """
     

--- /dev/null
+++ b/_py/test/plugin/pytest_skipping.py
@@ -0,0 +1,201 @@
+"""
+mark python test functions, classes or modules for conditional
+skipping (skipif) or as expected-to-fail (xfail).  Both declarations
+lead to special reporting and both can be systematically associated
+with functions, whole classes or modules. The difference between
+the two is that 'xfail' will still execute test functions
+but it will revert the outcome.  A passing test is now
+a failure and failing test is expected.  All skip conditions
+are reported at the end of test run through the terminal
+reporter.
+
+.. _skipif:
+
+skip a test function conditionally
+-------------------------------------------
+
+Here is an example for skipping a test function on Python3::
+
+    @py.test.mark.skipif("sys.version_info >= (3,0)")
+    def test_function():
+        ...
+
+Conditions are specified as python expressions
+and can access the ``sys`` module.  They can also
+access the config object and thus depend on command
+line or conftest options::
+
+    @py.test.mark.skipif("config.getvalue('db') is None")
+    def test_function(...):
+        ...
+
+conditionally mark a function as "expected to fail"
+-------------------------------------------------------
+
+You can use the ``xfail`` keyword to mark your test functions as
+'expected to fail'::
+
+    @py.test.mark.xfail
+    def test_hello():
+        ...
+
+This test will be executed but no traceback will be reported
+when it fails. Instead terminal reporting will list it in the
+"expected to fail" or "unexpectedly passing" sections.
+As with skipif_ you may selectively expect a failure
+depending on platform::
+
+    @py.test.mark.xfail("sys.version_info >= (3,0)")
+    def test_function():
+        ...
+
+skip/xfail a whole test class or module
+-------------------------------------------
+
+Instead of marking single functions you can skip
+a whole class of tests when runnign on a specific
+platform::
+
+    class TestSomething:
+        skipif = "sys.platform == 'win32'"
+
+Or you can mark all test functions as expected
+to fail for a specific test configuration::
+
+    xfail = "config.getvalue('db') == 'mysql'"
+
+
+skip if a dependency cannot be imported
+---------------------------------------------
+
+You can use a helper to skip on a failing import::
+
+    docutils = py.test.importorskip("docutils")
+
+You can use this helper at module level or within
+a test or setup function.
+
+You can aslo skip if a library does not have the right version::
+
+    docutils = py.test.importorskip("docutils", minversion="0.3")
+
+The version will be read from the specified module's ``__version__`` attribute.
+
+
+dynamically skip from within a test or setup
+-------------------------------------------------
+
+If you want to skip the execution of a test you can call
+``py.test.skip()`` within a test, a setup or from a
+`funcarg factory`_ function.  Example::
+
+    def test_function():
+        if not valid_config():
+            py.test.skip("unsuppored configuration")
+
+.. _`funcarg factory`: ../funcargs.html#factory
+
+"""
+# XXX not all skip-related code is contained in
+# this plugin yet, some remains in outcome.py and
+# the Skipped Exception is imported here and there.
+
+
+import py
+
+def pytest_namespace():
+    return {'importorskip': importorskip}
+
+def pytest_runtest_setup(item):
+    expr, result = evalexpression(item, 'skipif')
+    if result:
+        py.test.skip(expr)
+
+def pytest_runtest_makereport(__multicall__, item, call):
+    if call.when != "call":
+        return
+    if hasattr(item, 'obj'):
+        expr, result = evalexpression(item, 'xfail')
+        if result:
+            res = __multicall__.execute()
+            if call.excinfo:
+                res.skipped = True
+                res.failed = res.passed = False
+            else:
+                res.skipped = res.passed = False
+                res.failed = True
+            return res
+
+def pytest_report_teststatus(report):
+    if 'xfail' in report.keywords:
+        if report.skipped:
+            return "xfailed", "x", "xfail"
+        elif report.failed:
+            return "xpassed", "P", "xpass"
+
+# called by the terminalreporter instance/plugin
+def pytest_terminal_summary(terminalreporter):
+    tr = terminalreporter
+    xfailed = tr.stats.get("xfailed")
+    if xfailed:
+        tr.write_sep("_", "expected failures")
+        for rep in xfailed:
+            entry = rep.longrepr.reprcrash
+            modpath = rep.item.getmodpath(includemodule=True)
+            pos = "%s %s:%d: " %(modpath, entry.path, entry.lineno)
+            reason = rep.longrepr.reprcrash.message
+            i = reason.find("\n")
+            if i != -1:
+                reason = reason[:i]
+            tr._tw.line("%s %s" %(pos, reason))
+
+    xpassed = terminalreporter.stats.get("xpassed")
+    if xpassed:
+        tr.write_sep("_", "UNEXPECTEDLY PASSING TESTS")
+        for rep in xpassed:
+            fspath, lineno, modpath = rep.item.reportinfo()
+            pos = "%s %s:%d: unexpectedly passing" %(modpath, fspath, lineno)
+            tr._tw.line(pos)
+
+def importorskip(modname, minversion=None):
+    """ return imported module or perform a dynamic skip() """
+    compile(modname, '', 'eval') # to catch syntaxerrors
+    try:
+        mod = __import__(modname)
+    except ImportError:
+        py.test.skip("could not import %r" %(modname,))
+    if minversion is None:
+        return mod
+    verattr = getattr(mod, '__version__', None)
+    if isinstance(minversion, str):
+        minver = minversion.split(".")
+    else:
+        minver = list(minversion)
+    if verattr is None or verattr.split(".") < minver:
+        py.test.skip("module %r has __version__ %r, required is: %r" %(
+                     modname, verattr, minversion))
+    return mod
+
+def getexpression(item, keyword):
+    if isinstance(item, py.test.collect.Function):
+        val = getattr(item.obj, keyword, None)
+        val = getattr(val, '_0', val)
+        if val is not None:
+            return val
+        cls = item.getparent(py.test.collect.Class)
+        if cls and hasattr(cls.obj, keyword):
+            return getattr(cls.obj, keyword)
+        mod = item.getparent(py.test.collect.Module)
+        return getattr(mod.obj, keyword, None)
+
+def evalexpression(item, keyword):
+    expr = getexpression(item, keyword)
+    result = None
+    if expr:
+        if isinstance(expr, str):
+            d = {'sys': py.std.sys, 'config': item.config}
+            result = eval(expr, d)
+        else:
+            result = expr
+    return expr, result
+

--- a/testing/pytest/plugin/test_pytest_keyword.py
+++ b/testing/pytest/plugin/test_pytest_keyword.py
@@ -14,12 +14,14 @@ def test_pytest_mark_api():
     assert f.world.x == 3
     assert f.world.y == 4
 
+    mark.world("hello")(f)
+    assert f.world._0 == "hello"
+
     py.test.raises(TypeError, "mark.some(x=3)(f=5)")
 
 def test_mark_plugin(testdir):
     p = testdir.makepyfile("""
         import py
-        pytest_plugins = "keyword" 
         @py.test.mark.hello
         def test_hello():
             assert hasattr(test_hello, 'hello')



More information about the pytest-commit mailing list