Skip to content

Instantly share code, notes, and snippets.

@ikonst
Created November 11, 2021 02:45
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ikonst/cc28e815008c6487af91a10e3a1fdd7a to your computer and use it in GitHub Desktop.
Save ikonst/cc28e815008c6487af91a10e3a1fdd7a to your computer and use it in GitHub Desktop.
pytest_mock with patch.method
import gc
import sys
import types
import unittest.mock
from typing import Any
from typing import Callable
from typing import Generator
from typing import Optional
from typing import TYPE_CHECKING
import pytest
import pytest_mock
def _class_holding(fn: Callable) -> Optional[type]: # see https://stackoverflow.com/a/65756960
for possible_dict in gc.get_referrers(fn):
if not isinstance(possible_dict, dict):
continue
for possible_class in gc.get_referrers(possible_dict):
if isinstance(possible_class, type) and getattr(possible_class, fn.__name__, None) is fn:
return possible_class
return None
class MockerFixture(pytest_mock.MockerFixture):
patch: '_Patcher'
class _Patcher(pytest_mock.MockerFixture._Patcher):
if TYPE_CHECKING:
def method(
self,
method: Callable,
new: object = ...,
spec: Optional[object] = ...,
create: bool = ...,
spec_set: Optional[object] = ...,
autospec: Optional[object] = ...,
new_callable: object = ...,
**kwargs: Any,
) -> unittest.mock.MagicMock:
...
else:
def method(
self,
method: Callable,
*args: Any,
**kwargs: Any,
) -> unittest.mock.MagicMock:
"""
Enables patching bound methods:
-patch.object(my_instance, 'my_method')
+patch.method(my_instance.my_method)
and unbound methods:
-patch.object(MyClass, 'my_method')
+patch.method(MyClass.my_method)
by passing a reference (not stringly-typed paths!), allowing for easier IDE navigation and refactoring.
"""
if isinstance(method, types.MethodType): # handle bound methods
return self.object(method.__self__, method.__name__, *args, **kwargs)
elif isinstance(method, types.FunctionType): # handle unbound methods
cls = _class_holding(method)
if cls is None:
raise ValueError(
f"Could not determine class for {method}: if it's not an unbound method "
f'but a function, consider patch.object.'
)
return self.object(cls, method.__name__, *args, **kwargs)
else:
raise ValueError(f"{method} doesn't look like a method")
@pytest.fixture
def mocker(pytestconfig: Any) -> Generator[MockerFixture, None, None]:
"""
Extends the pytest_mock 'mocker' fixture with additional methods:
def test_foo(mocker):
mocker.patch.method(...)
"""
result = MockerFixture(pytestconfig)
yield result
result.stopall()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment