Skip to content

Commit b8d60f7

Browse files
committed
Merge remote-tracking branch 'origin/master' into python-3
2 parents 359c38a + 2e78816 commit b8d60f7

File tree

3 files changed

+199
-14
lines changed

3 files changed

+199
-14
lines changed

traits/ctraits.c

Lines changed: 73 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3318,13 +3318,52 @@ validate_trait_integer ( trait_object * trait, has_traits_object * obj,
33183318
return raise_trait_error( trait, obj, name, value );
33193319
}
33203320

3321+
3322+
/*-----------------------------------------------------------------------------
3323+
| Verifies that a Python value is convertible to float
3324+
|
3325+
| Will convert anything whose type has a __float__ method to a Python
3326+
| float. Returns a Python object of exact type "float". Raises TraitError
3327+
| with a suitable message if the given value isn't convertible to float.
3328+
|
3329+
| Any exception other than TypeError raised by the value's __float__ method
3330+
| will be propagated. A TypeError will be caught and turned into TraitError.
3331+
|
3332+
+----------------------------------------------------------------------------*/
3333+
3334+
static PyObject *
3335+
validate_trait_float(trait_object * trait, has_traits_object * obj,
3336+
PyObject * name, PyObject * value) {
3337+
/* Fast path for the most common case. */
3338+
if (PyFloat_CheckExact(value)) {
3339+
Py_INCREF(value);
3340+
return value;
3341+
}
3342+
else {
3343+
double value_as_double = PyFloat_AsDouble(value);
3344+
/* Translate a TypeError to a TraitError, but propagate
3345+
other exceptions. */
3346+
if (value_as_double == -1.0 && PyErr_Occurred()) {
3347+
if (PyErr_ExceptionMatches(PyExc_TypeError)) {
3348+
PyErr_Clear();
3349+
goto error;
3350+
}
3351+
return NULL;
3352+
}
3353+
return PyFloat_FromDouble(value_as_double);
3354+
}
3355+
3356+
error:
3357+
return raise_trait_error(trait, obj, name, value);
3358+
}
3359+
33213360
/*-----------------------------------------------------------------------------
33223361
| Verifies a Python value is a float within a specified range:
33233362
+----------------------------------------------------------------------------*/
33243363

