[py-svn] py-trunk commit f5b6aad2ea2d: fix issue89 - allow py.test.mark decorators to be used with classes

commits-noreply at bitbucket.org commits-noreply at bitbucket.org
Fri May 21 18:08:46 CEST 2010


# HG changeset patch -- Bitbucket.org
# Project py-trunk
# URL http://bitbucket.org/hpk42/py-trunk/overview
# User holger krekel <holger at merlinux.eu>
# Date 1274458307 -7200
# Node ID f5b6aad2ea2dc0e4822d58c1ac92cef315480d07
# Parent  a6cc8c8e751d15425ab899c512a8a6801f678989
fix issue89 - allow py.test.mark decorators to be used with classes
(if you are using >=python2.6)
also allow to have multiple markers applied at class level
and test and fix a bug with chained skip/xfail decorators:
if any of the conditions is true a test will be skipped/xfailed
with a explanation which condition evaluated to true.

--- a/doc/test/plugin/skipping.txt
+++ b/doc/test/plugin/skipping.txt
@@ -65,6 +65,18 @@ for skipping all methods of a test class
             #
 
 The ``pytestmark`` decorator will be applied to each test function.
+If your code targets python2.6 or above you can also use the
+skipif decorator with classes::
+
+    @py.test.mark.skipif("sys.platform == 'win32'")
+    class TestPosixCalls:
+    
+        def test_function(self):
+            # will not be setup or run under 'win32' platform
+            #
+
+It is fine in both situations to use multiple "skipif" decorators
+on a single function. 
 
 .. _`whole class- or module level`: mark.html#scoped-marking
 

--- a/doc/test/plugin/mark.txt
+++ b/doc/test/plugin/mark.txt
@@ -39,38 +39,43 @@ and later access it with ``test_receive.
 
 .. _`scoped-marking`:
 
-Marking classes or modules 
+Marking whole classes or modules 
 ----------------------------------------------------
 
-To mark all methods of a class set a ``pytestmark`` attribute like this::
+If you are programming with Python2.6 you may use ``py.test.mark`` decorators
+with classes to apply markers to all its test methods::
+
+    @py.test.mark.webtest
+    class TestClass:
+        def test_startup(self):
+            ...
+
+This is equivalent to directly applying the decorator to the
+``test_startup`` function. 
+
+To remain compatible with Python2.5 you can instead set a 
+``pytestmark`` attribute on a TestClass like this::
 
     import py
 
     class TestClass:
         pytestmark = py.test.mark.webtest
 
-You can re-use the same markers that you would use for decorating
-a function - in fact this marker decorator will be applied
-to all test methods of the class. 
+or if you need to use multiple markers::
+
+    import py
+
+    class TestClass:
+        pytestmark = [py.test.mark.webtest, pytest.mark.slowtest]
 
 You can also set a module level marker::
 
     import py
     pytestmark = py.test.mark.webtest
 
-in which case then the marker decorator will be applied to all functions and 
+in which case then it will be applied to all functions and 
 methods defined in the module.  
 
-The order in which marker functions are called is this::
-
-    per-function (upon import of module already) 
-    per-class
-    per-module 
-
-Later called markers may overwrite previous key-value settings. 
-Positional arguments are all appended to the same 'args' list 
-of the Marker object. 
-
 Using "-k MARKNAME" to select tests
 ----------------------------------------------------
 

--- a/CHANGELOG
+++ b/CHANGELOG
@@ -6,6 +6,14 @@ Changes between 1.3.0 and 1.3.1
   to the underlying capturing functionality to avoid race 
   conditions).
 
+- fix issue89 - allow py.test.mark decorators to be used on classes
+  (class decorators were introduced with python2.6)
+  also allow to have multiple markers applied at class/module level
+
+- fix chaining of conditional skipif/xfail decorators - so it works now 
+  as expected to use multiple @py.test.mark.skipif(condition) decorators,
+  including specific reporting which of the conditions lead to skipping. 
+
 - fix issue95: late-import zlib so that it's not required 
   for general py.test startup. 
 

--- a/py/_plugin/pytest_skipping.py
+++ b/py/_plugin/pytest_skipping.py
@@ -60,6 +60,19 @@ for skipping all methods of a test class
             #
 
 The ``pytestmark`` decorator will be applied to each test function.
