Skip to content

fix(presets): tolerate non-string and non-list catalog fields in preset search/info - #3769

Open
Noor-ul-ain001 wants to merge 1 commit into
github:mainfrom
Noor-ul-ain001:fix/preset-search-nonstring-fields
Open

fix(presets): tolerate non-string and non-list catalog fields in preset search/info#3769
Noor-ul-ain001 wants to merge 1 commit into
github:mainfrom
Noor-ul-ain001:fix/preset-search-nonstring-fields

Conversation

@Noor-ul-ain001

Copy link
Copy Markdown
Contributor

Problem

preset search and preset info crash with a raw Python traceback on catalog payloads that are valid YAML/JSON but not string-typed. Catalog files are user-editable, so these shapes reach the code unvalidated.

PresetCatalog.search had three unguarded assumptions:

Input Result before this PR
author: 789 + --author 789 AttributeError: 'int' object has no attribute 'lower'
name: 123 / description: 456 + a query TypeError: sequence item 0: expected str instance, int found
tags: 5 (scalar, not a list) + --tag ci TypeError: 'int' object is not iterable

Reproduced directly against PresetCatalog.search:

author filter: AttributeError: 'int' object has no attribute 'lower'
query filter: TypeError: sequence item 0: expected str instance, int found
tag filter:   TypeError: 'int' object is not iterable

#3743 fixed only the non-string elements of tags in this method. A non-list tags, and the author / name / description fields, were still unguarded.

Fix

The sibling catalogs already handle every one of these cases — extensions/__init__.py::search and integrations/catalog.py::search coerce with str(...) and gate on isinstance(raw_tags, list). This PR aligns presets with that existing reference pattern rather than inventing a new one.

The same scalar-tags crash also reached the four display sites in presets/_commands.py, which now gate on isinstance(tags, list), matching integrations/_query_commands.py:331.

That includes the local-manifest branch of preset info: PresetManifest.tags returns self.data.get("tags", []) and manifest validation does not enforce list-ness, so a local preset.yaml containing tags: 5 validates successfully and then crashed the display — the same validate-accepts / render-crashes split seen in prior fixes.

While here: preset search printed tags unescaped, so a tag containing [bold] was silently swallowed as a Rich style tag. It now routes through _escape_markup, like the preset list line directly above it.

Tests

Five regression tests added to TestPresetTagsNonString, all driving the full CLI path via CliRunner (testing only the display helper would have missed the backend search crashes):

  • test_search_by_author_tolerates_non_string_author
  • test_search_query_tolerates_non_string_name_and_description
  • test_search_tolerates_non_list_tags
  • test_info_tolerates_non_list_tags
  • test_search_escapes_rich_markup_in_tags

Each fails before the fix with the exact exception it targets, verified by stashing the source changes:

FAILED ... test_search_by_author_tolerates_non_string_author
  → AttributeError("'int' object has no attribute 'lower'")
FAILED ... test_search_query_tolerates_non_string_name_and_description
  → TypeError('sequence item 0: expected str instance, int found')
FAILED ... test_search_tolerates_non_list_tags
  → TypeError("'int' object is not iterable")
FAILED ... test_info_tolerates_non_list_tags
  → TypeError("'int' object is not iterable")
FAILED ... test_search_escapes_rich_markup_in_tags
5 failed, 2 passed

tests/test_presets.py after the fix: 462 passed, 7 failed. The 7 failures are identical on clean main (457 passed, 7 failed) — pre-existing TestPresetSkills symlink cases that need elevation on Windows. Net effect is +5 passing, no new failures.

🤖 Generated with Claude Code

…et search/info

`preset search` and `preset info` crashed with a raw traceback on catalog
payloads that are valid YAML/JSON but not string-typed. Catalog files are
user-editable, so these shapes reach the code unvalidated.

`PresetCatalog.search` had three unguarded assumptions:

- `--author` called `.lower()` on the raw value → `AttributeError: 'int'
  object has no attribute 'lower'` for `author: 789`.
- the query searchable-text join passed raw `name`/`description` through →
  `TypeError: sequence item 0: expected str instance, int found`.
- the `--tag` filter iterated `tags` without a list check, so a scalar
  `tags: 5` (truthy, not iterable) raised `TypeError: 'int' object is not
  iterable`.

PR github#3743 fixed only the non-string *elements* of `tags` here; a non-list
`tags` and the `author`/`name`/`description` fields were still unguarded.
The sibling catalogs already handle all of these — `extensions/__init__.py`
and `integrations/catalog.py` coerce with `str(...)` and gate on
`isinstance(raw_tags, list)`. This aligns presets with them.

The same scalar-`tags` crash reached the four display sites in
`presets/_commands.py`, so those now gate on `isinstance(tags, list)`,
matching `integrations/_query_commands.py`. Note `PresetManifest.tags`
returns `self.data.get("tags", [])` and manifest validation does not
enforce list-ness, so a local `preset.yaml` with `tags: 5` validates
successfully and then crashed `preset info` — hence the guard on the
local-manifest branch too.

While here, `preset search` printed tags unescaped, so a tag containing
`[bold]` was silently swallowed as a Rich style tag; it now routes through
`_escape_markup` like the `preset list` line directly above it.

Regression tests in `TestPresetTagsNonString` drive the full CLI path via
CliRunner. All five fail before the fix, each with the exact exception it
targets.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Assisted-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant