Skip to content
Open

ts #2646

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## 2024-05-19 - Fast YAML Loading using C Extensions in PyYAML
**Learning:** PyYAML's default `safe_load` and `safe_dump` are written in pure Python. When the `libyaml` C library is available, PyYAML provides `CSafeLoader` and `CSafeDumper` which offer significantly faster YAML parsing and serialization. This is a critical performance detail in IO-bound CLI tools that heavily rely on parsing configuration files and manifests (like Spec Kit).
**Action:** Created `src/specify_cli/yaml_utils.py` to transparently select the fastest available YAML loaders and dumpers, gracefully falling back to pure Python if C extensions are unavailable. Always use these custom utility functions over `yaml.safe_load`/`yaml.safe_dump` directly.
24 changes: 13 additions & 11 deletions src/specify_cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@
import yaml
from pathlib import Path

from .yaml_utils import yaml_safe_load, yaml_dump

from packaging.version import InvalidVersion, Version
from typing import Any, Optional

Expand Down Expand Up @@ -3123,7 +3125,7 @@ def preset_catalog_add(
# Load existing config
if config_path.exists():
try:
config = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {}
config = yaml_safe_load(config_path.read_text(encoding="utf-8")) or {}
except Exception as e:
console.print(f"[red]Error:[/red] Failed to read {config_path}: {e}")
raise typer.Exit(1)
Expand Down Expand Up @@ -3151,7 +3153,7 @@ def preset_catalog_add(
})

config["catalogs"] = catalogs
config_path.write_text(yaml.dump(config, default_flow_style=False, sort_keys=False, allow_unicode=True), encoding="utf-8")
config_path.write_text(yaml_dump(config, default_flow_style=False, sort_keys=False, allow_unicode=True), encoding="utf-8")

