@@ -301,49 +301,30 @@ def _upsert_safe(
301301 * ,
302302 exists_ok : bool = False ,
303303) -> yamltrip .Document :
304- """Upsert a value into a yamltrip Document, handling empty documents."""
305- # For complex values (list/dict) where the key doesn't exist yet, add a
306- # null placeholder first then replace — this forces block-style output.
307- if isinstance (value , (list , dict )) and tuple (keys ) not in doc :
308- try :
309- doc = doc .upsert (* keys , value = None )
310- except yamltrip .PatchError :
311- pass
312- else :
313- return doc .upsert (* keys , value = value )
304+ """Upsert a value into a yamltrip Document.
314305
306+ A RoutingError means a key in the path passes through a non-mapping node. When
307+ overwriting is allowed we prune the conflicting prefix and retry; otherwise we
308+ translate it into a friendly YAMLValueAlreadySetError.
309+ """
315310 try :
311+ # Seed the leaf as null first, then write the value. Seeding forces
312+ # complex values to render in block style (rather than flow style) and
313+ # fully replaces any existing value at the path.
314+ doc = doc .upsert (* keys , value = None )
316315 return doc .upsert (* keys , value = value )
317- except yamltrip .PatchError as err :
318- if not doc .source .strip ():
319- # Empty document: bootstrap with a block-style seed then upsert.
320- # yamlpatch's Add operation requires an existing mapping node at the
321- # target route, so it cannot add keys to a truly empty document.
322- # We work around this by seeding with a throwaway sentinel key ("_")
323- # to create a root mapping, inserting the real content, then removing
324- # the sentinel. The sentinel is safe because the document is empty
325- # (no user data to collide with).
326- # Tracking issue to remove this workaround:
327- # https://github.com/usethis-python/yamltrip/issues/34
328- doc = yamltrip .loads ("_: null\n " )
329- doc = doc .upsert (* keys , value = None )
330- doc = doc .upsert (* keys , value = value )
331- return doc .remove ("_" )
332- err_msg = str (err )
333- if (
334- "non-mapping route" in err_msg
335- or "expected mapping containing key" in err_msg
336- ):
337- if exists_ok :
338- # Remove the conflicting prefix and retry.
339- for i in range (len (keys ) - 1 , 0 , - 1 ):
340- if tuple (keys [:i ]) in doc :
341- doc = doc .prune_remove (* keys [:i ])
342- return _upsert_safe (doc , keys , value , exists_ok = exists_ok )
343- # Trying to add a key under a non-mapping node.
344- # Find the longest existing prefix to report.
345- for i in range (len (keys ) - 1 , 0 , - 1 ):
346- if tuple (keys [:i ]) in doc :
347- msg = f"Configuration value '{ print_keys (list (keys [:i ]))} ' is already set."
348- raise YAMLValueAlreadySetError (msg ) from err
349- raise
316+ except yamltrip .RoutingError as err :
317+ # A key along the path maps to a non-mapping node. Find the longest
318+ # existing prefix: the node we collided with.
319+ for i in range (len (keys ) - 1 , 0 , - 1 ):
320+ if tuple (keys [:i ]) in doc :
321+ if exists_ok :
322+ # Overwrite: remove the conflicting prefix and retry.
323+ doc = doc .prune_remove (* keys [:i ])
324+ return _upsert_safe (doc , keys , value , exists_ok = exists_ok )
325+ msg = f"Configuration value '{ print_keys (list (keys [:i ]))} ' is already set."
326+ raise YAMLValueAlreadySetError (msg ) from err
327+ # A RoutingError always collides with an existing non-mapping prefix, so
328+ # the loop above must have returned or raised.
329+ msg = "RoutingError did not pass through an existing prefix."
330+ raise AssertionError (msg ) from err
0 commit comments