fix handling MarkDecorators called with a single positional plus keyword args

When a MarkDecorator instance is called it does the following:
  1. If called with a single class as its only positional argument and no
     additional keyword arguments, it attaches itself to the class so it gets
     applied automatically to all test cases found in that class.
  2. If called with a single function as its only positional argument and no
     additional keyword arguments, it attaches a MarkInfo object to the
     function, containing all the arguments already stored internally in the
     MarkDecorator.
  3. When called in any other case, it performs a 'fake construction' call, i.e.
     it returns a new MarkDecorator instance with the original MarkDecorator's
     content updated with the arguments passed to this call.

When Python applies a function decorator it always passes the target class/
function to the decorator as its positional argument with no additional
positional or keyword arguments. However, when MarkDecorator was deciding
whether it was being called to decorate a target function/class (cases 1. & 2.
as documented above) or to return an updated MarkDecorator (case 3. as
documented above), it only checked that it received a single callable positional
argument and did not take into consideration whether additional keyword
arguments were being passed in as well.

With this change, it is now possible to create a pytest mark storing a function/
class parameter passed as its only positional argument and accompanied by one or
more additional keyword arguments. Before, it was only possible to do so if the
function/class parameter argument was accompanied by at least one other
positional argument.

Added a related unit test.

Updated MarkDecorator doc-string.
This commit is contained in:
Jurko Gospodnetić 2014-01-20 01:27:33 +01:00
parent 492c60c202
commit 8e457338ee
2 changed files with 30 additions and 1 deletions

View File

@ -206,6 +206,24 @@ class MarkDecorator:
@mark2 @mark2
def test_function(): def test_function():
pass pass
When a MarkDecorator instance is called it does the following:
1. If called with a single class as its only positional argument and no
additional keyword arguments, it attaches itself to the class so it
gets applied automatically to all test cases found in that class.
2. If called with a single function as its only positional argument and
no additional keyword arguments, it attaches a MarkInfo object to the
function, containing all the arguments already stored internally in
the MarkDecorator.
3. When called in any other case, it performs a 'fake construction' call,
i.e. it returns a new MarkDecorator instance with the original
MarkDecorator's content updated with the arguments passed to this
call.
Note: The rules above prevent MarkDecorator objects from storing only a
single function or class reference as their positional argument with no
additional keyword or positional arguments.
""" """
def __init__(self, name, args=None, kwargs=None): def __init__(self, name, args=None, kwargs=None):
self.name = name self.name = name
@ -224,7 +242,7 @@ class MarkDecorator:
def __call__(self, *args, **kwargs): def __call__(self, *args, **kwargs):
""" if passed a single callable argument: decorate it with mark info. """ if passed a single callable argument: decorate it with mark info.
otherwise add *args/**kwargs in-place to mark information. """ otherwise add *args/**kwargs in-place to mark information. """
if args: if args and not kwargs:
func = args[0] func = args[0]
if len(args) == 1 and (istestfunc(func) or if len(args) == 1 and (istestfunc(func) or
hasattr(func, '__bases__')): hasattr(func, '__bases__')):

View File

@ -57,6 +57,17 @@ class TestMark:
assert f.world.args[0] == "hello" assert f.world.args[0] == "hello"
mark.world("world")(f) mark.world("world")(f)
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"
def test_pytest_mark_reuse(self): def test_pytest_mark_reuse(self):
mark = Mark() mark = Mark()
def f(): def f():