install_label = "install allowed" if install_allowed else "discovery only"
console.print(f"\n[green]✓[/green] Added catalog '[bold]{name}[/bold]' ({install_label})")
Expand Down Expand Up @@ -3179,7 +3181,7 @@ def preset_catalog_remove(
raise typer.Exit(1)

try:
config = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {}
config = yaml_safe_load(config_path.read_text(encoding="utf-8")) or {}
except Exception:
console.print("[red]Error:[/red] Failed to read preset catalog config.")
raise typer.Exit(1)
Expand All @@ -3196,7 +3198,7 @@ def preset_catalog_remove(
raise typer.Exit(1)

config["catalogs"] = catalogs
config_path.write_text(yaml.dump(config, default_flow_style=False, sort_keys=False, allow_unicode=True), encoding="utf-8")
config_path.write_text(yaml_dump(config, default_flow_style=False, sort_keys=False, allow_unicode=True), encoding="utf-8")

console.print(f"[green]✓[/green] Removed catalog '{name}'")
if not catalogs:
Expand Down Expand Up @@ -3465,7 +3467,7 @@ def catalog_add(
# Load existing config
if config_path.exists():
try:
config = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {}
config = yaml_safe_load(config_path.read_text(encoding="utf-8")) or {}
except Exception as e:
console.print(f"[red]Error:[/red] Failed to read {config_path}: {e}")
raise typer.Exit(1)
Expand Down Expand Up @@ -3493,7 +3495,7 @@ def catalog_add(
})

config["catalogs"] = catalogs
config_path.write_text(yaml.dump(config, default_flow_style=False, sort_keys=False, allow_unicode=True), encoding="utf-8")
config_path.write_text(yaml_dump(config, default_flow_style=False, sort_keys=False, allow_unicode=True), encoding="utf-8")

install_label = "install allowed" if install_allowed else "discovery only"
console.print(f"\n[green]✓[/green] Added catalog '[bold]{name}[/bold]' ({install_label})")
Expand Down Expand Up @@ -3521,7 +3523,7 @@ def catalog_remove(
raise typer.Exit(1)

try:
config = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {}
config = yaml_safe_load(config_path.read_text(encoding="utf-8")) or {}
except Exception:
console.print("[red]Error:[/red] Failed to read catalog config.")
raise typer.Exit(1)
Expand All @@ -3538,7 +3540,7 @@ def catalog_remove(
raise typer.Exit(1)

config["catalogs"] = catalogs
config_path.write_text(yaml.dump(config, default_flow_style=False, sort_keys=False, allow_unicode=True), encoding="utf-8")
config_path.write_text(yaml_dump(config, default_flow_style=False, sort_keys=False, allow_unicode=True), encoding="utf-8")

console.print(f"[green]✓[/green] Removed catalog '{name}'")
if not catalogs:
Expand Down Expand Up @@ -4282,21 +4284,21 @@ def extension_update(
# 6. Validate extension ID from ZIP BEFORE modifying installation
# Handle both root-level and nested extension.yml (GitHub auto-generated ZIPs)
with zipfile.ZipFile(zip_path, "r") as zf:
import yaml
from .yaml_utils import yaml_safe_load
manifest_data = None
namelist = zf.namelist()

# First try root-level extension.yml
if "extension.yml" in namelist:
with zf.open("extension.yml") as f:
manifest_data = yaml.safe_load(f) or {}
manifest_data = yaml_safe_load(f) or {}
else:
# Look for extension.yml in a single top-level subdirectory
# (e.g., "repo-name-branch/extension.yml")
manifest_paths = [n for n in namelist if n.endswith("/extension.yml") and n.count("/") == 1]
if len(manifest_paths) == 1:
with zf.open(manifest_paths[0]) as f:
manifest_data = yaml.safe_load(f) or {}
manifest_data = yaml_safe_load(f) or {}

if manifest_data is None:
raise ValueError("Downloaded extension archive is missing 'extension.yml'")
Expand Down
5 changes: 3 additions & 2 deletions src/specify_cli/agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import re
from copy import deepcopy
import yaml
from .yaml_utils import yaml_safe_load, yaml_dump


def _build_agent_configs() -> dict[str, Any]:
Expand Down Expand Up @@ -80,7 +81,7 @@ def parse_frontmatter(content: str) -> tuple[dict, str]:
body = content[end_marker + 3 :].strip()

try:
frontmatter = yaml.safe_load(frontmatter_str) or {}
frontmatter = yaml_safe_load(frontmatter_str) or {}
except yaml.YAMLError:
frontmatter = {}

Expand All @@ -102,7 +103,7 @@ def render_frontmatter(fm: dict) -> str:
if not fm:
return ""

yaml_str = yaml.dump(
yaml_str = yaml_dump(
fm, default_flow_style=False, sort_keys=False, allow_unicode=True
)
return f"---\n{yaml_str}---\n"
Expand Down
22 changes: 11 additions & 11 deletions src/specify_cli/extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

import yaml
from packaging import version as pkg_version
from .yaml_utils import yaml_safe_load, yaml_safe_dump, yaml_dump
from packaging.specifiers import SpecifierSet, InvalidSpecifier

_FALLBACK_CORE_COMMAND_NAMES = frozenset({
Expand Down Expand Up @@ -140,7 +141,7 @@ def _load_yaml(self, path: Path) -> dict:
"""Load YAML file safely."""
try:
with open(path, 'r') as f:
data = yaml.safe_load(f)
data = yaml_safe_load(f)
except yaml.YAMLError as e:
raise ValidationError(f"Invalid YAML in {path}: {e}")
except FileNotFoundError:
Expand Down Expand Up @@ -856,7 +857,6 @@ def _register_extension_skills(
from . import load_init_options
from .agents import CommandRegistrar
from .integrations import get_integration
import yaml

written: List[str] = []
opts = load_init_options(self.project_root)
Expand Down Expand Up @@ -931,7 +931,7 @@ def _register_extension_skills(
description,
f"extension:{manifest.id}",
)
frontmatter_text = yaml.safe_dump(frontmatter_data, sort_keys=False).strip()
frontmatter_text = yaml_safe_dump(frontmatter_data, sort_keys=False).strip()

# Derive a human-friendly title from the command name
short_name = cmd_name
Expand Down Expand Up @@ -1004,13 +1004,13 @@ def _unregister_extension_skills(self, skill_names: List[str], extension_id: str
if not skill_md.is_file():
continue
try:
import yaml as _yaml
from .yaml_utils import yaml_safe_load as _yaml_safe_load
raw = skill_md.read_text(encoding="utf-8")
source = ""
if raw.startswith("---"):
parts = raw.split("---", 2)
if len(parts) >= 3:
fm = _yaml.safe_load(parts[1]) or {}
fm = _yaml_safe_load(parts[1]) or {}
source = (
fm.get("metadata", {}).get("source", "")
if isinstance(fm, dict)
Expand Down Expand Up @@ -1055,13 +1055,13 @@ def _unregister_extension_skills(self, skill_names: List[str], extension_id: str
if not skill_md.is_file():
continue
try:
import yaml as _yaml
from .yaml_utils import yaml_safe_load as _yaml_safe_load
raw = skill_md.read_text(encoding="utf-8")
source = ""
if raw.startswith("---"):
parts = raw.split("---", 2)
if len(parts) >= 3:
fm = _yaml.safe_load(parts[1]) or {}
fm = _yaml_safe_load(parts[1]) or {}
source = (
fm.get("metadata", {}).get("source", "")
if isinstance(fm, dict)
Expand Down Expand Up @@ -1557,7 +1557,7 @@ def _load_catalog_config(self, config_path: Path) -> Optional[List[CatalogEntry]
if not config_path.exists():
return None
try:
data = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {}
data = yaml_safe_load(config_path.read_text(encoding="utf-8")) or {}
except (yaml.YAMLError, OSError, UnicodeError) as e:
raise ValidationError(
f"Failed to read catalog config {config_path}: {e}"
Expand Down Expand Up @@ -2057,7 +2057,7 @@ def _load_yaml_config(self, file_path: Path) -> Dict[str, Any]:
return {}

try:
return yaml.safe_load(file_path.read_text(encoding="utf-8")) or {}
return yaml_safe_load(file_path.read_text(encoding="utf-8")) or {}
except (yaml.YAMLError, OSError, UnicodeError):
return {}

Expand Down Expand Up @@ -2301,7 +2301,7 @@ def get_project_config(self) -> Dict[str, Any]:
}

try:
return yaml.safe_load(self.config_file.read_text(encoding="utf-8")) or {}
return yaml_safe_load(self.config_file.read_text(encoding="utf-8")) or {}
except (yaml.YAMLError, OSError, UnicodeError):
return {
"installed": [],
Expand All @@ -2317,7 +2317,7 @@ def save_project_config(self, config: Dict[str, Any]):
"""
self.config_file.parent.mkdir(parents=True, exist_ok=True)
self.config_file.write_text(
yaml.dump(config, default_flow_style=False, sort_keys=False, allow_unicode=True),
yaml_dump(config, default_flow_style=False, sort_keys=False, allow_unicode=True),
encoding="utf-8",
)

Expand Down
13 changes: 8 additions & 5 deletions src/specify_cli/integrations/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -907,12 +907,13 @@ def _extract_description(content: str) -> str:
raw text.
"""
import yaml
from ..yaml_utils import yaml_safe_load

frontmatter_text, _ = TomlIntegration._split_frontmatter(content)
if not frontmatter_text:
return ""
try:
frontmatter = yaml.safe_load(frontmatter_text) or {}
frontmatter = yaml_safe_load(frontmatter_text) or {}
except yaml.YAMLError:
return ""

Expand Down Expand Up @@ -1094,6 +1095,7 @@ def command_filename(self, template_name: str) -> str:
def _extract_frontmatter(content: str) -> dict[str, Any]:
"""Extract frontmatter as a dict from YAML frontmatter block."""
import yaml
from ..yaml_utils import yaml_safe_load

if not content.startswith("---"):
return {}
Expand All @@ -1113,7 +1115,7 @@ def _extract_frontmatter(content: str) -> dict[str, Any]:

frontmatter_text = "".join(lines[1:frontmatter_end])
try:
fm = yaml.safe_load(frontmatter_text) or {}
fm = yaml_safe_load(frontmatter_text) or {}
except yaml.YAMLError:
return {}

Expand Down Expand Up @@ -1162,7 +1164,7 @@ def _render_yaml(title: str, description: str, body: str, source_id: str) -> str
for the prompt content. Uses ``yaml.safe_dump()`` for the
header fields to ensure proper escaping.
"""
import yaml
from ..yaml_utils import yaml_safe_dump

header = {
"version": "1.0.0",
Expand All @@ -1173,7 +1175,7 @@ def _render_yaml(title: str, description: str, body: str, source_id: str) -> str
"activities": ["Spec-Driven Development"],
}

header_yaml = yaml.safe_dump(
header_yaml = yaml_safe_dump(
header,
sort_keys=False,
allow_unicode=True,
Expand Down Expand Up @@ -1343,6 +1345,7 @@ def setup(
``name``, ``description``, ``compatibility``, and ``metadata``.
"""
import yaml
from ..yaml_utils import yaml_safe_load

templates = self.list_command_templates()
if not templates:
Expand Down Expand Up @@ -1385,7 +1388,7 @@ def setup(
parts = raw.split("---", 2)
if len(parts) >= 3:
try:
fm = yaml.safe_load(parts[1])
fm = yaml_safe_load(parts[1])
if isinstance(fm, dict):
frontmatter = fm
except yaml.YAMLError:
Expand Down
6 changes: 4 additions & 2 deletions src/specify_cli/integrations/catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
import yaml
from packaging import version as pkg_version

from ..yaml_utils import yaml_safe_load


# ---------------------------------------------------------------------------
# Errors
Expand Down Expand Up @@ -101,7 +103,7 @@ def _load_catalog_config(
if not config_path.exists():
return None
try:
data = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {}
data = yaml_safe_load(config_path.read_text(encoding="utf-8")) or {}
except (yaml.YAMLError, OSError, UnicodeError) as exc:
raise IntegrationCatalogError(
f"Failed to read catalog config {config_path}: {exc}"
Expand Down Expand Up @@ -447,7 +449,7 @@ def __init__(self, descriptor_path: Path) -> None:
def _load(path: Path) -> dict:
try:
with open(path, "r", encoding="utf-8") as fh:
return yaml.safe_load(fh) or {}
return yaml_safe_load(fh) or {}
except yaml.YAMLError as exc:
raise IntegrationDescriptorError(f"Invalid YAML in {path}: {exc}")
except FileNotFoundError:
Expand Down
5 changes: 2 additions & 3 deletions src/specify_cli/integrations/claude/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,8 @@

import re

import yaml

from ..base import SkillsIntegration
from ...yaml_utils import yaml_safe_dump
from ..manifest import IntegrationManifest

# Note injected into hook sections so Claude maps dot-notation command
Expand Down Expand Up @@ -112,7 +111,7 @@ def _render_skill(self, template_name: str, frontmatter: dict[str, Any], body: s
skill_frontmatter = self._build_skill_fm(
skill_name, description, f"templates/commands/{template_name}.md"
)
frontmatter_text = yaml.safe_dump(skill_frontmatter, sort_keys=False).strip()
frontmatter_text = yaml_safe_dump(skill_frontmatter, sort_keys=False).strip()
return f"---\n{frontmatter_text}\n---\n\n{body.strip()}\n"

def _build_skill_fm(self, name: str, description: str, source: str) -> dict:
Expand Down
Loading