Skip to content

Commit 9ca6848

Browse files
jawwad-aliclaude
andcommitted
fix(bundler): reject a top-level non-mapping bundle-catalogs.yml in _merge_config
_merge_config silently ignored a top-level non-mapping document (a YAML list or scalar) — `data.get("catalogs") if isinstance(data, dict) else None` made it fall through to the built-in default stack — while the sibling reader of the SAME file (commands_impl/catalog_config._read) raises "expected a mapping at the top level". #3623 already made the inner non-list `catalogs` value agree between the two readers; this closes the remaining top-level-shape gap so both readers reject the same malformed documents. An empty file (load_yaml coerces to {}), absent `catalogs`, and `catalogs: []` all remain no-ops. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 8db7228 commit 9ca6848

2 files changed

Lines changed: 22 additions & 1 deletion

File tree

src/specify_cli/bundler/models/catalog.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,17 @@ def _merge_config(by_id: dict[str, CatalogSource], config_path: Path, scope: Sco
250250
if not config_path.exists():
251251
return
252252
data = load_yaml(config_path)
253-
catalogs = data.get("catalogs") if isinstance(data, dict) else None
253+
if not isinstance(data, dict):
254+
# A top-level non-mapping (a YAML list or scalar) is malformed. The
255+
# sibling reader of the SAME file (commands_impl/catalog_config._read)
256+
# raises here; #3623 already made the inner non-list `catalogs` value
257+
# agree between the two readers, and this closes the remaining
258+
# top-level-shape gap so both readers reject the same documents.
259+
raise BundlerError(
260+
f"Malformed catalog config at {config_path}: expected a mapping at "
261+
f"the top level, got {type(data).__name__}."
262+
)
263+
catalogs = data.get("catalogs")
254264
if catalogs is None:
255265
return
256266
if not isinstance(catalogs, list):

tests/contract/test_catalog_schema.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,17 @@ def test_falsy_non_list_catalogs_still_raises(tmp_path: Path, value: str):
6868
load_source_stack(tmp_path)
6969

7070

71+
@pytest.mark.parametrize("body", ["- a\n- b\n", "42\n"])
72+
def test_toplevel_non_mapping_raises(tmp_path: Path, body: str):
73+
"""A top-level non-mapping bundle-catalogs.yml (a YAML list or scalar) must
74+
raise, matching the sibling reader (catalog_config._read) — not silently
75+
fall back to the built-in default stack."""
76+
make_project(tmp_path)
77+
(tmp_path / ".specify" / "bundle-catalogs.yml").write_text(body, encoding="utf-8")
78+
with pytest.raises(BundlerError, match="expected a mapping at the top level"):
79+
load_source_stack(tmp_path)
80+
81+
7182
@pytest.mark.parametrize("body", ["catalogs:\n", "catalogs: []\n"])
7283
def test_absent_or_empty_catalogs_is_noop(tmp_path: Path, body: str):
7384
"""An absent (``None``) or empty-list ``catalogs:`` is valid: it contributes

0 commit comments

Comments
 (0)