Merge pull request #2517 from RonnyPfannschmidt/mark-expose-nontransfered

Mark expose nontransfered marks via pytestmark property
This commit is contained in:
Bruno Oliveira 2017-06-23 10:15:39 -03:00 committed by GitHub
commit bab18e10eb
5 changed files with 109 additions and 51 deletions

View File

@ -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"
)

View File

@ -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)

View File

@ -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. """

1
changelog/2516.feature Normal file
View File

@ -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.

View File

@ -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]