From 8d5e1c24d175b90264dfb528569129d241d101af Mon Sep 17 00:00:00 2001 From: Alex Castillo Date: Wed, 9 Sep 2026 13:36:10 -0400 Subject: [PATCH] Raise a clear error when fenn.yaml is empty or not a mapping Parser.load_configuration did 'self._args = yaml.safe_load(f)' then 'self._args["project"] = ...'. An empty config file makes safe_load return None, and a top-level list/scalar makes it a non-dict, so the next line raised a bare 'TypeError: NoneType object does not support item assignment' (or 'list indices must be integers') instead of telling the user their config is wrong. Both app.py and 'fenn grid' hit this. Validate the parsed value and raise ValueError with a message naming the file and the problem, mirroring _config_missing. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_018uqJtTJWawYLVA5EdpUmho --- fenn/parser.py | 19 +++++++++++++++-- tests/unit/test_parser.py | 43 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 tests/unit/test_parser.py diff --git a/fenn/parser.py b/fenn/parser.py index 221517d..2914c6b 100644 --- a/fenn/parser.py +++ b/fenn/parser.py @@ -45,6 +45,12 @@ def _config_missing(self) -> None: self._config_file, ) + def _config_invalid(self, detail: str) -> None: + message = f"Configuration file {self._config_file} is invalid: {detail}" + logger.exception(message) + + raise ValueError(message) + def load_configuration(self) -> Any: """Loads the YAML configuration into the _args dictionary.""" @@ -53,8 +59,17 @@ def load_configuration(self) -> Any: # File exists → load YAML with open(self._config_file) as f: - self._args = yaml.safe_load(f) - self._args["project"] = self._config_file.stem + loaded = yaml.safe_load(f) + + if loaded is None: + self._config_invalid("the file is empty") + if not isinstance(loaded, dict): + self._config_invalid( + f"the top level must be a mapping, got {type(loaded).__name__}" + ) + + self._args = loaded + self._args["project"] = self._config_file.stem return self._args diff --git a/tests/unit/test_parser.py b/tests/unit/test_parser.py new file mode 100644 index 0000000..8c7d147 --- /dev/null +++ b/tests/unit/test_parser.py @@ -0,0 +1,43 @@ +"""Tests for `fenn.parser.Parser.load_configuration`.""" + +import pytest + +from fenn.parser import Parser + + +@pytest.fixture(autouse=True) +def _reset_parser(): + Parser.reset() + yield + Parser.reset() + + +def test_load_configuration_reads_a_mapping(tmp_path): + cfg = tmp_path / "fenn.yaml" + cfg.write_text("logger:\n dir: logs\n") + + args = Parser(cfg).load_configuration() + + assert args["logger"] == {"dir": "logs"} + assert args["project"] == "fenn" + + +def test_load_configuration_missing_file_raises_file_not_found(tmp_path): + with pytest.raises(FileNotFoundError): + Parser(tmp_path / "missing.yaml").load_configuration() + + +def test_load_configuration_empty_file_raises_value_error(tmp_path): + cfg = tmp_path / "fenn.yaml" + cfg.write_text("") + + with pytest.raises(ValueError, match="empty"): + Parser(cfg).load_configuration() + + +def test_load_configuration_non_mapping_raises_value_error(tmp_path): + cfg = tmp_path / "fenn.yaml" + cfg.write_text("- just\n- a\n- list\n") + + with pytest.raises(ValueError, match="mapping"): + Parser(cfg).load_configuration()