From 360d608da4e11c66539967e625c70e6cae31395e Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Fri, 16 Mar 2018 11:02:34 +0100 Subject: [PATCH 01/27] introduce a own storage for markers --- _pytest/mark/structures.py | 25 +++++++++++++++++++++++++ _pytest/nodes.py | 15 +++++++++------ _pytest/python.py | 10 +++++++++- 3 files changed, 43 insertions(+), 7 deletions(-) diff --git a/_pytest/mark/structures.py b/_pytest/mark/structures.py index c5697298066..fe0e1983dd7 100644 --- a/_pytest/mark/structures.py +++ b/_pytest/mark/structures.py @@ -365,3 +365,28 @@ def __len__(self): def __repr__(self): return "" % (self.node, ) + + +@attr.s(cmp=False, hash=False) +class NodeMarkers(object): + node = attr.ib(repr=False) + own_markers = attr.ib(default=attr.Factory(list)) + + @classmethod + def from_node(cls, node): + return cls(node=node) + + 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 node in reversed(self.node.listchain()): + for mark in node._markers.own_markers: + if mark.name == name: + yield mark diff --git a/_pytest/nodes.py b/_pytest/nodes.py index 97f4da6028b..e7686b39fd4 100644 --- a/_pytest/nodes.py +++ b/_pytest/nodes.py @@ -4,11 +4,12 @@ import six import py import attr +from more_itertools import first import _pytest import _pytest._code -from _pytest.mark.structures import NodeKeywords +from _pytest.mark.structures import NodeKeywords, NodeMarkers SEP = "/" @@ -89,6 +90,7 @@ def __init__(self, name, parent=None, config=None, session=None, fspath=None, no #: keywords/markers collected from all scopes self.keywords = NodeKeywords(self) + self._markers = NodeMarkers.from_node(self) #: allow adding of extra keywords to use for matching self.extra_keyword_matches = set() @@ -178,15 +180,16 @@ def add_marker(self, marker): elif not isinstance(marker, MarkDecorator): raise ValueError("is not a string or pytest.mark.* Marker") self.keywords[marker.name] = marker + self._markers.update([marker]) + + def find_markers(self, name): + """find all marks with the given name on the node and its parents""" + return self._markers.find(name) 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 + return first(self.find_markers(name), None) def listextrakeywords(self): """ Return a set of all extra keywords in self and any parents.""" diff --git a/_pytest/python.py b/_pytest/python.py index f9f17afd794..f7d4c601ce7 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -28,7 +28,7 @@ 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; @@ -212,11 +212,17 @@ class PyobjContext(object): class PyobjMixin(PyobjContext): + + 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 hacn + self._markers.update(get_unpacked_marks(self.obj)) return obj def fset(self, value): @@ -1114,6 +1120,7 @@ def __init__(self, name, parent, args=None, config=None, self.obj = callobj self.keywords.update(self.obj.__dict__) + self._markers.update(get_unpacked_marks(self.obj)) if callspec: self.callspec = callspec # this is total hostile and a mess @@ -1123,6 +1130,7 @@ def __init__(self, name, parent, args=None, config=None, # feel free to cry, this was broken for years before # and keywords cant fix it per design self.keywords[mark.name] = mark + self._markers.update(callspec.marks) if keywords: self.keywords.update(keywords) From 27072215593b2808bbae2fb633caf556cc5852e3 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Fri, 16 Mar 2018 11:16:08 +0100 Subject: [PATCH 02/27] port mark evaluation to the new storage and fix a bug in evaluation --- _pytest/fixtures.py | 5 +---- _pytest/mark/evaluate.py | 11 +---------- testing/test_mark.py | 1 - 3 files changed, 2 insertions(+), 15 deletions(-) diff --git a/_pytest/fixtures.py b/_pytest/fixtures.py index 2ac340e6f4f..5c6a4a23019 100644 --- a/_pytest/fixtures.py +++ b/_pytest/fixtures.py @@ -371,10 +371,7 @@ def applymarker(self, marker): :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. """ diff --git a/_pytest/mark/evaluate.py b/_pytest/mark/evaluate.py index 295373e17c6..1aef138b175 100644 --- a/_pytest/mark/evaluate.py +++ b/_pytest/mark/evaluate.py @@ -4,7 +4,6 @@ import platform import traceback -from . import MarkDecorator, MarkInfo from ..outcomes import fail, TEST_OUTCOME @@ -28,7 +27,6 @@ def __init__(self, item, name): self._mark_name = name def __bool__(self): - self._marks = self._get_marks() return bool(self._marks) __nonzero__ = __bool__ @@ -36,14 +34,7 @@ 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 list(self.item.find_markers(self._mark_name)) def invalidraise(self, exc): raises = self.get('raises') diff --git a/testing/test_mark.py b/testing/test_mark.py index b4dd656340e..95e391a0c9f 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -715,7 +715,6 @@ def assert_markers(self, items, **expected): if isinstance(v, MarkInfo)]) assert marker_names == set(expected_markers) - @pytest.mark.xfail(reason='callspec2.setmulti misuses keywords') @pytest.mark.issue1540 def test_mark_from_parameters(self, testdir): testdir.makepyfile(""" From f1a1695aaabf554fea21382b7edc0ee79e59b3d5 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Fri, 16 Mar 2018 11:31:33 +0100 Subject: [PATCH 03/27] enable deep merging test - new structure fixed it --- testing/test_mark.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/testing/test_mark.py b/testing/test_mark.py index 95e391a0c9f..9f4a7fc8875 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -509,7 +509,6 @@ def test_func(self): 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 +525,7 @@ def test_d(self): items, rec = testdir.inline_genitems(p) for item in items: print(item, item.keywords) - assert 'a' in item.keywords + assert list(item.find_markers('a')) def test_mark_decorator_subclass_does_not_propagate_to_base(self, testdir): p = testdir.makepyfile(""" @@ -716,6 +715,7 @@ def assert_markers(self, items, **expected): assert marker_names == set(expected_markers) @pytest.mark.issue1540 + @pytest.mark.filterwarnings("ignore") def test_mark_from_parameters(self, testdir): testdir.makepyfile(""" import pytest From e8feee0612246d4f3414530388c9aee6dd0f563c Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Fri, 16 Mar 2018 15:56:45 +0100 Subject: [PATCH 04/27] fix up the mark evaluator validity check --- _pytest/mark/evaluate.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/_pytest/mark/evaluate.py b/_pytest/mark/evaluate.py index 1aef138b175..19eff8e00ff 100644 --- a/_pytest/mark/evaluate.py +++ b/_pytest/mark/evaluate.py @@ -27,7 +27,8 @@ def __init__(self, item, name): self._mark_name = name def __bool__(self): - return bool(self._marks) + # dont cache here to prevent staleness + return bool(self._get_marks()) __nonzero__ = __bool__ def wasvalid(self): From 180ae0920235e883138d56fa4274565793058465 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sat, 17 Mar 2018 21:42:43 +0100 Subject: [PATCH 05/27] deprecate markinfo and fix up most marker scoping access while completely breaking metafunc testing --- _pytest/deprecated.py | 3 ++- _pytest/fixtures.py | 7 +++++-- _pytest/mark/structures.py | 12 +++++------ _pytest/python.py | 41 ++++++++++++++++++++++++++++---------- _pytest/skipping.py | 16 ++++++--------- testing/python/fixture.py | 4 +++- testing/python/metafunc.py | 8 +++++++- testing/test_mark.py | 9 +++++++++ 8 files changed, 68 insertions(+), 32 deletions(-) diff --git a/_pytest/deprecated.py b/_pytest/deprecated.py index a0eec0e7df6..be4a5a080c7 100644 --- a/_pytest/deprecated.py +++ b/_pytest/deprecated.py @@ -32,7 +32,8 @@ class RemovedInPytest4Warning(DeprecationWarning): ) 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.find_markers to iterate over markers correctly" ) MARK_PARAMETERSET_UNPACKING = RemovedInPytest4Warning( diff --git a/_pytest/fixtures.py b/_pytest/fixtures.py index 5c6a4a23019..457aa9d4f48 100644 --- a/_pytest/fixtures.py +++ b/_pytest/fixtures.py @@ -5,6 +5,7 @@ import sys import warnings from collections import OrderedDict, deque, defaultdict +from more_itertools import flatten import attr import py @@ -982,10 +983,10 @@ def getfixtureinfo(self, node, func, cls, funcargs=True): argnames = getfuncargnames(func, cls=cls) else: argnames = () - usefixtures = getattr(func, "usefixtures", None) + usefixtures = flatten(uf_mark.args for uf_mark in node.find_markers("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) @@ -1067,6 +1068,8 @@ def pytest_generate_tests(self, metafunc): 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 diff --git a/_pytest/mark/structures.py b/_pytest/mark/structures.py index fe0e1983dd7..b42a665517a 100644 --- a/_pytest/mark/structures.py +++ b/_pytest/mark/structures.py @@ -4,7 +4,7 @@ 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 @@ -260,10 +260,10 @@ 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) class MarkInfo(object): @@ -274,9 +274,9 @@ def __init__(self, mark): self.combined = mark self._marks = [mark] - 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) def __repr__(self): return "".format(self.combined) diff --git a/_pytest/python.py b/_pytest/python.py index f7d4c601ce7..843d7f5348c 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -117,11 +117,7 @@ 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.find_markers('parametrize'): metafunc.parametrize(*marker.args, **marker.kwargs) @@ -212,6 +208,7 @@ class PyobjContext(object): class PyobjMixin(PyobjContext): + _ALLOW_MARKERS = True def __init__(self, *k, **kw): super(PyobjMixin, self).__init__(*k, **kw) @@ -221,8 +218,9 @@ def fget(self): obj = getattr(self, '_obj', None) if obj is None: self._obj = obj = self._getobj() - # XXX evil hacn - self._markers.update(get_unpacked_marks(self.obj)) + # XXX evil hack + if self._ALLOW_MARKERS: + self._markers.update(get_unpacked_marks(self.obj)) return obj def fset(self, value): @@ -370,8 +368,13 @@ def _genfunctions(self, name, funcobj): 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, + ) + metafunc = Metafunc(definition, fixtureinfo, self.config, cls=cls, module=module) methods = [] if hasattr(module, "pytest_generate_tests"): methods.append(module.pytest_generate_tests) @@ -530,6 +533,8 @@ def setup(self): class Instance(PyCollector): + _ALLOW_MARKERS = False # hack, destroy later + def _getobj(self): return self.parent.obj() @@ -729,15 +734,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) + 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 @@ -1189,3 +1196,15 @@ def runtest(self): 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 diff --git a/_pytest/skipping.py b/_pytest/skipping.py index 48b837def0a..318c8795e4e 100644 --- a/_pytest/skipping.py +++ b/_pytest/skipping.py @@ -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,12 @@ def nop(*args, **kwargs): 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)): + eval_skipif = MarkEvaluator(item, 'skipif') + if eval_skipif.istrue(): + item._skipped_by_mark = True + skip(eval_skipif.getexplanation()) + + for skip_info in item.find_markers('skip'): item._skipped_by_mark = True if 'reason' in skip_info.kwargs: skip(skip_info.kwargs['reason']) diff --git a/testing/python/fixture.py b/testing/python/fixture.py index 59c5266cb7b..c558ea3cf62 100644 --- a/testing/python/fixture.py +++ b/testing/python/fixture.py @@ -1781,6 +1781,8 @@ def test_parametrization_setup_teardown_ordering(self, testdir): 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 @@ def test_finish(): 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): diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index f2732ef3b9c..b65a42e09d0 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -26,11 +26,17 @@ def __init__(self, names): names = fixtures.getfuncargnames(func) fixtureinfo = FixtureInfo(names) - return python.Metafunc(func, fixtureinfo, config) + definition = python.FunctionDefinition( + name=func.__name__, + parent=None, + callobj=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) diff --git a/testing/test_mark.py b/testing/test_mark.py index 9f4a7fc8875..42c5d8bc53f 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -8,6 +8,8 @@ EMPTY_PARAMETERSET_OPTION, ) +ignore_markinfo = pytest.mark.filterwarnings('ignore:MarkInfo objects:_pytest.deprecated.RemovedInPytest4Warning') + class TestMark(object): def test_markinfo_repr(self): @@ -51,6 +53,7 @@ def f(): mark.hello(f) assert f.hello + @ignore_markinfo def test_pytest_mark_keywords(self): mark = Mark() @@ -62,6 +65,7 @@ def f(): 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 @@ def f(): 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 @@ def f(): 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 @@ def g(): 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 @@ def test_func(self): assert 'hello' in keywords assert 'world' in keywords + @ignore_markinfo def test_merging_markers(self, testdir): p = testdir.makepyfile(""" import pytest @@ -621,6 +629,7 @@ def test_func(arg): "keyword: *hello*" ]) + @ignore_markinfo def test_merging_markers_two_functions(self, testdir): p = testdir.makepyfile(""" import pytest From 99015bfc86d6ad309bdd9abbe2c0700d58ca36d1 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sat, 17 Mar 2018 22:04:22 +0100 Subject: [PATCH 06/27] fix most of metafunc tests by mocking --- _pytest/python.py | 2 +- testing/python/metafunc.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/_pytest/python.py b/_pytest/python.py index 843d7f5348c..e33de018e37 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -736,7 +736,7 @@ class Metafunc(fixtures.FuncargnamesCompatAttr): 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) + assert isinstance(definition, FunctionDefinition) or type(definition).__name__ == "DefinitionMock" self.definition = definition self.config = config diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index b65a42e09d0..9b70c330580 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- import re import sys - +import attr import _pytest._code import py import pytest @@ -24,13 +24,13 @@ class FixtureInfo(object): def __init__(self, names): self.names_closure = names + @attr.s + class DefinitionMock(object): + obj = attr.ib() + names = fixtures.getfuncargnames(func) fixtureinfo = FixtureInfo(names) - definition = python.FunctionDefinition( - name=func.__name__, - parent=None, - callobj=func, - ) + definition = DefinitionMock(func) return python.Metafunc(definition, fixtureinfo, config) def test_no_funcargs(self, testdir): From 2d06ae0f65f88ca21d3f2ad8fe51286548dafab5 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sat, 17 Mar 2018 22:19:17 +0100 Subject: [PATCH 07/27] base metafunc fixtureinfo on the functiondefinition to caputure its marks --- _pytest/python.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/_pytest/python.py b/_pytest/python.py index e33de018e37..86c3be4479f 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -367,13 +367,14 @@ def _genfunctions(self, name, funcobj): cls = clscol and clscol.obj or None transfer_markers(funcobj, cls, module) fm = self.session._fixturemanager - fixtureinfo = fm.getfixtureinfo(self, funcobj, cls) 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"): From 5e56e9b4f67185bf52f851ea5d1f5829e01f804c Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sun, 18 Mar 2018 16:52:43 +0100 Subject: [PATCH 08/27] refactor node markers, remove need to be aware of nodes --- _pytest/mark/structures.py | 12 +++--------- _pytest/nodes.py | 6 ++++-- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/_pytest/mark/structures.py b/_pytest/mark/structures.py index b42a665517a..3236c78289b 100644 --- a/_pytest/mark/structures.py +++ b/_pytest/mark/structures.py @@ -369,13 +369,8 @@ def __repr__(self): @attr.s(cmp=False, hash=False) class NodeMarkers(object): - node = attr.ib(repr=False) own_markers = attr.ib(default=attr.Factory(list)) - @classmethod - def from_node(cls, node): - return cls(node=node) - def update(self, add_markers): """update the own markers """ @@ -386,7 +381,6 @@ def find(self, name): find markers in own nodes or parent nodes needs a better place """ - for node in reversed(self.node.listchain()): - for mark in node._markers.own_markers: - if mark.name == name: - yield mark + for mark in self.own_markers: + if mark.name == name: + yield mark diff --git a/_pytest/nodes.py b/_pytest/nodes.py index e7686b39fd4..bd141ae7cbb 100644 --- a/_pytest/nodes.py +++ b/_pytest/nodes.py @@ -90,7 +90,7 @@ def __init__(self, name, parent=None, config=None, session=None, fspath=None, no #: keywords/markers collected from all scopes self.keywords = NodeKeywords(self) - self._markers = NodeMarkers.from_node(self) + self._markers = NodeMarkers() #: allow adding of extra keywords to use for matching self.extra_keyword_matches = set() @@ -184,7 +184,9 @@ def add_marker(self, marker): def find_markers(self, name): """find all marks with the given name on the node and its parents""" - return self._markers.find(name) + for node in reversed(self.listchain()): + for mark in node._markers.find(name): + yield mark def get_marker(self, name): """ get a marker object from this node or None if From ced1316bc80454d461df1ce1a281844e273e7dda Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Mon, 19 Mar 2018 17:40:55 +0100 Subject: [PATCH 09/27] add docstrings for nodemarkers --- _pytest/mark/structures.py | 8 ++++++++ _pytest/nodes.py | 5 ++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/_pytest/mark/structures.py b/_pytest/mark/structures.py index 3236c78289b..9b1411acfe3 100644 --- a/_pytest/mark/structures.py +++ b/_pytest/mark/structures.py @@ -369,6 +369,14 @@ def __repr__(self): @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): diff --git a/_pytest/nodes.py b/_pytest/nodes.py index bd141ae7cbb..0e08c1c28de 100644 --- a/_pytest/nodes.py +++ b/_pytest/nodes.py @@ -183,7 +183,10 @@ def add_marker(self, marker): self._markers.update([marker]) def find_markers(self, name): - """find all marks with the given name on the node and its parents""" + """find all marks with the given name on the node and its parents + + :param str name: name of the marker + """ for node in reversed(self.listchain()): for mark in node._markers.find(name): yield mark From 775fb96ac3f86de3e53120769fd0eb72c1d9dce0 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Mon, 19 Mar 2018 17:49:23 +0100 Subject: [PATCH 10/27] first changelog entry --- changelog/3317.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/3317.feature diff --git a/changelog/3317.feature b/changelog/3317.feature new file mode 100644 index 00000000000..be4625c6ecb --- /dev/null +++ b/changelog/3317.feature @@ -0,0 +1 @@ +introduce correct per node mark handling and deprecate the always incorrect existing mark handling \ No newline at end of file From 159ea9b7c0654f7e300ec2a91d7502ce1ae45571 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Mon, 19 Mar 2018 18:39:56 +0100 Subject: [PATCH 11/27] turn Markinfo into atts clsas, and return correct instances of it from node.get_marker --- _pytest/mark/structures.py | 21 ++++++++++++++------- _pytest/nodes.py | 7 ++++--- _pytest/warnings.py | 3 +-- testing/test_mark.py | 3 ++- 4 files changed, 21 insertions(+), 13 deletions(-) diff --git a/_pytest/mark/structures.py b/_pytest/mark/structures.py index 9b1411acfe3..853d89fdd17 100644 --- a/_pytest/mark/structures.py +++ b/_pytest/mark/structures.py @@ -4,9 +4,10 @@ import inspect import attr + 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" @@ -233,7 +234,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) @@ -266,18 +267,24 @@ def _marked(func, mark): 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', 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 "".format(self.combined) @@ -288,7 +295,7 @@ def add_mark(self, mark): 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): diff --git a/_pytest/nodes.py b/_pytest/nodes.py index 0e08c1c28de..a37fa4698e9 100644 --- a/_pytest/nodes.py +++ b/_pytest/nodes.py @@ -4,12 +4,11 @@ import six import py import attr -from more_itertools import first import _pytest import _pytest._code -from _pytest.mark.structures import NodeKeywords, NodeMarkers +from _pytest.mark.structures import NodeKeywords, NodeMarkers, MarkInfo SEP = "/" @@ -194,7 +193,9 @@ def find_markers(self, name): 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. """ - return first(self.find_markers(name), None) + markers = list(self.find_markers(name)) + if markers: + return MarkInfo(markers) def listextrakeywords(self): """ Return a set of all extra keywords in self and any parents.""" diff --git a/_pytest/warnings.py b/_pytest/warnings.py index 3c2b1914fb6..8183d6d66d1 100644 --- a/_pytest/warnings.py +++ b/_pytest/warnings.py @@ -60,8 +60,7 @@ def catch_warnings_for_item(item): for arg in inifilters: _setoption(warnings, arg) - mark = item.get_marker('filterwarnings') - if mark: + for mark in item.find_markers('filterwarnings'): for arg in mark.args: warnings._setoption(arg) diff --git a/testing/test_mark.py b/testing/test_mark.py index 42c5d8bc53f..64b201577bf 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -14,7 +14,7 @@ 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']) @@ -684,6 +684,7 @@ def test_function(): reprec = testdir.inline_run() reprec.assertoutcome(passed=1) + @ignore_markinfo def test_keyword_added_for_session(self, testdir): testdir.makeconftest(""" import pytest From a92a51b01b261df5b8491c6b13784c982a5320ca Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Tue, 20 Mar 2018 15:22:28 +0100 Subject: [PATCH 12/27] clarify find_markers return value --- _pytest/nodes.py | 1 + 1 file changed, 1 insertion(+) diff --git a/_pytest/nodes.py b/_pytest/nodes.py index a37fa4698e9..4988421c43f 100644 --- a/_pytest/nodes.py +++ b/_pytest/nodes.py @@ -185,6 +185,7 @@ def find_markers(self, name): """find all marks with the given name on the node and its parents :param str name: name of the marker + :returns: iterator over marks matching the name """ for node in reversed(self.listchain()): for mark in node._markers.find(name): From 02315c04891fa04d7877761a9c85fd6ca47c49a6 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Wed, 21 Mar 2018 07:13:07 +0100 Subject: [PATCH 13/27] remove unnecessary of in the code figuring the fixture names --- _pytest/fixtures.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/_pytest/fixtures.py b/_pytest/fixtures.py index 457aa9d4f48..27c2b2c5cf4 100644 --- a/_pytest/fixtures.py +++ b/_pytest/fixtures.py @@ -985,8 +985,7 @@ def getfixtureinfo(self, node, func, cls, funcargs=True): argnames = () usefixtures = flatten(uf_mark.args for uf_mark in node.find_markers("usefixtures")) initialnames = argnames - if usefixtures is not None: - initialnames = tuple(usefixtures) + initialnames + initialnames = tuple(usefixtures) + initialnames fm = node.session._fixturemanager names_closure, arg2fixturedefs = fm.getfixtureclosure(initialnames, node) From 2cb7e725ce30d99153af0ed8cf62e534088d5179 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Wed, 21 Mar 2018 07:21:54 +0100 Subject: [PATCH 14/27] document the hack used to avoid duplicate markers due Instance collectors --- _pytest/python.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/_pytest/python.py b/_pytest/python.py index 86c3be4479f..d85f4a89e39 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -219,6 +219,7 @@ def fget(self): if obj is None: self._obj = obj = self._getobj() # XXX evil hack + # used to avoid Instance collector marker duplication if self._ALLOW_MARKERS: self._markers.update(get_unpacked_marks(self.obj)) return obj @@ -535,6 +536,9 @@ def setup(self): 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() From ee51fa58812e23ab3b26a2d11e8706d5d9b7cd67 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Mon, 26 Mar 2018 17:46:07 +0200 Subject: [PATCH 15/27] add api to iterate over all marerks of a node --- _pytest/mark/structures.py | 3 +++ _pytest/nodes.py | 15 ++++++++++++++- doc/en/mark.rst | 3 +++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/_pytest/mark/structures.py b/_pytest/mark/structures.py index 853d89fdd17..3879c9f8fd1 100644 --- a/_pytest/mark/structures.py +++ b/_pytest/mark/structures.py @@ -399,3 +399,6 @@ def find(self, name): for mark in self.own_markers: if mark.name == name: yield mark + + def __iter__(self): + return iter(self.own_markers) diff --git a/_pytest/nodes.py b/_pytest/nodes.py index 4988421c43f..5238e345dd5 100644 --- a/_pytest/nodes.py +++ b/_pytest/nodes.py @@ -1,6 +1,8 @@ from __future__ import absolute_import, division, print_function import os +from itertools import chain + import six import py import attr @@ -191,9 +193,20 @@ def find_markers(self, name): for mark in node._markers.find(name): yield mark + def iter_markers(self): + """ + iterate over all markers of the node + """ + return chain.from_iterable(x._markers for x in reversed(self.listchain())) + 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. """ + the node doesn't have a marker with that name. + + ..warning:: + + deprecated + """ markers = list(self.find_markers(name)) if markers: return MarkInfo(markers) diff --git a/doc/en/mark.rst b/doc/en/mark.rst index e4858bf8386..6c5c9c49e56 100644 --- a/doc/en/mark.rst +++ b/doc/en/mark.rst @@ -26,3 +26,6 @@ which also serve as documentation. :ref:`fixtures `. + + +.. autoclass:: Mark \ No newline at end of file From 8805036fd8596750caa588853707346235ae6bdd Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Mon, 26 Mar 2018 17:53:04 +0200 Subject: [PATCH 16/27] add node iteration apis TODO: add tests --- _pytest/nodes.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/_pytest/nodes.py b/_pytest/nodes.py index 5238e345dd5..215da0825af 100644 --- a/_pytest/nodes.py +++ b/_pytest/nodes.py @@ -2,7 +2,7 @@ import os from itertools import chain - +from operator import itemgetter import six import py import attr @@ -187,11 +187,18 @@ def find_markers(self, name): """find all marks with the given name on the node and its parents :param str name: name of the marker - :returns: iterator over marks matching the name + :returns: iterator over marks matching the name""" + return map(itemgetter(1), self.find_markers_with_node(name)) + + def find_markers_with_node(self, name): + """find all marks with the given name on the node and its parents + + :param str name: name of the marker + :returns: iterator over (node, mark) matching the name """ for node in reversed(self.listchain()): for mark in node._markers.find(name): - yield mark + yield node, mark def iter_markers(self): """ @@ -199,6 +206,15 @@ def iter_markers(self): """ return chain.from_iterable(x._markers for x in reversed(self.listchain())) + 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._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. From dbb1b5a2279775730d593622fb2a993e12960b22 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Thu, 29 Mar 2018 18:24:10 +0200 Subject: [PATCH 17/27] remove NodeMarkers, turn own_markers into a list and use iter_markers api exclusively --- _pytest/fixtures.py | 2 +- _pytest/mark/evaluate.py | 2 +- _pytest/nodes.py | 33 ++++++++------------------------- _pytest/python.py | 11 ++++++----- _pytest/skipping.py | 4 +++- _pytest/warnings.py | 7 ++++--- testing/test_mark.py | 2 +- 7 files changed, 24 insertions(+), 37 deletions(-) diff --git a/_pytest/fixtures.py b/_pytest/fixtures.py index 27c2b2c5cf4..6190dea0176 100644 --- a/_pytest/fixtures.py +++ b/_pytest/fixtures.py @@ -983,7 +983,7 @@ def getfixtureinfo(self, node, func, cls, funcargs=True): argnames = getfuncargnames(func, cls=cls) else: argnames = () - usefixtures = flatten(uf_mark.args for uf_mark in node.find_markers("usefixtures")) + usefixtures = flatten(mark.args for mark in node.iter_markers() if mark.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 19eff8e00ff..c89b4933a95 100644 --- a/_pytest/mark/evaluate.py +++ b/_pytest/mark/evaluate.py @@ -35,7 +35,7 @@ def wasvalid(self): return not hasattr(self, 'exc') def _get_marks(self): - return list(self.item.find_markers(self._mark_name)) + return [x for x in self.item.iter_markers() if x.name == self._mark_name] def invalidraise(self, exc): raises = self.get('raises') diff --git a/_pytest/nodes.py b/_pytest/nodes.py index 215da0825af..799ee078abb 100644 --- a/_pytest/nodes.py +++ b/_pytest/nodes.py @@ -1,8 +1,6 @@ from __future__ import absolute_import, division, print_function import os -from itertools import chain -from operator import itemgetter import six import py import attr @@ -10,7 +8,7 @@ import _pytest import _pytest._code -from _pytest.mark.structures import NodeKeywords, NodeMarkers, MarkInfo +from _pytest.mark.structures import NodeKeywords, MarkInfo SEP = "/" @@ -91,7 +89,9 @@ def __init__(self, name, parent=None, config=None, session=None, fspath=None, no #: keywords/markers collected from all scopes self.keywords = NodeKeywords(self) - self._markers = NodeMarkers() + + #: the marker objects belonging to this node + self.own_markers = [] #: allow adding of extra keywords to use for matching self.extra_keyword_matches = set() @@ -181,30 +181,13 @@ def add_marker(self, marker): elif not isinstance(marker, MarkDecorator): raise ValueError("is not a string or pytest.mark.* Marker") self.keywords[marker.name] = marker - self._markers.update([marker]) - - def find_markers(self, name): - """find all marks with the given name on the node and its parents - - :param str name: name of the marker - :returns: iterator over marks matching the name""" - return map(itemgetter(1), self.find_markers_with_node(name)) - - def find_markers_with_node(self, name): - """find all marks with the given name on the node and its parents - - :param str name: name of the marker - :returns: iterator over (node, mark) matching the name - """ - for node in reversed(self.listchain()): - for mark in node._markers.find(name): - yield node, mark + self.own_markers.append(marker) def iter_markers(self): """ iterate over all markers of the node """ - return chain.from_iterable(x._markers for x in reversed(self.listchain())) + return (x[1] for x in self.iter_markers_with_node()) def iter_markers_with_node(self): """ @@ -212,7 +195,7 @@ def iter_markers_with_node(self): returns sequence of tuples (node, mark) """ for node in reversed(self.listchain()): - for mark in node._markers: + for mark in node.own_markers: yield node, mark def get_marker(self, name): @@ -223,7 +206,7 @@ def get_marker(self, name): deprecated """ - markers = list(self.find_markers(name)) + markers = [x for x in self.iter_markers() if x.name == name] if markers: return MarkInfo(markers) diff --git a/_pytest/python.py b/_pytest/python.py index d85f4a89e39..5b3e0138f38 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -117,8 +117,9 @@ 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.find_markers('parametrize'): - metafunc.parametrize(*marker.args, **marker.kwargs) + for marker in metafunc.definition.iter_markers(): + if marker.name == 'parametrize': + metafunc.parametrize(*marker.args, **marker.kwargs) def pytest_configure(config): @@ -221,7 +222,7 @@ def fget(self): # XXX evil hack # used to avoid Instance collector marker duplication if self._ALLOW_MARKERS: - self._markers.update(get_unpacked_marks(self.obj)) + self.own_markers.extend(get_unpacked_marks(self.obj)) return obj def fset(self, value): @@ -1132,7 +1133,7 @@ def __init__(self, name, parent, args=None, config=None, self.obj = callobj self.keywords.update(self.obj.__dict__) - self._markers.update(get_unpacked_marks(self.obj)) + self.own_markers.extend(get_unpacked_marks(self.obj)) if callspec: self.callspec = callspec # this is total hostile and a mess @@ -1142,7 +1143,7 @@ def __init__(self, name, parent, args=None, config=None, # feel free to cry, this was broken for years before # and keywords cant fix it per design self.keywords[mark.name] = mark - self._markers.update(callspec.marks) + self.own_markers.extend(callspec.marks) if keywords: self.keywords.update(keywords) diff --git a/_pytest/skipping.py b/_pytest/skipping.py index 318c8795e4e..f62edcf9a0a 100644 --- a/_pytest/skipping.py +++ b/_pytest/skipping.py @@ -64,7 +64,9 @@ def pytest_runtest_setup(item): item._skipped_by_mark = True skip(eval_skipif.getexplanation()) - for skip_info in item.find_markers('skip'): + 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']) diff --git a/_pytest/warnings.py b/_pytest/warnings.py index 8183d6d66d1..d8b9fc460d5 100644 --- a/_pytest/warnings.py +++ b/_pytest/warnings.py @@ -60,9 +60,10 @@ def catch_warnings_for_item(item): for arg in inifilters: _setoption(warnings, arg) - for mark in item.find_markers('filterwarnings'): - for arg in mark.args: - warnings._setoption(arg) + for mark in item.iter_markers(): + if mark.name == 'filterwarnings': + for arg in mark.args: + warnings._setoption(arg) yield diff --git a/testing/test_mark.py b/testing/test_mark.py index 64b201577bf..9ec1ce75a99 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -533,7 +533,7 @@ def test_d(self): items, rec = testdir.inline_genitems(p) for item in items: print(item, item.keywords) - assert list(item.find_markers('a')) + 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(""" From 802da781c62519b26f1a9d0ad9ac6adcc331222c Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Fri, 30 Mar 2018 07:50:12 +0200 Subject: [PATCH 18/27] fix method reference to iter_markers in warning --- _pytest/deprecated.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_pytest/deprecated.py b/_pytest/deprecated.py index be4a5a080c7..2be6b7300d4 100644 --- a/_pytest/deprecated.py +++ b/_pytest/deprecated.py @@ -33,7 +33,7 @@ class RemovedInPytest4Warning(DeprecationWarning): MARK_INFO_ATTRIBUTE = RemovedInPytest4Warning( "MarkInfo objects are deprecated as they contain the merged marks.\n" - "Please use node.find_markers to iterate over markers correctly" + "Please use node.iter_markers to iterate over markers correctly" ) MARK_PARAMETERSET_UNPACKING = RemovedInPytest4Warning( From e4a52c1795c068a8b113578429532f9e5103a835 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Fri, 30 Mar 2018 08:19:45 +0200 Subject: [PATCH 19/27] prevent doubleing of function level marks --- _pytest/python.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/_pytest/python.py b/_pytest/python.py index 5b3e0138f38..94f83a37dd5 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -1122,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, From 7454a381e2f619f32579fb31ed8bb1ed4ed2604c Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Fri, 30 Mar 2018 08:36:58 +0200 Subject: [PATCH 20/27] update configuration examples to new mark api --- doc/en/example/markers.rst | 36 ++++++++++++++++-------------------- doc/en/example/simple.rst | 2 +- doc/en/usage.rst | 8 ++++---- doc/en/warnings.rst | 18 +++++++++--------- 4 files changed, 30 insertions(+), 34 deletions(-) diff --git a/doc/en/example/markers.rst b/doc/en/example/markers.rst index 7b75c790035..b162c938cbb 100644 --- a/doc/en/example/markers.rst +++ b/doc/en/example/markers.rst @@ -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=(,) kwars={} + Mark(name='my_marker', args=(,), 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): - plat = sys.platform - if not item.get_marker(plat): - if ALL.intersection(item.keywords): - pytest.skip("cannot run on platform %s" %(plat)) + supported_platforms = ALL.intersection(mark.name for mark in item.iter_markers()) + plat = sys.platform + 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 ==================== diff --git a/doc/en/example/simple.rst b/doc/en/example/simple.rst index 25d1225b55f..3dc94201898 100644 --- a/doc/en/example/simple.rst +++ b/doc/en/example/simple.rst @@ -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 diff --git a/doc/en/usage.rst b/doc/en/usage.rst index 7274dccc965..9b6db82c517 100644 --- a/doc/en/usage.rst +++ b/doc/en/usage.rst @@ -260,10 +260,10 @@ 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: - test_id = marker.args[0] - item.user_properties.append(('test_id', test_id)) + for marker in item.iter_markers(): + if marker.name == 'test_id': + test_id = marker.args[0] + item.user_properties.append(('test_id', test_id)) And in your tests: diff --git a/doc/en/warnings.rst b/doc/en/warnings.rst index e78a6afc05a..f7b67f5f2a2 100644 --- a/doc/en/warnings.rst +++ b/doc/en/warnings.rst @@ -25,14 +25,14 @@ Running pytest now produces this output:: platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR, inifile: collected 1 item - + test_show_warnings.py . [100%] - + ============================= warnings summary ============================= test_show_warnings.py::test_one $REGENDOC_TMPDIR/test_show_warnings.py:4: UserWarning: api v1, should use functions from v2 warnings.warn(UserWarning("api v1, should use functions from v2")) - + -- Docs: http://doc.pytest.org/en/latest/warnings.html =================== 1 passed, 1 warnings in 0.12 seconds =================== @@ -45,17 +45,17 @@ them into errors:: F [100%] ================================= FAILURES ================================= _________________________________ test_one _________________________________ - + def test_one(): > assert api_v1() == 1 - - test_show_warnings.py:8: - _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ - + + test_show_warnings.py:8: + _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + def api_v1(): > warnings.warn(UserWarning("api v1, should use functions from v2")) E UserWarning: api v1, should use functions from v2 - + test_show_warnings.py:4: UserWarning 1 failed in 0.12 seconds From a2974dd06795b783ebcdd68625d0ff953df352c7 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Fri, 30 Mar 2018 16:48:27 +0200 Subject: [PATCH 21/27] fix doc building --- doc/en/mark.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/en/mark.rst b/doc/en/mark.rst index 6c5c9c49e56..24d9b1cdcb5 100644 --- a/doc/en/mark.rst +++ b/doc/en/mark.rst @@ -27,5 +27,5 @@ which also serve as documentation. - +.. currentmodule:: _pytest.mark.structures .. autoclass:: Mark \ No newline at end of file From 1fcadeb2ce443b3166320a38740834f00fbb3432 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Thu, 5 Apr 2018 15:30:31 +0200 Subject: [PATCH 22/27] extend marker docs with reasons on marker iteration --- _pytest/mark/structures.py | 16 +++++++++++++--- doc/en/mark.rst | 32 +++++++++++++++++++++++++++++++- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/_pytest/mark/structures.py b/_pytest/mark/structures.py index 3879c9f8fd1..451614d925f 100644 --- a/_pytest/mark/structures.py +++ b/_pytest/mark/structures.py @@ -114,11 +114,21 @@ def _for_parametrize(cls, argnames, argvalues, func, config): @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, diff --git a/doc/en/mark.rst b/doc/en/mark.rst index 24d9b1cdcb5..1ce716f8e6f 100644 --- a/doc/en/mark.rst +++ b/doc/en/mark.rst @@ -27,5 +27,35 @@ which also serve as documentation. + + .. currentmodule:: _pytest.mark.structures -.. autoclass:: Mark \ No newline at end of file +.. autoclass:: Mark + :members: + + + + + +.. `marker-iteration` + +Marker iteration +================= + +.. versionadded:: 3.6 + +A new api to access markers was introduced in order to elevate the inherent design mistakes +which accumulated over the evolution of markers from simply updating the ``__dict__`` attribute of functions to something more powerful. + +At the end of this evolution Markers would unintenedly pass along in class hierachies and the api for retriving them was inconsistent, +as markers from parameterization would store differently than markers from objects and markers added via ``node.add_marker`` + +This in turnd made it technically next to impossible to use the data of markers correctly without having a deep understanding of the broken internals. + +The new api is provides :func:`_pytest.nodes.Node.iter_markers` on :py:class:`_pytest.nodes.node` method to iterate over markers in a consistent manner. + +.. warning:: + + 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` + From 48bcc3419f3c69e6646038e008a6410f094968df Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 5 Apr 2018 12:39:21 -0300 Subject: [PATCH 23/27] Reword the docs on markers a bit --- doc/en/mark.rst | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/doc/en/mark.rst b/doc/en/mark.rst index 1ce716f8e6f..bf1584bf060 100644 --- a/doc/en/mark.rst +++ b/doc/en/mark.rst @@ -26,17 +26,11 @@ which also serve as documentation. :ref:`fixtures `. - - - .. currentmodule:: _pytest.mark.structures .. autoclass:: Mark :members: - - - .. `marker-iteration` Marker iteration @@ -44,17 +38,13 @@ Marker iteration .. versionadded:: 3.6 -A new api to access markers was introduced in order to elevate the inherent design mistakes -which accumulated over the evolution of markers from simply updating the ``__dict__`` attribute of functions to something more powerful. +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``. -At the end of this evolution Markers would unintenedly pass along in class hierachies and the api for retriving them was inconsistent, -as markers from parameterization would store differently than markers from objects and markers added via ``node.add_marker`` +This state of things made it technically next to impossible to use data from markers correctly (``args`` and ``kwargs``) without having a deep understanding of the internals, leading to subtle and hard to understand bugs in more advanced usages. -This in turnd made it technically next to impossible to use the data of markers correctly without having a deep understanding of the broken internals. +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. -The new api is provides :func:`_pytest.nodes.Node.iter_markers` on :py:class:`_pytest.nodes.node` method to iterate over markers in a consistent manner. - -.. warning:: +.. 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` From a8ad89cdb3973537d9f75a33c0c6a96c760b4a69 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Thu, 5 Apr 2018 18:39:57 +0200 Subject: [PATCH 24/27] fix documentation references --- doc/en/reference.rst | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/doc/en/reference.rst b/doc/en/reference.rst index d87ec28f3ac..5af2ee903b2 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -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 ~~~~~~~~ From 3582e1f6bedf5007a8936de71f5865d9a2192822 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Thu, 5 Apr 2018 18:46:33 +0200 Subject: [PATCH 25/27] include more detail on the marker api issues --- doc/en/mark.rst | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/doc/en/mark.rst b/doc/en/mark.rst index bf1584bf060..2b84037bff2 100644 --- a/doc/en/mark.rst +++ b/doc/en/mark.rst @@ -40,7 +40,16 @@ Marker iteration 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 (``args`` and ``kwargs``) without having a deep understanding of the internals, leading to subtle and hard to understand bugs in more advanced usages. +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 siebling classes, +MarkDecroators when marks came from parameterization or from a `add_marker` call, while 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. From e534cc81a353b178b017f070dbe421f563e604a0 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 5 Apr 2018 14:45:30 -0300 Subject: [PATCH 26/27] Fix typos in docs --- doc/en/mark.rst | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/doc/en/mark.rst b/doc/en/mark.rst index 2b84037bff2..72c597e5068 100644 --- a/doc/en/mark.rst +++ b/doc/en/mark.rst @@ -42,14 +42,11 @@ pytest's marker implementation traditionally worked by simply updating the ``__d 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 siebling classes, -MarkDecroators when marks came from parameterization or from a `add_marker` call, while 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. +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 - +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. From 4df8f2b153339387f13949c37a40ff86073bda70 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Fri, 6 Apr 2018 08:13:28 +0200 Subject: [PATCH 27/27] fix doc build, use noindex on the mark reference --- doc/en/mark.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/en/mark.rst b/doc/en/mark.rst index 72c597e5068..b08788254ea 100644 --- a/doc/en/mark.rst +++ b/doc/en/mark.rst @@ -29,6 +29,7 @@ which also serve as documentation. .. currentmodule:: _pytest.mark.structures .. autoclass:: Mark :members: + :noindex: .. `marker-iteration`