diff --git a/aiida/common/test_hashing.py b/aiida/common/test_hashing.py index 262318bf9e..2eb14a2e82 100644 --- a/aiida/common/test_hashing.py +++ b/aiida/common/test_hashing.py @@ -17,6 +17,7 @@ import itertools import collections import uuid +import math from datetime import datetime import numpy as np @@ -27,7 +28,7 @@ except ImportError: import unittest -from aiida.common.hashing import make_hash, create_unusable_pass, is_password_usable +from aiida.common.hashing import make_hash, create_unusable_pass, is_password_usable, truncate_float64 from aiida.common.folders import SandboxFolder @@ -46,6 +47,22 @@ def test_is_usable(self): self.assertFalse(is_password_usable('random string without hash identification')) +class TruncationTest(unittest.TestCase): + """ + Tests for the truncate_* methods + """ + + def test_nan(self): + self.assertTrue(math.isnan(truncate_float64(np.nan))) + + def test_inf(self): + self.assertTrue(math.isinf(truncate_float64(np.inf))) + self.assertTrue(math.isinf(truncate_float64(-np.inf))) + + def test_subnormal(self): + self.assertTrue(np.isclose(truncate_float64(1.0e-308), 1.0e-308, atol=1.0e-309)) + + class MakeHashTest(unittest.TestCase): """ Tests for the make_hash function. diff --git a/aiida/orm/implementation/general/node.py b/aiida/orm/implementation/general/node.py index 6f39fdee68..836f8952dc 100644 --- a/aiida/orm/implementation/general/node.py +++ b/aiida/orm/implementation/general/node.py @@ -16,6 +16,8 @@ import logging import importlib import collections +import numbers +import math import six @@ -51,13 +53,20 @@ def clean_value(value): # Must be imported in here to avoid recursive imports from aiida.orm.data import BaseType + def clean_builtin(val): + if isinstance(val, numbers.Real) and (math.isnan(val) or math.isinf(val)): + # see https://www.postgresql.org/docs/current/static/datatype-json.html#JSON-TYPE-MAPPING-TABLE + raise ValidationError("nan and inf/-inf can not be serialized to the database") + + return val + if isinstance(value, BaseType): - return value.value - elif isinstance(value, dict): + return clean_builtin(value.value) + + if isinstance(value, dict): # Check dictionary before iterables return {k: clean_value(v) for k, v in value.items()} - elif (isinstance(value, collections.Iterable) and - not isinstance(value, six.string_types)): + if (isinstance(value, collections.Iterable) and not isinstance(value, six.string_types)): # list, tuple, ... but not a string # This should also properly take care of dealing with the # basedatatypes.List object @@ -67,7 +76,8 @@ def clean_value(value): # itself - it's not super robust, but relies on duck typing # (e.g. if there is something that behaves like an integer # but is not an integer, I still accept it) - return value + + return clean_builtin(value) class _AbstractNodeMeta(ABCMeta):