Skip to content

Commit bab18e1

Browse files
authored
Merge pull request #2517 from RonnyPfannschmidt/mark-expose-nontransfered
Mark expose nontransfered marks via pytestmark property
2 parents 9bd8907 + 8d5f287 commit bab18e1

File tree

5 files changed

+109
-51
lines changed

5 files changed

+109
-51
lines changed

_pytest/deprecated.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@
77
"""
88
from __future__ import absolute_import, division, print_function
99

10+
11+
class RemovedInPytest4Warning(DeprecationWarning):
12+
"""warning class for features removed in pytest 4.0"""
13+
14+
1015
MAIN_STR_ARGS = 'passing a string to pytest.main() is deprecated, ' \
1116
'pass a list of arguments instead.'
1217

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

2429
RESULT_LOG = '--result-log is deprecated and scheduled for removal in pytest 4.0'
30+
31+
MARK_INFO_ATTRIBUTE = RemovedInPytest4Warning(
32+
"MarkInfo objects are deprecated as they contain the merged marks"
33+
)

_pytest/mark.py

Lines changed: 75 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,20 @@
22
from __future__ import absolute_import, division, print_function
33

44
import inspect
5+
import warnings
56
from collections import namedtuple
67
from operator import attrgetter
78
from .compat import imap
9+
from .deprecated import MARK_INFO_ATTRIBUTE
810

11+
def alias(name, warning=None):
12+
getter = attrgetter(name)
913

10-
def alias(name):
11-
return property(attrgetter(name), doc='alias for ' + name)
14+
def warned(self):
15+
warnings.warn(warning, stacklevel=2)
16+
return getter(self)
17+
18+
return property(getter if warning is None else warned, doc='alias for ' + name)
1219

1320

1421
class ParameterSet(namedtuple('ParameterSet', 'values, marks, id')):
@@ -329,31 +336,51 @@ def __call__(self, *args, **kwargs):
329336
is_class = inspect.isclass(func)
330337
if len(args) == 1 and (istestfunc(func) or is_class):
331338
if is_class:
332-
if hasattr(func, 'pytestmark'):
333-
mark_list = func.pytestmark
334-
if not isinstance(mark_list, list):
335-
mark_list = [mark_list]
336-
# always work on a copy to avoid updating pytestmark
337-
# from a superclass by accident
338-
mark_list = mark_list + [self]
339-
func.pytestmark = mark_list
340-
else:
341-
func.pytestmark = [self]
339+
store_mark(func, self.mark)
342340
else:
343-
holder = getattr(func, self.name, None)
344-
if holder is None:
345-
holder = MarkInfo(self.mark)
346-
setattr(func, self.name, holder)
347-
else:
348-
holder.add_mark(self.mark)
341+
store_legacy_markinfo(func, self.mark)
342+
store_mark(func, self.mark)
349343
return func
350344

351345
mark = Mark(self.name, args, kwargs)
352346
return self.__class__(self.mark.combined_with(mark))
353347

348+
def get_unpacked_marks(obj):
349+
"""
350+
obtain the unpacked marks that are stored on a object
351+
"""
352+
mark_list = getattr(obj, 'pytestmark', [])
354353

354+
if not isinstance(mark_list, list):
355+
mark_list = [mark_list]
356+
return [
357+
getattr(mark, 'mark', mark) # unpack MarkDecorator
358+
for mark in mark_list
359+
]
355360

356361

362+
def store_mark(obj, mark):
363+
"""store a Mark on a object
364+
this is used to implement the Mark declarations/decorators correctly
365+
"""
366+
assert isinstance(mark, Mark), mark
367+
# always reassign name to avoid updating pytestmark
368+
# in a referene that was only borrowed
369+
obj.pytestmark = get_unpacked_marks(obj) + [mark]
370+
371+
372+
def store_legacy_markinfo(func, mark):
373+
"""create the legacy MarkInfo objects and put them onto the function
374+
"""
375+
if not isinstance(mark, Mark):
376+
raise TypeError("got {mark!r} instead of a Mark".format(mark=mark))
377+
holder = getattr(func, mark.name, None)
378+
if holder is None:
379+
holder = MarkInfo(mark)
380+
setattr(func, mark.name, holder)
381+
else:
382+
holder.add_mark(mark)
383+
357384

358385
class Mark(namedtuple('Mark', 'name, args, kwargs')):
359386

@@ -371,9 +398,9 @@ def __init__(self, mark):
371398
self.combined = mark
372399
self._marks = [mark]
373400

374-
name = alias('combined.name')
375-
args = alias('combined.args')
376-
kwargs = alias('combined.kwargs')
401+
name = alias('combined.name', warning=MARK_INFO_ATTRIBUTE)
402+
args = alias('combined.args', warning=MARK_INFO_ATTRIBUTE)
403+
kwargs = alias('combined.kwargs', warning=MARK_INFO_ATTRIBUTE)
377404

378405
def __repr__(self):
379406
return "<MarkInfo {0!r}>".format(self.combined)
@@ -389,3 +416,30 @@ def __iter__(self):
389416

390417

391418
MARK_GEN = MarkGenerator()
419+
420+
421+
def _marked(func, mark):
422+
""" Returns True if :func: is already marked with :mark:, False otherwise.
423+
This can happen if marker is applied to class and the test file is
424+
invoked more than once.
425+
"""
426+
try:
427+
func_mark = getattr(func, mark.name)
428+
except AttributeError:
429+
return False
430+
return mark.args == func_mark.args and mark.kwargs == func_mark.kwargs
431+
432+
433+
def transfer_markers(funcobj, cls, mod):
434+
"""
435+
this function transfers class level markers and module level markers
436+
into function level markinfo objects
437+
438+
this is the main reason why marks are so broken
439+
the resolution will involve phasing out function level MarkInfo objects
440+
441+
"""
442+
for obj in (cls, mod):
443+
for mark in get_unpacked_marks(obj):
444+
if not _marked(funcobj, mark):
445+
store_legacy_markinfo(funcobj, mark)

_pytest/python.py

Lines changed: 1 addition & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
safe_str, getlocation, enum,
2424
)
2525
from _pytest.runner import fail
26+
from _pytest.mark import transfer_markers
2627

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

363364

364-
def _marked(func, mark):
365-
""" Returns True if :func: is already marked with :mark:, False otherwise.
366-
This can happen if marker is applied to class and the test file is
367-
invoked more than once.
368-
"""
369-
try:
370-
func_mark = getattr(func, mark.name)
371-
except AttributeError:
372-
return False
373-
return mark.args == func_mark.args and mark.kwargs == func_mark.kwargs
374-
375-
376-
def transfer_markers(funcobj, cls, mod):
377-
# XXX this should rather be code in the mark plugin or the mark
378-
# plugin should merge with the python plugin.
379-
for holder in (cls, mod):
380-
try:
381-
pytestmark = holder.pytestmark
382-
except AttributeError:
383-
continue
384-
if isinstance(pytestmark, list):
385-
for mark in pytestmark:
386-
if not _marked(funcobj, mark):
387-
mark(funcobj)
388-
else:
389-
if not _marked(funcobj, pytestmark):
390-
pytestmark(funcobj)
391-
392-
393365
class Module(main.File, PyCollector):
394366
""" Collector for test classes and functions. """
395367

changelog/2516.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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.

testing/test_mark.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import sys
44

55
import pytest
6-
from _pytest.mark import MarkGenerator as Mark, ParameterSet
6+
from _pytest.mark import MarkGenerator as Mark, ParameterSet, transfer_markers
77

88
class TestMark(object):
99
def test_markinfo_repr(self):
@@ -772,3 +772,25 @@ def assert_test_is_not_selected(keyword):
772772
def test_parameterset_extractfrom(argval, expected):
773773
extracted = ParameterSet.extract_from(argval)
774774
assert extracted == expected
775+
776+
777+
def test_legacy_transfer():
778+
779+
class FakeModule(object):
780+
pytestmark = []
781+
782+
class FakeClass(object):
783+
pytestmark = pytest.mark.nofun
784+
785+
@pytest.mark.fun
786+
def fake_method(self):
787+
pass
788+
789+
790+
transfer_markers(fake_method, FakeClass, FakeModule)
791+
792+
# legacy marks transfer smeared
793+
assert fake_method.nofun
794+
assert fake_method.fun
795+
# pristine marks dont transfer
796+
assert fake_method.pytestmark == [pytest.mark.fun.mark]

0 commit comments

Comments
 (0)