Repository: Pylons/colander

Issue:
passing a float to colander.Integer does not throw exception
When passing a float value (e.g. 1.21) to a colander.Integer node, no exception is thrown.
I am not sure of the expected behaviour though...

Version: 1.3.3

Discussion:

Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index c40160b..babbd56 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -11,6 +11,11 @@ Unreleased
   reverts an accidental revert. See
   https://github.com/Pylons/colander/issues/204#issuecomment-459556100
 
+- Integer SchemaType now supports an optional ``strict`` mode that will
+  validate that the number is an integer, rather than silently accepting floats
+  and truncating. See https://github.com/Pylons/colander/pull/322 and
+  https://github.com/Pylons/colander/issues/292
+
 1.6.0 (2019-01-31)
 ==================
 
diff --git a/colander/__init__.py b/colander/__init__.py
index 6f0dff5..23f9aa2 100644
--- a/colander/__init__.py
+++ b/colander/__init__.py
@@ -1426,12 +1426,28 @@ class Integer(Number):
     method of this class, the :attr:`colander.null` value will be
     returned.
 
+    The Integer constructor takes an optional argument ``strict``, which if
+    enabled will verify that the number passed to serialize/deserialize is an
+    integer, and not a float that would get truncated.
+
     The subnodes of the :class:`colander.SchemaNode` that wraps
     this type are ignored.
     """
 
     num = int
 
+    def __init__(self, strict=False):
+        if strict:
+
+            def _strict_int(val):
+                if not float(val).is_integer():
+                    raise ValueError("Value is not an Integer")
+                return int(val)
+
+            self.num = _strict_int
+
+        super(Integer, self).__init__()
+
 
 Int = Integer
 


Test patch:
diff --git a/colander/tests/test_colander.py b/colander/tests/test_colander.py
index 917a719..286374b 100644
--- a/colander/tests/test_colander.py
+++ b/colander/tests/test_colander.py
@@ -1730,10 +1730,10 @@ class TestString(unittest.TestCase):
 
 
 class TestInteger(unittest.TestCase):
-    def _makeOne(self):
+    def _makeOne(self, strict=False):
         from colander import Integer
 
-        return Integer()
+        return Integer(strict=strict)
 
     def test_alias(self):
         from colander import Int
@@ -1802,6 +1802,34 @@ class TestInteger(unittest.TestCase):
         result = typ.serialize(node, val)
         self.assertEqual(result, '0')
 
+    def test_serialize_strict_float(self):
+        val = 1.2
+        node = DummySchemaNode(None)
+        typ = self._makeOne(strict=True)
+        e = invalid_exc(typ.serialize, node, val)
+        self.assertTrue(e.msg)
+
+    def test_serialize_strict_int(self):
+        val = 1
+        node = DummySchemaNode(None)
+        typ = self._makeOne(strict=True)
+        result = typ.serialize(node, val)
+        self.assertEqual(result, '1')
+
+    def test_deserialize_strict(self):
+        val = '58'
+        node = DummySchemaNode(None)
+        typ = self._makeOne(strict=True)
+        result = typ.deserialize(node, val)
+        self.assertEqual(result, 58)
+
+    def test_serialize_truncates(self):
+        val = 1.4
+        node = DummySchemaNode(None)
+        typ = self._makeOne(strict=False)
+        result = typ.serialize(node, val)
+        self.assertEqual(result, '1')
+
 
 class TestFloat(unittest.TestCase):
     def _makeOne(self):