Skip to content

Commit

Permalink
Merge pull request #207 from nareshnootoo/feature
Browse files Browse the repository at this point in the history
Adding Number to Voluptuous for validating precision and scale
  • Loading branch information
tusharmakkar08 authored Sep 29, 2016
2 parents 987fd98 + a51b20c commit 8f5a41b
Show file tree
Hide file tree
Showing 2 changed files with 151 additions and 2 deletions.
92 changes: 91 additions & 1 deletion voluptuous/tests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
Schema, Required, Extra, Invalid, In, Remove, Literal,
Url, MultipleInvalid, LiteralInvalid, NotIn, Match, Email,
Replace, Range, Coerce, All, Any, Length, FqdnUrl, ALLOW_EXTRA, PREVENT_EXTRA,
validate, ExactSequence, Equal, Unordered
validate, ExactSequence, Equal, Unordered, Number
)
from voluptuous.humanize import humanize_error

Expand Down Expand Up @@ -556,3 +556,93 @@ def fn(arg):
return "hello"

assert_raises(Invalid, fn, 1)


def test_number_validation_with_string():
""" test with Number with string"""
schema = Schema({"number" : Number(precision=6, scale=2)})
try:
schema({"number": 'teststr'})
except MultipleInvalid as e:
assert_equal(str(e),
"Value must be a number enclosed with string for dictionary value @ data['number']")
else:
assert False, "Did not raise Invalid for String"


def test_number_validation_with_invalid_precision_invalid_scale():
""" test with Number with invalid precision and scale"""
schema = Schema({"number" : Number(precision=6, scale=2)})
try:
schema({"number": '123456.712'})
except MultipleInvalid as e:
assert_equal(str(e),
"Precision must be equal to 6, and Scale must be equal to 2 for dictionary value @ data['number']")
else:
assert False, "Did not raise Invalid for String"


def test_number_validation_with_valid_precision_scale_yield_decimal_true():
""" test with Number with valid precision and scale"""
schema = Schema({"number" : Number(precision=6, scale=2, yield_decimal=True)})
out_ = schema({"number": '1234.00'})
assert_equal(float(out_.get("number")), 1234.00)


def test_number_when_precision_scale_none_yield_decimal_true():
""" test with Number with no precision and scale"""
schema = Schema({"number" : Number(yield_decimal=True)})
out_ = schema({"number": '12345678901234'})
assert_equal(out_.get("number"), 12345678901234)


def test_number_when_precision_none_n_valid_scale_case1_yield_decimal_true():
""" test with Number with no precision and valid scale case 1"""
schema = Schema({"number" : Number(scale=2, yield_decimal=True)})
out_ = schema({"number": '123456789.34'})
assert_equal(float(out_.get("number")), 123456789.34)


def test_number_when_precision_none_n_valid_scale_case2_yield_decimal_true():
""" test with Number with no precision and valid scale case 2 with zero in decimal part"""
schema = Schema({"number" : Number(scale=2, yield_decimal=True)})
out_ = schema({"number": '123456789012.00'})
assert_equal(float(out_.get("number")), 123456789012.00)


def test_number_when_precision_none_n_invalid_scale_yield_decimal_true():
""" test with Number with no precision and invalid scale"""
schema = Schema({"number" : Number(scale=2, yield_decimal=True)})
try:
schema({"number": '12345678901.234'})
except MultipleInvalid as e:
assert_equal(str(e),
"Scale must be equal to 2 for dictionary value @ data['number']")
else:
assert False, "Did not raise Invalid for String"


def test_number_when_valid_precision_n_scale_none_yield_decimal_true():
""" test with Number with no precision and valid scale"""
schema = Schema({"number" : Number(precision=14, yield_decimal=True)})
out_ = schema({"number": '1234567.8901234'})
assert_equal(float(out_.get("number")), 1234567.8901234)


def test_number_when_invalid_precision_n_scale_none_yield_decimal_true():
""" test with Number with no precision and invalid scale"""
schema = Schema({"number" : Number(precision=14, yield_decimal=True)})
try:
schema({"number": '12345674.8901234'})
except MultipleInvalid as e:
assert_equal(str(e),
"Precision must be equal to 14 for dictionary value @ data['number']")
else:
assert False, "Did not raise Invalid for String"


def test_number_validation_with_valid_precision_scale_yield_decimal_false():
""" test with Number with valid precision, scale and no yield_decimal"""
schema = Schema({"number" : Number(precision=6, scale=2, yield_decimal=False)})
out_ = schema({"number": '1234.00'})
assert_equal(out_.get("number"), '1234.00')
61 changes: 60 additions & 1 deletion voluptuous/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import datetime
import sys
from functools import wraps

from decimal import Decimal, InvalidOperation

try:
from schema_builder import Schema, raises, message
Expand Down Expand Up @@ -794,3 +794,62 @@ def __call__(self, v):

def __repr__(self):
return 'Unordered([{}])'.format(", ".join(repr(v) for v in self.validators))


class Number(object):
"""
Verify the number of digits that are present in the number(Precision),
and the decimal places(Scale)
:raises Invalid: If the value does not match the provided Precision and Scale.
>>> schema = Schema(Number(precision=6, scale=2))
>>> schema('1234.01')
'1234.01'
>>> schema = Schema(Number(precision=6, scale=2, yield_decimal=True))
>>> schema('1234.01')
Decimal('1234.01')
"""

def __init__(self, precision=None, scale=None, msg=None, yield_decimal=False):
self.precision = precision
self.scale = scale
self.msg = msg
self.yield_decimal = yield_decimal

def __call__(self, v):
"""
:param v: is a number enclosed with string
:return: Decimal number
"""
precision, scale, decimal_num = self._get_precision_scale(v)

if self.precision is not None and self.scale is not None and\
precision != self.precision and scale != self.scale:
raise Invalid(self.msg or "Precision must be equal to %s, and Scale must be equal to %s" %(self.precision, self.scale))
else:
if self.precision is not None and precision != self.precision:
raise Invalid(self.msg or "Precision must be equal to %s"%self.precision)

if self.scale is not None and scale != self.scale :
raise Invalid(self.msg or "Scale must be equal to %s"%self.scale)

if self.yield_decimal:
return decimal_num
else:
return v

def __repr__(self):
return ('Number(precision=%s, scale=%s, msg=%s)' % (self.precision, self.scale, self.msg))

def _get_precision_scale(self, number):
"""
:param number:
:return: tuple(precision, scale, decimal_number)
"""
try:
decimal_num = Decimal(number)
except InvalidOperation:
raise Invalid(self.msg or 'Value must be a number enclosed with string')

return (len(decimal_num.as_tuple().digits), -(decimal_num.as_tuple().exponent), decimal_num)

0 comments on commit 8f5a41b

Please sign in to comment.