Skip to content

Commit a351c82

Browse files
authored
fix(cli): add allow_unicode=True and encoding="utf-8" to YAML I/O (#1936)
None of the yaml.dump() calls specify allow_unicode=True, causing non-ASCII characters in extension descriptions to be escaped to \uXXXX sequences in generated .agent.md frontmatter and config files. Add allow_unicode=True to all 6 yaml.dump() call sites, and encoding="utf-8" to all corresponding write_text() and read_text() calls to ensure consistent UTF-8 handling across platforms.
1 parent 6223d10 commit a351c82

File tree

5 files changed

+31
-18
lines changed

5 files changed

+31
-18
lines changed

src/specify_cli/__init__.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3042,7 +3042,7 @@ def preset_catalog_add(
30423042
# Load existing config
30433043
if config_path.exists():
30443044
try:
3045-
config = yaml.safe_load(config_path.read_text()) or {}
3045+
config = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {}
30463046
except Exception as e:
30473047
console.print(f"[red]Error:[/red] Failed to read {config_path}: {e}")
30483048
raise typer.Exit(1)
@@ -3070,7 +3070,7 @@ def preset_catalog_add(
30703070
})
30713071

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

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

31003100
try:
3101-
config = yaml.safe_load(config_path.read_text()) or {}
3101+
config = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {}
31023102
except Exception:
31033103
console.print("[red]Error:[/red] Failed to read preset catalog config.")
31043104
raise typer.Exit(1)
@@ -3115,7 +3115,7 @@ def preset_catalog_remove(
31153115
raise typer.Exit(1)
31163116

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

31203120
console.print(f"[green]✓[/green] Removed catalog '{name}'")
31213121
if not catalogs:
@@ -3384,7 +3384,7 @@ def catalog_add(
33843384
# Load existing config
33853385
if config_path.exists():
33863386
try:
3387-
config = yaml.safe_load(config_path.read_text()) or {}
3387+
config = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {}
33883388
except Exception as e:
33893389
console.print(f"[red]Error:[/red] Failed to read {config_path}: {e}")
33903390
raise typer.Exit(1)
@@ -3412,7 +3412,7 @@ def catalog_add(
34123412
})
34133413

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

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

34423442
try:
3443-
config = yaml.safe_load(config_path.read_text()) or {}
3443+
config = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {}
34443444
except Exception:
34453445
console.print("[red]Error:[/red] Failed to read catalog config.")
34463446
raise typer.Exit(1)
@@ -3457,7 +3457,7 @@ def catalog_remove(
34573457
raise typer.Exit(1)
34583458

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

34623462
console.print(f"[green]✓[/green] Removed catalog '{name}'")
34633463
if not catalogs:

src/specify_cli/agents.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ def render_frontmatter(fm: dict) -> str:
207207
if not fm:
208208
return ""
209209

210-
yaml_str = yaml.dump(fm, default_flow_style=False, sort_keys=False)
210+
yaml_str = yaml.dump(fm, default_flow_style=False, sort_keys=False, allow_unicode=True)
211211
return f"---\n{yaml_str}---\n"
212212

213213
def _adjust_script_paths(self, frontmatter: dict) -> dict:

src/specify_cli/extensions.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -975,8 +975,8 @@ def _load_catalog_config(self, config_path: Path) -> Optional[List[CatalogEntry]
975975
if not config_path.exists():
976976
return None
977977
try:
978-
data = yaml.safe_load(config_path.read_text()) or {}
979-
except (yaml.YAMLError, OSError) as e:
978+
data = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {}
979+
except (yaml.YAMLError, OSError, UnicodeError) as e:
980980
raise ValidationError(
981981
f"Failed to read catalog config {config_path}: {e}"
982982
)
@@ -1467,8 +1467,8 @@ def _load_yaml_config(self, file_path: Path) -> Dict[str, Any]:
14671467
return {}
14681468

14691469
try:
1470-
return yaml.safe_load(file_path.read_text()) or {}
1471-
except (yaml.YAMLError, OSError):
1470+
return yaml.safe_load(file_path.read_text(encoding="utf-8")) or {}
1471+
except (yaml.YAMLError, OSError, UnicodeError):
14721472
return {}
14731473

14741474
def _get_extension_defaults(self) -> Dict[str, Any]:
@@ -1659,8 +1659,8 @@ def get_project_config(self) -> Dict[str, Any]:
16591659
}
16601660

16611661
try:
1662-
return yaml.safe_load(self.config_file.read_text()) or {}
1663-
except (yaml.YAMLError, OSError):
1662+
return yaml.safe_load(self.config_file.read_text(encoding="utf-8")) or {}
1663+
except (yaml.YAMLError, OSError, UnicodeError):
16641664
return {
16651665
"installed": [],
16661666
"settings": {"auto_execute_hooks": True},
@@ -1675,7 +1675,8 @@ def save_project_config(self, config: Dict[str, Any]):
16751675
"""
16761676
self.config_file.parent.mkdir(parents=True, exist_ok=True)
16771677
self.config_file.write_text(
1678-
yaml.dump(config, default_flow_style=False, sort_keys=False)
1678+
yaml.dump(config, default_flow_style=False, sort_keys=False, allow_unicode=True),
1679+
encoding="utf-8",
16791680
)
16801681

16811682
def register_hooks(self, manifest: ExtensionManifest):

src/specify_cli/presets.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1062,8 +1062,8 @@ def _load_catalog_config(self, config_path: Path) -> Optional[List[PresetCatalog
10621062
if not config_path.exists():
10631063
return None
10641064
try:
1065-
data = yaml.safe_load(config_path.read_text()) or {}
1066-
except (yaml.YAMLError, OSError) as e:
1065+
data = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {}
1066+
except (yaml.YAMLError, OSError, UnicodeError) as e:
10671067
raise PresetValidationError(
10681068
f"Failed to read catalog config {config_path}: {e}"
10691069
)

tests/test_extensions.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -747,6 +747,18 @@ def test_render_frontmatter(self):
747747
assert output.endswith("---\n")
748748
assert "description: Test command" in output
749749

750+
def test_render_frontmatter_unicode(self):
751+
"""Test rendering frontmatter preserves non-ASCII characters."""
752+
frontmatter = {
753+
"description": "Prüfe Konformität der Implementierung"
754+
}
755+
756+
registrar = CommandRegistrar()
757+
output = registrar.render_frontmatter(frontmatter)
758+
759+
assert "Prüfe Konformität" in output
760+
assert "\\u" not in output
761+
750762
def test_register_commands_for_claude(self, extension_dir, project_dir):
751763
"""Test registering commands for Claude agent."""
752764
# Create .claude directory

0 commit comments

Comments
 (0)