bpo-35712: Make using NotImplemented in a boolean context issue a deprecation warning (GH-13195)

This commit is contained in:
MojoVampire 2020-03-03 18:50:17 +00:00 committed by GitHub
parent ae75a29435
commit 469325c30e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 64 additions and 12 deletions

View File

@ -31,7 +31,7 @@ A small number of constants live in the built-in namespace. They are:
etc.) to indicate that the operation is not implemented with respect to etc.) to indicate that the operation is not implemented with respect to
the other type; may be returned by the in-place binary special methods the other type; may be returned by the in-place binary special methods
(e.g. :meth:`__imul__`, :meth:`__iand__`, etc.) for the same purpose. (e.g. :meth:`__imul__`, :meth:`__iand__`, etc.) for the same purpose.
Its truth value is true. It should not be evaluated in a boolean context.
.. note:: .. note::
@ -50,6 +50,11 @@ A small number of constants live in the built-in namespace. They are:
even though they have similar names and purposes. even though they have similar names and purposes.
See :exc:`NotImplementedError` for details on when to use it. See :exc:`NotImplementedError` for details on when to use it.
.. versionchanged:: 3.9
Evaluating ``NotImplemented`` in a boolean context is deprecated. While
it currently evaluates as true, it will emit a :exc:`DeprecationWarning`.
It will raise a :exc:`TypeError` in a future version of Python.
.. index:: single: ...; ellipsis literal .. index:: single: ...; ellipsis literal
.. data:: Ellipsis .. data:: Ellipsis

View File

@ -156,13 +156,18 @@ NotImplemented
object is accessed through the built-in name ``NotImplemented``. Numeric methods object is accessed through the built-in name ``NotImplemented``. Numeric methods
and rich comparison methods should return this value if they do not implement the and rich comparison methods should return this value if they do not implement the
operation for the operands provided. (The interpreter will then try the operation for the operands provided. (The interpreter will then try the
reflected operation, or some other fallback, depending on the operator.) Its reflected operation, or some other fallback, depending on the operator.) It
truth value is true. should not be evaluated in a boolean context.
See See
:ref:`implementing-the-arithmetic-operations` :ref:`implementing-the-arithmetic-operations`
for more details. for more details.
.. versionchanged:: 3.9
Evaluating ``NotImplemented`` in a boolean context is deprecated. While
it currently evaluates as true, it will emit a :exc:`DeprecationWarning`.
It will raise a :exc:`TypeError` in a future version of Python.
Ellipsis Ellipsis
.. index:: .. index::

View File

@ -452,6 +452,12 @@ Deprecated
of Python. For the majority of use cases, users can leverage the Abstract Syntax of Python. For the majority of use cases, users can leverage the Abstract Syntax
Tree (AST) generation and compilation stage, using the :mod:`ast` module. Tree (AST) generation and compilation stage, using the :mod:`ast` module.
* Using :data:`NotImplemented` in a boolean context has been deprecated,
as it is almost exclusively the result of incorrect rich comparator
implementations. It will be made a :exc:`TypeError` in a future version
of Python.
(Contributed by Josh Rosenberg in :issue:`35712`.)
* The :mod:`random` module currently accepts any hashable type as a * The :mod:`random` module currently accepts any hashable type as a
possible seed value. Unfortunately, some of those types are not possible seed value. Unfortunately, some of those types are not
guaranteed to have a deterministic hash value. After Python 3.9, guaranteed to have a deterministic hash value. After Python 3.9,

View File

@ -96,6 +96,8 @@ def _gt_from_lt(self, other, NotImplemented=NotImplemented):
def _le_from_lt(self, other, NotImplemented=NotImplemented): def _le_from_lt(self, other, NotImplemented=NotImplemented):
'Return a <= b. Computed by @total_ordering from (a < b) or (a == b).' 'Return a <= b. Computed by @total_ordering from (a < b) or (a == b).'
op_result = self.__lt__(other) op_result = self.__lt__(other)
if op_result is NotImplemented:
return op_result
return op_result or self == other return op_result or self == other
def _ge_from_lt(self, other, NotImplemented=NotImplemented): def _ge_from_lt(self, other, NotImplemented=NotImplemented):
@ -136,6 +138,8 @@ def _lt_from_gt(self, other, NotImplemented=NotImplemented):
def _ge_from_gt(self, other, NotImplemented=NotImplemented): def _ge_from_gt(self, other, NotImplemented=NotImplemented):
'Return a >= b. Computed by @total_ordering from (a > b) or (a == b).' 'Return a >= b. Computed by @total_ordering from (a > b) or (a == b).'
op_result = self.__gt__(other) op_result = self.__gt__(other)
if op_result is NotImplemented:
return op_result
return op_result or self == other return op_result or self == other
def _le_from_gt(self, other, NotImplemented=NotImplemented): def _le_from_gt(self, other, NotImplemented=NotImplemented):

View File

