Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deprecate PymatgenTest, migrate pymatgen tests to pytest from unittest #4212

Open
wants to merge 35 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
08aebc1
revert for a separate migration PR
DanielYang59 Dec 1, 2024
21a36f5
Revert "revert for a separate migration PR"
DanielYang59 Dec 1, 2024
8bacdbc
remove hard-coded and failing test
DanielYang59 Dec 1, 2024
344a872
batch remove PymatgenTest inheritance and see what fail
DanielYang59 Dec 3, 2024
ab8bb0e
migrate alchemy and command_line
DanielYang59 Dec 4, 2024
9ed871d
deprecate pymatgen with matscitest
DanielYang59 Dec 4, 2024
2666983
migrate utils
DanielYang59 Dec 4, 2024
21123bd
migrate optimization
DanielYang59 Dec 4, 2024
614f4f7
migrate vis
DanielYang59 Dec 4, 2024
a2ea374
global replacement of setUp and TearDown, setupClass
DanielYang59 Dec 4, 2024
6f2bfe9
revert all changes to before global replace PymatgenTest
DanielYang59 Dec 4, 2024
9d33460
deprecate pymatgen with matscitest
DanielYang59 Dec 4, 2024
be187d0
global replace setUp with setup_method
DanielYang59 Dec 4, 2024
7366ec7
replace tearDown
DanielYang59 Dec 4, 2024
3239fd5
replace setUpClass(cls) with setup_class(cls)
DanielYang59 Dec 4, 2024
4429546
global remove inheritance from TestCase
DanielYang59 Dec 4, 2024
98f7fc7
global replace inheritance from PymatgenTest
DanielYang59 Dec 4, 2024
dfb616c
global replace PymatgenTest import
DanielYang59 Dec 4, 2024
9ae0deb
global replace get_structure
DanielYang59 Dec 4, 2024
42f9fb6
global replace PymatgenTest with MatSciTest
DanielYang59 Dec 4, 2024
96ccce4
fix tests
DanielYang59 Dec 4, 2024
631ef93
fix internal index and add require for pytest style
DanielYang59 Dec 4, 2024
5c89fc0
fix test
DanielYang59 Dec 4, 2024
e4ac465
remove seemingly unused conftest
DanielYang59 Dec 4, 2024
4292f38
add deprecated decorator and enhance docstring
DanielYang59 Dec 5, 2024
027f84b
inherit to reduce code duplicate
DanielYang59 Dec 5, 2024
73328d0
add some test for legacy tester
DanielYang59 Dec 5, 2024
ebd48ce
also check future warning
DanielYang59 Dec 5, 2024
f6bda1e
remove unnecessary chdir
DanielYang59 Dec 5, 2024
58dc537
prep for merge
DanielYang59 Dec 11, 2024
ec33a39
Merge branch 'master' into real-real-real-migrate-pmg-tests-to-pytest
DanielYang59 Dec 12, 2024
def9886
fix bad merge behaviour
DanielYang59 Dec 12, 2024
881e092
Merge remote-tracking branch 'upstream/master' into real-real-real-mi…
DanielYang59 Dec 13, 2024
13dfc2e
Merge remote-tracking branch 'upstream/master' into real-real-real-mi…
DanielYang59 Jan 2, 2025
3a4e479
Merge remote-tracking branch 'upstream/master' into real-real-real-mi…
DanielYang59 Jan 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 16 additions & 16 deletions docs/contributing.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

127 changes: 125 additions & 2 deletions src/pymatgen/util/testing/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,15 @@
FAKE_POTCAR_DIR = f"{VASP_IN_DIR}/fake_potcars"


class PymatgenTest(TestCase):
"""Extends unittest.TestCase with several assert methods for array and str comparison."""
class MatSciTest:
Copy link
Contributor Author

@DanielYang59 DanielYang59 Dec 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I originally didn't plan to deprecate PymatgenTest in this PR, but it turns out we have to do this here, otherwise tests that subclassing PymatgenTest would still rely on the TestCase and pytest methods like setup_method/class would not be compatible, meaning we have to do another migration after PymatgenTest dropped TestCase inheritance.

So I guess the easiest way is not to drop inheriting TestCase by PymatgenTest, but replacing PymatgenTest with another one that doesn't subclassing TestCase.

