Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion src/specify_cli/bundler/models/catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,17 @@ def _merge_config(by_id: dict[str, CatalogSource], config_path: Path, scope: Sco
if not config_path.exists():
return
data = load_yaml(config_path)
catalogs = data.get("catalogs") if isinstance(data, dict) else None
if not isinstance(data, dict):
# A top-level non-mapping (a YAML list or scalar) is malformed. The
# sibling reader of the SAME file (commands_impl/catalog_config._read)
# raises here; #3623 already made the inner non-list `catalogs` value
# agree between the two readers, and this closes the remaining
# top-level-shape gap so both readers reject the same documents.
raise BundlerError(
f"Malformed catalog config at {config_path}: expected a mapping at "
f"the top level, got {type(data).__name__}."
)
catalogs = data.get("catalogs")
if catalogs is None:
return
if not isinstance(catalogs, list):
Expand Down
11 changes: 11 additions & 0 deletions tests/contract/test_catalog_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,17 @@ def test_falsy_non_list_catalogs_still_raises(tmp_path: Path, value: str):
load_source_stack(tmp_path)


@pytest.mark.parametrize("body", ["- a\n- b\n", "42\n"])
def test_toplevel_non_mapping_raises(tmp_path: Path, body: str):
"""A top-level non-mapping bundle-catalogs.yml (a YAML list or scalar) must
raise, matching the sibling reader (catalog_config._read) — not silently
fall back to the built-in default stack."""
make_project(tmp_path)
(tmp_path / ".specify" / "bundle-catalogs.yml").write_text(body, encoding="utf-8")
with pytest.raises(BundlerError, match="expected a mapping at the top level"):
load_source_stack(tmp_path)


@pytest.mark.parametrize("body", ["catalogs:\n", "catalogs: []\n"])
def test_absent_or_empty_catalogs_is_noop(tmp_path: Path, body: str):
"""An absent (``None``) or empty-list ``catalogs:`` is valid: it contributes
Expand Down