+If your code targets python2.6 or above you can equivalently use 
+the skipif decorator on classes::
+
+    @py.test.mark.skipif("sys.platform == 'win32'")
+    class TestPosixCalls:
+    
+        def test_function(self):
+            # will not be setup or run under 'win32' platform
+            #
+
+It is fine in general to apply multiple "skipif" decorators
+on a single function - this means that if any of the conditions
+apply the function will be skipped. 
 
 .. _`whole class- or module level`: mark.html#scoped-marking
 
@@ -144,17 +157,20 @@ class MarkEvaluator:
     def istrue(self):
         if self.holder:
             d = {'os': py.std.os, 'sys': py.std.sys, 'config': self.item.config}
-            self.result = True
-            for expr in self.holder.args:
-                self.expr = expr
-                if isinstance(expr, str):
-                    result = cached_eval(self.item.config, expr, d)
-                else:
-                    result = expr
-                if not result:
-                    self.result = False
+            if self.holder.args:
+                self.result = False
+                for expr in self.holder.args:
                     self.expr = expr
-                    break
+                    if isinstance(expr, str):
+                        result = cached_eval(self.item.config, expr, d)
+                    else:
+                        result = expr
+                    if result:
+                        self.result = True
+                        self.expr = expr
+                        break
+            else:
+                self.result = True
         return getattr(self, 'result', False)
 
     def get(self, attr, default=None):

--- a/testing/plugin/test_pytest_skipping.py
+++ b/testing/plugin/test_pytest_skipping.py
@@ -52,6 +52,39 @@ class TestEvaluator:
         assert expl == "hello world"
         assert ev.get("attr") == 2
 
