remove most of markertransfer

keywords are still a big issue
This commit is contained in:
Ronny Pfannschmidt 2018-12-19 16:00:59 +01:00
parent 58fc918d0a
commit 9f9f6ee48b
8 changed files with 52 additions and 421 deletions

View File

@ -270,8 +270,11 @@ class PytestPluginManager(PluginManager):
opts = {}
if opts is not None:
# TODO: DeprecationWarning, people should use hookimpl
known_marks = {m.name for m in getattr(method, "pytestmark", [])}
for name in ("tryfirst", "trylast", "optionalhook", "hookwrapper"):
opts.setdefault(name, hasattr(method, name))
opts.setdefault(name, hasattr(method, name) or name in known_marks)
return opts
def parse_hookspec_opts(self, module_or_class, name):

View File

@ -1207,19 +1207,20 @@ class FixtureManager(object):
if faclist:
fixturedef = faclist[-1]
if fixturedef.params is not None:
parametrize_func = getattr(metafunc.function, "parametrize", None)
if parametrize_func is not None:
parametrize_func = parametrize_func.combined
func_params = getattr(parametrize_func, "args", [[None]])
func_kwargs = getattr(parametrize_func, "kwargs", {})
# skip directly parametrized arguments
if "argnames" in func_kwargs:
argnames = parametrize_func.kwargs["argnames"]
markers = list(metafunc.definition.iter_markers("parametrize"))
for parametrize_mark in markers:
if "argnames" in parametrize_mark.kwargs:
argnames = parametrize_mark.kwargs["argnames"]
else:
argnames = parametrize_mark.args[0]
if not isinstance(argnames, (tuple, list)):
argnames = [
x.strip() for x in argnames.split(",") if x.strip()
]
if argname in argnames:
break
else:
argnames = func_params[0]
if not isinstance(argnames, (tuple, list)):
argnames = [x.strip() for x in argnames.split(",") if x.strip()]
if argname not in func_params and argname not in argnames:
metafunc.parametrize(
argname,
fixturedef.params,

View File

@ -11,19 +11,10 @@ from .structures import Mark
from .structures import MARK_GEN
from .structures import MarkDecorator
from .structures import MarkGenerator
from .structures import MarkInfo
from .structures import ParameterSet
from .structures import transfer_markers
from _pytest.config import UsageError
__all__ = [
"Mark",
"MarkInfo",
"MarkDecorator",
"MarkGenerator",
"transfer_markers",
"get_empty_parameterset_mark",
]
__all__ = ["Mark", "MarkDecorator", "MarkGenerator", "get_empty_parameterset_mark"]
def param(*values, **kw):

View File

@ -1,18 +1,15 @@
import inspect
import warnings
from collections import namedtuple
from functools import reduce
from operator import attrgetter
import attr
import six
from six.moves import map
from ..compat import ascii_escaped
from ..compat import getfslineno
from ..compat import MappingMixin
from ..compat import NOTSET
from ..deprecated import MARK_INFO_ATTRIBUTE
from _pytest.outcomes import fail
@ -233,11 +230,7 @@ class MarkDecorator(object):
func = args[0]
is_class = inspect.isclass(func)
if len(args) == 1 and (istestfunc(func) or is_class):
if is_class:
store_mark(func, self.mark)
else:
store_legacy_markinfo(func, self.mark)
store_mark(func, self.mark)
store_mark(func, self.mark)
return func
return self.with_args(*args, **kwargs)
@ -259,7 +252,13 @@ def normalize_mark_list(mark_list):
:type mark_list: List[Union[Mark, Markdecorator]]
:rtype: List[Mark]
"""
return [getattr(mark, "mark", mark) for mark in mark_list] # unpack MarkDecorator
extracted = [
getattr(mark, "mark", mark) for mark in mark_list
] # unpack MarkDecorator
for mark in extracted:
if not isinstance(mark, Mark):
raise TypeError("got {!r} instead of Mark".format(mark))
return [x for x in extracted if isinstance(x, Mark)]
def store_mark(obj, mark):
@ -272,90 +271,6 @@ def store_mark(obj, mark):
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.for_mark(mark)
setattr(func, mark.name, holder)
elif isinstance(holder, MarkInfo):
holder.add_mark(mark)
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)
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, getattr(mark, "combined", mark).name)
except AttributeError:
return False
return any(mark == info.combined for info in func_mark)
@attr.s(repr=False)
class MarkInfo(object):
""" Marking object created by :class:`MarkDecorator` instances. """
_marks = attr.ib(converter=list)
@_marks.validator
def validate_marks(self, attribute, value):
for item in value:
if not isinstance(item, Mark):
raise ValueError(
"MarkInfo expects Mark instances, got {!r} ({!r})".format(
item, type(item)
)
)
combined = attr.ib(
repr=False,
default=attr.Factory(
lambda self: reduce(Mark.combined_with, self._marks), takes_self=True
),
)
name = alias("combined.name", warning=MARK_INFO_ATTRIBUTE)
args = alias("combined.args", warning=MARK_INFO_ATTRIBUTE)
kwargs = alias("combined.kwargs", warning=MARK_INFO_ATTRIBUTE)
@classmethod
def for_mark(cls, mark):
return cls([mark])
def __repr__(self):
return "<MarkInfo {!r}>".format(self.combined)
def add_mark(self, mark):
""" add a MarkInfo with the given args and kwargs. """
self._marks.append(mark)
self.combined = self.combined.combined_with(mark)
def __iter__(self):
""" yield MarkInfo objects each relating to a marking-call. """
return map(MarkInfo.for_mark, self._marks)
class MarkGenerator(object):
""" Factory for :class:`MarkDecorator` objects - exposed as
a ``pytest.mark`` singleton instance. Example::

View File

@ -10,7 +10,6 @@ import six
import _pytest._code
from _pytest.compat import getfslineno
from _pytest.mark.structures import MarkInfo
from _pytest.mark.structures import NodeKeywords
from _pytest.outcomes import fail
@ -211,20 +210,6 @@ class Node(object):
"""
return next(self.iter_markers(name=name), default)
def get_marker(self, name):
""" get a marker object from this node or None if
the node doesn't have a marker with that name.
.. deprecated:: 3.6
This function has been deprecated in favor of
:meth:`Node.get_closest_marker <_pytest.nodes.Node.get_closest_marker>` and
:meth:`Node.iter_markers <_pytest.nodes.Node.iter_markers>`, see :ref:`update marker code`
for more details.
"""
markers = list(self.iter_markers(name=name))
if markers:
return MarkInfo(markers)
def listextrakeywords(self):
""" Return a set of all extra keywords in self and any parents."""
extra_keywords = set()

View File

@ -41,7 +41,6 @@ from _pytest.main import FSHookProxy
from _pytest.mark import MARK_GEN
from _pytest.mark.structures import get_unpacked_marks
from _pytest.mark.structures import normalize_mark_list
from _pytest.mark.structures import transfer_markers
from _pytest.outcomes import fail
from _pytest.pathlib import parts
from _pytest.warning_types import PytestWarning
@ -125,10 +124,10 @@ def pytest_generate_tests(metafunc):
# those alternative spellings are common - raise a specific error to alert
# the user
alt_spellings = ["parameterize", "parametrise", "parameterise"]
for attr in alt_spellings:
if hasattr(metafunc.function, attr):
for mark_name in alt_spellings:
if metafunc.definition.get_closest_marker(mark_name):
msg = "{0} has '{1}' mark, spelling should be 'parametrize'"
fail(msg.format(metafunc.function.__name__, attr), pytrace=False)
fail(msg.format(metafunc.function.__name__, mark_name), pytrace=False)
for marker in metafunc.definition.iter_markers(name="parametrize"):
metafunc.parametrize(*marker.args, **marker.kwargs)
@ -385,7 +384,6 @@ class PyCollector(PyobjMixin, nodes.Collector):
module = self.getparent(Module).obj
clscol = self.getparent(Class)
cls = clscol and clscol.obj or None
transfer_markers(funcobj, cls, module)
fm = self.session._fixturemanager
definition = FunctionDefinition(name=name, parent=self, callobj=funcobj)
@ -1291,6 +1289,18 @@ class Function(FunctionMixin, nodes.Item, fixtures.FuncargnamesCompatAttr):
if keywords:
self.keywords.update(keywords)
# todo: this is a hell of a hack
self.keywords.update(
dict.fromkeys(
[
mark.name
for mark in self.iter_markers()
if mark.name not in self.keywords
],
True,
)
)
if fixtureinfo is None:
fixtureinfo = self.session._fixturemanager.getfixtureinfo(
self, self.obj, self.cls, funcargs=not self._isyieldedfunction()

View File

@ -14,8 +14,6 @@ from _pytest.outcomes import skip
from _pytest.outcomes import xfail
from _pytest.python import Class
from _pytest.python import Function
from _pytest.python import Module
from _pytest.python import transfer_markers
def pytest_pycollect_makeitem(collector, name, obj):
@ -54,14 +52,12 @@ class UnitTestCase(Class):
return
self.session._fixturemanager.parsefactories(self, unittest=True)
loader = TestLoader()
module = self.getparent(Module).obj
foundsomething = False
for name in loader.getTestCaseNames(self.obj):
x = getattr(self.obj, name)
if not getattr(x, "__test__", True):
continue
funcobj = getimfunc(x)
transfer_markers(funcobj, cls, module)
yield TestCaseFunction(name, parent=self, callobj=funcobj)
foundsomething = True

View File

@ -10,7 +10,6 @@ import six
import pytest
from _pytest.mark import EMPTY_PARAMETERSET_OPTION
from _pytest.mark import MarkGenerator as Mark
from _pytest.mark import transfer_markers
from _pytest.nodes import Collector
from _pytest.nodes import Node
from _pytest.warnings import SHOW_PYTEST_WARNINGS_ARG
@ -26,12 +25,6 @@ ignore_markinfo = pytest.mark.filterwarnings(
class TestMark(object):
def test_markinfo_repr(self):
from _pytest.mark import MarkInfo, Mark
m = MarkInfo.for_mark(Mark("hello", (1, 2), {}))
repr(m)
@pytest.mark.parametrize("attr", ["mark", "param"])
@pytest.mark.parametrize("modulename", ["py.test", "pytest"])
def test_pytest_exists_in_namespace_all(self, attr, modulename):
@ -57,105 +50,8 @@ class TestMark(object):
def test_pytest_mark_name_starts_with_underscore(self):
mark = Mark()
pytest.raises(AttributeError, getattr, mark, "_some_name")
def test_pytest_mark_bare(self):
mark = Mark()
def f():
pass
mark.hello(f)
assert f.hello
def test_mark_legacy_ignore_fail(self):
def add_attribute(func):
func.foo = 1
return func
@pytest.mark.foo
@add_attribute
def test_fun():
pass
assert test_fun.foo == 1
assert test_fun.pytestmark
@ignore_markinfo
def test_pytest_mark_keywords(self):
mark = Mark()
def f():
pass
mark.world(x=3, y=4)(f)
assert f.world
assert f.world.kwargs["x"] == 3
assert f.world.kwargs["y"] == 4
@ignore_markinfo
def test_apply_multiple_and_merge(self):
mark = Mark()
def f():
pass
mark.world
mark.world(x=3)(f)
assert f.world.kwargs["x"] == 3
mark.world(y=4)(f)
assert f.world.kwargs["x"] == 3
assert f.world.kwargs["y"] == 4
mark.world(y=1)(f)
assert f.world.kwargs["y"] == 1
assert len(f.world.args) == 0
@ignore_markinfo
def test_pytest_mark_positional(self):
mark = Mark()
def f():
pass
mark.world("hello")(f)
assert f.world.args[0] == "hello"
mark.world("world")(f)
@ignore_markinfo
def test_pytest_mark_positional_func_and_keyword(self):
mark = Mark()
def f():
raise Exception
m = mark.world(f, omega="hello")
def g():
pass
assert m(g) == g
assert g.world.args[0] is f
assert g.world.kwargs["omega"] == "hello"
@ignore_markinfo
def test_pytest_mark_reuse(self):
mark = Mark()
def f():
pass
w = mark.some
w("hello", reason="123")(f)
assert f.some.args[0] == "hello"
assert f.some.kwargs["reason"] == "123"
def g():
pass
w("world", reason2="456")(g)
assert g.some.args[0] == "world"
assert "reason" not in g.some.kwargs
assert g.some.kwargs["reason2"] == "456"
with pytest.raises(AttributeError):
mark._some_name
def test_marked_class_run_twice(testdir, request):
@ -505,116 +401,6 @@ def test_parametrized_with_kwargs(testdir):
class TestFunctional(object):
def test_mark_per_function(self, testdir):
p = testdir.makepyfile(
"""
import pytest
@pytest.mark.hello
def test_hello():
assert hasattr(test_hello, 'hello')
"""
)
result = testdir.runpytest(p)
result.stdout.fnmatch_lines(["*1 passed*"])
def test_mark_per_module(self, testdir):
item = testdir.getitem(
"""
import pytest
pytestmark = pytest.mark.hello
def test_func():
pass
"""
)
keywords = item.keywords
assert "hello" in keywords
def test_marklist_per_class(self, testdir):
item = testdir.getitem(
"""
import pytest
class TestClass(object):
pytestmark = [pytest.mark.hello, pytest.mark.world]
def test_func(self):
assert TestClass.test_func.hello
assert TestClass.test_func.world
"""
)
keywords = item.keywords
assert "hello" in keywords
def test_marklist_per_module(self, testdir):
item = testdir.getitem(
"""
import pytest
pytestmark = [pytest.mark.hello, pytest.mark.world]
class TestClass(object):
def test_func(self):
assert TestClass.test_func.hello
assert TestClass.test_func.world
"""
)
keywords = item.keywords
assert "hello" in keywords
assert "world" in keywords
def test_mark_per_class_decorator(self, testdir):
item = testdir.getitem(
"""
import pytest
@pytest.mark.hello
class TestClass(object):
def test_func(self):
assert TestClass.test_func.hello
"""
)
keywords = item.keywords
assert "hello" in keywords
def test_mark_per_class_decorator_plus_existing_dec(self, testdir):
item = testdir.getitem(
"""
import pytest
@pytest.mark.hello
class TestClass(object):
pytestmark = pytest.mark.world
def test_func(self):
assert TestClass.test_func.hello
assert TestClass.test_func.world
"""
)
keywords = item.keywords
assert "hello" in keywords
assert "world" in keywords
@ignore_markinfo
def test_merging_markers(self, testdir):
p = testdir.makepyfile(
"""
import pytest
pytestmark = pytest.mark.hello("pos1", x=1, y=2)
class TestClass(object):
# classlevel overrides module level
pytestmark = pytest.mark.hello(x=3)
@pytest.mark.hello("pos0", z=4)
def test_func(self):
pass
"""
)
items, rec = testdir.inline_genitems(p)
item, = items
keywords = item.keywords
marker = keywords["hello"]
assert marker.args == ("pos0", "pos1")
assert marker.kwargs == {"x": 1, "y": 2, "z": 4}
# test the new __iter__ interface
values = list(marker)
assert len(values) == 3
assert values[0].args == ("pos0",)
assert values[1].args == ()
assert values[2].args == ("pos1",)
def test_merging_markers_deep(self, testdir):
# issue 199 - propagate markers into nested classes
p = testdir.makepyfile(
@ -677,11 +463,6 @@ class TestFunctional(object):
items, rec = testdir.inline_genitems(p)
base_item, sub_item, sub_item_other = items
print(items, [x.nodeid for x in items])
# legacy api smears
assert hasattr(base_item.obj, "b")
assert hasattr(sub_item_other.obj, "b")
assert hasattr(sub_item.obj, "b")
# new api seregates
assert not list(base_item.iter_markers(name="b"))
assert not list(sub_item_other.iter_markers(name="b"))
@ -767,26 +548,6 @@ class TestFunctional(object):
result = testdir.runpytest()
result.stdout.fnmatch_lines(["keyword: *hello*"])
@ignore_markinfo
def test_merging_markers_two_functions(self, testdir):
p = testdir.makepyfile(
"""
import pytest
@pytest.mark.hello("pos1", z=4)
@pytest.mark.hello("pos0", z=3)
def test_func():
pass
"""
)
items, rec = testdir.inline_genitems(p)
item, = items
keywords = item.keywords
marker = keywords["hello"]
values = list(marker)
assert len(values) == 2
assert values[0].args == ("pos0",)
assert values[1].args == ("pos1",)
def test_no_marker_match_on_unmarked_names(self, testdir):
p = testdir.makepyfile(
"""
@ -860,7 +621,7 @@ class TestFunctional(object):
assert "mark2" in request.keywords
assert "mark3" in request.keywords
assert 10 not in request.keywords
marker = request.node.get_marker("mark1")
marker = request.node.get_closest_marker("mark1")
assert marker.name == "mark1"
assert marker.args == ()
assert marker.kwargs == {}
@ -876,15 +637,11 @@ class TestFunctional(object):
.. note:: this could be moved to ``testdir`` if proven to be useful
to other modules.
"""
from _pytest.mark import MarkInfo
items = {x.name: x for x in items}
for name, expected_markers in expected.items():
markers = items[name].keywords._markers
marker_names = {
name for (name, v) in markers.items() if isinstance(v, MarkInfo)
}
assert marker_names == set(expected_markers)
markers = {m.name for m in items[name].iter_markers()}
assert markers == set(expected_markers)
@pytest.mark.issue1540
@pytest.mark.filterwarnings("ignore")
@ -1043,26 +800,6 @@ class TestKeywordSelection(object):
assert_test_is_not_selected("()")
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]
class TestMarkDecorator(object):
@pytest.mark.parametrize(
"lhs, rhs, expected",
@ -1163,19 +900,12 @@ def test_mark_expressions_no_smear(testdir):
deselected_tests = dlist[0].items
assert len(deselected_tests) == 1
# todo: fixed
# keywords smear - expected behaviour
reprec_keywords = testdir.inline_run("-k", "FOO")
passed_k, skipped_k, failed_k = reprec_keywords.countoutcomes()
assert passed_k == 2
assert skipped_k == failed_k == 0
def test_addmarker_getmarker():
node = Node("Test", config=mock.Mock(), session=mock.Mock(), nodeid="Test")
node.add_marker(pytest.mark.a(1))
node.add_marker("b")
node.get_marker("a").combined
node.get_marker("b").combined
# reprec_keywords = testdir.inline_run("-k", "FOO")
# passed_k, skipped_k, failed_k = reprec_keywords.countoutcomes()
# assert passed_k == 2
# assert skipped_k == failed_k == 0
def test_addmarker_order():
@ -1199,7 +929,7 @@ def test_markers_from_parametrize(testdir):
custom_mark = pytest.mark.custom_mark
@pytest.fixture(autouse=True)
def trigger(request):
custom_mark =request.node.get_marker('custom_mark')
custom_mark = list(request.node.iter_markers('custom_mark'))
print("Custom mark %s" % custom_mark)
@custom_mark("custom mark non parametrized")