testing-cabal/mock

"TypeError: too many positional arguments" when using create_autospec on pypy3

Closed this issue · 6 comments

Once all the backports are up to date, this fails with:

__________ SpecSignatureTest.test_autospec_on_bound_builtin_function ___________
self = <mock.tests.testhelpers.SpecSignatureTest testMethod=test_autospec_on_bound_builtin_function>
    def test_autospec_on_bound_builtin_function(self):
        meth = six.create_bound_method(time.ctime, time.time())
        self.assertIsInstance(meth(), str)
        mocked = create_autospec(meth)
    
        # no signature, so no spec to check against
        mocked()
        mocked.assert_called_once_with()
        mocked.reset_mock()
>       mocked(4, 5, 6)
mock/tests/testhelpers.py:989: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
<string>:2: in ctime
    ???
mock/mock.py:279: in checksig
    sig.bind(*args, **kwargs)
/opt/python/pypy3.6-7.1.1/lib-python/3/inspect.py:2970: in bind
    return args[0]._bind(args[1:], kwargs)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
self = <Signature ()>, args = (4, 5, 6), kwargs = {}
    def _bind(self, args, kwargs, *, partial=False):
        """Private method. Don't use directly."""
    
        arguments = OrderedDict()
    
        parameters = iter(self.parameters.values())
        parameters_ex = ()
        arg_vals = iter(args)0m
    
        while True:
            # Let's iterate through the positional arguments and corresponding
            # parameters
            try:
                arg_val = next(arg_vals)
            except StopIteration:
                # No more positional arguments
                try:
                    param = next(parameters)
                except StopIteration:
                    # No more parameters. That's it. Just need to check that
                    # we have no `kwargs` after this while loop
                    break
                else:
                    if param.kind == _VAR_POSITIONAL:
                        # That's OK, just empty *args.  Let's start parsing
                        # kwargs
                        break
                    elif param.name in kwargs:
                        if param.kind == _POSITIONAL_ONLY:
                            msg = '{arg!r} parameter is positional only, ' \
                                  'but was passed as a keyword'
                            msg = msg.format(arg=param.name)
                            raise TypeError(msg) from None
                        parameters_ex = (param,)
                        break
                    elif (param.kind == _VAR_KEYWORD or
                                                param.default is not _empty):
                        # That's fine too - we have a default value for this
                        # parameter.  So, lets start parsing `kwargs`, starting
                        # with the current parameter
                        parameters_ex = (param,)
                        break
                    else:
                        # No default, not VAR_KEYWORD, not VAR_POSITIONAL,
                        # not in `kwargs`
                        if partial:
                            parameters_ex = (param,)
                            break
                        else:
                            msg = 'missing a required argument: {arg!r}'
                            msg = msg.format(arg=param.name)
                            raise TypeError(msg) from None
            else:
                # We have a positional argument to process
                try:
                    param = next(parameters)
                except StopIteration:
>                   raise TypeError('too many positional arguments') from None
E                   TypeError: too many positional arguments

Here's the full build log:
https://travis-ci.org/testing-cabal/mock/jobs/526308383

I don't want this to hold up a new backport release, so parking this issue here.

Sadly no response from the pypy guys, so closing this as wontfix :-(

This is a documented difference between CPython and PyPy, look for 'inspect' here: https://doc.pypy.org/en/latest/cpython_differences.html#miscellaneous
in PyPy, even builtin functions have signatures that the inspect module can find. Therefore create_autospec actually does the right thing in this test, and refuses to let you call the mock with the wrong number of arguments.

I think it's the right approach to just skip the test on PyPy (or even in general on non-CPython implementations).

@cfbolz - thanks for the followup, I'm unsure whether it'd be best for you if I followed up here or on https://bitbucket.org/pypy/pypy/issues/3010/help-with-failing-test-on-mock-backport, please let me know if you have a preference.

Okay, so for SpecSignatureTest.test_autospec_on_bound_builtin_function above, I see what you're saying and agree, so I can fix the test to show pypy gets it right, however, it doesn't appear to on this one:

    def test_spec_has_descriptor_returning_function(self):

        class CrazyDescriptor(object):

            def __get__(self, obj, type_):
                if obj is None:
                    return lambda x: None

        class MyClass(object):

            some_attr = CrazyDescriptor()

        mock = create_autospec(MyClass)
        mock.some_attr(1)

On cpython:

MyClass.some_attr(1)
mock.some_attr(1)
<MagicMock name='mock.some_attr()' id='4566378704'>

On pypy:

(Pdb) MyClass.some_attr(1)
(Pdb) mock.some_attr(1)
*** TypeError: too many positional arguments

Why is pypy getting this one wrong?

Interesting, the other skip, 9aa24d5 appears to no longer be necessary, so I've removed it.

For this last one, see https://bugs.python.org/issue39485.