Merge pull request #3317 from RonnyPfannschmidt/marker-pristine-node-storage

introduce a distinct searchable non-broken storage for markers
This commit is contained in:
Bruno Oliveira 2018-04-09 19:40:12 -03:00 committed by GitHub
commit 715337011b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 260 additions and 110 deletions

View File

@ -32,7 +32,8 @@ RESULT_LOG = (
) )
MARK_INFO_ATTRIBUTE = RemovedInPytest4Warning( MARK_INFO_ATTRIBUTE = RemovedInPytest4Warning(
"MarkInfo objects are deprecated as they contain the merged marks" "MarkInfo objects are deprecated as they contain the merged marks.\n"
"Please use node.iter_markers to iterate over markers correctly"
) )
MARK_PARAMETERSET_UNPACKING = RemovedInPytest4Warning( MARK_PARAMETERSET_UNPACKING = RemovedInPytest4Warning(

View File

@ -5,6 +5,7 @@ import inspect
import sys import sys
import warnings import warnings
from collections import OrderedDict, deque, defaultdict from collections import OrderedDict, deque, defaultdict
from more_itertools import flatten
import attr import attr
import py import py
@ -371,10 +372,7 @@ class FixtureRequest(FuncargnamesCompatAttr):
:arg marker: a :py:class:`_pytest.mark.MarkDecorator` object :arg marker: a :py:class:`_pytest.mark.MarkDecorator` object
created by a call to ``pytest.mark.NAME(...)``. created by a call to ``pytest.mark.NAME(...)``.
""" """
try: self.node.add_marker(marker)
self.node.keywords[marker.markname] = marker
except AttributeError:
raise ValueError(marker)
def raiseerror(self, msg): def raiseerror(self, msg):
""" raise a FixtureLookupError with the given message. """ """ raise a FixtureLookupError with the given message. """
@ -985,10 +983,9 @@ class FixtureManager(object):
argnames = getfuncargnames(func, cls=cls) argnames = getfuncargnames(func, cls=cls)
else: else:
argnames = () argnames = ()
usefixtures = getattr(func, "usefixtures", None) usefixtures = flatten(mark.args for mark in node.iter_markers() if mark.name == "usefixtures")
initialnames = argnames initialnames = argnames
if usefixtures is not None: initialnames = tuple(usefixtures) + initialnames
initialnames = usefixtures.args + initialnames
fm = node.session._fixturemanager fm = node.session._fixturemanager
names_closure, arg2fixturedefs = fm.getfixtureclosure(initialnames, names_closure, arg2fixturedefs = fm.getfixtureclosure(initialnames,
node) node)
@ -1070,6 +1067,8 @@ class FixtureManager(object):
fixturedef = faclist[-1] fixturedef = faclist[-1]
if fixturedef.params is not None: if fixturedef.params is not None:
parametrize_func = getattr(metafunc.function, 'parametrize', None) parametrize_func = getattr(metafunc.function, 'parametrize', None)
if parametrize_func is not None:
parametrize_func = parametrize_func.combined
func_params = getattr(parametrize_func, 'args', [[None]]) func_params = getattr(parametrize_func, 'args', [[None]])
func_kwargs = getattr(parametrize_func, 'kwargs', {}) func_kwargs = getattr(parametrize_func, 'kwargs', {})
# skip directly parametrized arguments # skip directly parametrized arguments

View File

@ -4,7 +4,6 @@ import sys
import platform import platform
import traceback import traceback
from . import MarkDecorator, MarkInfo
from ..outcomes import fail, TEST_OUTCOME from ..outcomes import fail, TEST_OUTCOME
@ -28,22 +27,15 @@ class MarkEvaluator(object):
self._mark_name = name self._mark_name = name
def __bool__(self): def __bool__(self):
self._marks = self._get_marks() # dont cache here to prevent staleness
return bool(self._marks) return bool(self._get_marks())
__nonzero__ = __bool__ __nonzero__ = __bool__
def wasvalid(self): def wasvalid(self):
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]
keyword = self.item.keywords.get(self._mark_name)
if isinstance(keyword, MarkDecorator):
return [keyword.mark]
elif isinstance(keyword, MarkInfo):
return [x.combined for x in keyword]
else:
return []
def invalidraise(self, exc): def invalidraise(self, exc):
raises = self.get('raises') raises = self.get('raises')

View File

@ -4,9 +4,10 @@ from operator import attrgetter
import inspect import inspect
import attr import attr
from ..deprecated import MARK_PARAMETERSET_UNPACKING
from ..deprecated import MARK_PARAMETERSET_UNPACKING, MARK_INFO_ATTRIBUTE
from ..compat import NOTSET, getfslineno from ..compat import NOTSET, getfslineno
from six.moves import map from six.moves import map, reduce
EMPTY_PARAMETERSET_OPTION = "empty_parameter_set_mark" EMPTY_PARAMETERSET_OPTION = "empty_parameter_set_mark"
@ -113,11 +114,21 @@ class ParameterSet(namedtuple('ParameterSet', 'values, marks, id')):
@attr.s(frozen=True) @attr.s(frozen=True)
class Mark(object): class Mark(object):
name = attr.ib() #: name of the mark
args = attr.ib() name = attr.ib(type=str)
kwargs = attr.ib() #: positional arguments of the mark decorator
args = attr.ib(type="List[object]")
#: keyword arguments of the mark decorator
kwargs = attr.ib(type="Dict[str, object]")
def combined_with(self, other): def combined_with(self, other):
"""
:param other: the mark to combine with
:type other: Mark
:rtype: Mark
combines by appending aargs and merging the mappings
"""
assert self.name == other.name assert self.name == other.name
return Mark( return Mark(
self.name, self.args + other.args, self.name, self.args + other.args,
@ -233,7 +244,7 @@ def store_legacy_markinfo(func, mark):
raise TypeError("got {mark!r} instead of a Mark".format(mark=mark)) raise TypeError("got {mark!r} instead of a Mark".format(mark=mark))
holder = getattr(func, mark.name, None) holder = getattr(func, mark.name, None)
if holder is None: if holder is None:
holder = MarkInfo(mark) holder = MarkInfo.for_mark(mark)
setattr(func, mark.name, holder) setattr(func, mark.name, holder)
else: else:
holder.add_mark(mark) holder.add_mark(mark)
@ -260,23 +271,29 @@ def _marked(func, mark):
invoked more than once. invoked more than once.
""" """
try: try:
func_mark = getattr(func, mark.name) func_mark = getattr(func, getattr(mark, 'combined', mark).name)
except AttributeError: except AttributeError:
return False return False
return mark.args == func_mark.args and mark.kwargs == func_mark.kwargs return any(mark == info.combined for info in func_mark)
@attr.s
class MarkInfo(object): class MarkInfo(object):
""" Marking object created by :class:`MarkDecorator` instances. """ """ Marking object created by :class:`MarkDecorator` instances. """
def __init__(self, mark): _marks = attr.ib()
assert isinstance(mark, Mark), repr(mark) combined = attr.ib(
self.combined = mark repr=False,
self._marks = [mark] default=attr.Factory(lambda self: reduce(Mark.combined_with, self._marks),
takes_self=True))
name = alias('combined.name') name = alias('combined.name', warning=MARK_INFO_ATTRIBUTE)
args = alias('combined.args') args = alias('combined.args', warning=MARK_INFO_ATTRIBUTE)
kwargs = alias('combined.kwargs') kwargs = alias('combined.kwargs', warning=MARK_INFO_ATTRIBUTE)
@classmethod
def for_mark(cls, mark):
return cls([mark])
def __repr__(self): def __repr__(self):
return "<MarkInfo {0!r}>".format(self.combined) return "<MarkInfo {0!r}>".format(self.combined)
@ -288,7 +305,7 @@ class MarkInfo(object):
def __iter__(self): def __iter__(self):
""" yield MarkInfo objects each relating to a marking-call. """ """ yield MarkInfo objects each relating to a marking-call. """
return map(MarkInfo, self._marks) return map(MarkInfo.for_mark, self._marks)
class MarkGenerator(object): class MarkGenerator(object):
@ -365,3 +382,33 @@ class NodeKeywords(MappingMixin):
def __repr__(self): def __repr__(self):
return "<NodeKeywords for node %s>" % (self.node, ) return "<NodeKeywords for node %s>" % (self.node, )
@attr.s(cmp=False, hash=False)
class NodeMarkers(object):
"""
internal strucutre for storing marks belongong to a node
..warning::
unstable api
"""
own_markers = attr.ib(default=attr.Factory(list))
def update(self, add_markers):
"""update the own markers
"""
self.own_markers.extend(add_markers)
def find(self, name):
"""
find markers in own nodes or parent nodes
needs a better place
"""
for mark in self.own_markers:
if mark.name == name:
yield mark
def __iter__(self):
return iter(self.own_markers)

View File

@ -8,7 +8,7 @@ import attr
import _pytest import _pytest
import _pytest._code import _pytest._code
from _pytest.mark.structures import NodeKeywords from _pytest.mark.structures import NodeKeywords, MarkInfo
SEP = "/" SEP = "/"
@ -90,6 +90,9 @@ class Node(object):
#: keywords/markers collected from all scopes #: keywords/markers collected from all scopes
self.keywords = NodeKeywords(self) self.keywords = NodeKeywords(self)
#: the marker objects belonging to this node
self.own_markers = []
#: allow adding of extra keywords to use for matching #: allow adding of extra keywords to use for matching
self.extra_keyword_matches = set() self.extra_keyword_matches = set()
@ -178,15 +181,34 @@ class Node(object):
elif not isinstance(marker, MarkDecorator): elif not isinstance(marker, MarkDecorator):
raise ValueError("is not a string or pytest.mark.* Marker") raise ValueError("is not a string or pytest.mark.* Marker")
self.keywords[marker.name] = marker self.keywords[marker.name] = marker
self.own_markers.append(marker)
def iter_markers(self):
"""
iterate over all markers of the node
"""
return (x[1] for x in self.iter_markers_with_node())
def iter_markers_with_node(self):
"""
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
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.
val = self.keywords.get(name, None)
if val is not None: ..warning::
from _pytest.mark import MarkInfo, MarkDecorator
if isinstance(val, (MarkDecorator, MarkInfo)): deprecated
return val """
markers = [x for x in self.iter_markers() if x.name == name]
if markers:
return MarkInfo(markers)
def listextrakeywords(self): def listextrakeywords(self):
""" Return a set of all extra keywords in self and any parents.""" """ Return a set of all extra keywords in self and any parents."""

View File

@ -28,7 +28,7 @@ from _pytest.compat import (
safe_str, getlocation, enum, safe_str, getlocation, enum,
) )
from _pytest.outcomes import fail from _pytest.outcomes import fail
from _pytest.mark.structures import transfer_markers from _pytest.mark.structures import transfer_markers, get_unpacked_marks
# relative paths that we use to filter traceback entries from appearing to the user; # relative paths that we use to filter traceback entries from appearing to the user;
@ -117,12 +117,9 @@ 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))
try: for marker in metafunc.definition.iter_markers():
markers = metafunc.function.parametrize if marker.name == 'parametrize':
except AttributeError: metafunc.parametrize(*marker.args, **marker.kwargs)
return
for marker in markers:
metafunc.parametrize(*marker.args, **marker.kwargs)
def pytest_configure(config): def pytest_configure(config):
@ -212,11 +209,20 @@ class PyobjContext(object):
class PyobjMixin(PyobjContext): class PyobjMixin(PyobjContext):
_ALLOW_MARKERS = True
def __init__(self, *k, **kw):
super(PyobjMixin, self).__init__(*k, **kw)
def obj(): def obj():
def fget(self): def fget(self):
obj = getattr(self, '_obj', None) obj = getattr(self, '_obj', None)
if obj is None: if obj is None:
self._obj = obj = self._getobj() self._obj = obj = self._getobj()
# XXX evil hack
# used to avoid Instance collector marker duplication
if self._ALLOW_MARKERS:
self.own_markers.extend(get_unpacked_marks(self.obj))
return obj return obj
def fset(self, value): def fset(self, value):
@ -363,9 +369,15 @@ class PyCollector(PyobjMixin, nodes.Collector):
cls = clscol and clscol.obj or None cls = clscol and clscol.obj or None
transfer_markers(funcobj, cls, module) transfer_markers(funcobj, cls, module)
fm = self.session._fixturemanager fm = self.session._fixturemanager
fixtureinfo = fm.getfixtureinfo(self, funcobj, cls)
metafunc = Metafunc(funcobj, fixtureinfo, self.config, definition = FunctionDefinition(
cls=cls, module=module) name=name,
parent=self,
callobj=funcobj,
)
fixtureinfo = fm.getfixtureinfo(definition, funcobj, cls)
metafunc = Metafunc(definition, fixtureinfo, self.config, cls=cls, module=module)
methods = [] methods = []
if hasattr(module, "pytest_generate_tests"): if hasattr(module, "pytest_generate_tests"):
methods.append(module.pytest_generate_tests) methods.append(module.pytest_generate_tests)
@ -524,6 +536,11 @@ class Class(PyCollector):
class Instance(PyCollector): class Instance(PyCollector):
_ALLOW_MARKERS = False # hack, destroy later
# instances share the object with their parents in a way
# that duplicates markers instances if not taken out
# can be removed at node strucutre reorganization time
def _getobj(self): def _getobj(self):
return self.parent.obj() return self.parent.obj()
@ -723,15 +740,17 @@ class Metafunc(fixtures.FuncargnamesCompatAttr):
test function is defined. test function is defined.
""" """
def __init__(self, function, fixtureinfo, config, cls=None, module=None): def __init__(self, definition, fixtureinfo, config, cls=None, module=None):
#: access to the :class:`_pytest.config.Config` object for the test session #: access to the :class:`_pytest.config.Config` object for the test session
assert isinstance(definition, FunctionDefinition) or type(definition).__name__ == "DefinitionMock"
self.definition = definition
self.config = config self.config = config
#: the module object where the test function is defined in. #: the module object where the test function is defined in.
self.module = module self.module = module
#: underlying python test function #: underlying python test function
self.function = function self.function = definition.obj
#: set of fixture names required by the test function #: set of fixture names required by the test function
self.fixturenames = fixtureinfo.names_closure self.fixturenames = fixtureinfo.names_closure
@ -1103,6 +1122,8 @@ class Function(FunctionMixin, nodes.Item, fixtures.FuncargnamesCompatAttr):
Python test function. Python test function.
""" """
_genid = None _genid = None
# disable since functions handle it themselfes
_ALLOW_MARKERS = False
def __init__(self, name, parent, args=None, config=None, def __init__(self, name, parent, args=None, config=None,
callspec=None, callobj=NOTSET, keywords=None, session=None, callspec=None, callobj=NOTSET, keywords=None, session=None,
@ -1114,6 +1135,7 @@ class Function(FunctionMixin, nodes.Item, fixtures.FuncargnamesCompatAttr):
self.obj = callobj self.obj = callobj
self.keywords.update(self.obj.__dict__) self.keywords.update(self.obj.__dict__)
self.own_markers.extend(get_unpacked_marks(self.obj))
if callspec: if callspec:
self.callspec = callspec self.callspec = callspec
# this is total hostile and a mess # this is total hostile and a mess
@ -1123,6 +1145,7 @@ class Function(FunctionMixin, nodes.Item, fixtures.FuncargnamesCompatAttr):
# feel free to cry, this was broken for years before # feel free to cry, this was broken for years before
# and keywords cant fix it per design # and keywords cant fix it per design
self.keywords[mark.name] = mark self.keywords[mark.name] = mark
self.own_markers.extend(callspec.marks)
if keywords: if keywords:
self.keywords.update(keywords) self.keywords.update(keywords)
@ -1181,3 +1204,15 @@ class Function(FunctionMixin, nodes.Item, fixtures.FuncargnamesCompatAttr):
def setup(self): def setup(self):
super(Function, self).setup() super(Function, self).setup()
fixtures.fillfixtures(self) fixtures.fillfixtures(self)
class FunctionDefinition(Function):
"""
internal hack until we get actual definition nodes instead of the
crappy metafunc hack
"""
def runtest(self):
raise RuntimeError("function definitions are not supposed to be used")
setup = runtest

View File

@ -2,7 +2,6 @@
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
from _pytest.config import hookimpl from _pytest.config import hookimpl
from _pytest.mark import MarkInfo, MarkDecorator
from _pytest.mark.evaluate import MarkEvaluator from _pytest.mark.evaluate import MarkEvaluator
from _pytest.outcomes import fail, skip, xfail from _pytest.outcomes import fail, skip, xfail
@ -60,15 +59,14 @@ def pytest_configure(config):
def pytest_runtest_setup(item): def pytest_runtest_setup(item):
# Check if skip or skipif are specified as pytest marks # Check if skip or skipif are specified as pytest marks
item._skipped_by_mark = False item._skipped_by_mark = False
skipif_info = item.keywords.get('skipif') eval_skipif = MarkEvaluator(item, 'skipif')
if isinstance(skipif_info, (MarkInfo, MarkDecorator)): if eval_skipif.istrue():
eval_skipif = MarkEvaluator(item, 'skipif') item._skipped_by_mark = True
if eval_skipif.istrue(): skip(eval_skipif.getexplanation())
item._skipped_by_mark = True
skip(eval_skipif.getexplanation())
skip_info = item.keywords.get('skip') for skip_info in item.iter_markers():
if isinstance(skip_info, (MarkInfo, MarkDecorator)): 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'])

View File

@ -60,10 +60,10 @@ def catch_warnings_for_item(item):
for arg in inifilters: for arg in inifilters:
_setoption(warnings, arg) _setoption(warnings, arg)
mark = item.get_marker('filterwarnings') for mark in item.iter_markers():
if mark: if mark.name == 'filterwarnings':
for arg in mark.args: for arg in mark.args:
warnings._setoption(arg) warnings._setoption(arg)
yield yield

1
changelog/3317.feature Normal file
View File

@ -0,0 +1 @@
introduce correct per node mark handling and deprecate the always incorrect existing mark handling

View File

@ -330,11 +330,10 @@ 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):
envmarker = item.get_marker("env") envnames = [mark.args[0] for mark in item.iter_markers() if mark.name == "env"]
if envmarker is not None: if envnames:
envname = envmarker.args[0] if item.config.getoption("-E") not in envnames:
if envname != item.config.getoption("-E"): pytest.skip("test requires env in %r" % envnames)
pytest.skip("test requires env %r" % envname)
A test file using this local plugin:: A test file using this local plugin::
@ -403,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):
marker = item.get_marker('my_marker') for marker in item.iter_markers():
if marker is not None: if marker.name == 'my_marker':
for info in marker: print(marker)
print('Marker info name={} args={} kwars={}'.format(info.name, info.args, info.kwargs))
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.
@ -426,7 +424,7 @@ However, if there is a callable as the single positional argument with no keywor
The output is as follows:: The output is as follows::
$ pytest -q -s $ pytest -q -s
Marker info name=my_marker args=(<function hello_world at 0xdeadbeef>,) kwars={} Mark(name='my_marker', args=(<function hello_world at 0xdeadbeef>,), kwargs={})
. .
1 passed in 0.12 seconds 1 passed in 0.12 seconds
@ -460,10 +458,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):
g = item.get_marker("glob") for mark in item.iter_markers():
if g is not None: if mark.name == 'glob':
for info in g: print ("glob args=%s kwargs=%s" %(mark.args, mark.kwargs))
print ("glob args=%s kwargs=%s" %(info.args, info.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::
@ -494,11 +491,10 @@ for your particular platform, you could use the following plugin::
ALL = set("darwin linux win32".split()) ALL = set("darwin linux win32".split())
def pytest_runtest_setup(item): def pytest_runtest_setup(item):
if isinstance(item, item.Function): supported_platforms = ALL.intersection(mark.name for mark in item.iter_markers())
plat = sys.platform plat = sys.platform
if not item.get_marker(plat): if supported_platforms and plat not in supported_platforms:
if ALL.intersection(item.keywords): pytest.skip("cannot run on platform %s" % (plat))
pytest.skip("cannot run on platform %s" %(plat))
then tests will be skipped if they were specified for a different platform. then tests will be skipped if they were specified for a different platform.
Let's do a little test file to show how this looks like:: Let's do a little test file to show how this looks like::
@ -532,7 +528,7 @@ then you will see two tests skipped and two executed tests as expected::
test_plat.py s.s. [100%] test_plat.py s.s. [100%]
========================= short test summary info ========================== ========================= short test summary info ==========================
SKIP [2] $REGENDOC_TMPDIR/conftest.py:13: cannot run on platform linux SKIP [2] $REGENDOC_TMPDIR/conftest.py:12: cannot run on platform linux
=================== 2 passed, 2 skipped in 0.12 seconds ==================== =================== 2 passed, 2 skipped in 0.12 seconds ====================

View File

@ -389,7 +389,7 @@ Now we can profile which test functions execute the slowest::
========================= slowest 3 test durations ========================= ========================= slowest 3 test durations =========================
0.30s call test_some_are_slow.py::test_funcslow2 0.30s call test_some_are_slow.py::test_funcslow2
0.20s call test_some_are_slow.py::test_funcslow1 0.20s call test_some_are_slow.py::test_funcslow1
0.16s call test_some_are_slow.py::test_funcfast 0.10s call test_some_are_slow.py::test_funcfast
========================= 3 passed in 0.12 seconds ========================= ========================= 3 passed in 0.12 seconds =========================
incremental testing - test steps incremental testing - test steps

View File

@ -26,3 +26,33 @@ which also serve as documentation.
:ref:`fixtures <fixtures>`. :ref:`fixtures <fixtures>`.
.. currentmodule:: _pytest.mark.structures
.. autoclass:: Mark
:members:
:noindex:
.. `marker-iteration`
Marker iteration
=================
.. versionadded:: 3.6
pytest's marker implementation traditionally worked by simply updating the ``__dict__`` attribute of functions to add markers, in a cumulative manner. As a result of the this, markers would unintendely be passed along class hierarchies in surprising ways plus the API for retriving them was inconsistent, as markers from parameterization would be stored differently than markers applied using the ``@pytest.mark`` decorator and markers added via ``node.add_marker``.
This state of things made it technically next to impossible to use data from markers correctly without having a deep understanding of the internals, leading to subtle and hard to understand bugs in more advanced usages.
Depending on how a marker got declared/changed one would get either a ``MarkerInfo`` which might contain markers from sibling classes,
``MarkDecorators`` when marks came from parameterization or from a ``node.add_marker`` call, discarding prior marks. Also ``MarkerInfo`` acts like a single mark, when it in fact repressents a merged view on multiple marks with the same name.
On top of that markers where not accessible the same way for modules, classes, and functions/methods,
in fact, markers where only accessible in functions, even if they where declared on classes/modules.
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.
.. note::
in a future major relase of pytest we will introduce class based markers,
at which points markers will no longer be limited to instances of :py:class:`Mark`

View File

@ -94,6 +94,8 @@ Marks can be used apply meta data to *test functions* (but not fixtures), which
fixtures or plugins. fixtures or plugins.
.. _`pytest.mark.filterwarnings ref`: .. _`pytest.mark.filterwarnings ref`:
pytest.mark.filterwarnings pytest.mark.filterwarnings
@ -200,9 +202,9 @@ For example:
def test_function(): def test_function():
... ...
Will create and attach a :class:`MarkInfo <_pytest.mark.MarkInfo>` object to the collected Will create and attach a :class:`Mark <_pytest.mark.structures.Mark>` object to the collected
:class:`Item <_pytest.nodes.Item>`, which can then be accessed by fixtures or hooks with :class:`Item <_pytest.nodes.Item>`, which can then be accessed by fixtures or hooks with
:meth:`Node.get_marker <_pytest.nodes.Node.get_marker>`. The ``mark`` object will have the following attributes: :meth:`Node.iter_markers <_pytest.nodes.Node.iter_markers>`. The ``mark`` object will have the following attributes:
.. code-block:: python .. code-block:: python
@ -685,18 +687,28 @@ MarkDecorator
.. autoclass:: _pytest.mark.MarkDecorator .. autoclass:: _pytest.mark.MarkDecorator
:members: :members:
MarkGenerator MarkGenerator
~~~~~~~~~~~~~ ~~~~~~~~~~~~~
.. autoclass:: _pytest.mark.MarkGenerator .. autoclass:: _pytest.mark.MarkGenerator
:members: :members:
MarkInfo MarkInfo
~~~~~~~~ ~~~~~~~~
.. autoclass:: _pytest.mark.MarkInfo .. autoclass:: _pytest.mark.MarkInfo
:members: :members:
Mark
~~~~
.. autoclass:: _pytest.mark.structures.Mark
:members:
Metafunc Metafunc
~~~~~~~~ ~~~~~~~~

View File

@ -260,10 +260,10 @@ 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:
marker = item.get_marker('test_id') for marker in item.iter_markers():
if marker is not None: 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:

View File

@ -1781,6 +1781,8 @@ class TestAutouseManagement(object):
import pytest import pytest
values = [] values = []
def pytest_generate_tests(metafunc): def pytest_generate_tests(metafunc):
if metafunc.cls is None:
assert metafunc.function is test_finish
if metafunc.cls is not None: if metafunc.cls is not None:
metafunc.parametrize("item", [1,2], scope="class") metafunc.parametrize("item", [1,2], scope="class")
class TestClass(object): class TestClass(object):
@ -1798,7 +1800,7 @@ class TestAutouseManagement(object):
assert values == ["setup-1", "step1-1", "step2-1", "teardown-1", assert values == ["setup-1", "step1-1", "step2-1", "teardown-1",
"setup-2", "step1-2", "step2-2", "teardown-2",] "setup-2", "step1-2", "step2-2", "teardown-2",]
""") """)
reprec = testdir.inline_run() reprec = testdir.inline_run('-s')
reprec.assertoutcome(passed=5) reprec.assertoutcome(passed=5)
def test_ordering_autouse_before_explicit(self, testdir): def test_ordering_autouse_before_explicit(self, testdir):

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import re import re
import sys import sys
import attr
import _pytest._code import _pytest._code
import py import py
import pytest import pytest
@ -24,13 +24,19 @@ class TestMetafunc(object):
def __init__(self, names): def __init__(self, names):
self.names_closure = names self.names_closure = names
@attr.s
class DefinitionMock(object):
obj = attr.ib()
names = fixtures.getfuncargnames(func) names = fixtures.getfuncargnames(func)
fixtureinfo = FixtureInfo(names) fixtureinfo = FixtureInfo(names)
return python.Metafunc(func, fixtureinfo, config) definition = DefinitionMock(func)
return python.Metafunc(definition, fixtureinfo, config)
def test_no_funcargs(self, testdir): def test_no_funcargs(self, testdir):
def function(): def function():
pass pass
metafunc = self.Metafunc(function) metafunc = self.Metafunc(function)
assert not metafunc.fixturenames assert not metafunc.fixturenames
repr(metafunc._calls) repr(metafunc._calls)

View File

@ -8,11 +8,13 @@ from _pytest.mark import (
EMPTY_PARAMETERSET_OPTION, EMPTY_PARAMETERSET_OPTION,
) )
ignore_markinfo = pytest.mark.filterwarnings('ignore:MarkInfo objects:_pytest.deprecated.RemovedInPytest4Warning')
class TestMark(object): class TestMark(object):
def test_markinfo_repr(self): def test_markinfo_repr(self):
from _pytest.mark import MarkInfo, Mark from _pytest.mark import MarkInfo, Mark
m = MarkInfo(Mark("hello", (1, 2), {})) m = MarkInfo.for_mark(Mark("hello", (1, 2), {}))
repr(m) repr(m)
@pytest.mark.parametrize('attr', ['mark', 'param']) @pytest.mark.parametrize('attr', ['mark', 'param'])
@ -51,6 +53,7 @@ class TestMark(object):
mark.hello(f) mark.hello(f)
assert f.hello assert f.hello
@ignore_markinfo
def test_pytest_mark_keywords(self): def test_pytest_mark_keywords(self):
mark = Mark() mark = Mark()
@ -62,6 +65,7 @@ class TestMark(object):
assert f.world.kwargs['x'] == 3 assert f.world.kwargs['x'] == 3
assert f.world.kwargs['y'] == 4 assert f.world.kwargs['y'] == 4
@ignore_markinfo
def test_apply_multiple_and_merge(self): def test_apply_multiple_and_merge(self):
mark = Mark() mark = Mark()
@ -78,6 +82,7 @@ class TestMark(object):
assert f.world.kwargs['y'] == 1 assert f.world.kwargs['y'] == 1
assert len(f.world.args) == 0 assert len(f.world.args) == 0
@ignore_markinfo
def test_pytest_mark_positional(self): def test_pytest_mark_positional(self):
mark = Mark() mark = Mark()
@ -88,6 +93,7 @@ class TestMark(object):
assert f.world.args[0] == "hello" assert f.world.args[0] == "hello"
mark.world("world")(f) mark.world("world")(f)
@ignore_markinfo
def test_pytest_mark_positional_func_and_keyword(self): def test_pytest_mark_positional_func_and_keyword(self):
mark = Mark() mark = Mark()
@ -103,6 +109,7 @@ class TestMark(object):
assert g.world.args[0] is f assert g.world.args[0] is f
assert g.world.kwargs["omega"] == "hello" assert g.world.kwargs["omega"] == "hello"
@ignore_markinfo
def test_pytest_mark_reuse(self): def test_pytest_mark_reuse(self):
mark = Mark() mark = Mark()
@ -484,6 +491,7 @@ class TestFunctional(object):
assert 'hello' in keywords assert 'hello' in keywords
assert 'world' in keywords assert 'world' in keywords
@ignore_markinfo
def test_merging_markers(self, testdir): def test_merging_markers(self, testdir):
p = testdir.makepyfile(""" p = testdir.makepyfile("""
import pytest import pytest
@ -509,7 +517,6 @@ class TestFunctional(object):
assert values[1].args == () assert values[1].args == ()
assert values[2].args == ("pos1", ) assert values[2].args == ("pos1", )
@pytest.mark.xfail(reason='unfixed')
def test_merging_markers_deep(self, testdir): def test_merging_markers_deep(self, testdir):
# issue 199 - propagate markers into nested classes # issue 199 - propagate markers into nested classes
p = testdir.makepyfile(""" p = testdir.makepyfile("""
@ -526,7 +533,7 @@ class TestFunctional(object):
items, rec = testdir.inline_genitems(p) items, rec = testdir.inline_genitems(p)
for item in items: for item in items:
print(item, item.keywords) print(item, item.keywords)
assert 'a' in item.keywords assert [x for x in item.iter_markers() if x.name == 'a']
def test_mark_decorator_subclass_does_not_propagate_to_base(self, testdir): def test_mark_decorator_subclass_does_not_propagate_to_base(self, testdir):
p = testdir.makepyfile(""" p = testdir.makepyfile("""
@ -622,6 +629,7 @@ class TestFunctional(object):
"keyword: *hello*" "keyword: *hello*"
]) ])
@ignore_markinfo
def test_merging_markers_two_functions(self, testdir): def test_merging_markers_two_functions(self, testdir):
p = testdir.makepyfile(""" p = testdir.makepyfile("""
import pytest import pytest
@ -676,6 +684,7 @@ class TestFunctional(object):
reprec = testdir.inline_run() reprec = testdir.inline_run()
reprec.assertoutcome(passed=1) reprec.assertoutcome(passed=1)
@ignore_markinfo
def test_keyword_added_for_session(self, testdir): def test_keyword_added_for_session(self, testdir):
testdir.makeconftest(""" testdir.makeconftest("""
import pytest import pytest
@ -715,8 +724,8 @@ class TestFunctional(object):
if isinstance(v, MarkInfo)]) if isinstance(v, MarkInfo)])
assert marker_names == set(expected_markers) assert marker_names == set(expected_markers)
@pytest.mark.xfail(reason='callspec2.setmulti misuses keywords')
@pytest.mark.issue1540 @pytest.mark.issue1540
@pytest.mark.filterwarnings("ignore")
def test_mark_from_parameters(self, testdir): def test_mark_from_parameters(self, testdir):
testdir.makepyfile(""" testdir.makepyfile("""
import pytest import pytest