Skip to content

Commit 71dd429

Browse files
committed
fix bad merge behaviour
1 parent ec33a39 commit 71dd429

File tree

1 file changed

+67
-54
lines changed

1 file changed

+67
-54
lines changed

src/pymatgen/util/testing.py

Lines changed: 67 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from __future__ import annotations
99

1010
import json
11-
import pickle # use pickle, not cPickle so that we get the traceback in case of errors
11+
import pickle # use pickle over cPickle to get traceback in case of errors
1212
import string
1313
from pathlib import Path
1414
from typing import TYPE_CHECKING
@@ -19,12 +19,15 @@
1919
from monty.json import MontyDecoder, MontyEncoder, MSONable
2020
from monty.serialization import loadfn
2121

22-
from pymatgen.core import ROOT, SETTINGS, Structure
22+
from pymatgen.core import ROOT, SETTINGS
2323

2424
if TYPE_CHECKING:
2525
from collections.abc import Sequence
2626
from typing import Any, ClassVar
2727

28+
from pymatgen.core import Structure
29+
from pymatgen.util.typing import PathLike
30+
2831
_MODULE_DIR: Path = Path(__file__).absolute().parent
2932

3033
STRUCTURES_DIR: Path = _MODULE_DIR / "structures"
@@ -33,10 +36,10 @@
3336
VASP_IN_DIR: str = f"{TEST_FILES_DIR}/io/vasp/inputs"
3437
VASP_OUT_DIR: str = f"{TEST_FILES_DIR}/io/vasp/outputs"
3538

36-
# fake POTCARs have original header information, meaning properties like number of electrons,
39+
# Fake POTCARs have original header information, meaning properties like number of electrons,
3740
# nuclear charge, core radii, etc. are unchanged (important for testing) while values of the and
3841
# pseudopotential kinetic energy corrections are scrambled to avoid VASP copyright infringement
39-
FAKE_POTCAR_DIR = f"{VASP_IN_DIR}/fake_potcars"
42+
FAKE_POTCAR_DIR: str = f"{VASP_IN_DIR}/fake_potcars"
4043

4144

4245
class MatSciTest:
@@ -50,36 +53,53 @@ class MatSciTest:
5053
"""
5154

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

55-
@pytest.fixture(autouse=True) # make all tests run a in a temporary directory accessible via self.tmp_path
58+
@pytest.fixture(autouse=True)
5659
def _tmp_dir(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
57-
# https://pytest.org/en/latest/how-to/unittest.html#using-autouse-fixtures-and-accessing-other-fixtures
58-
monkeypatch.chdir(tmp_path) # change to pytest-provided temporary directory
59-
self.tmp_path = tmp_path
60+
"""Make all tests run a in a temporary directory accessible via self.tmp_path.
6061
61-
@classmethod
62-
def get_structure(cls, name: str) -> Structure:
62+
References:
63+
https://docs.pytest.org/en/stable/how-to/tmp_path.html
6364
"""
64-
Load a structure from `pymatgen.util.structures`.
65+
monkeypatch.chdir(tmp_path) # change to temporary directory
66+
self.tmp_path = tmp_path
67+
68+
@staticmethod
69+
def assert_msonable(obj: MSONable, test_is_subclass: bool = True) -> str:
70+
"""Test if an object is MSONable and verify the contract is fulfilled,
71+
and return the serialized object.
72+
73+
By default, the method tests whether obj is an instance of MSONable.
74+
This check can be deactivated by setting `test_is_subclass` to False.
6575
6676
Args:
67-
name (str): Name of the structure file, for example "LiFePO4".
77+
obj (Any): The object to be checked.
78+
test_is_subclass (bool): Check if object is an instance of MSONable
79+
or its subclasses.
6880
6981
Returns:
70-
Structure
82+
str: Serialized object.
7183
"""
72-
try:
73-
struct = cls.TEST_STRUCTURES.get(name) or loadfn(f"{STRUCTURES_DIR}/{name}.json")
74-
except FileNotFoundError as exc:
75-
raise FileNotFoundError(f"structure for {name} doesn't exist") from exc
84+
obj_name = obj.__class__.__name__
7685

77-
cls.TEST_STRUCTURES[name] = struct
86+
# Check if is an instance of MONable (or its subclasses)
87+
if test_is_subclass and not isinstance(obj, MSONable):
88+
raise TypeError(f"{obj_name} object is not MSONable")
7889

79-
return struct.copy()
90+
# Check if the object can be accurately reconstructed from its dict representation
91+
if obj.as_dict() != type(obj).from_dict(obj.as_dict()).as_dict():
92+
raise ValueError(f"{obj_name} object could not be reconstructed accurately from its dict representation.")
93+
94+
# Verify that the deserialized object's class is a subclass of the original object's class
95+
json_str = json.dumps(obj.as_dict(), cls=MontyEncoder)
96+
round_trip = json.loads(json_str, cls=MontyDecoder)
97+
if not issubclass(type(round_trip), type(obj)):
98+
raise TypeError(f"The reconstructed {round_trip.__class__.__name__} object is not a subclass of {obj_name}")
99+
return json_str
80100

81101
@staticmethod
82-
def assert_str_content_equal(actual, expected):
102+
def assert_str_content_equal(actual: str, expected: str) -> None:
83103
"""Test if two strings are equal, ignoring whitespaces.
84104
85105
Args:
@@ -99,7 +119,32 @@ def assert_str_content_equal(actual, expected):
99119
f"{expected}\n"
100120
)
101121

102-
def serialize_with_pickle(self, objects: Any, protocols: Sequence[int] | None = None, test_eq: bool = True):
122+
@classmethod
123+
def get_structure(cls, name: str) -> Structure:
124+
"""
125+
Load a structure from `pymatgen.util.structures`.
126+
127+
Args:
128+
name (str): Name of the structure file, for example "LiFePO4".
129+
130+
Returns:
131+
Structure
132+
"""
133+
try:
134+
struct = cls.TEST_STRUCTURES.get(name) or loadfn(f"{STRUCTURES_DIR}/{name}.json")
135+
except FileNotFoundError as exc:
136+
raise FileNotFoundError(f"structure for {name} doesn't exist") from exc
137+
138+
cls.TEST_STRUCTURES[name] = struct
139+
140+
return struct.copy()
141+
142+
def serialize_with_pickle(
143+
self,
144+
objects: Any,
145+
protocols: Sequence[int] | None = None,
146+
test_eq: bool = True,
147+
):
103148
"""Test whether the object(s) can be serialized and deserialized with
104149
`pickle`. This method tries to serialize the objects with `pickle` and the
105150
protocols specified in input. Then it deserializes the pickled format
@@ -163,38 +208,6 @@ def serialize_with_pickle(self, objects: Any, protocols: Sequence[int] | None =
163208
return [o[0] for o in objects_by_protocol]
164209
return objects_by_protocol
165210

166-
def assert_msonable(self, obj: MSONable, test_is_subclass: bool = True) -> str:
167-
"""Test if an object is MSONable and verify the contract is fulfilled,
168-
and return the serialized object.
169-
170-
By default, the method tests whether obj is an instance of MSONable.
171-
This check can be deactivated by setting `test_is_subclass` to False.
172-
173-
Args:
174-
obj (Any): The object to be checked.
175-
test_is_subclass (bool): Check if object is an instance of MSONable
176-
or its subclasses.
177-
178-
Returns:
179-
str: Serialized object.
180-
"""
181-
obj_name = obj.__class__.__name__
182-
183-
# Check if is an instance of MONable (or its subclasses)
184-
if test_is_subclass and not isinstance(obj, MSONable):
185-
raise TypeError(f"{obj_name} object is not MSONable")
186-
187-
# Check if the object can be accurately reconstructed from its dict representation
188-
if obj.as_dict() != type(obj).from_dict(obj.as_dict()).as_dict():
189-
raise ValueError(f"{obj_name} object could not be reconstructed accurately from its dict representation.")
190-
191-
# Verify that the deserialized object's class is a subclass of the original object's class
192-
json_str = json.dumps(obj.as_dict(), cls=MontyEncoder)
193-
round_trip = json.loads(json_str, cls=MontyDecoder)
194-
if not issubclass(type(round_trip), type(obj)):
195-
raise TypeError(f"The reconstructed {round_trip.__class__.__name__} object is not a subclass of {obj_name}")
196-
return json_str
197-
198211

199212
@deprecated(MatSciTest, deadline=(2026, 1, 1))
200213
class PymatgenTest(TestCase, MatSciTest):

0 commit comments

Comments
 (0)