@@ -155,11 +155,18 @@ def make_comment(content: str, mode: Mode) -> str:
155155 space between the hash sign and the content.
156156
157157 If `content` didn't start with a hash sign, one is provided.
158+
159+ Comments containing fmt directives are preserved exactly as-is to respect
160+ user intent (e.g., `#no space # fmt: skip` stays as-is).
158161 """
159162 content = content .rstrip ()
160163 if not content :
161164 return "#"
162165
166+ # Preserve comments with fmt directives exactly as-is
167+ if content .startswith ("#" ) and _contains_fmt_directive (content ):
168+ return content
169+
163170 if content [0 ] == "#" :
164171 content = content [1 :]
165172 if (
@@ -191,6 +198,126 @@ def normalize_fmt_off(
191198 try_again = convert_one_fmt_off_pair (node , mode , lines )
192199
193200
201+ def _should_process_fmt_comment (
202+ comment : ProtoComment , leaf : Leaf
203+ ) -> tuple [bool , bool , bool ]:
204+ """Check if comment should be processed for fmt handling.
205+
206+ Returns (should_process, is_fmt_off, is_fmt_skip).
207+ """
208+ is_fmt_off = _contains_fmt_directive (comment .value , FMT_OFF )
209+ is_fmt_skip = _contains_fmt_directive (comment .value , FMT_SKIP )
210+
211+ if not is_fmt_off and not is_fmt_skip :
212+ return False , False , False
213+
214+ # Invalid use when `# fmt: off` is applied before a closing bracket
215+ if is_fmt_off and leaf .type in CLOSING_BRACKETS :
216+ return False , False , False
217+
218+ return True , is_fmt_off , is_fmt_skip
219+
220+
221+ def _is_valid_standalone_fmt_comment (
222+ comment : ProtoComment , leaf : Leaf , is_fmt_off : bool , is_fmt_skip : bool
223+ ) -> bool :
224+ """Check if comment is a valid standalone fmt directive.
225+
226+ We only want standalone comments. If there's no previous leaf or if
227+ the previous leaf is indentation, it's a standalone comment in disguise.
228+ """
229+ if comment .type == STANDALONE_COMMENT :
230+ return True
231+
232+ prev = preceding_leaf (leaf )
233+ if not prev :
234+ return True
235+
236+ # Treat STANDALONE_COMMENT nodes as whitespace for check
237+ if is_fmt_off and prev .type not in WHITESPACE and prev .type != STANDALONE_COMMENT :
238+ return False
239+ if is_fmt_skip and prev .type in WHITESPACE :
240+ return False
241+
242+ return True
243+
244+
245+ def _handle_comment_only_fmt_block (
246+ leaf : Leaf ,
247+ comment : ProtoComment ,
248+ previous_consumed : int ,
249+ mode : Mode ,
250+ ) -> bool :
251+ """Handle fmt:off/on blocks that contain only comments.
252+
253+ Returns True if a block was converted, False otherwise.
254+ """
255+ all_comments = list_comments (leaf .prefix , is_endmarker = False , mode = mode )
256+
257+ # Find the first fmt:off and its matching fmt:on
258+ fmt_off_idx = None
259+ fmt_on_idx = None
260+ for idx , c in enumerate (all_comments ):
261+ if fmt_off_idx is None and c .value in FMT_OFF :
262+ fmt_off_idx = idx
263+ if fmt_off_idx is not None and idx > fmt_off_idx and c .value in FMT_ON :
264+ fmt_on_idx = idx
265+ break
266+
267+ # Only proceed if we found both directives
268+ if fmt_on_idx is None or fmt_off_idx is None :
269+ return False
270+
271+ comment = all_comments [fmt_off_idx ]
272+ fmt_on_comment = all_comments [fmt_on_idx ]
273+ original_prefix = leaf .prefix
274+
275+ # Build the hidden value
276+ start_pos = comment .consumed
277+ end_pos = fmt_on_comment .consumed
278+ content_between_and_fmt_on = original_prefix [start_pos :end_pos ]
279+ hidden_value = comment .value + "\n " + content_between_and_fmt_on
280+
281+ if hidden_value .endswith ("\n " ):
282+ hidden_value = hidden_value [:- 1 ]
283+
284+ # Build the standalone comment prefix
285+ standalone_comment_prefix = (
286+ original_prefix [:previous_consumed ] + "\n " * comment .newlines
287+ )
288+
289+ fmt_off_prefix = original_prefix .split (comment .value )[0 ]
290+ if "\n " in fmt_off_prefix :
291+ fmt_off_prefix = fmt_off_prefix .split ("\n " )[- 1 ]
292+ standalone_comment_prefix += fmt_off_prefix
293+
294+ # Update leaf prefix
295+ leaf .prefix = original_prefix [fmt_on_comment .consumed :]
296+
297+ # Insert the STANDALONE_COMMENT
298+ parent = leaf .parent
299+ assert parent is not None , "INTERNAL ERROR: fmt: on/off handling (prefix only)"
300+
301+ leaf_idx = None
302+ for idx , child in enumerate (parent .children ):
303+ if child is leaf :
304+ leaf_idx = idx
305+ break
306+
307+ assert leaf_idx is not None , "INTERNAL ERROR: fmt: on/off handling (leaf index)"
308+
309+ parent .insert_child (
310+ leaf_idx ,
311+ Leaf (
312+ STANDALONE_COMMENT ,
313+ hidden_value ,
314+ prefix = standalone_comment_prefix ,
315+ fmt_pass_converted_first_leaf = None ,
316+ ),
317+ )
318+ return True
319+
320+
194321def convert_one_fmt_off_pair (
195322 node : Node , mode : Mode , lines : Collection [tuple [int , int ]]
196323) -> bool :
@@ -201,82 +328,112 @@ def convert_one_fmt_off_pair(
201328 for leaf in node .leaves ():
202329 previous_consumed = 0
203330 for comment in list_comments (leaf .prefix , is_endmarker = False , mode = mode ):
204- is_fmt_off = comment .value in FMT_OFF
205- is_fmt_skip = _contains_fmt_skip_comment (comment .value , mode )
206- if (not is_fmt_off and not is_fmt_skip ) or (
207- # Invalid use when `# fmt: off` is applied before a closing bracket.
208- is_fmt_off
209- and leaf .type in CLOSING_BRACKETS
331+ should_process , is_fmt_off , is_fmt_skip = _should_process_fmt_comment (
332+ comment , leaf
333+ )
334+ if not should_process :
335+ previous_consumed = comment .consumed
336+ continue
337+
338+ if not _is_valid_standalone_fmt_comment (
339+ comment , leaf , is_fmt_off , is_fmt_skip
210340 ):
211341 previous_consumed = comment .consumed
212342 continue
213- # We only want standalone comments. If there's no previous leaf or
214- # the previous leaf is indentation, it's a standalone comment in
215- # disguise.
216- if comment .type != STANDALONE_COMMENT :
217- prev = preceding_leaf (leaf )
218- if prev :
219- if is_fmt_off and prev .type not in WHITESPACE :
220- continue
221- if is_fmt_skip and prev .type in WHITESPACE :
222- continue
223343
224344 ignored_nodes = list (generate_ignored_nodes (leaf , comment , mode ))
345+
346+ # Handle comment-only blocks
347+ if not ignored_nodes and is_fmt_off :
348+ if _handle_comment_only_fmt_block (
349+ leaf , comment , previous_consumed , mode
350+ ):
351+ return True
352+ continue
353+
354+ # Need actual nodes to process
225355 if not ignored_nodes :
226356 continue
227357
228- first = ignored_nodes [0 ] # Can be a container node with the `leaf`.
229- parent = first .parent
230- prefix = first .prefix
231- if comment .value in FMT_OFF :
232- first .prefix = prefix [comment .consumed :]
233- if is_fmt_skip :
234- first .prefix = ""
235- standalone_comment_prefix = prefix
236- else :
237- standalone_comment_prefix = (
238- prefix [:previous_consumed ] + "\n " * comment .newlines
239- )
240- hidden_value = "" .join (str (n ) for n in ignored_nodes )
241- comment_lineno = leaf .lineno - comment .newlines
242- if comment .value in FMT_OFF :
243- fmt_off_prefix = ""
244- if len (lines ) > 0 and not any (
245- line [0 ] <= comment_lineno <= line [1 ] for line in lines
246- ):
247- # keeping indentation of comment by preserving original whitespaces.
248- fmt_off_prefix = prefix .split (comment .value )[0 ]
249- if "\n " in fmt_off_prefix :
250- fmt_off_prefix = fmt_off_prefix .split ("\n " )[- 1 ]
251- standalone_comment_prefix += fmt_off_prefix
252- hidden_value = comment .value + "\n " + hidden_value
253- if is_fmt_skip :
254- hidden_value += comment .leading_whitespace + comment .value
255- if hidden_value .endswith ("\n " ):
256- # That happens when one of the `ignored_nodes` ended with a NEWLINE
257- # leaf (possibly followed by a DEDENT).
258- hidden_value = hidden_value [:- 1 ]
259- first_idx : Optional [int ] = None
260- for ignored in ignored_nodes :
261- index = ignored .remove ()
262- if first_idx is None :
263- first_idx = index
264- assert parent is not None , "INTERNAL ERROR: fmt: on/off handling (1)"
265- assert first_idx is not None , "INTERNAL ERROR: fmt: on/off handling (2)"
266- parent .insert_child (
267- first_idx ,
268- Leaf (
269- STANDALONE_COMMENT ,
270- hidden_value ,
271- prefix = standalone_comment_prefix ,
272- fmt_pass_converted_first_leaf = first_leaf_of (first ),
273- ),
358+ # Handle regular fmt blocks
359+
360+ _handle_regular_fmt_block (
361+ ignored_nodes ,
362+ comment ,
363+ previous_consumed ,
364+ is_fmt_skip ,
365+ lines ,
366+ leaf ,
274367 )
275368 return True
276369
277370 return False
278371
279372
373+ def _handle_regular_fmt_block (
374+ ignored_nodes : list [LN ],
375+ comment : ProtoComment ,
376+ previous_consumed : int ,
377+ is_fmt_skip : bool ,
378+ lines : Collection [tuple [int , int ]],
379+ leaf : Leaf ,
380+ ) -> None :
381+ """Handle fmt blocks with actual AST nodes."""
382+ first = ignored_nodes [0 ] # Can be a container node with the `leaf`.
383+ parent = first .parent
384+ prefix = first .prefix
385+
386+ if comment .value in FMT_OFF :
387+ first .prefix = prefix [comment .consumed :]
388+ if is_fmt_skip :
389+ first .prefix = ""
390+ standalone_comment_prefix = prefix
391+ else :
392+ standalone_comment_prefix = prefix [:previous_consumed ] + "\n " * comment .newlines
393+
394+ hidden_value = "" .join (str (n ) for n in ignored_nodes )
395+ comment_lineno = leaf .lineno - comment .newlines
396+
397+ if comment .value in FMT_OFF :
398+ fmt_off_prefix = ""
399+ if len (lines ) > 0 and not any (
400+ line [0 ] <= comment_lineno <= line [1 ] for line in lines
401+ ):
402+ # keeping indentation of comment by preserving original whitespaces.
403+ fmt_off_prefix = prefix .split (comment .value )[0 ]
404+ if "\n " in fmt_off_prefix :
405+ fmt_off_prefix = fmt_off_prefix .split ("\n " )[- 1 ]
406+ standalone_comment_prefix += fmt_off_prefix
407+ hidden_value = comment .value + "\n " + hidden_value
408+
409+ if is_fmt_skip :
410+ hidden_value += comment .leading_whitespace + comment .value
411+
412+ if hidden_value .endswith ("\n " ):
413+ # That happens when one of the `ignored_nodes` ended with a NEWLINE
414+ # leaf (possibly followed by a DEDENT).
415+ hidden_value = hidden_value [:- 1 ]
416+
417+ first_idx : Optional [int ] = None
418+ for ignored in ignored_nodes :
419+ index = ignored .remove ()
420+ if first_idx is None :
421+ first_idx = index
422+
423+ assert parent is not None , "INTERNAL ERROR: fmt: on/off handling (1)"
424+ assert first_idx is not None , "INTERNAL ERROR: fmt: on/off handling (2)"
425+
426+ parent .insert_child (
427+ first_idx ,
428+ Leaf (
429+ STANDALONE_COMMENT ,
430+ hidden_value ,
431+ prefix = standalone_comment_prefix ,
432+ fmt_pass_converted_first_leaf = first_leaf_of (first ),
433+ ),
434+ )
435+
436+
280437def generate_ignored_nodes (
281438 leaf : Leaf , comment : ProtoComment , mode : Mode
282439) -> Iterator [LN ]:
@@ -285,7 +442,7 @@ def generate_ignored_nodes(
285442 If comment is skip, returns leaf only.
286443 Stops at the end of the block.
287444 """
288- if _contains_fmt_skip_comment (comment .value , mode ):
445+ if _contains_fmt_directive (comment .value , FMT_SKIP ):
289446 yield from _generate_ignored_nodes_from_fmt_skip (leaf , comment , mode )
290447 return
291448 container : Optional [LN ] = container_of (leaf )
@@ -548,13 +705,20 @@ def contains_pragma_comment(comment_list: list[Leaf]) -> bool:
548705 return False
549706
550707
551- def _contains_fmt_skip_comment (comment_line : str , mode : Mode ) -> bool :
708+ def _contains_fmt_directive (
709+ comment_line : str , directives : set [str ] = FMT_OFF | FMT_ON | FMT_SKIP
710+ ) -> bool :
552711 """
553- Checks if the given comment contains FMT_SKIP alone or paired with other comments.
712+ Checks if the given comment contains format directives, alone or paired with
713+ other comments.
714+
715+ Defaults to checking all directives (skip, off, on, yapf), but can be
716+ narrowed to specific ones.
717+
554718 Matching styles:
555- # fmt:skip <-- single comment
556- # noqa:XXX # fmt:skip # a nice line <-- multiple comments (Preview)
557- # pylint:XXX; fmt:skip <-- list of comments (; separated, Preview )
719+ # foobar <-- single comment
720+ # foobar # foobar # foobar <-- multiple comments
721+ # foobar; foobar <-- list of comments (; separated)
558722 """
559723 semantic_comment_blocks = [
560724 comment_line ,
@@ -570,4 +734,4 @@ def _contains_fmt_skip_comment(comment_line: str, mode: Mode) -> bool:
570734 ],
571735 ]
572736
573- return any (comment in FMT_SKIP for comment in semantic_comment_blocks )
737+ return any (comment in directives for comment in semantic_comment_blocks )
0 commit comments