From 0be4c3c8cbb6434512c5b9ee207be21dfb875aa9 Mon Sep 17 00:00:00 2001 From: Aman Sachan Date: Sun, 14 Jun 2026 01:11:08 +0000 Subject: [PATCH] fix: raise descriptive error for empty/comments-only config files ruamel.yaml's yaml.load() returns None for an empty or comments-only file. The very next line ("instruments" not in rawConfig) then evaluated "instruments" not in None and crashed with the opaque TypeError: argument of type 'NoneType' is not iterable. Guard for the None case right after the load() call and raise an AttributeError with a message in the same spirit as the existing missing-'instruments'-key error, so the user is told the file is empty and what section it needs. Fixes #139. --- src/instrumentserver/config.py | 11 +++++++++++ test/pytest/test_config.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/src/instrumentserver/config.py b/src/instrumentserver/config.py index f30d874..92bb950 100644 --- a/src/instrumentserver/config.py +++ b/src/instrumentserver/config.py @@ -47,6 +47,17 @@ def loadConfig( yaml = ruamel.yaml.YAML() rawConfig = yaml.load(configPath) + # ruamel.yaml returns ``None`` for an empty (or comments-only) config + # file. The membership test on the next line would then raise an opaque + # ``TypeError: argument of type 'NoneType' is not iterable``, so we + # surface a descriptive error that matches the style of the missing-key + # message below. + if rawConfig is None: + raise AttributeError( + f"The config file '{configPath}' is empty. " + "It needs at least an 'instruments:' section." + ) + if "instruments" not in rawConfig: raise AttributeError( "All configurations must be inside the 'instruments' field. " diff --git a/test/pytest/test_config.py b/test/pytest/test_config.py index 1a4e6e1..fce70b4 100644 --- a/test/pytest/test_config.py +++ b/test/pytest/test_config.py @@ -177,6 +177,39 @@ def test_missing_instruments_key_raises(tmp_path): loadConfig(cfg) +# --------------------------------------------------------------------------- +# Error: empty / comments-only config file +# --------------------------------------------------------------------------- + + +def test_empty_config_raises(tmp_path): + """An empty config file must not crash with an opaque TypeError.""" + cfg = _write_config(tmp_path, "") + with pytest.raises(AttributeError, match="empty"): + loadConfig(cfg) + + +def test_comments_only_config_raises(tmp_path): + """A config file that contains only comments also parses to ``None``.""" + cfg = _write_config( + tmp_path, + """\ +# this file is intentionally just comments +# nothing else +""", + ) + with pytest.raises(AttributeError, match="empty"): + loadConfig(cfg) + + +def test_empty_config_error_mentions_instruments(tmp_path): + """The empty-config error should guide the user toward the required key.""" + cfg = _write_config(tmp_path, "") + with pytest.raises(AttributeError) as excinfo: + loadConfig(cfg) + assert "instruments" in str(excinfo.value) + + # --------------------------------------------------------------------------- # pollingRate # ---------------------------------------------------------------------------