Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add equality rich comparision operators to telegram objects #604

Merged
merged 19 commits into from
May 14, 2017
Merged

Conversation

jsmnbom
Copy link
Member

@jsmnbom jsmnbom commented May 12, 2017

Fixes #587 by adding __eq__ and __hash__ methods to the base TelegramObject class to make them work with the == operator.
This makes the following classes behave correctly:

  • Animation
  • Audio
  • Bot
  • CallbackQuery
  • Chat
  • ChosenInlineResult
  • Contact
  • Document
  • File
  • Game
  • InlineQuery
  • InlineQueryResult*
  • Message
  • PhotoSize (already had a __eq__ implementation, is it still needed?)
  • Sticker
  • Update
  • Video
  • Voice

There's still a few classes that still don't work since they don't have an id. Many of them I don't think needs handling of this (like InlineKeyboard, I don't think anyone ever needs to compare that), but the following we should maybe try to find a solution to. For now I just wanted to get the basics working, to get your opinion on weather this is the way we wanna do it.

  • ChatMember
  • Game
  • GameHighScore
  • Location
  • MessageEntity
  • Venue
  • WebhookInfo?

We might also wanna have a look at the ext classes to see if any of them need __eq__ too.
Still lacking tests. Should I test all of them in their respective test_CLASSNAME or should I add a new test_equality.py test?

telegram/base.py Outdated
raise NotImplementedError

def __eq__(self, other):
if isinstance(other, TelegramObject):
Copy link
Member

@Eldinnie Eldinnie May 12, 2017

Choose a reason for hiding this comment

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

From the Photosize __eq__ it has:
if not isinstance(other, self.__class__):
Might use that here as if isinstance(other, self.__class__):

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, good idea :D

@jsmnbom
Copy link
Member Author

jsmnbom commented May 12, 2017

@tsnoam could you shrine some light on why PhotoSize has a __eq__ method? It was implemented in ec8cd37 for some reason...
I know it was two years ago, so you probably don't :P

@Eldinnie
Copy link
Member

Seems pretty solid. I added a comment.
I think the photosize could as well be deleted, since it checks size too, but I think if file_id is different the rest is different as well.
for the excluded cases I think maybe implement them in their seperate classes.

  • ChatMember (user == user and status == status)
  • Game (title?)
  • GameHighscore, seems pointless, but for completion?
  • Location (lat == lat and lon == lon)
  • MessageEntity Seems pointless
  • Venue (lat==lat and lon==lon and title==title)
  • WebhookInfo no...

Regarding tests, I would vote for adding them in their respective classes.

@Eldinnie
Copy link
Member

@bomjacob @tsnoam For the tests :D see test_video.py line 159 and test_video.py line 159

@jsmnbom
Copy link
Member Author

jsmnbom commented May 12, 2017

For the tests :D see test_video.py line 159 and test_video.py line 159

Ahh, thanks, I see :D

@Eldinnie I agree with implementing the other in their respective classes ^^

@tsnoam
Copy link
Member

tsnoam commented May 12, 2017

@bomjacob As far as i recall, telegram servers started to return actual data in reply to sending certain messages (sendPhoto from a url?)
So i had to actually compare the received data with what we expected it to be.

I believe that comparing id may be enough but I don't have insight into telegram internals...

@j-maas
Copy link

j-maas commented May 13, 2017

I want to point out an alternative to having the equality check for identity:

One could also have two objects, User1<id=1, first_name="Same"> and User2<id=2, first_name="Same">, which are equal in the sense that they have exactly the same "relevant" attributes, but are not identical since they are two different objects (hence having different ids).
I would argue that it still would make sense if User1 == User2 would return true, since they are the same concerning all relevant attributes.
If I wanted to check for identity, I could use User1 is User2, which returns true if the variables reference the exact same instance. Since the id should be unique, there should never be two instances with the same id and thus this check would be reliable.

I understand however that the use case for the equality operator might mostly be to check for identity, in which case the implementation based on id is perfect. I still wanted to point this out to be able to discuss it.

@jsmnbom
Copy link
Member Author

jsmnbom commented May 13, 2017

That's not quite how python works though....

In [4]: user1 = telegram.User(1, "Same")

In [5]: user2 = telegram.User(2, "Same")

In [6]: user1 is user2
Out[6]: False

@jsmnbom jsmnbom changed the title Add equality rich comparision operator to base class Add equality rich comparision operators to telegram objects May 13, 2017
@j-maas
Copy link

j-maas commented May 13, 2017

But that's exactly what I'd want:

user1 = telegram.User(1, "Same, but not identical")
user2 = telegram.User(2, "Same, but not identical")
user3 = user2  # Identical

I'd expect:

user1 == user2  # True
user1 is user2  # False
user2 == user3  # True
user2 is user3  # True

The critical thing is that there shouldn't be the possibility of creating two objects user1 = telegram.User(1, "Identical") and user2 = telegram.User(1, "Identical") with the same id for this to work perfectly. However, I'm sure that it is in principal possible to create those two objects, since Python would not enforce the id being unique.

Also, just to be explicit: I don't want to insist on this implementation. I just wanted to pitch this alternative and discuss it. It might be more powerful but I also suspect that this is not intuitive and too specialized.

Also thanks for taking your time to implement it! :)

