Skip to content

Conversation

@mariocj89
Copy link
Contributor

@mariocj89 mariocj89 commented Jun 2, 2017

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.

There is an alternative implementation proposed in issue30541 using classes, but this seems much nicer.

Happy to rename seal to freeze, lock or any other similar name.

https://bugs.python.org/issue30541

mariocj89 added 2 commits June 1, 2017 21:48
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 <mariocj89@gmail.com>
@mention-bot
Copy link

@mariocj89, thanks for your PR! By analyzing the history of the files in this pull request, we identified @mfoord, @kushaldas and @berkerpeksag to be potential reviewers.

@Mariatta Mariatta added the type-feature A feature request or enhancement label Jun 3, 2017
@lkollar
Copy link
Contributor

lkollar commented Jun 3, 2017

👍

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
@mariocj89 mariocj89 force-pushed the sealablemock_method branch from ebb5959 to 69be962 Compare June 5, 2017 19:43
__dict__['_mock_name'] = name
__dict__['_mock_new_name'] = _new_name
__dict__['_mock_new_parent'] = _new_parent
__dict__['_is_sealed'] = False
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest '_mock_sealed' key instead to remain consistent and reduce the risk of name collision.


if self._is_sealed:
mock_name = self._extract_mock_name() + name
raise AttributeError("Cannot set " + mock_name)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just using repr() here?

"Cannot set %r" % self

Copy link
Contributor Author

@mariocj89 mariocj89 Jun 6, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just realized that this wont allow to set even existing attributes, as in the following will fail:

        m = mock.Mock()
        m.test.test2 = 1
        mock.seal(m)
        m.test.test2 = 2

To be consistent with the logic on gettr, it is probably better to dont allow to set new attributes. I've changed it to do so in 4044ffa

With that change, repr does not contain the attribute that caused the issue.

mock.test1.test3.test4 = 1 will give:

Using repr:
AttributeError(Cannot set <MagicMock name='mock.test1.test3' id='4337541584'>)

Using mock_name:
AttributeError(Cannot set mock.test1.test3.test4)

Note the former lacks test4

self(val)


def seal(in_mock):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use "mock" parameter name, see mock_open().

if not isinstance(m, NonCallableMock):
continue
if m._mock_new_parent is in_mock:
seal(m)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it work to use _mock_methods and _mock_children attributes instead of dir()?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It wont recurse on magic methods, and not only for magic mocks but also for __call__ for example.

The following won't work:

x = Mock()
x.return_value.a = 1
seal(x)
x().a  # this will raise

I can try to make it work if you prefer it that way, use those two attributes and manually add return_value and magic methods.

@@ -0,0 +1,164 @@
import unittest
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add "_" in the filename. For example, rename it to test_seal.py.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, didn't add it to be consistent with the rest of the tests in testmock folder.

I can change the others as well in a separate commit if you want to.

assert isinstance(m.test, mock.Mock)
assert isinstance(m.test(), mock.Mock)
assert isinstance(m.test().test2(), mock.Mock)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PEP8: only one empty line between methods.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wops, indeed, blindly converted them from plain functions in pytest and missed it, should have lint it before submitting it again sorry.

Confirmed the score for mock.py hasn't go down (prev run = master):
pylint: Your code has been rated at 7.99/10 (previous run: 7.98/10, +0.01)

New code follows pep8:

$ pep8 unittest/test/testmock/test_sealable.py  && echo OK
OK

>>> seal(mock)
>>> mock.submock.attribute2 # This will raise
>>> mock.not_submock.attribute2 # This won't raise
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please move this doc to Doc/library/unittest.mock.rst. Don't forget the ".. versionadded:: 3.7" marker.

Copy link
Contributor Author

@mariocj89 mariocj89 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated! Plus some answers.

Thanks for having a look @Haypo

if not isinstance(m, NonCallableMock):
continue
if m._mock_new_parent is in_mock:
seal(m)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It wont recurse on magic methods, and not only for magic mocks but also for __call__ for example.

The following won't work:

x = Mock()
x.return_value.a = 1
seal(x)
x().a  # this will raise

I can try to make it work if you prefer it that way, use those two attributes and manually add return_value and magic methods.


if self._is_sealed:
mock_name = self._extract_mock_name() + name
raise AttributeError("Cannot set " + mock_name)
Copy link
Contributor Author

@mariocj89 mariocj89 Jun 6, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just realized that this wont allow to set even existing attributes, as in the following will fail:

        m = mock.Mock()
        m.test.test2 = 1
        mock.seal(m)
        m.test.test2 = 2

To be consistent with the logic on gettr, it is probably better to dont allow to set new attributes. I've changed it to do so in 4044ffa

With that change, repr does not contain the attribute that caused the issue.

mock.test1.test3.test4 = 1 will give:

Using repr:
AttributeError(Cannot set <MagicMock name='mock.test1.test3' id='4337541584'>)

