Skip to content

Commit a7a3ba5

Browse files
DanielYang59shyuep
andauthored
Properly test ase not installed error, use single skip mark for module level test skip (#4107)
* use a single skip mark * use a single skip mark * properly test no ase installed * use specific import error * check None instead of truthy * use single skip mark * separate two skip conditions * use module level skip mark * more descriptive var name * try to remove ignore code * simplify conditional skip for package * use is not None to instead of Falsy * enable unit test * importorskip might be even cleaner * NEED CONFIRM: redirect emmet graph_hashing import * correctly test module not available * fix mock --------- Signed-off-by: Shyue Ping Ong <[email protected]> Co-authored-by: Shyue Ping Ong <[email protected]>
1 parent ddf96bf commit a7a3ba5

File tree

11 files changed

+46
-79
lines changed

11 files changed

+46
-79
lines changed

src/pymatgen/io/ase.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from ase.spacegroup import Spacegroup
2424

2525
NO_ASE_ERR = None
26+
2627
except ImportError:
2728
NO_ASE_ERR = PackageNotFoundError("AseAtomsAdaptor requires the ASE package. Use `pip install ase`")
2829
encode = decode = FixAtoms = SinglePointDFTCalculator = Spacegroup = None
@@ -94,7 +95,7 @@ def get_atoms(structure: SiteCollection, msonable: bool = True, **kwargs) -> MSO
9495
Returns:
9596
Atoms: ASE Atoms object
9697
"""
97-
if NO_ASE_ERR:
98+
if NO_ASE_ERR is not None:
9899
raise NO_ASE_ERR
99100
if not structure.is_ordered:
100101
raise ValueError("ASE Atoms only supports ordered structures")

tests/electronic_structure/test_boltztrap2.py

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from monty.serialization import loadfn
88
from pytest import approx
99

10+
from pymatgen.electronic_structure.boltztrap import BoltztrapError
1011
from pymatgen.electronic_structure.core import OrbitalType, Spin
1112
from pymatgen.io.vasp import Vasprun
1213
from pymatgen.util.testing import TEST_FILES_DIR
@@ -21,9 +22,8 @@
2122
VasprunLoader,
2223
)
2324

24-
BOLTZTRAP2_PRESENT = True
25-
except Exception:
26-
BOLTZTRAP2_PRESENT = False
25+
except BoltztrapError:
26+
pytest.skip("No boltztrap2.", allow_module_level=True)
2727

2828

2929
TEST_DIR = f"{TEST_FILES_DIR}/electronic_structure/boltztrap2"
@@ -40,7 +40,6 @@
4040
BZT_TRANSP_FN = f"{TEST_DIR}/bztTranspProps.json.gz"
4141

4242

43-
@pytest.mark.skipif(not BOLTZTRAP2_PRESENT, reason="No boltztrap2, skipping tests.")
4443
class TestVasprunBSLoader(TestCase):
4544
def setUp(self):
4645
self.loader = VasprunBSLoader(VASP_RUN)
@@ -80,7 +79,6 @@ def test_get_volume(self):
8079
assert self.loader.get_volume() == approx(477.6256714925874, abs=1e-5)
8180

8281

83-
@pytest.mark.skipif(not BOLTZTRAP2_PRESENT, reason="No boltztrap2, skipping tests.")
8482
class TestBandstructureLoader(TestCase):
8583
def setUp(self):
8684
self.loader = BandstructureLoader(BAND_STRUCT, VASP_RUN.structures[-1])
@@ -108,7 +106,6 @@ def test_set_upper_lower_bands(self):
108106
assert self.loader_sp_dn.ebands.shape == (14, 198)
109107

110108

111-
@pytest.mark.skipif(not BOLTZTRAP2_PRESENT, reason="No boltztrap2, skipping tests.")
112109
class TestVasprunLoader(TestCase):
113110
def setUp(self):
114111
self.loader = VasprunLoader(VASP_RUN)
@@ -128,7 +125,6 @@ def test_from_file(self):
128125
assert self.loader is not None
129126

130127

131-
@pytest.mark.skipif(not BOLTZTRAP2_PRESENT, reason="No boltztrap2, skipping tests.")
132128
class TestBztInterpolator(TestCase):
133129
def setUp(self):
134130
self.loader = VasprunBSLoader(VASP_RUN)
@@ -206,7 +202,6 @@ def test_tot_proj_dos(self):
206202
assert pdos == approx(272.194174, abs=1e-5)
207203

208204

209-
@pytest.mark.skipif(not BOLTZTRAP2_PRESENT, reason="No boltztrap2, skipping tests.")
210205
class TestBztTransportProperties(TestCase):
211206
def setUp(self):
212207
loader = VasprunBSLoader(VASP_RUN)
@@ -307,7 +302,6 @@ def test_compute_properties_doping(self):
307302
assert self.bztTransp_sp.contain_props_doping
308303

309304

310-
@pytest.mark.skipif(not BOLTZTRAP2_PRESENT, reason="No boltztrap2, skipping tests.")
311305
class TestBztPlotter(TestCase):
312306
def test_plot(self):
313307
loader = VasprunBSLoader(VASP_RUN)

tests/ext/test_matproj.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,17 +32,21 @@
3232
MP_URL = "https://api.materialsproject.org"
3333
else:
3434
MP_URL = "https://materialsproject.org"
35+
3536
try:
3637
skip_mprester_tests = requests.get(MP_URL, timeout=60).status_code != 200
3738

3839
except (ModuleNotFoundError, ImportError, requests.exceptions.ConnectionError):
3940
# Skip all MPRester tests if some downstream problem on the website, mp-api or whatever.
4041
skip_mprester_tests = True
4142

43+
if skip_mprester_tests:
44+
pytest.skip("MP API is down", allow_module_level=True)
45+
4246

4347
@pytest.mark.skipif(
44-
skip_mprester_tests or (not 10 < len(PMG_MAPI_KEY) <= 20),
45-
reason="Legacy PMG_MAPI_KEY environment variable not set or MP API is down.",
48+
not 10 < len(PMG_MAPI_KEY) <= 20,
49+
reason="Legacy PMG_MAPI_KEY environment variable not set.",
4650
)
4751
class TestMPResterOld(PymatgenTest):
4852
def setUp(self):
@@ -515,8 +519,8 @@ def test_api_key_is_none(self):
515519

516520

517521
@pytest.mark.skipif(
518-
skip_mprester_tests or (not len(PMG_MAPI_KEY) > 20),
519-
reason="PMG_MAPI_KEY environment variable not set or MP API is down.",
522+
not len(PMG_MAPI_KEY) > 20,
523+
reason="PMG_MAPI_KEY environment variable not set.",
520524
)
521525
class TestMPResterNewBasic(PymatgenTest):
522526
def setUp(self):

tests/ext/test_optimade.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88

99
try:
1010
# 403 is returned when server detects bot-like behavior
11-
website_down = requests.get(OptimadeRester.aliases["mp"], timeout=60).status_code not in (200, 403)
11+
mp_website_down = requests.get(OptimadeRester.aliases["mp"], timeout=60).status_code not in (200, 403)
1212
except requests.exceptions.ConnectionError:
13-
website_down = True
13+
mp_website_down = True
1414

1515
try:
1616
optimade_providers_down = requests.get("https://providers.optimade.org", timeout=60).status_code not in (200, 403)
@@ -37,7 +37,7 @@
3737

3838

3939
class TestOptimade(PymatgenTest):
40-
@pytest.mark.skipif(website_down, reason="MP OPTIMADE is down.")
40+
@pytest.mark.skipif(mp_website_down, reason="MP OPTIMADE is down.")
4141
def test_get_structures_mp(self):
4242
with OptimadeRester("mp") as optimade:
4343
structs = optimade.get_structures(elements=["Ga", "N"], nelements=2)
@@ -55,7 +55,7 @@ def test_get_structures_mp(self):
5555
raw_filter_structs["mp"]
5656
), f"Raw filter {_filter} did not return the same number of results as the query builder."
5757

58-
@pytest.mark.skipif(website_down, reason="MP OPTIMADE is down.")
58+
@pytest.mark.skipif(mp_website_down, reason="MP OPTIMADE is down.")
5959
def test_get_snls_mp(self):
6060
base_query = dict(elements=["Ga", "N"], nelements=2, nsites=[2, 6])
6161
with OptimadeRester("mp") as optimade:

tests/io/pwmat/test_inputs.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
from __future__ import annotations
22

3+
import importlib
4+
from unittest import mock
5+
36
import pytest
47
from monty.io import zopen
58
from numpy.testing import assert_allclose
69

10+
import pymatgen
711
from pymatgen.core import Composition, Structure
812
from pymatgen.io.pwmat.inputs import (
913
ACExtractor,
@@ -129,10 +133,16 @@ def test_write_file(self):
129133
assert tmp_high_symmetry_points_str == high_symmetry_points.get_str()
130134

131135

132-
def test_err_msg_on_seekpath_not_installed(monkeypatch):
136+
def test_err_msg_on_seekpath_not_installed():
133137
"""Simulate and test error message when seekpath is not installed."""
134-
try:
135-
import seekpath # noqa: F401
136-
except ImportError:
138+
139+
with mock.patch.dict("sys.modules", {"seekpath": None}):
140+
# As the import error is raised during init of KPathSeek,
141+
# have to import it as well (order matters)
142+
importlib.reload(pymatgen.symmetry.kpath)
143+
importlib.reload(pymatgen.io.pwmat.inputs)
144+
145+
from pymatgen.io.pwmat.inputs import GenKpt
146+
137147
with pytest.raises(RuntimeError, match="SeeK-path needs to be installed to use the convention of Hinuma et al"):
138148
GenKpt.from_structure(Structure.from_file(f"{TEST_DIR}/atom.config"), dim=2, density=0.01)

tests/io/test_ase.py

Lines changed: 9 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from __future__ import annotations
22

3+
import importlib
34
from importlib.metadata import PackageNotFoundError
5+
from unittest import mock
46

57
import numpy as np
68
import pytest
@@ -11,18 +13,12 @@
1113
from pymatgen.io.ase import AseAtomsAdaptor, MSONAtoms
1214
from pymatgen.util.testing import TEST_FILES_DIR, VASP_IN_DIR, VASP_OUT_DIR
1315

14-
try:
15-
import ase
16-
except ImportError:
17-
ase = None
16+
ase = pytest.importorskip("ase", reason="ase not installed")
1817

1918
STRUCTURE = Structure.from_file(f"{VASP_IN_DIR}/POSCAR")
2019
XYZ_STRUCTURE = f"{TEST_FILES_DIR}/io/xyz/acetylene.xyz"
2120

22-
skip_if_no_ase = pytest.mark.skipif(ase is None, reason="ase not installed")
2321

24-
25-
@skip_if_no_ase
2622
def test_get_atoms_from_structure():
2723
atoms = AseAtomsAdaptor.get_atoms(STRUCTURE)
2824
ase_composition = Composition(atoms.get_chemical_formula())
@@ -42,7 +38,6 @@ def test_get_atoms_from_structure():
4238
assert atoms.get_array("prop").tolist() == prop
4339

4440

45-
@skip_if_no_ase
4641
def test_get_atoms_from_structure_mags():
4742
mags = [1.0] * len(STRUCTURE)
4843
STRUCTURE.add_site_property("final_magmom", mags)
@@ -64,7 +59,6 @@ def test_get_atoms_from_structure_mags():
6459
assert atoms.get_magnetic_moments().tolist(), mags
6560

6661

67-
@skip_if_no_ase
6862
def test_get_atoms_from_structure_charge():
6963
charges = [1.0] * len(STRUCTURE)
7064
STRUCTURE.add_site_property("final_charge", charges)
@@ -86,22 +80,19 @@ def test_get_atoms_from_structure_charge():
8680
assert atoms.get_charges().tolist(), charges
8781

8882

89-
@skip_if_no_ase
9083
def test_get_atoms_from_structure_oxi_states():
9184
oxi_states = [1.0] * len(STRUCTURE)
9285
STRUCTURE.add_oxidation_state_by_site(oxi_states)
9386
atoms = AseAtomsAdaptor.get_atoms(STRUCTURE)
9487
assert atoms.get_array("oxi_states").tolist() == oxi_states
9588

9689

97-
@skip_if_no_ase
9890
def test_get_atoms_from_structure_dyn():
9991
STRUCTURE.add_site_property("selective_dynamics", [[False] * 3] * len(STRUCTURE))
10092
atoms = AseAtomsAdaptor.get_atoms(STRUCTURE)
10193
assert atoms.constraints[0].get_indices().tolist() == [atom.index for atom in atoms]
10294

10395

104-
@skip_if_no_ase
10596
def test_get_atoms_from_molecule():
10697
mol = Molecule.from_file(XYZ_STRUCTURE)
10798
atoms = AseAtomsAdaptor.get_atoms(mol)
@@ -113,7 +104,6 @@ def test_get_atoms_from_molecule():
113104
assert not atoms.has("initial_magmoms")
114105

115106

116-
@skip_if_no_ase
117107
def test_get_atoms_from_molecule_mags():
118108
molecule = Molecule.from_file(XYZ_STRUCTURE)
119109
atoms = AseAtomsAdaptor.get_atoms(molecule)
@@ -139,15 +129,13 @@ def test_get_atoms_from_molecule_mags():
139129
assert atoms.spin_multiplicity == 3
140130

141131

142-
@skip_if_no_ase
143132
def test_get_atoms_from_molecule_dyn():
144133
molecule = Molecule.from_file(XYZ_STRUCTURE)
145134
molecule.add_site_property("selective_dynamics", [[False] * 3] * len(molecule))
146135
atoms = AseAtomsAdaptor.get_atoms(molecule)
147136
assert atoms.constraints[0].get_indices().tolist() == [atom.index for atom in atoms]
148137

149138

150-
@skip_if_no_ase
151139
def test_get_structure():
152140
atoms = ase.io.read(f"{VASP_IN_DIR}/POSCAR")
153141
struct = AseAtomsAdaptor.get_structure(atoms)
@@ -170,7 +158,6 @@ def test_get_structure():
170158
struct = AseAtomsAdaptor.get_structure(atoms, validate_proximity=True)
171159

172160

173-
@skip_if_no_ase
174161
def test_get_structure_mag():
175162
atoms = ase.io.read(f"{VASP_IN_DIR}/POSCAR")
176163
mags = [1.0] * len(atoms)
@@ -187,7 +174,6 @@ def test_get_structure_mag():
187174
assert "initial_magmoms" not in structure.site_properties
188175

189176

190-
@skip_if_no_ase
191177
@pytest.mark.parametrize(
192178
"select_dyn",
193179
[[True, True, True], [False, False, False], np.array([True, True, True]), np.array([False, False, False])],
@@ -212,7 +198,6 @@ def test_get_structure_dyn(select_dyn):
212198
assert len(ase_atoms) == len(structure)
213199

214200

215-
@skip_if_no_ase
216201
def test_get_molecule():
217202
atoms = ase.io.read(XYZ_STRUCTURE)
218203
molecule = AseAtomsAdaptor.get_molecule(atoms)
@@ -240,7 +225,6 @@ def test_get_molecule():
240225
assert molecule.spin_multiplicity == 3
241226

242227

243-
@skip_if_no_ase
244228
@pytest.mark.parametrize("filename", ["io/vasp/outputs/OUTCAR.gz", "cif/V2O3.cif"])
245229
def test_back_forth(filename):
246230
# Atoms --> Structure --> Atoms --> Structure
@@ -259,7 +243,6 @@ def test_back_forth(filename):
259243
assert str(atoms_back.todict()[key]) == str(val)
260244

261245

262-
@skip_if_no_ase
263246
def test_back_forth_v2():
264247
# Structure --> Atoms --> Structure --> Atoms
265248
structure = Structure.from_file(f"{VASP_IN_DIR}/POSCAR")
@@ -281,7 +264,6 @@ def test_back_forth_v2():
281264
MontyDecoder().process_decoded(dct)
282265

283266

284-
@skip_if_no_ase
285267
def test_back_forth_v3():
286268
# Atoms --> Molecule --> Atoms --> Molecule
287269
atoms = ase.io.read(XYZ_STRUCTURE)
@@ -299,7 +281,6 @@ def test_back_forth_v3():
299281
assert molecule_back == molecule
300282

301283

302-
@skip_if_no_ase
303284
def test_back_forth_v4():
304285
# Molecule --> Atoms --> Molecule --> Atoms
305286
molecule = Molecule.from_file(XYZ_STRUCTURE)
@@ -317,7 +298,6 @@ def test_back_forth_v4():
317298
MontyDecoder().process_decoded(dct)
318299

319300

320-
@skip_if_no_ase
321301
def test_back_forth_v5():
322302
# Structure --> Atoms --> Structure --> Atoms
323303
structure = Structure.from_file(f"{VASP_IN_DIR}/POSCAR")
@@ -331,7 +311,6 @@ def test_back_forth_v5():
331311
assert str(atoms_back.todict()[key]) == str(val)
332312

333313

334-
@skip_if_no_ase
335314
def test_msonable_atoms():
336315
structure = Structure.from_file(f"{VASP_IN_DIR}/POSCAR")
337316

@@ -369,10 +348,12 @@ def test_msonable_atoms():
369348
assert isinstance(atoms, ase.Atoms)
370349

371350

372-
@pytest.mark.skipif(ase is not None, reason="ase is present")
373351
def test_no_ase_err():
374352
import pymatgen.io.ase
375353

376-
expected_msg = str(pymatgen.io.ase.NO_ASE_ERR)
377-
with pytest.raises(PackageNotFoundError, match=expected_msg):
378-
pymatgen.io.ase.MSONAtoms()
354+
with mock.patch.dict("sys.modules", {"ase.atoms": None}):
355+
importlib.reload(pymatgen.io.ase)
356+
from pymatgen.io.ase import MSONAtoms
357+
358+
with pytest.raises(PackageNotFoundError, match="AseAtomsAdaptor requires the ASE package."):
359+
MSONAtoms()

0 commit comments

Comments
 (0)