@jsmnbom
Copy link
Member Author

jsmnbom commented May 13, 2017

Oh that was what you meant?
Well.. that's pretty much the way to handle these things... you should really never mess with the id implementation :D

@Eldinnie
Copy link
Member

@y0hy0h If you want to compare if users have the same first name, you could do that manually. What we want to achieve is to figure out if two User instances represent the same user. Checking if they have a similar feature is not what we're trying to achieve.

What you suggest is like saying you live at privet drive nr 4. and when I come to visit I get to diagon ally nr 4 and expect you to be there.

@tsnoam
Copy link
Member

tsnoam commented May 13, 2017

The is operator is meant to be used to compare that two objects are exactly the same object, in memory. Changing our implementation to always return (for example) the same User instance for the same user will create undesirable side effects, like, the consumer of User won't be able to change any value inside (not sure why one would want it, but still) without changing it for all other places his program, whether he'd like it or not. (it will be kind of a singleton per user).
At the devel team, we find this behaviour non intuitive and "non pythonic".
Therefor, the implementation would be for a == (equality) operator.

Audio, Document, File, Location, Photo, Sticker, Venue, Video and Voice
@j-maas
Copy link

j-maas commented May 13, 2017

@Eldinnie It's not about checking the name. It's more like a = 5 and b = 5 being two different objects (at least for the sake of the argument) but evaluating as a == b # True. The user's id is similar to the memory address of the object: It's a representation for an instance. It would not count into the natural notion of two users being equal (having exactly the same attributes). However, I get what you're getting at: In code, most of the time we want to check if the two users are identical, not if they are the same regarding all relevant (i. e. ignoring id) attributes.

@tsnoam That's a good point against enforcing users to be singletons. That also makes checking by is invalid, leaving only the identity check through ==. 👍

My suggestion was just something that was intuitive to me. I simply feel that == works more like Java's .equals function for strings, in that it should indicate whether objects are the same, not if they are the same instance. This intuition gets really tricky with an id attribute, since from a enduser's perspective, the id is invisible and wouldn't count into their notion of two users being the same.
I totally agree that it is justified to use == for checking if two users are identical from the API's point of view! Let's just keep the implementation above, then. :)

Also thank you for your patience and answers! It's really awesome that you addressed my issue in such detail!

@Eldinnie
Copy link
Member

@y0hy0h
The problem is that in your example (a=5 b=5) a and b are not the objects, 5 is. a and b are both pointing to that same object.
I still don't understand why you would want two users with different ID evaluate as equal, but maybe it's better to keep this discussion away from this thread.

Copy link
Member

@tsnoam tsnoam left a comment

Choose a reason for hiding this comment

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

See comments on the changes.
Basically, the comments about how to hash/eq/raise are relevant for all types.

telegram/base.py Outdated
raise NotImplementedError

def __eq__(self, other):
if isinstance(other, self.__class__):
Copy link
Member

Choose a reason for hiding this comment

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

This is one of the few places where I prefer comparing the class, instead of using isinstance. The reason is that multiple levels of inheritance may create undesirable effects.
So, I think if type(other) is type(self): would be more appropriate here.

Copy link
Member Author

Choose a reason for hiding this comment

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

As I see it, if I subclass User as SpecialUser for some reason I'd expect to be able to compare Users and SpecialUsers. The inheritence should only be able to go one way (other needs to be a subclass of self), so having them all subclass TelegramObject is not an issue. Also I don't think we ever subclass a subclass of TelegramObject anywhere in the library atm.

