From a351c826ee1d1a8c7bd38974cdfdf3a6678b6914 Mon Sep 17 00:00:00 2001 From: Seiya Kojima Date: Mon, 23 Mar 2026 22:49:10 +0900 Subject: [PATCH] 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. --- src/specify_cli/__init__.py | 16 ++++++++-------- src/specify_cli/agents.py | 2 +- src/specify_cli/extensions.py | 15 ++++++++------- src/specify_cli/presets.py | 4 ++-- tests/test_extensions.py | 12 ++++++++++++ 5 files changed, 31 insertions(+), 18 deletions(-) diff --git a/src/specify_cli/__init__.py b/src/specify_cli/__init__.py index 1e4c296e..08af6510 100644 --- a/src/specify_cli/__init__.py +++ b/src/specify_cli/__init__.py @@ -3042,7 +3042,7 @@ def preset_catalog_add( # Load existing config if config_path.exists(): try: - config = yaml.safe_load(config_path.read_text()) 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) @@ -3070,7 +3070,7 @@ def preset_catalog_add( }) config["catalogs"] = catalogs - config_path.write_text(yaml.dump(config, default_flow_style=False, sort_keys=False)) + 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})") @@ -3098,7 +3098,7 @@ def preset_catalog_remove( raise typer.Exit(1) try: - config = yaml.safe_load(config_path.read_text()) 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) @@ -3115,7 +3115,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)) + 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: @@ -3384,7 +3384,7 @@ def catalog_add( # Load existing config if config_path.exists(): try: - config = yaml.safe_load(config_path.read_text()) 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) @@ -3412,7 +3412,7 @@ def catalog_add( }) config["catalogs"] = catalogs - config_path.write_text(yaml.dump(config, default_flow_style=False, sort_keys=False)) + 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})") @@ -3440,7 +3440,7 @@ def catalog_remove( raise typer.Exit(1) try: - config = yaml.safe_load(config_path.read_text()) 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) @@ -3457,7 +3457,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)) + 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: diff --git a/src/specify_cli/agents.py b/src/specify_cli/agents.py index 4f1ab728..7fe53160 100644 --- a/src/specify_cli/agents.py +++ b/src/specify_cli/agents.py @@ -207,7 +207,7 @@ class CommandRegistrar: if not fm: return "" - yaml_str = yaml.dump(fm, default_flow_style=False, sort_keys=False) + yaml_str = yaml.dump(fm, default_flow_style=False, sort_keys=False, allow_unicode=True) return f"---\n{yaml_str}---\n" def _adjust_script_paths(self, frontmatter: dict) -> dict: diff --git a/src/specify_cli/extensions.py b/src/specify_cli/extensions.py index 0dca39a0..b26b1e93 100644 --- a/src/specify_cli/extensions.py +++ b/src/specify_cli/extensions.py @@ -975,8 +975,8 @@ class ExtensionCatalog: if not config_path.exists(): return None try: - data = yaml.safe_load(config_path.read_text()) or {} - except (yaml.YAMLError, OSError) as e: + 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}" ) @@ -1467,8 +1467,8 @@ class ConfigManager: return {} try: - return yaml.safe_load(file_path.read_text()) or {} - except (yaml.YAMLError, OSError): + return yaml.safe_load(file_path.read_text(encoding="utf-8")) or {} + except (yaml.YAMLError, OSError, UnicodeError): return {} def _get_extension_defaults(self) -> Dict[str, Any]: @@ -1659,8 +1659,8 @@ class HookExecutor: } try: - return yaml.safe_load(self.config_file.read_text()) or {} - except (yaml.YAMLError, OSError): + return yaml.safe_load(self.config_file.read_text(encoding="utf-8")) or {} + except (yaml.YAMLError, OSError, UnicodeError): return { "installed": [], "settings": {"auto_execute_hooks": True}, @@ -1675,7 +1675,8 @@ class HookExecutor: """ 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) + yaml.dump(config, default_flow_style=False, sort_keys=False, allow_unicode=True), + encoding="utf-8", ) def register_hooks(self, manifest: ExtensionManifest): diff --git a/src/specify_cli/presets.py b/src/specify_cli/presets.py index c53915fa..24d523aa 100644 --- a/src/specify_cli/presets.py +++ b/src/specify_cli/presets.py @@ -1062,8 +1062,8 @@ class PresetCatalog: if not config_path.exists(): return None try: - data = yaml.safe_load(config_path.read_text()) or {} - except (yaml.YAMLError, OSError) as e: + data = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {} + except (yaml.YAMLError, OSError, UnicodeError) as e: raise PresetValidationError( f"Failed to read catalog config {config_path}: {e}" ) diff --git a/tests/test_extensions.py b/tests/test_extensions.py index c0aa00ad..cd0f9ba4 100644 --- a/tests/test_extensions.py +++ b/tests/test_extensions.py @@ -747,6 +747,18 @@ $ARGUMENTS assert output.endswith("---\n") assert "description: Test command" in output + def test_render_frontmatter_unicode(self): + """Test rendering frontmatter preserves non-ASCII characters.""" + frontmatter = { + "description": "Prüfe Konformität der Implementierung" + } + + registrar = CommandRegistrar() + output = registrar.render_frontmatter(frontmatter) + + assert "Prüfe Konformität" in output + assert "\\u" not in output + def test_register_commands_for_claude(self, extension_dir, project_dir): """Test registering commands for Claude agent.""" # Create .claude directory