From bdec2c8f9e1b39914b5e9fbc6d73adc29cf88f3a Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Thu, 22 Jun 2017 08:45:10 +0200 Subject: [PATCH 1/8] move marker transfer to _pytest.mark --- _pytest/mark.py | 29 +++++++++++++++++++++++++++++ _pytest/python.py | 31 +------------------------------ 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/_pytest/mark.py b/_pytest/mark.py index 8b40a4f6e..928690d07 100644 --- a/_pytest/mark.py +++ b/_pytest/mark.py @@ -389,3 +389,32 @@ class MarkInfo(object): 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): + # 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) diff --git a/_pytest/python.py b/_pytest/python.py index 1a313a59e..c5af9106a 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -23,7 +23,7 @@ from _pytest.compat import ( 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() cutdir3 = py.path.local(py.__file__).dirpath() @@ -361,35 +361,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): """ Collector for test classes and functions. """ From 64ae6ae25dba3169529ea7ed56d65b749314599b Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Thu, 22 Jun 2017 10:41:28 +0200 Subject: [PATCH 2/8] extract application of marks and legacy markinfos --- _pytest/mark.py | 70 ++++++++++++++++++++++++++++--------------------- 1 file changed, 40 insertions(+), 30 deletions(-) diff --git a/_pytest/mark.py b/_pytest/mark.py index 928690d07..4a3694049 100644 --- a/_pytest/mark.py +++ b/_pytest/mark.py @@ -8,6 +8,7 @@ from .compat import imap def alias(name): + # todo: introduce deprecationwarnings return property(attrgetter(name), doc='alias for ' + name) @@ -329,30 +330,39 @@ class MarkDecorator: 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] + apply_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) + apply_legacy_mark(func, self.mark) return func mark = Mark(self.name, args, kwargs) return self.__class__(self.mark.combined_with(mark)) +def apply_mark(obj, mark): + assert isinstance(mark, Mark), mark + """applies a marker to an object, + makrer transfers only update legacy markinfo objects + """ + mark_list = getattr(obj, '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 + [mark] + obj.pytestmark = mark_list + + +def apply_legacy_mark(func, mark): + 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')): @@ -404,17 +414,17 @@ def _marked(func, mark): 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) + """ + transfer legacy markers to the function level marminfo objects + this one is a major fsckup for mark breakages + """ + for obj in (cls, mod): + mark_list = getattr(obj, 'pytestmark', []) + + if not isinstance(mark_list, list): + mark_list = [mark_list] + + for mark in mark_list: + mark = getattr(mark, 'mark', mark) # unpack MarkDecorator + if not _marked(funcobj, mark): + apply_legacy_mark(funcobj, mark) From 19b12b22e7e5c23bb9a9a0e74ca36d441481973f Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Thu, 22 Jun 2017 10:48:45 +0200 Subject: [PATCH 3/8] store pristine marks on function.pytestmark fixes #2516 --- _pytest/mark.py | 1 + testing/test_mark.py | 24 +++++++++++++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/_pytest/mark.py b/_pytest/mark.py index 4a3694049..08a0bd164 100644 --- a/_pytest/mark.py +++ b/_pytest/mark.py @@ -333,6 +333,7 @@ class MarkDecorator: apply_mark(func, self.mark) else: apply_legacy_mark(func, self.mark) + apply_mark(func, self.mark) return func mark = Mark(self.name, args, kwargs) diff --git a/testing/test_mark.py b/testing/test_mark.py index 0792b04fd..dff04f407 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -3,7 +3,7 @@ import os 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): @@ -772,3 +772,25 @@ class TestKeywordSelection(object): 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] \ No newline at end of file From c791895c93308699458cdf73fd647b92ed1779c0 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Thu, 22 Jun 2017 10:50:46 +0200 Subject: [PATCH 4/8] changelog addition --- changelog/2516.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/2516.feature diff --git a/changelog/2516.feature b/changelog/2516.feature new file mode 100644 index 000000000..35b9ebae0 --- /dev/null +++ b/changelog/2516.feature @@ -0,0 +1 @@ +store unmeshed marks on functions pytestmark attribute From 1d926011a4d684c4bd505a3e7ecc444f43cdc446 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Thu, 22 Jun 2017 15:12:50 +0200 Subject: [PATCH 5/8] add deprecation warnings for using markinfo attributes --- _pytest/deprecated.py | 8 ++++++++ _pytest/mark.py | 18 ++++++++++++------ 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/_pytest/deprecated.py b/_pytest/deprecated.py index e75ff099e..44af1b48e 100644 --- a/_pytest/deprecated.py +++ b/_pytest/deprecated.py @@ -7,6 +7,11 @@ be removed when the time comes. """ from __future__ import absolute_import, division, print_function + +class RemovedInPytest4_0Warning(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.' @@ -22,3 +27,6 @@ SETUP_CFG_PYTEST = '[pytest] section in setup.cfg files is deprecated, use [tool 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 = RemovedInPytest4_0Warning( + "Markinfo attributes are deprecated, please iterate the mark Collection") \ No newline at end of file diff --git a/_pytest/mark.py b/_pytest/mark.py index 08a0bd164..a3f84867e 100644 --- a/_pytest/mark.py +++ b/_pytest/mark.py @@ -2,14 +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): - # todo: introduce deprecationwarnings - 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')): @@ -382,9 +388,9 @@ class MarkInfo(object): 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 "".format(self.combined) From 23d016f1149812babbaf42da3f6c21acd70597c2 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Fri, 23 Jun 2017 11:05:38 +0200 Subject: [PATCH 6/8] address review comments * enhance api for fetching marks off an object * rename functions for storing marks * enhance deprecation message for MarkInfo --- _pytest/deprecated.py | 9 ++++---- _pytest/mark.py | 54 +++++++++++++++++++++++++------------------ 2 files changed, 36 insertions(+), 27 deletions(-) diff --git a/_pytest/deprecated.py b/_pytest/deprecated.py index 44af1b48e..c5aedd0c9 100644 --- a/_pytest/deprecated.py +++ b/_pytest/deprecated.py @@ -8,8 +8,8 @@ be removed when the time comes. from __future__ import absolute_import, division, print_function -class RemovedInPytest4_0Warning(DeprecationWarning): - "warning class for features removed in pytest 4.0" +class RemovedInPytest4Warning(DeprecationWarning): + """warning class for features removed in pytest 4.0""" MAIN_STR_ARGS = 'passing a string to pytest.main() is deprecated, ' \ @@ -28,5 +28,6 @@ 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 = RemovedInPytest4_0Warning( - "Markinfo attributes are deprecated, please iterate the mark Collection") \ No newline at end of file +MARK_INFO_ATTRIBUTE = RemovedInPytest4Warning( + "MarkInfo objects are deprecated as they contain the merged marks" +) \ No newline at end of file diff --git a/_pytest/mark.py b/_pytest/mark.py index a3f84867e..11f1e30d6 100644 --- a/_pytest/mark.py +++ b/_pytest/mark.py @@ -336,32 +336,42 @@ class MarkDecorator: is_class = inspect.isclass(func) if len(args) == 1 and (istestfunc(func) or is_class): if is_class: - apply_mark(func, self.mark) + store_mark(func, self.mark) else: - apply_legacy_mark(func, self.mark) - apply_mark(func, 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 apply_mark(obj, mark): - assert isinstance(mark, Mark), mark - """applies a marker to an object, - makrer transfers only update legacy markinfo objects +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] - # always work on a copy to avoid updating pytestmark - # from a superclass by accident - mark_list = mark_list + [mark] - obj.pytestmark = mark_list + return [ + getattr(mark, 'mark', mark) # unpack MarkDecorator + for mark in mark_list + ] -def apply_legacy_mark(func, mark): +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) @@ -422,16 +432,14 @@ def _marked(func, mark): def transfer_markers(funcobj, cls, mod): """ - transfer legacy markers to the function level marminfo objects - this one is a major fsckup for mark breakages + 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): - mark_list = getattr(obj, 'pytestmark', []) - - if not isinstance(mark_list, list): - mark_list = [mark_list] - - for mark in mark_list: - mark = getattr(mark, 'mark', mark) # unpack MarkDecorator + for mark in get_unpacked_marks(obj): if not _marked(funcobj, mark): - apply_legacy_mark(funcobj, mark) + store_legacy_markinfo(funcobj, mark) From b0b6c355f7ab17fbaccfea6f70efe03866ed5940 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Fri, 23 Jun 2017 11:09:16 +0200 Subject: [PATCH 7/8] fixup changelog, thanks Bruno --- changelog/2516.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog/2516.feature b/changelog/2516.feature index 35b9ebae0..6436de16a 100644 --- a/changelog/2516.feature +++ b/changelog/2516.feature @@ -1 +1 @@ -store unmeshed marks on functions pytestmark attribute +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. \ No newline at end of file From 8d5f2872d3aa7549299a7bfe7dfca1bd8f022bf5 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Fri, 23 Jun 2017 11:59:03 +0200 Subject: [PATCH 8/8] minor code style fix --- _pytest/python.py | 1 + 1 file changed, 1 insertion(+) diff --git a/_pytest/python.py b/_pytest/python.py index c5af9106a..e10282a8c 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -24,6 +24,7 @@ from _pytest.compat import ( ) 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() cutdir3 = py.path.local(py.__file__).dirpath()