From 7d7cddb0f81b5b33671d422f28d5734cc723eef6 Mon Sep 17 00:00:00 2001 From: Arthur031221 Date: Thu, 1 Oct 2026 09:09:53 +0800 Subject: [PATCH 1/6] Fix input_path resolving empty filenames to the working directory UnstructuredMesh.from_hdf5 and DAGMCUniverse.from_hdf5 pass an empty string for filename when the HDF5 group has no stored filename, which happens for meshes and DAGMC universes built in memory and never backed by a file. input_path then resolved that empty string to the current working directory instead of leaving it empty, so filename silently became an arbitrary real path instead of indicating no file. input_path now returns an empty Path unresolved when given an empty filename, matching the from_hdf5 round-trip tests added in #4118. --- openmc/utility_funcs.py | 7 +++++++ tests/unit_tests/test_utility_funcs.py | 20 ++++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 tests/unit_tests/test_utility_funcs.py diff --git a/openmc/utility_funcs.py b/openmc/utility_funcs.py index 9d397a89bab..3cc1f871da9 100644 --- a/openmc/utility_funcs.py +++ b/openmc/utility_funcs.py @@ -69,6 +69,13 @@ def input_path(filename: PathLike) -> Path: Path object """ + # An empty filename represents the absence of a path (e.g., a mesh or + # DAGMC universe read back from a summary/statepoint file that was never + # written with a source file) and should not be resolved to the current + # working directory. + if not str(filename): + return Path(filename) + if openmc.config['resolve_paths']: path = Path(filename) xml_dir = _XML_INPUT_PATH.get() diff --git a/tests/unit_tests/test_utility_funcs.py b/tests/unit_tests/test_utility_funcs.py new file mode 100644 index 00000000000..77b28df3449 --- /dev/null +++ b/tests/unit_tests/test_utility_funcs.py @@ -0,0 +1,20 @@ +from pathlib import Path + +import openmc +from openmc.utility_funcs import input_path + + +def test_input_path_empty_not_resolved(): + """An empty filename represents "no file" (e.g., an in-memory mesh or + DAGMC universe read back from a summary/statepoint that was never + written with a source file, see UnstructuredMesh.from_hdf5 and + DAGMCUniverse.from_hdf5) and should stay empty rather than being + resolved to the current working directory.""" + with openmc.config.patch('resolve_paths', True): + assert input_path('') == Path() + + +def test_input_path_nonempty_resolved(tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + with openmc.config.patch('resolve_paths', True): + assert input_path('foo.h5') == (tmp_path / 'foo.h5').resolve() From 6c2ba5d5f17d1ff0d8354590c9e929aa6db6ba47 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Sat, 3 Oct 2026 17:25:13 -0500 Subject: [PATCH 2/6] Normalize PathLike inputs before checking empty filenames Use the filesystem protocol rather than string formatting to inspect input filenames. An os.PathLike object can provide an empty filesystem path while its string representation is nonempty, causing the new guard to resolve a missing filename to the working or XML input directory. Normalize with os.fspath and cover empty and nonempty inputs with path resolution enabled and disabled, including an XML input context. --- openmc/utility_funcs.py | 3 +- tests/unit_tests/test_utility_funcs.py | 48 ++++++++++++++++++++++---- 2 files changed, 43 insertions(+), 8 deletions(-) diff --git a/openmc/utility_funcs.py b/openmc/utility_funcs.py index 3cc1f871da9..487f498936b 100644 --- a/openmc/utility_funcs.py +++ b/openmc/utility_funcs.py @@ -73,7 +73,8 @@ def input_path(filename: PathLike) -> Path: # DAGMC universe read back from a summary/statepoint file that was never # written with a source file) and should not be resolved to the current # working directory. - if not str(filename): + filename = os.fspath(filename) + if not filename: return Path(filename) if openmc.config['resolve_paths']: diff --git a/tests/unit_tests/test_utility_funcs.py b/tests/unit_tests/test_utility_funcs.py index 77b28df3449..3e764a91a92 100644 --- a/tests/unit_tests/test_utility_funcs.py +++ b/tests/unit_tests/test_utility_funcs.py @@ -1,20 +1,54 @@ from pathlib import Path +import pytest + import openmc -from openmc.utility_funcs import input_path +from openmc.utility_funcs import input_path, set_xml_input_path + + +class _PathLike: + def __init__(self, filename): + self.filename = filename + + def __fspath__(self): + return self.filename -def test_input_path_empty_not_resolved(): +@pytest.mark.parametrize('filename', ['', _PathLike('')]) +@pytest.mark.parametrize('resolve_paths', [True, False]) +@pytest.mark.parametrize('xml_context', [True, False]) +def test_input_path_empty_not_resolved( + filename, resolve_paths, xml_context, tmp_path +): """An empty filename represents "no file" (e.g., an in-memory mesh or DAGMC universe read back from a summary/statepoint that was never written with a source file, see UnstructuredMesh.from_hdf5 and DAGMCUniverse.from_hdf5) and should stay empty rather than being resolved to the current working directory.""" - with openmc.config.patch('resolve_paths', True): - assert input_path('') == Path() + with openmc.config.patch('resolve_paths', resolve_paths): + if xml_context: + with set_xml_input_path(tmp_path / 'model' / 'model.xml'): + assert input_path(filename) == Path() + else: + assert input_path(filename) == Path() -def test_input_path_nonempty_resolved(tmp_path, monkeypatch): +@pytest.mark.parametrize('filename', [ + 'foo.h5', Path('foo.h5'), _PathLike('foo.h5') +]) +@pytest.mark.parametrize('resolve_paths', [True, False]) +@pytest.mark.parametrize('xml_context', [True, False]) +def test_input_path_nonempty_resolved( + filename, resolve_paths, xml_context, tmp_path, monkeypatch +): monkeypatch.chdir(tmp_path) - with openmc.config.patch('resolve_paths', True): - assert input_path('foo.h5') == (tmp_path / 'foo.h5').resolve() + with openmc.config.patch('resolve_paths', resolve_paths): + if xml_context: + with set_xml_input_path(tmp_path / 'model' / 'model.xml'): + expected = tmp_path / 'model' / 'foo.h5' + if not resolve_paths: + expected = Path('foo.h5') + assert input_path(filename) == expected + else: + expected = tmp_path / 'foo.h5' if resolve_paths else Path('foo.h5') + assert input_path(filename) == expected From bdee1d42063fc7c0a39d58a793b2b5eed8b8dc95 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Sat, 3 Oct 2026 17:26:10 -0500 Subject: [PATCH 3/6] Exercise filename-less HDF5 readers with path resolution The session fixture disables path resolution, so the existing mesh and DAGMC reader tests do not catch empty filenames resolving to the working directory under the default configuration. Parameterize both tests over the enabled and disabled settings and patch configuration while reading the objects. The enabled cases fail against the pre-PR helper. --- tests/unit_tests/test_mesh.py | 6 ++++-- tests/unit_tests/test_universe.py | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/unit_tests/test_mesh.py b/tests/unit_tests/test_mesh.py index c6ebd687f7e..11402076d62 100644 --- a/tests/unit_tests/test_mesh.py +++ b/tests/unit_tests/test_mesh.py @@ -457,7 +457,8 @@ def test_umesh_interface_hdf5(tmp_path, interface): assert mesh.interface == interface -def test_umesh_from_hdf5_without_filename(run_in_tmpdir): +@pytest.mark.parametrize('resolve_paths', [True, False]) +def test_umesh_from_hdf5_without_filename(run_in_tmpdir, resolve_paths): """An in-memory unstructured mesh has no source filename.""" with h5py.File('mesh.h5', 'w') as f: group = f.create_group('mesh 1') @@ -468,7 +469,8 @@ def test_umesh_from_hdf5_without_filename(run_in_tmpdir): group['connectivity'] = np.zeros((1, 8), dtype=int) group['element_types'] = [0] - mesh = openmc.MeshBase.from_hdf5(group) + with openmc.config.patch('resolve_paths', resolve_paths): + mesh = openmc.MeshBase.from_hdf5(group) assert mesh.filename == Path() assert mesh.has_statepoint_data diff --git a/tests/unit_tests/test_universe.py b/tests/unit_tests/test_universe.py index 8b3e07a773a..e5653d70ecc 100644 --- a/tests/unit_tests/test_universe.py +++ b/tests/unit_tests/test_universe.py @@ -189,7 +189,8 @@ def test_get_all_universes(): assert not (univs ^ {u1, u2, u3}) -def test_dagmc_universe_from_hdf5_without_filename(run_in_tmpdir): +@pytest.mark.parametrize('resolve_paths', [True, False]) +def test_dagmc_universe_from_hdf5_without_filename(run_in_tmpdir, resolve_paths): """A DAGMC universe built from an in-memory MOAB instance has no filename.""" with h5py.File('summary.h5', 'w') as f: group = f.create_group('universe 1') @@ -198,7 +199,8 @@ def test_dagmc_universe_from_hdf5_without_filename(run_in_tmpdir): group.attrs['auto_mat_ids'] = 1 group.attrs['length_multiplier'] = 1.0 - univ = openmc.DAGMCUniverse.from_hdf5(group) + with openmc.config.patch('resolve_paths', resolve_paths): + univ = openmc.DAGMCUniverse.from_hdf5(group) assert univ.id == 1 assert univ.filename == Path() From eb641957019b9289a7d180128444dcf12d262133 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Sat, 3 Oct 2026 17:35:57 -0500 Subject: [PATCH 4/6] Simplify path helper and filename-less reader tests Replace the input-type, resolution-mode, and XML-context cross product with focused cases for empty filenames, ordinary paths, and XML-relative resolution. Keep one custom PathLike case for the filesystem-protocol regression. Exercise each HDF5 reader under the default enabled path resolution setting without duplicating its disabled-setting case. --- tests/unit_tests/test_mesh.py | 5 +- tests/unit_tests/test_universe.py | 5 +- tests/unit_tests/test_utility_funcs.py | 66 +++++++++----------------- 3 files changed, 26 insertions(+), 50 deletions(-) diff --git a/tests/unit_tests/test_mesh.py b/tests/unit_tests/test_mesh.py index 11402076d62..5e87df3a66f 100644 --- a/tests/unit_tests/test_mesh.py +++ b/tests/unit_tests/test_mesh.py @@ -457,8 +457,7 @@ def test_umesh_interface_hdf5(tmp_path, interface): assert mesh.interface == interface -@pytest.mark.parametrize('resolve_paths', [True, False]) -def test_umesh_from_hdf5_without_filename(run_in_tmpdir, resolve_paths): +def test_umesh_from_hdf5_without_filename(run_in_tmpdir): """An in-memory unstructured mesh has no source filename.""" with h5py.File('mesh.h5', 'w') as f: group = f.create_group('mesh 1') @@ -469,7 +468,7 @@ def test_umesh_from_hdf5_without_filename(run_in_tmpdir, resolve_paths): group['connectivity'] = np.zeros((1, 8), dtype=int) group['element_types'] = [0] - with openmc.config.patch('resolve_paths', resolve_paths): + with openmc.config.patch('resolve_paths', True): mesh = openmc.MeshBase.from_hdf5(group) assert mesh.filename == Path() diff --git a/tests/unit_tests/test_universe.py b/tests/unit_tests/test_universe.py index e5653d70ecc..cc13950b7bc 100644 --- a/tests/unit_tests/test_universe.py +++ b/tests/unit_tests/test_universe.py @@ -189,8 +189,7 @@ def test_get_all_universes(): assert not (univs ^ {u1, u2, u3}) -@pytest.mark.parametrize('resolve_paths', [True, False]) -def test_dagmc_universe_from_hdf5_without_filename(run_in_tmpdir, resolve_paths): +def test_dagmc_universe_from_hdf5_without_filename(run_in_tmpdir): """A DAGMC universe built from an in-memory MOAB instance has no filename.""" with h5py.File('summary.h5', 'w') as f: group = f.create_group('universe 1') @@ -199,7 +198,7 @@ def test_dagmc_universe_from_hdf5_without_filename(run_in_tmpdir, resolve_paths) group.attrs['auto_mat_ids'] = 1 group.attrs['length_multiplier'] = 1.0 - with openmc.config.patch('resolve_paths', resolve_paths): + with openmc.config.patch('resolve_paths', True): univ = openmc.DAGMCUniverse.from_hdf5(group) assert univ.id == 1 diff --git a/tests/unit_tests/test_utility_funcs.py b/tests/unit_tests/test_utility_funcs.py index 3e764a91a92..bb1b260c0e8 100644 --- a/tests/unit_tests/test_utility_funcs.py +++ b/tests/unit_tests/test_utility_funcs.py @@ -6,49 +6,27 @@ from openmc.utility_funcs import input_path, set_xml_input_path -class _PathLike: - def __init__(self, filename): - self.filename = filename - +class _EmptyPath: def __fspath__(self): - return self.filename - - -@pytest.mark.parametrize('filename', ['', _PathLike('')]) -@pytest.mark.parametrize('resolve_paths', [True, False]) -@pytest.mark.parametrize('xml_context', [True, False]) -def test_input_path_empty_not_resolved( - filename, resolve_paths, xml_context, tmp_path -): - """An empty filename represents "no file" (e.g., an in-memory mesh or - DAGMC universe read back from a summary/statepoint that was never - written with a source file, see UnstructuredMesh.from_hdf5 and - DAGMCUniverse.from_hdf5) and should stay empty rather than being - resolved to the current working directory.""" - with openmc.config.patch('resolve_paths', resolve_paths): - if xml_context: - with set_xml_input_path(tmp_path / 'model' / 'model.xml'): - assert input_path(filename) == Path() - else: - assert input_path(filename) == Path() - - -@pytest.mark.parametrize('filename', [ - 'foo.h5', Path('foo.h5'), _PathLike('foo.h5') -]) -@pytest.mark.parametrize('resolve_paths', [True, False]) -@pytest.mark.parametrize('xml_context', [True, False]) -def test_input_path_nonempty_resolved( - filename, resolve_paths, xml_context, tmp_path, monkeypatch -): + return '' + + +@pytest.mark.parametrize('filename', ['', _EmptyPath()]) +def test_input_path_empty_not_resolved(filename): + """Missing filenames stay unresolved, including path-like inputs.""" + with openmc.config.patch('resolve_paths', True): + assert input_path(filename) == Path() + + +@pytest.mark.parametrize('filename', ['foo.h5', Path('foo.h5')]) +def test_input_path_nonempty_resolved(filename, tmp_path, monkeypatch): monkeypatch.chdir(tmp_path) - with openmc.config.patch('resolve_paths', resolve_paths): - if xml_context: - with set_xml_input_path(tmp_path / 'model' / 'model.xml'): - expected = tmp_path / 'model' / 'foo.h5' - if not resolve_paths: - expected = Path('foo.h5') - assert input_path(filename) == expected - else: - expected = tmp_path / 'foo.h5' if resolve_paths else Path('foo.h5') - assert input_path(filename) == expected + with openmc.config.patch('resolve_paths', True): + assert input_path(filename) == tmp_path / 'foo.h5' + + +def test_input_path_xml_context(tmp_path): + with openmc.config.patch('resolve_paths', True): + with set_xml_input_path(tmp_path / 'model.xml'): + assert input_path('') == Path() + assert input_path('foo.h5') == tmp_path / 'foo.h5' From 051c6ddc273529327e1381ff7207ec83b09430ea Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Sat, 3 Oct 2026 17:44:52 -0500 Subject: [PATCH 5/6] Revert some test changes --- tests/unit_tests/test_utility_funcs.py | 32 ++++++++------------------ 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/tests/unit_tests/test_utility_funcs.py b/tests/unit_tests/test_utility_funcs.py index bb1b260c0e8..77b28df3449 100644 --- a/tests/unit_tests/test_utility_funcs.py +++ b/tests/unit_tests/test_utility_funcs.py @@ -1,32 +1,20 @@ from pathlib import Path -import pytest - import openmc -from openmc.utility_funcs import input_path, set_xml_input_path - - -class _EmptyPath: - def __fspath__(self): - return '' +from openmc.utility_funcs import input_path -@pytest.mark.parametrize('filename', ['', _EmptyPath()]) -def test_input_path_empty_not_resolved(filename): - """Missing filenames stay unresolved, including path-like inputs.""" +def test_input_path_empty_not_resolved(): + """An empty filename represents "no file" (e.g., an in-memory mesh or + DAGMC universe read back from a summary/statepoint that was never + written with a source file, see UnstructuredMesh.from_hdf5 and + DAGMCUniverse.from_hdf5) and should stay empty rather than being + resolved to the current working directory.""" with openmc.config.patch('resolve_paths', True): - assert input_path(filename) == Path() + assert input_path('') == Path() -@pytest.mark.parametrize('filename', ['foo.h5', Path('foo.h5')]) -def test_input_path_nonempty_resolved(filename, tmp_path, monkeypatch): +def test_input_path_nonempty_resolved(tmp_path, monkeypatch): monkeypatch.chdir(tmp_path) with openmc.config.patch('resolve_paths', True): - assert input_path(filename) == tmp_path / 'foo.h5' - - -def test_input_path_xml_context(tmp_path): - with openmc.config.patch('resolve_paths', True): - with set_xml_input_path(tmp_path / 'model.xml'): - assert input_path('') == Path() - assert input_path('foo.h5') == tmp_path / 'foo.h5' + assert input_path('foo.h5') == (tmp_path / 'foo.h5').resolve() From 61f644a2a6ce5f5944887ff31308880ddcd9b54b Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Sat, 3 Oct 2026 17:47:07 -0500 Subject: [PATCH 6/6] Fix broken import --- tests/unit_tests/weightwindows/test.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/unit_tests/weightwindows/test.py b/tests/unit_tests/weightwindows/test.py index 1d10f50ac3c..adb563c31a4 100644 --- a/tests/unit_tests/weightwindows/test.py +++ b/tests/unit_tests/weightwindows/test.py @@ -1,11 +1,10 @@ import os from itertools import product +from pathlib import Path import pytest - import numpy as np from uncertainties import ufloat - import openmc import openmc.lib from openmc.stats import Discrete, Point