diff --git a/_pytest/deprecated.py b/_pytest/deprecated.py index a0eec0e7d..2be6b7300 100644 --- a/_pytest/deprecated.py +++ b/_pytest/deprecated.py @@ -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( diff --git a/_pytest/fixtures.py b/_pytest/fixtures.py index 2ac340e6f..6190dea01 100644 --- a/_pytest/fixtures.py +++ b/_pytest/fixtures.py @@ -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 diff --git a/_pytest/mark/evaluate.py b/_pytest/mark/evaluate.py index 295373e17..c89b4933a 100644 --- a/_pytest/mark/evaluate.py +++ b/_pytest/mark/evaluate.py @@ -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') diff --git a/_pytest/mark/structures.py b/_pytest/mark/structures.py index c56972980..451614d92 100644 --- a/_pytest/mark/structures.py +++ b/_pytest/mark/structures.py @@ -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 "".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 "" % (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) diff --git a/_pytest/nodes.py b/_pytest/nodes.py index 97f4da602..799ee078a 100644 --- a/_pytest/nodes.py +++ b/_pytest/nodes.py @@ -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.""" diff --git a/_pytest/python.py b/_pytest/python.py index f9f17afd7..94f83a37d 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -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,12 +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)) - try: - markers = metafunc.function.parametrize - except AttributeError: - return - for marker in markers: - 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): @@ -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 diff --git a/_pytest/skipping.py b/_pytest/skipping.py index 48b837def..f62edcf9a 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,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()) + 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']) diff --git a/_pytest/warnings.py b/_pytest/warnings.py index 3c2b1914f..d8b9fc460 100644 --- a/_pytest/warnings.py +++ b/_pytest/warnings.py @@ -60,10 +60,10 @@ def catch_warnings_for_item(item): for arg in inifilters: _setoption(warnings, arg) - mark = item.get_marker('filterwarnings') - if mark: - 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/changelog/3317.feature b/changelog/3317.feature new file mode 100644 index 000000000..be4625c6e --- /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 diff --git a/doc/en/example/markers.rst b/doc/en/example/markers.rst index 7b75c7900..b162c938c 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 25d1225b5..3dc942018 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/mark.rst b/doc/en/mark.rst index e4858bf83..b08788254 100644 --- a/doc/en/mark.rst +++ b/doc/en/mark.rst @@ -26,3 +26,33 @@ which also serve as documentation. :ref:`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` + diff --git a/doc/en/reference.rst b/doc/en/reference.rst index d87ec28f3..5af2ee903 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 ~~~~~~~~ diff --git a/doc/en/usage.rst b/doc/en/usage.rst index 7274dccc9..9b6db82c5 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 e78a6afc0..f7b67f5f2 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 diff --git a/testing/python/fixture.py b/testing/python/fixture.py index 59c5266cb..c558ea3cf 100644 --- a/testing/python/fixture.py +++ b/testing/python/fixture.py @@ -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): diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index f2732ef3b..9b70c3305 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,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) diff --git a/testing/test_mark.py b/testing/test_mark.py index b4dd65634..9ec1ce75a 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -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