@ -1398,7 +1398,7 @@ class IPv4Interface(IPv4Address):
def __eq__(self, other): def __eq__(self, other):
address_equal = IPv4Address.__eq__(self, other) address_equal = IPv4Address.__eq__(self, other)
if not address_equal or address_equal is NotImplemented: if address_equal is NotImplemented or not address_equal:
return address_equal return address_equal
try: try:
return self.network == other.network return self.network == other.network
@ -2096,7 +2096,7 @@ class IPv6Interface(IPv6Address):
def __eq__(self, other): def __eq__(self, other):
address_equal = IPv6Address.__eq__(self, other) address_equal = IPv6Address.__eq__(self, other)
if not address_equal or address_equal is NotImplemented: if address_equal is NotImplemented or not address_equal:
return address_equal return address_equal
try: try:
return self.network == other.network return self.network == other.network
@ -2109,7 +2109,7 @@ class IPv6Interface(IPv6Address):
def __lt__(self, other): def __lt__(self, other):
address_less = IPv6Address.__lt__(self, other) address_less = IPv6Address.__lt__(self, other)
if address_less is NotImplemented: if address_less is NotImplemented:
return NotImplemented return address_less
try: try:
return (self.network < other.network or return (self.network < other.network or
self.network == other.network and address_less) self.network == other.network and address_less)

View File

@ -2528,7 +2528,7 @@ class TestBufferProtocol(unittest.TestCase):
values = [INT(9), IDX(9), values = [INT(9), IDX(9),
2.2+3j, Decimal("-21.1"), 12.2, Fraction(5, 2), 2.2+3j, Decimal("-21.1"), 12.2, Fraction(5, 2),
[1,2,3], {4,5,6}, {7:8}, (), (9,), [1,2,3], {4,5,6}, {7:8}, (), (9,),
True, False, None, NotImplemented, True, False, None, Ellipsis,
b'a', b'abc', bytearray(b'a'), bytearray(b'abc'), b'a', b'abc', bytearray(b'a'), bytearray(b'abc'),
'a', 'abc', r'a', r'abc', 'a', 'abc', r'a', r'abc',
f, lambda x: x] f, lambda x: x]

View File

@ -1666,6 +1666,20 @@ class BuiltinTest(unittest.TestCase):
self.assertRaises(TypeError, tp, 1, 2) self.assertRaises(TypeError, tp, 1, 2)
self.assertRaises(TypeError, tp, a=1, b=2) self.assertRaises(TypeError, tp, a=1, b=2)
def test_warning_notimplemented(self):
# Issue #35712: NotImplemented is a sentinel value that should never
# be evaluated in a boolean context (virtually all such use cases
# are a result of accidental misuse implementing rich comparison
# operations in terms of one another).
# For the time being, it will continue to evaluate as truthy, but
# issue a deprecation warning (with the eventual intent to make it
# a TypeError).
self.assertWarns(DeprecationWarning, bool, NotImplemented)
with self.assertWarns(DeprecationWarning):
self.assertTrue(NotImplemented)
with self.assertWarns(DeprecationWarning):
self.assertFalse(not NotImplemented)
class TestBreakpoint(unittest.TestCase): class TestBreakpoint(unittest.TestCase):
def setUp(self): def setUp(self):

View File

@ -2526,9 +2526,9 @@ order (MRO) for bases """
except TypeError: except TypeError:
pass pass
# Two essentially featureless objects, just inheriting stuff from # Two essentially featureless objects, (Ellipsis just inherits stuff
# object. # from object.
self.assertEqual(dir(NotImplemented), dir(Ellipsis)) self.assertEqual(dir(object()), dir(Ellipsis))
# Nasty test case for proxied objects # Nasty test case for proxied objects
class Wrapper(object): class Wrapper(object):

View File

@ -0,0 +1,2 @@
Using :data:`NotImplemented` in a boolean context has been deprecated. Patch
contributed by Josh Rosenberg.

View File

@ -1673,6 +1673,22 @@ notimplemented_dealloc(PyObject* ignore)
Py_FatalError("deallocating NotImplemented"); Py_FatalError("deallocating NotImplemented");
} }
static int
notimplemented_bool(PyObject *v)
{
if (PyErr_WarnEx(PyExc_DeprecationWarning,
"NotImplemented should not be used in a boolean context",
1) < 0)
{
return -1;
}
return 1;
}
static PyNumberMethods notimplemented_as_number = {
.nb_bool = notimplemented_bool,
};
PyTypeObject _PyNotImplemented_Type = { PyTypeObject _PyNotImplemented_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0) PyVarObject_HEAD_INIT(&PyType_Type, 0)
"NotImplementedType", "NotImplementedType",
@ -1683,8 +1699,8 @@ PyTypeObject _PyNotImplemented_Type = {
0, /*tp_getattr*/ 0, /*tp_getattr*/
0, /*tp_setattr*/ 0, /*tp_setattr*/
0, /*tp_as_async*/ 0, /*tp_as_async*/
NotImplemented_repr, /*tp_repr*/ NotImplemented_repr, /*tp_repr*/
0, /*tp_as_number*/ &notimplemented_as_number, /*tp_as_number*/
0, /*tp_as_sequence*/ 0, /*tp_as_sequence*/
0, /*tp_as_mapping*/ 0, /*tp_as_mapping*/
0, /*tp_hash */ 0, /*tp_hash */