Repository: di/vladiate

Issue:
RangeValidator has no empty_ok parameter
RangeValidator has no option to allow empty records in a field. RangeValidator fails if the field contains an empty record "". Propose to add an empty_ok parameter to the RangeValidator class similar to other validators as shown below:

```python
class RangeValidator(Validator):
    def __init__(self, low, high, empty_ok=False):
        self.fail_count = 0
        self.low = low
        self.high = high
        self.empty_ok = empty_ok
        self.outside = set()

    def validate(self, field, row={}):
        if field == '' and self.empty_ok:
            pass
        else:
            try:
                value = float(field)
                if not self.low <= value <= self.high:
                    raise ValueError
            except ValueError:
                self.outside.add(field)
                raise ValidationException(
                    "'{}' is not in range {} to {}".format(
                        field, self.low, self.high
                    )
                )

    @property
    def bad(self):
        return self.outside
```

Discussion:

Patch:
diff --git a/README.rst b/README.rst
index f36c851..77af67e 100644
--- a/README.rst
+++ b/README.rst
@@ -236,6 +236,8 @@ Vladiate comes with a few common validators built-in:
 
   :``unique_with=[]``:
       List of field names to make the primary field unique with.
+  :``empty_ok=False``:
+      Specify whether a field which is an empty string should be ignored.
 
 *class* ``RegexValidator``
 
@@ -243,6 +245,8 @@ Vladiate comes with a few common validators built-in:
 
   :``pattern=r'di^'``:
       The regex pattern. Fails for all fields by default.
+  :``empty_ok=False``:
+      Specify whether a field which is an empty string should be ignored.
 
 *class* ``RangeValidator``
 
@@ -253,6 +257,8 @@ Vladiate comes with a few common validators built-in:
       The low value of the range.
   :``high``:
       The high value of the range.
+  :``empty_ok=False``:
+      Specify whether a field which is an empty string should be ignored.
 
 *class* ``EmptyValidator``
 
diff --git a/setup.py b/setup.py
index 62b35f8..b5c8c72 100644
--- a/setup.py
+++ b/setup.py
@@ -4,7 +4,7 @@ import sys
 from setuptools import setup, find_packages
 from setuptools.command.test import test as TestCommand
 
-__version__ = '0.0.17'
+__version__ = '0.0.18'
 
 
 class PyTest(TestCommand):
diff --git a/vladiate/validators.py b/vladiate/validators.py
index a425c2c..72dccdb 100644
--- a/vladiate/validators.py
+++ b/vladiate/validators.py
@@ -6,8 +6,9 @@ from vladiate.exceptions import ValidationException, BadValidatorException
 class Validator(object):
     ''' Generic Validator class '''
 
-    def __init__(self):
+    def __init__(self, empty_ok=False):
         self.fail_count = 0
+        self.empty_ok = empty_ok
 
     @property
     def bad(self):
@@ -22,8 +23,8 @@ class Validator(object):
 class CastValidator(Validator):
     ''' Validates that a field can be cast to a float '''
 
-    def __init__(self):
-        super(CastValidator, self).__init__()
+    def __init__(self, **kwargs):
+        super(CastValidator, self).__init__(**kwargs)
         self.invalid_set = set([])
 
     def validate(self, field, row={}):
@@ -42,29 +43,27 @@ class CastValidator(Validator):
 class FloatValidator(CastValidator):
     ''' Validates that a field can be cast to a float '''
 
-    def __init__(self, empty_ok=False):
-        super(FloatValidator, self).__init__()
-        self.empty_ok = empty_ok
+    def __init__(self, **kwargs):
+        super(FloatValidator, self).__init__(**kwargs)
         self.cast = float
 
 
 class IntValidator(CastValidator):
     ''' Validates that a field can be cast to an int '''
 
-    def __init__(self, empty_ok=False):
-        super(IntValidator, self).__init__()
-        self.empty_ok = empty_ok
+    def __init__(self, **kwargs):
+        super(IntValidator, self).__init__(**kwargs)
         self.cast = int
 
 
 class SetValidator(Validator):
     ''' Validates that a field is in the given set '''
 
