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

bpo-39274: Ensure Fraction.__bool__ returns a bool #18017

Merged
merged 1 commit into from
Feb 6, 2020

Conversation

seberg
Copy link
Contributor

@seberg seberg commented Jan 15, 2020

Some numerator types used (specifically NumPy) decides to not
return a python boolean for the != operation. Using the equivalent
call to bool() guarantees a bool return also for such types.

Closes bpo-39274

https://bugs.python.org/issue39274

https://bugs.python.org/issue39274

return bool(self.value)

r = F(1, 1)
r._numerator = custom_value(1)
Copy link
Member

Choose a reason for hiding this comment

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

I would prefer to avoid accessing private attribute _numerator:

numerator = custom_value(1)
r = F(numerator, 1)
...
self.assertEqual(numerator.bool_used, True) 

(same below)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I agree fixed, I just got lost a bit because I missed that numerator.numerator is used (which loses the original type), but since its only that it seems nicer. Of course if the instantiation logic changes, there is a chance of the test needing to be revised.

@seberg seberg force-pushed the fraction-bool-return-bool branch from 56bdc21 to 89fc30c Compare January 15, 2020 23:53
Lib/fractions.py Show resolved Hide resolved
return self

numerator = custom_value(1)
r = F(numerator)
Copy link
Member

Choose a reason for hiding this comment

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

I would prefer to explicit the denominator: F(numerator, 1). Same below.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That does not change much, if I add that, the only difference is that I also have to define __mul__. The only solution is going back to r._numerator = ... (it is used elsewhere in the file)


def __bool__(self):
self.bool_used = True
return bool(self.value)
Copy link
Member

Choose a reason for hiding this comment

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

The bug is that "numerator != 0" returns an object which is not a subtype of bool. Does your test reproduce the bug if you revert the fractions.py change?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

True, I am testing that r._numerator.__bool__ is called, which is not quite the same thing of course. Other solutions to achieve the same things (e.g. bool(a._numerator != 0) are possible and would fail the test as well).
I can add __ne__ that does the bad thing, but it would never get called. This forces to adapt the test in case the code is adapted, but without a full fledged numpy integer around, it seems hard to write a generic test that only checks for a bool result.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Well, tried to make it a bit more waterproof (by resetting bool_used), if I can import NumPy – skipping the test when its not available – that may be nicer of course (but I assume that is a no-go). Otherwise it feels like I would have to implement a full fleged rational with added strange logic...

Its over engineered, but right now I do not have a better idea TBH (unless we just skip the test...)

Copy link
Member

Choose a reason for hiding this comment

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

You cannot use 3rd party modules like numpy in the Python test suite, sadly. (At least, we are trying hard to avoid that.)

So you should try to mimick numpy's bool behavior. Please just implement a ne() method which raises an AssertionError. It would make me more comfortable. Or better: implement eq and lt, and @ @functools.total_ordering:
https://docs.python.org/dev/library/functools.html

Again, these methods would raise an exception.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Next try... Had tried to avoid registering with numbers.Rational but there is probably no reason, so now not inheriting from int anymore, which is much cleaner (and added your error, instead of ensuring __bool__ is called).

Overall, seems much better, since now it really is a crippled object that cannot do anything but what it is supposed to do...

@property
def numerator(self):
# required to preserve `self` during instantiation
return self
Copy link
Member

Choose a reason for hiding this comment

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

I would prefer to remove that and use F(numerator, 1) instead.

@seberg seberg force-pushed the fraction-bool-return-bool branch 2 times, most recently from 08aee01 to 2413700 Compare January 16, 2020 17:17

def __bool__(self):
self.bool_used = True
return bool(self.value)
Copy link
Member

Choose a reason for hiding this comment

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

You cannot use 3rd party modules like numpy in the Python test suite, sadly. (At least, we are trying hard to avoid that.)

So you should try to mimick numpy's bool behavior. Please just implement a ne() method which raises an AssertionError. It would make me more comfortable. Or better: implement eq and lt, and @ @functools.total_ordering:
https://docs.python.org/dev/library/functools.html

Again, these methods would raise an exception.

r = F(numerator)
# ensure the numerator was not lost during instantiation:
self.assertTrue(r.numerator is numerator)
numerator.bool_used = 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 self.assertFalse(numerator.bool_used) instead.

Suggested change
numerator.bool_used = False
self.assertFalse(numerator.bool_used)

numerator.bool_used = False
self.assertTrue(bool(r) is True)
# If bool(numerator) was called, a bool result should be guaranteed:
self.assertEqual(numerator.bool_used, True)
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
self.assertEqual(numerator.bool_used, True)
self.assertTrue(numerator.bool_used)

# ensure the numerator was not lost during instantiation:
self.assertTrue(r.numerator is numerator)
numerator.bool_used = False
self.assertTrue(bool(r) is True)
Copy link
Member

Choose a reason for hiding this comment

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

To get the value if the test fails:

Suggested change
self.assertTrue(bool(r) is True)
self.assertIs(bool(r), True)

@seberg seberg force-pushed the fraction-bool-return-bool branch from 2413700 to 4bf428a Compare January 17, 2020 15:43
@vstinner
Copy link
Member

vstinner commented Feb 3, 2020

@seberg: Did you see my latest comments? Would you mind to update your PR?

Some numerator types used (specifically NumPy) decides to not
return a python boolean for the `!=` operation. Using the equivalent
call to `bool()` guarantees a bool return also for such types.

Closes bpo-39274
@seberg seberg force-pushed the fraction-bool-return-bool branch from 4bf428a to a465dad Compare February 3, 2020 17:55
@seberg
Copy link
Contributor Author

