8
8
from __future__ import annotations
9
9
10
10
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
12
12
import string
13
13
from pathlib import Path
14
14
from typing import TYPE_CHECKING
19
19
from monty .json import MontyDecoder , MontyEncoder , MSONable
20
20
from monty .serialization import loadfn
21
21
22
- from pymatgen .core import ROOT , SETTINGS , Structure
22
+ from pymatgen .core import ROOT , SETTINGS
23
23
24
24
if TYPE_CHECKING :
25
25
from collections .abc import Sequence
26
26
from typing import Any , ClassVar
27
27
28
+ from pymatgen .core import Structure
29
+ from pymatgen .util .typing import PathLike
30
+
28
31
_MODULE_DIR : Path = Path (__file__ ).absolute ().parent
29
32
30
33
STRUCTURES_DIR : Path = _MODULE_DIR / "structures"
33
36
VASP_IN_DIR : str = f"{ TEST_FILES_DIR } /io/vasp/inputs"
34
37
VASP_OUT_DIR : str = f"{ TEST_FILES_DIR } /io/vasp/outputs"
35
38
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,
37
40
# nuclear charge, core radii, etc. are unchanged (important for testing) while values of the and
38
41
# 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"
40
43
41
44
42
45
class MatSciTest :
@@ -50,36 +53,53 @@ class MatSciTest:
50
53
"""
51
54
52
55
# 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 ("*" ))
54
57
55
- @pytest .fixture (autouse = True ) # make all tests run a in a temporary directory accessible via self.tmp_path
58
+ @pytest .fixture (autouse = True )
56
59
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.
60
61
61
- @ classmethod
62
- def get_structure ( cls , name : str ) -> Structure :
62
+ References:
63
+ https://docs.pytest.org/en/stable/how-to/tmp_path.html
63
64
"""
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.
65
75
66
76
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.
68
80
69
81
Returns:
70
- Structure
82
+ str: Serialized object.
71
83
"""
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__
76
85
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" )
78
89
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
80
100
81
101
@staticmethod
82
- def assert_str_content_equal (actual , expected ) :
102
+ def assert_str_content_equal (actual : str , expected : str ) -> None :
83
103
"""Test if two strings are equal, ignoring whitespaces.
84
104
85
105
Args:
@@ -99,7 +119,32 @@ def assert_str_content_equal(actual, expected):
99
119
f"{ expected } \n "
100
120
)
101
121
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
+ ):
103
148
"""Test whether the object(s) can be serialized and deserialized with
104
149
`pickle`. This method tries to serialize the objects with `pickle` and the
105
150
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 =
163
208
return [o [0 ] for o in objects_by_protocol ]
164
209
return objects_by_protocol
165
210
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
-
198
211
199
212
@deprecated (MatSciTest , deadline = (2026 , 1 , 1 ))
200
213
class PymatgenTest (TestCase , MatSciTest ):
0 commit comments