[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