Skip to content

Class' __hash__ not being used in MagicMock subclass #150

@Galarzaa90

Description

@Galarzaa90

I have a series of Mock classes for my testing module, based on this:
https://github.com/python-discord/bot/blob/master/tests/helpers.py
But I'm using asynctest because I'm using Python 3.6

Base classes:

class CustomMockMixin:
    discord_id = itertools.count(0)
    spec_set = None

    def __init__(self, *args, **kwargs):
        name = kwargs.pop('name', None)  # `name` has special meaning for Mock classes, so we need to set it manually.
        super().__init__(*args, spec_set=self.spec_set, **kwargs)

        if name:
            self.name = name

    def _get_child_mock(self, *args, **kwargs):
        """This method by default returns an instance of the same class for any attribute or method of the class.

        This would cause MockBot, for instance, to return another instance of MockBot when using ``bot.get_guild``.

        This overwrites the original logic to return MagicMock objects by default, and CoroutineMocks for names defined
        in ``_spec_coroutines``"""
        _new_name = kwargs.get("_new_name")
        if _new_name in self.__dict__['_spec_coroutines']:
            return asynctest.CoroutineMock(*args, **kwargs)

        _type = type(self)

        if issubclass(_type, asynctest.MagicMock) and _new_name in asynctest.mock.async_magic_coroutines:
            klass = asynctest.CoroutineMock
        elif issubclass(_type, asynctest.CoroutineMock):
            klass = asynctest.MagicMock
        elif not issubclass(_type, unittest.mock.CallableMixin):
            # noinspection PyTypeHints
            if issubclass(_type, unittest.mock.NonCallableMagicMock):
                klass = asynctest.MagicMock
            elif issubclass(_type, asynctest.NonCallableMock):
                klass = asynctest.Mock
        else:
            klass = asynctest.MagicMock

        # noinspection PyUnboundLocalVariable
        return klass(*args, **kwargs)


class HashableMixin(discord.mixins.EqualityComparable):
    """
    Mixin that provides similar hashing and equality functionality as discord.py's `Hashable` mixin.
    Note: discord.py`s `Hashable` mixin bit-shifts `self.id` (`>> 22`); to prevent hash-collisions
    for the relative small `id` integers we generally use in tests, this bit-shift is omitted.
    """

    def __hash__(self):
        return self.id

And this is the specific class I'm struggling with:

class MockRole(CustomMockMixin, asynctest.MagicMock, ColourMixin, HashableMixin):
    spec_set = discord.Role

    def __init__(self, *args, **kwargs):
        default_kwargs = {
            'id': next(self.discord_id),
            'name': 'role',
            'position': 1,
            'colour': discord.Colour(0xdeadbf),
            'permissions': discord.Permissions(),
        }
        super().__init__(*args, **collections.ChainMap(kwargs, default_kwargs))

        if 'mention' not in kwargs:
            self.mention = f'&{self.name}'

    def __str__(self):
        return f"<{self.mention}>"

Needless to say, I'm over 100 test cases in with this, and I had already noticed that __str__ was not working as expected, but now I noticed that __hash__ isn't, and this affects my tests' equality checks.

If I call the methods explicitly stating the class, they work (even calling the parent's classes methods)

>>> role = MockRole(name="Premium", id=3)
>>> str(role )
'<MockRole spec_set=\'Role\' id=\'2508777936776\'>'
>>> MockRole.__str__(role )
'<&Premium>'
>>> hash(role )
-9223371880056154760
>>> MockRole.__hash__(role )
5

I'm not sure if this is part of my implementation or part of the library.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions