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

pytest.parametrize fails on test function with non-signature-preserving decorator on Python 3 #3435

Closed
andy-maier opened this issue Apr 28, 2018 · 13 comments
Labels
status: needs information reporter needs to provide more information; can be closed after 2 or more weeks of inactivity topic: fixtures anything involving fixtures directly or indirectly

Comments

@andy-maier
Copy link
Contributor

In the pywbem project, we are using pytest.parametrize on top of our own decorator pytest_extension.test_function that handles expected exceptions, expected warnings, and some more things, in an attempt to simplify the test function that is coded. That decorator is signature-preserving. An example of such a test function is here. This runs fine on Python 2 and Python 3 since a while now. There is no issue with that, and I'm just mentioning this as a basis.

Now we have made an attempt to further simplify the test function that is coded, with a new variant pytest_extension.test_function_new of our decorator, which eliminates the explicit unpacking of kwargs passed to the test function, and avoids having unused arguments. That new decorator is signature-changing. Its wrapper function provides exactly the signature that is needed by the pytest.parametrize decorator. The accordingly modified example test function is here. This runs fine on Python 2 but not on Python 3.

On Python 3.6, it fails while collecting testcases (see this Travis job):

bash -c "set -o pipefail; PYTHONWARNINGS=default py.test --cov pywbem --cov pywbem_mock  --cov-config coveragerc --ignore=attic --ignore=releases -s 2>&1 |tee test_36.tmp.log"
============================= test session starts ==============================
platform linux -- Python 3.6.3, pytest-3.5.1, py-1.5.2, pluggy-0.6.0
rootdir: /home/travis/build/pywbem/pywbem, inifile:
plugins: cov-2.5.1
Debug: CHECK_0_12_0 = True, __version__ = 0.12.1.dev20
collected 4647 items / 4 errors
==================================== ERRORS ====================================
__________________ ERROR collecting testsuite/test_cim_obj.py __________________
../../../virtualenv/python3.6.3/lib/python3.6/site-packages/pluggy/__init__.py:617: in __call__
    return self._hookexec(self, self._nonwrappers + self._wrappers, kwargs)
../../../virtualenv/python3.6.3/lib/python3.6/site-packages/pluggy/__init__.py:222: in _hookexec
    return self._inner_hookexec(hook, methods, kwargs)
../../../virtualenv/python3.6.3/lib/python3.6/site-packages/pluggy/__init__.py:216: in <lambda>
    firstresult=hook.spec_opts.get('firstresult'),
../../../virtualenv/python3.6.3/lib/python3.6/site-packages/_pytest/python.py:201: in pytest_pycollect_makeitem
    res = list(collector._genfunctions(name, obj))
../../../virtualenv/python3.6.3/lib/python3.6/site-packages/_pytest/python.py:379: in _genfunctions
    self.ihook.pytest_generate_tests(metafunc=metafunc)
../../../virtualenv/python3.6.3/lib/python3.6/site-packages/pluggy/__init__.py:617: in __call__
    return self._hookexec(self, self._nonwrappers + self._wrappers, kwargs)
../../../virtualenv/python3.6.3/lib/python3.6/site-packages/pluggy/__init__.py:222: in _hookexec
    return self._inner_hookexec(hook, methods, kwargs)
../../../virtualenv/python3.6.3/lib/python3.6/site-packages/pluggy/__init__.py:216: in <lambda>
    firstresult=hook.spec_opts.get('firstresult'),
../../../virtualenv/python3.6.3/lib/python3.6/site-packages/_pytest/python.py:126: in pytest_generate_tests
    metafunc.parametrize(*marker.args, **marker.kwargs)
../../../virtualenv/python3.6.3/lib/python3.6/site-packages/_pytest/python.py:811: in parametrize
    self.function, name, arg))
E   ValueError: <function test_CIMInstanceName_hash at 0x2ac23c8f9d90> uses no argument 'desc'
. . .

In that Travis job, the packages in the venv were:

Package                  Version      Location                        
------------------------ ------------ --------------------------------
alabaster                0.7.10       
attrs                    17.4.0       
Babel                    2.5.3        
backcall                 0.1.0        
bleach                   2.1.3        
certifi                  2018.4.16    
chardet                  3.0.4        
coverage                 4.5.1        
decorator                4.3.0        
distro                   1.2.0        
docutils                 0.14         
entrypoints              0.2.3        
flake8                   3.5.0        
gitdb2                   2.0.3        
GitPython                2.1.9        
html5lib                 1.0.1        
httpretty                0.9.4        
idna                     2.6          
imagesize                1.0.0        
ipykernel                4.8.2        
ipython                  6.3.1        
ipython-genutils         0.2.0        
ipywidgets               7.2.1        
jedi                     0.12.0       
Jinja2                   2.10         
jsonschema               2.6.0        
jupyter                  1.0.0        
jupyter-client           5.2.3        
jupyter-console          5.2.0        
jupyter-core             4.4.0        
linecache2               1.0.0        
lxml                     4.2.1        
MarkupSafe               1.0          
mccabe                   0.6.1        
mistune                  0.8.3        
mock                     2.0.0        
more-itertools           4.1.0        
nbconvert                5.3.1        
nbformat                 4.4.0        
nose                     1.3.7        
notebook                 5.4.1        
numpy                    1.13.3       
packaging                17.1         
pandocfilters            1.4.2        
parso                    0.2.0        
pbr                      4.0.2        
pexpect                  4.5.0        
pickleshare              0.7.4        
pip                      10.0.1       
pkginfo                  1.4.2        
pluggy                   0.6.0        
ply                      3.11         
prompt-toolkit           1.0.15       
ptyprocess               0.5.2        
py                       1.5.2        
pycodestyle              2.3.1        
pyflakes                 1.6.0        
Pygments                 2.2.0        
pyparsing                2.2.0        
pytest                   3.5.1        
pytest-cov               2.5.1        
python-dateutil          2.7.2        
pytz                     2018.4       
pywbem                   0.12.1.dev19 /home/travis/build/pywbem/pywbem
PyYAML                   3.12         
pyzmq                    17.0.0       
qtconsole                4.3.1        
requests                 2.18.4       
requests-toolbelt        0.8.0        
Send2Trash               1.5.0        
setuptools               39.1.0       
simplegeneric            0.8.1        
six                      1.11.0       
smmap2                   2.0.3        
snowballstemmer          1.2.1        
Sphinx                   1.7.4        
sphinx-git               10.1.1       
sphinxcontrib-websupport 1.0.1        
terminado                0.8.1        
testfixtures             5.4.0        
testpath                 0.3.1        
tornado                  5.0.2        
tqdm                     4.23.1       
traceback2               1.4.0        
traitlets                4.3.2        
twine                    1.11.0       
unittest2                1.1.0        
urllib3                  1.22         
wcwidth                  0.1.7        
webencodings             0.5.1        
wheel                    0.31.0       
widgetsnbextension       3.2.1        
yamlordereddictloader    0.4.0        

It also fails on Python 3.4 with older package versions, see this Travis job. The job log shows the pip list.

From the error that is raised, my conclusion was that pytest looks at the signature of the original test function when checking the signature of the function to be called by parametrize, instead of looking at the signature of its wrapper function. At least on Python 3.

I have debugged into pytest and was found that conclusion to be confirmed, but when trying to find the root cause, I gave up at some point.

In case you want to reproduce the issue, create a Python 3 venv, clone the pywbem repo, and issue make all in its work directory.

Some interesting questions may be:
-> Why does it succeed on Python 2 but fail on Python 3?
-> Is the use of a signature-changing decorator ok in combination with pytest.parametrize?

@pytestbot
Copy link
Contributor

GitMate.io thinks possibly related issues are #1007 (pytest-xdist appears to fail on Python 3.5), #1111 (pytest.mark.parametrize fails with lambdas), #18 (pytest-2.0.0 fails testing/test_nose.py on python-3.1.3), #2132 (pytest tests explicitly check for ImportError, fails on Python 3.6), and #2975 (Pytest 3.3: tests being skipped without failing).

@RonnyPfannschmidt
Copy link
Member

as far as i can tell the decorator library is completely wrong at preserving python signatures adhering to the __signature__ protocols - on older pytest it may simply work because we don't adhere to the protocols

for sanity/safety pytest unpacks wrappers unless they declare a valid signature

@nicoddemus nicoddemus added status: needs information reporter needs to provide more information; can be closed after 2 or more weeks of inactivity topic: fixtures anything involving fixtures directly or indirectly and removed type: bug problem that needs to be addressed labels Apr 29, 2018
@andy-maier
Copy link
Contributor Author

andy-maier commented May 2, 2018

Note that the decorator package is used only by our existing approach, which works on py2 and py3. Our new approach which works on py2 but not on py3 uses functools.update_wrapper().

The pytest version used in both approaches on both py2 and p3 is the same: 3.5.1.

@RonnyPfannschmidt
Copy link
Member

@andy-maier functools.update_wrapper is broken on python2

@andy-maier
Copy link
Contributor Author

ok, so can you tell me whether we can fix this, and if so, how?

@RonnyPfannschmidt
Copy link
Member

RonnyPfannschmidt commented May 2, 2018

@andy-maier attaching a __signature__ to the function you return should fix it

@andy-maier
Copy link
Contributor Author

Ok, great. We will try that.

@andy-maier
Copy link
Contributor Author

andy-maier commented May 2, 2018

Because the __signature__ special attribute is not really described, I first tried to understand whether inspect.signature() would return something meaningful, without having specified __signature__ anywhere. Here is the result, for Python 3.6 (in directory testsuite of a pywbem repo clone, with branch andy/#1201-improved-testfunc-decorator checked out (which contains our new test_function_new decorator):

(pywbem36) Andreass-MacBook-Pro:testsuite maiera$ python
Python 3.6.5 (default, Mar 30 2018, 06:41:53) 
[GCC 4.2.1 Compatible Apple LLVM 9.0.0 (clang-900.0.39.2)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import test_nocasedict, inspect
>>> st=inspect.signature(test_nocasedict.test_NocaseDict_eq, follow_wrapped=True)
>>> st
<Signature (testcase, obj1, obj2, exp_obj_equal)>
>>> sf=inspect.signature(test_nocasedict.test_NocaseDict_eq, follow_wrapped=False)
>>> sf
<Signature (desc, kwargs, exp_exc_types, exp_warn_types, condition)>
>>> 

So this returns exactly whatz I would expect, but then maybe my expectation is incorrect ;-) When following the wrapper, it returns the signature of the original function (which would be incorrect), and when not following the wrapper, it returns the signature of the outer (wrapper) function (which would be correct).

So if I understand your statement above right:

for sanity/safety pytest unpacks wrappers unless they declare a valid signature

then pytest does not trust the (correct) result of inspect.signature() for follow_wrapped=False, but thinks setting the (incorrect) follow_wrapped=True is a better default if the wrapper does not have a __signature__ attribute, but if it has one it trusts that one? Just to understand it, not to criticize it ...

@RonnyPfannschmidt
Copy link
Member

i believe this might have been an oversight when switching to using signature objects

CC @nicoddemus

historically pytest used various forms of unpacking to deal with different kinds of decorators so its entirely possible that this is a unclear interaction and we need to find a way to shift toward consistency

@andy-maier
Copy link
Contributor Author

@RonnyPfannschmidt Hi Ronny, I first did not want to believe your recommendation because it basically said that exactly in all environments where it worked, some function would be flawed. However, it turned out you were exactly right. So thanks a lot!!

I now have it running on all Python versions we support (2.6+, 3.4+) for both our minimum package levels and the latest package levels. I had to inrease the minimum package levels of pytest to 3.3.2, and of "py" to 1.5.1.

The key parts of our signature-changing test_function_new decorator look like this. I cannot use signature() with the follow_wrapped=False argument because that argument was introduced only in Python 3.5. This would have been more elegant and I verified that it works (On Python 3.5+).

Setting the signature for Python 2 was not necessary to make it work, but I am setting it nevertheless, in case the flawed behavior on Python 2 gets fixed one day.

Let me know if you have further comments.

Andy

if six.PY3:
    from inspect import Signature, Parameter
else:
    from funcsigs import Signature, Parameter

TESTFUNC_SIGNATURE = Signature(
    parameters=[
        Parameter('desc', Parameter.POSITIONAL_OR_KEYWORD),
        Parameter('kwargs', Parameter.POSITIONAL_OR_KEYWORD),
        Parameter('exp_exc_types', Parameter.POSITIONAL_OR_KEYWORD),
        Parameter('exp_warn_types', Parameter.POSITIONAL_OR_KEYWORD),
        Parameter('condition', Parameter.POSITIONAL_OR_KEYWORD),
    ]
)

def test_function_new(test_func):
    def wrapper_func(desc, kwargs, exp_exc_types, exp_warn_types, condition):
        . . .

    wrapper_func.__signature__ = TESTFUNC_SIGNATURE
    return functools.update_wrapper(wrapper_func, test_func)

@RonnyPfannschmidt
Copy link
Member

@andy-maier the main problem is that on python2 the update_wrapper function does incorrectly not set __wrapped__ - thus the code works since its not discoverable

that issue has been fixed on python3, which in turn makes things fall apart as they should

@andy-maier
Copy link
Contributor Author

andy-maier commented May 3, 2018

Thanks for the info. My original problem is solved now.

I guess what remains to be done is for pytest to figure out whether to follow up on what Ronny pointed out 4 comments above.

@RonnyPfannschmidt
Copy link
Member

thanks for your follow-up, i created a tracking issue

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status: needs information reporter needs to provide more information; can be closed after 2 or more weeks of inactivity topic: fixtures anything involving fixtures directly or indirectly
Projects
None yet
Development

No branches or pull requests

4 participants