Skip to content

fix(presets): guard non-list/non-mapping provides.templates in PresetManifest#3712

Open
jawwad-ali wants to merge 2 commits into
github:mainfrom
jawwad-ali:fix/preset-manifest-templates-type-guard
Open

fix(presets): guard non-list/non-mapping provides.templates in PresetManifest#3712
jawwad-ali wants to merge 2 commits into
github:mainfrom
jawwad-ali:fix/preset-manifest-templates-type-guard

Conversation

@jawwad-ali

Copy link
Copy Markdown
Contributor

Problem

PresetManifest._validate iterates provides["templates"] with no shape guards, unlike the direct sibling ExtensionManifest._validate (which guards both the container and each entry). A malformed third-party preset.yml therefore crashes with a raw TypeError rather than a clean validation error:

templates: 5        →  TypeError: 'int' object is not iterable
templates: [null]   →  TypeError: argument of type 'NoneType' is not iterable
templates: [5]      →  TypeError: argument of type 'int' is not iterable

The install handler (presets/_commands.py) only catches PresetCompatibilityError / PresetValidationError / PresetError, so the TypeError escapes all three and dumps an unhandled Python traceback to the user on specify 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 sibling ExtensionManifest return clean ValidationErrors — confirming the parity gap.

Fix

Add the two guards ExtensionManifest already has, raising PresetValidationError:

templates = provides["templates"]
if not isinstance(templates, list):
    raise PresetValidationError("Invalid provides.templates: expected a list")
for tmpl in templates:
    if not isinstance(tmpl, dict):
        raise PresetValidationError("Each template entry in 'provides.templates' must be a mapping")
    ...

Valid manifests (a list of mappings) pass both guards unchanged — no behaviour change for correct input.

Verification

  • New test_non_list_templates_raises_validation_error + parametrized test_non_mapping_template_entry_raises_validation_error (None/5/"oops"/["nested"]): fail before (raw TypeError or the misleading "Template missing…" message), pass after (clean PresetValidationError). Mirrors the existing test_non_mapping_yaml_raises_validation_error precedent.
  • Full TestPresetManifest: 24 passed. ruff clean.

AI-assisted: authored with Claude Code. I reproduced the raw TypeErrors on main and confirmed the sibling ExtensionManifest already returns clean errors for the identical shapes.

…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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Improves preset manifest validation so malformed template declarations raise controlled errors.

Changes:

  • Validates provides.templates container 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

Comment thread src/specify_cli/presets/__init__.py
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>
@jawwad-ali

Copy link
Copy Markdown
Contributor Author

Correct, and a good catch — fixed in 7612936. My original test used templates: 5 (truthy), which is exactly why it passed while every falsy non-list still hit the misleading message.

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 test_no_templates_provided is unaffected. I added a regression guard for that, and parametrized the non-list test over truthy (5, "oops", {"a": 1}) and falsy (0, False, None, "", {}) values — the 5 falsy cases fail before this commit.

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.

3 participants