diff --git a/CHANGELOG b/CHANGELOG index 535d1baa9..d9fa3a4f0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,10 @@ 2.7.3 (compared to 2.7.2) ----------------------------- +- fix issue842: applying markers in classes no longer propagate this markers + to superclasses which also have markers. + Thanks xmo-odoo for the report and Bruno Oliveira for the PR. + - preserve warning functions after call to pytest.deprecated_call. Thanks Pieter Mulder for PR. diff --git a/_pytest/mark.py b/_pytest/mark.py index 1d5043578..791f6ef55 100644 --- a/_pytest/mark.py +++ b/_pytest/mark.py @@ -1,4 +1,5 @@ """ generic mechanism for marking and selecting python functions. """ +import inspect import py @@ -253,15 +254,17 @@ class MarkDecorator: otherwise add *args/**kwargs in-place to mark information. """ if args and not kwargs: func = args[0] - if len(args) == 1 and (istestfunc(func) or - hasattr(func, '__bases__')): - if hasattr(func, '__bases__'): + is_class = inspect.isclass(func) + if len(args) == 1 and (istestfunc(func) or is_class): + if is_class: if hasattr(func, 'pytestmark'): - l = func.pytestmark - if not isinstance(l, list): - func.pytestmark = [l, self] - else: - l.append(self) + mark_list = func.pytestmark + if not isinstance(mark_list, list): + mark_list = [mark_list] + # always work on a copy to avoid updating pytestmark + # from a superclass by accident + mark_list = mark_list + [self] + func.pytestmark = mark_list else: func.pytestmark = [self] else: diff --git a/testing/test_mark.py b/testing/test_mark.py index a7ee038ea..3527deb1e 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -369,6 +369,45 @@ class TestFunctional: print (item, item.keywords) assert 'a' in item.keywords + def test_mark_decorator_subclass_does_not_propagate_to_base(self, testdir): + p = testdir.makepyfile(""" + import pytest + + @pytest.mark.a + class Base: pass + + @pytest.mark.b + class Test1(Base): + def test_foo(self): pass + + class Test2(Base): + def test_bar(self): pass + """) + items, rec = testdir.inline_genitems(p) + self.assert_markers(items, test_foo=('a', 'b'), test_bar=('a',)) + + def test_mark_decorator_baseclasses_merged(self, testdir): + p = testdir.makepyfile(""" + import pytest + + @pytest.mark.a + class Base: pass + + @pytest.mark.b + class Base2(Base): pass + + @pytest.mark.c + class Test1(Base2): + def test_foo(self): pass + + class Test2(Base2): + @pytest.mark.d + def test_bar(self): pass + """) + items, rec = testdir.inline_genitems(p) + self.assert_markers(items, test_foo=('a', 'b', 'c'), + test_bar=('a', 'b', 'd')) + def test_mark_with_wrong_marker(self, testdir): reprec = testdir.inline_runsource(""" import pytest @@ -477,6 +516,22 @@ class TestFunctional: reprec = testdir.inline_run("-m", "mark1") reprec.assertoutcome(passed=1) + def assert_markers(self, items, **expected): + """assert that given items have expected marker names applied to them. + expected should be a dict of (item name -> seq of expected marker names) + + .. note:: this could be moved to ``testdir`` if proven to be useful + to other modules. + """ + from _pytest.mark import MarkInfo + items = dict((x.name, x) for x in items) + for name, expected_markers in expected.items(): + markers = items[name].keywords._markers + marker_names = set([name for (name, v) in markers.items() + if isinstance(v, MarkInfo)]) + assert marker_names == set(expected_markers) + + class TestKeywordSelection: def test_select_simple(self, testdir): file_test = testdir.makepyfile("""