Merge pull request #3317 from RonnyPfannschmidt/marker-pristine-node-storage
introduce a distinct searchable non-broken storage for markers
This commit is contained in:
commit
715337011b
|
@ -32,7 +32,8 @@ RESULT_LOG = (
|
|||
)
|
||||
|
||||
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(
|
||||
|
|
|
@ -5,6 +5,7 @@ import inspect
|
|||
import sys
|
||||
import warnings
|
||||
from collections import OrderedDict, deque, defaultdict
|
||||
from more_itertools import flatten
|
||||
|
||||
import attr
|
||||
import py
|
||||
|
@ -371,10 +372,7 @@ class FixtureRequest(FuncargnamesCompatAttr):
|
|||
:arg marker: a :py:class:`_pytest.mark.MarkDecorator` object
|
||||
created by a call to ``pytest.mark.NAME(...)``.
|
||||
"""
|
||||
try:
|
||||
self.node.keywords[marker.markname] = marker
|
||||
except AttributeError:
|
||||
raise ValueError(marker)
|
||||
self.node.add_marker(marker)
|
||||
|
||||
def raiseerror(self, msg):
|
||||
""" raise a FixtureLookupError with the given message. """
|
||||
|
@ -985,10 +983,9 @@ class FixtureManager(object):
|
|||
argnames = getfuncargnames(func, cls=cls)
|
||||
else:
|
||||
argnames = ()
|
||||
usefixtures = getattr(func, "usefixtures", None)
|
||||
usefixtures = flatten(mark.args for mark in node.iter_markers() if mark.name == "usefixtures")
|
||||
initialnames = argnames
|
||||
if usefixtures is not None:
|
||||
initialnames = usefixtures.args + initialnames
|
||||
initialnames = tuple(usefixtures) + initialnames
|
||||
fm = node.session._fixturemanager
|
||||
names_closure, arg2fixturedefs = fm.getfixtureclosure(initialnames,
|
||||
node)
|
||||
|
@ -1070,6 +1067,8 @@ class FixtureManager(object):
|
|||
fixturedef = faclist[-1]
|
||||
if fixturedef.params is not 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_kwargs = getattr(parametrize_func, 'kwargs', {})
|
||||
# skip directly parametrized arguments
|
||||
|
|
|
@ -4,7 +4,6 @@ import sys
|
|||
import platform
|
||||
import traceback
|
||||
|
||||
from . import MarkDecorator, MarkInfo
|
||||
from ..outcomes import fail, TEST_OUTCOME
|
||||
|
||||
|
||||
|
@ -28,22 +27,15 @@ class MarkEvaluator(object):
|
|||
self._mark_name = name
|
||||
|
||||
def __bool__(self):
|
||||
self._marks = self._get_marks()
|
||||
return bool(self._marks)
|
||||
# dont cache here to prevent staleness
|
||||
return bool(self._get_marks())
|
||||
__nonzero__ = __bool__
|
||||
|
||||
def wasvalid(self):
|
||||
return not hasattr(self, 'exc')
|
||||
|
||||
def _get_marks(self):
|
||||
|
||||
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 []
|
||||
return [x for x in self.item.iter_markers() if x.name == self._mark_name]
|
||||
|
||||
def invalidraise(self, exc):
|
||||
raises = self.get('raises')
|
||||
|
|
|
@ -4,9 +4,10 @@ from operator import attrgetter
|
|||
import inspect
|
||||
|
||||
import attr
|
||||
from ..deprecated import MARK_PARAMETERSET_UNPACKING
|
||||
|
||||
from ..deprecated import MARK_PARAMETERSET_UNPACKING, MARK_INFO_ATTRIBUTE
|
||||
from ..compat import NOTSET, getfslineno
|
||||
from six.moves import map
|
||||
from six.moves import map, reduce
|
||||
|
||||
|
||||
EMPTY_PARAMETERSET_OPTION = "empty_parameter_set_mark"
|
||||
|
@ -113,11 +114,21 @@ class ParameterSet(namedtuple('ParameterSet', 'values, marks, id')):
|
|||
|
||||
@attr.s(frozen=True)
|
||||
class Mark(object):
|
||||
name = attr.ib()
|
||||
args = attr.ib()
|
||||
kwargs = attr.ib()
|
||||
#: name of the mark
|
||||
name = attr.ib(type=str)
|
||||
#: 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):
|
||||
"""
|
||||
: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
|
||||
return Mark(
|
||||
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))
|
||||
holder = getattr(func, mark.name, None)
|
||||
if holder is None:
|
||||
holder = MarkInfo(mark)
|
||||
holder = MarkInfo.for_mark(mark)
|
||||
setattr(func, mark.name, holder)
|
||||
else:
|
||||
holder.add_mark(mark)
|
||||
|
@ -260,23 +271,29 @@ def _marked(func, mark):
|
|||
invoked more than once.
|
||||
"""
|
||||
try:
|
||||
func_mark = getattr(func, mark.name)
|
||||
func_mark = getattr(func, getattr(mark, 'combined', mark).name)
|
||||
except AttributeError:
|
||||
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):
|
||||
""" Marking object created by :class:`MarkDecorator` instances. """
|
||||
|
||||
def __init__(self, mark):
|
||||
assert isinstance(mark, Mark), repr(mark)
|
||||
self.combined = mark
|
||||
self._marks = [mark]
|
||||
_marks = attr.ib()
|
||||
combined = attr.ib(
|
||||
repr=False,
|
||||
default=attr.Factory(lambda self: reduce(Mark.combined_with, self._marks),
|
||||
takes_self=True))
|
||||
|
||||
name = alias('combined.name')
|
||||
args = alias('combined.args')
|
||||
kwargs = alias('combined.kwargs')
|
||||
name = alias('combined.name', warning=MARK_INFO_ATTRIBUTE)
|
||||
args = alias('combined.args', warning=MARK_INFO_ATTRIBUTE)
|
||||
kwargs = alias('combined.kwargs', warning=MARK_INFO_ATTRIBUTE)
|
||||
|
||||
@classmethod
|
||||
def for_mark(cls, mark):
|
||||
return cls([mark])
|
||||
|
||||
def __repr__(self):
|
||||
return "<MarkInfo {0!r}>".format(self.combined)
|
||||
|
@ -288,7 +305,7 @@ class MarkInfo(object):
|
|||
|
||||
def __iter__(self):
|
||||
""" yield MarkInfo objects each relating to a marking-call. """
|
||||
return map(MarkInfo, self._marks)
|
||||
return map(MarkInfo.for_mark, self._marks)
|
||||
|
||||
|
||||
class MarkGenerator(object):
|
||||
|
@ -365,3 +382,33 @@ class NodeKeywords(MappingMixin):
|
|||
|
||||
def __repr__(self):
|
||||
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)
|
||||
|
|
|
@ -8,7 +8,7 @@ import attr
|
|||
import _pytest
|
||||
import _pytest._code
|
||||
|
||||
from _pytest.mark.structures import NodeKeywords
|
||||
from _pytest.mark.structures import NodeKeywords, MarkInfo
|
||||
|
||||
SEP = "/"
|
||||
|
||||
|
@ -90,6 +90,9 @@ class Node(object):
|
|||
#: keywords/markers collected from all scopes
|
||||
self.keywords = NodeKeywords(self)
|
||||
|
||||
#: the marker objects belonging to this node
|
||||
self.own_markers = []
|
||||
|
||||
#: allow adding of extra keywords to use for matching
|
||||
self.extra_keyword_matches = set()
|
||||
|
||||
|
@ -178,15 +181,34 @@ class Node(object):
|
|||
elif not isinstance(marker, MarkDecorator):
|
||||
raise ValueError("is not a string or pytest.mark.* 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):
|
||||
""" get a marker object from this node or None if
|
||||
the node doesn't have a marker with that name. """
|
||||
val = self.keywords.get(name, None)
|
||||
if val is not None:
|
||||
from _pytest.mark import MarkInfo, MarkDecorator
|
||||
if isinstance(val, (MarkDecorator, MarkInfo)):
|
||||
return val
|
||||
the node doesn't have a marker with that name.
|
||||
|
||||
..warning::
|
||||
|
||||
deprecated
|
||||
"""
|
||||
markers = [x for x in self.iter_markers() if x.name == name]
|
||||
if markers:
|
||||
return MarkInfo(markers)
|
||||
|
||||
def listextrakeywords(self):
|
||||
""" Return a set of all extra keywords in self and any parents."""
|
||||
|
|
|
@ -28,7 +28,7 @@ from _pytest.compat import (
|
|||
safe_str, getlocation, enum,
|
||||
)
|
||||
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;
|
||||
|
@ -117,11 +117,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))
|
||||
try:
|
||||
markers = metafunc.function.parametrize
|
||||
except AttributeError:
|
||||
return
|
||||
for marker in markers:
|
||||
for marker in metafunc.definition.iter_markers():
|
||||
if marker.name == 'parametrize':
|
||||
metafunc.parametrize(*marker.args, **marker.kwargs)
|
||||
|
||||
|
||||
|
@ -212,11 +209,20 @@ class PyobjContext(object):
|
|||
|
||||
|
||||
class PyobjMixin(PyobjContext):
|
||||
_ALLOW_MARKERS = True
|
||||
|
||||
def __init__(self, *k, **kw):
|
||||
super(PyobjMixin, self).__init__(*k, **kw)
|
||||
|
||||
def obj():
|
||||
def fget(self):
|
||||
obj = getattr(self, '_obj', None)
|
||||
if obj is None:
|
||||
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
|
||||
|
||||
def fset(self, value):
|
||||
|
@ -363,9 +369,15 @@ class PyCollector(PyobjMixin, nodes.Collector):
|
|||
cls = clscol and clscol.obj or None
|
||||
transfer_markers(funcobj, cls, module)
|
||||
fm = self.session._fixturemanager
|
||||
fixtureinfo = fm.getfixtureinfo(self, funcobj, cls)
|
||||
metafunc = Metafunc(funcobj, fixtureinfo, self.config,
|
||||
cls=cls, module=module)
|
||||
|
||||
definition = FunctionDefinition(
|
||||
name=name,
|
||||
parent=self,
|
||||
callobj=funcobj,
|
||||
)
|
||||
fixtureinfo = fm.getfixtureinfo(definition, funcobj, cls)
|
||||
|
||||
metafunc = Metafunc(definition, fixtureinfo, self.config, cls=cls, module=module)
|
||||
methods = []
|
||||
if hasattr(module, "pytest_generate_tests"):
|
||||
methods.append(module.pytest_generate_tests)
|
||||
|
@ -524,6 +536,11 @@ class Class(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):
|
||||
return self.parent.obj()
|
||||
|
||||
|
@ -723,15 +740,17 @@ class Metafunc(fixtures.FuncargnamesCompatAttr):
|
|||
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
|
||||
assert isinstance(definition, FunctionDefinition) or type(definition).__name__ == "DefinitionMock"
|
||||
self.definition = definition
|
||||
self.config = config
|
||||
|
||||
#: the module object where the test function is defined in.
|
||||
self.module = module
|
||||
|
||||
#: underlying python test function
|
||||
self.function = function
|
||||
self.function = definition.obj
|
||||
|
||||
#: set of fixture names required by the test function
|
||||
self.fixturenames = fixtureinfo.names_closure
|
||||
|
@ -1103,6 +1122,8 @@ class Function(FunctionMixin, nodes.Item, fixtures.FuncargnamesCompatAttr):
|
|||
Python test function.
|
||||
"""
|
||||
_genid = None
|
||||
# disable since functions handle it themselfes
|
||||
_ALLOW_MARKERS = False
|
||||
|
||||
def __init__(self, name, parent, args=None, config=None,
|
||||
callspec=None, callobj=NOTSET, keywords=None, session=None,
|
||||
|
@ -1114,6 +1135,7 @@ class Function(FunctionMixin, nodes.Item, fixtures.FuncargnamesCompatAttr):
|
|||
self.obj = callobj
|
||||
|
||||
self.keywords.update(self.obj.__dict__)
|
||||
self.own_markers.extend(get_unpacked_marks(self.obj))
|
||||
if callspec:
|
||||
self.callspec = callspec
|
||||
# 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
|
||||
# and keywords cant fix it per design
|
||||
self.keywords[mark.name] = mark
|
||||
self.own_markers.extend(callspec.marks)
|
||||
if keywords:
|
||||
self.keywords.update(keywords)
|
||||
|
||||
|
@ -1181,3 +1204,15 @@ class Function(FunctionMixin, nodes.Item, fixtures.FuncargnamesCompatAttr):
|
|||
def setup(self):
|
||||
super(Function, self).setup()
|
||||
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
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
from _pytest.config import hookimpl
|
||||
from _pytest.mark import MarkInfo, MarkDecorator
|
||||
from _pytest.mark.evaluate import MarkEvaluator
|
||||
from _pytest.outcomes import fail, skip, xfail
|
||||
|
||||
|
@ -60,15 +59,14 @@ def pytest_configure(config):
|
|||
def pytest_runtest_setup(item):
|
||||
# Check if skip or skipif are specified as pytest marks
|
||||
item._skipped_by_mark = False
|
||||
skipif_info = item.keywords.get('skipif')
|
||||
if isinstance(skipif_info, (MarkInfo, MarkDecorator)):
|
||||
eval_skipif = MarkEvaluator(item, 'skipif')
|
||||
if eval_skipif.istrue():
|
||||
item._skipped_by_mark = True
|
||||
skip(eval_skipif.getexplanation())
|
||||
|
||||
skip_info = item.keywords.get('skip')
|
||||
if isinstance(skip_info, (MarkInfo, MarkDecorator)):
|
||||
for skip_info in item.iter_markers():
|
||||
if skip_info.name != 'skip':
|
||||
continue
|
||||
item._skipped_by_mark = True
|
||||
if 'reason' in skip_info.kwargs:
|
||||
skip(skip_info.kwargs['reason'])
|
||||
|
|
|
@ -60,8 +60,8 @@ def catch_warnings_for_item(item):
|
|||
for arg in inifilters:
|
||||
_setoption(warnings, arg)
|
||||
|
||||
mark = item.get_marker('filterwarnings')
|
||||
if mark:
|
||||
for mark in item.iter_markers():
|
||||
if mark.name == 'filterwarnings':
|
||||
for arg in mark.args:
|
||||
warnings._setoption(arg)
|
||||
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
introduce correct per node mark handling and deprecate the always incorrect existing mark handling
|
|
@ -330,11 +330,10 @@ specifies via named environments::
|
|||
"env(name): mark test to run only on named environment")
|
||||
|
||||
def pytest_runtest_setup(item):
|
||||
envmarker = item.get_marker("env")
|
||||
if envmarker is not None:
|
||||
envname = envmarker.args[0]
|
||||
if envname != item.config.getoption("-E"):
|
||||
pytest.skip("test requires env %r" % envname)
|
||||
envnames = [mark.args[0] for mark in item.iter_markers() if mark.name == "env"]
|
||||
if envnames:
|
||||
if item.config.getoption("-E") not in envnames:
|
||||
pytest.skip("test requires env in %r" % envnames)
|
||||
|
||||
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
|
||||
|
||||
def pytest_runtest_setup(item):
|
||||
marker = item.get_marker('my_marker')
|
||||
if marker is not None:
|
||||
for info in marker:
|
||||
print('Marker info name={} args={} kwars={}'.format(info.name, info.args, info.kwargs))
|
||||
for marker in item.iter_markers():
|
||||
if marker.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.
|
||||
|
@ -426,7 +424,7 @@ However, if there is a callable as the single positional argument with no keywor
|
|||
The output is as follows::
|
||||
|
||||
$ 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
|
||||
|
||||
|
@ -460,10 +458,9 @@ test function. From a conftest file we can read it like this::
|
|||
import sys
|
||||
|
||||
def pytest_runtest_setup(item):
|
||||
g = item.get_marker("glob")
|
||||
if g is not None:
|
||||
for info in g:
|
||||
print ("glob args=%s kwargs=%s" %(info.args, info.kwargs))
|
||||
for mark in item.iter_markers():
|
||||
if mark.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::
|
||||
|
@ -494,11 +491,10 @@ for your particular platform, you could use the following plugin::
|
|||
ALL = set("darwin linux win32".split())
|
||||
|
||||
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
|
||||
if not item.get_marker(plat):
|
||||
if ALL.intersection(item.keywords):
|
||||
pytest.skip("cannot run on platform %s" %(plat))
|
||||
if supported_platforms and plat not in supported_platforms:
|
||||
pytest.skip("cannot run on platform %s" % (plat))
|
||||
|
||||
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::
|
||||
|
@ -532,7 +528,7 @@ then you will see two tests skipped and two executed tests as expected::
|
|||
|
||||
test_plat.py s.s. [100%]
|
||||
========================= 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 ====================
|
||||
|
||||
|
|
|
@ -389,7 +389,7 @@ Now we can profile which test functions execute the slowest::
|
|||
========================= slowest 3 test durations =========================
|
||||
0.30s call test_some_are_slow.py::test_funcslow2
|
||||
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 =========================
|
||||
|
||||
incremental testing - test steps
|
||||
|
|
|
@ -26,3 +26,33 @@ which also serve as documentation.
|
|||
: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`
|
||||
|
||||
|
|
|
@ -94,6 +94,8 @@ Marks can be used apply meta data to *test functions* (but not fixtures), which
|
|||
fixtures or plugins.
|
||||
|
||||
|
||||
|
||||
|
||||
.. _`pytest.mark.filterwarnings ref`:
|
||||
|
||||
pytest.mark.filterwarnings
|
||||
|
@ -200,9 +202,9 @@ For example:
|
|||
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
|
||||
: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
|
||||
|
||||
|
@ -685,18 +687,28 @@ MarkDecorator
|
|||
.. autoclass:: _pytest.mark.MarkDecorator
|
||||
:members:
|
||||
|
||||
|
||||
MarkGenerator
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
.. autoclass:: _pytest.mark.MarkGenerator
|
||||
:members:
|
||||
|
||||
|
||||
MarkInfo
|
||||
~~~~~~~~
|
||||
|
||||
.. autoclass:: _pytest.mark.MarkInfo
|
||||
:members:
|
||||
|
||||
|
||||
Mark
|
||||
~~~~
|
||||
|
||||
.. autoclass:: _pytest.mark.structures.Mark
|
||||
:members:
|
||||
|
||||
|
||||
Metafunc
|
||||
~~~~~~~~
|
||||
|
||||
|
|
|
@ -260,8 +260,8 @@ Alternatively, you can integrate this functionality with custom markers:
|
|||
|
||||
def pytest_collection_modifyitems(session, config, items):
|
||||
for item in items:
|
||||
marker = item.get_marker('test_id')
|
||||
if marker is not None:
|
||||
for marker in item.iter_markers():
|
||||
if marker.name == 'test_id':
|
||||
test_id = marker.args[0]
|
||||
item.user_properties.append(('test_id', test_id))
|
||||
|
||||
|
|
|
@ -1781,6 +1781,8 @@ class TestAutouseManagement(object):
|
|||
import pytest
|
||||
values = []
|
||||
def pytest_generate_tests(metafunc):
|
||||
if metafunc.cls is None:
|
||||
assert metafunc.function is test_finish
|
||||
if metafunc.cls is not None:
|
||||
metafunc.parametrize("item", [1,2], scope="class")
|
||||
class TestClass(object):
|
||||
|
@ -1798,7 +1800,7 @@ class TestAutouseManagement(object):
|
|||
assert values == ["setup-1", "step1-1", "step2-1", "teardown-1",
|
||||
"setup-2", "step1-2", "step2-2", "teardown-2",]
|
||||
""")
|
||||
reprec = testdir.inline_run()
|
||||
reprec = testdir.inline_run('-s')
|
||||
reprec.assertoutcome(passed=5)
|
||||
|
||||
def test_ordering_autouse_before_explicit(self, testdir):
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
|
||||
import attr
|
||||
import _pytest._code
|
||||
import py
|
||||
import pytest
|
||||
|
@ -24,13 +24,19 @@ class TestMetafunc(object):
|
|||
def __init__(self, names):
|
||||
self.names_closure = names
|
||||
|
||||
@attr.s
|
||||
class DefinitionMock(object):
|
||||
obj = attr.ib()
|
||||
|
||||
names = fixtures.getfuncargnames(func)
|
||||
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 function():
|
||||
pass
|
||||
|
||||
metafunc = self.Metafunc(function)
|
||||
assert not metafunc.fixturenames
|
||||
repr(metafunc._calls)
|
||||
|
|
|
@ -8,11 +8,13 @@ from _pytest.mark import (
|
|||
EMPTY_PARAMETERSET_OPTION,
|
||||
)
|
||||
|
||||
ignore_markinfo = pytest.mark.filterwarnings('ignore:MarkInfo objects:_pytest.deprecated.RemovedInPytest4Warning')
|
||||
|
||||
|
||||
class TestMark(object):
|
||||
def test_markinfo_repr(self):
|
||||
from _pytest.mark import MarkInfo, Mark
|
||||
m = MarkInfo(Mark("hello", (1, 2), {}))
|
||||
m = MarkInfo.for_mark(Mark("hello", (1, 2), {}))
|
||||
repr(m)
|
||||
|
||||
@pytest.mark.parametrize('attr', ['mark', 'param'])
|
||||
|
@ -51,6 +53,7 @@ class TestMark(object):
|
|||
mark.hello(f)
|
||||
assert f.hello
|
||||
|
||||
@ignore_markinfo
|
||||
def test_pytest_mark_keywords(self):
|
||||
mark = Mark()
|
||||
|
||||
|
@ -62,6 +65,7 @@ class TestMark(object):
|
|||
assert f.world.kwargs['x'] == 3
|
||||
assert f.world.kwargs['y'] == 4
|
||||
|
||||
@ignore_markinfo
|
||||
def test_apply_multiple_and_merge(self):
|
||||
mark = Mark()
|
||||
|
||||
|
@ -78,6 +82,7 @@ class TestMark(object):
|
|||
assert f.world.kwargs['y'] == 1
|
||||
assert len(f.world.args) == 0
|
||||
|
||||
@ignore_markinfo
|
||||
def test_pytest_mark_positional(self):
|
||||
mark = Mark()
|
||||
|
||||
|
@ -88,6 +93,7 @@ class TestMark(object):
|
|||
assert f.world.args[0] == "hello"
|
||||
mark.world("world")(f)
|
||||
|
||||
@ignore_markinfo
|
||||
def test_pytest_mark_positional_func_and_keyword(self):
|
||||
mark = Mark()
|
||||
|
||||
|
@ -103,6 +109,7 @@ class TestMark(object):
|
|||
assert g.world.args[0] is f
|
||||
assert g.world.kwargs["omega"] == "hello"
|
||||
|
||||
@ignore_markinfo
|
||||
def test_pytest_mark_reuse(self):
|
||||
mark = Mark()
|
||||
|
||||
|
@ -484,6 +491,7 @@ class TestFunctional(object):
|
|||
assert 'hello' in keywords
|
||||
assert 'world' in keywords
|
||||
|
||||
@ignore_markinfo
|
||||
def test_merging_markers(self, testdir):
|
||||
p = testdir.makepyfile("""
|
||||
import pytest
|
||||
|
@ -509,7 +517,6 @@ class TestFunctional(object):
|
|||
assert values[1].args == ()
|
||||
assert values[2].args == ("pos1", )
|
||||
|
||||
@pytest.mark.xfail(reason='unfixed')
|
||||
def test_merging_markers_deep(self, testdir):
|
||||
# issue 199 - propagate markers into nested classes
|
||||
p = testdir.makepyfile("""
|
||||
|
@ -526,7 +533,7 @@ class TestFunctional(object):
|
|||
items, rec = testdir.inline_genitems(p)
|
||||
for item in items:
|
||||
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):
|
||||
p = testdir.makepyfile("""
|
||||
|
@ -622,6 +629,7 @@ class TestFunctional(object):
|
|||
"keyword: *hello*"
|
||||
])
|
||||
|
||||
@ignore_markinfo
|
||||
def test_merging_markers_two_functions(self, testdir):
|
||||
p = testdir.makepyfile("""
|
||||
import pytest
|
||||
|
@ -676,6 +684,7 @@ class TestFunctional(object):
|
|||
reprec = testdir.inline_run()
|
||||
reprec.assertoutcome(passed=1)
|
||||
|
||||
@ignore_markinfo
|
||||
def test_keyword_added_for_session(self, testdir):
|
||||
testdir.makeconftest("""
|
||||
import pytest
|
||||
|
@ -715,8 +724,8 @@ class TestFunctional(object):
|
|||
if isinstance(v, MarkInfo)])
|
||||
assert marker_names == set(expected_markers)
|
||||
|
||||
@pytest.mark.xfail(reason='callspec2.setmulti misuses keywords')
|
||||
@pytest.mark.issue1540
|
||||
@pytest.mark.filterwarnings("ignore")
|
||||
def test_mark_from_parameters(self, testdir):
|
||||
testdir.makepyfile("""
|
||||
import pytest
|
||||
|
|
Loading…
Reference in New Issue