def test_vq_method__get_validations(self): # method must exist assert hasattr(ValidateQuantities, "validations") assert hasattr(ValidateQuantities, "_get_validations") # setup default validations default_validations = { **self.check_defaults.copy(), "units": [self.check_defaults["units"]], } # setup test cases # 'setup' = arguments for `_get_validations` # 'output' = expected return from `_get_validations` # 'raises' = if `_get_validations` raises an Exception # 'warns' = if `_get_validations` issues a warning # _cases = [ { "descr": "typical call...using 'can_be_negative'", "setup": { "function": self.foo, "args": (5, ), "kwargs": {}, "validations": { "x": { "units": u.cm, "can_be_negative": False } }, }, "output": { "x": { "units": [u.cm], "can_be_negative": False } }, }, { "descr": "typical call...using 'none_shall_pass'", "setup": { "function": self.foo, "args": (5, ), "kwargs": {}, "validations": { "x": { "units": u.cm, "none_shall_pass": True } }, }, "output": { "x": { "units": [u.cm], "none_shall_pass": True } }, }, { "descr": "call w/o value validations", "setup": { "function": self.foo, "args": (5, ), "kwargs": {}, "validations": { "x": { "units": u.cm } }, }, "output": { "x": { "units": [u.cm] } }, }, { "descr": "call w/o unit validations", "setup": { "function": self.foo, "args": (5, ), "kwargs": {}, "validations": { "x": { "can_be_inf": False } }, }, "raises": ValueError, }, { "descr": "'none_shall_pass' defined w/ validations", "setup": { "function": self.foo, "args": (5, ), "kwargs": {}, "validations": { "x": { "units": [u.cm, None] } }, }, "output": { "x": { "units": [u.cm], "none_shall_pass": True } }, }, { "descr": "units are defined via function annotations", "setup": { "function": self.foo_anno, "args": (5, ), "kwargs": {}, "validations": {}, }, "output": { "x": { "units": [u.cm] } }, }, { "descr": "define 'validations_on_return", "setup": { "function": self.foo, "args": (5, ), "kwargs": {}, "validations": { "validations_on_return": { "units": [u.cm, None] } }, }, "output": { "validations_on_return": { "units": [u.cm], "none_shall_pass": True } }, }, { "descr": "'none_shall_pass' is inconsistently doubly defined'", "setup": { "function": self.foo, "args": (5, ), "kwargs": {}, "validations": { "x": { "units": [u.cm, None], "none_shall_pass": False } }, }, "raises": ValueError, }, { "descr": "define both validations on args and validations_on_return", "setup": { "function": self.foo, "args": (5, ), "kwargs": {}, "validations": { "x": { "units": [u.cm], "none_shall_pass": False }, "validations_on_return": { "units": [u.cm], "can_be_zero": False, }, }, }, "output": { "x": { "units": [u.cm], "none_shall_pass": False }, "validations_on_return": { "units": [u.cm], "can_be_zero": False }, }, }, ] # type: List[Dict[str, Any]] for case in _cases: sig = inspect.signature(case["setup"]["function"]) args = case["setup"]["args"] kwargs = case["setup"]["kwargs"] bound_args = sig.bind(*args, **kwargs) vq = ValidateQuantities(**case["setup"]["validations"]) vq.f = case["setup"]["function"] if "warns" in case: with pytest.warns(case["warns"]): validations = vq._get_validations(bound_args) elif "raises" in case: with pytest.raises(case["raises"]): vq._get_validations(bound_args)
def test_vq_method__validate_quantity(self): # method must exist assert hasattr(ValidateQuantities, "_validate_quantity") # setup default validations default_validations = { **self.check_defaults.copy(), "units": [self.check_defaults["units"]], } # setup test cases # 'setup' = arguments for `_get_validations` # 'output' = expected return from `_get_validations` # 'raises' = if `_get_validations` raises an Exception # 'warns' = if `_get_validations` issues a warning # _cases = [ # typical call { "input": { "args": (5 * u.cm, "arg"), "validations": { **default_validations, "units": [u.cm] }, }, "output": 5 * u.cm, }, # argument does not have units, but only one is specified { "input": { "args": (5, "arg"), "validations": { **default_validations, "units": [u.cm] }, }, "output": 5 * u.cm, "warns": u.UnitsWarning, }, # argument does not have units and multiple unit validations specified { "input": { "args": (5, "arg"), "validations": { **default_validations, "units": [u.cm, u.km] }, }, "raises": TypeError, }, # units can NOT be applied to argument { "input": { "args": ({}, "arg"), "validations": { **default_validations, "units": [u.cm] }, }, "raises": TypeError, }, # argument has a standard unit conversion { "input": { "args": (5.0 * u.cm, "arg"), "validations": { **default_validations, "units": [u.km] }, }, "output": (5.0 * u.cm).to(u.km), }, # return value is None and not allowed { "input": { "args": (None, "validations_on_return"), "validations": { **default_validations, "units": [u.cm], "none_shall_pass": False, }, }, "raises": ValueError, }, # 'pass_equivalent_units' is True and unit conversion is not performed { "input": { "args": (5 * u.cm, "arg"), "validations": { **default_validations, "units": [u.km], "pass_equivalent_units": True, }, }, "output": 5 * u.cm, }, ] # setup wrapped function vq = ValidateQuantities() vq.f = self.foo # perform tests for ii, case in enumerate(_cases): arg, arg_name = case["input"]["args"] validations = case["input"]["validations"] if "warns" in case: with pytest.warns(case["warns"]): _result = vq._validate_quantity(arg, arg_name, validations) elif "raises" in case: with pytest.raises(case["raises"]): vq._validate_quantity(arg, arg_name, validations)
for key in default_validations.keys(): if key in case["output"][arg_name]: val = case["output"][arg_name][key] else: val = default_validations[key] assert arg_validations[key] == val # method calls `_get_unit_checks` and `_get_value_checks` with mock.patch.object( CheckUnits, "_get_unit_checks", return_value={}) as mock_cu_get, mock.patch.object( CheckValues, "_get_value_checks", return_value={}) as mock_cv_get: vq = ValidateQuantities(x=u.cm) vq.f = self.foo sig = inspect.signature(self.foo) bound_args = sig.bind(5) assert vq._get_validations(bound_args) == {} assert mock_cu_get.called assert mock_cv_get.called def test_vq_method__validate_quantity(self): # method must exist assert hasattr(ValidateQuantities, "_validate_quantity") # setup default validations default_validations = {
def test_vq_method__get_validations(self): # method must exist assert hasattr(ValidateQuantities, 'validations') assert hasattr(ValidateQuantities, '_get_validations') # setup default validations default_validations = { **self.check_defaults.copy(), 'units': [self.check_defaults['units']] } # setup test cases # 'setup' = arguments for `_get_validations` # 'output' = expected return from `_get_validations` # 'raises' = if `_get_validations` raises an Exception # 'warns' = if `_get_validations` issues a warning # _cases = [ { 'descr': "typical call...using 'can_be_negative'", 'setup': { 'function': self.foo, 'args': (5, ), 'kwargs': {}, 'validations': { 'x': { 'units': u.cm, 'can_be_negative': False } }, }, 'output': { 'x': { 'units': [u.cm], 'can_be_negative': False } }, }, { 'descr': "typical call...using 'none_shall_pass'", 'setup': { 'function': self.foo, 'args': (5, ), 'kwargs': {}, 'validations': { 'x': { 'units': u.cm, 'none_shall_pass': True } }, }, 'output': { 'x': { 'units': [u.cm], 'none_shall_pass': True } }, }, { 'descr': "call w/o value validations", 'setup': { 'function': self.foo, 'args': (5, ), 'kwargs': {}, 'validations': { 'x': { 'units': u.cm } }, }, 'output': { 'x': { 'units': [u.cm] } }, }, { 'descr': "call w/o unit validations", 'setup': { 'function': self.foo, 'args': (5, ), 'kwargs': {}, 'validations': { 'x': { 'can_be_inf': False } }, }, 'raises': ValueError, }, { 'descr': "'none_shall_pass' defined w/ validations", 'setup': { 'function': self.foo, 'args': (5, ), 'kwargs': {}, 'validations': { 'x': { 'units': [u.cm, None] } }, }, 'output': { 'x': { 'units': [u.cm], 'none_shall_pass': True } }, }, { 'descr': "units are defined via function annotations", 'setup': { 'function': self.foo_anno, 'args': (5, ), 'kwargs': {}, 'validations': {}, }, 'output': { 'x': { 'units': [u.cm] } }, }, { 'descr': "define 'validations_on_return", 'setup': { 'function': self.foo, 'args': (5, ), 'kwargs': {}, 'validations': { 'validations_on_return': { 'units': [u.cm, None] } }, }, 'output': { 'validations_on_return': { 'units': [u.cm], 'none_shall_pass': True } }, }, { 'descr': "'none_shall_pass' is inconsistently doubly defined'", 'setup': { 'function': self.foo, 'args': (5, ), 'kwargs': {}, 'validations': { 'x': { 'units': [u.cm, None], 'none_shall_pass': False } }, }, 'raises': ValueError, }, ] # type: List[Dict[str, Any]] for case in _cases: sig = inspect.signature(case['setup']['function']) args = case['setup']['args'] kwargs = case['setup']['kwargs'] bound_args = sig.bind(*args, **kwargs) vq = ValidateQuantities(**case['setup']['validations']) vq.f = case['setup']['function'] if 'warns' in case: with pytest.warns(case['warns']): validations = vq._get_validations(bound_args) elif 'raises' in case: with pytest.raises(case['raises']): vq._get_validations(bound_args)
def test_vq_called_as_decorator(self): """ Test behavior of `ValidateQuantities.__call__` (i.e. used as a decorator). """ # setup test cases # 'setup' = arguments for `CheckUnits` and wrapped function # 'output' = expected return from wrapped function # 'raises' = if an Exception is expected to be raised # 'warns' = if a warning is expected to be issued # _cases = [ { 'descr': 'clean execution', 'setup': { 'function': self.foo, 'args': (2 * u.cm, ), 'kwargs': {}, 'validations': { 'x': u.cm, 'validations_on_return': u.cm } }, 'output': 2 * u.cm, }, { 'descr': 'call with unit conversion', 'setup': { 'function': self.foo, 'args': (2 * u.cm, ), 'kwargs': {}, 'validations': { 'x': u.cm, 'validations_on_return': u.um } }, 'output': (2 * u.cm).to(u.um), }, { 'descr': 'argument fails checks', 'setup': { 'function': self.foo, 'args': (2 * u.cm, ), 'kwargs': {}, 'validations': { 'x': u.g, 'validations_on_return': u.cm } }, 'raises': u.UnitTypeError, }, { 'descr': 'return fails checks', 'setup': { 'function': self.foo, 'args': (2 * u.cm, ), 'kwargs': {}, 'validations': { 'x': u.cm, 'validations_on_return': u.kg } }, 'raises': u.UnitTypeError, }, { 'descr': 'decomposed units are still covnerted', 'setup': { 'function': self.foo, 'args': (2 * u.kg * u.m / u.s**2, ), 'kwargs': {}, 'validations': { 'x': u.N } }, 'output': (2 * u.kg * u.m / u.s**2).to(u.N), 'extra assert': lambda x: x.unit.to_string() == u.N.to_string(), }, ] # perform tests for case in _cases: validations = case['setup']['validations'] func = case['setup']['function'] wfoo = ValidateQuantities(**validations)(func) args = case['setup']['args'] kwargs = case['setup']['kwargs'] if 'raises' in case: with pytest.raises(case['raises']): wfoo(*args, **kwargs) continue _result = wfoo(*args, **kwargs) assert _result == case['output'] if 'extra assert' in case: assert case['extra assert'](_result)
def test_vq_method__validate_quantity(self): # method must exist assert hasattr(ValidateQuantities, '_validate_quantity') # setup default validations default_validations = { **self.check_defaults.copy(), 'units': [self.check_defaults['units']] } # setup test cases # 'setup' = arguments for `_get_validations` # 'output' = expected return from `_get_validations` # 'raises' = if `_get_validations` raises an Exception # 'warns' = if `_get_validations` issues a warning # _cases = [ # typical call { 'input': { 'args': (5 * u.cm, 'arg'), 'validations': { **default_validations, 'units': [u.cm] } }, 'output': 5 * u.cm }, # argument does not have units, but only one is specified { 'input': { 'args': (5, 'arg'), 'validations': { **default_validations, 'units': [u.cm] } }, 'output': 5 * u.cm, 'warns': u.UnitsWarning }, # argument does not have units and multiple unit validations specified { 'input': { 'args': (5, 'arg'), 'validations': { **default_validations, 'units': [u.cm, u.km] } }, 'raises': TypeError }, # units can NOT be applied to argument { 'input': { 'args': ({}, 'arg'), 'validations': { **default_validations, 'units': [u.cm] } }, 'raises': TypeError }, # argument has a non-standard unit conversion { 'input': { 'args': (5. * u.K, 'arg'), 'validations': { **default_validations, 'units': [u.eV], 'equivalencies': u.temperature_energy() } }, 'output': (5. * u.K).to(u.eV, equivalencies=u.temperature_energy()), 'warns': ImplicitUnitConversionWarning }, # argument has a standard unit conversion { 'input': { 'args': (5. * u.cm, 'arg'), 'validations': { **default_validations, 'units': [u.km] } }, 'output': (5. * u.cm).to(u.km) }, # return value is None and not allowed { 'input': { 'args': (None, 'validations_on_return'), 'validations': { **default_validations, 'units': [u.cm], 'none_shall_pass': False } }, 'raises': ValueError }, # 'pass_equivalent_units' is True and unit conversion is not performed { 'input': { 'args': (5 * u.cm, 'arg'), 'validations': { **default_validations, 'units': [u.km], 'pass_equivalent_units': True } }, 'output': 5 * u.cm }, ] # setup wrapped function vq = ValidateQuantities() vq.f = self.foo # perform tests for ii, case in enumerate(_cases): arg, arg_name = case['input']['args'] validations = case['input']['validations'] if 'warns' in case: with pytest.warns(case['warns']): warnings.simplefilter( "always", category=ImplicitUnitConversionWarning) _result = vq._validate_quantity(arg, arg_name, validations) elif 'raises' in case: with pytest.raises(case['raises']): vq._validate_quantity(arg, arg_name, validations)