fix(presets): guard non-list/non-mapping provides.templates in PresetManifest#3712
fix(presets): guard non-list/non-mapping provides.templates in PresetManifest#3712jawwad-ali wants to merge 2 commits into
Conversation
…Manifest PresetManifest._validate iterated provides["templates"] with no shape guards, unlike the sibling ExtensionManifest. A malformed third-party preset.yml crashed with a raw TypeError that escapes the install handler's PresetValidationError/PresetError catch and dumps an unhandled traceback: templates: 5 -> "'int' object is not iterable" templates: [null] -> "argument of type 'NoneType' is not iterable" templates: [5] -> "argument of type 'int' is not iterable" (and a string/list entry raised the misleading "Template missing 'type', 'name', or 'file'"). Add a container list-guard and a per-entry mapping-guard that raise a clean PresetValidationError, mirroring ExtensionManifest's provides.commands guards. Valid manifests (list of mappings) are unaffected. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Improves preset manifest validation so malformed template declarations raise controlled errors.
Changes:
- Validates
provides.templatescontainer and entry shapes. - Adds regression tests for malformed values.
Show a summary per file
| File | Description |
|---|---|
src/specify_cli/presets/__init__.py |
Adds template shape guards. |
tests/test_presets.py |
Tests invalid containers and entries. |
Review details
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 2/2 changed files
- Comments generated: 1
- Review effort level: Medium
Address review feedback: the new shape guard sat behind the existing truthiness
check, so a FALSY non-list (templates: 0/false/null/''/{}) still reported the
misleading "Preset must provide at least one template" instead of the type
error. Only truthy non-lists (5, "oops", {"a": 1}) reached the guard, which is
why the original test (templates: 5) passed.
Split the checks: presence -> container type -> emptiness. A falsy non-list now
reports "expected a list"; an EMPTY LIST keeps the "at least one template"
message, since that genuinely is a well-typed container with no templates.
Parametrize the non-list test over truthy AND falsy values, and add a
regression guard for the empty-list message.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Correct, and a good catch — fixed in 7612936. My original test used Reordered to presence → container type → emptiness: if "templates" not in provides: raise ...("must provide at least one template")
templates = provides["templates"]
if not isinstance(templates, list): raise ...("Invalid provides.templates: expected a list")
if not templates: raise ...("must provide at least one template")An empty list deliberately keeps the "at least one template" message — it genuinely is a well-typed container with no templates — so the pre-existing |
Problem
PresetManifest._validateiteratesprovides["templates"]with no shape guards, unlike the direct siblingExtensionManifest._validate(which guards both the container and each entry). A malformed third-partypreset.ymltherefore crashes with a rawTypeErrorrather than a clean validation error:The install handler (
presets/_commands.py) only catchesPresetCompatibilityError/PresetValidationError/PresetError, so theTypeErrorescapes all three and dumps an unhandled Python traceback to the user onspecify preset install. (A string/list entry is arguably worse — it slips past into the misleading"Template missing 'type', 'name', or 'file'".)Reproduced on
main(4d3a428): the same three malformed shapes fed to the siblingExtensionManifestreturn cleanValidationErrors — confirming the parity gap.Fix
Add the two guards
ExtensionManifestalready has, raisingPresetValidationError:Valid manifests (a list of mappings) pass both guards unchanged — no behaviour change for correct input.
Verification
test_non_list_templates_raises_validation_error+ parametrizedtest_non_mapping_template_entry_raises_validation_error(None/5/"oops"/["nested"]): fail before (rawTypeErroror the misleading "Template missing…" message), pass after (cleanPresetValidationError). Mirrors the existingtest_non_mapping_yaml_raises_validation_errorprecedent.TestPresetManifest: 24 passed.ruffclean.AI-assisted: authored with Claude Code. I reproduced the raw
TypeErrors onmainand confirmed the siblingExtensionManifestalready returns clean errors for the identical shapes.