I drafted a name here but free feel to comment if you have better ideas (batch rename would be super easy with an IDE so don't worry).

"""TODO: name might need discussion.

TODO: finish docstring

TODO: reduce code duplicate

TODO: add deprecation warning
"""

# dict of lazily-loaded test structures (initialized to None)
TEST_STRUCTURES: ClassVar[dict[str | Path, Structure | None]] = dict.fromkeys(STRUCTURES_DIR.glob("*"))
Expand Down Expand Up @@ -149,3 +156,119 @@ def assert_msonable(self, obj: MSONable, test_is_subclass: bool = True) -> str:
if not issubclass(type(round_trip), type(obj)):
raise TypeError(f"{type(round_trip)} != {type(obj)}")
return json_str


class PymatgenTest(TestCase):
"""Extends unittest.TestCase with several assert methods for array and str comparison.

Deprecated: please use `MatSciTest` instead (migrate from `unittest` to `pytest`).
"""

# dict of lazily-loaded test structures (initialized to None)
TEST_STRUCTURES: ClassVar[dict[str | Path, Structure | None]] = dict.fromkeys(STRUCTURES_DIR.glob("*"))

@pytest.fixture(autouse=True) # make all tests run a in a temporary directory accessible via self.tmp_path
def _tmp_dir(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
# https://pytest.org/en/latest/how-to/unittest.html#using-autouse-fixtures-and-accessing-other-fixtures
monkeypatch.chdir(tmp_path) # change to pytest-provided temporary directory
self.tmp_path = tmp_path

@classmethod
def get_structure(cls, name: str) -> Structure:
"""
Lazily load a structure from pymatgen/util/structures.

Args:
name (str): Name of structure file.

Returns:
Structure
"""
return MatSciTest.get_structure(name)

@staticmethod
def assert_str_content_equal(actual, expected):
"""Test if two strings are equal, ignoring things like trailing spaces, etc."""
MatSciTest.assert_str_content_equal(actual, expected)

def serialize_with_pickle(self, objects: Any, protocols: Sequence[int] | None = None, test_eq: bool = True):
"""Test whether the object(s) can be serialized and deserialized with
pickle. This method tries to serialize the objects with pickle and the
protocols specified in input. Then it deserializes the pickle format
and compares the two objects with the __eq__ operator if
test_eq is True.

Args:
objects: Object or list of objects.
protocols: List of pickle protocols to test. If protocols is None,
HIGHEST_PROTOCOL is tested.
test_eq: If True, the deserialized object is compared with the
original object using the __eq__ method.

Returns:
Nested list with the objects deserialized with the specified
protocols.
"""
# Build a list even when we receive a single object.
got_single_object = False
if not isinstance(objects, list | tuple):
got_single_object = True
objects = [objects]

protocols = protocols or [pickle.HIGHEST_PROTOCOL]

# This list will contain the objects deserialized with the different protocols.
objects_by_protocol, errors = [], []

for protocol in protocols:
# Serialize and deserialize the object.
tmpfile = self.tmp_path / f"tempfile_{protocol}.pkl"

try:
with open(tmpfile, "wb") as file:
pickle.dump(objects, file, protocol=protocol)
except Exception as exc:
errors.append(f"pickle.dump with {protocol=} raised:\n{exc}")
continue

try:
with open(tmpfile, "rb") as file:
unpickled_objs = pickle.load(file) # noqa: S301
except Exception as exc:
errors.append(f"pickle.load with {protocol=} raised:\n{exc}")
continue

# Test for equality
if test_eq:
for orig, unpickled in zip(objects, unpickled_objs, strict=True):
if orig != unpickled:
raise ValueError(
f"Unpickled and original objects are unequal for {protocol=}\n{orig=}\n{unpickled=}"
)

# Save the deserialized objects and test for equality.
objects_by_protocol.append(unpickled_objs)

if errors:
raise ValueError("\n".join(errors))

# Return nested list so that client code can perform additional tests.
if got_single_object:
return [o[0] for o in objects_by_protocol]
return objects_by_protocol

def assert_msonable(self, obj: MSONable, test_is_subclass: bool = True) -> str:
"""Test if obj is MSONable and verify the contract is fulfilled.

By default, the method tests whether obj is an instance of MSONable.
This check can be deactivated by setting test_is_subclass=False.
"""
if test_is_subclass and not isinstance(obj, MSONable):
raise TypeError("obj is not MSONable")
if obj.as_dict() != type(obj).from_dict(obj.as_dict()).as_dict():
raise ValueError("obj could not be reconstructed accurately from its dict representation.")
json_str = json.dumps(obj.as_dict(), cls=MontyEncoder)
round_trip = json.loads(json_str, cls=MontyDecoder)
if not issubclass(type(round_trip), type(obj)):
raise TypeError(f"{type(round_trip)} != {type(obj)}")
return json_str
15 changes: 7 additions & 8 deletions tests/alchemy/test_filters.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from __future__ import annotations

import json
from unittest import TestCase

from monty.json import MontyDecoder

Expand All @@ -14,10 +13,10 @@
from pymatgen.alchemy.transmuters import StandardTransmuter
from pymatgen.analysis.structure_matcher import StructureMatcher
from pymatgen.core import Lattice, Species, Structure
from pymatgen.util.testing import TEST_FILES_DIR, PymatgenTest
from pymatgen.util.testing import TEST_FILES_DIR, MatSciTest


class TestContainsSpecieFilter(PymatgenTest):
class TestContainsSpecieFilter(MatSciTest):
def test_filtering(self):
coords = [[0, 0, 0], [0.75, 0.75, 0.75], [0.5, 0.5, 0.5], [0.25, 0.25, 0.25]]
lattice = Lattice([[3.0, 0.0, 0.0], [1.0, 3.0, 0], [0, -2.0, 3.0]])
Expand Down Expand Up @@ -52,7 +51,7 @@ def test_as_from_dict(self):
assert isinstance(ContainsSpecieFilter.from_dict(dct), ContainsSpecieFilter)


class TestSpecieProximityFilter(PymatgenTest):
class TestSpecieProximityFilter(MatSciTest):
def test_filter(self):
struct = self.get_structure("Li10GeP2S12")
sf = SpecieProximityFilter({"Li": 1})
Expand All @@ -70,8 +69,8 @@ def test_as_from_dict(self):
assert isinstance(SpecieProximityFilter.from_dict(dct), SpecieProximityFilter)


class TestRemoveDuplicatesFilter(TestCase):
def setUp(self):
class TestRemoveDuplicatesFilter:
def setup_method(self):
with open(f"{TEST_FILES_DIR}/entries/TiO2_entries.json") as file:
entries = json.load(file, cls=MontyDecoder)
self._struct_list = [entry.structure for entry in entries]
Expand All @@ -89,8 +88,8 @@ def test_as_from_dict(self):
assert isinstance(RemoveDuplicatesFilter().from_dict(dct), RemoveDuplicatesFilter)


class TestRemoveExistingFilter(TestCase):
def setUp(self):
class TestRemoveExistingFilter:
def setup_method(self):
with open(f"{TEST_FILES_DIR}/entries/TiO2_entries.json") as file:
entries = json.load(file, cls=MontyDecoder)
self._struct_list = [entry.structure for entry in entries]
Expand Down
8 changes: 4 additions & 4 deletions tests/alchemy/test_materials.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@
SupercellTransformation,
)
from pymatgen.util.provenance import StructureNL
from pymatgen.util.testing import FAKE_POTCAR_DIR, TEST_FILES_DIR, PymatgenTest
from pymatgen.util.testing import FAKE_POTCAR_DIR, TEST_FILES_DIR, MatSciTest

TEST_DIR = f"{TEST_FILES_DIR}/alchemy"


class TestTransformedStructure(PymatgenTest):
def setUp(self):
structure = PymatgenTest.get_structure("LiFePO4")
class TestTransformedStructure(MatSciTest):
def setup_method(self):
structure = MatSciTest.get_structure("LiFePO4")
self.structure = structure
trafos = [SubstitutionTransformation({"Li": "Na"})]
self.trans = TransformedStructure(structure, trafos)
Expand Down
6 changes: 3 additions & 3 deletions tests/alchemy/test_transmuters.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@
RemoveSpeciesTransformation,
SubstitutionTransformation,
)
from pymatgen.util.testing import TEST_FILES_DIR, VASP_IN_DIR, PymatgenTest
from pymatgen.util.testing import TEST_FILES_DIR, VASP_IN_DIR, MatSciTest


class TestCifTransmuter(PymatgenTest):
class TestCifTransmuter(MatSciTest):
def test_init(self):
trafos = [SubstitutionTransformation({"Fe": "Mn", "Fe2+": "Mn2+"})]
tsc = CifTransmuter.from_filenames([f"{TEST_FILES_DIR}/cif/MultiStructure.cif"], trafos)
Expand All @@ -22,7 +22,7 @@ def test_init(self):
assert expected == els


class TestPoscarTransmuter(PymatgenTest):
class TestPoscarTransmuter(MatSciTest):
def test_init(self):
trafos = [SubstitutionTransformation({"Fe": "Mn"})]
tsc = PoscarTransmuter.from_filenames([f"{VASP_IN_DIR}/POSCAR", f"{VASP_IN_DIR}/POSCAR"], trafos)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@
from pymatgen.core.lattice import Lattice
from pymatgen.core.sites import PeriodicSite
from pymatgen.core.structure import Structure
from pymatgen.util.testing import TEST_FILES_DIR, PymatgenTest
from pymatgen.util.testing import TEST_FILES_DIR, MatSciTest

__author__ = "waroquiers"


class TestConnectedComponent(PymatgenTest):
class TestConnectedComponent(MatSciTest):
def test_init(self):
# Generic connected component not using EnvironmentNodes
# (as_dict won't work on such a ConnectedComponent instance)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import json

from pymatgen.analysis.chemenv.connectivity.environment_nodes import EnvironmentNode
from pymatgen.util.testing import PymatgenTest
from pymatgen.util.testing import MatSciTest

try:
import bson
Expand All @@ -13,7 +13,7 @@
__author__ = "waroquiers"


class TestEnvironmentNodes(PymatgenTest):
class TestEnvironmentNodes(MatSciTest):
def test_equal(self):
struct = self.get_structure("SiO2")
en = EnvironmentNode(central_site=struct[0], i_central_site=0, ce_symbol="T:4")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@
LightStructureEnvironments,
StructureEnvironments,
)
from pymatgen.util.testing import TEST_FILES_DIR, PymatgenTest
from pymatgen.util.testing import TEST_FILES_DIR, MatSciTest

__author__ = "waroquiers"


class TestStructureConnectivity(PymatgenTest):
class TestStructureConnectivity(MatSciTest):
def test_serialization(self):
BaTiO3_se_fpath = f"{TEST_FILES_DIR}/analysis/chemenv/structure_environments/se_mp-5020.json"
with open(BaTiO3_se_fpath) as file:
Expand Down
Loading
Loading