seberg commented Feb 3, 2020

Thanks for the ping, sorry, I had somehow forgotten/missed the very true comments regarding the assert statements. (updated now)

@codecov
Copy link

codecov bot commented Feb 3, 2020

Codecov Report

Merging #18017 into master will decrease coverage by 0.01%.
The diff coverage is n/a.

Impacted file tree graph

@@            Coverage Diff             @@
##           master   #18017      +/-   ##
==========================================
- Coverage   82.13%   82.11%   -0.02%     
==========================================
  Files        1953     1954       +1     
  Lines      582680   583290     +610     
  Branches    44349    44407      +58     
==========================================
+ Hits       478602   478993     +391     
- Misses      94416    94658     +242     
+ Partials     9662     9639      -23     
Impacted Files Coverage Δ
Lib/test/test_binhex.py 13.33% <0.00%> (-79.78%) ⬇️
Lib/binhex.py 17.84% <0.00%> (-58.49%) ⬇️
Lib/test/test_distutils.py 66.66% <0.00%> (-4.77%) ⬇️
Lib/concurrent/futures/thread.py 64.70% <0.00%> (-3.52%) ⬇️
Lib/concurrent/futures/process.py 55.72% <0.00%> (-1.47%) ⬇️
Objects/codeobject.c 82.59% <0.00%> (-1.31%) ⬇️
Lib/functools.py 95.46% <0.00%> (-0.82%) ⬇️
Modules/binascii.c 86.77% <0.00%> (-0.55%) ⬇️
Lib/json/__init__.py 92.30% <0.00%> (-0.45%) ⬇️
Modules/_pickle.c 74.73% <0.00%> (-0.34%) ⬇️
... and 124 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update dc0284e...a465dad. Read the comment docs.

__lt__ = __eq__

# We did not implement all abstract methods, so register:
numbers.Rational.register(CustomValue)
Copy link
Member

Choose a reason for hiding this comment

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

This may fail if you run the test with reference leak test:

./python -m test -R 3:3 test_fractions

Can you check that it does not leak? I would prefer to avoid .register() if it does leak.

... This test is getting more and more complex... Maybe we should just ignore the test and just merge the fix.

What do you think @mdickinson?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmmm, seems to work:

 % /home/sebastian/python-dev/bin/python3.9d  -m test -R 3:3 test_fractions
0:00:00 load avg: 0.39 Run tests sequentially
0:00:00 load avg: 0.39 [1/1] test_fractions
beginning 6 repetitions
123456
......

== Tests result: SUCCESS ==

1 test OK.

Total duration: 221 ms
Tests result: SUCCESS

I have not used the python refcount checker, so not sure if it clears ABCs are not.

Copy link
Member

Choose a reason for hiding this comment

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

The fix itself looks good to me; I think we should merge as is.

@vstinner vstinner merged commit 427c84f into python:master Feb 6, 2020
@vstinner
Copy link
Member

vstinner commented Feb 6, 2020

The fix itself looks good to me; I think we should merge as is.

Ok, I was maybe too far in term of nitpicking, sorry :-) It's very difficult to implement a type implementing the numbers.Rational ABC :-( I had exactly the same problem on another fractions bug, and I decided to not add such test: https://bugs.python.org/issue39350#msg361481

@miss-islington
Copy link
Contributor

Thanks @seberg for the PR, and @vstinner for merging it 🌮🎉.. I'm working now to backport this PR to: 3.8.
🐍🍒⛏🤖

miss-islington pushed a commit to miss-islington/cpython that referenced this pull request Feb 6, 2020
Some numerator types used (specifically NumPy) decides to not
return a Python boolean for the "a != b" operation. Using the equivalent
call to bool() guarantees a bool return also for such types.
(cherry picked from commit 427c84f)

Co-authored-by: Sebastian Berg <sebastian@sipsolutions.net>
@bedevere-bot
Copy link

GH-18376 is a backport of this pull request to the 3.8 branch.

@miss-islington
Copy link
Contributor

Thanks @seberg for the PR, and @vstinner for merging it 🌮🎉.. I'm working now to backport this PR to: 3.7.
🐍🍒⛏🤖

miss-islington pushed a commit to miss-islington/cpython that referenced this pull request Feb 6, 2020
Some numerator types used (specifically NumPy) decides to not
return a Python boolean for the "a != b" operation. Using the equivalent
call to bool() guarantees a bool return also for such types.
(cherry picked from commit 427c84f)

Co-authored-by: Sebastian Berg <sebastian@sipsolutions.net>
@bedevere-bot
Copy link

GH-18377 is a backport of this pull request to the 3.7 branch.

miss-islington added a commit that referenced this pull request Feb 6, 2020
Some numerator types used (specifically NumPy) decides to not
return a Python boolean for the "a != b" operation. Using the equivalent
call to bool() guarantees a bool return also for such types.
(cherry picked from commit 427c84f)

Co-authored-by: Sebastian Berg <sebastian@sipsolutions.net>
miss-islington added a commit that referenced this pull request Feb 6, 2020
Some numerator types used (specifically NumPy) decides to not
return a Python boolean for the "a != b" operation. Using the equivalent
call to bool() guarantees a bool return also for such types.
(cherry picked from commit 427c84f)

Co-authored-by: Sebastian Berg <sebastian@sipsolutions.net>
@seberg
Copy link
Contributor Author

seberg commented Feb 6, 2020

Ah, nitpicking has its upsides :). If anything, maybe the next time there is a fraction issue, this actually helps writing a reasonable test for it.

@seberg seberg deleted the fraction-bool-return-bool branch February 6, 2020 21:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants