# Licensed under a 3-clause BSD style license - see LICENSE.rst from collections import OrderedDict import numpy as np import pytest from astropy.cosmology import Cosmology from astropy.cosmology._io.mapping import from_mapping, to_mapping from .base import ToFromDirectTestBase, ToFromTestMixinBase ############################################################################### class ToFromMappingTestMixin(ToFromTestMixinBase): """Tests for a Cosmology[To/From]Format with ``format="mapping"``. This class will not be directly called by :mod:`pytest` since its name does not begin with ``Test``. To activate the contained tests this class must be inherited in a subclass. Subclasses must define a :func:`pytest.fixture` ``cosmo`` that returns/yields an instance of a |Cosmology|. See ``TestCosmology`` for an example. """ def test_to_mapping_default(self, cosmo, to_format): """Test default usage of Cosmology -> mapping.""" m = to_format("mapping") keys = tuple(m.keys()) assert isinstance(m, dict) # Check equality of all expected items assert keys[0] == "cosmology" assert m.pop("cosmology") is cosmo.__class__ assert keys[1] == "name" assert m.pop("name") == cosmo.name for i, (k, v) in enumerate(cosmo.parameters.items(), start=2): assert keys[i] == k np.testing.assert_array_equal(m.pop(k), v) assert keys[-1] == "meta" assert m.pop("meta") == cosmo.meta # No unexpected items assert not m def test_to_mapping_wrong_cls(self, to_format): """Test incorrect argument ``cls`` in ``to_mapping()``.""" with pytest.raises(TypeError, match="'cls' must be"): to_format("mapping", cls=list) @pytest.mark.parametrize("map_cls", [dict, OrderedDict]) def test_to_mapping_cls(self, to_format, map_cls): """Test argument ``cls`` in ``to_mapping()``.""" m = to_format("mapping", cls=map_cls) assert isinstance(m, map_cls) # test type def test_to_mapping_cosmology_as_str(self, cosmo_cls, to_format): """Test argument ``cosmology_as_str`` in ``to_mapping()``.""" default = to_format("mapping") # Cosmology is the class m = to_format("mapping", cosmology_as_str=False) assert isinstance(m["cosmology"], type) assert cosmo_cls is m["cosmology"] assert m == default # False is the default option # Cosmology is a string m = to_format("mapping", cosmology_as_str=True) assert isinstance(m["cosmology"], str) assert m["cosmology"] == cosmo_cls.__qualname__ # Correct class assert tuple(m.keys())[0] == "cosmology" # Stayed at same index def test_tofrom_mapping_cosmology_as_str(self, cosmo, to_format, from_format): """Test roundtrip with ``cosmology_as_str=True``. The test for the default option (`False`) is in ``test_tofrom_mapping_instance``. """ m = to_format("mapping", cosmology_as_str=True) got = from_format(m, format="mapping") assert got == cosmo assert got.meta == cosmo.meta def test_to_mapping_move_from_meta(self, to_format): """Test argument ``move_from_meta`` in ``to_mapping()``.""" default = to_format("mapping") # Metadata is 'separate' from main mapping m = to_format("mapping", move_from_meta=False) assert "meta" in m.keys() assert not any(k in m for k in m["meta"]) # Not added to main assert m == default # False is the default option # Metadata is mixed into main mapping. m = to_format("mapping", move_from_meta=True) assert "meta" not in m.keys() assert all(k in m for k in default["meta"]) # All added to main # The parameters take precedence over the metadata assert all(np.array_equal(v, m[k]) for k, v in default.items() if k != "meta") def test_tofrom_mapping_move_tofrom_meta(self, cosmo, to_format, from_format): """Test roundtrip of ``move_from/to_meta`` in ``to/from_mapping()``.""" # Metadata is mixed into main mapping. m = to_format("mapping", move_from_meta=True) # (Just adding something to ensure there's 'metadata') m["mismatching"] = "will error" # (Tests are different if the last argument is a **kwarg) if cosmo._init_has_kwargs: got = from_format(m, format="mapping") assert got.name == cosmo.name assert "mismatching" not in got.meta return # don't continue testing # Reading with mismatching parameters errors... with pytest.raises(TypeError, match="there are unused parameters"): from_format(m, format="mapping") # unless mismatched are moved to meta. got = from_format(m, format="mapping", move_to_meta=True) assert got == cosmo # (Doesn't check metadata) assert got.meta["mismatching"] == "will error" def test_to_mapping_rename_conflict(self, cosmo, to_format): """Test ``rename`` in ``to_mapping()``.""" to_rename = {"name": "name", "H0": "H_0"} match = ( "'renames' values must be disjoint from 'map' keys, " "the common keys are: {'name'}" ) with pytest.raises(ValueError, match=match): to_format("mapping", rename=to_rename) def test_from_mapping_rename_conflict(self, cosmo, to_format, from_format): """Test ``rename`` in `from_mapping()``.""" m = to_format("mapping") match = ( "'renames' values must be disjoint from 'map' keys, " "the common keys are: {'name'}" ) with pytest.raises(ValueError, match=match): from_format(m, format="mapping", rename={"name": "name", "H0": "H_0"}) def test_tofrom_mapping_rename_roundtrip(self, cosmo, to_format, from_format): """Test roundtrip in ``to/from_mapping()`` with ``rename``.""" to_rename = {"name": "cosmo_name"} m = to_format("mapping", rename=to_rename) assert "name" not in m assert "cosmo_name" in m # Wrong names = error with pytest.raises( TypeError, match="there are unused parameters {'cosmo_name':" ): from_format(m, format="mapping") # Roundtrip. correct names = success from_rename = {v: k for k, v in to_rename.items()} got = from_format(m, format="mapping", rename=from_rename) assert got == cosmo # ----------------------------------------------------- def test_from_not_mapping(self, cosmo, from_format): """Test incorrect map type in ``from_mapping()``.""" with pytest.raises((TypeError, ValueError)): from_format("NOT A MAP", format="mapping") def test_from_mapping_default(self, cosmo, to_format, from_format): """Test (cosmology -> Mapping) -> cosmology.""" m = to_format("mapping") # Read from exactly as given. got = from_format(m, format="mapping") assert got == cosmo assert got.meta == cosmo.meta # Reading auto-identifies 'format' got = from_format(m) assert got == cosmo assert got.meta == cosmo.meta def test_fromformat_subclass_partial_info_mapping(self, cosmo): """ Test writing from an instance and reading from that class. This works with missing information. """ m = cosmo.to_format("mapping") # partial information m.pop("cosmology", None) m.pop("Tcmb0", None) # read with the same class that wrote fills in the missing info with # the default value got = cosmo.__class__.from_format(m, format="mapping") got2 = Cosmology.from_format(m, format="mapping", cosmology=cosmo.__class__) got3 = Cosmology.from_format( m, format="mapping", cosmology=cosmo.__class__.__qualname__ ) assert (got == got2) and (got2 == got3) # internal consistency # not equal, because Tcmb0 is changed, which also changes m_nu assert got != cosmo assert got.Tcmb0 == cosmo.__class__.parameters["Tcmb0"].default assert got.clone(name=cosmo.name, Tcmb0=cosmo.Tcmb0, m_nu=cosmo.m_nu) == cosmo # but the metadata is the same assert got.meta == cosmo.meta @pytest.mark.parametrize("format", [True, False, None, "mapping"]) def test_is_equivalent_to_mapping(self, cosmo, to_format, format): """Test :meth:`astropy.cosmology.Cosmology.is_equivalent`. This test checks that Cosmology equivalency can be extended to any Python object that can be converted to a Cosmology -- in this case a mapping. """ obj = to_format("mapping") assert not isinstance(obj, Cosmology) is_equiv = cosmo.is_equivalent(obj, format=format) assert is_equiv is (format is not False) class TestToFromMapping(ToFromDirectTestBase, ToFromMappingTestMixin): """Directly test ``to/from_mapping``.""" def setup_class(self): self.functions = {"to": to_mapping, "from": from_mapping} @pytest.mark.skip("N/A") def test_fromformat_subclass_partial_info_mapping(self): """This test does not apply to the direct functions."""