Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.13.3
hooks:
- id: ruff
- id: ruff-check
args: [--fix, --unsafe-fixes]
- id: ruff-format

Expand All @@ -30,15 +30,15 @@ repos:
rev: v2.4.1
hooks:
- id: codespell
stages: [ commit-msg ]
exclude_types: [ html ]
additional_dependencies: [ tomli ] # needed to read pyproject.toml below py3.11
stages: [commit-msg]
exclude_types: [html]
additional_dependencies: [tomli] # needed to read pyproject.toml below py3.11

- repo: https://github.com/MarcoGorelli/cython-lint
rev: v0.17.0
hooks:
- id: cython-lint
args: [ --no-pycodestyle ]
args: [--no-pycodestyle]
- id: double-quote-cython-strings

- repo: https://github.com/adamchainz/blacken-docs
Expand All @@ -55,13 +55,13 @@ repos:
# MD033: no inline HTML
# MD041: first line in a file should be a top-level heading
# MD025: single title
args: [ --disable, MD013, MD024, MD025, MD033, MD041, "--" ]
args: [--disable, MD013, MD024, MD025, MD033, MD041, '--']

- repo: https://github.com/kynan/nbstripout
rev: 0.8.1
hooks:
- id: nbstripout
args: [ --drop-empty-cells, --keep-output ]
args: [--drop-empty-cells, --keep-output]

- repo: https://github.com/RobertCraigie/pyright-python
rev: v1.1.406
Expand Down
113 changes: 60 additions & 53 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,15 @@ authors = [
]
maintainers = [{ name = "Shyue Ping Ong" }]
readme = "README.md"
keywords = ["jit", "job", "just-in-time", "management", "vasp", "nwchem", "qchem"]
keywords = [
"jit",
"job",
"just-in-time",
"management",
"nwchem",
"qchem",
"vasp",
]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Science/Research",
Expand All @@ -38,8 +46,10 @@ requires-python = ">=3.10"
dependencies = ["monty>=2.0.6", "psutil", "ruamel.yaml>=0.15.6"]

[project.optional-dependencies]
matsci = ["pymatgen"] # Error handlers and jobs for materials simulations, e.g., VASP, Nwchem, qchem, etc.
gaussian = ["pymatgen", "matplotlib"]
matsci = [
"pymatgen",
] # Error handlers and jobs for materials simulations, e.g., VASP, Nwchem, qchem, etc.
gaussian = ["matplotlib", "pymatgen"]
error-statistics = ["sentry-sdk>=0.8.0"]

[project.scripts]
Expand All @@ -63,55 +73,56 @@ line-length = 120

