@@ -1472,18 +1472,57 @@ def _add_keymap(self, keys, location):
14721472 # each Location own its comments outright -- two Locations that
14731473 # share a Line (parent and its first child) cannot then
14741474 # double-emit the same comment block.
1475- kl = location .key_line if location .key_line is not None else location .line
1475+ key_first = (
1476+ location .key_line if location .key_line is not None
1477+ else location .line
1478+ )
14761479 vl = location .line
14771480 ve = location .value_end_line
1478- if kl is not None and kl .leading_comments :
1479- location .key_leading_comments = list (kl .leading_comments )
1480- kl .leading_comments = []
1481- if vl is not None and vl is not kl and vl .leading_comments :
1481+ # For a multi-line key, walk the chain of fragment lines via
1482+ # next_line (set by Lines.read_lines only between consecutive
1483+ # ``key item`` lines at the same depth) so that comments
1484+ # staged on later fragments are claimed too.
1485+ key_last = key_first
1486+ if key_first is not None :
1487+ while (
1488+ getattr (key_last , "next_line" , None ) is not None
1489+ and key_last .next_line .kind == "key item"
1490+ and key_last .next_line .depth == key_last .depth
1491+ ):
1492+ key_last = key_last .next_line
1493+ if key_first is not None and key_first .leading_comments :
1494+ location .key_leading_comments = list (key_first .leading_comments )
1495+ key_first .leading_comments = []
1496+ if (
1497+ vl is not None
1498+ and vl is not key_first
1499+ and vl is not key_last
1500+ and vl .leading_comments
1501+ ):
14821502 location .value_leading_comments = list (vl .leading_comments )
14831503 vl .leading_comments = []
1484- if kl is not None and kl is not ve and kl .trailing_comments :
1485- location .key_trailing_comments = list (kl .trailing_comments )
1486- kl .trailing_comments = []
1504+ # key_trailing collects (a) leading_comments staged on each
1505+ # *intermediate* key-fragment line -- these are comments that
1506+ # appeared between fragments of the multi-line key, the
1507+ # multi-line-key analogue of inline-in-multi-line-string -- and
1508+ # (b) the trailing_comments on each fragment, including the
1509+ # last. All are emitted at the key-trailing position. When
1510+ # the entire key+value is on one line (key_first == ve), the
1511+ # trailing comments belong to value_trailing instead.
1512+ kt = []
1513+ cur = key_first
1514+ while cur is not None :
1515+ if cur is not key_first and cur .leading_comments :
1516+ kt .extend (cur .leading_comments )
1517+ cur .leading_comments = []
1518+ if cur is not ve and cur .trailing_comments :
1519+ kt .extend (cur .trailing_comments )
1520+ cur .trailing_comments = []
1521+ if cur is key_last :
1522+ break
1523+ cur = cur .next_line
1524+ if kt :
1525+ location .key_trailing_comments = kt
14871526 if ve is not None and ve .trailing_comments :
14881527 location .value_trailing_comments = list (ve .trailing_comments )
14891528 ve .trailing_comments = []
@@ -2079,6 +2118,16 @@ def render_dict_item(self, key, value, keys, values):
20792118 or key [:2 ] in ["- " , "> " , ": " ]
20802119 or ": " in key
20812120 )
2121+ # The key_trailing and value_leading comment slots only have a
2122+ # rendering position in the multi-line dict-item *value* form
2123+ # (between the key line and the value's first line). If either
2124+ # slot has any contribution -- static or via a parent provider --
2125+ # force the value onto its own line so those comments don't get
2126+ # silently dropped.
2127+ force_multiline_value = (
2128+ not multiline_key_required
2129+ and self ._comments_force_multiline (keys )
2130+ )
20822131 if multiline_key_required :
20832132 key = "\n " .join (": " + l if l else ":" for l in key .split ("\n " ))
20842133 if self .is_a_dict (value ) or self .is_a_list (value ):
@@ -2089,8 +2138,39 @@ def render_dict_item(self, key, value, keys, values):
20892138 else :
20902139 value = self .render_value (value , keys , values )
20912140 return key + "\n " + add_leader (value , self .indent * " " + "> " )
2092- else :
2093- return add_prefix (key + ":" , self .render_value (value , keys , values ))
2141+ if force_multiline_value :
2142+ # Plain "key:" syntax, but force the value onto its own line
2143+ # so key_trailing / value_leading have a place to render.
2144+ if self .is_a_dict (value ) or self .is_a_list (value ):
2145+ return key + ":" + self .render_value (value , keys , values )
2146+ if is_str (value ):
2147+ value_text = convert_line_terminators (value )
2148+ else :
2149+ value_text = self .render_value (value , keys , values )
2150+ return key + ":\n " + add_leader (value_text , self .indent * " " + "> " )
2151+ return add_prefix (key + ":" , self .render_value (value , keys , values ))
2152+
2153+ # _comments_force_multiline {{{3
2154+ def _comments_force_multiline (self , keys ):
2155+ """Return True if any source -- static key_trailing/value_leading
2156+ on this Location, or a parent provider for either slot -- will
2157+ contribute Comments that need the multi-line dict-item form.
2158+ """
2159+ if not is_mapping (self .map_keys ):
2160+ return False
2161+ loc = self .map_keys .get (keys )
2162+ if loc is not None :
2163+ if loc .get_key_trailing_comments () or loc .get_value_leading_comments ():
2164+ return True
2165+ if keys :
2166+ parent_loc = self .map_keys .get (keys [:- 1 ])
2167+ if parent_loc is not None :
2168+ if (
2169+ parent_loc .get_key_trailing_provider () is not None
2170+ or parent_loc .get_value_leading_provider () is not None
2171+ ):
2172+ return True
2173+ return False
20942174
20952175 # render_inline_value {{{3
20962176 def render_inline_value (self , obj , exclude , keys , values ):
@@ -2318,15 +2398,36 @@ def _wrap_with_comments(self, rendered_value, keys):
23182398 value_leading = self ._comments_to_lines (vl , natural = val_natural )
23192399 trailing = self ._comments_to_lines (vt , natural = val_natural )
23202400 value_lines = rendered_value .split ("\n " )
2321- # Inject key_trailing and value_leading between the key line (first
2322- # line) and the value's first line. Only meaningful when the item
2323- # spans multiple lines.
2401+ # Inject key_trailing and value_leading between the rendered key
2402+ # (which may span several lines for multi-line keys) and the
2403+ # value's first line. We detect the key's line count by counting
2404+ # consecutive leading lines that look like multi-line key
2405+ # fragments (``: frag`` or ``:`` after lstrip) at the *same*
2406+ # indent. If no multi-line-key prefix is present, the key is the
2407+ # first line (e.g. ``key:``). Inline values (single-line output)
2408+ # don't get key_trailing / value_leading -- those are forced into
2409+ # multi-line by ``_comments_force_multiline``, which is what
2410+ # ensures we never silently drop them here.
23242411 if (key_trailing or value_leading ) and len (value_lines ) > 1 :
2412+ boundary = 0
2413+ key_indent = None
2414+ for line in value_lines :
2415+ stripped = line .lstrip ()
2416+ if not (stripped == ":" or stripped .startswith (": " )):
2417+ break
2418+ indent = len (line ) - len (stripped )
2419+ if key_indent is None :
2420+ key_indent = indent
2421+ elif indent != key_indent :
2422+ break
2423+ boundary += 1
2424+ if boundary == 0 :
2425+ boundary = 1
23252426 value_lines = (
2326- value_lines [:1 ]
2427+ value_lines [:boundary ]
23272428 + key_trailing
23282429 + value_leading
2329- + value_lines [1 :]
2430+ + value_lines [boundary :]
23302431 )
23312432 return "\n " .join (leading + value_lines + trailing )
23322433
0 commit comments