Skip to content

Commit f584362

Browse files
committed
Re-added support for Float32/Float64.
Summary: I thought it might be worth it to have Float32 (Proto has it, and plenty of languages we'll be supporting still differentiate between float/double). A more restrictive min_value and max_value can be specified, which we/Carousel could use for sanity checking longitude and latitude values for GPS coordinates. Test Plan: Added tests Reviewed By: guido
1 parent 59ec5ce commit f584362

File tree

6 files changed

+233
-34
lines changed

6 files changed

+233
-34
lines changed

babelapi/babel/tower.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ def _create_struct_field(self, env, babel_field):
172172
babelapi.data_type.StructField: A field of a struct.
173173
"""
174174
if babel_field.type_ref.name not in env:
175-
raise InvalidSpec('Symbol %r is undefined.' % babel_field.data_type_name)
175+
raise InvalidSpec('Symbol %r is undefined.' % babel_field.type_ref.name)
176176
else:
177177
data_type = self._resolve_type(env, babel_field.type_ref)
178178
if data_type.nullable and babel_field.has_default:

babelapi/data_type.py

Lines changed: 90 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
"""
22
Defines data types for Babel.
33
4-
The data types defined here should include all data types we expect to support
5-
across all serialization formats. For example, if we were just supporting JSON,
6-
we would only have a Number type since all numbers in Javascript are doubles.
7-
But, we may want to eventually support Protobuf, which has primitives closer
8-
to those in C. This is why Int32 and UInt32 are differentiated, though it has
9-
the added benefit that the user is able to reason between signed vs. unsigned.
4+
The goal of this module is to define all data types that are common to the
5+
languages and serialization formats we want to support.
6+
7+
TODO: This file is in need of refactoring, and should take after the
8+
babelapi.generator.target.python.babel_validators module.
109
"""
1110

1211
from abc import ABCMeta, abstractmethod
1312
from collections import OrderedDict
1413
import copy
1514
import datetime
15+
import math
1616
import numbers
1717
import re
1818
import six
@@ -78,7 +78,8 @@ def get_example(self, label):
7878

7979
class _BoundedInteger(PrimitiveType):
8080
"""
81-
When extending, specify 'minimum' and 'maximum' as class variables.
81+
When extending, specify 'minimum' and 'maximum' as class variables. This
82+
is the range of values supported by the data type.
8283
"""
8384
def __init__(self, min_value=None, max_value=None):
8485
"""
@@ -107,9 +108,15 @@ def __init__(self, min_value=None, max_value=None):
107108
def check(self, val):
108109
if not isinstance(val, numbers.Integral):
109110
raise ValueError('%r is not a valid integer type' % val)
110-
elif not (self.minimum <= val <= self.maximum):
111-
raise ValueError('%s is not within range [%r, %r]'
111+
if not (self.minimum <= val <= self.maximum):
112+
raise ValueError('%d is not within range [%r, %r]'
112113
% (val, self.minimum, self.maximum))
114+
if self.min_value is not None and val < self.min_value:
115+
raise ValueError('%d is less than %d' %
116+
(val, self.min_value))
117+
if self.max_value is not None and val > self.max_value:
118+
raise ValueError('%d is greater than %d' %
119+
(val, self.max_value))
113120

114121
def __repr__(self):
115122
return '%s()' % self.name
@@ -130,21 +137,82 @@ class UInt64(_BoundedInteger):
130137
minimum = 0
131138
maximum = 2**64 - 1
132139

133-
class BigInt(PrimitiveType):
134-
def check(self, val):
135-
if not isinstance(val, numbers.Integral):
136-
raise ValueError('%r is not a valid integer')
140+
class _BoundedFloat(PrimitiveType):
141+
"""
142+
When extending, optionally specify 'minimum' and 'maximum' as class
143+
variables. This is the range of values supported by the data type. For
144+
a float64, there is no need to specify a minimum and maximum since Python's
145+
native float implementation is a float64/double. Therefore, any Python
146+
float will pass the data type range check automatically.
147+
"""
148+
minimum = None
149+
maximum = None
137150

138-
class Float32(PrimitiveType):
139-
# TODO: Decide how to enforce a 32-bit float
140-
def check(self, val):
141-
if not isinstance(val, numbers.Real):
142-
raise ValueError('%r is not a valid float' % val)
151+
def __init__(self, min_value=None, max_value=None):
152+
"""
153+
A more restrictive minimum or maximum value can be specified than the
154+
range inherent to the defined type.
155+
"""
156+
if min_value is not None:
157+
assert isinstance(min_value, numbers.Real), \
158+
'min_value must be a real number'
159+
if not isinstance(min_value, float):
160+
try:
161+
min_value = float(min_value)
162+
except OverflowError:
163+
raise AssertionError('min_value is too small for a float')
164+
if self.minimum is not None and min_value < self.minimum:
165+
raise AssertionError('min_value cannot be less than the '
166+
'minimum value for this type (%f < %f)' %
167+
(min_value, self.minimum))
168+
if max_value is not None:
169+
assert isinstance(max_value, numbers.Real), \
170+
'max_value must be a real number'
171+
if not isinstance(max_value, float):
172+
try:
173+
max_value = float(max_value)
174+
except OverflowError:
175+
raise AssertionError('max_value is too large for a float')
176+
if self.maximum is not None and max_value > self.maximum:
177+
raise AssertionError('max_value cannot be greater than the '
178+
'maximum value for this type (%f < %f)' %
179+
(max_value, self.maximum))
180+
self.min_value = min_value
181+
self.max_value = max_value
143182

144-
class Float64(PrimitiveType):
145183
def check(self, val):
146184
if not isinstance(val, numbers.Real):
147-
raise ValueError('%r is not a valid double' % val)
185+
raise ValueError('%r is not a valid real number' % val)
186+
if not isinstance(val, float):
187+
try:
188+
val = float(val)
189+
except OverflowError:
190+
raise ValueError('%r is too large for float' % val)
191+
if math.isnan(val) or math.isinf(val):
192+
raise ValueError('%f values are not supported' % val)
193+
if self.minimum is not None and val < self.minimum:
194+
raise ValueError('%f is less than %f' %
195+
(val, self.minimum))
196+
if self.maximum is not None and val > self.maximum:
197+
raise ValueError('%f is greater than %f' %
198+
(val, self.maximum))
199+
if self.min_value is not None and val < self.min_value:
200+
raise ValueError('%f is less than %f' %
201+
(val, self.min_value))
202+
if self.max_value is not None and val > self.max_value:
203+
raise ValueError('%f is greater than %f' %
204+
(val, self.min_value))
205+
206+
def __repr__(self):
207+
return '%s()' % self.name
208+
209+
class Float32(_BoundedFloat):
210+
# Maximum and minimums from the IEEE 754-1985 standard
211+
minimum = -3.40282 * 10**38
212+
maximum = 3.40282 * 10**38
213+
214+
class Float64(_BoundedFloat):
215+
pass
148216

149217
class Boolean(PrimitiveType):
150218
def check(self, val):
@@ -636,6 +704,8 @@ def is_composite_type(data_type):
636704
return isinstance(data_type, CompositeType)
637705
def is_integer_type(data_type):
638706
return isinstance(data_type, (UInt32, UInt64, Int32, Int64))
707+
def is_float_type(data_type):
708+
return isinstance(data_type, (Float32, Float64))
639709
def is_list_type(data_type):
640710
return isinstance(data_type, List)
641711
def is_null_type(data_type):

babelapi/generator/target/python/babel_validators.py

Lines changed: 83 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
from abc import ABCMeta, abstractmethod
1616
import datetime
17+
import math
1718
import numbers
1819
import re
1920
import six
@@ -70,7 +71,7 @@ def validate(self, val):
7071
class Integer(Primitive):
7172
"""
7273
Do not use this class directly. Extend it and specify a 'minimum' and
73-
'maximum' value as class variables for the more restrictive integer range.
74+
'maximum' value as class variables for a more restrictive integer range.
7475
"""
7576
minimum = None
7677
maximum = None
@@ -83,18 +84,16 @@ def __init__(self, min_value=None, max_value=None):
8384
if min_value is not None:
8485
assert isinstance(min_value, numbers.Integral), \
8586
'min_value must be an integral number'
86-
if min_value < self.minimum:
87-
raise ValueError('min_value cannot be less than the minimum '
88-
'value for this type (%d < %d)'
89-
% (min_value, self.minimum))
87+
assert min_value >= self.minimum, \
88+
'min_value cannot be less than the minimum value for this ' \
89+
'type (%d < %d)' % (min_value, self.minimum)
9090
self.minimum = min_value
9191
if max_value is not None:
9292
assert isinstance(max_value, numbers.Integral), \
9393
'max_value must be an integral number'
94-
if max_value > self.maximum:
95-
raise ValueError('max_value cannot be greater than the maximum '
96-
'value for this type (%d < %d)'
97-
% (max_value, self.maximum))
94+
assert max_value <= self.maximum, \
95+
'max_value cannot be greater than the maximum value for ' \
96+
'this type (%d < %d)' % (max_value, self.maximum)
9897
self.maximum = max_value
9998

10099
def validate(self, val):
@@ -125,6 +124,79 @@ class UInt64(Integer):
125124
minimum = 0
126125
maximum = 2**64 - 1
127126

127+
class Real(Primitive):
128+
"""
129+
Do not use this class directly. Extend it and optionally set a 'minimum'
130+
and 'maximum' value to enforce a range that's a subset of the Python float
131+
implementation. Python floats are doubles.
132+
"""
133+
minimum = None
134+
maximum = None
135+
136+
def __init__(self, min_value=None, max_value=None):
137+
"""
138+
A more restrictive minimum or maximum value can be specified than the
139+
range inherent to the defined type.
140+
"""
141+
if min_value is not None:
142+
assert isinstance(min_value, numbers.Real), \
143+
'min_value must be a real number'
144+
if not isinstance(min_value, float):
145+
try:
146+
min_value = float(min_value)
147+
except OverflowError:
148+
raise AssertionError('min_value is too small for a float')
149+
if self.minimum is not None and min_value < self.minimum:
150+
raise AssertionError('min_value cannot be less than the '
151+
'minimum value for this type (%f < %f)' %
152+
(min_value, self.minimum))
153+
self.minimum = min_value
154+
if max_value is not None:
155+
assert isinstance(max_value, numbers.Real), \
156+
'max_value must be a real number'
157+
if not isinstance(max_value, float):
158+
try:
159+
max_value = float(max_value)
160+
except OverflowError:
161+
raise AssertionError('max_value is too large for a float')
162+
if self.maximum is not None and max_value > self.maximum:
163+
raise AssertionError('max_value cannot be greater than the '
164+
'maximum value for this type (%f < %f)' %
165+
(max_value, self.maximum))
166+
self.maximum = max_value
167+
168+
def validate(self, val):
169+
if not isinstance(val, numbers.Real):
170+
raise ValidationError('expected real number, got %s' %
171+
generic_type_name(val))
172+
if not isinstance(val, float):
173+
# This checks for the case where a number is passed in with a
174+
# magnitude larger than supported by float64.
175+
try:
176+
val = float(val)
177+
except OverflowError:
178+
raise ValidationError('too large for float')
179+
if math.isnan(val) or math.isinf(val):
180+
raise ValidationError('%f values are not supported' % val)
181+
if self.minimum is not None and val < self.minimum:
182+
raise ValidationError('%f is not greater than %f' %
183+
(val, self.minimum))
184+
if self.maximum is not None and val > self.maximum:
185+
raise ValidationError('%f is not less than %f' %
186+
(val, self.maximum))
187+
return val
188+
189+
def __repr__(self):
190+
return '%s()' % self.__class__.__name__
191+
192+
class Float32(Real):
193+
# Maximum and minimums from the IEEE 754-1985 standard
194+
minimum = -3.40282 * 10**38
195+
maximum = 3.40282 * 10**38
196+
197+
class Float64(Real):
198+
pass
199+
128200
class String(Primitive):
129201
"""Represents a unicode string."""
130202

@@ -152,7 +224,8 @@ def __init__(self, min_length=None, max_length=None, pattern=None):
152224
try:
153225
self.pattern_re = re.compile(pattern)
154226
except re.error as e:
155-
raise ValueError('Regex {!r} failed: {}'.format(pattern, e.args[0]))
227+
raise AssertionError('Regex {!r} failed: {}'.format(
228+
pattern, e.args[0]))
156229

157230
def validate(self, val):
158231
"""

babelapi/generator/target/python/python.babelg.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
is_binary_type,
1111
is_boolean_type,
1212
is_composite_type,
13+
is_float_type,
1314
is_integer_type,
1415
is_list_type,
1516
is_null_type,
@@ -116,6 +117,8 @@ def _python_type_mapping(self, data_type):
116117
return 'str'
117118
elif is_boolean_type(data_type):
118119
return 'bool'
120+
elif is_float_type(data_type):
121+
return 'float'
119122
elif is_integer_type(data_type):
120123
return 'long'
121124
elif is_null_type(data_type):
@@ -218,6 +221,14 @@ def _determine_validator_type(self, data_type):
218221
'max_items': data_type.max_items,
219222
})
220223
)
224+
elif is_float_type(data_type):
225+
v = 'bv.{}({})'.format(
226+
data_type.name,
227+
self._func_args_from_dict({
228+
'min_value': data_type.min_value,
229+
'max_value': data_type.max_value,
230+
})
231+
)
221232
elif is_integer_type(data_type):
222233
v = 'bv.{}({})'.format(
223234
data_type.name,

test/test_babel_internal.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from babelapi.data_type import (
44
Boolean,
55
Float32,
6+
Float64,
67
Int32,
78
Int64,
89
List,
@@ -125,6 +126,14 @@ def test_int(self):
125126
i.check(-1)
126127
self.assertIn('not within range', cm.exception.args[0])
127128

129+
i = Int64(min_value=0, max_value=10)
130+
with self.assertRaises(ValueError) as cm:
131+
i.check(20)
132+
self.assertIn('20 is greater than 10', cm.exception.args[0])
133+
with self.assertRaises(ValueError) as cm:
134+
i.check(-5)
135+
self.assertIn('-5 is less than 0', cm.exception.args[0])
136+
128137
def test_boolean(self):
129138

130139
b = Boolean()
@@ -147,9 +156,20 @@ def test_float(self):
147156
# check non-float
148157
with self.assertRaises(ValueError) as cm:
149158
f.check('1.1')
150-
self.assertIn('not a valid float', cm.exception.args[0])
159+
self.assertIn('not a valid real', cm.exception.args[0])
160+
161+
f = Float64(min_value=0, max_value=100)
162+
with self.assertRaises(ValueError) as cm:
163+
f.check(101)
164+
self.assertIn('is greater than', cm.exception.args[0])
165+
166+
with self.assertRaises(AssertionError) as cm:
167+
Float64(min_value=0, max_value=10**330)
168+
self.assertIn('too large for a float', cm.exception.args[0])
151169

152-
# TODO: Need to check float range once it's been implemented.
170+
with self.assertRaises(AssertionError) as cm:
171+
Float32(min_value=0, max_value=10**50)
172+
self.assertIn('greater than the maximum value', cm.exception.args[0])
153173

154174
def test_timestamp(self):
155175
t = Timestamp('%a, %d %b %Y %H:%M:%S')

0 commit comments

Comments
 (0)