pylint-dev / pylint

It's not just a linter that annoys you!
https://pylint.readthedocs.io/en/latest/
GNU General Public License v2.0
5.26k stars 1.12k forks source link

False positive C2801 (unnecessary-dunder-call) with unittest.mock.call #7351

Open jamesbraza opened 2 years ago

jamesbraza commented 2 years ago

Bug description

Here is a minimal repro:

# pylint: disable=missing-module-docstring, disable=missing-class-docstring

from unittest import TestCase
from unittest.mock import MagicMock, call, patch

class HasDunder:  # pylint: disable=too-few-public-methods
    def __setitem__(self, key: int, value: int) -> None:
        pass

class TestClass(TestCase):
    def test___setitem__(self) -> None:  # pylint: disable=missing-function-docstring
        all_mocks = MagicMock()

        has_dunder = HasDunder()
        with patch.object(HasDunder, "__setitem__") as mock__setitem__:
            all_mocks.attach_mock(mock__setitem__, "HasDunder.__setitem__")
            has_dunder[1] = 2
        all_mocks.assert_has_calls([call.HasDunder.__setitem__(1, 2)])

Configuration

No response

Command used

pylint a.py

Pylint output

************* Module a
a.py:20:36: C2801: Unnecessarily calls dunder method __setitem__. Set item via subscript. (unnecessary-dunder-call)

Expected behavior

I expect pylint to now throw an unnecessary-dunder-call here.

Pylint version

pylint 2.14.5
astroid 2.11.7
Python 3.10.4 (main, May 22 2022, 14:36:56) [Clang 13.0.0 (clang-1300.0.29.30)]

OS / Environment

macOS Big Sur v11.5.1

Additional dependencies

No response

DanielNoord commented 2 years ago

So basically, we shouldn't emit this warning if the call is within unittest.MagicMock.assert_has_calls?

jamesbraza commented 2 years ago

Yeah that is a good question, and I was sort of hoping you all'd know how to better generalize this 😅

The call.HasDunder.__setitem__ that's causing the pylint error is set up to match the string passed in the all_mocks.attach_mock(mock__setitem__, "HasDunder.__setitem__").

This:

all_mocks.assert_has_calls([call.HasDunder.__setitem__(1, 2)])

Can be re-written as:

call_ = call.HasDunder.__setitem__(1, 2)
all_mocks.assert_has_calls([call_])

And it would move the error message to be on the first call_ line. Two options:

DanielNoord commented 2 years ago
  • Don't emit the message universally for unittest.mock.call?

I think this makes the most sense and is relatively easy to fix knowing how pylint deals with stuff like this. I like this proposal, thanks!

jontwo commented 6 months ago

I've just hit this same issue, as a result of following previous pylint advice!

Originally my code included this:

response = request.urlopen(request_obj)
retcode = response.getcode()

and in the test (using pytest-mock, which is a wrapper for unittest.mock)

mock_open = mocker.MagicMock()
mock_open.getcode.return_value = 418

Then pylint warned that I should be using urlopen as a context manager, so I changed the code to this:

with request.urlopen(request_obj) as response:
    retcode = response.getcode()

and setting the mock return value had to change to this, which triggered a C2801 warning:

mock_open.__enter__().getcode.return_value = 418

There may be a better way to set the return value on the mock, but this is how I've always done it. If pylint could allow dunder calls on mock objects that would be great!