Merge pull request #2517 from RonnyPfannschmidt/mark-expose-nontransfered
Mark expose nontransfered marks via pytestmark property
This commit is contained in:
commit
bab18e10eb
|
@ -7,6 +7,11 @@ be removed when the time comes.
|
||||||
"""
|
"""
|
||||||
from __future__ import absolute_import, division, print_function
|
from __future__ import absolute_import, division, print_function
|
||||||
|
|
||||||
|
|
||||||
|
class RemovedInPytest4Warning(DeprecationWarning):
|
||||||
|
"""warning class for features removed in pytest 4.0"""
|
||||||
|
|
||||||
|
|
||||||
MAIN_STR_ARGS = 'passing a string to pytest.main() is deprecated, ' \
|
MAIN_STR_ARGS = 'passing a string to pytest.main() is deprecated, ' \
|
||||||
'pass a list of arguments instead.'
|
'pass a list of arguments instead.'
|
||||||
|
|
||||||
|
@ -22,3 +27,7 @@ SETUP_CFG_PYTEST = '[pytest] section in setup.cfg files is deprecated, use [tool
|
||||||
GETFUNCARGVALUE = "use of getfuncargvalue is deprecated, use getfixturevalue"
|
GETFUNCARGVALUE = "use of getfuncargvalue is deprecated, use getfixturevalue"
|
||||||
|
|
||||||
RESULT_LOG = '--result-log is deprecated and scheduled for removal in pytest 4.0'
|
RESULT_LOG = '--result-log is deprecated and scheduled for removal in pytest 4.0'
|
||||||
|
|
||||||
|
MARK_INFO_ATTRIBUTE = RemovedInPytest4Warning(
|
||||||
|
"MarkInfo objects are deprecated as they contain the merged marks"
|
||||||
|
)
|
|
@ -2,13 +2,20 @@
|
||||||
from __future__ import absolute_import, division, print_function
|
from __future__ import absolute_import, division, print_function
|
||||||
|
|
||||||
import inspect
|
import inspect
|
||||||
|
import warnings
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
from .compat import imap
|
from .compat import imap
|
||||||
|
from .deprecated import MARK_INFO_ATTRIBUTE
|
||||||
|
|
||||||
|
def alias(name, warning=None):
|
||||||
|
getter = attrgetter(name)
|
||||||
|
|
||||||
def alias(name):
|
def warned(self):
|
||||||
return property(attrgetter(name), doc='alias for ' + name)
|
warnings.warn(warning, stacklevel=2)
|
||||||
|
return getter(self)
|
||||||
|
|
||||||
|
return property(getter if warning is None else warned, doc='alias for ' + name)
|
||||||
|
|
||||||
|
|
||||||
class ParameterSet(namedtuple('ParameterSet', 'values, marks, id')):
|
class ParameterSet(namedtuple('ParameterSet', 'values, marks, id')):
|
||||||
|
@ -329,30 +336,50 @@ class MarkDecorator:
|
||||||
is_class = inspect.isclass(func)
|
is_class = inspect.isclass(func)
|
||||||
if len(args) == 1 and (istestfunc(func) or is_class):
|
if len(args) == 1 and (istestfunc(func) or is_class):
|
||||||
if is_class:
|
if is_class:
|
||||||
if hasattr(func, 'pytestmark'):
|
store_mark(func, self.mark)
|
||||||
mark_list = func.pytestmark
|
|
||||||
if not isinstance(mark_list, list):
|
|
||||||
mark_list = [mark_list]
|
|
||||||
# always work on a copy to avoid updating pytestmark
|
|
||||||
# from a superclass by accident
|
|
||||||
mark_list = mark_list + [self]
|
|
||||||
func.pytestmark = mark_list
|
|
||||||
else:
|
|
||||||
func.pytestmark = [self]
|
|
||||||
else:
|
else:
|
||||||
holder = getattr(func, self.name, None)
|
store_legacy_markinfo(func, self.mark)
|
||||||
if holder is None:
|
store_mark(func, self.mark)
|
||||||
holder = MarkInfo(self.mark)
|
|
||||||
setattr(func, self.name, holder)
|
|
||||||
else:
|
|
||||||
holder.add_mark(self.mark)
|
|
||||||
return func
|
return func
|
||||||
|
|
||||||
mark = Mark(self.name, args, kwargs)
|
mark = Mark(self.name, args, kwargs)
|
||||||
return self.__class__(self.mark.combined_with(mark))
|
return self.__class__(self.mark.combined_with(mark))
|
||||||
|
|
||||||
|
def get_unpacked_marks(obj):
|
||||||
|
"""
|
||||||
|
obtain the unpacked marks that are stored on a object
|
||||||
|
"""
|
||||||
|
mark_list = getattr(obj, 'pytestmark', [])
|
||||||
|
|
||||||
|
if not isinstance(mark_list, list):
|
||||||
|
mark_list = [mark_list]
|
||||||
|
return [
|
||||||
|
getattr(mark, 'mark', mark) # unpack MarkDecorator
|
||||||
|
for mark in mark_list
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def store_mark(obj, mark):
|
||||||
|
"""store a Mark on a object
|
||||||
|
this is used to implement the Mark declarations/decorators correctly
|
||||||
|
"""
|
||||||
|
assert isinstance(mark, Mark), mark
|
||||||
|
# always reassign name to avoid updating pytestmark
|
||||||
|
# in a referene that was only borrowed
|
||||||
|
obj.pytestmark = get_unpacked_marks(obj) + [mark]
|
||||||
|
|
||||||
|
|
||||||
|
def store_legacy_markinfo(func, mark):
|
||||||
|
"""create the legacy MarkInfo objects and put them onto the function
|
||||||
|
"""
|
||||||
|
if not isinstance(mark, 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)
|
||||||
|
setattr(func, mark.name, holder)
|
||||||
|
else:
|
||||||
|
holder.add_mark(mark)
|
||||||
|
|
||||||
|
|
||||||
class Mark(namedtuple('Mark', 'name, args, kwargs')):
|
class Mark(namedtuple('Mark', 'name, args, kwargs')):
|
||||||
|
@ -371,9 +398,9 @@ class MarkInfo(object):
|
||||||
self.combined = mark
|
self.combined = mark
|
||||||
self._marks = [mark]
|
self._marks = [mark]
|
||||||
|
|
||||||
name = alias('combined.name')
|
name = alias('combined.name', warning=MARK_INFO_ATTRIBUTE)
|
||||||
args = alias('combined.args')
|
args = alias('combined.args', warning=MARK_INFO_ATTRIBUTE)
|
||||||
kwargs = alias('combined.kwargs')
|
kwargs = alias('combined.kwargs', warning=MARK_INFO_ATTRIBUTE)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<MarkInfo {0!r}>".format(self.combined)
|
return "<MarkInfo {0!r}>".format(self.combined)
|
||||||
|
@ -389,3 +416,30 @@ class MarkInfo(object):
|
||||||
|
|
||||||
|
|
||||||
MARK_GEN = MarkGenerator()
|
MARK_GEN = MarkGenerator()
|
||||||
|
|
||||||
|
|
||||||
|
def _marked(func, mark):
|
||||||
|
""" Returns True if :func: is already marked with :mark:, False otherwise.
|
||||||
|
This can happen if marker is applied to class and the test file is
|
||||||
|
invoked more than once.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
func_mark = getattr(func, mark.name)
|
||||||
|
except AttributeError:
|
||||||
|
return False
|
||||||
|
return mark.args == func_mark.args and mark.kwargs == func_mark.kwargs
|
||||||
|
|
||||||
|
|
||||||
|
def transfer_markers(funcobj, cls, mod):
|
||||||
|
"""
|
||||||
|
this function transfers class level markers and module level markers
|
||||||
|
into function level markinfo objects
|
||||||
|
|
||||||
|
this is the main reason why marks are so broken
|
||||||
|
the resolution will involve phasing out function level MarkInfo objects
|
||||||
|
|
||||||
|
"""
|
||||||
|
for obj in (cls, mod):
|
||||||
|
for mark in get_unpacked_marks(obj):
|
||||||
|
if not _marked(funcobj, mark):
|
||||||
|
store_legacy_markinfo(funcobj, mark)
|
||||||
|
|
|
@ -23,6 +23,7 @@ from _pytest.compat import (
|
||||||
safe_str, getlocation, enum,
|
safe_str, getlocation, enum,
|
||||||
)
|
)
|
||||||
from _pytest.runner import fail
|
from _pytest.runner import fail
|
||||||
|
from _pytest.mark import transfer_markers
|
||||||
|
|
||||||
cutdir1 = py.path.local(pluggy.__file__.rstrip("oc"))
|
cutdir1 = py.path.local(pluggy.__file__.rstrip("oc"))
|
||||||
cutdir2 = py.path.local(_pytest.__file__).dirpath()
|
cutdir2 = py.path.local(_pytest.__file__).dirpath()
|
||||||
|
@ -361,35 +362,6 @@ class PyCollector(PyobjMixin, main.Collector):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _marked(func, mark):
|
|
||||||
""" Returns True if :func: is already marked with :mark:, False otherwise.
|
|
||||||
This can happen if marker is applied to class and the test file is
|
|
||||||
invoked more than once.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
func_mark = getattr(func, mark.name)
|
|
||||||
except AttributeError:
|
|
||||||
return False
|
|
||||||
return mark.args == func_mark.args and mark.kwargs == func_mark.kwargs
|
|
||||||
|
|
||||||
|
|
||||||
def transfer_markers(funcobj, cls, mod):
|
|
||||||
# XXX this should rather be code in the mark plugin or the mark
|
|
||||||
# plugin should merge with the python plugin.
|
|
||||||
for holder in (cls, mod):
|
|
||||||
try:
|
|
||||||
pytestmark = holder.pytestmark
|
|
||||||
except AttributeError:
|
|
||||||
continue
|
|
||||||
if isinstance(pytestmark, list):
|
|
||||||
for mark in pytestmark:
|
|
||||||
if not _marked(funcobj, mark):
|
|
||||||
mark(funcobj)
|
|
||||||
else:
|
|
||||||
if not _marked(funcobj, pytestmark):
|
|
||||||
pytestmark(funcobj)
|
|
||||||
|
|
||||||
|
|
||||||
class Module(main.File, PyCollector):
|
class Module(main.File, PyCollector):
|
||||||
""" Collector for test classes and functions. """
|
""" Collector for test classes and functions. """
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Now test function objects have a ``pytestmark`` attribute containing a list of marks applied directly to the test function, as opposed to marks inherited from parent classes or modules.
|
|
@ -3,7 +3,7 @@ import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from _pytest.mark import MarkGenerator as Mark, ParameterSet
|
from _pytest.mark import MarkGenerator as Mark, ParameterSet, transfer_markers
|
||||||
|
|
||||||
class TestMark(object):
|
class TestMark(object):
|
||||||
def test_markinfo_repr(self):
|
def test_markinfo_repr(self):
|
||||||
|
@ -772,3 +772,25 @@ class TestKeywordSelection(object):
|
||||||
def test_parameterset_extractfrom(argval, expected):
|
def test_parameterset_extractfrom(argval, expected):
|
||||||
extracted = ParameterSet.extract_from(argval)
|
extracted = ParameterSet.extract_from(argval)
|
||||||
assert extracted == expected
|
assert extracted == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_legacy_transfer():
|
||||||
|
|
||||||
|
class FakeModule(object):
|
||||||
|
pytestmark = []
|
||||||
|
|
||||||
|
class FakeClass(object):
|
||||||
|
pytestmark = pytest.mark.nofun
|
||||||
|
|
||||||
|
@pytest.mark.fun
|
||||||
|
def fake_method(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
transfer_markers(fake_method, FakeClass, FakeModule)
|
||||||
|
|
||||||
|
# legacy marks transfer smeared
|
||||||
|
assert fake_method.nofun
|
||||||
|
assert fake_method.fun
|
||||||
|
# pristine marks dont transfer
|
||||||
|
assert fake_method.pytestmark == [pytest.mark.fun.mark]
|
Loading…
Reference in New Issue