-    def __init__(self, valid_set=[], empty_ok=False):
-        super(SetValidator, self).__init__()
+    def __init__(self, valid_set=[], **kwargs):
+        super(SetValidator, self).__init__(**kwargs)
         self.valid_set = set(valid_set)
         self.invalid_set = set([])
-        if empty_ok:
+        if self.empty_ok:
             self.valid_set.add('')
 
     def validate(self, field, row={}):
@@ -81,8 +80,8 @@ class SetValidator(Validator):
 class UniqueValidator(Validator):
     ''' Validates that a field is unique within the file '''
 
-    def __init__(self, unique_with=[]):
-        super(UniqueValidator, self).__init__()
+    def __init__(self, unique_with=[], **kwargs):
+        super(UniqueValidator, self).__init__(**kwargs)
         self.unique_values = set([])
         self.duplicates = set([])
         self.unique_with = unique_with
@@ -119,10 +118,9 @@ class UniqueValidator(Validator):
 class RegexValidator(Validator):
     ''' Validates that a field matches a given regex '''
 
-    def __init__(self, pattern=r'di^', empty_ok=False):
-        super(RegexValidator, self).__init__()
+    def __init__(self, pattern=r'di^', **kwargs):
+        super(RegexValidator, self).__init__(**kwargs)
         self.regex = re.compile(pattern)
-        self.empty_ok = empty_ok
         self.failures = set([])
 
     def validate(self, field, row={}):
@@ -137,13 +135,16 @@ class RegexValidator(Validator):
 
 
 class RangeValidator(Validator):
-    def __init__(self, low, high):
+    def __init__(self, low, high, **kwargs):
+        super(RangeValidator, self).__init__(**kwargs)
         self.fail_count = 0
         self.low = low
         self.high = high
         self.outside = set()
 
     def validate(self, field, row={}):
+        if field == '' and self.empty_ok:
+            return
         try:
             value = float(field)
             if not self.low <= value <= self.high:
@@ -164,8 +165,8 @@ class RangeValidator(Validator):
 class EmptyValidator(Validator):
     ''' Validates that a field is always empty '''
 
-    def __init__(self):
-        super(EmptyValidator, self).__init__()
+    def __init__(self, **kwargs):
+        super(EmptyValidator, self).__init__(**kwargs)
         self.nonempty = set([])
 
     def validate(self, field, row={}):
@@ -183,7 +184,8 @@ class EmptyValidator(Validator):
 class NotEmptyValidator(Validator):
     ''' Validates that a field is not empty '''
 
-    def __init__(self):
+    def __init__(self, **kwargs):
+        super(NotEmptyValidator, self).__init__(**kwargs)
         self.fail_count = 0
         self.failed = False
 


Test patch:
diff --git a/vladiate/test/test_validators.py b/vladiate/test/test_validators.py
index 7b80686..369e9a4 100644
--- a/vladiate/test/test_validators.py
+++ b/vladiate/test/test_validators.py
@@ -81,12 +81,6 @@ def test_set_validator_works(field_set, field):
     assert field in validator.valid_set
 
 
-def test_set_validator_empty_ok():
-    validator = SetValidator(['foo'], empty_ok=True)
-    validator.validate('')
-    assert '' in validator.valid_set
-
-
 @pytest.mark.parametrize('field_set, field', [
     ([], 'bar'),
     (['foo'], 'bar'),
@@ -139,10 +133,6 @@ def test_regex_validator_works(pattern, field):
     RegexValidator(pattern).validate(field)
 
 
-def test_regex_validator_allows_empty():
-    RegexValidator(r'foo.*', empty_ok=True).validate('')
-
-
 @pytest.mark.parametrize('pattern, field', [
     (r'foo.*', 'afoo'),
     (r'^$', 'foo'),
@@ -213,3 +203,16 @@ def test_base_class_raises():
 
     with pytest.raises(NotImplementedError):
         validator.validate(stub(), stub())
+
+
+@pytest.mark.parametrize('validator_class,args', [
+    (SetValidator, [['foo']]),
+    (RegexValidator, [r'foo.*']),
+    (IntValidator, []),
+    (FloatValidator, []),
+    (RangeValidator, [0, 42]),
+    (UniqueValidator, []),
+])
+def test_all_validators_support_empty_ok(validator_class, args):
+    validator = validator_class(*args, empty_ok=True)
+    validator.validate('')