diff --git a/nomad/metainfo/metainfo.py b/nomad/metainfo/metainfo.py index 38d31445c460ca2904c28b5682b86fc9e7facd0a..951931d06621361d0603c199c911dd6d48a33ff0 100644 --- a/nomad/metainfo/metainfo.py +++ b/nomad/metainfo/metainfo.py @@ -24,6 +24,7 @@ from typing import Type, TypeVar, Union, Tuple, Iterable, List, Any, Dict, Set, from dataclasses import dataclass from collections.abc import Iterable as IterableABC, Sequence import sys +from functools import reduce import inspect import re import json @@ -80,6 +81,7 @@ _types_num = _types_num_python | _types_num_numpy _types_str_numpy = {np.str_} _types_bool_numpy = {np.bool_} _types_numpy = _types_num_numpy | _types_str_numpy | _types_bool_numpy +_delta_symbols = {'delta_', 'Δ'} validElnTypes = { 'str': ['str'], @@ -485,13 +487,6 @@ class _Unit(DataType): if quantity_def is None or unit is None: return - # Explicitly providing a Pint delta-unit is not currently allowed. - # Implicit conversions are fine as MathJS on the frontend supports them. - # todo add back - # unit_string = str(unit) - # if 'delta_' in unit_string or 'Δ' in unit_string: - # raise TypeError(f'Explicit Pint "delta"-unit {unit_string} are not yet supported.') - dimensionality = getattr(quantity_def, 'dimensionality', None) if dimensionality is None: # not set, do not validate @@ -509,7 +504,25 @@ class _Unit(DataType): raise TypeError(f'Dimensionality {dimensionality} is not met by unit {unit}') + @staticmethod + def check_unit(unit: Union[str, pint.Unit]) -> None: + '''Check that the unit is valid. + ''' + if isinstance(unit, str): + unit_str = unit + elif isinstance(unit, pint.unit._Unit): + unit_str = str(unit) + else: + raise TypeError('Units must be given as str or pint Unit instances.') + + # Explicitly providing a Pint delta-unit is not currently allowed. + # Implicit conversions are fine as MathJS on the frontend supports them. + if any(x in unit_str for x in _delta_symbols): + raise TypeError('Explicit Pint "delta"-units are not yet supported.') + def set_normalize(self, section, quantity_def: 'Quantity', value): + _Unit.check_unit(value) + if isinstance(value, str): value = units.parse_units(value) @@ -524,9 +537,13 @@ class _Unit(DataType): return value def serialize(self, section, quantity_def: 'Quantity', value): - return value.__str__() + value = value.__str__() + # The delta prefixes are not serialized: only implicit deltas are + # allowed currently. + return reduce(lambda a, b: a.replace(b, ''), _delta_symbols, value) def deserialize(self, section, quantity_def: 'Quantity', value): + _Unit.check_unit(value) value = units.parse_units(value) _Unit.check_dimensionality(quantity_def, value) return value diff --git a/tests/metainfo/test_metainfo.py b/tests/metainfo/test_metainfo.py index 6e78ab7c9936c1d61537f60bc7fc821d316f6f43..45835a2a1f90301115ebda34f28468672bca6be2 100644 --- a/tests/metainfo/test_metainfo.py +++ b/tests/metainfo/test_metainfo.py @@ -182,16 +182,31 @@ class TestM2: def test_unit(self): assert System.lattice_vectors.unit is not None - @pytest.mark.skip() - def test_unit_explicit_delta(self): - with pytest.raises(TypeError): - Quantity(type=np.dtype(np.float64), unit='delta_degC / hr') + @pytest.mark.parametrize('unit', [ + pytest.param('delta_degC / hr'), + pytest.param('ΔdegC / hr'), + pytest.param(ureg.delta_degC / ureg.hour), + ]) + def test_unit_explicit_delta(self, unit): + '''Explicit delta values are not allowed when setting or de-serializing. + ''' with pytest.raises(TypeError): - Quantity(type=np.dtype(np.float64), unit='ΔdegC / hr') - Quantity(type=np.dtype(np.float64), unit='degC / hr') + Quantity(type=np.dtype(np.float64), unit=unit) with pytest.raises(TypeError): - Quantity(type=np.dtype(np.float64), unit=ureg.delta_degC / ureg.hour) - Quantity(type=np.dtype(np.float64), unit=ureg.degC / ureg.hour) + Quantity.m_from_dict({'m_def': 'nomad.metainfo.metainfo.Quantity', 'unit': str(unit)}) + + @pytest.mark.parametrize('unit', [ + pytest.param('degC / hr'), + pytest.param(ureg.degC / ureg.hour), + ]) + def test_unit_implicit_delta(self, unit): + '''Implicit delta values are allowed in setting and deserializing, delta + prefixes are not serialized. + ''' + quantity = Quantity(type=np.dtype(np.float64), unit=unit) + serialized = quantity.m_to_dict() + assert serialized['unit'] == 'degree_Celsius / hour' + Quantity.m_from_dict(serialized) @pytest.mark.parametrize('dtype', [ pytest.param(np.longlong),