+    def test_marked_one_arg_twice(self, testdir):
+        lines = [
+            '''@py.test.mark.skipif("not hasattr(os, 'murks')")''',
+            '''@py.test.mark.skipif("hasattr(os, 'murks')")'''
+        ]
+        for i in range(0, 2):
+            item = testdir.getitem("""
+                import py 
+                %s
+                %s
+                def test_func(): 
+                    pass
+            """ % (lines[i], lines[(i+1) %2]))
+            ev = MarkEvaluator(item, 'skipif')
+            assert ev
+            assert ev.istrue()
+            expl = ev.getexplanation()
+            assert expl == "condition: not hasattr(os, 'murks')"
+
+    def test_marked_one_arg_twice2(self, testdir):
+        item = testdir.getitem("""
+            import py 
+            @py.test.mark.skipif("hasattr(os, 'murks')")
+            @py.test.mark.skipif("not hasattr(os, 'murks')")
+            def test_func(): 
+                pass
+        """)
+        ev = MarkEvaluator(item, 'skipif')
+        assert ev
+        assert ev.istrue()
+        expl = ev.getexplanation()
+        assert expl == "condition: not hasattr(os, 'murks')"
+
     def test_skipif_class(self, testdir):
         item, = testdir.getitems("""
             import py

--- a/py/_plugin/pytest_mark.py
+++ b/py/_plugin/pytest_mark.py
@@ -34,38 +34,43 @@ and later access it with ``test_receive.
 
 .. _`scoped-marking`:
 
-Marking classes or modules 
+Marking whole classes or modules 
 ----------------------------------------------------
 
-To mark all methods of a class set a ``pytestmark`` attribute like this::
+If you are programming with Python2.6 you may use ``py.test.mark`` decorators
+with classes to apply markers to all its test methods::
+
+    @py.test.mark.webtest
+    class TestClass:
+        def test_startup(self):
+            ...
+
+This is equivalent to directly applying the decorator to the
+``test_startup`` function. 
+
+To remain compatible with Python2.5 you can instead set a 
+``pytestmark`` attribute on a TestClass like this::
 
     import py
 
     class TestClass:
         pytestmark = py.test.mark.webtest
 
-You can re-use the same markers that you would use for decorating
-a function - in fact this marker decorator will be applied
-to all test methods of the class. 
+or if you need to use multiple markers::
+
+    import py
+
+    class TestClass:
+        pytestmark = [py.test.mark.webtest, pytest.mark.slowtest]
 
 You can also set a module level marker::
 
     import py
     pytestmark = py.test.mark.webtest
 
-in which case then the marker decorator will be applied to all functions and 
+in which case then it will be applied to all functions and 
 methods defined in the module.  
 
-The order in which marker functions are called is this::
-
-    per-function (upon import of module already) 
-    per-class
-    per-module 
-
-Later called markers may overwrite previous key-value settings. 
-Positional arguments are all appended to the same 'args' list 
-of the Marker object. 
-
 Using "-k MARKNAME" to select tests
 ----------------------------------------------------
 
@@ -105,15 +110,23 @@ class MarkDecorator:
         """ if passed a single callable argument: decorate it with mark info. 
             otherwise add *args/**kwargs in-place to mark information. """
         if args:
-            if len(args) == 1 and hasattr(args[0], '__call__'):
-                func = args[0]
-                holder = getattr(func, self.markname, None)
-                if holder is None:
-                    holder = MarkInfo(self.markname, self.args, self.kwargs)
-                    setattr(func, self.markname, holder)
+            func = args[0]
+            if len(args) == 1 and hasattr(func, '__call__') or \
+               hasattr(func, '__bases__'):
+                if hasattr(func, '__bases__'):
+                    l = func.__dict__.setdefault("pytestmark", [])
+                    if not isinstance(l, list):
+                       func.pytestmark = [l, self]
+                    else: 
+                       l.append(self)
                 else:
-                    holder.kwargs.update(self.kwargs)
-                    holder.args.extend(self.args)
+                    holder = getattr(func, self.markname, None)
+                    if holder is None:
+                        holder = MarkInfo(self.markname, self.args, self.kwargs)
+                        setattr(func, self.markname, holder)
+                    else:
+                        holder.kwargs.update(self.kwargs)
+                        holder.args.extend(self.args)
                 return func
             else:
                 self.args.extend(args)
@@ -147,6 +160,10 @@ def pytest_pycollect_makeitem(__multical
         func = getattr(func, 'im_func', func)  # py2
         for parent in [x for x in (mod, cls) if x]:
             marker = getattr(parent.obj, 'pytestmark', None)
-            if isinstance(marker, MarkDecorator):
-                marker(func)
+            if marker is not None:
+                if not isinstance(marker, list):
+                    marker = [marker]
+                for mark in marker:
+                    if isinstance(mark, MarkDecorator):
+                        mark(func)
     return item

--- a/testing/plugin/test_pytest_mark.py
+++ b/testing/plugin/test_pytest_mark.py
@@ -68,11 +68,41 @@ class TestFunctional:
         keywords = item.readkeywords()
         assert 'hello' in keywords
 
-    def test_mark_per_class(self, testdir):
+    def test_marklist_per_class(self, testdir):
         modcol = testdir.getmodulecol("""
             import py
             class TestClass:
-                pytestmark = py.test.mark.hello
+                pytestmark = [py.test.mark.hello, py.test.mark.world]
+                def test_func(self):
+                    assert TestClass.test_func.hello  
+                    assert TestClass.test_func.world
+        """)
+        clscol = modcol.collect()[0]
+        item = clscol.collect()[0].collect()[0]
+        keywords = item.readkeywords()
+        assert 'hello' in keywords
+
+    def test_marklist_per_module(self, testdir):
+        modcol = testdir.getmodulecol("""
+            import py
+            pytestmark = [py.test.mark.hello, py.test.mark.world]
+            class TestClass:
+                def test_func(self):
+                    assert TestClass.test_func.hello  
+                    assert TestClass.test_func.world
+        """)
+        clscol = modcol.collect()[0]
+        item = clscol.collect()[0].collect()[0]
+        keywords = item.readkeywords()
+        assert 'hello' in keywords
+        assert 'world' in keywords
+
+    @py.test.mark.skipif("sys.version_info < (2,6)")
+    def test_mark_per_class_decorator(self, testdir):
+        modcol = testdir.getmodulecol("""
+            import py
+            @py.test.mark.hello
+            class TestClass:
                 def test_func(self):
                     assert TestClass.test_func.hello  
         """)
@@ -81,6 +111,23 @@ class TestFunctional:
         keywords = item.readkeywords()
         assert 'hello' in keywords
 
+    @py.test.mark.skipif("sys.version_info < (2,6)")
+    def test_mark_per_class_decorator_plus_existing_dec(self, testdir):
+        modcol = testdir.getmodulecol("""
+            import py
+            @py.test.mark.hello
+            class TestClass:
+                pytestmark = py.test.mark.world
+                def test_func(self):
+                    assert TestClass.test_func.hello  
+                    assert TestClass.test_func.world
+        """)
+        clscol = modcol.collect()[0]
+        item = clscol.collect()[0].collect()[0]
+        keywords = item.readkeywords()
+        assert 'hello' in keywords
+        assert 'world' in keywords
+
     def test_merging_markers(self, testdir):
         p = testdir.makepyfile("""
             import py



More information about the pytest-commit mailing list