33253364
static PyObject *
3326-
validate_trait_float ( trait_object * trait, has_traits_object * obj,
3327-
PyObject * name, PyObject * value ) {
3365+
validate_trait_float_range ( trait_object * trait, has_traits_object * obj,
3366+
PyObject * name, PyObject * value ) {
33283367

33293368
register PyObject * low;
33303369
register PyObject * high;
@@ -4015,7 +4054,7 @@ validate_trait_complex ( trait_object * trait, has_traits_object * obj,
40154054
goto done;
40164055
break;
40174056

4018-
case 20: /* Integer check: */
4057+
case 20: /* Integer check: */
40194058

40204059
/* Fast paths for the most common cases. */
40214060
#if PY_MAJOR_VERSION < 3
@@ -4063,6 +4102,29 @@ validate_trait_complex ( trait_object * trait, has_traits_object * obj,
40634102
Py_DECREF(int_value);
40644103
return result;
40654104

4105+
case 21: /* Float check */
4106+
/* Fast path for most common case. */
4107+
if (PyFloat_CheckExact(value)) {
4108+
Py_INCREF(value);
4109+
return value;
4110+
}
4111+
else {
4112+
double value_as_double = PyFloat_AsDouble(value);
4113+
if (value_as_double == -1.0 && PyErr_Occurred()) {
4114+
/* TypeError indicates that we don't have a match;
4115+
clear the error and continue with the next item
4116+
in the complex sequence. */
4117+
if (PyErr_ExceptionMatches(PyExc_TypeError)) {
4118+
PyErr_Clear();
4119+
break;
4120+
}
4121+
/* Any other exception is unexpected and likely
4122+
a code bug; propagate it. */
4123+
return NULL;
4124+
}
4125+
return PyFloat_FromDouble(value_as_double);
4126+
}
4127+
40664128
default: /* Should never happen...indicates an internal error: */
40674129
goto error;
40684130
}
@@ -4086,7 +4148,7 @@ static trait_validate validate_handlers[] = {
40864148
#else
40874149
validate_trait_self_type, NULL,
40884150
#endif // #if PY_MAJOR_VERSION < 3
4089-
validate_trait_float, validate_trait_enum,
4151+
validate_trait_float_range, validate_trait_enum,
40904152
validate_trait_map, validate_trait_complex,
40914153
NULL, validate_trait_tuple,
40924154
validate_trait_prefix_map, validate_trait_coerce_type,
@@ -4097,6 +4159,7 @@ static trait_validate validate_handlers[] = {
40974159
setattr_validate2, setattr_validate3,
40984160
/* ...End of __getstate__ method entries */
40994161
validate_trait_adapt, validate_trait_integer,
4162+
validate_trait_float,
41004163
};
41014164

41024165
static PyObject *
@@ -4246,6 +4309,12 @@ _trait_set_validate ( trait_object * trait, PyObject * args ) {
42464309
if ( n == 1 )
42474310
goto done;
42484311
break;
4312+
4313+
case 21: /* Float check: */
4314+
if ( n == 1 )
4315+
goto done;
4316+
break;
4317+
42494318
}
42504319
}
42514320
}

traits/tests/test_float.py

Lines changed: 103 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,25 +17,53 @@
1717
"""
1818
import sys
1919

20+
try:
21+
import numpy
22+
except ImportError:
23+
numpy_available = False
24+
else:
25+
numpy_available = True
2026
import six
2127

28+
from traits.api import BaseFloat, Either, Float, HasTraits, TraitError, Unicode
29+
from traits.testing.unittest_tools import unittest
30+
2231
if six.PY2:
2332
LONG_TYPE = long
2433
else:
2534
LONG_TYPE = int
2635

27-
from traits.testing.unittest_tools import unittest
2836

29-
from traits.api import BaseFloat, Float, HasTraits
37+
class MyFloat(object):
38+
def __init__(self, value):
39+
self._value = value
40+
41+
def __float__(self):
42+
return self._value
43+
44+
45+
class BadFloat(object):
46+
def __float__(self):
47+
raise ZeroDivisionError
3048

3149

3250
class FloatModel(HasTraits):
3351
value = Float
3452

53+
# Assignment to the `Either` trait exercises a different C code path (see
54+
# validate_trait_complex in ctraits.c).
55+
value_or_none = Either(None, Float)
56+
57+
float_or_text = Either(Float, Unicode)
58+
3559

3660
class BaseFloatModel(HasTraits):
3761
value = BaseFloat
3862

63+
value_or_none = Either(None, BaseFloat)
64+
65+
float_or_text = Either(Float, Unicode)
66+
3967

4068
class CommonFloatTests(object):
4169
""" Common tests for Float and BaseFloat """
@@ -45,36 +73,109 @@ def test_default(self):
4573

4674
def test_accepts_float(self):
4775
a = self.test_class()
76+
4877
a.value = 5.6
4978
self.assertIs(type(a.value), float)
5079
self.assertEqual(a.value, 5.6)
5180

81+
a.value_or_none = 5.6
82+
self.assertIs(type(a.value_or_none), float)
83+
self.assertEqual(a.value_or_none, 5.6)
84+
5285
def test_accepts_int(self):
5386
a = self.test_class()
87+
5488
a.value = 2
5589
self.assertIs(type(a.value), float)
5690
self.assertEqual(a.value, 2.0)
5791

92+
a.value_or_none = 2
93+
self.assertIs(type(a.value_or_none), float)
94+
self.assertEqual(a.value_or_none, 2.0)
95+
96+
def test_accepts_float_like(self):
97+
a = self.test_class()
98+
99+
a.value = MyFloat(1729.0)
100+
self.assertIs(type(a.value), float)
101+
self.assertEqual(a.value, 1729.0)
102+
103+
a.value = MyFloat(594.0)
104+
self.assertIs(type(a.value), float)
105+
self.assertEqual(a.value, 594.0)
106+
107+
def test_rejects_string(self):
108+
a = self.test_class()
109+
with self.assertRaises(TraitError):
110+
a.value = "2.3"
111+
with self.assertRaises(TraitError):
112+
a.value_or_none = "2.3"
113+
114+
def test_bad_float_exceptions_propagated(self):
115+
a = self.test_class()
116+
with self.assertRaises(ZeroDivisionError):
117+
a.value = BadFloat()
118+
119+
def test_compound_trait_float_conversion_fail(self):
120+
# Check that a failure to convert to float doesn't terminate
121+
# an assignment to a compound trait.
122+
a = self.test_class()
123+
a.float_or_text = u"not a float"
124+
self.assertEqual(a.float_or_text, u"not a float")
125+
58126
@unittest.skipUnless(sys.version_info < (3,), "Not applicable to Python 3")
59127
def test_accepts_small_long(self):
60128
a = self.test_class()
61129
a.value = LONG_TYPE(2)
62130
self.assertIs(type(a.value), float)
63131
self.assertEqual(a.value, 2.0)
64132

133+
a.value_or_none = LONG_TYPE(2)
134+
self.assertIs(type(a.value_or_none), float)
135+
self.assertEqual(a.value_or_none, 2.0)
136+
65137
@unittest.skipUnless(sys.version_info < (3,), "Not applicable to Python 3")
66138
def test_accepts_large_long(self):
67139
a = self.test_class()
140+
68141
# Value large enough to be a long on Python 2.
69142
a.value = 2**64
70143
self.assertIs(type(a.value), float)
71144
self.assertEqual(a.value, 2**64)
72145

146+
a.value_or_none = 2**64
147+
self.assertIs(type(a.value_or_none), float)
148+
self.assertEqual(a.value_or_none, 2**64)
149+
150+
@unittest.skipUnless(numpy_available, "Test requires NumPy")
151+
def test_accepts_numpy_floats(self):
152+
test_values = [
153+
numpy.float64(2.3),
154+
numpy.float32(3.7),
155+
numpy.float16(1.28),
156+
]
157+
a = self.test_class()
158+
for test_value in test_values:
159+
a.value = test_value
160+
self.assertIs(type(a.value), float)
161+
self.assertEqual(a.value, test_value)
162+
163+
a.value_or_none = test_value
164+
self.assertIs(type(a.value_or_none), float)
165+
self.assertEqual(a.value_or_none, test_value)
166+
73167

74168
class TestFloat(unittest.TestCase, CommonFloatTests):
75169
def setUp(self):
76170
self.test_class = FloatModel
77171

172+
def test_exceptions_propagate_in_compound_trait(self):
173+
# This test doesn't currently pass for BaseFloat, which is why it's not
174+
# in the common tests. That's probably a bug.
175+
a = self.test_class()
176+
with self.assertRaises(ZeroDivisionError):
177+
a.value_or_none = BadFloat()
178+
78179

79180
class TestBaseFloat(unittest.TestCase, CommonFloatTests):
80181
def setUp(self):

traits/trait_types.py

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,23 @@ def default_text_editor ( trait, type = None ):
125125
enter_set = enter_set,
126126
evaluate = type )
127127

128+
129+
# Generic validators
130+
131+
def _validate_float(value):
132+
"""
133+
Convert an arbitrary Python object to a float, or raise TypeError.
134+
"""
135+
if type(value) == float: # fast path for common case
136+
return value
137+
try:
138+
nb_float = type(value).__float__
139+
except AttributeError:
140+
raise TypeError(
141+
"Object of type {!r} not convertible to float".format(type(value)))
142+
return nb_float(value)
143+
144+
128145
#-------------------------------------------------------------------------------
129146
# 'Any' trait:
130147
#-------------------------------------------------------------------------------
@@ -243,6 +260,7 @@ class Long ( BaseLong ):
243260
#: The C-level fast validator to use:
244261
fast_validate = long_fast_validate
245262

263+
246264
#-------------------------------------------------------------------------------
247265
# 'BaseFloat' and 'Float' traits:
248266
#-------------------------------------------------------------------------------
@@ -264,13 +282,10 @@ def validate ( self, object, name, value ):
264282
265283
Note: The 'fast validator' version performs this check in C.
266284
"""
267-
if isinstance( value, float ):
268-
return value
269-
270-
if isinstance( value, six.integer_types):
271-
return float( value )
272-
273-
self.error( object, name, value )
285+
try:
286+
return _validate_float(value)
287+
except TypeError:
288+
self.error(object, name, value)
274289

275290
def create_editor ( self ):
276291
""" Returns the default traits UI editor for this type of trait.
@@ -284,7 +299,7 @@ class Float ( BaseFloat ):
284299
"""
285300

286301
#: The C-level fast validator to use:
287-
fast_validate = float_fast_validate
302+
fast_validate = ( 21, )
288303

289304
#-------------------------------------------------------------------------------
290305
# 'BaseComplex' and 'Complex' traits:

0 commit comments

Comments
 (0)