Merge pull request #3459 from RonnyPfannschmidt/mark-iter-name-filter
introduce name filtering for marker iteration again
This commit is contained in:
commit
7d0c9837ce
|
@ -32,8 +32,9 @@ RESULT_LOG = (
|
||||||
)
|
)
|
||||||
|
|
||||||
MARK_INFO_ATTRIBUTE = RemovedInPytest4Warning(
|
MARK_INFO_ATTRIBUTE = RemovedInPytest4Warning(
|
||||||
"MarkInfo objects are deprecated as they contain the merged marks.\n"
|
"MarkInfo objects are deprecated as they contain merged marks which are hard to deal with correctly.\n"
|
||||||
"Please use node.iter_markers to iterate over markers correctly"
|
"Please use node.get_closest_marker(name) or node.iter_markers(name).\n"
|
||||||
|
"Docs: https://docs.pytest.org/en/latest/mark.html#updating-code"
|
||||||
)
|
)
|
||||||
|
|
||||||
MARK_PARAMETERSET_UNPACKING = RemovedInPytest4Warning(
|
MARK_PARAMETERSET_UNPACKING = RemovedInPytest4Warning(
|
||||||
|
|
|
@ -988,7 +988,7 @@ class FixtureManager(object):
|
||||||
argnames = getfuncargnames(func, cls=cls)
|
argnames = getfuncargnames(func, cls=cls)
|
||||||
else:
|
else:
|
||||||
argnames = ()
|
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 = argnames
|
||||||
initialnames = tuple(usefixtures) + initialnames
|
initialnames = tuple(usefixtures) + initialnames
|
||||||
fm = node.session._fixturemanager
|
fm = node.session._fixturemanager
|
||||||
|
|
|
@ -35,7 +35,7 @@ class MarkEvaluator(object):
|
||||||
return not hasattr(self, 'exc')
|
return not hasattr(self, 'exc')
|
||||||
|
|
||||||
def _get_marks(self):
|
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):
|
def invalidraise(self, exc):
|
||||||
raises = self.get('raises')
|
raises = self.get('raises')
|
||||||
|
|
|
@ -183,30 +183,46 @@ class Node(object):
|
||||||
self.keywords[marker.name] = marker
|
self.keywords[marker.name] = marker
|
||||||
self.own_markers.append(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
|
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
|
iterate over all markers of the node
|
||||||
returns sequence of tuples (node, mark)
|
returns sequence of tuples (node, mark)
|
||||||
"""
|
"""
|
||||||
for node in reversed(self.listchain()):
|
for node in reversed(self.listchain()):
|
||||||
for mark in node.own_markers:
|
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, from closest (for example function) to farther level (for example
|
||||||
|
module level).
|
||||||
|
|
||||||
|
: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):
|
def get_marker(self, name):
|
||||||
""" get a marker object from this node or None if
|
""" get a marker object from this node or None if
|
||||||
the node doesn't have a marker with that name.
|
the node doesn't have a marker with that name.
|
||||||
|
|
||||||
..warning::
|
.. deprecated:: 3.6
|
||||||
|
This function has been deprecated in favor of
|
||||||
deprecated
|
:meth:`Node.get_closest_marker <_pytest.nodes.Node.get_closest_marker>` and
|
||||||
|
:meth:`Node.iter_markers <_pytest.nodes.Node.iter_markers>`, see :ref:`update marker code`
|
||||||
|
for more details.
|
||||||
"""
|
"""
|
||||||
markers = [x for x in self.iter_markers() if x.name == name]
|
markers = list(self.iter_markers(name=name))
|
||||||
if markers:
|
if markers:
|
||||||
return MarkInfo(markers)
|
return MarkInfo(markers)
|
||||||
|
|
||||||
|
|
|
@ -118,9 +118,8 @@ def pytest_generate_tests(metafunc):
|
||||||
if hasattr(metafunc.function, attr):
|
if hasattr(metafunc.function, attr):
|
||||||
msg = "{0} has '{1}', spelling should be 'parametrize'"
|
msg = "{0} has '{1}', spelling should be 'parametrize'"
|
||||||
raise MarkerError(msg.format(metafunc.function.__name__, attr))
|
raise MarkerError(msg.format(metafunc.function.__name__, attr))
|
||||||
for marker in metafunc.definition.iter_markers():
|
for marker in metafunc.definition.iter_markers(name='parametrize'):
|
||||||
if marker.name == 'parametrize':
|
metafunc.parametrize(*marker.args, **marker.kwargs)
|
||||||
metafunc.parametrize(*marker.args, **marker.kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
def pytest_configure(config):
|
def pytest_configure(config):
|
||||||
|
|
|
@ -64,9 +64,7 @@ def pytest_runtest_setup(item):
|
||||||
item._skipped_by_mark = True
|
item._skipped_by_mark = True
|
||||||
skip(eval_skipif.getexplanation())
|
skip(eval_skipif.getexplanation())
|
||||||
|
|
||||||
for skip_info in item.iter_markers():
|
for skip_info in item.iter_markers(name='skip'):
|
||||||
if skip_info.name != 'skip':
|
|
||||||
continue
|
|
||||||
item._skipped_by_mark = True
|
item._skipped_by_mark = True
|
||||||
if 'reason' in skip_info.kwargs:
|
if 'reason' in skip_info.kwargs:
|
||||||
skip(skip_info.kwargs['reason'])
|
skip(skip_info.kwargs['reason'])
|
||||||
|
|
|
@ -60,10 +60,9 @@ def catch_warnings_for_item(item):
|
||||||
for arg in inifilters:
|
for arg in inifilters:
|
||||||
_setoption(warnings, arg)
|
_setoption(warnings, arg)
|
||||||
|
|
||||||
for mark in item.iter_markers():
|
for mark in item.iter_markers(name='filterwarnings'):
|
||||||
if mark.name == 'filterwarnings':
|
for arg in mark.args:
|
||||||
for arg in mark.args:
|
warnings._setoption(arg)
|
||||||
warnings._setoption(arg)
|
|
||||||
|
|
||||||
yield
|
yield
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
Revamp the internals of the ``pytest.mark`` implementation with correct per node handling and introduce a new ``Node.iter_markers``
|
Revamp the internals of the ``pytest.mark`` implementation with correct per node handling which fixes a number of
|
||||||
API for mark iteration over nodes which fixes a number of long standing bugs caused by the old approach. More details can be
|
long standing bugs caused by the old design. This introduces new ``Node.iter_markers(name)`` and ``Node.get_closest_mark(name)`` APIs.
|
||||||
found in `the marks documentation <https://docs.pytest.org/en/latest/mark.html#marker-revamp-and-iteration>`_.
|
Users are **strongly encouraged** to read the `reasons for the revamp in the docs <https://docs.pytest.org/en/latest/mark.html#marker-revamp-and-iteration>`_,
|
||||||
|
or jump over to details about `updating existing code to use the new APIs <https://docs.pytest.org/en/latest/mark.html#updating-code>`_.
|
||||||
|
|
|
@ -330,7 +330,7 @@ specifies via named environments::
|
||||||
"env(name): mark test to run only on named environment")
|
"env(name): mark test to run only on named environment")
|
||||||
|
|
||||||
def pytest_runtest_setup(item):
|
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 envnames:
|
||||||
if item.config.getoption("-E") not in envnames:
|
if item.config.getoption("-E") not in envnames:
|
||||||
pytest.skip("test requires env in %r" % 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
|
import sys
|
||||||
|
|
||||||
def pytest_runtest_setup(item):
|
def pytest_runtest_setup(item):
|
||||||
for marker in item.iter_markers():
|
for marker in item.iter_markers(name='my_marker'):
|
||||||
if marker.name == 'my_marker':
|
print(marker)
|
||||||
print(marker)
|
sys.stdout.flush()
|
||||||
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.
|
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
|
import sys
|
||||||
|
|
||||||
def pytest_runtest_setup(item):
|
def pytest_runtest_setup(item):
|
||||||
for mark in item.iter_markers():
|
for mark in item.iter_markers(name='glob'):
|
||||||
if mark.name == 'glob':
|
print ("glob args=%s kwargs=%s" %(mark.args, mark.kwargs))
|
||||||
print ("glob args=%s kwargs=%s" %(mark.args, mark.kwargs))
|
sys.stdout.flush()
|
||||||
sys.stdout.flush()
|
|
||||||
|
|
||||||
Let's run this without capturing output and see what we get::
|
Let's run this without capturing output and see what we get::
|
||||||
|
|
||||||
|
|
|
@ -28,8 +28,8 @@ which also serve as documentation.
|
||||||
|
|
||||||
.. currentmodule:: _pytest.mark.structures
|
.. currentmodule:: _pytest.mark.structures
|
||||||
.. autoclass:: Mark
|
.. autoclass:: Mark
|
||||||
:members:
|
:members:
|
||||||
:noindex:
|
:noindex:
|
||||||
|
|
||||||
|
|
||||||
.. `marker-iteration`
|
.. `marker-iteration`
|
||||||
|
@ -51,8 +51,60 @@ in fact, markers where only accessible in functions, even if they where declared
|
||||||
|
|
||||||
A new API to access markers has been introduced in pytest 3.6 in order to solve the problems with the initial design, providing :func:`_pytest.nodes.Node.iter_markers` method to iterate over markers in a consistent manner and reworking the internals, which solved great deal of problems with the initial design.
|
A new API to access markers has been introduced in pytest 3.6 in order to solve the problems with the initial design, providing :func:`_pytest.nodes.Node.iter_markers` method to iterate over markers in a consistent manner and reworking the internals, which solved great deal of problems with the initial design.
|
||||||
|
|
||||||
Here is a non-exhaustive list of issues fixed by the new implementation:
|
|
||||||
|
|
||||||
|
.. _update marker code:
|
||||||
|
|
||||||
|
Updating code
|
||||||
|
~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
The old ``Node.get_marker(name)`` function is considered deprecated because it returns an internal ``MarkerInfo`` object
|
||||||
|
which contains the merged name, ``*args`` and ``**kwargs**`` of all the markers which apply to that node.
|
||||||
|
|
||||||
|
In general there are two scenarios on how markers should be handled:
|
||||||
|
|
||||||
|
1. Marks overwrite each other. Order matters but you only want to think of your mark as a single item. E.g.
|
||||||
|
``log_level('info')`` at a module level can be overwritten by ``log_level('debug')`` for a specific test.
|
||||||
|
|
||||||
|
In this case replace use ``Node.get_closest_marker(name)``:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
# replace this:
|
||||||
|
marker = item.get_marker('log_level')
|
||||||
|
if marker:
|
||||||
|
level = marker.args[0]
|
||||||
|
|
||||||
|
# by this:
|
||||||
|
marker = item.get_closest_marker('log_level')
|
||||||
|
if marker:
|
||||||
|
level = marker.args[0]
|
||||||
|
|
||||||
|
2. Marks compose additive. E.g. ``skipif(condition)`` marks means you just want to evaluate all of them,
|
||||||
|
order doesn't even matter. You probably want to think of your marks as a set here.
|
||||||
|
|
||||||
|
In this case iterate over each mark and handle their ``*args`` and ``**kwargs`` individually.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
# replace this
|
||||||
|
skipif = item.get_marker('skipif')
|
||||||
|
if skipif:
|
||||||
|
for condition in skipif.args:
|
||||||
|
# eval condition
|
||||||
|
|
||||||
|
# by this:
|
||||||
|
for skipif in item.iter_markers('skipif'):
|
||||||
|
condition = skipif.args[0]
|
||||||
|
# eval condition
|
||||||
|
|
||||||
|
|
||||||
|
If you are unsure or have any questions, please consider opening
|
||||||
|
`an issue <https://github.com/pytest-dev/pytest/issues>`_.
|
||||||
|
|
||||||
|
Related issues
|
||||||
|
~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Here is a non-exhaustive list of issues fixed by the new implementation:
|
||||||
|
|
||||||
* Marks don't pick up nested classes (`#199 <https://github.com/pytest-dev/pytest/issues/199>`_).
|
* Marks don't pick up nested classes (`#199 <https://github.com/pytest-dev/pytest/issues/199>`_).
|
||||||
|
|
||||||
|
|
|
@ -274,10 +274,9 @@ Alternatively, you can integrate this functionality with custom markers:
|
||||||
|
|
||||||
def pytest_collection_modifyitems(session, config, items):
|
def pytest_collection_modifyitems(session, config, items):
|
||||||
for item in items:
|
for item in items:
|
||||||
for marker in item.iter_markers():
|
for marker in item.iter_markers(name='test_id'):
|
||||||
if marker.name == 'test_id':
|
test_id = marker.args[0]
|
||||||
test_id = marker.args[0]
|
item.user_properties.append(('test_id', test_id))
|
||||||
item.user_properties.append(('test_id', test_id))
|
|
||||||
|
|
||||||
And in your tests:
|
And in your tests:
|
||||||
|
|
||||||
|
|
|
@ -553,7 +553,6 @@ class TestFunctional(object):
|
||||||
self.assert_markers(items, test_foo=('a', 'b'), test_bar=('a',))
|
self.assert_markers(items, test_foo=('a', 'b'), test_bar=('a',))
|
||||||
|
|
||||||
@pytest.mark.issue568
|
@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):
|
def test_mark_should_not_pass_to_siebling_class(self, testdir):
|
||||||
p = testdir.makepyfile("""
|
p = testdir.makepyfile("""
|
||||||
import pytest
|
import pytest
|
||||||
|
@ -573,8 +572,16 @@ class TestFunctional(object):
|
||||||
""")
|
""")
|
||||||
items, rec = testdir.inline_genitems(p)
|
items, rec = testdir.inline_genitems(p)
|
||||||
base_item, sub_item, sub_item_other = items
|
base_item, sub_item, sub_item_other = items
|
||||||
assert not hasattr(base_item.obj, 'b')
|
print(items, [x.nodeid for x in items])
|
||||||
assert not hasattr(sub_item_other.obj, 'b')
|
# 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):
|
def test_mark_decorator_baseclasses_merged(self, testdir):
|
||||||
p = testdir.makepyfile("""
|
p = testdir.makepyfile("""
|
||||||
|
@ -598,6 +605,26 @@ class TestFunctional(object):
|
||||||
self.assert_markers(items, test_foo=('a', 'b', 'c'),
|
self.assert_markers(items, test_foo=('a', 'b', 'c'),
|
||||||
test_bar=('a', 'b', 'd'))
|
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):
|
def test_mark_with_wrong_marker(self, testdir):
|
||||||
reprec = testdir.inline_runsource("""
|
reprec = testdir.inline_runsource("""
|
||||||
import pytest
|
import pytest
|
||||||
|
|
Loading…
Reference in New Issue