Using mock_name:
AttributeError(Cannot set mock.test1.test3.test4)

Note the former lacks test4

@@ -0,0 +1,164 @@
import unittest
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, didn't add it to be consistent with the rest of the tests in testmock folder.

I can change the others as well in a separate commit if you want to.

assert isinstance(m.test, mock.Mock)
assert isinstance(m.test(), mock.Mock)
assert isinstance(m.test().test2(), mock.Mock)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wops, indeed, blindly converted them from plain functions in pytest and missed it, should have lint it before submitting it again sorry.

Confirmed the score for mock.py hasn't go down (prev run = master):
pylint: Your code has been rated at 7.99/10 (previous run: 7.98/10, +0.01)

New code follows pep8:

$ pep8 unittest/test/testmock/test_sealable.py  && echo OK
OK

@vstinner
Copy link
Member

While the change LGTM, I would prefer to see a review of someone who knows the unittest module better than me :-( @vadmium or @berkerpeksag maybe? I don't know. @voidspace: are you still around? :-)

vstinner
vstinner previously approved these changes Jun 15, 2017
>>> mock.submock.attribute2 # This will raise
>>> mock.not_submock.attribute2 # This won't raise

.. versionadded:: 3.7
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indentation is off here. Please indent it like the following:

.. function:: seal(mock)
    ...

    .. versionadded:: 3.7

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

>>> mock.submock.attribute1 = 2
>>> mock.not_submock = mock.Mock()
>>> seal(mock)
>>> mock.submock.attribute2 # This will raise
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd change it to # This will raise AttributeError..

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done


if self._mock_sealed and not getattr(self, name):
mock_name = self._extract_mock_name() + "." + name
raise AttributeError("Cannot set " + mock_name)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could use an f-string here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

self._mock_children[name] = value

if self._mock_sealed and not getattr(self, name):
mock_name = self._extract_mock_name() + "." + name
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most of the module use single quotes. It would be nice to follow the existing style.

if _check_and_set_parent(self, value, name, name):
self._mock_children[name] = value

if self._mock_sealed and not getattr(self, name):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you want to use hasattr here? getattr(self, name) may raise an exception.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

from unittest import mock


class SampleObject(object):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: class SampleObject:

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done



class TestSealable(unittest.TestCase):
"""Validates the ability to seal a mock which freezes its spec"""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to add a docstring here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

def test_attributes_return_more_mocks_by_default(self):
m = mock.Mock()

assert isinstance(m.test, mock.Mock)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use standard unittest assert methods, assertIsInstance in this case.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done


mock.seal(m)
try:
m.SECRETE_name
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the following pattern is more Pythonic and we use it in the CPython test suite:

with self.assertRaises(AttributeError) as cm:
    m.SECRETE_name
self.assertIn('SECRET_name', str(cm.exception))

@@ -0,0 +1,185 @@
import unittest
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please rename file name to testsealable.py to follow the mock module standard.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@mariocj89
Copy link
Contributor Author

@berkerpeksag updated! Happy to change anything else! ヽ(´▽`)/

@vstinner
Copy link
Member

@berkerpeksag: Would you mind to review the latest version of this seal PR?

@mariocj89
Copy link
Contributor Author

Hi @berkerpeksag ! Just a reminder in case this got outside of your radar!

@mariocj89
Copy link
Contributor Author

@Haypo @berkerpeksag it is not happening, right? 😞

I am OK closing it if you think it is not worth it. Just thought it was a good idea from the bpo comments.

@mariocj89
Copy link
Contributor Author

Note though that if there is any way I can move it forward I rather prefer it 😄

@vstinner vstinner dismissed stale reviews from berkerpeksag and themself September 4, 2017 17:30

@mariocj89 took all berker comments in account.

Copy link
Member

@vstinner vstinner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

@mariocj89
Copy link
Contributor Author

@Haypo Should I add an entry in Misc/NEWS.d/next/ ?

@vstinner
Copy link
Member

@mariocj89: Yes, please use the blurb tool to add a NEWS entry. Please mention also the new feature in Doc/whatsnew/3.7.rst document in the "unittest" section.

In the lack of feedback from @berkerpeksag, I will merge this PR once you add the NEWS entry.

@mariocj89
Copy link
Contributor Author

@Haypo updated using blurb and following previous PRs structure. Thanks!

@vstinner vstinner merged commit 552be9d into python:master Oct 17, 2017
@vstinner
Copy link
Member

@mariocj89: I merged your PR, thanks for this nice function!

Sorry for the delay, unittest.mock is a popular module and it takes time to decide if an addition is worth it, since we will have to maintain it forever.

@mariocj89 mariocj89 deleted the sealablemock_method branch October 17, 2017 11:38
@mariocj89
Copy link
Contributor Author

Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type-feature A feature request or enhancement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants