From 9ba039e3996f4bf357d4827123e0b570d84f5bb6 Mon Sep 17 00:00:00 2001 From: Mario Corchero Date: Thu, 1 Jun 2017 21:19:14 +0100 Subject: [PATCH 01/13] Add new method to seal mocks The new method allows the developer to control when to stop the feature of mocks that automagically creates new mocks when accessing an attribute that was not declared before Signed-off-by: Mario Corchero --- Lib/unittest/mock.py | 35 ++++- Lib/unittest/test/testmock/testsealable.py | 144 +++++++++++++++++++++ 2 files changed, 177 insertions(+), 2 deletions(-) create mode 100644 Lib/unittest/test/testmock/testsealable.py diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index 6989dc792eb7632..9dc3a82dcd6d511 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -382,6 +382,7 @@ def __init__( __dict__['_mock_name'] = name __dict__['_mock_new_name'] = _new_name __dict__['_mock_new_parent'] = _new_parent + __dict__['_is_sealed'] = False if spec_set is not None: spec = spec_set @@ -608,7 +609,11 @@ def __getattr__(self, name): return result - def __repr__(self): + def _extract_mock_name(self): + """Extracts the mock access path as a string + + Returns the whole access chain since the root mock + """ _name_list = [self._mock_new_name] _parent = self._mock_new_parent last = self @@ -638,7 +643,10 @@ def __repr__(self): if _name_list[1] not in ('()', '().'): _first += '.' _name_list[0] = _first - name = ''.join(_name_list) + return ''.join(_name_list) + + def __repr__(self): + name = self._extract_mock_name() name_string = '' if name not in ('mock', 'mock.'): @@ -888,6 +896,12 @@ def _get_child_mock(self, **kw): klass = Mock else: klass = _type.__mro__[1] + + if self._is_sealed: + attribute = "." + kw["name"] if "name" in kw else "()" + mock_name = self._extract_mock_name() + attribute + raise AttributeError(mock_name) + return klass(**kw) @@ -2401,3 +2415,20 @@ def __get__(self, obj, obj_type): return self() def __set__(self, obj, val): self(val) + + +def seal(input_mock): + """Disables the automatic generation of "submocks" + + Given an input Mock, seals it to ensure no further mocks will be generated + when accessing an attribute that was not already defined + """ + input_mock._is_sealed = True + for attr in dir(input_mock): + try: + m = getattr(input_mock, attr) + except AttributeError: + pass + else: + if isinstance(m, NonCallableMock): + seal(m) diff --git a/Lib/unittest/test/testmock/testsealable.py b/Lib/unittest/test/testmock/testsealable.py new file mode 100644 index 000000000000000..b7a9b70ca6c1b64 --- /dev/null +++ b/Lib/unittest/test/testmock/testsealable.py @@ -0,0 +1,144 @@ +import unittest +from unittest import mock + + +class SampleObject(object): + def __init__(self): + self.attr_sample1 = 1 + self.attr_sample2 = 1 + + def method_sample1(self): + pass + + def method_sample2(self): + pass + + +class TestSealable(unittest.TestCase): + """Validates the ability to seal a mock which freezes its spec""" + + def test_attributes_return_more_mocks_by_default(self): + m = mock.Mock() + + assert isinstance(m.test, mock.Mock) + assert isinstance(m.test(), mock.Mock) + assert isinstance(m.test().test2(), mock.Mock) + + + def test_new_attributes_cannot_be_accessed_on_seal(self): + m = mock.Mock() + + mock.seal(m) + with self.assertRaises(AttributeError): + m.test + with self.assertRaises(AttributeError): + m.test() + + + def test_existing_attributes_allowed_after_seal(self): + m = mock.Mock() + + m.test.return_value = 3 + + mock.seal(m) + assert m.test() == 3 + + + def test_initialized_attributes_allowed_after_seal(self): + m = mock.Mock(test_value=1) + + mock.seal(m) + assert m.test_value == 1 + + + def test_call_on_sealed_mock_fails(self): + m = mock.Mock() + + mock.seal(m) + with self.assertRaises(AttributeError): + m() + + + def test_call_on_defined_sealed_mock_succeeds(self): + m = mock.Mock(return_value=5) + + mock.seal(m) + assert m() == 5 + + + def test_seals_recurse_on_added_attributes(self): + m = mock.Mock() + + m.test1.test2().test3 = 4 + + mock.seal(m) + assert m.test1.test2().test3 == 4 + with self.assertRaises(AttributeError): + m.test1.test2.test4 + + + def test_integration_with_spec_att_definition(self): + """You are not restricted when defining attributes on a mock with spec""" + m = mock.Mock(SampleObject) + + m.attr_sample1 = 1 + m.attr_sample3 = 3 + + mock.seal(m) + assert m.attr_sample1 == 1 + assert m.attr_sample3 == 3 + with self.assertRaises(AttributeError): + m.attr_sample2 + + + def test_integration_with_spec_method_definition(self): + """You need to defin the methods, even if they are in the spec""" + m = mock.Mock(SampleObject) + + m.method_sample1.return_value = 1 + + mock.seal(m) + assert m.method_sample1() == 1 + with self.assertRaises(AttributeError): + m.method_sample2() + + + def test_integration_with_spec_method_definition_respects_spec(self): + """You cannot define methods out of the spec""" + m = mock.Mock(SampleObject) + + with self.assertRaises(AttributeError): + m.method_sample3.return_value = 3 + + + def test_sealed_exception_has_attribute_name(self): + m = mock.Mock() + + mock.seal(m) + try: + m.SECRETE_name + except AttributeError as ex: + assert "SECRETE_name" in str(ex) + + def test_attribute_chain_is_maintained(self): + m = mock.Mock(name="mock_name") + m.test1.test2.test3.test4 + + mock.seal(m) + try: + m.test1.test2.test3.test4.boom + except AttributeError as ex: + assert "mock_name.test1.test2.test3.test4.boom" in str(ex) + + def test_call_chain_is_maintained(self): + m = mock.Mock() + m.test1().test2.test3().test4 + + mock.seal(m) + try: + m.test1().test2.test3().test4() + except AttributeError as ex: + assert "mock.test1().test2.test3().test4" in str(ex) + +if __name__ == "__main__": + unittest.main() From ba164939f6a20f690a947eaa85bd4b6948052187 Mon Sep 17 00:00:00 2001 From: Mario Corchero Date: Fri, 2 Jun 2017 00:45:00 +0100 Subject: [PATCH 02/13] Ensure attributes cannot be set on a sealed mock --- Lib/unittest/mock.py | 5 +++++ Lib/unittest/test/testmock/testsealable.py | 10 +++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index 9dc3a82dcd6d511..4c842053b1fa6af 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -713,6 +713,11 @@ def __setattr__(self, name, value): else: if _check_and_set_parent(self, value, name, name): self._mock_children[name] = value + + if self._is_sealed: + mock_name = self._extract_mock_name() + name + raise AttributeError("Cannot set " + mock_name) + return object.__setattr__(self, name, value) diff --git a/Lib/unittest/test/testmock/testsealable.py b/Lib/unittest/test/testmock/testsealable.py index b7a9b70ca6c1b64..3458908cf1686e3 100644 --- a/Lib/unittest/test/testmock/testsealable.py +++ b/Lib/unittest/test/testmock/testsealable.py @@ -32,7 +32,15 @@ def test_new_attributes_cannot_be_accessed_on_seal(self): with self.assertRaises(AttributeError): m.test with self.assertRaises(AttributeError): - m.test() + m() + + + def test_new_attributes_cannot_be_set_on_seal(self): + m = mock.Mock() + + mock.seal(m) + with self.assertRaises(AttributeError): + m.test = 1 def test_existing_attributes_allowed_after_seal(self): From 69be962997995b9efc81bb521abba486db91fdd6 Mon Sep 17 00:00:00 2001 From: Mario Corchero Date: Fri, 2 Jun 2017 00:45:16 +0100 Subject: [PATCH 03/13] Dont seal mocks which are asigned manually If an user assigns a mock to the attribute of another mock, dont recurse on those when sealing, providing a way to the users to ignore parts when performing a seal --- Lib/unittest/mock.py | 33 ++++++++++++++++------ Lib/unittest/test/testmock/testsealable.py | 12 ++++++++ 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index 4c842053b1fa6af..35fed8777fba348 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -2422,18 +2422,33 @@ def __set__(self, obj, val): self(val) -def seal(input_mock): +def seal(in_mock): """Disables the automatic generation of "submocks" Given an input Mock, seals it to ensure no further mocks will be generated - when accessing an attribute that was not already defined + when accessing an attribute that was not already defined. + + Submocks are defined as all mocks which were created DIRECTLY from the + parent. If a mock is assigned to an attribute of an existing mock, + it is not considered a submock. + + :Example: + + >>> mock = Mock() + >>> mock.submock.attribute1 = 2 + >>> mock.not_submock = mock.Mock() + >>> seal(mock) + >>> mock.submock.attribute2 # This will raise + >>> mock.not_submock.attribute2 # This won't raise + """ - input_mock._is_sealed = True - for attr in dir(input_mock): + in_mock._is_sealed = True + for attr in dir(in_mock): try: - m = getattr(input_mock, attr) + m = getattr(in_mock, attr) except AttributeError: - pass - else: - if isinstance(m, NonCallableMock): - seal(m) + continue + if not isinstance(m, NonCallableMock): + continue + if m._mock_new_parent is in_mock: + seal(m) diff --git a/Lib/unittest/test/testmock/testsealable.py b/Lib/unittest/test/testmock/testsealable.py index 3458908cf1686e3..3919130b99c4861 100644 --- a/Lib/unittest/test/testmock/testsealable.py +++ b/Lib/unittest/test/testmock/testsealable.py @@ -85,6 +85,18 @@ def test_seals_recurse_on_added_attributes(self): m.test1.test2.test4 + def test_seals_dont_recurse_on_manual_attributes(self): + m = mock.Mock(name="root_mock") + + m.test1.test2 = mock.Mock(name="not_sealed") + m.test1.test2.test3= 4 + + mock.seal(m) + assert m.test1.test2.test3 == 4 + m.test1.test2.test4 # Does not raise + m.test1.test2.test4 = 1 # Does not raise + + def test_integration_with_spec_att_definition(self): """You are not restricted when defining attributes on a mock with spec""" m = mock.Mock(SampleObject) From ed2a67028e8e3ae90e55ed1aa05cdccf38d0ba31 Mon Sep 17 00:00:00 2001 From: Mario Corchero-jimenez Date: Tue, 6 Jun 2017 19:09:05 +0100 Subject: [PATCH 04/13] Rename _is_sealed to _mock_sealed --- Lib/unittest/mock.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index 35fed8777fba348..969506578b4d3f6 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -18,6 +18,7 @@ 'NonCallableMagicMock', 'mock_open', 'PropertyMock', + 'seal', ) @@ -382,7 +383,7 @@ def __init__( __dict__['_mock_name'] = name __dict__['_mock_new_name'] = _new_name __dict__['_mock_new_parent'] = _new_parent - __dict__['_is_sealed'] = False + __dict__['_mock_sealed'] = False if spec_set is not None: spec = spec_set @@ -714,7 +715,7 @@ def __setattr__(self, name, value): if _check_and_set_parent(self, value, name, name): self._mock_children[name] = value - if self._is_sealed: + if self._mock_sealed: mock_name = self._extract_mock_name() + name raise AttributeError("Cannot set " + mock_name) @@ -902,7 +903,7 @@ def _get_child_mock(self, **kw): else: klass = _type.__mro__[1] - if self._is_sealed: + if self._mock_sealed: attribute = "." + kw["name"] if "name" in kw else "()" mock_name = self._extract_mock_name() + attribute raise AttributeError(mock_name) @@ -2442,7 +2443,7 @@ def seal(in_mock): >>> mock.not_submock.attribute2 # This won't raise """ - in_mock._is_sealed = True + in_mock._mock_sealed = True for attr in dir(in_mock): try: m = getattr(in_mock, attr) From ef61569aa08cec54735559b0af05e7ab1d6dfc62 Mon Sep 17 00:00:00 2001 From: Mario Corchero-jimenez Date: Tue, 6 Jun 2017 19:09:55 +0100 Subject: [PATCH 05/13] Rename input parameter in_mock to mock in seal --- Lib/unittest/mock.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index 969506578b4d3f6..96eff83d2c289d9 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -2423,7 +2423,7 @@ def __set__(self, obj, val): self(val) -def seal(in_mock): +def seal(mock): """Disables the automatic generation of "submocks" Given an input Mock, seals it to ensure no further mocks will be generated @@ -2443,13 +2443,13 @@ def seal(in_mock): >>> mock.not_submock.attribute2 # This won't raise """ - in_mock._mock_sealed = True - for attr in dir(in_mock): + mock._mock_sealed = True + for attr in dir(mock): try: - m = getattr(in_mock, attr) + m = getattr(mock, attr) except AttributeError: continue if not isinstance(m, NonCallableMock): continue - if m._mock_new_parent is in_mock: + if m._mock_new_parent is mock: seal(m) From 3039bcf2024b035bc9d3626b417d278283c1bcdb Mon Sep 17 00:00:00 2001 From: Mario Corchero-jimenez Date: Tue, 6 Jun 2017 19:16:42 +0100 Subject: [PATCH 06/13] Add test for magic mocks --- Lib/unittest/test/testmock/testsealable.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Lib/unittest/test/testmock/testsealable.py b/Lib/unittest/test/testmock/testsealable.py index 3919130b99c4861..2e06ab4ec415c30 100644 --- a/Lib/unittest/test/testmock/testsealable.py +++ b/Lib/unittest/test/testmock/testsealable.py @@ -85,6 +85,21 @@ def test_seals_recurse_on_added_attributes(self): m.test1.test2.test4 + def test_seals_recurse_on_magic_methods(self): + m = mock.MagicMock() + + m.test1.test2["a"].test3 = 4 + m.test1.test3[2:5].test3 = 4 + + mock.seal(m) + assert m.test1.test2["a"].test3 == 4 + assert m.test1.test2[2:5].test3 == 4 + with self.assertRaises(AttributeError): + m.test1.test2["a"].test4 + with self.assertRaises(AttributeError): + m.test1.test3[2:5].test4 + + def test_seals_dont_recurse_on_manual_attributes(self): m = mock.Mock(name="root_mock") @@ -160,5 +175,6 @@ def test_call_chain_is_maintained(self): except AttributeError as ex: assert "mock.test1().test2.test3().test4" in str(ex) + if __name__ == "__main__": unittest.main() From 4044ffa3582f528c0159680121c98a1fdd0a1596 Mon Sep 17 00:00:00 2001 From: Mario Corchero-jimenez Date: Tue, 6 Jun 2017 19:43:56 +0100 Subject: [PATCH 07/13] Allow to set existing attributes on sealed mocks --- Lib/unittest/mock.py | 4 ++-- Lib/unittest/test/testmock/testsealable.py | 22 +++++++++++++++++++++- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index 96eff83d2c289d9..8242198c96dbbc1 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -715,8 +715,8 @@ def __setattr__(self, name, value): if _check_and_set_parent(self, value, name, name): self._mock_children[name] = value - if self._mock_sealed: - mock_name = self._extract_mock_name() + name + if self._mock_sealed and not getattr(self, name): + mock_name = self._extract_mock_name() + "." + name raise AttributeError("Cannot set " + mock_name) return object.__setattr__(self, name, value) diff --git a/Lib/unittest/test/testmock/testsealable.py b/Lib/unittest/test/testmock/testsealable.py index 2e06ab4ec415c30..6d00a84ac1108c3 100644 --- a/Lib/unittest/test/testmock/testsealable.py +++ b/Lib/unittest/test/testmock/testsealable.py @@ -43,6 +43,24 @@ def test_new_attributes_cannot_be_set_on_seal(self): m.test = 1 + def test_existing_attributes_can_be_set_on_seal(self): + m = mock.Mock() + m.test.test2 = 1 + + mock.seal(m) + m.test.test2 = 2 + assert m.test.test2 == 2 + + + def test_new_attributes_cannot_be_set_on_child_of_seal(self): + m = mock.Mock() + m.test.test2 = 1 + + mock.seal(m) + with self.assertRaises(AttributeError): + m.test.test3 = 1 + + def test_existing_attributes_allowed_after_seal(self): m = mock.Mock() @@ -82,7 +100,9 @@ def test_seals_recurse_on_added_attributes(self): mock.seal(m) assert m.test1.test2().test3 == 4 with self.assertRaises(AttributeError): - m.test1.test2.test4 + m.test1.test2().test4 + with self.assertRaises(AttributeError): + m.test1.test3 def test_seals_recurse_on_magic_methods(self): From 0a9bf3f4e1325ff4fc7453e3b2c808ae741fb00c Mon Sep 17 00:00:00 2001 From: Mario Corchero-jimenez Date: Tue, 6 Jun 2017 19:49:18 +0100 Subject: [PATCH 08/13] Rename testsealable to test_sealable --- Lib/unittest/test/testmock/{testsealable.py => test_sealable.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Lib/unittest/test/testmock/{testsealable.py => test_sealable.py} (100%) diff --git a/Lib/unittest/test/testmock/testsealable.py b/Lib/unittest/test/testmock/test_sealable.py similarity index 100% rename from Lib/unittest/test/testmock/testsealable.py rename to Lib/unittest/test/testmock/test_sealable.py From e23d151f86a5c85ce229985a9b836af3af260912 Mon Sep 17 00:00:00 2001 From: Mario Corchero-jimenez Date: Tue, 6 Jun 2017 19:58:33 +0100 Subject: [PATCH 09/13] Fix pep8 violations in test_sealed --- Lib/unittest/test/testmock/test_sealable.py | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/Lib/unittest/test/testmock/test_sealable.py b/Lib/unittest/test/testmock/test_sealable.py index 6d00a84ac1108c3..ecaffe3010a1755 100644 --- a/Lib/unittest/test/testmock/test_sealable.py +++ b/Lib/unittest/test/testmock/test_sealable.py @@ -24,7 +24,6 @@ def test_attributes_return_more_mocks_by_default(self): assert isinstance(m.test(), mock.Mock) assert isinstance(m.test().test2(), mock.Mock) - def test_new_attributes_cannot_be_accessed_on_seal(self): m = mock.Mock() @@ -34,7 +33,6 @@ def test_new_attributes_cannot_be_accessed_on_seal(self): with self.assertRaises(AttributeError): m() - def test_new_attributes_cannot_be_set_on_seal(self): m = mock.Mock() @@ -42,7 +40,6 @@ def test_new_attributes_cannot_be_set_on_seal(self): with self.assertRaises(AttributeError): m.test = 1 - def test_existing_attributes_can_be_set_on_seal(self): m = mock.Mock() m.test.test2 = 1 @@ -51,7 +48,6 @@ def test_existing_attributes_can_be_set_on_seal(self): m.test.test2 = 2 assert m.test.test2 == 2 - def test_new_attributes_cannot_be_set_on_child_of_seal(self): m = mock.Mock() m.test.test2 = 1 @@ -60,7 +56,6 @@ def test_new_attributes_cannot_be_set_on_child_of_seal(self): with self.assertRaises(AttributeError): m.test.test3 = 1 - def test_existing_attributes_allowed_after_seal(self): m = mock.Mock() @@ -69,14 +64,12 @@ def test_existing_attributes_allowed_after_seal(self): mock.seal(m) assert m.test() == 3 - def test_initialized_attributes_allowed_after_seal(self): m = mock.Mock(test_value=1) mock.seal(m) assert m.test_value == 1 - def test_call_on_sealed_mock_fails(self): m = mock.Mock() @@ -84,14 +77,12 @@ def test_call_on_sealed_mock_fails(self): with self.assertRaises(AttributeError): m() - def test_call_on_defined_sealed_mock_succeeds(self): m = mock.Mock(return_value=5) mock.seal(m) assert m() == 5 - def test_seals_recurse_on_added_attributes(self): m = mock.Mock() @@ -104,7 +95,6 @@ def test_seals_recurse_on_added_attributes(self): with self.assertRaises(AttributeError): m.test1.test3 - def test_seals_recurse_on_magic_methods(self): m = mock.MagicMock() @@ -119,21 +109,19 @@ def test_seals_recurse_on_magic_methods(self): with self.assertRaises(AttributeError): m.test1.test3[2:5].test4 - def test_seals_dont_recurse_on_manual_attributes(self): m = mock.Mock(name="root_mock") m.test1.test2 = mock.Mock(name="not_sealed") - m.test1.test2.test3= 4 + m.test1.test2.test3 = 4 mock.seal(m) assert m.test1.test2.test3 == 4 m.test1.test2.test4 # Does not raise m.test1.test2.test4 = 1 # Does not raise - def test_integration_with_spec_att_definition(self): - """You are not restricted when defining attributes on a mock with spec""" + """You are not restricted when using mock with spec""" m = mock.Mock(SampleObject) m.attr_sample1 = 1 @@ -145,7 +133,6 @@ def test_integration_with_spec_att_definition(self): with self.assertRaises(AttributeError): m.attr_sample2 - def test_integration_with_spec_method_definition(self): """You need to defin the methods, even if they are in the spec""" m = mock.Mock(SampleObject) @@ -157,7 +144,6 @@ def test_integration_with_spec_method_definition(self): with self.assertRaises(AttributeError): m.method_sample2() - def test_integration_with_spec_method_definition_respects_spec(self): """You cannot define methods out of the spec""" m = mock.Mock(SampleObject) @@ -165,7 +151,6 @@ def test_integration_with_spec_method_definition_respects_spec(self): with self.assertRaises(AttributeError): m.method_sample3.return_value = 3 - def test_sealed_exception_has_attribute_name(self): m = mock.Mock() From d94dbf57128b0af3760d6142ec54ec0b74d82b4f Mon Sep 17 00:00:00 2001 From: Mario Corchero-jimenez Date: Tue, 6 Jun 2017 20:28:29 +0100 Subject: [PATCH 10/13] Move seal example to docs --- Doc/library/unittest.mock.rst | 20 ++++++++++++++++++++ Lib/unittest/mock.py | 9 --------- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/Doc/library/unittest.mock.rst b/Doc/library/unittest.mock.rst index 9e8bf11a92d45a2..f7bd7f2148f98a8 100644 --- a/Doc/library/unittest.mock.rst +++ b/Doc/library/unittest.mock.rst @@ -2365,3 +2365,23 @@ alternative object as the *autospec* argument: a mocked class to create a mock instance *does not* create a real instance. It is only attribute lookups - along with calls to :func:`dir` - that are done. +Sealing mocks +~~~~~~~~~~~~~ + +.. function:: seal(mock) + + Seal will disable the creation of mock children by preventing to get or set + any new attribute on the sealed mock. The sealing process is performed recursively. + + If a mock instance is assigned to an attribute instead of being dynamically created + it wont be considered in the sealing chain. This allows to prevent seal from fixing + part of the mock object. + + >>> mock = Mock() + >>> mock.submock.attribute1 = 2 + >>> mock.not_submock = mock.Mock() + >>> seal(mock) + >>> mock.submock.attribute2 # This will raise + >>> mock.not_submock.attribute2 # This won't raise + +.. versionadded:: 3.7 diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index 8242198c96dbbc1..8483a399ff28d7f 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -2433,15 +2433,6 @@ def seal(mock): parent. If a mock is assigned to an attribute of an existing mock, it is not considered a submock. - :Example: - - >>> mock = Mock() - >>> mock.submock.attribute1 = 2 - >>> mock.not_submock = mock.Mock() - >>> seal(mock) - >>> mock.submock.attribute2 # This will raise - >>> mock.not_submock.attribute2 # This won't raise - """ mock._mock_sealed = True for attr in dir(mock): From 0b908ae407c18bee7f43748324ee119c68eeadec Mon Sep 17 00:00:00 2001 From: Mario Corchero-jimenez Date: Fri, 16 Jun 2017 08:57:55 +0100 Subject: [PATCH 11/13] Minor fixes in the docs --- Doc/library/unittest.mock.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Doc/library/unittest.mock.rst b/Doc/library/unittest.mock.rst index f7bd7f2148f98a8..6fdfdc4fa1cec40 100644 --- a/Doc/library/unittest.mock.rst +++ b/Doc/library/unittest.mock.rst @@ -2381,7 +2381,7 @@ Sealing mocks >>> mock.submock.attribute1 = 2 >>> mock.not_submock = mock.Mock() >>> seal(mock) - >>> mock.submock.attribute2 # This will raise - >>> mock.not_submock.attribute2 # This won't raise + >>> mock.submock.attribute2 # This will raise AttributeError. + >>> mock.not_submock.attribute2 # This won't raise. -.. versionadded:: 3.7 + .. versionadded:: 3.7 From b8fc6b70bde0c334835e2a26ba236038f169038c Mon Sep 17 00:00:00 2001 From: Mario Corchero-jimenez Date: Fri, 16 Jun 2017 09:23:21 +0100 Subject: [PATCH 12/13] Address comments from last review --- Lib/unittest/mock.py | 12 ++--- .../{test_sealable.py => testsealable.py} | 46 +++++++++---------- 2 files changed, 25 insertions(+), 33 deletions(-) rename Lib/unittest/test/testmock/{test_sealable.py => testsealable.py} (79%) diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index 8483a399ff28d7f..d82f0efdb276e3e 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -611,10 +611,6 @@ def __getattr__(self, name): def _extract_mock_name(self): - """Extracts the mock access path as a string - - Returns the whole access chain since the root mock - """ _name_list = [self._mock_new_name] _parent = self._mock_new_parent last = self @@ -715,9 +711,9 @@ def __setattr__(self, name, value): if _check_and_set_parent(self, value, name, name): self._mock_children[name] = value - if self._mock_sealed and not getattr(self, name): - mock_name = self._extract_mock_name() + "." + name - raise AttributeError("Cannot set " + mock_name) + if self._mock_sealed and not hasattr(self, name): + mock_name = f'{self._extract_mock_name()}.{name}' + raise AttributeError(f'Cannot set {mock_name}') return object.__setattr__(self, name, value) @@ -2424,7 +2420,7 @@ def __set__(self, obj, val): def seal(mock): - """Disables the automatic generation of "submocks" + """Disable the automatic generation of "submocks" Given an input Mock, seals it to ensure no further mocks will be generated when accessing an attribute that was not already defined. diff --git a/Lib/unittest/test/testmock/test_sealable.py b/Lib/unittest/test/testmock/testsealable.py similarity index 79% rename from Lib/unittest/test/testmock/test_sealable.py rename to Lib/unittest/test/testmock/testsealable.py index ecaffe3010a1755..0e72b32411c646e 100644 --- a/Lib/unittest/test/testmock/test_sealable.py +++ b/Lib/unittest/test/testmock/testsealable.py @@ -2,7 +2,7 @@ from unittest import mock -class SampleObject(object): +class SampleObject: def __init__(self): self.attr_sample1 = 1 self.attr_sample2 = 1 @@ -15,14 +15,13 @@ def method_sample2(self): class TestSealable(unittest.TestCase): - """Validates the ability to seal a mock which freezes its spec""" def test_attributes_return_more_mocks_by_default(self): m = mock.Mock() - assert isinstance(m.test, mock.Mock) - assert isinstance(m.test(), mock.Mock) - assert isinstance(m.test().test2(), mock.Mock) + self.assertIsInstance(m.test, mock.Mock) + self.assertIsInstance(m.test(), mock.Mock) + self.assertIsInstance(m.test().test2(), mock.Mock) def test_new_attributes_cannot_be_accessed_on_seal(self): m = mock.Mock() @@ -46,7 +45,7 @@ def test_existing_attributes_can_be_set_on_seal(self): mock.seal(m) m.test.test2 = 2 - assert m.test.test2 == 2 + self.assertEqual(m.test.test2, 2) def test_new_attributes_cannot_be_set_on_child_of_seal(self): m = mock.Mock() @@ -62,13 +61,13 @@ def test_existing_attributes_allowed_after_seal(self): m.test.return_value = 3 mock.seal(m) - assert m.test() == 3 + self.assertEqual(m.test(), 3) def test_initialized_attributes_allowed_after_seal(self): m = mock.Mock(test_value=1) mock.seal(m) - assert m.test_value == 1 + self.assertEqual(m.test_value, 1) def test_call_on_sealed_mock_fails(self): m = mock.Mock() @@ -81,7 +80,7 @@ def test_call_on_defined_sealed_mock_succeeds(self): m = mock.Mock(return_value=5) mock.seal(m) - assert m() == 5 + self.assertEqual(m(), 5) def test_seals_recurse_on_added_attributes(self): m = mock.Mock() @@ -89,7 +88,7 @@ def test_seals_recurse_on_added_attributes(self): m.test1.test2().test3 = 4 mock.seal(m) - assert m.test1.test2().test3 == 4 + self.assertEqual(m.test1.test2().test3, 4) with self.assertRaises(AttributeError): m.test1.test2().test4 with self.assertRaises(AttributeError): @@ -102,8 +101,8 @@ def test_seals_recurse_on_magic_methods(self): m.test1.test3[2:5].test3 = 4 mock.seal(m) - assert m.test1.test2["a"].test3 == 4 - assert m.test1.test2[2:5].test3 == 4 + self.assertEqual(m.test1.test2["a"].test3, 4) + self.assertEqual(m.test1.test2[2:5].test3, 4) with self.assertRaises(AttributeError): m.test1.test2["a"].test4 with self.assertRaises(AttributeError): @@ -116,7 +115,7 @@ def test_seals_dont_recurse_on_manual_attributes(self): m.test1.test2.test3 = 4 mock.seal(m) - assert m.test1.test2.test3 == 4 + self.assertEqual(m.test1.test2.test3, 4) m.test1.test2.test4 # Does not raise m.test1.test2.test4 = 1 # Does not raise @@ -128,8 +127,8 @@ def test_integration_with_spec_att_definition(self): m.attr_sample3 = 3 mock.seal(m) - assert m.attr_sample1 == 1 - assert m.attr_sample3 == 3 + self.assertEqual(m.attr_sample1, 1) + self.assertEqual(m.attr_sample3, 3) with self.assertRaises(AttributeError): m.attr_sample2 @@ -140,7 +139,7 @@ def test_integration_with_spec_method_definition(self): m.method_sample1.return_value = 1 mock.seal(m) - assert m.method_sample1() == 1 + self.assertEqual(m.method_sample1(), 1) with self.assertRaises(AttributeError): m.method_sample2() @@ -155,30 +154,27 @@ def test_sealed_exception_has_attribute_name(self): m = mock.Mock() mock.seal(m) - try: + with self.assertRaises(AttributeError) as cm: m.SECRETE_name - except AttributeError as ex: - assert "SECRETE_name" in str(ex) + self.assertIn("SECRETE_name", str(cm.exception)) def test_attribute_chain_is_maintained(self): m = mock.Mock(name="mock_name") m.test1.test2.test3.test4 mock.seal(m) - try: + with self.assertRaises(AttributeError) as cm: m.test1.test2.test3.test4.boom - except AttributeError as ex: - assert "mock_name.test1.test2.test3.test4.boom" in str(ex) + self.assertIn("mock_name.test1.test2.test3.test4.boom", str(cm.exception)) def test_call_chain_is_maintained(self): m = mock.Mock() m.test1().test2.test3().test4 mock.seal(m) - try: + with self.assertRaises(AttributeError) as cm: m.test1().test2.test3().test4() - except AttributeError as ex: - assert "mock.test1().test2.test3().test4" in str(ex) + self.assertIn("mock.test1().test2.test3().test4", str(cm.exception)) if __name__ == "__main__": From 2b9a27100f5eb2bfa9c2efa549e5d39fb37246e1 Mon Sep 17 00:00:00 2001 From: Mario Corchero Date: Tue, 17 Oct 2017 12:19:30 +0100 Subject: [PATCH 13/13] Add entry in News --- Doc/whatsnew/3.7.rst | 5 +++++ .../next/Library/2017-10-17-12-04-37.bpo-30541.q3BM6C.rst | 2 ++ 2 files changed, 7 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2017-10-17-12-04-37.bpo-30541.q3BM6C.rst diff --git a/Doc/whatsnew/3.7.rst b/Doc/whatsnew/3.7.rst index 761c85fd22084bb..932ba82cd5f5cdd 100644 --- a/Doc/whatsnew/3.7.rst +++ b/Doc/whatsnew/3.7.rst @@ -177,6 +177,11 @@ The :const:`~unittest.mock.sentinel` attributes now preserve their identity when they are :mod:`copied ` or :mod:`pickled `. (Contributed by Serhiy Storchaka in :issue:`20804`.) +New function :const:`~unittest.mock.seal` will disable the creation of mock +children by preventing to get or set any new attribute on the sealed mock. +The sealing process is performed recursively. (Contributed by Mario Corchero +in :issue:`30541`.) + xmlrpc.server ------------- diff --git a/Misc/NEWS.d/next/Library/2017-10-17-12-04-37.bpo-30541.q3BM6C.rst b/Misc/NEWS.d/next/Library/2017-10-17-12-04-37.bpo-30541.q3BM6C.rst new file mode 100644 index 000000000000000..7eb5e16faa0c842 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2017-10-17-12-04-37.bpo-30541.q3BM6C.rst @@ -0,0 +1,2 @@ +Add new function to seal a mock and prevent the automatically creation of +child mocks. Patch by Mario Corchero.