Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions _pytest/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ def isclass(object):

if _PY3:
import codecs

imap = map
STRING_TYPES = bytes, str

def _escape_strings(val):
Expand Down Expand Up @@ -145,6 +145,8 @@ def _escape_strings(val):
else:
STRING_TYPES = bytes, str, unicode

from itertools import imap # NOQA

def _escape_strings(val):
"""In py2 bytes and str are the same type, so return if it's a bytes
object, return it unchanged if it is a full ascii string,
Expand Down Expand Up @@ -213,4 +215,4 @@ def _is_unittest_unexpected_success_a_failure():
Changed in version 3.4: Returns False if there were any
unexpectedSuccesses from tests marked with the expectedFailure() decorator.
"""
return sys.version_info >= (3, 4)
return sys.version_info >= (3, 4)
2 changes: 1 addition & 1 deletion _pytest/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,7 @@ def add_marker(self, marker):
"""
from _pytest.mark import MarkDecorator
if isinstance(marker, py.builtin._basestring):
marker = MarkDecorator(marker)
marker = getattr(pytest.mark, marker)
elif not isinstance(marker, MarkDecorator):
raise ValueError("is not a string or pytest.mark.* Marker")
self.keywords[marker.name] = marker
Expand Down
83 changes: 46 additions & 37 deletions _pytest/mark.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
""" generic mechanism for marking and selecting python functions. """
import inspect
from collections import namedtuple
from operator import attrgetter
from .compat import imap

def alias(name):
return property(attrgetter(name), doc='alias for ' + name)


class MarkerError(Exception):
Expand Down Expand Up @@ -182,7 +188,7 @@ def __getattr__(self, name):
raise AttributeError("Marker name must NOT start with underscore")
if hasattr(self, '_config'):
self._check(name)
return MarkDecorator(name)
return MarkDecorator(Mark(name, (), {}))

def _check(self, name):
try:
Expand Down Expand Up @@ -235,19 +241,20 @@ def test_function():
additional keyword or positional arguments.

"""
def __init__(self, name, args=None, kwargs=None):
self.name = name
self.args = args or ()
self.kwargs = kwargs or {}
def __init__(self, mark):
assert isinstance(mark, Mark), repr(mark)
self.mark = mark

name = alias('mark.name')
args = alias('mark.args')
kwargs = alias('mark.kwargs')

@property
def markname(self):
return self.name # for backward-compat (2.4.1 had this attr)

def __repr__(self):
d = self.__dict__.copy()
name = d.pop('name')
return "<MarkDecorator %r %r>" % (name, d)
return "<MarkDecorator %r>" % self.mark

def __call__(self, *args, **kwargs):
""" if passed a single callable argument: decorate it with mark info.
Expand All @@ -270,17 +277,14 @@ def __call__(self, *args, **kwargs):
else:
holder = getattr(func, self.name, None)
if holder is None:
holder = MarkInfo(
self.name, self.args, self.kwargs
)
holder = MarkInfo(self.mark)
setattr(func, self.name, holder)
else:
holder.add(self.args, self.kwargs)
holder.add_mark(self.mark)
return func
kw = self.kwargs.copy()
kw.update(kwargs)
args = self.args + args
return self.__class__(self.name, args=args, kwargs=kw)

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


def extract_argvalue(maybe_marked_args):
Expand All @@ -291,36 +295,41 @@ def extract_argvalue(maybe_marked_args):
newmarks = {}
argval = maybe_marked_args
while isinstance(argval, MarkDecorator):
newmark = MarkDecorator(argval.markname,
argval.args[:-1], argval.kwargs)
newmarks[newmark.markname] = newmark
newmark = MarkDecorator(Mark(
argval.markname, argval.args[:-1], argval.kwargs))
newmarks[newmark.name] = newmark
argval = argval.args[-1]
return argval, newmarks


class MarkInfo:
class Mark(namedtuple('Mark', 'name, args, kwargs')):

def combined_with(self, other):
assert self.name == other.name
return Mark(
self.name, self.args + other.args,
dict(self.kwargs, **other.kwargs))


class MarkInfo(object):
""" Marking object created by :class:`MarkDecorator` instances. """
def __init__(self, name, args, kwargs):
#: name of attribute
self.name = name
#: positional argument list, empty if none specified
self.args = args
#: keyword argument dictionary, empty if nothing specified
self.kwargs = kwargs.copy()
self._arglist = [(args, kwargs.copy())]
def __init__(self, mark):
assert isinstance(mark, Mark), repr(mark)
self.combined = mark
self._marks = [mark]

name = alias('combined.name')
args = alias('combined.args')
kwargs = alias('combined.kwargs')

def __repr__(self):
return "<MarkInfo %r args=%r kwargs=%r>" % (
self.name, self.args, self.kwargs
)
return "<MarkInfo {0!r}>".format(self.combined)

def add(self, args, kwargs):
def add_mark(self, mark):
""" add a MarkInfo with the given args and kwargs. """
self._arglist.append((args, kwargs))
self.args += args
self.kwargs.update(kwargs)
self._marks.append(mark)
self.combined = self.combined.combined_with(mark)

def __iter__(self):
""" yield MarkInfo objects each relating to a marking-call. """
for args, kwargs in self._arglist:
yield MarkInfo(self.name, args, kwargs)
return imap(MarkInfo, self._marks)
8 changes: 3 additions & 5 deletions _pytest/skipping.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,11 +121,9 @@ def _istrue(self):
# "holder" might be a MarkInfo or a MarkDecorator; only
# MarkInfo keeps track of all parameters it received in an
# _arglist attribute
if hasattr(self.holder, '_arglist'):
arglist = self.holder._arglist
else:
arglist = [(self.holder.args, self.holder.kwargs)]
for args, kwargs in arglist:
marks = getattr(self.holder, '_marks', None) \
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not that this is made worse by this PR but it's not nice that this uses an internal attribute of the MarkInfo. Should markinfo not make this public somehow instead? How about using the MarkInfo as an iterator? I think that gives you access to all args/kwargs individually.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@flub when i did this the first time (the pr we rejected at the sprint)

i did move this code into the marks module

i plan to fix it eventually, but that was not in the scope of this pr

or [self.holder.mark]
for _, args, kwargs in marks:
if 'condition' in kwargs:
args = (kwargs['condition'],)
for expr in args:
Expand Down
4 changes: 2 additions & 2 deletions testing/test_mark.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@

class TestMark:
def test_markinfo_repr(self):
from _pytest.mark import MarkInfo
m = MarkInfo("hello", (1,2), {})
from _pytest.mark import MarkInfo, Mark
m = MarkInfo(Mark("hello", (1,2), {}))
repr(m)

def test_pytest_exists_in_namespace_all(self):
Expand Down