Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove MarkInfo #4564

Merged
merged 7 commits into from Dec 21, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog/4546.removal.rst
@@ -0,0 +1,3 @@
Remove ``Node.get_marker(name)`` the return value was not usable for more than a existence check.

Use ``Node.get_closest_marker(name)`` as a replacement.
1 change: 1 addition & 0 deletions changelog/891.removal.rst
@@ -0,0 +1 @@
Remove ``testfunction.markername`` attributes - use ``Node.iter_markers(name=None)`` to iterate them.
24 changes: 18 additions & 6 deletions doc/en/deprecations.rst
Expand Up @@ -77,13 +77,7 @@ Becomes:



``Node.get_marker``
~~~~~~~~~~~~~~~~~~~

.. deprecated:: 3.6

As part of a large :ref:`marker-revamp`, :meth:`_pytest.nodes.Node.get_marker` is deprecated. See
:ref:`the documentation <update marker code>` on tips on how to update your code.


Result log (``--result-log``)
Expand Down Expand Up @@ -497,3 +491,21 @@ Removed all ``py.test-X*`` entry points. The versioned, suffixed entry points
were never documented and a leftover from a pre-virtualenv era. These entry
points also created broken entry points in wheels, so removing them also
removes a source of confusion for users.


``Node.get_marker``
~~~~~~~~~~~~~~~~~~~

*Removed in version 4.0*

As part of a large :ref:`marker-revamp`, :meth:`_pytest.nodes.Node.get_marker` is deprecated. See
:ref:`the documentation <update marker code>` on tips on how to update your code.


``somefunction.markname``
~~~~~~~~~~~~~~~~~~~~~~~~~

*Removed in version 4.0*

As part of a large :ref:`marker-revamp` we already deprecated using ``MarkInfo``
the only correct way to get markers of an element is via ``node.iter_markers(name)``.
7 changes: 0 additions & 7 deletions doc/en/reference.rst
Expand Up @@ -724,13 +724,6 @@ MarkGenerator
:members:


MarkInfo
~~~~~~~~

.. autoclass:: _pytest.mark.MarkInfo
:members:


Mark
~~~~

Expand Down
18 changes: 14 additions & 4 deletions src/_pytest/config/__init__.py
Expand Up @@ -268,10 +268,14 @@ def parse_hookimpl_opts(self, plugin, name):
# collect unmarked hooks as long as they have the `pytest_' prefix
if opts is None and name.startswith("pytest_"):
opts = {}

if opts is not None:
# TODO: DeprecationWarning, people should use hookimpl
RonnyPfannschmidt marked this conversation as resolved.
Show resolved Hide resolved
# https://github.com/pytest-dev/pytest/issues/4562
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)
RonnyPfannschmidt marked this conversation as resolved.
Show resolved Hide resolved
return opts

def parse_hookspec_opts(self, module_or_class, name):
Expand All @@ -280,10 +284,16 @@ def parse_hookspec_opts(self, module_or_class, name):
)
if opts is None:
method = getattr(module_or_class, name)

if name.startswith("pytest_"):
# todo: deprecate hookspec hacks
RonnyPfannschmidt marked this conversation as resolved.
Show resolved Hide resolved
# https://github.com/pytest-dev/pytest/issues/4562
known_marks = {m.name for m in getattr(method, "pytestmark", [])}
opts = {
"firstresult": hasattr(method, "firstresult"),
"historic": hasattr(method, "historic"),
"firstresult": hasattr(method, "firstresult")
or "firstresult" in known_marks,
"historic": hasattr(method, "historic")
or "historic" in known_marks,
}
return opts

Expand Down
25 changes: 13 additions & 12 deletions src/_pytest/fixtures.py
Expand Up @@ -1207,19 +1207,20 @@ def pytest_generate_tests(self, metafunc):
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
nicoddemus marked this conversation as resolved.
Show resolved Hide resolved
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,
Expand Down
11 changes: 1 addition & 10 deletions src/_pytest/mark/__init__.py
Expand Up @@ -11,19 +11,10 @@
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):
Expand Down
101 changes: 8 additions & 93 deletions src/_pytest/mark/structures.py
@@ -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


Expand Down Expand Up @@ -233,11 +230,7 @@ def __call__(self, *args, **kwargs):
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)

Expand All @@ -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):
Expand All @@ -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::
Expand Down
15 changes: 0 additions & 15 deletions src/_pytest/nodes.py
Expand Up @@ -10,7 +10,6 @@

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

Expand Down Expand Up @@ -211,20 +210,6 @@ def get_closest_marker(self, name, default=None):
"""
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()
Expand Down
22 changes: 17 additions & 5 deletions src/_pytest/python.py
Expand Up @@ -41,7 +41,6 @@
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
Expand Down Expand Up @@ -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):
nicoddemus marked this conversation as resolved.
Show resolved Hide resolved
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)

Expand Down Expand Up @@ -385,7 +384,6 @@ def _genfunctions(self, name, funcobj):
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)
Expand Down Expand Up @@ -1291,6 +1289,20 @@ def __init__(
if keywords:
self.keywords.update(keywords)

# todo: this is a hell of a hack
RonnyPfannschmidt marked this conversation as resolved.
Show resolved Hide resolved
# https://github.com/pytest-dev/pytest/issues/4569

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()
Expand Down
4 changes: 0 additions & 4 deletions src/_pytest/unittest.py
Expand Up @@ -14,8 +14,6 @@
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):
Expand Down Expand Up @@ -54,14 +52,12 @@ def collect(self):
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

Expand Down