From 3fb74136abfd9252827589b20338692354a2de3b Mon Sep 17 00:00:00 2001 From: Mark Dickinson Date: Thu, 28 May 2020 11:59:55 +0100 Subject: [PATCH 1/3] Add operator.as_float --- Doc/library/operator.rst | 7 ++ Doc/whatsnew/3.10.rst | 7 ++ Lib/operator.py | 10 +++ Lib/test/test_operator.py | 76 +++++++++++++++++++ .../2020-05-28-11-54-56.bpo-40801.CFOWc1.rst | 6 ++ Modules/_operator.c | 45 +++++++++++ Modules/clinic/_operator.c.h | 28 ++++++- 7 files changed, 178 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2020-05-28-11-54-56.bpo-40801.CFOWc1.rst diff --git a/Doc/library/operator.rst b/Doc/library/operator.rst index 36c53556c2685e..92cd36f39da29a 100644 --- a/Doc/library/operator.rst +++ b/Doc/library/operator.rst @@ -101,6 +101,13 @@ The mathematical and bitwise operations are the most numerous: Return the bitwise and of *a* and *b*. +.. function:: as_float(a) + + Return *a* converted to an float. Equivalent to ``float(a)``, except + that conversion from a string or bytestring is not permitted. The result + always has exact type :class:`float`. + + .. function:: floordiv(a, b) __floordiv__(a, b) diff --git a/Doc/whatsnew/3.10.rst b/Doc/whatsnew/3.10.rst index 34a09fe4b505cc..04753c5abd2015 100644 --- a/Doc/whatsnew/3.10.rst +++ b/Doc/whatsnew/3.10.rst @@ -92,6 +92,13 @@ New Modules Improved Modules ================ +operator +-------- + +Added :func:`operator.as_float` to convert a numeric object to :class:`float`. +This exposes to Python a conversion whose semantics exactly match Python's +own implicit float conversions, for example as used in the :mod:`math` module. + tracemalloc ----------- diff --git a/Lib/operator.py b/Lib/operator.py index fb58851fa6ef67..a0760545f2d416 100644 --- a/Lib/operator.py +++ b/Lib/operator.py @@ -80,6 +80,16 @@ def and_(a, b): "Same as a & b." return a & b +def as_float(obj): + """ + Convert something numeric to float. + + Same as float(obj), but does not accept strings. + """ + if isinstance(obj, (str, bytes, bytearray)): + raise TypeError("as_float argument must be numeric") + return float(obj) + def floordiv(a, b): "Same as a // b." return a // b diff --git a/Lib/test/test_operator.py b/Lib/test/test_operator.py index f46d94a226717a..e588c7583afef2 100644 --- a/Lib/test/test_operator.py +++ b/Lib/test/test_operator.py @@ -499,6 +499,75 @@ def __length_hint__(self): with self.assertRaises(LookupError): operator.length_hint(X(LookupError)) + def test_as_float(self): + operator = self.module + + # Exact type float. + self.assertIsFloatWithValue(operator.as_float(2.3), 2.3) + + # Subclass of float. + class MyFloat(float): + pass + + self.assertIsFloatWithValue(operator.as_float(MyFloat(-1.56)), -1.56) + + # Non-float with a __float__ method. + class FloatLike: + def __float__(self): + return 1729.0 + + self.assertIsFloatWithValue(operator.as_float(FloatLike()), 1729.0) + + # Non-float with a __float__ method that returns an instance + # of a subclass of float. + class FloatLike2: + def __float__(self): + return MyFloat(918.0) + + self.assertIsFloatWithValue(operator.as_float(FloatLike2()), 918.0) + + # Plain old integer + self.assertIsFloatWithValue(operator.as_float(2), 2.0) + + # Integer subclass. + class MyInt(int): + pass + + self.assertIsFloatWithValue(operator.as_float(MyInt(-3)), -3.0) + + # Object supplying __index__ but not __float__. + class IntegerLike: + def __index__(self): + return 77 + + self.assertIsFloatWithValue(operator.as_float(IntegerLike()), 77.0) + + # Same as above, but with __index__ returning an instance of an + # int subclass. + class IntegerLike2: + def __index__(self): + return MyInt(78) + + self.assertIsFloatWithValue(operator.as_float(IntegerLike2()), 78.0) + + # Object with both __float__ and __index__; __float__ should take + # precedence. + class Confused: + def __float__(self): + return 123.456 + + def __index__(self): + return 123 + + self.assertIsFloatWithValue(operator.as_float(Confused()), 123.456) + + # Not convertible. + bad_values = ["123", b"123", bytearray(b"123"), None, 1j] + for bad_value in bad_values: + with self.subTest(bad_value=bad_value): + with self.assertRaises(TypeError): + operator.as_float(bad_value) + def test_dunder_is_original(self): operator = self.module @@ -509,6 +578,13 @@ def test_dunder_is_original(self): if dunder: self.assertIs(dunder, orig) + def assertIsFloatWithValue(self, actual, expected): + self.assertIs(type(actual), float) + # Compare reprs rather than values, to deal correctly with corner + # cases like nans and signed zeros. + self.assertEqual(repr(actual), repr(expected)) + + class PyOperatorTestCase(OperatorTestCase, unittest.TestCase): module = py_operator diff --git a/Misc/NEWS.d/next/Library/2020-05-28-11-54-56.bpo-40801.CFOWc1.rst b/Misc/NEWS.d/next/Library/2020-05-28-11-54-56.bpo-40801.CFOWc1.rst new file mode 100644 index 00000000000000..efafd94c583a9b --- /dev/null +++ b/Misc/NEWS.d/next/Library/2020-05-28-11-54-56.bpo-40801.CFOWc1.rst @@ -0,0 +1,6 @@ +Add a new :func:`operator.as_float` function for converting an arbitrary +Python numeric object to :class:`float`. This is a simple wrapper around the +:c:func:`PyFloat_AsDouble` C-API function. The intent is to provide at +Python level a conversion whose semantics exactly match Python's own +implicit float conversions, for example those used in the :mod:`math` +module. diff --git a/Modules/_operator.c b/Modules/_operator.c index 8a54829e5bbcc8..4aa4d2311617f7 100644 --- a/Modules/_operator.c +++ b/Modules/_operator.c @@ -761,6 +761,50 @@ _tscmp(const unsigned char *a, const unsigned char *b, return (result == 0); } +/*[clinic input] +_operator.as_float -> + + obj: object + / + +Return *obj* interpreted as a float. + +If *obj* is already of exact type float, return it unchanged. + +If *obj* is already an instance of float (including possibly an instance of a +float subclass), return a float with the same value as *obj*. + +If *obj* is not an instance of float but its type has a __float__ method, use +that method to convert *obj* to a float. + +If *obj* is not an instance of float and its type does not have a __float__ +method but does have an __index__ method, use that method to +convert *obj* to an integer, and then convert that integer to a float. + +If *obj* cannot be converted to a float, raise TypeError. + +Calling as_float is equivalent to calling *float* directly, except that string +objects are not accepted. + +[clinic start generated code]*/ + +static PyObject * +_operator_as_float(PyObject *module, PyObject *obj) +/*[clinic end generated code: output=25b27903bbd14913 input=39db64b91327f393]*/ +{ + if (PyFloat_CheckExact(obj)) { + Py_INCREF(obj); + return obj; + } + + double x = PyFloat_AsDouble(obj); + if (x == -1.0 && PyErr_Occurred()) { + return NULL; + } + return PyFloat_FromDouble(x); +} + + /*[clinic input] _operator.length_hint -> Py_ssize_t @@ -929,6 +973,7 @@ static struct PyMethodDef operator_methods[] = { _OPERATOR_GE_METHODDEF _OPERATOR__COMPARE_DIGEST_METHODDEF _OPERATOR_LENGTH_HINT_METHODDEF + _OPERATOR_AS_FLOAT_METHODDEF {NULL, NULL} /* sentinel */ }; diff --git a/Modules/clinic/_operator.c.h b/Modules/clinic/_operator.c.h index 34b6fdadfb7309..5935fd9d5e1bfe 100644 --- a/Modules/clinic/_operator.c.h +++ b/Modules/clinic/_operator.c.h @@ -1390,6 +1390,32 @@ _operator_is_not(PyObject *module, PyObject *const *args, Py_ssize_t nargs) return return_value; } +PyDoc_STRVAR(_operator_as_float__doc__, +"as_float($module, obj, /)\n" +"--\n" +"\n" +"Return *obj* interpreted as a float.\n" +"\n" +"If *obj* is already of exact type float, return it unchanged.\n" +"\n" +"If *obj* is already an instance of float (including possibly an instance of a\n" +"float subclass), return a float with the same value as *obj*.\n" +"\n" +"If *obj* is not an instance of float but its type has a __float__ method, use\n" +"that method to convert *obj* to a float.\n" +"\n" +"If *obj* is not an instance of float and its type does not have a __float__\n" +"method but does have an __index__ method, use that method to\n" +"convert *obj* to an integer, and then convert that integer to a float.\n" +"\n" +"If *obj* cannot be converted to a float, raise TypeError.\n" +"\n" +"Calling as_float is equivalent to calling *float* directly, except that string\n" +"objects are not accepted."); + +#define _OPERATOR_AS_FLOAT_METHODDEF \ + {"as_float", (PyCFunction)_operator_as_float, METH_O, _operator_as_float__doc__}, + PyDoc_STRVAR(_operator_length_hint__doc__, "length_hint($module, obj, default=0, /)\n" "--\n" @@ -1486,4 +1512,4 @@ _operator__compare_digest(PyObject *module, PyObject *const *args, Py_ssize_t na exit: return return_value; } -/*[clinic end generated code: output=eae5d08f971a65fd input=a9049054013a1b77]*/ +/*[clinic end generated code: output=5a828b48c9f80dbf input=a9049054013a1b77]*/ From 34ebbae20123e7ebd714fb8d05328e09c2e00e92 Mon Sep 17 00:00:00 2001 From: Mark Dickinson Date: Thu, 28 May 2020 12:02:28 +0100 Subject: [PATCH 2/3] Add missing versionadded --- Doc/library/operator.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Doc/library/operator.rst b/Doc/library/operator.rst index 92cd36f39da29a..d6e5455a2f8f12 100644 --- a/Doc/library/operator.rst +++ b/Doc/library/operator.rst @@ -107,6 +107,8 @@ The mathematical and bitwise operations are the most numerous: that conversion from a string or bytestring is not permitted. The result always has exact type :class:`float`. + .. versionadded:: 3.10 + .. function:: floordiv(a, b) __floordiv__(a, b) From ef94333cb57763bdb949aaf42a76d0fff481104d Mon Sep 17 00:00:00 2001 From: Mark Dickinson Date: Thu, 28 May 2020 14:09:16 +0100 Subject: [PATCH 3/3] Fix docstring typo; exclude buffers --- Doc/library/operator.rst | 2 +- Lib/operator.py | 16 ++++++++++++++-- Lib/test/test_operator.py | 7 ++++++- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/Doc/library/operator.rst b/Doc/library/operator.rst index d6e5455a2f8f12..aeb7d9063b46ae 100644 --- a/Doc/library/operator.rst +++ b/Doc/library/operator.rst @@ -103,7 +103,7 @@ The mathematical and bitwise operations are the most numerous: .. function:: as_float(a) - Return *a* converted to an float. Equivalent to ``float(a)``, except + Return *a* converted to a float. Equivalent to ``float(a)``, except that conversion from a string or bytestring is not permitted. The result always has exact type :class:`float`. diff --git a/Lib/operator.py b/Lib/operator.py index a0760545f2d416..0da0e02ca75c22 100644 --- a/Lib/operator.py +++ b/Lib/operator.py @@ -86,8 +86,20 @@ def as_float(obj): Same as float(obj), but does not accept strings. """ - if isinstance(obj, (str, bytes, bytearray)): - raise TypeError("as_float argument must be numeric") + # Exclude strings and anything exposing the buffer interface. + bad_type = False + if isinstance(obj, str): + bad_type = True + else: + try: + memoryview(obj) + bad_type = True + except TypeError: + pass + + if bad_type: + raise TypeError(f"must be real number, not {obj.__class__.__name__}") + return float(obj) def floordiv(a, b): diff --git a/Lib/test/test_operator.py b/Lib/test/test_operator.py index e588c7583afef2..c2922afaa86b53 100644 --- a/Lib/test/test_operator.py +++ b/Lib/test/test_operator.py @@ -562,7 +562,12 @@ def __index__(self): self.assertIsFloatWithValue(operator.as_float(Confused()), 123.456) # Not convertible. - bad_values = ["123", b"123", bytearray(b"123"), None, 1j] + import array + + bad_values = [ + "123", b"123", bytearray(b"123"), None, 1j, + array.array('B', b"123.0"), + ] for bad_value in bad_values: with self.subTest(bad_value=bad_value): with self.assertRaises(TypeError):