From 42483387d0bdfce0b48db94400054ad16bfd9ef1 Mon Sep 17 00:00:00 2001 From: Bas van Beek Date: Tue, 31 Aug 2021 16:36:58 +0200 Subject: [PATCH 1/6] ENH: Add `is_integer` to the `np.floating` subclasses --- numpy/__init__.pyi | 2 +- numpy/core/_add_newdocs_scalars.py | 29 +++++++++++++++---- numpy/core/src/multiarray/scalartypes.c.src | 31 +++++++++++++++++++++ 3 files changed, 55 insertions(+), 7 deletions(-) diff --git a/numpy/__init__.pyi b/numpy/__init__.pyi index f398f67b789b..b2c64d17cd01 100644 --- a/numpy/__init__.pyi +++ b/numpy/__init__.pyi @@ -3342,7 +3342,7 @@ class floating(inexact[_NBit1]): __args: Union[L[0], Tuple[()], Tuple[L[0]]] = ..., ) -> float: ... def tolist(self) -> float: ... - def is_integer(self: float64) -> bool: ... + def is_integer(self) -> bool: ... def hex(self: float64) -> str: ... @classmethod def fromhex(cls: Type[float64], __string: str) -> float64: ... diff --git a/numpy/core/_add_newdocs_scalars.py b/numpy/core/_add_newdocs_scalars.py index 602b1db6e64a..709a71889e13 100644 --- a/numpy/core/_add_newdocs_scalars.py +++ b/numpy/core/_add_newdocs_scalars.py @@ -205,12 +205,12 @@ def add_newdoc_for_scalar_type(obj, fixed_aliases, doc): add_newdoc_for_scalar_type('void', [], r""" Either an opaque sequence of bytes, or a structure. - + >>> np.void(b'abcd') void(b'\x61\x62\x63\x64') - + Structured `void` scalars can only be constructed via extraction from :ref:`structured_arrays`: - + >>> arr = np.array((1, 2), dtype=[('x', np.int8), ('y', np.int8)]) >>> arr[()] (1, 2) # looks like a tuple, but is `np.void` @@ -226,17 +226,17 @@ def add_newdoc_for_scalar_type(obj, fixed_aliases, doc): >>> np.datetime64(10, 'Y') numpy.datetime64('1980') >>> np.datetime64('1980', 'Y') - numpy.datetime64('1980') + numpy.datetime64('1980') >>> np.datetime64(10, 'D') numpy.datetime64('1970-01-11') - + See :ref:`arrays.datetime` for more information. """) add_newdoc_for_scalar_type('timedelta64', [], """ A timedelta stored as a 64-bit integer. - + See :ref:`arrays.datetime` for more information. """) @@ -257,3 +257,20 @@ def add_newdoc_for_scalar_type(obj, fixed_aliases, doc): >>> np.{ftype}(-.25).as_integer_ratio() (-1, 4) """.format(ftype=float_name))) + + add_newdoc('numpy.core.numerictypes', float_name, ('is_integer', + f""" + {float_name}.is_integer() -> bool + + Return ``True`` if the floating point number is finite with integral + value, and ``False`` otherwise. + + .. versionadded:: 1.22 + + Examples + -------- + >>> np.{float_name}(-2.0).is_integer() + True + >>> np.{float_name}(3.2).is_integer() + False + """)) diff --git a/numpy/core/src/multiarray/scalartypes.c.src b/numpy/core/src/multiarray/scalartypes.c.src index 40f736125de3..bf22acfec411 100644 --- a/numpy/core/src/multiarray/scalartypes.c.src +++ b/numpy/core/src/multiarray/scalartypes.c.src @@ -1908,6 +1908,34 @@ error: } /**end repeat**/ +/**begin repeat + * #name = half, float, double, longdouble# + * #Name = Half, Float, Double, LongDouble# + * #is_half = 1,0,0,0# + * #c = f, f, , l# + */ +static PyObject * +@name@_is_integer(PyObject *self) +{ +#if @is_half@ + npy_double val = npy_half_to_double(PyArrayScalar_VAL(self, @Name@)); +#else + npy_@name@ val = PyArrayScalar_VAL(self, @Name@); +#endif + PyObject *o; + + if (npy_isnan(val)) { + Py_RETURN_FALSE; + } + if (!npy_isfinite(val)) { + Py_RETURN_FALSE; + } + + o = (npy_floor@c@(val) == val) ? Py_True : Py_False; + Py_INCREF(o); + return o; +} +/**end repeat**/ /* * need to fill in doc-strings for these methods on import -- copy from @@ -2185,6 +2213,9 @@ static PyMethodDef @name@type_methods[] = { {"as_integer_ratio", (PyCFunction)@name@_as_integer_ratio, METH_NOARGS, NULL}, + {"is_integer", + (PyCFunction)@name@_is_integer, + METH_NOARGS, NULL}, {NULL, NULL, 0, NULL} }; /**end repeat**/ From 5d86d8c9cc70f87b4aab10682d101acd8c4fb781 Mon Sep 17 00:00:00 2001 From: Bas van Beek Date: Tue, 31 Aug 2021 16:38:14 +0200 Subject: [PATCH 2/6] TST: Add tests for `np.floating.is_integer` --- numpy/core/tests/test_scalar_methods.py | 22 ++++++++++++++++++++++ numpy/typing/tests/data/fail/scalars.py | 1 - 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/numpy/core/tests/test_scalar_methods.py b/numpy/core/tests/test_scalar_methods.py index 3693bba59ce6..7bad6bebf3bc 100644 --- a/numpy/core/tests/test_scalar_methods.py +++ b/numpy/core/tests/test_scalar_methods.py @@ -102,3 +102,25 @@ def test_roundtrip(self, ftype, frac_vals, exp_vals): pytest.skip("longdouble too small on this platform") assert_equal(nf / df, f, "{}/{}".format(n, d)) + + +@pytest.mark.parametrize("code", np.typecodes["Float"]) +class TestIsInteger: + @pytest.mark.parametrize("str_value", ["inf", "nan"]) + def test_special(self, code: str, str_value: str) -> None: + cls = np.dtype(code).type + value = cls(str_value) + assert not value.is_integer() + + def test_true(self, code: str) -> None: + float_array = np.arange(-5, 5).astype(code) + for value in float_array: + assert value.is_integer() + + def test_false(self, code: str) -> None: + float_array = np.arange(-5, 5).astype(code) + float_array *= 1.1 + for value in float_array: + if value == 0: + continue + assert not value.is_integer() diff --git a/numpy/typing/tests/data/fail/scalars.py b/numpy/typing/tests/data/fail/scalars.py index 099418e67a81..94fe3f71e646 100644 --- a/numpy/typing/tests/data/fail/scalars.py +++ b/numpy/typing/tests/data/fail/scalars.py @@ -87,7 +87,6 @@ def func(a: np.float32) -> None: ... c8.__getnewargs__() # E: Invalid self argument f2.__getnewargs__() # E: Invalid self argument -f2.is_integer() # E: Invalid self argument f2.hex() # E: Invalid self argument np.float16.fromhex("0x0.0p+0") # E: Invalid self argument f2.__trunc__() # E: Invalid self argument From 9f11564c455f00fe5faa0a92aa02f1f3f59fc901 Mon Sep 17 00:00:00 2001 From: Bas van Beek Date: Tue, 31 Aug 2021 17:41:36 +0200 Subject: [PATCH 3/6] ENH: Add `integer.is_integer` Match `int.is_integer`, which was added in python/cpython#6121 --- numpy/__init__.pyi | 1 + numpy/core/_add_newdocs_scalars.py | 12 ++++++++++++ numpy/core/src/multiarray/scalartypes.c.src | 18 +++++++++++++++++- numpy/core/tests/test_scalar_methods.py | 6 +++++- numpy/typing/tests/data/reveal/scalars.py | 2 ++ 5 files changed, 37 insertions(+), 2 deletions(-) diff --git a/numpy/__init__.pyi b/numpy/__init__.pyi index b2c64d17cd01..f196556167a5 100644 --- a/numpy/__init__.pyi +++ b/numpy/__init__.pyi @@ -3190,6 +3190,7 @@ class integer(number[_NBit1]): # type: ignore __args: Union[L[0], Tuple[()], Tuple[L[0]]] = ..., ) -> int: ... def tolist(self) -> int: ... + def is_integer(self) -> L[True]: ... def __index__(self) -> int: ... __truediv__: _IntTrueDiv[_NBit1] __rtruediv__: _IntTrueDiv[_NBit1] diff --git a/numpy/core/_add_newdocs_scalars.py b/numpy/core/_add_newdocs_scalars.py index 709a71889e13..306adbc555a3 100644 --- a/numpy/core/_add_newdocs_scalars.py +++ b/numpy/core/_add_newdocs_scalars.py @@ -240,6 +240,18 @@ def add_newdoc_for_scalar_type(obj, fixed_aliases, doc): See :ref:`arrays.datetime` for more information. """) +add_newdoc('numpy.core.numerictypes', "integer", ('is_integer', + f""" + integer.is_integer() -> bool + + Return ``True`` if the number is finite with integral value. + + >>> np.int64(-2).is_integer() + True + >>> np.uint32(5).is_integer() + True + """)) + # TODO: work out how to put this on the base class, np.floating for float_name in ('half', 'single', 'double', 'longdouble'): add_newdoc('numpy.core.numerictypes', float_name, ('as_integer_ratio', diff --git a/numpy/core/src/multiarray/scalartypes.c.src b/numpy/core/src/multiarray/scalartypes.c.src index bf22acfec411..c9f3341fe441 100644 --- a/numpy/core/src/multiarray/scalartypes.c.src +++ b/numpy/core/src/multiarray/scalartypes.c.src @@ -1937,6 +1937,11 @@ static PyObject * } /**end repeat**/ +static PyObject * +integer_is_integer(PyObject *self) { + Py_RETURN_TRUE; +} + /* * need to fill in doc-strings for these methods on import -- copy from * array docstrings @@ -2195,7 +2200,7 @@ static PyMethodDef @name@type_methods[] = { /**end repeat**/ /**begin repeat - * #name = integer,floating, complexfloating# + * #name = floating, complexfloating# */ static PyMethodDef @name@type_methods[] = { /* Hook for the round() builtin */ @@ -2206,6 +2211,17 @@ static PyMethodDef @name@type_methods[] = { }; /**end repeat**/ +static PyMethodDef integertype_methods[] = { + /* Hook for the round() builtin */ + {"__round__", + (PyCFunction)integertype_dunder_round, + METH_VARARGS | METH_KEYWORDS, NULL}, + {"is_integer", + (PyCFunction)integer_is_integer, + METH_NOARGS, NULL}, + {NULL, NULL, 0, NULL} /* sentinel */ +}; + /**begin repeat * #name = half,float,double,longdouble# */ diff --git a/numpy/core/tests/test_scalar_methods.py b/numpy/core/tests/test_scalar_methods.py index 7bad6bebf3bc..94b2dd3c970d 100644 --- a/numpy/core/tests/test_scalar_methods.py +++ b/numpy/core/tests/test_scalar_methods.py @@ -104,19 +104,23 @@ def test_roundtrip(self, ftype, frac_vals, exp_vals): assert_equal(nf / df, f, "{}/{}".format(n, d)) -@pytest.mark.parametrize("code", np.typecodes["Float"]) class TestIsInteger: @pytest.mark.parametrize("str_value", ["inf", "nan"]) + @pytest.mark.parametrize("code", np.typecodes["Float"]) def test_special(self, code: str, str_value: str) -> None: cls = np.dtype(code).type value = cls(str_value) assert not value.is_integer() + @pytest.mark.parametrize( + "code", np.typecodes["Float"] + np.typecodes["AllInteger"] + ) def test_true(self, code: str) -> None: float_array = np.arange(-5, 5).astype(code) for value in float_array: assert value.is_integer() + @pytest.mark.parametrize("code", np.typecodes["Float"]) def test_false(self, code: str) -> None: float_array = np.arange(-5, 5).astype(code) float_array *= 1.1 diff --git a/numpy/typing/tests/data/reveal/scalars.py b/numpy/typing/tests/data/reveal/scalars.py index c3681300421d..e83d579e9b70 100644 --- a/numpy/typing/tests/data/reveal/scalars.py +++ b/numpy/typing/tests/data/reveal/scalars.py @@ -156,3 +156,5 @@ if sys.version_info >= (3, 9): reveal_type(f8.__ceil__()) # E: int reveal_type(f8.__floor__()) # E: int + +reveal_type(i8.is_integer()) # E: Literal[True] From 38be2fd05974c57a5234d199077b52365745bf18 Mon Sep 17 00:00:00 2001 From: Bas van Beek Date: Tue, 31 Aug 2021 17:01:11 +0200 Subject: [PATCH 4/6] DOC: Add a release not for `floating.is_integer` --- doc/release/upcoming_changes/19803.new_feature.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 doc/release/upcoming_changes/19803.new_feature.rst diff --git a/doc/release/upcoming_changes/19803.new_feature.rst b/doc/release/upcoming_changes/19803.new_feature.rst new file mode 100644 index 000000000000..b35c581edcba --- /dev/null +++ b/doc/release/upcoming_changes/19803.new_feature.rst @@ -0,0 +1,14 @@ +``is_integer`` is now available to `numpy.floating` and `numpy.integer` +----------------------------------------------------------------------- +Based on its counterpart in `float` and `int, the numpy floating point and +integer types types now support `~float.is_integer`. Returns ``True`` if the +floating point number is finite with integral value, and ``False`` otherwise. + +.. code-block:: python + + >>> np.float32(-2.0).is_integer() + True + >>> np.float64(3.2).is_integer() + False + >>> np.int32(-2).is_integer() + True From 11ae8fef08251d19207f1e7a08bca580298514b4 Mon Sep 17 00:00:00 2001 From: Bas van Beek <43369155+BvB93@users.noreply.github.com> Date: Tue, 31 Aug 2021 21:26:35 +0200 Subject: [PATCH 5/6] DOC: Misc documentation improvements --- doc/release/upcoming_changes/19803.new_feature.rst | 6 +++--- numpy/core/_add_newdocs_scalars.py | 6 +++++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/doc/release/upcoming_changes/19803.new_feature.rst b/doc/release/upcoming_changes/19803.new_feature.rst index b35c581edcba..942325822c91 100644 --- a/doc/release/upcoming_changes/19803.new_feature.rst +++ b/doc/release/upcoming_changes/19803.new_feature.rst @@ -1,8 +1,8 @@ ``is_integer`` is now available to `numpy.floating` and `numpy.integer` ----------------------------------------------------------------------- -Based on its counterpart in `float` and `int, the numpy floating point and -integer types types now support `~float.is_integer`. Returns ``True`` if the -floating point number is finite with integral value, and ``False`` otherwise. +Based on its counterpart in `float` and `int`, the numpy floating point and +integer types now support `~float.is_integer`. Returns ``True`` if the +number is finite with integral value, and ``False`` otherwise. .. code-block:: python diff --git a/numpy/core/_add_newdocs_scalars.py b/numpy/core/_add_newdocs_scalars.py index 306adbc555a3..8773d6c9631d 100644 --- a/numpy/core/_add_newdocs_scalars.py +++ b/numpy/core/_add_newdocs_scalars.py @@ -241,11 +241,15 @@ def add_newdoc_for_scalar_type(obj, fixed_aliases, doc): """) add_newdoc('numpy.core.numerictypes', "integer", ('is_integer', - f""" + """ integer.is_integer() -> bool Return ``True`` if the number is finite with integral value. + .. versionadded:: 1.22 + + Examples + -------- >>> np.int64(-2).is_integer() True >>> np.uint32(5).is_integer() From bb3e4077a32078deb31d7c31034303303fc268f4 Mon Sep 17 00:00:00 2001 From: Bas van Beek <43369155+BvB93@users.noreply.github.com> Date: Thu, 2 Sep 2021 21:05:54 +0200 Subject: [PATCH 6/6] STY: Use a more descriptive variable name Co-Authored-By: Charles Harris --- numpy/core/src/multiarray/scalartypes.c.src | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/numpy/core/src/multiarray/scalartypes.c.src b/numpy/core/src/multiarray/scalartypes.c.src index c9f3341fe441..740ec8cc28cd 100644 --- a/numpy/core/src/multiarray/scalartypes.c.src +++ b/numpy/core/src/multiarray/scalartypes.c.src @@ -1922,7 +1922,7 @@ static PyObject * #else npy_@name@ val = PyArrayScalar_VAL(self, @Name@); #endif - PyObject *o; + PyObject *ret; if (npy_isnan(val)) { Py_RETURN_FALSE; @@ -1931,9 +1931,9 @@ static PyObject * Py_RETURN_FALSE; } - o = (npy_floor@c@(val) == val) ? Py_True : Py_False; - Py_INCREF(o); - return o; + ret = (npy_floor@c@(val) == val) ? Py_True : Py_False; + Py_INCREF(ret); + return ret; } /**end repeat**/