[tool.ruff.lint]
select = [
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"D", # pydocstyle
"E", # pycodestyle error
"EXE", # flake8-executable
"F", # pyflakes
"FA", # flake8-future-annotations
"FLY", # flynt
"I", # isort
"ICN", # flake8-import-conventions
"ISC", # flake8-implicit-str-concat
"PD", # pandas-vet
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"D", # pydocstyle
"E", # pycodestyle error
"EXE", # flake8-executable
"F", # pyflakes
"FA", # flake8-future-annotations
"FLY", # flynt
"I", # isort
"ICN", # flake8-import-conventions
"ISC", # flake8-implicit-str-concat
"PD", # pandas-vet
"PERF", # perflint
"PIE", # flake8-pie
"PL", # pylint
"PT", # flake8-pytest-style
"PYI", # flakes8-pyi
"Q", # flake8-quotes
"RET", # flake8-return
"RSE", # flake8-raise
"RUF", # Ruff-specific rules
"SIM", # flake8-simplify
"PIE", # flake8-pie
"PL", # pylint
"PT", # flake8-pytest-style
"PYI", # flakes8-pyi
"Q", # flake8-quotes
"RET", # flake8-return
"RSE", # flake8-raise
"RUF", # Ruff-specific rules
"SIM", # flake8-simplify
"SLOT", # flake8-slots
"TCH", # flake8-type-checking
"TID", # tidy imports
"TID", # flake8-tidy-imports
"UP", # pyupgrade
"W", # pycodestyle warning
"YTT", # flake8-2020
"TCH", # flake8-type-checking
"TID", # tidy imports
"TID", # flake8-tidy-imports
"UP", # pyupgrade
"W", # pycodestyle warning
"YTT", # flake8-2020
]
ignore = [
"B023", # Function definition does not bind loop variable
"B028", # No explicit stacklevel keyword argument found
"B904", # Within an except clause, raise exceptions with ...
"C408", # unnecessary-collection-call
"B023", # Function definition does not bind loop variable
"B028", # No explicit stacklevel keyword argument found
"B904", # Within an except clause, raise exceptions with ...
"C408", # unnecessary-collection-call
"COM812",
"D105", # Missing docstring in magic method
"D205", # 1 blank line required between summary line and description
"D212", # Multi-line docstring summary should start at the first line
"D105", # Missing docstring in magic method
"D205", # 1 blank line required between summary line and description
"D212", # Multi-line docstring summary should start at the first line
"ISC001",
"PD011", # pandas-use-of-dot-values
"PD901", # pandas-df-variable-name
"PD011", # pandas-use-of-dot-values
"PD901", # pandas-df-variable-name
"PERF203", # try-except-in-loop
"PLR", # pylint refactor
"PLC0415", # import-outside-top-level (used for performance/optional deps)
"PLR", # pylint refactor
"PLW2901", # Outer for loop variable overwritten by inner assignment target
"PT013", # pytest-incorrect-pytest-import
"PT013", # pytest-incorrect-pytest-import
"PTH",
"RUF012", # Disable checks for mutable class args
"SIM105", # Use contextlib.suppress(OSError) instead of try-except-pass
"RUF012", # Disable checks for mutable class args
"SIM105", # Use contextlib.suppress(OSError) instead of try-except-pass
]
pydocstyle.convention = "google"
isort.split-on-trailing-comma = false
Expand Down Expand Up @@ -163,22 +174,18 @@ exclude = ["**/tests"]

[dependency-groups]
dev = [
"invoke>=2.2.0",
"mypy>=1.15.0",
"myst-parser>=4.0.1",
"pre-commit>=4.2.0",
"pymatgen>=2025.5.16",
"pytest>=8.3.5",
"pytest-cov>=6.0.0",
"mypy>=1.15.0",
"pytest>=8.3.5",
"ruff>=0.11.2",
"invoke>=2.2.0",
"sphinx>=8.1.3",
"myst-parser>=4.0.1",
"sphinx-markdown-builder>=0.6.8",
"sphinx>=8.1.3",
]
lint = [
"pre-commit>=4.2.0",
"mypy>=1.15.0",
"ruff>=0.11.2",
]
lint = ["mypy>=1.15.0", "pre-commit>=4.2.0", "ruff>=0.11.2"]

[tool.setuptools.package-data]
custodian = ["py.typed"]
2 changes: 1 addition & 1 deletion src/custodian/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ class tracked_lru_cache:
Allows Custodian to clear the cache after all the checks have been performed.
"""

cached_functions: ClassVar = set()
cached_functions: ClassVar[set] = set()

def __init__(self, func) -> None:
"""
Expand Down
5 changes: 4 additions & 1 deletion src/custodian/vasp/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1800,7 +1800,10 @@ def check(self, directory="./") -> bool:
if self.wall_time:
run_time = datetime.datetime.now() - self.start_time
total_secs = run_time.total_seconds()
outcar = load_outcar(os.path.join(directory, "OUTCAR"))
try:
outcar = load_outcar(os.path.join(directory, "OUTCAR"))
except Exception: # Can't perform check if Outcar not valid (e.g. file being written)
return False
if not self.electronic_step_stop:
# Determine max time per ionic step.
outcar.read_pattern({"timings": r"LOOP\+.+real time(.+)"}, postprocess=float)
Expand Down
2 changes: 1 addition & 1 deletion tests/qchem/test_jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -778,7 +778,7 @@ def test_OptFF(self) -> None:
QCInput.from_file(f"{TEST_DIR}/5690_frag18/mol.qin.freq_2").as_dict()
== QCInput.from_file(os.path.join(SCR_DIR, "mol.qin")).as_dict()
)
with pytest.raises(ValueError, match="ERROR: Can't deal with multiple neg frequencies yet! Exiting..."):
with pytest.raises(ValueError, match=r"ERROR: Can't deal with multiple neg frequencies yet! Exiting\.\.\."):
next(job)


