Repository navigation
Pass through None dataset in spglib dot access wrapper - #118
Merged
t-reents merged 2 commits intoJan 29, 2026
Merged
Conversation
get_dot_access_dataset is a wrapper designed to convert old-style (dictionary access) spglib results to new-style (class attribute access) objects. Currently it does not consider the case that it is passed "None", which is the error signal for spglib<2.7. This means that "if dataset is None" code block in seekpath.hpkot.__init__ is probably unreachable. To have this work as expected, allow None results to pass through the wrapper unchanged.
Collaborator
|
Thank you so much @ajjackson ! I was surprised that this is the case, but I just checked and this was actually integrated 1.5 years ago, after the previous 2.1 release. I initially assumed that this was already part of previous releases, and therefore working, when doing the refactoring for the 2.2 release. Thanks again for spotting and fixing the issue! |
t-reents
approved these changes
Jan 29, 2026
shyuep
added a commit
to materialsproject/pymatgen-core
that referenced
this pull request
Sep 16, 2026
…version (#138) * tests: probe seekpath usability instead of gating on platform/Python version The seekpath-backed k-path tests were skipped on Windows and Python >= 3.13 by a hard-coded platform/version check. Combined with the CI matrix that meant they never ran on any CI job: the windows 3.13 leg installs no 'optional' extra so importorskip fires, while the macos 3.13 and ubuntu 3.14 legs that do install seekpath were excluded by the version branch. The gate is stale. It was added in 3643cdb (2026-05-08) to work around a real failure, but 9f92544 removed the 'optional' extra from the Windows leg 11 days later, turning the platform branch into dead code while the version branch kept suppressing coverage. The underlying bug was upstream: seekpath's get_dot_access_dataset() did not handle the None dataset that spglib < 2.7 returns, fixed in materialscloud-org/seekpath#118 (released in 2.2.1), which uv.lock already pins. Replace the gate with an end-to-end probe over all seven crystal systems the tests exercise, calling get_kpoints() so the probe covers the same code path. The tests now run wherever seekpath works and skip with an accurate reason wherever it does not. Verified on Windows: py3.13 and py3.14 with seekpath 2.2.1 / spglib 2.7.0 now run 11 tests (previously 5 skipped), the full 230-spacegroup sweep passes, and py3.13 without the optional extra still skips cleanly. macOS/Linux not available locally to test. * tests: restore real modules after simulating seekpath absence test_err_msg_on_seekpath_not_installed reloads pymatgen.symmetry.kpath while seekpath is shadowed out of sys.modules. The reload executes the module's import fallback, leaving `get_path = None` in its namespace, and nothing restores it when the patch exits: sys.modules is rolled back, but the module namespace stays poisoned for the rest of the session. Any test that constructs a KPathSeek afterwards dies with "'NoneType' object is not callable" at kpath.py:941. Under xdist this only hits workers that also ran the pwmat tests, which made the failure look intermittent - the same symptom that motivated the hard-coded platform/Python skip in the seekpath tests (3643cdb). Reload both modules again after the patch exits, in a finally so that a failing assertion inside the block cannot skip the restore. This is also what makes the probe-based gate from the previous commit viable: the pollution happens at run time, after probes are evaluated at collection time, so a probe alone cannot catch it. * Address review on the seekpath test gate 1. The probe no longer swallows every exception. It used to catch bare Exception and return False, which converted any future regression in KPathSeek into a silent module skip -- the same failure mode this PR removes, one level down. It now tolerates only the two documented unavailability signatures (seekpath not installed, and spglib returning no dataset so kpath.get_path was left as None) and re-raises everything else. The caught exception text goes into the skip reason. 2. The duplicated probe is gone. seekpath_unusable_reason() now lives once in pymatgen.util.testing and is imported by both test modules, with functools.cache so the seven KPathSeek constructions happen once per session rather than once per importing module. 3. Kept the module reloads, deliberately -- see below. On (3): switching to monkeypatch.setattr(kpath, "get_path", None) does not work here. KPathSeek.__init__ is guarded by @requires(get_path is not None, ...) and monty's requires() captures that condition as a bool at decoration time. Once kpath has been imported with seekpath importable the guard is permanently satisfied, so blanking get_path afterwards only changes what kpath.py:941 calls: >>> kpath.get_path = None >>> KPathSeek(struct).get_kpoints() TypeError: 'NoneType' object is not callable whereas the test asserts RuntimeError "SeeK-path needs to be installed". Re-executing the module is what re-evaluates the guard, so the reload is load-bearing. Verified against pymatgen-core 2026.8.30 / seekpath 2.2.1 / spglib 2.7.0. The stale-class-object concern is real but does not bite here: importlib.reload re-executes into the same module namespace, so a class object captured before the reload still resolves get_path through kpath.__dict__ and heals once the restore runs. To make that contract explicit rather than implicit, the test now asserts kpath.get_path is not None at the end. Verified locally on Windows, Python 3.13, pymatgen-core 2026.8.30, seekpath 2.2.1, spglib 2.7.0, single process: tests/io/pwmat/test_inputs.py + tests/symmetry/test_kpath_hin.py + tests/symmetry/test_kpaths.py 26 passed same trio with the `finally` restore removed 6 failed (the new assertion, plus the same 5 seekpath tests that failed on CI) ruff check and ruff format clean on all four files. * Address review on the seekpath test gate 1. The probe no longer swallows every exception. It used to catch bare Exception and return False, which converted any future regression in KPathSeek into a silent module skip -- the same failure mode this PR removes, one level down. It now tolerates only the two documented unavailability signatures (seekpath not installed, and spglib returning no dataset so kpath.get_path was left as None) and re-raises everything else. The caught exception text goes into the skip reason. 2. The duplicated probe is gone. seekpath_unusable_reason() now lives once in pymatgen.util.testing and is imported by both test modules, with functools.cache so the seven KPathSeek constructions happen once per session rather than once per importing module. 3. Kept the module reloads, deliberately -- see below. On (3): switching to monkeypatch.setattr(kpath, "get_path", None) does not work here. KPathSeek.__init__ is guarded by @requires(get_path is not None, ...) and monty's requires() captures that condition as a bool at decoration time. Once kpath has been imported with seekpath importable the guard is permanently satisfied, so blanking get_path afterwards only changes what kpath.py:941 calls: >>> kpath.get_path = None >>> KPathSeek(struct).get_kpoints() TypeError: 'NoneType' object is not callable whereas the test asserts RuntimeError "SeeK-path needs to be installed". Re-executing the module is what re-evaluates the guard, so the reload is load-bearing. Verified against pymatgen-core 2026.8.30 / seekpath 2.2.1 / spglib 2.7.0. The stale-class-object concern is real but does not bite here: importlib.reload re-executes into the same module namespace, so a class object captured before the reload still resolves get_path through kpath.__dict__ and heals once the restore runs. To make that contract explicit rather than implicit, the test now asserts kpath.get_path is not None at the end. Verified locally on Windows, Python 3.13, pymatgen-core 2026.8.30, seekpath 2.2.1, spglib 2.7.0, single process: tests/io/pwmat/test_inputs.py + tests/symmetry/test_kpath_hin.py + tests/symmetry/test_kpaths.py 26 passed same trio with the `finally` restore removed 6 failed (the new assertion, plus the same 5 seekpath tests that failed on CI) ruff check and ruff format clean on all four files. * Address review on the seekpath test gate 1. The probe no longer swallows every exception. It used to catch bare Exception and return False, which converted any future regression in KPathSeek into a silent module skip -- the same failure mode this PR removes, one level down. It now tolerates only the two documented unavailability signatures (seekpath not installed, and spglib returning no dataset so kpath.get_path was left as None) and re-raises everything else. The caught exception text goes into the skip reason. 2. The duplicated probe is gone. seekpath_unusable_reason() now lives once in pymatgen.util.testing and is imported by both test modules, with functools.cache so the seven KPathSeek constructions happen once per session rather than once per importing module. 3. Kept the module reloads, deliberately -- see below. On (3): switching to monkeypatch.setattr(kpath, "get_path", None) does not work here. KPathSeek.__init__ is guarded by @requires(get_path is not None, ...) and monty's requires() captures that condition as a bool at decoration time. Once kpath has been imported with seekpath importable the guard is permanently satisfied, so blanking get_path afterwards only changes what kpath.py:941 calls: >>> kpath.get_path = None >>> KPathSeek(struct).get_kpoints() TypeError: 'NoneType' object is not callable whereas the test asserts RuntimeError "SeeK-path needs to be installed". Re-executing the module is what re-evaluates the guard, so the reload is load-bearing. Verified against pymatgen-core 2026.8.30 / seekpath 2.2.1 / spglib 2.7.0. The stale-class-object concern is real but does not bite here: importlib.reload re-executes into the same module namespace, so a class object captured before the reload still resolves get_path through kpath.__dict__ and heals once the restore runs. To make that contract explicit rather than implicit, the test now asserts kpath.get_path is not None at the end. Verified locally on Windows, Python 3.13, pymatgen-core 2026.8.30, seekpath 2.2.1, spglib 2.7.0, single process: tests/io/pwmat/test_inputs.py + tests/symmetry/test_kpath_hin.py + tests/symmetry/test_kpaths.py 26 passed same trio with the `finally` restore removed 6 failed (the new assertion, plus the same 5 seekpath tests that failed on CI) ruff check and ruff format clean on all four files. * Address review on the seekpath test gate 1. The probe no longer swallows every exception. It used to catch bare Exception and return False, which converted any future regression in KPathSeek into a silent module skip -- the same failure mode this PR removes, one level down. It now tolerates only the two documented unavailability signatures (seekpath not installed, and spglib returning no dataset so kpath.get_path was left as None) and re-raises everything else. The caught exception text goes into the skip reason. 2. The duplicated probe is gone. seekpath_unusable_reason() now lives once in pymatgen.util.testing and is imported by both test modules, with functools.cache so the seven KPathSeek constructions happen once per session rather than once per importing module. 3. Kept the module reloads, deliberately -- see below. On (3): switching to monkeypatch.setattr(kpath, "get_path", None) does not work here. KPathSeek.__init__ is guarded by @requires(get_path is not None, ...) and monty's requires() captures that condition as a bool at decoration time. Once kpath has been imported with seekpath importable the guard is permanently satisfied, so blanking get_path afterwards only changes what kpath.py:941 calls: >>> kpath.get_path = None >>> KPathSeek(struct).get_kpoints() TypeError: 'NoneType' object is not callable whereas the test asserts RuntimeError "SeeK-path needs to be installed". Re-executing the module is what re-evaluates the guard, so the reload is load-bearing. Verified against pymatgen-core 2026.8.30 / seekpath 2.2.1 / spglib 2.7.0. The stale-class-object concern is real but does not bite here: importlib.reload re-executes into the same module namespace, so a class object captured before the reload still resolves get_path through kpath.__dict__ and heals once the restore runs. To make that contract explicit rather than implicit, the test now asserts kpath.get_path is not None at the end. Verified locally on Windows, Python 3.13, pymatgen-core 2026.8.30, seekpath 2.2.1, spglib 2.7.0, single process: tests/io/pwmat/test_inputs.py + tests/symmetry/test_kpath_hin.py + tests/symmetry/test_kpaths.py 26 passed same trio with the `finally` restore removed 6 failed (the new assertion, plus the same 5 seekpath tests that failed on CI) ruff check and ruff format clean on all four files. * Fix the restore assertion and a probe comment label Per re-review: 1. The restore check asserted kpath.get_path is not None, which reads as "seekpath is installed" when what it means is "the reload was undone". On the two legs that do not install the optional extra (no seekpath at all) get_path is legitimately None after a correct restore, and the assertion would have turned a passing test into a failure. It now captures get_path before the reload and asserts identity afterwards: None is None on no-seekpath legs, a real check everywhere else. 2. The trigonal entry in the probe lattice tuple is Lattice.hexagonal(2, 95) built with hexagonal axes, so the "# rhombohedral" label was wrong; it now reads "# trigonal (hexagonal axes)". The lattice itself was correct. Verified locally on Windows, Python 3.13, pymatgen-core 2026.8.30, seekpath 2.2.1, spglib 2.7.0: tests/io/pwmat/test_inputs.py + tests/symmetry/test_kpath_hin.py + tests/symmetry/test_kpaths.py 26 passed ruff check and ruff format clean on both touched files. * Fix the restore assertion and a probe comment label Per re-review: 1. The restore check asserted kpath.get_path is not None, which reads as "seekpath is installed" when what it means is "the reload was undone". On the two legs that do not install the optional extra (no seekpath at all) get_path is legitimately None after a correct restore, and the assertion would have turned a passing test into a failure. It now captures get_path before the reload and asserts identity afterwards: None is None on no-seekpath legs, a real check everywhere else. 2. The trigonal entry in the probe lattice tuple is Lattice.hexagonal(2, 95) built with hexagonal axes, so the "# rhombohedral" label was wrong; it now reads "# trigonal (hexagonal axes)". The lattice itself was correct. Verified locally on Windows, Python 3.13, pymatgen-core 2026.8.30, seekpath 2.2.1, spglib 2.7.0: tests/io/pwmat/test_inputs.py + tests/symmetry/test_kpath_hin.py + tests/symmetry/test_kpaths.py 26 passed ruff check and ruff format clean on both touched files. * tests: probe seekpath with a single cubic cell Per review: the seven-lattice loop always resolved P1 (general-position sites), so it paid 7x for 1x coverage. Drop to one cubic cell and document that the probe only checks KPathSeek is callable; note why the cache is populated at collection time. * tests: align seekpath probe with review (in-cell coords, get_path check) - Fix probe fractional coords to in-cell values with to_unit_cell=True - Detect unavailability via kpath.get_path is None instead of message text - Drop @functools.cache (finally-restore removes the collection-order need) - Condense reload comments in test_err_msg_on_seekpath_not_installed * tests: evaluate the seekpath probe lazily via a module-scoped fixture seekpath_unusable_reason() was called at module scope in test_kpaths.py and test_kpath_hin.py, so a probe failure outside the known unavailability paths aborted collection of the whole file — including tests that never touch seekpath. A module-scoped `seekpath_usable` fixture in tests/symmetry/conftest.py keeps the re-raise intent but confines the error to tests that need seekpath. Also assert that the pwmat.inputs reload in test_err_msg_on_seekpath_not_installed rebinds KPathSeek to the restored module's class, closing the loop on both reloads. --------- Co-authored-by: Shyue Ping Ong <shyuep@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
I have been trying to update the requirements of Euphonic to a recent seekpath and found that some combinations of seekpath and spglib were giving different error messages than expected when the symmetry cannot be detected. Specifically the behaviour is encountered when using seekpath 2.2 with older versions of spglib.
get_dot_access_datasetis a wrapper designed to convert old-style (dictionary access) spglib results to new-style (class attribute access) objects. Currently it does not consider the case that it is passed "None", which is the error signal for spglib<2.7 so if the wrappedspglibcall returns None it can raise something likeI think this means the "if dataset is None" code block in
seekpath.hpkot.__init__has become unreachable. To have this work as expected, this PR allowsNoneresults to pass through the wrapper unchanged.Here is a
uvscript (i.e. Python with dependencies declared in header) that reproduces the behaviour. You can get different results by changing the seekpath and spglib versions!