From 26efc68bf14396da4c43b7749a59e7d5b9d9df5a Mon Sep 17 00:00:00 2001 From: dvmukul <83778475+dvmukul@users.noreply.github.com> Date: Wed, 8 Apr 2026 16:21:24 -0700 Subject: [PATCH] Standardize Unicode encodings to explicitly use UTF-8 This change adds encoding='utf-8' to file open operations in the script runner, configuration manager, marketplace client, and other core modules. This resolves reported issues (e.g., #604) where UnicodeDecodeError occurs on Windows when handling prompts and configuration files with non-ASCII characters. --- src/apm_cli/config.py | 6 +++--- src/apm_cli/core/script_runner.py | 6 +++--- src/apm_cli/marketplace/client.py | 10 +++++----- src/apm_cli/marketplace/registry.py | 6 +++--- src/apm_cli/models/plugin.py | 2 +- src/apm_cli/runtime/copilot_runtime.py | 2 +- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/apm_cli/config.py b/src/apm_cli/config.py index 0d453221..75028adf 100644 --- a/src/apm_cli/config.py +++ b/src/apm_cli/config.py @@ -16,7 +16,7 @@ def ensure_config_exists(): os.makedirs(CONFIG_DIR) if not os.path.exists(CONFIG_FILE): - with open(CONFIG_FILE, "w") as f: + with open(CONFIG_FILE, "w", encoding="utf-8") as f: json.dump({"default_client": "vscode"}, f) @@ -32,7 +32,7 @@ def get_config(): if _config_cache is not None: return _config_cache ensure_config_exists() - with open(CONFIG_FILE, "r") as f: + with open(CONFIG_FILE, "r", encoding="utf-8") as f: _config_cache = json.load(f) return _config_cache @@ -53,7 +53,7 @@ def update_config(updates): config = get_config() config.update(updates) - with open(CONFIG_FILE, "w") as f: + with open(CONFIG_FILE, "w", encoding="utf-8") as f: json.dump(config, f, indent=2) _invalidate_config_cache() diff --git a/src/apm_cli/core/script_runner.py b/src/apm_cli/core/script_runner.py index 87798c5c..fcb919fd 100644 --- a/src/apm_cli/core/script_runner.py +++ b/src/apm_cli/core/script_runner.py @@ -256,7 +256,7 @@ def _auto_compile_prompts( compiled_prompt_files.append(prompt_file) # Read the compiled content - with open(compiled_path, "r") as f: + with open(compiled_path, "r", encoding="utf-8") as f: compiled_content = f.read().strip() # Check if this is a runtime command (copilot, codex, llm) before transformation @@ -916,7 +916,7 @@ def compile(self, prompt_file: str, params: Dict[str, str]) -> str: # Now ensure compiled directory exists self.compiled_dir.mkdir(parents=True, exist_ok=True) - with open(prompt_path, "r") as f: + with open(prompt_path, "r", encoding="utf-8") as f: content = f.read() # Parse frontmatter and content @@ -939,7 +939,7 @@ def compile(self, prompt_file: str, params: Dict[str, str]) -> str: output_path = self.compiled_dir / output_name # Write compiled content - with open(output_path, "w") as f: + with open(output_path, "w", encoding="utf-8") as f: f.write(compiled_content) return str(output_path) diff --git a/src/apm_cli/marketplace/client.py b/src/apm_cli/marketplace/client.py index cee2d6eb..5bb766a6 100644 --- a/src/apm_cli/marketplace/client.py +++ b/src/apm_cli/marketplace/client.py @@ -71,13 +71,13 @@ def _read_cache(name: str) -> Optional[Dict]: if not os.path.exists(data_path) or not os.path.exists(meta_path): return None try: - with open(meta_path, "r") as f: + with open(meta_path, "r", encoding="utf-8") as f: meta = json.load(f) fetched_at = meta.get("fetched_at", 0) ttl = meta.get("ttl_seconds", _CACHE_TTL_SECONDS) if time.time() - fetched_at > ttl: return None # Expired - with open(data_path, "r") as f: + with open(data_path, "r", encoding="utf-8") as f: return json.load(f) except (json.JSONDecodeError, OSError, KeyError) as exc: logger.debug("Cache read failed for '%s': %s", name, exc) @@ -90,7 +90,7 @@ def _read_stale_cache(name: str) -> Optional[Dict]: if not os.path.exists(data_path): return None try: - with open(data_path, "r") as f: + with open(data_path, "r", encoding="utf-8") as f: return json.load(f) except (json.JSONDecodeError, OSError): return None @@ -101,9 +101,9 @@ def _write_cache(name: str, data: Dict) -> None: data_path = _cache_data_path(name) meta_path = _cache_meta_path(name) try: - with open(data_path, "w") as f: + with open(data_path, "w", encoding="utf-8") as f: json.dump(data, f, indent=2) - with open(meta_path, "w") as f: + with open(meta_path, "w", encoding="utf-8") as f: json.dump( {"fetched_at": time.time(), "ttl_seconds": _CACHE_TTL_SECONDS}, f, diff --git a/src/apm_cli/marketplace/registry.py b/src/apm_cli/marketplace/registry.py index 3aa668d5..4ddab2dd 100644 --- a/src/apm_cli/marketplace/registry.py +++ b/src/apm_cli/marketplace/registry.py @@ -30,7 +30,7 @@ def _ensure_file() -> str: ensure_config_exists() path = _marketplaces_path() if not os.path.exists(path): - with open(path, "w") as f: + with open(path, "w", encoding="utf-8") as f: json.dump({"marketplaces": []}, f, indent=2) return path @@ -48,7 +48,7 @@ def _load() -> List[MarketplaceSource]: path = _ensure_file() try: - with open(path, "r") as f: + with open(path, "r", encoding="utf-8") as f: data = json.load(f) except (json.JSONDecodeError, OSError) as exc: logger.warning("Failed to read %s: %s", path, exc) @@ -71,7 +71,7 @@ def _save(sources: List[MarketplaceSource]) -> None: path = _ensure_file() data = {"marketplaces": [s.to_dict() for s in sources]} tmp = path + ".tmp" - with open(tmp, "w") as f: + with open(tmp, "w", encoding="utf-8") as f: json.dump(data, f, indent=2) os.replace(tmp, path) _registry_cache = list(sources) diff --git a/src/apm_cli/models/plugin.py b/src/apm_cli/models/plugin.py index aa486d37..40eba1cb 100644 --- a/src/apm_cli/models/plugin.py +++ b/src/apm_cli/models/plugin.py @@ -108,7 +108,7 @@ def from_path(cls, plugin_path: Path) -> "Plugin": if metadata_file is None: raise FileNotFoundError(f"Plugin metadata not found in any expected location: {plugin_path}") - with open(metadata_file, "r") as f: + with open(metadata_file, "r", encoding="utf-8") as f: metadata_dict = json.load(f) metadata = PluginMetadata.from_dict(metadata_dict) diff --git a/src/apm_cli/runtime/copilot_runtime.py b/src/apm_cli/runtime/copilot_runtime.py index 485057f1..39bee709 100644 --- a/src/apm_cli/runtime/copilot_runtime.py +++ b/src/apm_cli/runtime/copilot_runtime.py @@ -200,7 +200,7 @@ def get_mcp_servers(self) -> Dict[str, Any]: return {} try: - with open(mcp_config_path, 'r') as f: + with open(mcp_config_path, 'r', encoding="utf-8") as f: config = json.load(f) return config.get('servers', {}) except Exception as e: