MCPcopy Index your code
hub / github.com/github/spec-kit / merge_json_files

Function merge_json_files

src/specify_cli/_utils.py:216–293  ·  view source on GitHub ↗

Merge new JSON content into existing JSON file. Performs a polite deep merge where: - New keys are added - Existing keys are preserved (not overwritten) unless both values are dictionaries - Nested dictionaries are merged recursively only when both sides are dictionaries - Lists

(existing_path: Path, new_content: Any, verbose: bool = False)

Source from the content-addressed store, hash-verified

214
215
216def merge_json_files(existing_path: Path, new_content: Any, verbose: bool = False) -> dict[str, Any] | None:
217 """Merge new JSON content into existing JSON file.
218
219 Performs a polite deep merge where:
220 - New keys are added
221 - Existing keys are preserved (not overwritten) unless both values are dictionaries
222 - Nested dictionaries are merged recursively only when both sides are dictionaries
223 - Lists and other values are preserved from base if they exist
224
225 Args:
226 existing_path: Path to existing JSON file
227 new_content: New JSON content to merge in
228 verbose: Whether to print merge details
229
230 Returns:
231 Merged JSON content as dict, or None if the existing file should be left untouched.
232 """
233 # Load existing content first to have a safe fallback
234 existing_content = None
235 exists = existing_path.exists()
236
237 if exists:
238 try:
239 with open(existing_path, 'r', encoding='utf-8') as f:
240 # Handle comments (JSONC) natively with json5
241 # Note: json5 handles BOM automatically
242 existing_content = json5.load(f)
243 except FileNotFoundError:
244 # Handle race condition where file is deleted after exists() check
245 exists = False
246 except Exception as e:
247 if verbose:
248 console.print(f"[yellow]Warning: Could not read or parse existing JSON in {existing_path.name} ({e}).[/yellow]")
249 # Skip merge to preserve existing file if unparseable or inaccessible (e.g. PermissionError)
250 return None
251
252 # Validate template content
253 if not isinstance(new_content, dict):
254 if verbose:
255 console.print(f"[yellow]Warning: Template content for {existing_path.name} is not a dictionary. Preserving existing settings.[/yellow]")
256 return None
257
258 if not exists:
259 return new_content
260
261 # If existing content parsed but is not a dict, skip merge to avoid data loss
262 if not isinstance(existing_content, dict):
263 if verbose:
264 console.print(f"[yellow]Warning: Existing JSON in {existing_path.name} is not an object. Skipping merge to avoid data loss.[/yellow]")
265 return None
266
267 def deep_merge_polite(base: dict[str, Any], update: dict[str, Any]) -> dict[str, Any]:
268 """Recursively merge update dict into base dict, preserving base values."""
269 result = base.copy()
270 for key, value in update.items():
271 if key not in result:
272 # Add new key
273 result[key] = value

Calls 3

deep_merge_politeFunction · 0.85
printMethod · 0.80
loadMethod · 0.45