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

gh-126061: Add PyLong_IsPositive/Zero/Negative() functions #126065

Merged
merged 20 commits into from
Nov 12, 2024
Merged
Show file tree
Hide file tree
Changes from 18 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
33 changes: 33 additions & 0 deletions Doc/c-api/long.rst
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,39 @@ distinguished from a number. Use :c:func:`PyErr_Occurred` to disambiguate.
.. versionadded:: 3.14
.. c:function:: int PyLong_IsPositive(PyObject *obj)
Check if the integer object *obj* is positive (``obj > 0``).
Copy link
Contributor

Choose a reason for hiding this comment

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

AFAICT, obj must not be NULL since otherwise it crashes (it's not the same as setting an exception). Other functions in this section mention when an input must not be NULL.

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 such details are documented for other functions.

But maybe we should add instead the NULL check (like e.g. PyLong_AsNativeBytes). IIUC, there is no rule to do so for new functions.

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think such details are documented for other functions.

There is: https://docs.python.org/3.14/c-api/long.html#c.PyLong_AsInt32. Those were recently added functions so that's why I suggested continuing specifying whether a NULL is allowed or not.

Copy link
Member

Choose a reason for hiding this comment

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

In fact, PyLong_AsInt32 doesn't crashes on NULL. So, value must not be NULL sentence is slightly redundant: in this case function set an exception and return -1 (exactly as specified right above).

Copy link
Contributor

@picnixz picnixz Nov 11, 2024

Choose a reason for hiding this comment

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

Ah, maybe I wasn't clear but value in this case is the output buffer which is then dereferenced without a NULL check. It's not the PyObject input. My point was that when we do not expect a NULL value (and don't check for it), we specify it.

Copy link
Member

Choose a reason for hiding this comment

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

Generally (unless documented otherwise), the C API has undefined behaviour if you pass in a NULL PyObject *. Usually it'll crash. Sometimes we assert that it's not NULL: that would probably be best here. But we don't need an if.

I advise against adding a note in the docs about NULL, since it could hint (to humans) that one could pass NULL to other functions.

PyLong_AsInt32 is different: there the pointer is an “output argument” that the function fills in. An explicit note makes more sense there. Generally, for many functions like that, it's sometimes useful to run other effects of a function without getting the result filled in, and so some functions like this do accept NULL.

Copy link
Contributor

Choose a reason for hiding this comment

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

I advise against adding a note in the docs about NULL, since it could hint (to humans) that one could pass NULL to other functions.

I see your point here. The alternative is to specify it to other functions explicitly as well. But it's probably better to just do it in one docs PR later if needed. So I don't mind not specifying "do not pass NULL".

Copy link
Member

Choose a reason for hiding this comment

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

Ok, no docs changes. I'll keep this comment unresolved, in case we want handle obj==NULL case in functions as suggested.

If *obj* is an instance of :c:type:`PyLongObject` or its subtype,
return ``1`` when it's positive and ``0`` otherwise. Else set an
exception and return ``-1``.
.. versionadded:: next
.. c:function:: int PyLong_IsNegative(PyObject *obj)
Check if the integer object *obj* is negative (``obj < 0``).
skirpichev marked this conversation as resolved.
Show resolved Hide resolved
If *obj* is an instance of :c:type:`PyLongObject` or its subtype,
return ``1`` when it's negative and ``0`` otherwise. Else set an
exception and return ``-1``.
.. versionadded:: next
.. c:function:: int PyLong_IsZero(PyObject *obj)
Check if the integer object *obj* is zero.
skirpichev marked this conversation as resolved.
Show resolved Hide resolved
If *obj* is an instance of :c:type:`PyLongObject` or its subtype,
return ``1`` when it's zero and ``0`` otherwise. Else set an
exception and return ``-1``.
.. versionadded:: next
.. c:function:: PyObject* PyLong_GetInfo(void)
On success, return a read only :term:`named tuple`, that holds
Expand Down
5 changes: 5 additions & 0 deletions Doc/whatsnew/3.14.rst
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,11 @@ New Features
an interned string and deallocate it during module shutdown.
(Contribued by Eddie Elizondo in :gh:`113601`.)

* Add :c:func:`PyLong_IsPositive`, :c:func:`PyLong_IsNegative`
and :c:func:`PyLong_IsZero` for checking if :c:type:`PyLongObject`
is positive, negative, or zero, respectively.
(Contribued by James Roy and Sergey B Kirpichev in :gh:`126061`.)

* Add new functions to convert C ``<stdint.h>`` numbers from/to Python
:class:`int`:

Expand Down
18 changes: 18 additions & 0 deletions Include/cpython/longobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,24 @@ PyAPI_FUNC(PyObject*) PyLong_FromUnsignedNativeBytes(const void* buffer,
PyAPI_FUNC(int) PyUnstable_Long_IsCompact(const PyLongObject* op);
PyAPI_FUNC(Py_ssize_t) PyUnstable_Long_CompactValue(const PyLongObject* op);

/* PyLong_IsPositive. Check if the integer object is positive.
- On success, return 1 if *obj is positive, and 0 otherwise.
- On failure, set an exception, and return -1. */
PyAPI_FUNC(int) PyLong_IsPositive(PyObject *obj);

/* PyLong_IsNegative. Check if the integer object is negative.
- On success, return 1 if *obj is negative, and 0 otherwise.
- On failure, set an exception, and return -1. */
PyAPI_FUNC(int) PyLong_IsNegative(PyObject *obj);

/* PyLong_IsZero. Check if the integer object is zero.
- On success, return 1 if *obj is zero, and 0 if it is non-zero.
- On failure, set an exception, and return -1. */
PyAPI_FUNC(int) PyLong_IsZero(PyObject *obj);
skirpichev marked this conversation as resolved.
Show resolved Hide resolved

/* PyLong_GetSign. Get the sign of an integer object:
0, -1 or +1 for zero, negative or positive integer, respectively.
Expand Down
45 changes: 45 additions & 0 deletions Lib/test/test_capi/test_long.py
Original file line number Diff line number Diff line change
Expand Up @@ -643,6 +643,51 @@ def test_long_getsign(self):

# CRASHES getsign(NULL)

def test_long_ispositive(self):
rruuaanng marked this conversation as resolved.
Show resolved Hide resolved
# Test PyLong_IsPositive()
ispositive = _testcapi.pylong_ispositive
self.assertEqual(ispositive(1), 1)
self.assertEqual(ispositive(123), 1)
self.assertEqual(ispositive(-1), 0)
self.assertEqual(ispositive(0), 0)
self.assertEqual(ispositive(True), 1)
self.assertEqual(ispositive(False), 0)
self.assertEqual(ispositive(IntSubclass(-1)), 0)
self.assertRaises(TypeError, ispositive, 1.0)
self.assertRaises(TypeError, ispositive, Index(123))
rruuaanng marked this conversation as resolved.
Show resolved Hide resolved
ZeroIntensity marked this conversation as resolved.
Show resolved Hide resolved

# CRASHES ispositive(NULL)
Comment on lines +658 to +659
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
# CRASHES ispositive(NULL)
self.assertRaises(SystemError, ispositive, NULL)


def test_long_isnegative(self):
rruuaanng marked this conversation as resolved.
Show resolved Hide resolved
# Test PyLong_IsNegative()
isnegative = _testcapi.pylong_isnegative
self.assertEqual(isnegative(1), 0)
self.assertEqual(isnegative(123), 0)
self.assertEqual(isnegative(-1), 1)
self.assertEqual(isnegative(0), 0)
self.assertEqual(isnegative(True), 0)
self.assertEqual(isnegative(False), 0)
self.assertEqual(isnegative(IntSubclass(-1)), 1)
self.assertRaises(TypeError, isnegative, 1.0)
self.assertRaises(TypeError, isnegative, Index(123))
rruuaanng marked this conversation as resolved.
Show resolved Hide resolved

# CRASHES isnegative(NULL)
Comment on lines +673 to +674
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
# CRASHES isnegative(NULL)
self.assertRaises(SystemError, isnegative, NULL)


def test_long_iszero(self):
rruuaanng marked this conversation as resolved.
Show resolved Hide resolved
# Test PyLong_IsZero()
iszero = _testcapi.pylong_iszero
self.assertEqual(iszero(1), 0)
self.assertEqual(iszero(-1), 0)
self.assertEqual(iszero(0), 1)
rruuaanng marked this conversation as resolved.
Show resolved Hide resolved
self.assertEqual(iszero(True), 0)
self.assertEqual(iszero(False), 1)
self.assertEqual(iszero(IntSubclass(-1)), 0)
self.assertEqual(iszero(IntSubclass(0)), 1)
self.assertRaises(TypeError, iszero, 1.0)
self.assertRaises(TypeError, iszero, Index(123))
rruuaanng marked this conversation as resolved.
Show resolved Hide resolved

# CRASHES iszero(NULL)
Comment on lines +688 to +689
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
# CRASHES iszero(NULL)
self.assertRaises(SystemError, iszero, NULL)


def test_long_asint32(self):
# Test PyLong_AsInt32() and PyLong_FromInt32()
to_int32 = _testlimitedcapi.pylong_asint32
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Add :c:func:`PyLong_IsPositive`, :c:func:`PyLong_IsNegative`
and :c:func:`PyLong_IsZero` for checking if :c:type:`PyLongObject`
is positive, negative, or zero, respectively.
skirpichev marked this conversation as resolved.
Show resolved Hide resolved
27 changes: 27 additions & 0 deletions Modules/_testcapi/long.c
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,30 @@ pylong_getsign(PyObject *module, PyObject *arg)
}


static PyObject *
pylong_ispositive(PyObject *module, PyObject *arg)
{
NULLABLE(arg);
RETURN_INT(PyLong_IsPositive(arg));
rruuaanng marked this conversation as resolved.
Show resolved Hide resolved
}


static PyObject *
pylong_isnegative(PyObject *module, PyObject *arg)
{
NULLABLE(arg);
RETURN_INT(PyLong_IsNegative(arg));
}


static PyObject *
pylong_iszero(PyObject *module, PyObject *arg)
{
NULLABLE(arg);
RETURN_INT(PyLong_IsZero(arg));
}


static PyObject *
pylong_aspid(PyObject *module, PyObject *arg)
{
Expand All @@ -124,6 +148,9 @@ static PyMethodDef test_methods[] = {
{"pylong_fromnativebytes", pylong_fromnativebytes, METH_VARARGS},
{"pylong_getsign", pylong_getsign, METH_O},
{"pylong_aspid", pylong_aspid, METH_O},
{"pylong_ispositive", pylong_ispositive, METH_O},
{"pylong_isnegative", pylong_isnegative, METH_O},
{"pylong_iszero", pylong_iszero, METH_O},
{NULL},
};

Expand Down
30 changes: 30 additions & 0 deletions Objects/longobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -769,6 +769,36 @@ PyLong_AsUnsignedLongMask(PyObject *op)
return val;
}

int
PyLong_IsPositive(PyObject *obj)
{
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
{
{
if (obj == NULL) {
PyErr_BadInternalCall();
return -1;
}

if (!PyLong_Check(obj)) {
PyErr_Format(PyExc_TypeError, "expected int, got %T", obj);
return -1;
}
return _PyLong_IsPositive((PyLongObject *)obj);
}

int
PyLong_IsNegative(PyObject *obj)
{
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
{
{
if (obj == NULL) {
PyErr_BadInternalCall();
return -1;
}

if (!PyLong_Check(obj)) {
PyErr_Format(PyExc_TypeError, "expected int, got %T", obj);
return -1;
}
return _PyLong_IsNegative((PyLongObject *)obj);
}

int
PyLong_IsZero(PyObject *obj)
{
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
{
{
if (obj == NULL) {
PyErr_BadInternalCall();
return -1;
}

if (!PyLong_Check(obj)) {
PyErr_Format(PyExc_TypeError, "expected int, got %T", obj);
return -1;
}
return _PyLong_IsZero((PyLongObject *)obj);
}

int
_PyLong_Sign(PyObject *vv)
{
Expand Down
Loading