From 4f2592861f018c8924e456e46921acb5f0978af7 Mon Sep 17 00:00:00 2001 From: Samuele Maci Date: Tue, 20 Jun 2023 05:50:25 -0700 Subject: [PATCH] Add mock_callable and mock_async_callable support for slotted classes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: This PR has the objective of supporting `self.mock_callable` and `self.mock_async_callable` for methods of slotted classes. Before the PR mocking was not possible as `testslide` implementation assumed the presence of `__dict__` (which cannot be ensured on slotted classes). ## Test plan ``` (venv) maci:testslide/ (maci-add-support-for-slotted-classes) $ git co -f HEAD~1 testslide Updated 1 path from ad55cd4 (venv) maci:testslide/ (maci-add-support-for-slotted-classes✗) $ git reset Unstaged changes after reset: M testslide/mock_callable.py (venv) maci:testslide/ (maci-add-support-for-slotted-classes✗) $ git diff (venv) maci:testslide/ (maci-add-support-for-slotted-classes✗) $ git diff | cat diff --git a/testslide/mock_callable.py b/testslide/mock_callable.py index 12fc9c9..87cf2f4 100644 --- a/testslide/mock_callable.py +++ b/testslide/mock_callable.py @@ -776,14 +776,8 @@ class _MockCallableDSL: self.type_validation or self.type_validation is None, ) - if isinstance(self._target, (Mock, StrictMock)) or not hasattr( - self._target, "__slots__" - ): - restore = self._method in self._target.__dict__ - restore_value = self._target.__dict__.get(self._method, None) - else: - restore = self._method in self._target.__slots__ - restore_value = getattr(self._target, self._method) + restore = self._method in self._target.__dict__ + restore_value = self._target.__dict__.get(self._method, None) if inspect.isclass(self._target): new_value = staticmethod(new_value) # type: ignore (venv) maci:testslide/ (maci-add-support-for-slotted-classes✗) $ make tests/mock_callable_testslide.py COVERAGE ERASE TESTSLIDE tests/mock_callable_testslide.py mock_callable() patching private functions raises valueerror patching private functions with allow private patching functions in slotted class: AttributeError: 'SomeClassWithSlots' object has no attribute '__dict__' Failures: 1) mock_callable(): patching functions in slotted class 1) AttributeError: 'SomeClassWithSlots' object has no attribute '__dict__' File "testslide/runner.py", line 835, in run self._run_example(example) File "testslide/runner.py", line 817, in _run_example _ExampleRunner( File "testslide/__init__.py", line 534, in run self._sync_run_all_hooks_and_example(context_data) File "testslide/__init__.py", line 506, in _sync_run_all_hooks_and_example aggregated_exceptions.raise_correct_exception() File "testslide/__init__.py", line 275, in raise_correct_exception raise self.exceptions[0] File "testslide/__init__.py", line 259, in catch yield File "testslide/__init__.py", line 497, in _sync_run_all_hooks_and_example self._fail_if_coroutine_function(self.example.code, context_data) File "testslide/__init__.py", line 465, in _fail_if_coroutine_function return func(*args, **kwargs) File "tests/mock_callable_testslide.py", line 110, in patching_functions_in_slotted_class self.mock_callable(t, "method").to_return_value(42).and_assert_called_once() File "testslide/mock_callable.py", line 52, in mock_callable return _MockCallableDSL( File "testslide/mock_callable.py", line 851, in __init__ original_callable, unpatcher = self._patch(callable_mock) File "testslide/mock_callable.py", line 779, in _patch restore = self._method in self._target.__dict__ Executed 3 examples in 0.4s: Successful: 2 Failed: 1 Skipped: 0 Not executed: 2392 https://testslide.readthedocs.io/ make: *** [tests/mock_callable_testslide.py] Error 1 (venv) maci:testslide/ (maci-add-support-for-slotted-classes✗) $ make tests/mock_async_callable_testslide.py COVERAGE ERASE TESTSLIDE tests/mock_async_callable_testslide.py mock_async_callable() patching functions in slotted class: AttributeError: 'SomeClassWithSlots' object has no attribute '__dict__' Failures: 1) mock_async_callable(): patching functions in slotted class 1) AttributeError: 'SomeClassWithSlots' object has no attribute '__dict__' File "testslide/runner.py", line 835, in run self._run_example(example) File "testslide/runner.py", line 817, in _run_example _ExampleRunner( File "testslide/__init__.py", line 532, in run self._async_run_all_hooks_and_example(context_data) File "testslide/__init__.py", line 457, in _async_run_all_hooks_and_example asyncio_run(coro) File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/asyncio/runners.py", line 44, in run return loop.run_until_complete(main) File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/asyncio/base_events.py", line 642, in run_until_complete return future.result() File "testslide/__init__.py", line 370, in _real_async_run_all_hooks_and_example aggregated_exceptions.raise_correct_exception() File "testslide/__init__.py", line 275, in raise_correct_exception raise self.exceptions[0] File "testslide/__init__.py", line 259, in catch yield File "testslide/__init__.py", line 356, in _real_async_run_all_hooks_and_example await _async_ensure_no_leaked_tasks( File "testslide/__init__.py", line 93, in _async_ensure_no_leaked_tasks result = await coro File "testslide/__init__.py", line 322, in _fail_if_not_coroutine_function return await func(*args, **kwargs) File "tests/mock_async_callable_testslide.py", line 96, in patching_functions_in_slotted_class self.mock_async_callable(t, "async_method").to_return_value( File "testslide/mock_callable.py", line 72, in mock_async_callable return _MockAsyncCallableDSL( File "testslide/mock_callable.py", line 1179, in __init__ super().__init__( File "testslide/mock_callable.py", line 851, in __init__ original_callable, unpatcher = self._patch(callable_mock) File "testslide/mock_callable.py", line 779, in _patch restore = self._method in self._target.__dict__ Executed 1 example in 0.1s: Successful: 0 Failed: 1 Skipped: 0 Not executed: 263 https://testslide.readthedocs.io/ make: *** [tests/mock_async_callable_testslide.py] Error 1 (venv) maci:testslide/ (maci-add-support-for-slotted-classes✗) $ git co -f . Updated 1 path from the index (venv) maci:testslide/ (maci-add-support-for-slotted-classes) $ git diff | cat (venv) maci:testslide/ (maci-add-support-for-slotted-classes) $ make tests/mock_callable_testslide.py COVERAGE ERASE TESTSLIDE tests/mock_callable_testslide.py (venv) maci:testslide/ (maci-add-support-for-slotted-classes) $ make tests/mock_async_callable_testslide.py COVERAGE ERASE TESTSLIDE tests/mock_async_callable_testslide.py (venv) maci:testslide/ (maci-add-support-for-slotted-classes) $ ``` All tests passes ``` maci:testslide/ (maci-add-support-for-slotted-classes) $ make ci CREATE VIRTUALENV (/Users/maci/github/testslide/venv) INSTALL BUILD DEPS INSTALL DEPS COVERAGE ERASE UNITTEST tests/accept_any_arg_unittest.py UNITTEST tests/cli_unittest.py UNITTEST tests/dsl_unittest.py UNITTEST tests/matchers_unittest.py UNITTEST tests/pep487_unittest.py UNITTEST tests/testcase_unittest.py TESTSLIDE tests/lib_testslide.py TESTSLIDE tests/mock_async_callable_testslide.py TESTSLIDE tests/mock_callable_testslide.py TESTSLIDE tests/mock_constructor_testslide.py TESTSLIDE tests/patch_attribute_testslide.py TESTSLIDE tests/strict_mock_testslide.py INSTALL pytest_testslide DEPS PYTEST pytest_testslide MYPY tests testslide util pytest-testslide FLAKE8 tests testslide util pytest-testslide ISORT tests testslide util pytest-testslide BLACK tests testslide util pytest-testslide Copyright structure intact for all python files COVERAGE COMBINE COVERAGE REPORT DOCS SDIST INSTALL LOCAL maci:testslide/ (maci-add-support-for-slotted-classes) $ ``` **Checklist**: - [x] Added tests, if you've added code that should be tested - [ ] Updated the documentation, if you've changed APIs - [x] Ensured the test suite passes - [x] Made sure your code lints - [x] Completed the Contributor License Agreement ("CLA") Differential Revision: D46856794 fbshipit-source-id: 1ef042d7057b52f0b5606deb6eb6646c203af5d0 --- tests/mock_async_callable_testslide.py | 8 ++++++++ tests/mock_callable_testslide.py | 6 ++++++ tests/sample_module.py | 13 +++++++++++++ testslide/mock_callable.py | 10 ++++++++-- 4 files changed, 35 insertions(+), 2 deletions(-) diff --git a/tests/mock_async_callable_testslide.py b/tests/mock_async_callable_testslide.py index 313c0e4b..06e55b12 100644 --- a/tests/mock_async_callable_testslide.py +++ b/tests/mock_async_callable_testslide.py @@ -90,6 +90,14 @@ async def cleanup_patches(self): ## Shared Contexts ## + @context.example + async def patching_functions_in_slotted_class(self): + t = sample_module.SomeClassWithSlots(attribute="value") + self.mock_async_callable(t, "async_method").to_return_value( + 42 + ).and_assert_called_once() + self.assertEqual(await t.async_method(), 42) + @context.shared_context def mock_async_callable_with_sync_examples(context, can_mock_with_flag=True): @context.example diff --git a/tests/mock_callable_testslide.py b/tests/mock_callable_testslide.py index 83431376..4073887f 100644 --- a/tests/mock_callable_testslide.py +++ b/tests/mock_callable_testslide.py @@ -104,6 +104,12 @@ def patching_private_functions_with_allow_private(self): ).and_assert_called_once() t._privatefun() + @context.example + def patching_functions_in_slotted_class(self): + t = sample_module.SomeClassWithSlots(attribute="value") + self.mock_callable(t, "method").to_return_value(42).and_assert_called_once() + self.assertEqual(t.method(), 42) + ## ## Shared Contexts ## diff --git a/tests/sample_module.py b/tests/sample_module.py index f4fd38e3..073a36d8 100644 --- a/tests/sample_module.py +++ b/tests/sample_module.py @@ -32,6 +32,19 @@ def instance_method_with_star_args( return 3 +class SomeClassWithSlots: + __slots__ = ("attribute",) + + def __init__(self, attribute: str) -> None: + self.attribute = attribute + + def method(self) -> int: + return 0 + + async def async_method(self) -> int: + return 0 + + class TargetStr: def __str__(self) -> str: return "original response" diff --git a/testslide/mock_callable.py b/testslide/mock_callable.py index 87cf2f4f..12fc9c9c 100644 --- a/testslide/mock_callable.py +++ b/testslide/mock_callable.py @@ -776,8 +776,14 @@ def _patch( self.type_validation or self.type_validation is None, ) - restore = self._method in self._target.__dict__ - restore_value = self._target.__dict__.get(self._method, None) + if isinstance(self._target, (Mock, StrictMock)) or not hasattr( + self._target, "__slots__" + ): + restore = self._method in self._target.__dict__ + restore_value = self._target.__dict__.get(self._method, None) + else: + restore = self._method in self._target.__slots__ + restore_value = getattr(self._target, self._method) if inspect.isclass(self._target): new_value = staticmethod(new_value) # type: ignore