Skip to content

Commit 12ab947

Browse files
mjwenjanosh
andauthored
Revert back TransformedStructure.__getattr__ (materialsproject#3617)
* Revert changes on __getattr__ * pre-commit auto-fixes * object.__getattribute__(self) -> self.__getattribute__() * add test for no infinite recursion (would have failed before this fix) * refactor --------- Co-authored-by: Janosh Riebesell <[email protected]>
1 parent 47f5ba5 commit 12ab947

File tree

6 files changed

+46
-32
lines changed

6 files changed

+46
-32
lines changed

.github/CODEOWNERS

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
* @shyuep @janosh @mkhorton
22
pymatgen/io/ase.py @Andrew-S-Rosen
33
pymatgen/io/abinit/* @gmatteo
4-
pymatgen/io/lobster/* @JaGeo
4+
pymatgen/io/lobster/* @JaGeo

docs/usage.md

+3-5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pymatgen/alchemy/materials.py

+24-9
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,13 @@
1717
from pymatgen.io.cif import CifParser
1818
from pymatgen.io.vasp.inputs import Poscar
1919
from pymatgen.io.vasp.sets import MPRelaxSet, VaspInputSet
20+
from pymatgen.transformations.transformation_abc import AbstractTransformation
2021
from pymatgen.util.provenance import StructureNL
2122

2223
if TYPE_CHECKING:
24+
from collections.abc import Sequence
25+
2326
from pymatgen.alchemy.filters import AbstractStructureFilter
24-
from pymatgen.transformations.transformation_abc import AbstractTransformation
2527

2628

2729
class TransformedStructure(MSONable):
@@ -35,16 +37,15 @@ class TransformedStructure(MSONable):
3537
def __init__(
3638
self,
3739
structure: Structure,
38-
transformations: list[AbstractTransformation] | None = None,
40+
transformations: AbstractTransformation | Sequence[AbstractTransformation] | None = None,
3941
history: list[AbstractTransformation | dict[str, Any]] | None = None,
4042
other_parameters: dict[str, Any] | None = None,
4143
) -> None:
4244
"""Initializes a transformed structure from a structure.
4345
4446
Args:
4547
structure (Structure): Input structure
46-
transformations (list[Transformation]): List of transformations to
47-
apply.
48+
transformations (list[Transformation]): List of transformations to apply.
4849
history (list[Transformation]): Previous history.
4950
other_parameters (dict): Additional parameters to be added.
5051
"""
@@ -53,6 +54,8 @@ def __init__(
5354
self.other_parameters = other_parameters or {}
5455
self._undone: list[tuple[AbstractTransformation | dict[str, Any], Structure]] = []
5556

57+
if isinstance(transformations, AbstractTransformation):
58+
transformations = [transformations]
5659
transformations = transformations or []
5760
for trafo in transformations:
5861
self.append_transformation(trafo)
@@ -86,8 +89,10 @@ def redo_next_change(self) -> None:
8689
self.history.append(hist)
8790
self.final_structure = struct
8891

89-
def __getattr__(self, name) -> Any:
90-
return getattr(self.final_structure, name)
92+
def __getattr__(self, name: str) -> Any:
93+
# Don't use getattr(self.final_structure, name) here to avoid infinite recursion if name = "final_structure"
94+
struct = self.__getattribute__("final_structure")
95+
return getattr(struct, name)
9196

9297
def __len__(self) -> int:
9398
return len(self.history)
@@ -161,7 +166,9 @@ def append_filter(self, structure_filter: AbstractStructureFilter) -> None:
161166
self.history.append(h_dict)
162167

163168
def extend_transformations(
164-
self, transformations: list[AbstractTransformation], return_alternatives: bool = False
169+
self,
170+
transformations: list[AbstractTransformation],
171+
return_alternatives: bool = False,
165172
) -> None:
166173
"""Extends a sequence of transformations to the TransformedStructure.
167174
@@ -209,7 +216,13 @@ def write_vasp_input(
209216
json.dump(self.as_dict(), file)
210217

211218
def __str__(self) -> str:
212-
output = ["Current structure", "------------", str(self.final_structure), "\nHistory", "------------"]
219+
output = [
220+
"Current structure",
221+
"------------",
222+
str(self.final_structure),
223+
"\nHistory",
224+
"------------",
225+
]
213226
for hist in self.history:
214227
hist.pop("input_structure", None)
215228
output.append(str(hist))
@@ -290,7 +303,9 @@ def from_cif_str(
290303

291304
@classmethod
292305
def from_poscar_str(
293-
cls, poscar_string: str, transformations: list[AbstractTransformation] | None = None
306+
cls,
307+
poscar_string: str,
308+
transformations: list[AbstractTransformation] | None = None,
294309
) -> TransformedStructure:
295310
"""Generates TransformedStructure from a poscar string.
296311

pymatgen/symmetry/analyzer.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -256,14 +256,14 @@ def _get_symmetry(self):
256256
# [1e-4, 2e-4, 1e-4]
257257
# (these are in fractional coordinates, so should be small denominator
258258
# fractions)
259-
trans = []
259+
translations = []
260260
for t in dct["translations"]:
261-
trans.append([float(Fraction.from_float(c).limit_denominator(1000)) for c in t])
262-
trans = np.array(trans)
261+
translations.append([float(Fraction.from_float(c).limit_denominator(1000)) for c in t])
262+
translations = np.array(translations)
263263

264264
# fractional translations of 1 are more simply 0
265-
trans[np.abs(trans) == 1] = 0
266-
return dct["rotations"], trans
265+
translations[np.abs(translations) == 1] = 0
266+
return dct["rotations"], translations
267267

268268
def get_symmetry_operations(self, cartesian=False):
269269
"""Return symmetry operations as a list of SymmOp objects. By default returns

tests/alchemy/test_materials.py

+7-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import json
4+
from copy import deepcopy
45

56
import pytest
67

@@ -22,8 +23,8 @@ class TestTransformedStructure(PymatgenTest):
2223
def setUp(self):
2324
structure = PymatgenTest.get_structure("LiFePO4")
2425
self.structure = structure
25-
trans = [SubstitutionTransformation({"Li": "Na"})]
26-
self.trans = TransformedStructure(structure, trans)
26+
trafos = [SubstitutionTransformation({"Li": "Na"})]
27+
self.trans = TransformedStructure(structure, trafos)
2728

2829
def test_append_transformation(self):
2930
trafo = SubstitutionTransformation({"Fe": "Mn"})
@@ -56,6 +57,8 @@ def test_get_vasp_input(self):
5657

5758
def test_final_structure(self):
5859
assert self.trans.final_structure.reduced_formula == "NaFePO4"
60+
# https://github.com/materialsproject/pymatgen/pull/3617
61+
assert isinstance(deepcopy(self.trans), TransformedStructure)
5962

6063
def test_from_dict(self):
6164
with open(f"{TEST_FILES_DIR}/transformations.json") as file:
@@ -68,11 +71,11 @@ def test_from_dict(self):
6871
assert ts.other_parameters == {"author": "Will", "tags": ["test"]}
6972

7073
def test_undo_and_redo_last_change(self):
71-
trans = [
74+
trafos = [
7275
SubstitutionTransformation({"Li": "Na"}),
7376
SubstitutionTransformation({"Fe": "Mn"}),
7477
]
75-
ts = TransformedStructure(self.structure, trans)
78+
ts = TransformedStructure(self.structure, trafos)
7679
assert ts.final_structure.reduced_formula == "NaMnPO4"
7780
ts.undo_last_change()
7881
assert ts.final_structure.reduced_formula == "NaFePO4"

tests/alchemy/test_transmuters.py

+6-8
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,8 @@
1313

1414
class TestCifTransmuter(PymatgenTest):
1515
def test_init(self):
16-
trans = []
17-
trans.append(SubstitutionTransformation({"Fe": "Mn", "Fe2+": "Mn2+"}))
18-
tsc = CifTransmuter.from_filenames([f"{TEST_FILES_DIR}/MultiStructure.cif"], trans)
16+
trafos = [SubstitutionTransformation({"Fe": "Mn", "Fe2+": "Mn2+"})]
17+
tsc = CifTransmuter.from_filenames([f"{TEST_FILES_DIR}/MultiStructure.cif"], trafos)
1918
assert len(tsc) == 2
2019
expected = {"Mn", "O", "Li", "P"}
2120
for s in tsc:
@@ -25,13 +24,12 @@ def test_init(self):
2524

2625
class TestPoscarTransmuter(PymatgenTest):
2726
def test_init(self):
28-
trans = []
29-
trans.append(SubstitutionTransformation({"Fe": "Mn"}))
30-
tsc = PoscarTransmuter.from_filenames([f"{TEST_FILES_DIR}/POSCAR", f"{TEST_FILES_DIR}/POSCAR"], trans)
27+
trafos = [SubstitutionTransformation({"Fe": "Mn"})]
28+
tsc = PoscarTransmuter.from_filenames([f"{TEST_FILES_DIR}/POSCAR", f"{TEST_FILES_DIR}/POSCAR"], trafos)
3129
assert len(tsc) == 2
3230
expected = {"Mn", "O", "P"}
33-
for s in tsc:
34-
els = {el.symbol for el in s.final_structure.elements}
31+
for substitution in tsc:
32+
els = {el.symbol for el in substitution.final_structure.elements}
3533
assert expected == els
3634

3735
def test_transmuter(self):

0 commit comments

Comments
 (0)