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: