Spying on pydantic objects
alex-linx opened this issue · 3 comments
alex-linx commented
Hey, the other day I was writing some tests for some pydantic objects, trying to use spy.
Apparently it is not possible to spy on methods of a pydantic object, probably because they override __getattr__
and __setattr__
..
Here is a minimal example
from pydantic import BaseModel
class MyModel(BaseModel):
def my_method(self):
pass
def test_my_model(mocker):
my_model = MyModel()
spy = mocker.spy(my_model, "my_method")
my_model.my_method()
spy.assert_called_once()
When running pytest I get an error on the line spy = mocker.spy(my_model, "my_method")
.
dummy.py::test_my_model FAILED [100%]
dummy.py:8 (test_my_model)
self = <unittest.mock._patch object at 0x10478b490>
def __enter__(self):
"""Perform the patch."""
new, spec, spec_set = self.new, self.spec, self.spec_set
autospec, kwargs = self.autospec, self.kwargs
new_callable = self.new_callable
self.target = self.getter()
# normalise False to None
if spec is False:
spec = None
if spec_set is False:
spec_set = None
if autospec is False:
autospec = None
if spec is not None and autospec is not None:
raise TypeError("Can't specify spec and autospec")
if ((spec is not None or autospec is not None) and
spec_set not in (True, None)):
raise TypeError("Can't provide explicit spec_set *and* spec or autospec")
original, local = self.get_original()
if new is DEFAULT and autospec is None:
inherit = False
if spec is True:
# set spec to the object we are replacing
spec = original
if spec_set is True:
spec_set = original
spec = None
elif spec is not None:
if spec_set is True:
spec_set = spec
spec = None
elif spec_set is True:
spec_set = original
if spec is not None or spec_set is not None:
if original is DEFAULT:
raise TypeError("Can't use 'spec' with create=True")
if isinstance(original, type):
# If we're patching out a class and there is a spec
inherit = True
if spec is None and _is_async_obj(original):
Klass = AsyncMock
else:
Klass = MagicMock
_kwargs = {}
if new_callable is not None:
Klass = new_callable
elif spec is not None or spec_set is not None:
this_spec = spec
if spec_set is not None:
this_spec = spec_set
if _is_list(this_spec):
not_callable = '__call__' not in this_spec
else:
not_callable = not callable(this_spec)
if _is_async_obj(this_spec):
Klass = AsyncMock
elif not_callable:
Klass = NonCallableMagicMock
if spec is not None:
_kwargs['spec'] = spec
if spec_set is not None:
_kwargs['spec_set'] = spec_set
# add a name to mocks
if (isinstance(Klass, type) and
issubclass(Klass, NonCallableMock) and self.attribute):
_kwargs['name'] = self.attribute
_kwargs.update(kwargs)
new = Klass(**_kwargs)
if inherit and _is_instance_mock(new):
# we can only tell if the instance should be callable if the
# spec is not a list
this_spec = spec
if spec_set is not None:
this_spec = spec_set
if (not _is_list(this_spec) and not
_instance_callable(this_spec)):
Klass = NonCallableMagicMock
_kwargs.pop('name')
new.return_value = Klass(_new_parent=new, _new_name='()',
**_kwargs)
elif autospec is not None:
# spec is ignored, new *must* be default, spec_set is treated
# as a boolean. Should we check spec is not None and that spec_set
# is a bool?
if new is not DEFAULT:
raise TypeError(
"autospec creates the mock for you. Can't specify "
"autospec and new."
)
if original is DEFAULT:
raise TypeError("Can't use 'autospec' with create=True")
spec_set = bool(spec_set)
if autospec is True:
autospec = original
new = create_autospec(autospec, spec_set=spec_set,
_name=self.attribute, **kwargs)
elif kwargs:
# can't set keyword args when we aren't creating the mock
# XXXX If new is a Mock we could call new.configure_mock(**kwargs)
raise TypeError("Can't pass kwargs to a mock we aren't creating")
new_attr = new
self.temp_original = original
self.is_local = local
self._exit_stack = contextlib.ExitStack()
try:
> setattr(self.target, self.attribute, new_attr)
../../../../../.pyenv/versions/3.9.16/lib/python3.9/unittest/mock.py:1501:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
> ???
E ValueError: "MyModel" object has no field "my_method"
pydantic/main.py:357: ValueError
During handling of the above exception, another exception occurred:
mocker = <pytest_mock.plugin.MockerFixture object at 0x10478b1f0>
def test_my_model(mocker):
my_model = MyModel()
> spy = mocker.spy(my_model, "my_method")
dummy.py:11:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../.venv/lib/python3.9/site-packages/pytest_mock/plugin.py:163: in spy
spy_obj = self.patch.object(obj, name, side_effect=wrapped, autospec=autospec)
../../.venv/lib/python3.9/site-packages/pytest_mock/plugin.py:249: in object
return self._start_patch(
../../.venv/lib/python3.9/site-packages/pytest_mock/plugin.py:214: in _start_patch
mocked = p.start() # type: unittest.mock.MagicMock
../../../../../.pyenv/versions/3.9.16/lib/python3.9/unittest/mock.py:1540: in start
result = self.__enter__()
../../../../../.pyenv/versions/3.9.16/lib/python3.9/unittest/mock.py:1514: in __enter__
if not self.__exit__(*sys.exc_info()):
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <unittest.mock._patch object at 0x10478b490>
exc_info = (<class 'ValueError'>, ValueError('"MyModel" object has no field "my_method"'), <traceback object at 0x10478ea80>)
def __exit__(self, *exc_info):
"""Undo the patch."""
if self.is_local and self.temp_original is not DEFAULT:
setattr(self.target, self.attribute, self.temp_original)
else:
> delattr(self.target, self.attribute)
E AttributeError: my_method
../../../../../.pyenv/versions/3.9.16/lib/python3.9/unittest/mock.py:1522: AttributeError
nicoddemus commented
Hi @alex-linx,
Indeed if pydantic overwrites __setattr__
it might not be possible for spy
to work.
spy
is just a thin wrapper which calls patch.object
with a side_effect
which calls the original method.
If you can reproduce the same error with:
with patch.object(my_model, "my_method", side_effect=my_model.my_method):
...
Then I don't think there's no action for pytest-mock
.
Closing for now, feel free to follow with further questions.