镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

Fix spy on inherited classmethod/staticmethod called through an instance - #661

Open
abhishekcpp007 wants to merge 2 commits into
pytest-dev:mainfrom
abhishekcpp007:fix/spy-inherited-static-class-methods
Open

abhishekcpp007 wants to merge 2 commits into
pytest-dev:mainfrom
abhishekcpp007:fix/spy-inherited-static-class-methods

Conversation

@abhishekcpp007

Copy link
Copy Markdown

mocker.spy() breaks for a classmethod or staticmethod that the spied class inherits from a base class, as soon as the method is called through an instance. The call fails inside inspect with a TypeError before the spied code even runs.

class Base:
    @staticmethod
    def double(x):
        return x * 2

class Foo(Base):
    pass

def test_it(mocker):
    mocker.spy(Foo, "double")
    Foo.double(1)    # works
    Foo().double(1)  # TypeError: too many positional arguments

A classmethod fails the same way. Calling through the class works, which is why the existing test_class_method_subclass and test_static_method_subclass tests don't catch it. If double is defined on Foo itself, both ways work.

Root cause

src/pytest_mock/plugin.py:247 sets autospec=True for any function or method. mock.patch.object then autospecs target.__dict__[name] when it's there, which keeps the staticmethod/classmethod descriptor and gives a non-binding mock. For an inherited one it falls back to getattr(target, name), which is already the plain or bound function, so it creates an autospecced function and sets it on Foo. A function on a class binds to the instance, so Foo().double(1) passes the instance as an extra argument and the signature check rejects it.

Before #399 the spy checked inspect.getattr_static(obj, name) for classmethod/staticmethod and didn't autospec them. That PR removed the check as an old Python workaround and mentioned it shouldn't affect users. This is the case it did affect, so it's a regression since 3.13.0. Plain mock.patch.object(Foo, "double", autospec=True) has the same problem, but the spy picks autospec itself, so it can avoid it.

Fix

When spying on a class, I look up the attribute with inspect.getattr_static and, if it's a classmethod or staticmethod, pass that descriptor as autospec. That's exactly what mock does on its own when the method is defined on the class, so inherited and local methods are now specced the same way, signature checks included.

Tests

I added test_class_method_subclass_called_on_instance and test_static_method_subclass_called_on_instance. They call the spy through an instance, check the recorded call and return value, check that a bad call still raises TypeError, and check that mocker.stop() removes the patch from the subclass again.

Run in Docker (python:3.12, package installed with coverage mock pytest-asyncio like tox does, pytest tests):

  • main: 168 passed, 9 warnings
  • this branch: 170 passed, 9 warnings
  • this branch's tests against main's src/: 2 failed, 168 passed, 9 warnings (the two new tests)

pre-commit (rst, ruff, ruff-format, mypy) passes on the changed files.

Since pytest-dev#399, mocker.spy() autospecs every function or method it spies.
mock.patch.object only recognises a classmethod or staticmethod that is
defined on the patched class itself. For one inherited from a base class
it autospecs the plain function, which then binds to the instance when
called through one and fails with TypeError: too many positional
arguments.

Pass the classmethod/staticmethod descriptor as autospec, which is what
mock does on its own for one defined on the class.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant