Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions Lib/test/test_math.py
Original file line number Diff line number Diff line change
Expand Up @@ -1992,6 +1992,22 @@ def test_ulp(self):
with self.subTest(x=x):
self.assertEqual(math.ulp(-x), math.ulp(x))

def test_issue39871(self):
# A SystemError should not be raised if the first arg to atan2(),
# copysign(), or remainder() cannot be converted to a float.
class F:
def __float__(self):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The purpose of this method wasn't obvious at the first look. Would you mind to add a comment explaining that it's written to ensure that the float() is never called?

Maybe you could use a mock object and ensure that mock.__float__.assert_not_called().

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Good idea!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Using mock doesn't seem to work well as a regression test here. I'm not sure why. For example, on Python 3.8.2:

>>> import math, unittest.mock
>>> y = unittest.mock.MagicMock()
>>> math.atan2("not a number", y)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/opt/local/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/unittest/mock.py", line 2111, in __get__
    return self.create_mock()
TypeError: must be real number, not str
>>> y.__float__.mock_calls  # expect to see one call here, but there's nothing
[]

Contrast with:

>>> math.atan(y)
0.7853981633974483
>>> y.__float__.mock_calls
[call()]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It looks correct to me. In the first example y.__float__ is not called, as expected, because an error is raised for "not a number".

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This was on Python 3.8.2, which still has the bug that this PR solves: namely, both arguments are converted to float before checking for errors. So the expectation is that because the bug is still present, y.__float__ is still called. (And then with Zackery's fix, of course, it won't be any more.)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@serhiy-storchaka: I'm quite baffled here; I really don't understand what your objection is (if you have an objection), or what change you're suggesting (if you're suggesting a change). Victor suggested using mock.__float__.assert_not_called(). You said that was a good idea. I pointed out that that doesn't work, but tweaked the test to add something equivalent that does work. As far as I'm concerned, the bug is fixed, it has a regression test, and there's nothing more to do here.

If there's something you feel needs to change, please open a new PR. If not, please let's stop wasting time and energy on a bug that's already fixed.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I do not suggest any changes. To me, there is absolute no difference between using a handmade class with the __float__ method and a mock, except that the latter would be several lines shorter. In both cases the test crashes without the fix applied and passed with the fix applied. This is why it looked a good idea to me -- simpler solution is better.

I am just puzzled why do you see a difference.

Copy link
Copy Markdown
Member

@mdickinson mdickinson Mar 14, 2020

Choose a reason for hiding this comment

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

Consider the following two unit tests:

    def test_with_mock(self):
        y = unittest.mock.MagicMock()
        try:
            math.atan2("not a number", y)
        except:
            pass
        y.__float__.assert_not_called()

    def test_without_mock(self):
        class F:
            def __init__(self):
                self.float_called = False

            def __float__(self):
                self.float_called = True
                return 1.0

        y = F()
        try:
            math.atan2("not a number", y)
        except:
            pass
        self.assertFalse(y.float_called)

Both tests assert the exact same thing: that the failed math.atan2 call did not call ys __float__ method. The first test uses a MagicMock. The second uses a custom class.

So the tests should be equivalent. However: before the fix in this PR, the first test will pass, while the second one will fail. (After the fix, they both pass, of course.)

Please note: I am not claiming that these are good unit tests; I'm just trying to demonstrate the difference I explained earlier in an explicit way.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I do not claim that the current test is bad. But both tests crash before the fix and I do not see a difference between these crashes. They are equivalent to me.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Did you run tests on the release build?

self.converted = True
1/0
for func in math.atan2, math.copysign, math.remainder:
y = F()
with self.assertRaises(TypeError):
func("not a number", y)

# There should not have been any attempt to convert the second
# argument to a float.
self.assertFalse(getattr(y, "converted", False))

# Custom assertions.

def assertIsNaN(self, value):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fix a possible :exc:`SystemError` in ``math.{atan2,copysign,remainder}()``
when the first argument cannot be converted to a :class:`float`. Patch by
Zachary Spytz.
6 changes: 5 additions & 1 deletion Modules/mathmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -1106,9 +1106,13 @@ math_2(PyObject *const *args, Py_ssize_t nargs,
if (!_PyArg_CheckPositional(funcname, nargs, 2, 2))
return NULL;
x = PyFloat_AsDouble(args[0]);
if (x == -1.0 && PyErr_Occurred()) {
return NULL;
}
y = PyFloat_AsDouble(args[1]);
if ((x == -1.0 || y == -1.0) && PyErr_Occurred())
if (y == -1.0 && PyErr_Occurred()) {
return NULL;
}
errno = 0;
r = (*func)(x, y);
if (Py_IS_NAN(r)) {
Expand Down