Repository navigation
[Bug]: preset add crashes with AttributeError when a composing command's frontmatter is not a YAML mapping #5085
Description
Bug Description
When a preset command uses a composition strategy (append, prepend or wrap), PresetResolver.resolve_content parses the top layer's frontmatter with _parse_fm_yaml, which returns yaml.safe_load(...) or {} (src/specify_cli/presets/_resolver.py:918-921). If the frontmatter is valid YAML but not a mapping, for example a bare line of text, the result is a string. The code then calls top_fm.pop("strategy", None) (_resolver.py:934), which raises AttributeError. Installation calls resolve_content from _register_commands, and preset add handles only PresetError subclasses (src/specify_cli/presets/command_add.py:434-445), so the user gets a raw traceback.
Invalid YAML in the same position is tolerated, so only valid non-mapping YAML triggers the crash.
Steps to Reproduce
From a fresh clone after uv sync, save the following as repro.py and run uv run python -I repro.py:
import traceback
import os, pathlib, tempfile
from typer.testing import CliRunner
from specify_cli import app
base = pathlib.Path(tempfile.mkdtemp())
proj, src = base / "proj", base / "src"
(proj / ".specify" / "templates" / "commands").mkdir(parents=True)
(proj / ".specify" / "templates" / "commands" / "speckit.demo.md").write_text(
"---\ndescription: core\n---\nCORE BODY\n")
(src / "commands").mkdir(parents=True)
(src / "commands" / "speckit.demo.md").write_text("---\njust a note\n---\nAPPENDED\n")
(src / "preset.yml").write_text(
'schema_version: "1.0"\n'
'preset:\n id: p1\n name: P\n version: "1.0.0"\n description: d\n'
'requires:\n speckit_version: ">=0.0.1"\n'
'provides:\n templates:\n - type: command\n name: speckit.demo\n'
' file: commands/speckit.demo.md\n strategy: append\n')
os.chdir(proj)
r = CliRunner().invoke(app, ["preset", "add", "--dev", str(src)])
print("exit code:", r.exit_code)
print("exception:", repr(r.exception))
traceback.print_exception(r.exc_info[1])Expected Behavior
Non-mapping frontmatter should be handled like invalid frontmatter: treated as empty or reported as a preset validation error. CommandRegistrar.parse_frontmatter handles the same input by replacing a non-dict result with {} (src/specify_cli/agents.py:158-159), and the resolver already falls back to {} on yaml.YAMLError (_resolver.py:920-921). resolve_content documents its return as "Composed content string, or None if not found" (_resolver.py:755-756).
Actual Behavior
exit code: 1
exception: AttributeError("'str' object has no attribute 'pop'")
Specify CLI Version
1.2.4.dev0 (main at 27d14e2)
AI Agent
Not applicable
Operating System
Linux 6.18 (Ubuntu container)
Python Version
Python 3.12.3
Error Logs
Traceback (most recent call last):
...
File "src/specify_cli/presets/command_add.py", line 193, in preset_add
manifest = manager.install_from_directory(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "src/specify_cli/presets/_manager.py", line 445, in install_from_directory
registered_commands = self._register_commands(manifest, dest_dir)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "src/specify_cli/presets/_manager_commands.py", line 147, in _register_commands
composed = resolver.resolve_content(cmd["name"], "command")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "src/specify_cli/presets/_resolver.py", line 934, in resolve_content
top_fm.pop("strategy", None)
^^^^^^^^^^
AttributeError: 'str' object has no attribute 'pop'
(The leading frames are from Typer's test runner and are omitted.)
Additional Context
Adding an isinstance(..., dict) check in _parse_fm_yaml, as CommandRegistrar.parse_frontmatter already does, would make the two parsers consistent.
Related: #4552 guards frontmatter that is not a mapping in integrations/base.py. It does not touch presets/_resolver.py.
Also reproduced on macOS (Darwin 25.6, arm64) with Python 3.14.4 and uv run python -I repro.py against 27d14e2; the output matches the above.
AI Disclosure
Found and reproduced with Claude Code (model: Claude Opus 4.8), run autonomously under human supervision. The human reporter re-ran every reproduction above before filing.
Activity