From dcdc823dd2c79688d7da27e9393cda4fd4323ac0 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 2 Jul 2015 23:13:59 -0300 Subject: [PATCH 1/2] Support for tests created with functools.partial Fix #811 --- _pytest/python.py | 30 +++++++++++++++++++++----- testing/python/collect.py | 44 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 5 deletions(-) diff --git a/_pytest/python.py b/_pytest/python.py index 052a07784d6..eafaf7fb8ae 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -18,10 +18,19 @@ # used to work around a python2 exception info leak exc_clear = getattr(sys, 'exc_clear', lambda: None) -def getfslineno(obj): - # xxx let decorators etc specify a sane ordering +def get_real_func(obj): + """gets the real function object of the (possibly) wrapped object by + functools.wraps or functools.partial. + """ while hasattr(obj, "__wrapped__"): obj = obj.__wrapped__ + if isinstance(obj, py.std.functools.partial): + obj = obj.func + return obj + +def getfslineno(obj): + # xxx let decorators etc specify a sane ordering + obj = get_real_func(obj) if hasattr(obj, 'place_as'): obj = obj.place_as fslineno = py.code.getfslineno(obj) @@ -594,7 +603,10 @@ def setup(self): def _prunetraceback(self, excinfo): if hasattr(self, '_obj') and not self.config.option.fulltrace: - code = py.code.Code(self.obj) + if isinstance(self.obj, py.std.functools.partial): + code = py.code.Code(self.obj.func) + else: + code = py.code.Code(self.obj) path, firstlineno = code.path, code.firstlineno traceback = excinfo.traceback ntraceback = traceback.cut(path=path, firstlineno=firstlineno) @@ -1537,7 +1549,7 @@ def formatrepr(self): for function in stack: fspath, lineno = getfslineno(function) try: - lines, _ = inspect.getsourcelines(function) + lines, _ = inspect.getsourcelines(get_real_func(function)) except IOError: error_msg = "file %s, line %s: source code not available" addline(error_msg % (fspath, lineno+1)) @@ -1937,7 +1949,15 @@ def getfuncargnames(function, startindex=None): if realfunction != function: startindex += num_mock_patch_args(function) function = realfunction - argnames = inspect.getargs(py.code.getrawcode(function))[0] + if isinstance(function, py.std.functools.partial): + argnames = inspect.getargs(py.code.getrawcode(function.func))[0] + partial = function + argnames = argnames[len(partial.args):] + if partial.keywords: + for kw in partial.keywords: + argnames.remove(kw) + else: + argnames = inspect.getargs(py.code.getrawcode(function))[0] defaults = getattr(function, 'func_defaults', getattr(function, '__defaults__', None)) or () numdefaults = len(defaults) diff --git a/testing/python/collect.py b/testing/python/collect.py index bdea33a7f01..c7292829f62 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -851,3 +851,47 @@ class Test: result = testdir.runpytest() assert "TypeError" not in result.stdout.str() assert result.ret == 0 + + +def test_collect_functools_partial(testdir): + """ + Test that collection of functools.partial object works, and arguments + to the wrapped functions are dealt correctly (see #811). + """ + testdir.makepyfile(""" + import functools + import pytest + + @pytest.fixture + def fix1(): + return 'fix1' + + @pytest.fixture + def fix2(): + return 'fix2' + + def check1(i, fix1): + assert i == 2 + assert fix1 == 'fix1' + + def check2(fix1, i): + assert i == 2 + assert fix1 == 'fix1' + + def check3(fix1, i, fix2): + assert i == 2 + assert fix1 == 'fix1' + assert fix2 == 'fix2' + + test_ok_1 = functools.partial(check1, i=2) + test_ok_2 = functools.partial(check1, i=2, fix1='fix1') + test_ok_3 = functools.partial(check1, 2) + test_ok_4 = functools.partial(check2, i=2) + test_ok_5 = functools.partial(check3, i=2) + test_ok_6 = functools.partial(check3, i=2, fix1='fix1') + + test_fail_1 = functools.partial(check2, 2) + test_fail_2 = functools.partial(check3, 2) + """) + result = testdir.inline_run() + result.assertoutcome(passed=6, failed=2) From a7b4ed89da6450b1eb93b785dce5f8f1a81c00c8 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 13 Jul 2015 12:16:51 -0300 Subject: [PATCH 2/2] Use functools.partial name explicitly and simplify the code a bit as asked in review --- _pytest/python.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/_pytest/python.py b/_pytest/python.py index eafaf7fb8ae..76a6c4ed4e1 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -1,5 +1,6 @@ """ Python test discovery, setup and run of test functions. """ import fnmatch +import functools import py import inspect import sys @@ -24,7 +25,7 @@ def get_real_func(obj): """ while hasattr(obj, "__wrapped__"): obj = obj.__wrapped__ - if isinstance(obj, py.std.functools.partial): + if isinstance(obj, functools.partial): obj = obj.func return obj @@ -603,10 +604,7 @@ def setup(self): def _prunetraceback(self, excinfo): if hasattr(self, '_obj') and not self.config.option.fulltrace: - if isinstance(self.obj, py.std.functools.partial): - code = py.code.Code(self.obj.func) - else: - code = py.code.Code(self.obj) + code = py.code.Code(get_real_func(self.obj)) path, firstlineno = code.path, code.firstlineno traceback = excinfo.traceback ntraceback = traceback.cut(path=path, firstlineno=firstlineno) @@ -1949,7 +1947,7 @@ def getfuncargnames(function, startindex=None): if realfunction != function: startindex += num_mock_patch_args(function) function = realfunction - if isinstance(function, py.std.functools.partial): + if isinstance(function, functools.partial): argnames = inspect.getargs(py.code.getrawcode(function.func))[0] partial = function argnames = argnames[len(partial.args):]