telegram/base.py Outdated
for x in ['id', 'file_id', 'message_id', 'result_id', 'update_id']:
if hasattr(self, x):
return self.__getattribute__(x)
raise NotImplementedError
Copy link
Member

Choose a reason for hiding this comment

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

I prefer RuntimeError to be raised. After all, _get_id is implemented...

Copy link
Member Author

@jsmnbom jsmnbom May 13, 2017

Choose a reason for hiding this comment

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

I return NotImplementedError as a way of saying "this object doesn't implement an id", which many objects do not (pretty much all objects not mentioned in the OP). This is why I'm catching it inside of __eq__ too.

telegram/base.py Outdated
def _get_id(self):
for x in ['id', 'file_id', 'message_id', 'result_id', 'update_id']:
if hasattr(self, x):
return self.__getattribute__(x)
Copy link
Member

Choose a reason for hiding this comment

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

I usually prefer using getattr(self, x). Any reason you chose to use __getattribute__ here?

Copy link
Member Author

Choose a reason for hiding this comment

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

Any reason you chose to use __getattribute__ here?

I'd forgotten it existed... I'll change it :D

telegram/base.py Outdated
def __eq__(self, other):
if isinstance(other, self.__class__):
try:
return self._get_id() == other._get_id()
Copy link
Member

Choose a reason for hiding this comment

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

Nitpick: you're accessing a private method _get_id() of something other than self.
Maybe it's the proper thing to do here, just wish that we think about it one more time.
Maybe return hash(self) == hash(other)?

Copy link
Member Author

Choose a reason for hiding this comment

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

I'd say it's okay since I'm checking the type of the class first, so I know with almost 100% certainty how _get_id() operates.
I seem to recall having read somewhere that basing __eq__ on hash is very discouraged.

Copy link
Member Author

Choose a reason for hiding this comment

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

If it's a big issue then I can make the _get_id() works either as a static method or just move it out of the class entirely and then have it accept the object to extract id from.

telegram/base.py Outdated
if isinstance(other, self.__class__):
try:
return self._get_id() == other._get_id()
except NotImplementedError:
Copy link
Member

Choose a reason for hiding this comment

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

This shouldn't happen if you properly perform the class comparison above.
Maybe if we use hash comparison will even save the need to compare the class all together.

telegram/base.py Outdated
return self._get_id() == other._get_id()
except NotImplementedError:
pass # return NotImplemented
return NotImplemented
Copy link
Member

Choose a reason for hiding this comment

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

Just return False.

Copy link
Member Author

Choose a reason for hiding this comment

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

I return NotImplemented as a way of telling the interpreter to try the comparison the other way around. This allows the other to say "Hey! I know this object, and yes we are indeed equal".

Copy link
Member

@tsnoam tsnoam May 13, 2017

Choose a reason for hiding this comment

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

I can go with your take on isinstance (above), but I don't think that returning something else than a bool is right.
What about something simple like:

return isinstance(other, self.__class__) and self._get_id() == other._get_id()

telegram/base.py Outdated
try:
return hash((
self.__class__,
self._get_id(),))
Copy link
Member

Choose a reason for hiding this comment

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

Style nitpick: the entire return statement can be in a single line...

Copy link
Member Author

Choose a reason for hiding this comment

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

Not according to my yapf

Copy link
Member

Choose a reason for hiding this comment

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

This is what I meant. Cleaner and works with yapf ;-)

def __hash__(self):
    return hash((self.__class__, self._get_id()))

telegram/base.py Outdated
return hash((
self.__class__,
self._get_id(),))
except NotImplementedError:
Copy link
Member

Choose a reason for hiding this comment

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

I don't think catching NotIMplementedError here is needed. If we get such exception, it's probably a bug which we'll mask,

def __eq__(self, other):
if isinstance(other, self.__class__):
return self.user == other.user and self.status == other.status
return NotImplemented
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 this will be more appropriate:
return type(self) == type(other) and self.user == other.user and self.status == other.status
(no need to return NotImplemented

Copy link
Member

Choose a reason for hiding this comment

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

mmm... following the comments above.
maybe we should use the same __eq__ from the parent class with a special class like this _get_id() will return a Tuple of (user, status)?

@tsnoam
Copy link
Member

tsnoam commented May 14, 2017

Great work @bomjacob !

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Equal objects (e. g. User) are not recognized as equal
4 participants