Skip to content

Commit 923e446

Browse files
jawwad-aliclaude
andcommitted
fix(bundler): reject non-list 'catalogs' in bundle-catalogs.yml with a clean error
_merge_config guarded only 'if not catalogs: return', so a non-empty scalar (catalogs: 5) passed through and raised a raw 'TypeError: int object is not iterable' from the loop below — escaping the module's BundlerError error contract. The sibling reader of the same file (commands_impl/catalog_config.py, used by 'bundle catalog list') already raises an actionable BundlerError for the identical mis-shape. Add the same isinstance(list) guard so both readers of bundle-catalogs.yml agree. Test: 'catalogs: 5' raises BundlerError('...must be a list...') (fails before: raw TypeError). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent d7699c3 commit 923e446

2 files changed

Lines changed: 22 additions & 0 deletions

File tree

src/specify_cli/bundler/models/catalog.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,16 @@ def _merge_config(by_id: dict[str, CatalogSource], config_path: Path, scope: Sco
253253
catalogs = data.get("catalogs") if isinstance(data, dict) else None
254254
if not catalogs:
255255
return
256+
if not isinstance(catalogs, list):
257+
# A non-empty scalar (``catalogs: 5``) passes the falsy check above and
258+
# would raise a raw ``TypeError: 'int' object is not iterable`` from the
259+
# loop below. Report the same actionable BundlerError the sibling reader
260+
# of this file raises (commands_impl/catalog_config.py) so both readers
261+
# of bundle-catalogs.yml agree.
262+
raise BundlerError(
263+
f"Malformed catalog config at {config_path}: 'catalogs' must be a "
264+
f"list, got {type(catalogs).__name__}."
265+
)
256266
for raw in catalogs:
257267
src = CatalogSource.from_dict(raw, scope)
258268
by_id[src.id] = src

tests/contract/test_catalog_schema.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,18 @@ def test_builtin_default_stack_when_no_config(tmp_path: Path):
4343
assert all(s.scope is Scope.BUILTIN for s in sources)
4444

4545

46+
def test_non_list_catalogs_raises_actionable_error(tmp_path: Path):
47+
"""A scalar ``catalogs:`` value raises a clean BundlerError, not a raw
48+
'int object is not iterable' TypeError — matching what the sibling reader
49+
(bundle catalog list) already reports for the same file."""
50+
make_project(tmp_path)
51+
(tmp_path / ".specify" / "bundle-catalogs.yml").write_text(
52+
"catalogs: 5\n", encoding="utf-8"
53+
)
54+
with pytest.raises(BundlerError, match="must be a list"):
55+
load_source_stack(tmp_path)
56+
57+
4658
def test_project_config_overrides_same_id(tmp_path: Path):
4759
make_project(tmp_path)
4860
config = {

0 commit comments

Comments
 (0)