diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..9e4fc8b0 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""Test suite package for pyunitwizard.""" diff --git a/tests/helpers.py b/tests/helpers.py new file mode 100644 index 00000000..0f01e86a --- /dev/null +++ b/tests/helpers.py @@ -0,0 +1,51 @@ +from contextlib import contextmanager + +import pyunitwizard as puw + + +@contextmanager +def loaded_libraries(libraries): + """ + Context manager to temporarily load the requested libraries for a test case. + + Parameters + ---------- + libraries : list of str + List of library names to load temporarily for the duration of the context. + + Behavior + -------- + - Saves the current loaded libraries, default form, and default parser. + - Resets the configuration and loads the specified libraries. + - After the context, restores the previous configuration. + - If no previous libraries were loaded, attempts to load a default set + ('pint', 'openmm.unit', 'unyt'), ignoring failures. + + Exceptions + ---------- + - Any exception raised by `puw.configure.load_library` when loading the requested libraries + will propagate unless caught internally. + - Exceptions when loading default libraries after the context are caught and ignored. + """ + previous_libraries = list(puw.configure.get_libraries_loaded()) + previous_default_form = puw.configure.get_default_form() + previous_default_parser = puw.configure.get_default_parser() + + puw.configure.reset() + puw.configure.load_library(libraries) + try: + yield + finally: + puw.configure.reset() + if previous_libraries: + puw.configure.load_library(previous_libraries) + else: + for library in ['pint', 'openmm.unit', 'unyt']: + try: + puw.configure.load_library(library) + except (ImportError, ModuleNotFoundError): + continue + if previous_default_form is not None: + puw.configure.set_default_form(previous_default_form) + if previous_default_parser is not None: + puw.configure.set_default_parser(previous_default_parser) diff --git a/tests/test_constants.py b/tests/test_constants.py new file mode 100644 index 00000000..a15070c4 --- /dev/null +++ b/tests/test_constants.py @@ -0,0 +1,70 @@ +import pyunitwizard as puw +import pytest + + +@pytest.fixture(autouse=True) +def configure_pint(): + loaded_libraries = list(puw.configure.get_libraries_loaded()) + default_form = puw.configure.get_default_form() + default_parser = puw.configure.get_default_parser() + standard_units = list(puw.configure.get_standard_units().keys()) + + puw.configure.reset() + puw.configure.load_library('pint') + puw.configure.set_standard_units(['nm', 'ps', 'kcal', 'mole']) + + yield + + puw.configure.reset() + + if loaded_libraries: + puw.configure.load_library(loaded_libraries) + + if standard_units: + puw.configure.set_standard_units(standard_units) + + if default_form is not None: + puw.configure.set_default_form(default_form) + + if default_parser is not None: + puw.configure.set_default_parser(default_parser) + + +@pytest.fixture +def constants_module(monkeypatch): + from pyunitwizard.constants import constants as const_mod + + monkeypatch.setattr(const_mod, 'quantity', puw.quantity, raising=False) + monkeypatch.setattr(const_mod, 'convert', puw.convert, raising=False) + + return const_mod + + +def test_get_constant_synonym_and_conversion(constants_module): + universal = constants_module.get_constant('R', to_unit='kJ/(mole*kelvin)') + value, unit = puw.get_value_and_unit(universal, to_form='string') + + assert value == pytest.approx(0.00813446, rel=1e-6) + assert unit == 'kilojoule / kelvin / mole' + + +def test_get_constant_standardized_string_form(constants_module): + raw = constants_module.get_constant('Avogadro') + standardized = constants_module.get_constant('NA', standardized=True) + + assert puw.are_equal(standardized, puw.standardize(raw)) + + string_form = constants_module.get_constant('NA', to_form='string') + assert string_form == '6.02214076e+23 / mole' + + +def test_get_constant_unknown_raises(constants_module): + with pytest.raises(ValueError): + constants_module.get_constant('Unknown constant') + + +def test_show_constants_lists_synonyms(constants_module): + registry = constants_module.show_constants() + + assert ('Avogadro', 'NA') in registry + assert registry[('Universal gas', 'R', 'Molar gas')] == '8.13446261815324 J/(kelvin*mole)' diff --git a/tests/test_parse.py b/tests/test_parse.py index 42eae0f8..f7d02588 100644 --- a/tests/test_parse.py +++ b/tests/test_parse.py @@ -2,6 +2,9 @@ import numpy as np import pyunitwizard as puw +from .helpers import loaded_libraries + + def test_parse_with_pint_scalar(): quantity = _parse_with_pint("5 meters") @@ -48,39 +51,42 @@ def test_parse_to_string(): def test_parse_to_openmm_scalar(): - quantity = parse("5 meters", to_form="openmm.unit") - assert quantity._value == 5 - assert str(quantity.unit) == "meter" + with loaded_libraries(['pint', 'openmm.unit']): + quantity = parse("5 meters", to_form="openmm.unit") + assert quantity._value == 5 + assert str(quantity.unit) == "meter" def test_parse_to_openmm_array(): - quantity = parse("[2, 5, 7] joules", to_form="openmm.unit") - assert np.allclose(quantity._value, np.array([2, 5, 7])) - assert str(quantity.unit) == "joule" + with loaded_libraries(['pint', 'openmm.unit']): + quantity = parse("[2, 5, 7] joules", to_form="openmm.unit") + assert np.allclose(quantity._value, np.array([2, 5, 7])) + assert str(quantity.unit) == "joule" - quantity = parse("(3, 2, 1) meters", to_form="openmm.unit") - assert np.allclose(quantity._value, np.array([3, 2, 1])) - assert str(quantity.unit) == "meter" + quantity = parse("(3, 2, 1) meters", to_form="openmm.unit") + assert np.allclose(quantity._value, np.array([3, 2, 1])) + assert str(quantity.unit) == "meter" - quantity = parse("[[2, 5, 7], [7, 8, 9]] joules", to_form="openmm.unit") - assert np.allclose(quantity._value, np.array([[2, 5, 7], [7, 8, 9]])) - assert str(quantity.unit) == "joule" + quantity = parse("[[2, 5, 7], [7, 8, 9]] joules", to_form="openmm.unit") + assert np.allclose(quantity._value, np.array([[2, 5, 7], [7, 8, 9]])) + assert str(quantity.unit) == "joule" def test_parse_to_unyt(): - quantity = parse("5 meters", to_form="unyt") - assert quantity.value == 5 - assert str(quantity.units) == "m" + with loaded_libraries(['pint', 'unyt']): + quantity = parse("5 meters", to_form="unyt") + assert quantity.value == 5 + assert str(quantity.units) == "m" - quantity = parse("[2, 5, 7] joules", to_form="unyt") - assert np.allclose(quantity.value, np.array([2, 5, 7])) - assert str(quantity.units) == "J" + quantity = parse("[2, 5, 7] joules", to_form="unyt") + assert np.allclose(quantity.value, np.array([2, 5, 7])) + assert str(quantity.units) == "J" - quantity = parse("(3, 2, 1) meters", to_form="unyt") - assert np.allclose(quantity.value, np.array([3, 2, 1])) - assert str(quantity.units) == "m" + quantity = parse("(3, 2, 1) meters", to_form="unyt") + assert np.allclose(quantity.value, np.array([3, 2, 1])) + assert str(quantity.units) == "m" - quantity = parse("[[2, 5, 7], [7, 8, 9]] joules", to_form="unyt") - assert np.allclose(quantity.value, np.array([[2, 5, 7], [7, 8, 9]])) - assert str(quantity.units) == "J" + quantity = parse("[[2, 5, 7], [7, 8, 9]] joules", to_form="unyt") + assert np.allclose(quantity.value, np.array([[2, 5, 7], [7, 8, 9]])) + assert str(quantity.units) == "J" diff --git a/tests/test_quantity.py b/tests/test_quantity.py index 125b7dc7..13fc8b8a 100644 --- a/tests/test_quantity.py +++ b/tests/test_quantity.py @@ -1,12 +1,16 @@ import pyunitwizard as puw import unyt +from .helpers import loaded_libraries + + def test_quantity_openmm_unit(): - openmm_unit = puw.forms.api_openmm_unit.openmm_unit - quantity = puw.quantity('10 kilojoule/mole', form='openmm.unit') - q_true = 10 * openmm_unit.kilojoule/openmm_unit.mole - assert puw.are_close(quantity, q_true) + with loaded_libraries(['pint', 'openmm.unit']): + openmm_unit = puw.forms.api_openmm_unit.openmm_unit + quantity = puw.quantity('10 kilojoule/mole', form='openmm.unit') + q_true = 10 * openmm_unit.kilojoule/openmm_unit.mole + assert puw.are_close(quantity, q_true) def test_quantity_pint(): @@ -16,5 +20,6 @@ def test_quantity_pint(): def test_quantity_unyt(): - assert puw.quantity(1.0, - unyt.J/unyt.s, form="unyt") == 1.0 * unyt.J/unyt.s + with loaded_libraries(['pint', 'unyt']): + assert puw.quantity(1.0, + unyt.J/unyt.s, form="unyt") == 1.0 * unyt.J/unyt.s diff --git a/tests/utils/numpy/test_repeat.py b/tests/utils/numpy/test_repeat.py index 923e9247..1d8fa429 100644 --- a/tests/utils/numpy/test_repeat.py +++ b/tests/utils/numpy/test_repeat.py @@ -16,3 +16,26 @@ def test_stack_1(): assert value.shape==(4, 3, 3) +@pytest.mark.parametrize('value_type', [None, 'list', 'tuple', 'numpy.ndarray']) +def test_repeat_respects_requested_unit(value_type): + puw.configure.reset() + puw.configure.load_library('pint') + + quantity = puw.quantity([1, 2], 'meter') + repeated = puw.utils.numpy.repeat(quantity, 2, to_unit='centimeter', value_type=value_type) + + value = puw.get_value(repeated) + assert np.array_equal(value, np.array([1, 1, 2, 2])) + assert np.allclose(puw.get_value(repeated, to_unit='meter'), np.array([0.01, 0.01, 0.02, 0.02])) + assert puw.get_unit(repeated, to_form='string') == 'centimeter' + + +def test_repeat_raises_for_unknown_value_type(): + puw.configure.reset() + puw.configure.load_library('pint') + + quantity = puw.quantity([1, 2], 'meter') + + with pytest.raises(ValueError): + puw.utils.numpy.repeat(quantity, 1, value_type='invalid') + diff --git a/tests/utils/sequences/test_concatenate.py b/tests/utils/sequences/test_concatenate.py index 530d49fa..08495ada 100644 --- a/tests/utils/sequences/test_concatenate.py +++ b/tests/utils/sequences/test_concatenate.py @@ -15,3 +15,33 @@ def test_concatenate_1(): assert value.shape==(10, 6, 3) +@pytest.mark.parametrize('value_type', ['list', 'tuple', 'numpy.ndarray']) +def test_concatenate_flat_sequence_value_types(value_type): + puw.configure.reset() + puw.configure.load_library('pint') + + sequence = [puw.quantity(index + 1, 'meter') for index in range(3)] + concatenated = puw.utils.sequences.concatenate(sequence, to_unit='centimeter', value_type=value_type) + + value = puw.get_value(concatenated) + assert np.array_equal(value, np.array([100, 200, 300])) + assert puw.get_unit(concatenated, to_form='string') == 'centimeter' + + +def test_concatenate_nested_sequences_and_error_branch(): + puw.configure.reset() + puw.configure.load_library('pint') + + nested = [ + [puw.quantity(1, 'meter'), puw.quantity(2, 'meter')], + [puw.quantity(3, 'meter')], + ] + + concatenated = puw.utils.sequences.concatenate(nested, to_unit='centimeter', value_type='numpy.ndarray') + value = puw.get_value(concatenated) + assert np.array_equal(value, np.array([100, 200, 300])) + + with pytest.raises(ValueError): + puw.utils.sequences.concatenate(nested, value_type='invalid') + +