Expand Down
18 changes: 18 additions & 0 deletions tests/vasp/test_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1016,6 +1016,24 @@ def test_check_and_correct(self) -> None:
assert content == "LABORT = .TRUE."
os.remove("STOPCAR")

def test_check_with_malformed_outcar(self, tmp_path: Path) -> None:
"""Test that WalltimeHandler.check() returns False on malformed OUTCAR.

This can happen when VASP is mid-write and the file contains incomplete
data like a standalone '-' instead of a float. See
https://github.com/materialsproject/pymatgen/issues/2251
"""
os.chdir(tmp_path)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@janosh: FYI that better practice here would be to do monkeypatch.chdir(tmp_path) to not modify the path of the main process in the test suite.

Copy link
Copy Markdown
Member

@Andrew-S-Rosen Andrew-S-Rosen Dec 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Never mind. I see this is addressed by the tearDown call and is done throughout the module.

Copy link
Copy Markdown
Member Author

@janosh janosh Dec 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

strongly agree. been meaning to overhaul the custodian test suite for a long time specifically to fix this


# Create a malformed OUTCAR that would cause parsing to fail
with open("OUTCAR", "w") as file:
file.write("Free energy of the ion-electron system (eV)\n")
file.write(" alpha Z PSCENC = -\n") # incomplete value

handler = WalltimeHandler(wall_time=3600, buffer_time=120)
# Should return False (not crash) when OUTCAR parsing fails
assert handler.check() is False

@classmethod
def tearDown(cls) -> None:
os.environ.pop("CUSTODIAN_WALLTIME_START", None)
Expand Down
2 changes: 0 additions & 2 deletions tests/vasp/test_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@
@pytest.fixture(autouse=True)
def _clear_tracked_cache() -> None:
"""Clear the cache of the stored functions between the tests."""
from custodian.utils import tracked_lru_cache

tracked_lru_cache.tracked_cache_clear()


Expand Down
4 changes: 2 additions & 2 deletions tests/vasp/test_jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ def test_terminate_exception_during_graceful_termination(self):
self.mock_process.terminate.side_effect = OSError("Permission denied")

# Act & Assert
with pytest.raises(OSError):
with pytest.raises(OSError, match="Permission denied"):
self.vasp_job.terminate()

self.mock_process.terminate.assert_called_once()
Expand All @@ -323,7 +323,7 @@ def test_terminate_exception_during_force_kill(self):
self.mock_process.kill.return_value = None

# Act & Assert
with pytest.raises(OSError):
with pytest.raises(OSError, match="Process not found"):
self.vasp_job.terminate()

self.mock_process.terminate.assert_called_once()
Expand Down
3 changes: 1 addition & 2 deletions tests/vasp/test_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,14 @@

import pytest

from custodian.utils import tracked_lru_cache
from custodian.vasp.validators import VaspAECCARValidator, VaspFilesValidator, VaspNpTMDValidator, VasprunXMLValidator
from tests.conftest import TEST_FILES


@pytest.fixture(autouse=True)
def _clear_tracked_cache() -> None:
"""Clear the cache of the stored functions between the tests."""
from custodian.utils import tracked_lru_cache

tracked_lru_cache.tracked_cache_clear()


Expand Down
Loading