Skip to content

Commit

Permalink
Merge pull request #2517 from RonnyPfannschmidt/mark-expose-nontransf…
Browse files Browse the repository at this point in the history
…ered

Mark expose nontransfered marks via pytestmark property
  • Loading branch information
nicoddemus authored Jun 23, 2017
2 parents 9bd8907 + 8d5f287 commit bab18e1
Show file tree
Hide file tree
Showing 5 changed files with 109 additions and 51 deletions.
9 changes: 9 additions & 0 deletions _pytest/deprecated.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
"""
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, ' \
'pass a list of arguments instead.'

Expand All @@ -22,3 +27,7 @@
GETFUNCARGVALUE = "use of getfuncargvalue is deprecated, use getfixturevalue"

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"
)
96 changes: 75 additions & 21 deletions _pytest/mark.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,20 @@
from __future__ import absolute_import, division, print_function

import inspect
import warnings
from collections import namedtuple
from operator import attrgetter
from .compat import imap
from .deprecated import MARK_INFO_ATTRIBUTE

def alias(name, warning=None):
getter = attrgetter(name)

def alias(name):
return property(attrgetter(name), doc='alias for ' + name)
def warned(self):
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')):
Expand Down Expand Up @@ -329,31 +336,51 @@ def __call__(self, *args, **kwargs):
is_class = inspect.isclass(func)
if len(args) == 1 and (istestfunc(func) or is_class):
if is_class:
if hasattr(func, 'pytestmark'):
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]
store_mark(func, self.mark)
else:
holder = getattr(func, self.name, None)
if holder is None:
holder = MarkInfo(self.mark)
setattr(func, self.name, holder)
else:
holder.add_mark(self.mark)
store_legacy_markinfo(func, self.mark)
store_mark(func, self.mark)
return func

mark = Mark(self.name, args, kwargs)
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')):

Expand All @@ -371,9 +398,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 "<MarkInfo {0!r}>".format(self.combined)
Expand All @@ -389,3 +416,30 @@ def __iter__(self):


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)
30 changes: 1 addition & 29 deletions _pytest/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
safe_str, getlocation, enum,
)
from _pytest.runner import fail
from _pytest.mark import transfer_markers

cutdir1 = py.path.local(pluggy.__file__.rstrip("oc"))
cutdir2 = py.path.local(_pytest.__file__).dirpath()
Expand Down Expand Up @@ -361,35 +362,6 @@ def _genfunctions(self, name, funcobj):
)


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):
""" Collector for test classes and functions. """

Expand Down
1 change: 1 addition & 0 deletions changelog/2516.feature
Original file line number Diff line number Diff line change
@@ -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.
24 changes: 23 additions & 1 deletion testing/test_mark.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import sys

import pytest
from _pytest.mark import MarkGenerator as Mark, ParameterSet
from _pytest.mark import MarkGenerator as Mark, ParameterSet, transfer_markers

class TestMark(object):
def test_markinfo_repr(self):
Expand Down Expand Up @@ -772,3 +772,25 @@ def assert_test_is_not_selected(keyword):
def test_parameterset_extractfrom(argval, expected):
extracted = ParameterSet.extract_from(argval)
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]

0 comments on commit bab18e1

Please sign in to comment.