Skip to content

Commit df879ae

Browse files
authored
fix(cfg_vanilla): treat empty-string solver_options_file as unset (#714)
shared_options() and apply_solver_specs() gated the load_solver_options_file() call on _hasit(cfg, ...), which returns True for any non-None value -- including "". Callers that default solver_options_file or {name}_solver_options_file to "" (e.g. as a YAML/JSON sentinel for "no file") were hitting FileNotFoundError: [Errno 2] No such file or directory: ''. Replace _hasit with a plain truthiness check on the path. None and "" are both treated as unset, matching conventional file-path-arg semantics. No other _hasit semantics changed. Adds regression tests in test_solver_options_layers.py for both the global flag and a per-spoke flag.
1 parent 265345b commit df879ae

2 files changed

Lines changed: 23 additions & 4 deletions

File tree

mpisppy/tests/test_solver_options_layers.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -910,6 +910,15 @@ def test_file_starting_at_iter_persists_until_next_starting_at_iter(self):
910910
finally:
911911
os.unlink(path)
912912

913+
def test_empty_string_solver_options_file_is_treated_as_unset(self):
914+
# Callers that default solver_options_file to "" (e.g. as a
915+
# YAML/JSON sentinel for "no file") should not trigger a file
916+
# load. Regression for FileNotFoundError on open("").
917+
cfg = _bare_cfg()
918+
cfg.solver_options_file = ""
919+
sh = shared_options(cfg)
920+
self.assertEqual(sh["solver_options_layers"], [])
921+
913922

914923
class TestSolverOptionsFilePerSpoke(unittest.TestCase):
915924
"""Per-spoke layers from (a) the global file's "spokes" sub-block
@@ -1030,11 +1039,21 @@ def test_spoke_inline_overrides_spoke_file(self):
10301039
opts["solver_options_layers"], 0)
10311040
self.assertEqual(folded["mipgap"], 0.001) # inline wins
10321041
self.assertEqual(folded["presolve"], 1) # file survives
1033-
1034-
10351042
finally:
10361043
os.unlink(path)
10371044

1045+
def test_empty_string_spoke_solver_options_file_is_treated_as_unset(self):
1046+
# Mirrors the top-level empty-string regression: a caller that
1047+
# sets --{name}-solver-options-file to "" should not trigger
1048+
# a file load.
1049+
cfg = _spoke_cfg("lagrangian")
1050+
cfg.lagrangian_solver_options_file = ""
1051+
sh = shared_options(cfg)
1052+
spoke = self._spoke_dict_from(sh)
1053+
apply_solver_specs("lagrangian", spoke, cfg) # must not raise
1054+
self.assertEqual(
1055+
spoke["opt_kwargs"]["options"]["solver_options_layers"], [])
1056+
10381057

10391058
class TestLagrangerDeprecation(unittest.TestCase):
10401059
"""lagranger_spoke() emits a DeprecationWarning at setup naming

mpisppy/utils/cfg_vanilla.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ def shared_options(cfg, is_hub=False):
9191
# axis 2: any CLI flags below override file entries at the same
9292
# predicate. Load and apply it first so the rest of the axis-2
9393
# chain can overlay on top.
94-
if _hasit(cfg, "solver_options_file"):
94+
if cfg.get("solver_options_file"): # treats None and "" as unset
9595
file_data = sputils.load_solver_options_file(cfg.solver_options_file)
9696
shoptions["iter0_solver_options"].update(file_data["default"])
9797
shoptions["iter0_solver_options"].update(file_data["iter0"])
@@ -175,7 +175,7 @@ def apply_solver_specs(name, spoke, cfg):
175175
options["iterk_solver_options"].update(spoke_file_blocks["iterk"])
176176
options["solver_options_layers"].extend(
177177
sputils.options_file_section_to_layers(spoke_file_blocks))
178-
if _hasit(cfg, name+"_solver_options_file"):
178+
if cfg.get(name+"_solver_options_file"): # treats None and "" as unset
179179
spoke_file_data = sputils.load_solver_options_file(
180180
cfg.get(name+"_solver_options_file"))
181181
# Per-spoke files only consume their own predicates; the

0 commit comments

Comments
 (0)