From c70ccc738542bf8a9ca7295b89eeed224d2943d8 Mon Sep 17 00:00:00 2001
From: Mark Dickinson <mdickinson@enthought.com>
Date: Mon, 8 Nov 2021 19:57:44 +0000
Subject: [PATCH 1/3] Fix BaseFloat validation to match Float validation

---
 traits/ctraits.c           | 27 ++++++++++++++++++++++-----
 traits/tests/test_float.py | 28 ++++++++++++++++++++++++++++
 traits/trait_types.py      | 19 +++----------------
 3 files changed, 53 insertions(+), 21 deletions(-)

diff --git a/traits/ctraits.c b/traits/ctraits.c
index 342e16bbd..3515c2092 100644
--- a/traits/ctraits.c
+++ b/traits/ctraits.c
@@ -3339,7 +3339,7 @@ validate_trait_integer(
 */
 
 static PyObject *
-as_float(PyObject *value)
+number_to_float(PyObject *value)
 {
     double value_as_double;
 
@@ -3357,6 +3357,12 @@ as_float(PyObject *value)
     return PyFloat_FromDouble(value_as_double);
 }
 
+static PyObject *
+_ctraits_number_to_float(PyObject *self, PyObject *value)
+{
+    return number_to_float(value);
+}
+
 /*-----------------------------------------------------------------------------
 |  Verifies that a Python value is convertible to float
 |
@@ -3374,7 +3380,7 @@ validate_trait_float(
     trait_object *trait, has_traits_object *obj, PyObject *name,
     PyObject *value)
 {
-    PyObject *result = as_float(value);
+    PyObject *result = number_to_float(value);
     /* A TypeError represents a type validation failure, and should be
        re-raised as a TraitError. Other exceptions should be propagated. */
     if (result == NULL && PyErr_ExceptionMatches(PyExc_TypeError)) {
@@ -3450,7 +3456,7 @@ validate_trait_float_range(
     PyObject *result;
     int in_range;
 
-    result = as_float(value);
+    result = number_to_float(value);
     if (result == NULL) {
         if (PyErr_ExceptionMatches(PyExc_TypeError)) {
             /* Reraise any TypeError as a TraitError. */
@@ -3914,7 +3920,7 @@ validate_trait_complex(
                 break;
 
             case 4: /* Floating point range check: */
-                result = as_float(value);
+                result = number_to_float(value);
                 if (result == NULL) {
                     if (PyErr_ExceptionMatches(PyExc_TypeError)) {
                         /* A TypeError should ultimately get re-raised
@@ -4132,7 +4138,7 @@ validate_trait_complex(
                 /* A TypeError indicates that we don't have a match.
                    Clear the error and continue with the next item
                    in the complex sequence. */
-                result = as_float(value);
+                result = number_to_float(value);
                 if (result == NULL
                     && PyErr_ExceptionMatches(PyExc_TypeError)) {
                     PyErr_Clear();
@@ -5547,6 +5553,15 @@ _ctraits_ctrait(PyObject *self, PyObject *args)
 |  'CTrait' instance methods:
 +----------------------------------------------------------------------------*/
 
+
+PyDoc_STRVAR(
+    _ctraits_number_to_float_doc,
+    "_number_to_float(number)\n"
+    "\n"
+    "Return *number* converted to a float. Raise TypeError if \n"
+    "conversion is not possible.\n"
+);
+
 static PyMethodDef ctraits_methods[] = {
     {"_list_classes", (PyCFunction)_ctraits_list_classes, METH_VARARGS,
      PyDoc_STR(
@@ -5555,6 +5570,8 @@ static PyMethodDef ctraits_methods[] = {
      PyDoc_STR("_adapt(adaptation_function)")},
     {"_ctrait", (PyCFunction)_ctraits_ctrait, METH_VARARGS,
      PyDoc_STR("_ctrait(CTrait_class)")},
+    {"_number_to_float", (PyCFunction)_ctraits_number_to_float, METH_O,
+     _ctraits_number_to_float_doc},
     {NULL, NULL},
 };
 
diff --git a/traits/tests/test_float.py b/traits/tests/test_float.py
index f10590773..c437ba932 100644
--- a/traits/tests/test_float.py
+++ b/traits/tests/test_float.py
@@ -18,6 +18,24 @@
 from traits.testing.optional_dependencies import numpy, requires_numpy
 
 
+class IntegerLike:
+    def __init__(self, value):
+        self._value = value
+
+    def __index__(self):
+        return self._value
+
+
+# Python versions < 3.8 don't support conversion of something with __index__
+# to complex.
+try:
+    float(IntegerLike(3))
+except TypeError:
+    float_accepts_index = False
+else:
+    float_accepts_index = True
+
+
 class MyFloat(object):
     def __init__(self, value):
         self._value = value
@@ -93,6 +111,16 @@ def test_accepts_int(self):
         self.assertIs(type(a.value_or_none), float)
         self.assertEqual(a.value_or_none, 2.0)
 
+    @unittest.skipUnless(
+        float_accepts_index,
+        "float does not support __index__ for this Python version",
+    )
+    def test_accepts_integer_like(self):
+        a = self.test_class()
+        a.value = IntegerLike(3)
+        self.assertIs(type(a.value), float)
+        self.assertEqual(a.value, 3.0)
+
     def test_accepts_float_like(self):
         a = self.test_class()
 
diff --git a/traits/trait_types.py b/traits/trait_types.py
index aa659ebdd..62d15bbf5 100644
--- a/traits/trait_types.py
+++ b/traits/trait_types.py
@@ -24,6 +24,7 @@
 import warnings
 
 from .constants import DefaultValue, TraitKind, ValidateTrait
+from .ctraits import _number_to_float
 from .trait_base import (
     strx,
     get_module_name,
@@ -169,20 +170,6 @@ def _validate_int(value):
         return int(operator.index(value))
 
 
-def _validate_float(value):
-    """ Convert an arbitrary Python object to a float, or raise TypeError.
-    """
-    if type(value) is float:  # fast path for common case
-        return value
-    try:
-        nb_float = type(value).__float__
-    except AttributeError:
-        raise TypeError(
-            "Object of type {!r} not convertible to float".format(type(value))
-        )
-    return nb_float(value)
-
-
 # Trait Types
 
 class Any(TraitType):
@@ -341,7 +328,7 @@ def validate(self, object, name, value):
         Note: The 'fast validator' version performs this check in C.
         """
         try:
-            return _validate_float(value)
+            return _number_to_float(value)
         except TypeError:
             self.error(object, name, value)
 
@@ -1871,7 +1858,7 @@ def float_validate(self, object, name, value):
         # error-reporting purposes.
         original_value = value
         try:
-            value = _validate_float(value)
+            value = _number_to_float(value)
         except TypeError:
             self.error(object, name, original_value)
 

From 17e0f737b161d97d07c30d23b55c3b5068d6d554 Mon Sep 17 00:00:00 2001
From: Mark Dickinson <mdickinson@enthought.com>
Date: Mon, 8 Nov 2021 20:08:54 +0000
Subject: [PATCH 2/3] Rename to reduce code churn and protect against existing
 imports of _validate_float

---
 traits/ctraits.c      | 22 +++++++++++-----------
 traits/trait_types.py |  6 +++---
 2 files changed, 14 insertions(+), 14 deletions(-)

diff --git a/traits/ctraits.c b/traits/ctraits.c
index 3515c2092..09247c575 100644
--- a/traits/ctraits.c
+++ b/traits/ctraits.c
@@ -3339,7 +3339,7 @@ validate_trait_integer(
 */
 
 static PyObject *
-number_to_float(PyObject *value)
+validate_float(PyObject *value)
 {
     double value_as_double;
 
@@ -3358,9 +3358,9 @@ number_to_float(PyObject *value)
 }
 
 static PyObject *
-_ctraits_number_to_float(PyObject *self, PyObject *value)
+_ctraits_validate_float(PyObject *self, PyObject *value)
 {
-    return number_to_float(value);
+    return validate_float(value);
 }
 
 /*-----------------------------------------------------------------------------
@@ -3380,7 +3380,7 @@ validate_trait_float(
     trait_object *trait, has_traits_object *obj, PyObject *name,
     PyObject *value)
 {
-    PyObject *result = number_to_float(value);
+    PyObject *result = validate_float(value);
     /* A TypeError represents a type validation failure, and should be
        re-raised as a TraitError. Other exceptions should be propagated. */
     if (result == NULL && PyErr_ExceptionMatches(PyExc_TypeError)) {
@@ -3456,7 +3456,7 @@ validate_trait_float_range(
     PyObject *result;
     int in_range;
 
-    result = number_to_float(value);
+    result = validate_float(value);
     if (result == NULL) {
         if (PyErr_ExceptionMatches(PyExc_TypeError)) {
             /* Reraise any TypeError as a TraitError. */
@@ -3920,7 +3920,7 @@ validate_trait_complex(
                 break;
 
             case 4: /* Floating point range check: */
-                result = number_to_float(value);
+                result = validate_float(value);
                 if (result == NULL) {
                     if (PyErr_ExceptionMatches(PyExc_TypeError)) {
                         /* A TypeError should ultimately get re-raised
@@ -4138,7 +4138,7 @@ validate_trait_complex(
                 /* A TypeError indicates that we don't have a match.
                    Clear the error and continue with the next item
                    in the complex sequence. */
-                result = number_to_float(value);
+                result = validate_float(value);
                 if (result == NULL
                     && PyErr_ExceptionMatches(PyExc_TypeError)) {
                     PyErr_Clear();
@@ -5555,8 +5555,8 @@ _ctraits_ctrait(PyObject *self, PyObject *args)
 
 
 PyDoc_STRVAR(
-    _ctraits_number_to_float_doc,
-    "_number_to_float(number)\n"
+    _ctraits_validate_float_doc,
+    "_validate_float(number)\n"
     "\n"
     "Return *number* converted to a float. Raise TypeError if \n"
     "conversion is not possible.\n"
@@ -5570,8 +5570,8 @@ static PyMethodDef ctraits_methods[] = {
      PyDoc_STR("_adapt(adaptation_function)")},
     {"_ctrait", (PyCFunction)_ctraits_ctrait, METH_VARARGS,
      PyDoc_STR("_ctrait(CTrait_class)")},
-    {"_number_to_float", (PyCFunction)_ctraits_number_to_float, METH_O,
-     _ctraits_number_to_float_doc},
+    {"_validate_float", (PyCFunction)_ctraits_validate_float, METH_O,
+     _ctraits_validate_float_doc},
     {NULL, NULL},
 };
 
diff --git a/traits/trait_types.py b/traits/trait_types.py
index 62d15bbf5..e8eb3d14e 100644
--- a/traits/trait_types.py
+++ b/traits/trait_types.py
@@ -24,7 +24,7 @@
 import warnings
 
 from .constants import DefaultValue, TraitKind, ValidateTrait
-from .ctraits import _number_to_float
+from .ctraits import _validate_float
 from .trait_base import (
     strx,
     get_module_name,
@@ -328,7 +328,7 @@ def validate(self, object, name, value):
         Note: The 'fast validator' version performs this check in C.
         """
         try:
-            return _number_to_float(value)
+            return _validate_float(value)
         except TypeError:
             self.error(object, name, value)
 
@@ -1858,7 +1858,7 @@ def float_validate(self, object, name, value):
         # error-reporting purposes.
         original_value = value
         try:
-            value = _number_to_float(value)
+            value = _validate_float(value)
         except TypeError:
             self.error(object, name, original_value)
 

From 9338c9584c6b47a68e41b81c439b6b1db492692a Mon Sep 17 00:00:00 2001
From: Mark Dickinson <mdickinson@enthought.com>
Date: Mon, 8 Nov 2021 20:18:31 +0000
Subject: [PATCH 3/3] Fix copypasta in comment.

---
 traits/tests/test_float.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/traits/tests/test_float.py b/traits/tests/test_float.py
index c437ba932..0e442447e 100644
--- a/traits/tests/test_float.py
+++ b/traits/tests/test_float.py
@@ -27,7 +27,7 @@ def __index__(self):
 
 
 # Python versions < 3.8 don't support conversion of something with __index__
-# to complex.
+# to float.
 try:
     float(IntegerLike(3))
 except TypeError: