New submission from Frank Harrison <fr...@doublethefish.com>:
This is my first bug logged here, I've tried to follow the guideline and search for this issue; please let me know if I missed anything. Summary: unittest.mock.MagicMock has a regression starting in 3.8. The regression was only tested on latest non-prerelease versions of python 3.5, 3.6, 3.7, 3.8 and 3.9. Tested on OSX and Fedora 31. Repro: ------ If you create an instance of a MagicMock specialisation with parameters to __init__(), you can no longer pass that instance to the __init__() function of another MagicMock object e.g. a base-class is replaced with MagicMock. See the unittests bellow for more details, use-cases and fail situations. What happens: ------------- Here's a python3.9 example traceback. It may be worth noting that there is a difference in the tracebacks between 3.8 and 3.9. Traceback (most recent call last): File "<...>", line <..>, in test_raw_magic_moc_passing_thru_single_pos mock_object = mock.MagicMock(mock_param) # error is here, instantiating another object File "/usr/lib64/python3.9/unittest/mock.py", line 408, in __new__ if spec_arg and _is_async_obj(spec_arg): File "/usr/lib64/python3.9/unittest/mock.py", line 2119, in __get__ return self.create_mock() File "/usr/lib64/python3.9/unittest/mock.py", line 2112, in create_mock m = parent._get_child_mock(name=entry, _new_name=entry, File "/usr/lib64/python3.9/unittest/mock.py", line 1014, in _get_child_mock return klass(**kw) TypeError: __init__() got an unexpected keyword argument 'name' Code demonstrating the problem: ------------------------------- import unittest from unittest import mock class TestMockMagicAssociativeHierarchies(unittest.TestCase): """ Mimicing real-world testing where we mock a base-class The intent here is to demonstrate some of the requirements of associative- hierarchies e.g. where a class may have its associative-parent set at run-time, rather that defining it via a class-hierarchy. Obviously this also needs to work with class-hierarchies, that is an associative-parent is likely to be a specialisation of some interface, usually one that is being mocked. For example tkinter and Qt have both a class-hierarchy and a layout- hierarchy; the latter is modifyable at runtime. Most of the tests here mimic a specialisation of an upstream object (say a tk.Frame class), instantiating that specialisation and then passing it to another object. The reason behind this is an observed regression in Python 3.8. """ def test_raw_magic_moc_passing_thru_no_params(self): """ REGRESSION: Python3.8 (inc Python3.9) Create a mocked specialisation passing it to another mock. One real-world use-case for this is simple cases where we simply want to define a new convenience type that forces a default configuration of the inherited type (calls super().__init__()). """ class MockSubCallsParentInit(mock.MagicMock): def __init__(self): super().__init__() # intentionally empty mock_param = MockSubCallsParentInit() mock_object = mock.MagicMock(mock_param) # error is here, instantiating another object self.assertIsInstance(mock_object, mock.MagicMock) def test_raw_magic_moc_passing_thru_single_pos(self): """ REGRESSION: Python3.8 (inc Python3.9) Same as test_raw_magic_moc_no_init_params() but we want to specialise with positional arguments. """ class MockSubCallsParentInitWithPositionalParam(mock.MagicMock): def __init__(self): super().__init__("specialise init calls") mock_param = MockSubCallsParentInitWithPositionalParam() mock_object = mock.MagicMock(mock_param) # error is here, instantiating another object self.assertIsInstance(mock_object, mock.MagicMock) def test_raw_magic_moc_passing_thru_single_kwarg(self): """ REGRESSION: Python3.8 (inc Python3.9) Same as test_raw_magic_moc_passing_thru_single_pos() but we want to specialise with a key-word argument. """ class MockSubCallsParentInitWithPositionalParam(mock.MagicMock): def __init__(self): super().__init__(__some_key_word__="some data") mock_param = MockSubCallsParentInitWithPositionalParam() mock_object = mock.MagicMock(mock_param) # error is here, instantiating another object self.assertIsInstance(mock_object, mock.MagicMock) def test_mock_as_param_no_inheritance(self): """ PASSES Mimic mocking out types, without type specialisation. for example in pseudo code tk.Frame = mock.MagicMock; tk.Frame(t.Frame) """ mock_param = mock.MagicMock() mock_object = mock.MagicMock(mock_param) self.assertIsInstance(mock_object, mock.MagicMock) def test_mock_as_param_no_init_override(self): """ PASSES Leaves the __init__() function behaviour as default; should always work. Note that we do not specialise member functions. Although the intent here is similar to the one captured by test_raw_magic_moc_passing_thru_no_params(), this is a less likely usecase, although it does happen, but is here for completeness """ class MockSub(mock.MagicMock): pass mock_param = MockSub() mock_object = mock.MagicMock(mock_param) self.assertIsInstance(mock_object, mock.MagicMock) def test_init_with_args_n_kwargs_passthru(self): """ PASSES Intended to be the same as test_mock_as_param_no_init_override as well as a base-test for ithe usecases where a user will define more complex behaviours such as key-word modification, member-variable definitions and so on. """ class MockSubInitPassThruArgsNKwargs(mock.MagicMock): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # intentionally redundant mock_param = MockSubInitPassThruArgsNKwargs() mock_object = mock.MagicMock(mock_param) self.assertIsInstance(mock_object, mock.MagicMock) def test_init_with_args_n_kwargs_modify_kwargs(self): """ PASSES Same as test_init_with_args_n_kwargs_passthru() but modifies the kwargs dict on the way through the __init__() function. """ class MockSubModifyKwargs(mock.MagicMock): def __init__(self, *args, **kwargs): kwargs["__kw args added__"] = "test value" super().__init__(*args, **kwargs) mock_param = MockSubModifyKwargs() mock_object = mock.MagicMock(mock_param) self.assertIsInstance(mock_object, mock.MagicMock) def test_init_with_args_n_kwargs_modify_args(self): """ PASSES Same as test_init_with_args_n_kwargs_passthru() but modifies the args on their way through the __init__() function. """ class MockSubModifyArgs(mock.MagicMock): def __init__(self, *args, **kwargs): super().__init__("test value", *args, **kwargs) mock_param = MockSubModifyArgs() mock_object = mock.MagicMock(mock_param) self.assertIsInstance(mock_object, mock.MagicMock) ---------- components: Library (Lib) messages: 361552 nosy: Frank Harrison priority: normal severity: normal status: open title: MagicMock specialisation instance can no longer be passed to new MagicMock instance versions: Python 3.8, Python 3.9 _______________________________________ Python tracker <rep...@bugs.python.org> <https://bugs.python.org/issue39578> _______________________________________ _______________________________________________ Python-bugs-list mailing list Unsubscribe: https://mail.python.org/mailman/options/python-bugs-list/archive%40mail-archive.com