From 4914135fdf45bc4fa0469f2a9d3161c5830e84fd Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Wed, 9 May 2018 15:40:52 +0200 Subject: [PATCH] introduce name filtering for marker iteration again --- _pytest/fixtures.py | 2 +- _pytest/mark/evaluate.py | 2 +- _pytest/nodes.py | 22 +++++++++++++++++----- _pytest/python.py | 5 ++--- _pytest/skipping.py | 4 +--- _pytest/warnings.py | 7 +++---- changelog/3446.feature | 1 + changelog/3459.feature | 1 + doc/en/example/markers.rst | 16 +++++++--------- doc/en/usage.rst | 7 +++---- testing/test_mark.py | 33 ++++++++++++++++++++++++++++++--- 11 files changed, 67 insertions(+), 33 deletions(-) create mode 100644 changelog/3446.feature create mode 100644 changelog/3459.feature diff --git a/_pytest/fixtures.py b/_pytest/fixtures.py index fa16fea64..59bf1fd45 100644 --- a/_pytest/fixtures.py +++ b/_pytest/fixtures.py @@ -988,7 +988,7 @@ class FixtureManager(object): argnames = getfuncargnames(func, cls=cls) else: argnames = () - usefixtures = flatten(mark.args for mark in node.iter_markers() if mark.name == "usefixtures") + usefixtures = flatten(mark.args for mark in node.iter_markers(name="usefixtures")) initialnames = argnames initialnames = tuple(usefixtures) + initialnames fm = node.session._fixturemanager diff --git a/_pytest/mark/evaluate.py b/_pytest/mark/evaluate.py index c89b4933a..0afbc56e7 100644 --- a/_pytest/mark/evaluate.py +++ b/_pytest/mark/evaluate.py @@ -35,7 +35,7 @@ class MarkEvaluator(object): return not hasattr(self, 'exc') def _get_marks(self): - return [x for x in self.item.iter_markers() if x.name == self._mark_name] + return list(self.item.iter_markers(name=self._mark_name)) def invalidraise(self, exc): raises = self.get('raises') diff --git a/_pytest/nodes.py b/_pytest/nodes.py index 799ee078a..00f49cd8a 100644 --- a/_pytest/nodes.py +++ b/_pytest/nodes.py @@ -183,20 +183,32 @@ class Node(object): self.keywords[marker.name] = marker self.own_markers.append(marker) - def iter_markers(self): + def iter_markers(self, name=None): """ + :param name: if given, filter the results by the name attribute + iterate over all markers of the node """ - return (x[1] for x in self.iter_markers_with_node()) + return (x[1] for x in self.iter_markers_with_node(name=name)) - def iter_markers_with_node(self): + def iter_markers_with_node(self, name=None): """ + :param name: if given, filter the results by the name attribute + iterate over all markers of the node returns sequence of tuples (node, mark) """ for node in reversed(self.listchain()): for mark in node.own_markers: - yield node, mark + if name is None or getattr(mark, 'name', None) == name: + yield node, mark + + def get_closest_marker(self, name, default=None): + """return the first marker matching the name + :param default: fallback return value of no marker was found + :param name: name to filter by + """ + return next(self.iter_markers(name=name), default) def get_marker(self, name): """ get a marker object from this node or None if @@ -206,7 +218,7 @@ class Node(object): deprecated """ - markers = [x for x in self.iter_markers() if x.name == name] + markers = list(self.iter_markers(name=name)) if markers: return MarkInfo(markers) diff --git a/_pytest/python.py b/_pytest/python.py index 5bb439480..c44cc04f0 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -118,9 +118,8 @@ def pytest_generate_tests(metafunc): if hasattr(metafunc.function, attr): msg = "{0} has '{1}', spelling should be 'parametrize'" raise MarkerError(msg.format(metafunc.function.__name__, attr)) - for marker in metafunc.definition.iter_markers(): - if marker.name == 'parametrize': - metafunc.parametrize(*marker.args, **marker.kwargs) + for marker in metafunc.definition.iter_markers(name='parametrize'): + metafunc.parametrize(*marker.args, **marker.kwargs) def pytest_configure(config): diff --git a/_pytest/skipping.py b/_pytest/skipping.py index f62edcf9a..36eb4a337 100644 --- a/_pytest/skipping.py +++ b/_pytest/skipping.py @@ -64,9 +64,7 @@ def pytest_runtest_setup(item): item._skipped_by_mark = True skip(eval_skipif.getexplanation()) - for skip_info in item.iter_markers(): - if skip_info.name != 'skip': - continue + for skip_info in item.iter_markers(name='skip'): item._skipped_by_mark = True if 'reason' in skip_info.kwargs: skip(skip_info.kwargs['reason']) diff --git a/_pytest/warnings.py b/_pytest/warnings.py index d8b9fc460..2179328dc 100644 --- a/_pytest/warnings.py +++ b/_pytest/warnings.py @@ -60,10 +60,9 @@ def catch_warnings_for_item(item): for arg in inifilters: _setoption(warnings, arg) - for mark in item.iter_markers(): - if mark.name == 'filterwarnings': - for arg in mark.args: - warnings._setoption(arg) + for mark in item.iter_markers(name='filterwarnings'): + for arg in mark.args: + warnings._setoption(arg) yield diff --git a/changelog/3446.feature b/changelog/3446.feature new file mode 100644 index 000000000..4f86e3eda --- /dev/null +++ b/changelog/3446.feature @@ -0,0 +1 @@ +introduce node.get_closest_marker(name, default=None) to support simple marker usage setups. \ No newline at end of file diff --git a/changelog/3459.feature b/changelog/3459.feature new file mode 100644 index 000000000..e81aeb0f4 --- /dev/null +++ b/changelog/3459.feature @@ -0,0 +1 @@ +Introduce optional name based filtering for iter_markers \ No newline at end of file diff --git a/doc/en/example/markers.rst b/doc/en/example/markers.rst index b162c938c..5b049d463 100644 --- a/doc/en/example/markers.rst +++ b/doc/en/example/markers.rst @@ -330,7 +330,7 @@ specifies via named environments:: "env(name): mark test to run only on named environment") def pytest_runtest_setup(item): - envnames = [mark.args[0] for mark in item.iter_markers() if mark.name == "env"] + envnames = [mark.args[0] for mark in item.iter_markers(name='env')] if envnames: if item.config.getoption("-E") not in envnames: pytest.skip("test requires env in %r" % envnames) @@ -402,10 +402,9 @@ Below is the config file that will be used in the next examples:: import sys def pytest_runtest_setup(item): - for marker in item.iter_markers(): - if marker.name == 'my_marker': - print(marker) - sys.stdout.flush() + for marker in item.iter_markers(name='my_marker'): + print(marker) + sys.stdout.flush() A custom marker can have its argument set, i.e. ``args`` and ``kwargs`` properties, defined by either invoking it as a callable or using ``pytest.mark.MARKER_NAME.with_args``. These two methods achieve the same effect most of the time. @@ -458,10 +457,9 @@ test function. From a conftest file we can read it like this:: import sys def pytest_runtest_setup(item): - for mark in item.iter_markers(): - if mark.name == 'glob': - print ("glob args=%s kwargs=%s" %(mark.args, mark.kwargs)) - sys.stdout.flush() + for mark in item.iter_markers(name='glob'): + print ("glob args=%s kwargs=%s" %(mark.args, mark.kwargs)) + sys.stdout.flush() Let's run this without capturing output and see what we get:: diff --git a/doc/en/usage.rst b/doc/en/usage.rst index 72b2eedc9..d95e270ef 100644 --- a/doc/en/usage.rst +++ b/doc/en/usage.rst @@ -274,10 +274,9 @@ Alternatively, you can integrate this functionality with custom markers: def pytest_collection_modifyitems(session, config, items): for item in items: - for marker in item.iter_markers(): - if marker.name == 'test_id': - test_id = marker.args[0] - item.user_properties.append(('test_id', test_id)) + for marker in item.iter_markers(name='test_id'): + test_id = marker.args[0] + item.user_properties.append(('test_id', test_id)) And in your tests: diff --git a/testing/test_mark.py b/testing/test_mark.py index 31d3af3e5..764678ab4 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -553,7 +553,6 @@ class TestFunctional(object): self.assert_markers(items, test_foo=('a', 'b'), test_bar=('a',)) @pytest.mark.issue568 - @pytest.mark.xfail(reason="markers smear on methods of base classes") def test_mark_should_not_pass_to_siebling_class(self, testdir): p = testdir.makepyfile(""" import pytest @@ -573,8 +572,16 @@ class TestFunctional(object): """) items, rec = testdir.inline_genitems(p) base_item, sub_item, sub_item_other = items - assert not hasattr(base_item.obj, 'b') - assert not hasattr(sub_item_other.obj, 'b') + print(items, [x.nodeid for x in items]) + # legacy api smears + assert hasattr(base_item.obj, 'b') + assert hasattr(sub_item_other.obj, 'b') + assert hasattr(sub_item.obj, 'b') + + # new api seregates + assert not list(base_item.iter_markers(name='b')) + assert not list(sub_item_other.iter_markers(name='b')) + assert list(sub_item.iter_markers(name='b')) def test_mark_decorator_baseclasses_merged(self, testdir): p = testdir.makepyfile(""" @@ -598,6 +605,26 @@ class TestFunctional(object): self.assert_markers(items, test_foo=('a', 'b', 'c'), test_bar=('a', 'b', 'd')) + def test_mark_closest(self, testdir): + p = testdir.makepyfile(""" + import pytest + + @pytest.mark.c(location="class") + class Test: + @pytest.mark.c(location="function") + def test_has_own(): + pass + + def test_has_inherited(): + pass + + """) + items, rec = testdir.inline_genitems(p) + has_own, has_inherited = items + assert has_own.get_closest_marker('c').kwargs == {'location': 'function'} + assert has_inherited.get_closest_marker('c').kwargs == {'location': 'class'} + assert has_own.get_closest_marker('missing') is None + def test_mark_with_wrong_marker(self, testdir): reprec = testdir.inline_runsource(""" import pytest