Skip to content

feat: enable # fmt: off recognized by snakefmt#280

Merged
mbhall88 merged 22 commits intomasterfrom
fmtoff
Apr 7, 2026
Merged

feat: enable # fmt: off recognized by snakefmt#280
mbhall88 merged 22 commits intomasterfrom
fmtoff

Conversation

@Hocnonsense
Copy link
Copy Markdown
Contributor

@Hocnonsense Hocnonsense commented Mar 27, 2026

Format Directives

snakefmt supports inline comment directives to control formatting behaviour for specific regions of code.

# fmt: off / # fmt: on

Disables all formatting for the region between the two directives. The directives must appear at the same indentation level. A # fmt: on at a deeper indent than the matching # fmt: off has no effect.

rule a:
    input:
        "a.txt",


# fmt: off
rule b:
  input: "b.txt"
  output:
          "c.txt"
# fmt: on


rule c:
    input:
        "d.txt",

Note: inside run: blocks and other Python code, # fmt: off / # fmt: on is passed through to [Black][black] which handles it natively.

# fmt: off[sort]

Disables only directive sorting for the region, while still applying all other formatting. Useful when you want to preserve a custom directive order for a specific rule.

# fmt: off[sort]
rule keep_my_order:
    output:
        "result.txt",
    input:
        "source.txt",
    shell:
        "cp {input} {output}"
# fmt: on[sort]

A plain # fmt: on (without [sort]) also ends a # fmt: off[sort] region.

# fmt: off[next]

Disables formatting for the single next Snakemake keyword block (e.g. rule, checkpoint, use rule). Only that one block is left unformatted; subsequent blocks are formatted normally.

rule formatted:
    input:
        "a.txt",
    output:
        "b.txt",


# fmt: off[next]
rule unformatted:
  input: "a.txt"
  output: "b.txt"


rule also_formatted:
    input:
        "a.txt",

Summary by CodeRabbit

  • New Features

    • Added inline format directives to mark verbatim regions, scoped sorting, and next-line behavior.
  • Documentation

    • Added a Format Directives section with examples; updated Table of Contents, headings, section names, and Recent Changes formatting.
  • Bug Fixes

    • Improved directive recognition, comment/indent handling, keyword-boundary detection, and end-of-file/blank-line preservation.
  • Tests

    • Expanded test suites to cover directives, sorting, boundaries, run-block interactions, and related edge cases.

@Hocnonsense Hocnonsense requested a review from mbhall88 as a code owner March 27, 2026 01:55
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 27, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds parser and formatter support for inline formatting directives (# fmt: off/on, # fmt: off[sort], # fmt: off[next]), implements parser-level fmt-off consumption with indent-stack tracking, updates formatter buffering/emission for verbatim/sort regions, expands tests, and updates README documentation. (No public API removals.)

Changes

Cohort / File(s) Summary
Documentation
README.md
TOC and heading adjustments; new "Format Directives" section describing # fmt: off/on, # fmt: off[sort], # fmt: off[next] with scope/indent rules, examples, # fmt: skip, and minor link/heading fixes.
Parser (core)
snakefmt/parser/parser.py
Added regex parsing for fmt directives, indentation-stack model, multiline-token handling, new _consume_python and _consume_fmt_off, parser fmt-off state (fmt_off, fmt_off_expected_indent, fmt_off_preceded_by_blank_line, sort_off_indent), lookahead comment-indent logic, and extended constructor to accept sort_directives.
Syntax / token lookahead
snakefmt/parser/syntax.py
ParameterSyntax.check_exit now accepts TokenIterator; added cached-token lookahead to simulate INDENT/DEDENT for comment-influenced exit detection and tightened early-exit checks.
Formatter (buffering & emit)
snakefmt/formatter.py
Removed storing sort_directives on Formatter; flush_buffer(...) adds exiting_keywords and handles fmt_off_expected_indent; added flush_fmt_off_region and flush_sort_signal; adjusted newline/EOF behavior and replaced a runtime ValueError with an assert including issue-report guidance.
Tests
tests/test_formatter.py
Large test refactor/additions: new fixtures, new test classes for fmt: off/on, fmt: off[sort], fmt: off[next], imported black.parsing, many updated expectations and input/output fixtures.

Sequence Diagram(s)

sequenceDiagram
    participant Tokenizer as Token Stream
    participant Parser
    participant Formatter
    participant Output

    Tokenizer->>Parser: emit tokens & comments
    Parser->>Parser: detect `# fmt: off` / modifiers
    alt fmt-off encountered
        Parser->>Formatter: flush_buffer(exiting_keywords=True)
        Parser->>Parser: set fmt_off, sort_off_indent, fmt_off_expected_indent
        Parser->>Parser: _consume_fmt_off() collect verbatim region
        Parser->>Formatter: flush_fmt_off_region(verbatim)
        Formatter->>Output: write buffered + verbatim
    end
    Tokenizer->>Parser: emit `# fmt: on` or dedent boundary
    Parser->>Parser: clear fmt_off state and resume keyword parsing
    Parser->>Formatter: resume normal formatting and flushes
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • mbhall88
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.21% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main feature: enabling # fmt: off directive recognition in snakefmt, which is the primary objective of the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fmtoff

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@Hocnonsense Hocnonsense added enhancement New feature or request help wanted Extra attention is needed labels Mar 27, 2026
@Hocnonsense Hocnonsense self-assigned this Mar 27, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
snakefmt/formatter.py (1)

183-188: ⚠️ Potential issue | 🟠 Major

# fmt: off[sort] spans can move to the wrong directive slot.

If self.result already holds a preserved off-sort region, Line 185 caches self.result + param_formatted under the first sortable directive after # fmt: on. post_process_keyword() then reinserts that whole span at the sorted position of that directive, so a mid-rule disabled region does not stay where it appeared. Please flush/freeze the pending sort cache before entering sort-off, or treat the off-sort span as its own barrier entry.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@snakefmt/formatter.py` around lines 183 - 188, When entering the "sort-off"
branch (the condition around fmt_sort_off, in_global_context and keyword_spec in
the block that calls format_params), flush any pending sortable-cache held in
self.result before combining it with param_formatted and storing into
self.keywords; do this by committing the existing self.result as its own barrier
(e.g., call the same flush/commit logic that post_process_keyword would use or
invoke post_process_keyword() to write out the pending span) or push self.result
into self.keywords as a separate entry, then set self.result = "" and only then
store the off-sort span (param_formatted) under the new directive slot so the
mid-rule disabled region stays in place rather than being moved during
post_process_keyword().
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@README.md`:
- Around line 50-77: The TOC's nested numbered list items are not indented
consistently, causing markdownlint MD005; update the README's main numbered list
so each nested level is indented by two spaces (or a consistent number) under
its parent (e.g., indent the subitems under "Install" and the sub-subitems under
"Containers"/"Plug Us") so all nested list levels follow the same indentation
convention and resolve the lint error.

In `@snakefmt/parser/parser.py`:
- Around line 697-706: The Status returned for column-0 formatter directives
currently uses self.cur_indent, which is wrong when the directive appears before
pending DEDENTs; update the Status call in this branch (the one guarded by
fmt_dir and col_nb(token) == 0) to pass 0 for the current indent instead of
self.cur_indent so the directive reports indent 0 (i.e., replace the
self.cur_indent argument in the Status(...) call with 0).

---

Outside diff comments:
In `@snakefmt/formatter.py`:
- Around line 183-188: When entering the "sort-off" branch (the condition around
fmt_sort_off, in_global_context and keyword_spec in the block that calls
format_params), flush any pending sortable-cache held in self.result before
combining it with param_formatted and storing into self.keywords; do this by
committing the existing self.result as its own barrier (e.g., call the same
flush/commit logic that post_process_keyword would use or invoke
post_process_keyword() to write out the pending span) or push self.result into
self.keywords as a separate entry, then set self.result = "" and only then store
the off-sort span (param_formatted) under the new directive slot so the mid-rule
disabled region stays in place rather than being moved during
post_process_keyword().
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 28559cac-9719-4d89-a1dd-2d42dc380d28

📥 Commits

Reviewing files that changed from the base of the PR and between d871276 and a24b5bf.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (5)
  • README.md
  • snakefmt/formatter.py
  • snakefmt/parser/parser.py
  • snakefmt/parser/syntax.py
  • tests/test_formatter.py

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (1)
snakefmt/parser/parser.py (1)

695-705: ⚠️ Potential issue | 🟠 Major

Return cur_indent=0 for column-0 # fmt: directives.

This branch still returns self.cur_indent, so a top-level directive emitted before pending DEDENTs keeps the old nested context open. After a run: or other nested Python block, the following top-level section can be parsed as if it were still nested.

Minimal fix
                     return Status(
-                        token, block_indent, self.cur_indent, buffer, False, pythonable
+                        token, block_indent, 0, buffer, False, pythonable
                     )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@snakefmt/parser/parser.py` around lines 695 - 705, The branch handling
column-0 FMT_DIRECTIVE currently returns self.cur_indent which preserves a
nested context; change it to return cur_indent=0 for column-0 directives so
top-level directives correctly trigger context exit. Specifically, in the block
where fmt_dir is set from FMT_DIRECTIVE.from_token(token) and col_nb(token) == 0
(and the disable/next check), construct the Status using 0 instead of
self.cur_indent (i.e., Status(token, block_indent, 0, buffer, False,
pythonable)), leaving the other arguments and conditional logic unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@snakefmt/parser/parser.py`:
- Around line 174-190: The handler that clears a "# fmt: on[sort]" currently
sets self.fmt_sort_off = None unconditionally, which wipes the global “sorting
disabled” sentinel (initialized to -1 in Formatter) and can re-enable sorting
when sort_params=False; update the branch in parser.py (the block using
FMT_DIRECTIVE, _check_fmt_on and status.token) so that when
_check_fmt_on(status.token) == "sort" you only clear fmt_sort_off to None if it
is not the global sentinel (i.e., if self.fmt_sort_off != -1), otherwise leave
it as -1 to preserve the global disable state.
- Around line 427-441: When parsing inside _consume_python, don't treat an
off[...] directive with modifiers (e.g., FMT_DIRECTIVE.from_token -> fmt_label
with fmt_label.disable and fmt_label.modifiers) as a global formatter toggle;
instead leave it as a plain comment by not mutating self.fmt_sort_off or
self.fmt_off and by pushing the token back (call self.snakefile.denext(token))
so the main loop can handle it — i.e., change the branch that currently bails on
fmt_label.disable to check for fmt_label.modifiers and, if present, treat the
token as a comment (denext + break) rather than applying/clearing state; keep
existing logic for real fmt: off/on (use _check_fmt_on and split_token_lines)
unchanged.

In `@tests/test_formatter.py`:
- Around line 1690-1696: The test fixture strings (e.g., the snakecode variable)
use unnecessary f-string prefixes though they contain no interpolations; remove
the leading f from those string literals so they become plain strings (e.g.,
change f" run:\n" to " run:\n") and do the same for the other similar fixtures
mentioned (the blocks around the other ranges) to avoid F541/flake8 failures
while keeping the test data identical.

---

Duplicate comments:
In `@snakefmt/parser/parser.py`:
- Around line 695-705: The branch handling column-0 FMT_DIRECTIVE currently
returns self.cur_indent which preserves a nested context; change it to return
cur_indent=0 for column-0 directives so top-level directives correctly trigger
context exit. Specifically, in the block where fmt_dir is set from
FMT_DIRECTIVE.from_token(token) and col_nb(token) == 0 (and the disable/next
check), construct the Status using 0 instead of self.cur_indent (i.e.,
Status(token, block_indent, 0, buffer, False, pythonable)), leaving the other
arguments and conditional logic unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a591b7e3-0fcf-4e6c-b1e8-d56ecc656a6b

📥 Commits

Reviewing files that changed from the base of the PR and between a24b5bf and 2913377.

📒 Files selected for processing (2)
  • snakefmt/parser/parser.py
  • tests/test_formatter.py

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 27, 2026

Codecov Report

❌ Patch coverage is 91.81287% with 28 lines in your changes missing coverage. Please review.
✅ Project coverage is 96.52%. Comparing base (d871276) to head (972860c).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
snakefmt/parser/parser.py 92.75% 10 Missing and 10 partials ⚠️
snakefmt/formatter.py 91.11% 1 Missing and 3 partials ⚠️
snakefmt/parser/syntax.py 80.95% 3 Missing and 1 partial ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #280      +/-   ##
==========================================
- Coverage   97.81%   96.52%   -1.29%     
==========================================
  Files          12       12              
  Lines        1188     1497     +309     
  Branches      227      309      +82     
==========================================
+ Hits         1162     1445     +283     
- Misses         11       24      +13     
- Partials       15       28      +13     
Flag Coverage Δ
unittests 96.45% <91.81%> (-1.27%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
snakefmt/formatter.py 98.41% <91.11%> (+0.21%) ⬆️
snakefmt/parser/syntax.py 96.16% <80.95%> (-1.44%) ⬇️
snakefmt/parser/parser.py 94.70% <92.75%> (-3.28%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (5)
tests/test_formatter.py (2)

2103-2110: Rename unused loop variable to suppress linter warning.

The formatted variable is not used in the loop body. Rename it to _formatted to indicate it's intentionally unused.

Suggested fix
     def test_fmt_off_at_start(self):
-        for code, formatted in (
+        for code, _formatted in (
             TestSimpleParamFormatting.example_shell_newline,
             TestSimpleParamFormatting.example_params_newline,
             TestSimpleParamFormatting.example_input_threads_newline,
         ):
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_formatter.py` around lines 2103 - 2110, In test_fmt_off_at_start,
the loop unpacks into (code, formatted) but formatted is unused; rename the
second loop variable to _formatted to suppress the linter warning (i.e., update
the for-loop tuple unpacking in test_fmt_off_at_start that iterates over
TestSimpleParamFormatting.example_shell_newline, example_params_newline, and
example_input_threads_newline to use (code, _formatted) instead of (code,
formatted)).

2144-2150: Same unused variable pattern here.

Suggested fix
     def test_fmt_off_not_on(self):
-        for code, formatted in (
+        for code, _formatted in (
             TestSimpleParamFormatting.example_shell_newline,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_formatter.py` around lines 2144 - 2150, The test
test_fmt_off_not_on declares an unused unpacked variable (for code, formatted in
...) causing the "formatted" variable to be unused; update the loop in
test_fmt_off_not_on to iterate a single variable (for code in
(TestSimpleParamFormatting.example_shell_newline,
TestSimpleParamFormatting.example_params_newline,
TestSimpleParamFormatting.example_input_threads_newline,): ) and adjust
downstream uses to reference only code, or alternatively make each element a
(code, formatted) tuple if formatted is actually needed—modify the loop header
and elements accordingly in the test function.
snakefmt/parser/parser.py (2)

49-64: Consider adding strict=True to zip() for safety.

The split_token_lines function uses zip() without an explicit strict= parameter. Since the line range and splitlines should always match in length for valid tokens, adding strict=True would catch any unexpected mismatches during development.

Suggested improvement
     return zip(
-        range(token.start[0], token.end[0] + 1), token.line.splitlines(keepends=True)
+        range(token.start[0], token.end[0] + 1),
+        token.line.splitlines(keepends=True),
+        strict=True,
     )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@snakefmt/parser/parser.py` around lines 49 - 64, The zip call in
split_token_lines may silently drop mismatches between the range(token.start[0],
token.end[0] + 1) and token.line.splitlines(keepends=True); update the return in
split_token_lines to use zip(..., strict=True) so mismatched lengths raise
immediately (referencing split_token_lines, token.start, token.end, token.line).
Ensure the zip call is the one changed and that the codebase targets a Python
version that supports strict=True.

584-626: Typo in method name: _determe_comment_indent.

The method name has a typo: "determe" should be "determine".

Suggested fix
-    def _determe_comment_indent(self, token: Token) -> int:
+    def _determine_comment_indent(self, token: Token) -> int:

This would also require updating the 6 call sites at lines 425, 549, 606, 636, 643, and 706.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@snakefmt/parser/parser.py` around lines 584 - 626, Rename the misspelled
method _determe_comment_indent to _determine_comment_indent in its definition
and update every call site that references self._determe_comment_indent to
self._determine_comment_indent; ensure the method signature and docstring remain
unchanged and run tests/linters to catch any remaining references (including in
parser logic, unit tests, or other modules) and update them accordingly.
snakefmt/formatter.py (1)

211-238: Review logic for no_formatting_yet assignment in handle_fmt_off_region.

At line 234, self.no_formatting_yet = bool(self.lagging_comments) will always be False because self.lagging_comments was just cleared to "" at line 228. This appears intentional based on the comment at lines 230-232 ("mark that we've emitted content"), but the expression is misleading.

Consider simplifying to make the intent explicit:

Suggested clarification
         if is_nested_next:
-            self.no_formatting_yet = bool(self.lagging_comments)
+            self.no_formatting_yet = False
         else:
             self.no_formatting_yet = True
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@snakefmt/formatter.py` around lines 211 - 238, The assignment to
self.no_formatting_yet inside handle_fmt_off_region is confusing because
bool(self.lagging_comments) will always be False (lagging_comments was cleared
earlier); update the logic to make intent explicit by either computing
had_lagging = bool(self.lagging_comments) before it is cleared and then using
that variable, or simpler: set self.no_formatting_yet = False when
is_nested_next (or True/False as the intended explicit value per the surrounding
comment), referencing handle_fmt_off_region, self.no_formatting_yet,
self.lagging_comments and is_nested_next to locate and change the assignment.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@snakefmt/formatter.py`:
- Around line 211-238: The assignment to self.no_formatting_yet inside
handle_fmt_off_region is confusing because bool(self.lagging_comments) will
always be False (lagging_comments was cleared earlier); update the logic to make
intent explicit by either computing had_lagging = bool(self.lagging_comments)
before it is cleared and then using that variable, or simpler: set
self.no_formatting_yet = False when is_nested_next (or True/False as the
intended explicit value per the surrounding comment), referencing
handle_fmt_off_region, self.no_formatting_yet, self.lagging_comments and
is_nested_next to locate and change the assignment.

In `@snakefmt/parser/parser.py`:
- Around line 49-64: The zip call in split_token_lines may silently drop
mismatches between the range(token.start[0], token.end[0] + 1) and
token.line.splitlines(keepends=True); update the return in split_token_lines to
use zip(..., strict=True) so mismatched lengths raise immediately (referencing
split_token_lines, token.start, token.end, token.line). Ensure the zip call is
the one changed and that the codebase targets a Python version that supports
strict=True.
- Around line 584-626: Rename the misspelled method _determe_comment_indent to
_determine_comment_indent in its definition and update every call site that
references self._determe_comment_indent to self._determine_comment_indent;
ensure the method signature and docstring remain unchanged and run tests/linters
to catch any remaining references (including in parser logic, unit tests, or
other modules) and update them accordingly.

In `@tests/test_formatter.py`:
- Around line 2103-2110: In test_fmt_off_at_start, the loop unpacks into (code,
formatted) but formatted is unused; rename the second loop variable to
_formatted to suppress the linter warning (i.e., update the for-loop tuple
unpacking in test_fmt_off_at_start that iterates over
TestSimpleParamFormatting.example_shell_newline, example_params_newline, and
example_input_threads_newline to use (code, _formatted) instead of (code,
formatted)).
- Around line 2144-2150: The test test_fmt_off_not_on declares an unused
unpacked variable (for code, formatted in ...) causing the "formatted" variable
to be unused; update the loop in test_fmt_off_not_on to iterate a single
variable (for code in (TestSimpleParamFormatting.example_shell_newline,
TestSimpleParamFormatting.example_params_newline,
TestSimpleParamFormatting.example_input_threads_newline,): ) and adjust
downstream uses to reference only code, or alternatively make each element a
(code, formatted) tuple if formatted is actually needed—modify the loop header
and elements accordingly in the test function.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0c0505cb-8592-4982-a50e-7e623574dc9b

📥 Commits

Reviewing files that changed from the base of the PR and between 2913377 and 41bb181.

📒 Files selected for processing (4)
  • README.md
  • snakefmt/formatter.py
  • snakefmt/parser/parser.py
  • tests/test_formatter.py
✅ Files skipped from review due to trivial changes (1)
  • README.md

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (3)
tests/test_formatter.py (1)

2103-2110: Unused loop variable formatted can be renamed to _formatted.

The loop unpacks (code, formatted) but only uses code. Per Python conventions, prefix unused variables with underscore.

♻️ Proposed fix
-        for code, formatted in (
+        for code, _formatted in (
             TestSimpleParamFormatting.example_shell_newline,
             TestSimpleParamFormatting.example_params_newline,
             TestSimpleParamFormatting.example_input_threads_newline,
         ):

Also applies to line 2145 (test_fmt_off_not_on).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_formatter.py` around lines 2103 - 2110, In the tests
test_fmt_off_at_start and test_fmt_off_not_on, the loop unpacks tuples as (code,
formatted) but never uses the formatted variable; rename this unused loop
variable to _formatted (i.e., change (code, formatted) to (code, _formatted)) to
follow Python conventions and silence linters, locating the changes in the test
methods named test_fmt_off_at_start and test_fmt_off_not_on that iterate over
TestSimpleParamFormatting.example_* tuples.
snakefmt/parser/parser.py (2)

62-64: Consider adding strict=True to zip() for defensive programming.

The static analysis tool flags that zip() lacks an explicit strict= parameter. If the line count from the token's start/end range ever diverges from splitlines() output (e.g., due to edge cases in token parsing), a mismatch would silently truncate. Adding strict=True would surface any such inconsistencies immediately.

That said, if the current behavior (silently truncating) is intentional for robustness, this can be safely ignored.

♻️ Optional fix
     return zip(
-        range(token.start[0], token.end[0] + 1), token.line.splitlines(keepends=True)
+        range(token.start[0], token.end[0] + 1), token.line.splitlines(keepends=True),
+        strict=True,
     )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@snakefmt/parser/parser.py` around lines 62 - 64, The zip(...) call that
returns paired lines for a token should be made strict to catch mismatched
lengths: update the return expression that zips range(token.start[0],
token.end[0] + 1) with token.line.splitlines(keepends=True) to pass strict=True
so any divergence raises immediately (adjust import/compat if targeting older
Python versions).

592-592: Minor: method name typo "determe" should be "determine".

The method is named _determe_comment_indent but should likely be _determine_comment_indent. Since this is used in multiple places, this is a minor naming consistency issue that could be addressed in a follow-up.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@snakefmt/parser/parser.py` at line 592, Rename the misspelled method
`_determe_comment_indent` to `_determine_comment_indent` across the codebase:
update the method definition in the class and replace all references/calls to
`_determe_comment_indent` with `_determine_comment_indent` (including any unit
tests or callers). Ensure any imports, references in docstrings, and usages like
in parser logic that expect the old name are updated to the new name to keep
behavior unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@snakefmt/parser/parser.py`:
- Line 425: The inline comment contains a typo: replace the word "diabler" with
"disabler" in the comment text that reads "`# fmt: off[` is not actual format
diabler, it affects limited" so it becomes "`# fmt: off[` is not actual format
disabler, it affects limited"; locate that comment in parser.py (the comment
line containing "`# fmt: off[`") and update the spelling only.

In `@tests/test_formatter.py`:
- Line 1828: The fixture name `sort_with_coments` has a typo; rename it to
`sort_with_comments` wherever it's defined and referenced (e.g., in the test
fixture declaration and any usages in tests) to maintain consistency; update the
symbol `sort_with_coments` to `sort_with_comments` throughout the file so
imports/fixtures and assertions continue to resolve.
- Line 2652: Rename the misspelled variable 'fomatter' to 'formatter' where it's
assigned from setup_formatter (search for setup_formatter(...) and the variable
declaration) and update any subsequent usages accordingly; specifically change
the call at the later usage to use formatter.get_formatted() instead of the old
name or method, ensuring all occurrences (assignment and references) match the
corrected 'formatter' identifier and the get_formatted() method call.

---

Nitpick comments:
In `@snakefmt/parser/parser.py`:
- Around line 62-64: The zip(...) call that returns paired lines for a token
should be made strict to catch mismatched lengths: update the return expression
that zips range(token.start[0], token.end[0] + 1) with
token.line.splitlines(keepends=True) to pass strict=True so any divergence
raises immediately (adjust import/compat if targeting older Python versions).
- Line 592: Rename the misspelled method `_determe_comment_indent` to
`_determine_comment_indent` across the codebase: update the method definition in
the class and replace all references/calls to `_determe_comment_indent` with
`_determine_comment_indent` (including any unit tests or callers). Ensure any
imports, references in docstrings, and usages like in parser logic that expect
the old name are updated to the new name to keep behavior unchanged.

In `@tests/test_formatter.py`:
- Around line 2103-2110: In the tests test_fmt_off_at_start and
test_fmt_off_not_on, the loop unpacks tuples as (code, formatted) but never uses
the formatted variable; rename this unused loop variable to _formatted (i.e.,
change (code, formatted) to (code, _formatted)) to follow Python conventions and
silence linters, locating the changes in the test methods named
test_fmt_off_at_start and test_fmt_off_not_on that iterate over
TestSimpleParamFormatting.example_* tuples.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a2c4e289-3806-4114-8205-c8c14f2a8459

📥 Commits

Reviewing files that changed from the base of the PR and between 41bb181 and afd273c.

📒 Files selected for processing (3)
  • snakefmt/formatter.py
  • snakefmt/parser/parser.py
  • tests/test_formatter.py
✅ Files skipped from review due to trivial changes (1)
  • snakefmt/formatter.py

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
tests/test_formatter.py (1)

2103-2110: Rename unused loop variable formatted to _formatted.

The loop unpacks (code, formatted) but only uses code. The same pattern appears at line 2145. Prefix with underscore to indicate intentional discard and silence the Ruff B007 warning.

♻️ Proposed fix
     def test_fmt_off_at_start(self):
-        for code, formatted in (
+        for code, _formatted in (
             TestSimpleParamFormatting.example_shell_newline,
             TestSimpleParamFormatting.example_params_newline,
             TestSimpleParamFormatting.example_input_threads_newline,
         ):

Also at line 2145:

     def test_fmt_off_not_on(self):
-        for code, formatted in (
+        for code, _formatted in (
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_formatter.py` around lines 2103 - 2110, In the test loop inside
test_fmt_off_at_start (and the second similar loop later) the unpacked variable
named formatted is unused; rename it to _formatted in the tuple unpacking (for
example change "for code, formatted in (...)" to "for code, _formatted in (...)"
where the tuples use TestSimpleParamFormatting.example_shell_newline,
example_params_newline, example_input_threads_newline) to indicate the value is
intentionally discarded and silence the Ruff B007 warning.
snakefmt/parser/parser.py (1)

49-64: Consider adding strict=True to zip() for defensive programming.

The zip() call pairs line numbers with line content. If the ranges ever mismatch (due to edge cases in tokenization), strict=True would raise an error immediately rather than silently truncating.

♻️ Proposed fix
     return zip(
-        range(token.start[0], token.end[0] + 1), token.line.splitlines(keepends=True)
+        range(token.start[0], token.end[0] + 1),
+        token.line.splitlines(keepends=True),
+        strict=True,
     )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@snakefmt/parser/parser.py` around lines 49 - 64, The pairing in
split_token_lines currently uses zip(range(...), token.line.splitlines(...))
which can silently truncate if the number of line numbers and splitlines differ;
update the call in split_token_lines to use zip(..., strict=True) so a mismatch
raises immediately, making tokenization edge-case failures visible and easier to
debug.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@snakefmt/parser/parser.py`:
- Around line 424-432: The long conditional in the block that checks
fmt_label.modifiers exceeds the 88-char limit; shorten it by extracting
self._determine_comment_indent(token) into a local variable (e.g.,
comment_indent) and/or by splitting the compound condition across lines so the
expression "if not self.fmt_off or ( self.fmt_off[1] != 'region' and
self._determine_comment_indent(token) == self.fmt_off[0] )" is rewritten into
multiple shorter lines while preserving logic; update the condition around
fmt_label.modifiers / self.fmt_off and keep the subsequent
self.snakefile.denext(token) and break behavior unchanged.

---

Nitpick comments:
In `@snakefmt/parser/parser.py`:
- Around line 49-64: The pairing in split_token_lines currently uses
zip(range(...), token.line.splitlines(...)) which can silently truncate if the
number of line numbers and splitlines differ; update the call in
split_token_lines to use zip(..., strict=True) so a mismatch raises immediately,
making tokenization edge-case failures visible and easier to debug.

In `@tests/test_formatter.py`:
- Around line 2103-2110: In the test loop inside test_fmt_off_at_start (and the
second similar loop later) the unpacked variable named formatted is unused;
rename it to _formatted in the tuple unpacking (for example change "for code,
formatted in (...)" to "for code, _formatted in (...)" where the tuples use
TestSimpleParamFormatting.example_shell_newline, example_params_newline,
example_input_threads_newline) to indicate the value is intentionally discarded
and silence the Ruff B007 warning.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a7bd98b4-3b5f-4471-bdb8-6952c18c9add

📥 Commits

Reviewing files that changed from the base of the PR and between afd273c and 5814007.

📒 Files selected for processing (2)
  • snakefmt/parser/parser.py
  • tests/test_formatter.py

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 27, 2026

Note

Unit test generation is a public access feature. Expect some limitations and changes as we gather feedback and continue to improve it.


Generating unit tests... This may take up to 20 minutes.

1 similar comment
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 27, 2026

Note

Unit test generation is a public access feature. Expect some limitations and changes as we gather feedback and continue to improve it.


Generating unit tests... This may take up to 20 minutes.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 27, 2026

✅ Created PR with unit tests: #281

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 27, 2026

Request timed out after 900000ms (requestId=b778f470-fd5f-40cf-8eca-6ed6710e5a6b)

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
snakefmt/parser/parser.py (1)

62-64: Consider adding strict=True to zip() for defensive coding.

The zip() call pairs line numbers with line content. While the lengths should match by token semantics, adding strict=True would catch any unexpected mismatch during development.

♻️ Suggested change
     return zip(
-        range(token.start[0], token.end[0] + 1), token.line.splitlines(keepends=True)
+        range(token.start[0], token.end[0] + 1), token.line.splitlines(keepends=True),
+        strict=True,
     )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@snakefmt/parser/parser.py` around lines 62 - 64, Update the zip call that
pairs token line numbers with line contents to use strict=True to detect length
mismatches at runtime; specifically change the return statement that currently
calls zip(range(token.start[0], token.end[0] + 1),
token.line.splitlines(keepends=True)) to pass strict=True (i.e., zip(..., ...,
strict=True)) so any unexpected mismatch between the range and
token.line.splitlines() is raised immediately; ensure the change compiles
against the project's minimum Python version.
tests/test_formatter.py (1)

2103-2110: Rename unused loop variable formatted to _formatted.

The loop iterates over (code, formatted) tuples but only uses code. Following Python convention, prefix unused variables with underscore.

♻️ Suggested change
     def test_fmt_off_at_start(self):
-        for code, formatted in (
+        for code, _formatted in (
             TestSimpleParamFormatting.example_shell_newline,
             TestSimpleParamFormatting.example_params_newline,
             TestSimpleParamFormatting.example_input_threads_newline,
         ):

Also applies to test_fmt_off_not_on at line 2145.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_formatter.py` around lines 2103 - 2110, In the two tests
test_fmt_off_at_start and test_fmt_off_not_on change the unused loop variable
name from formatted to _formatted when unpacking the (code, formatted) tuples so
the loops read (code, _formatted); update references in those test functions
(test_fmt_off_at_start and test_fmt_off_not_on) to use the new _formatted
variable name to follow Python convention for unused variables.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@snakefmt/parser/parser.py`:
- Around line 469-504: Rename the private method _comsume_fmt_off_in_python to
_consume_fmt_off_in_python to fix the typo; update every call site that
references _comsume_fmt_off_in_python (for example the call near where Python
fmt directives are handled) to the new name, and ensure any internal
references/docstrings remain consistent (no behavioral changes).

---

Nitpick comments:
In `@snakefmt/parser/parser.py`:
- Around line 62-64: Update the zip call that pairs token line numbers with line
contents to use strict=True to detect length mismatches at runtime; specifically
change the return statement that currently calls zip(range(token.start[0],
token.end[0] + 1), token.line.splitlines(keepends=True)) to pass strict=True
(i.e., zip(..., ..., strict=True)) so any unexpected mismatch between the range
and token.line.splitlines() is raised immediately; ensure the change compiles
against the project's minimum Python version.

In `@tests/test_formatter.py`:
- Around line 2103-2110: In the two tests test_fmt_off_at_start and
test_fmt_off_not_on change the unused loop variable name from formatted to
_formatted when unpacking the (code, formatted) tuples so the loops read (code,
_formatted); update references in those test functions (test_fmt_off_at_start
and test_fmt_off_not_on) to use the new _formatted variable name to follow
Python convention for unused variables.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8565f45b-c25e-4065-b0db-738081a008d6

📥 Commits

Reviewing files that changed from the base of the PR and between afd273c and a418503.

📒 Files selected for processing (3)
  • snakefmt/formatter.py
  • snakefmt/parser/parser.py
  • tests/test_formatter.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • snakefmt/formatter.py

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
snakefmt/parser/parser.py (1)

49-64: Consider adding strict=True to zip() for safety.

The static analysis flags that zip() lacks an explicit strict= parameter. While the line count should theoretically match, adding strict=True would catch any unexpected mismatches during development.

💡 Suggested fix
     return zip(
-        range(token.start[0], token.end[0] + 1), token.line.splitlines(keepends=True)
+        range(token.start[0], token.end[0] + 1),
+        token.line.splitlines(keepends=True),
+        strict=True,
     )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@snakefmt/parser/parser.py` around lines 49 - 64, The zip in split_token_lines
can silently drop mismatched sequences; update the return to use zip(...,
strict=True) so split_token_lines(token: tokenize.TokenInfo) raises if the range
of line numbers (range(token.start[0], token.end[0] + 1)) and
token.line.splitlines(keepends=True) lengths differ, preventing silent
mismatches during development.
snakefmt/formatter.py (2)

127-133: Consider using an explicit exception instead of assert for production safety.

The assert statement can be disabled when Python runs with optimization flags (-O or -OO). If this invariant fails in production, the code would continue with undefined behavior instead of raising an error.

💡 Suggested alternative
-                assert re_rematch, (
-                    "This should always match as we just formatted it with the same "
-                    "regex. If this error is raised, it's a bug in snakefmt's "
-                    "handling of snakemake syntax. Please report this to the "
-                    "developers with the code so we can fix it: "
-                    "https://github.com/snakemake/snakefmt/issues"
-                )
+                if not re_rematch:
+                    raise RuntimeError(
+                        "This should always match as we just formatted it with the same "
+                        "regex. If this error is raised, it's a bug in snakefmt's "
+                        "handling of snakemake syntax. Please report this to the "
+                        "developers with the code so we can fix it: "
+                        "https://github.com/snakemake/snakefmt/issues"
+                    )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@snakefmt/formatter.py` around lines 127 - 133, Replace the
runtime-conditional assert on re_rematch with an explicit exception to ensure
the invariant is enforced even when Python is optimized; locate the assert
re_rematch in formatter.py (the block that checks the regex match after
formatting) and raise a clear exception type (e.g., RuntimeError or ValueError)
with the same descriptive message about a bug in snakefmt's handling of
snakemake syntax and the report URL so failures cannot be silently skipped under
-O/-OO.

198-204: Same consideration: assert can be disabled in optimized mode.

This assertion guards an internal invariant. If running with -O, a failed invariant would silently continue instead of alerting the user.

💡 Suggested alternative
-        assert not self.keywords, (
-            "All directives should have been consumed; "
-            "if not, this is a bug in snakefmt's handling of snakemake syntax. "
-            "It must be the coder's fault, not the user's. "
-            "So please report this to the developers with the code so we can fix it: "
-            "https://github.com/snakemake/snakefmt/issues"
-        )
+        if self.keywords:
+            raise RuntimeError(
+                "All directives should have been consumed; "
+                "if not, this is a bug in snakefmt's handling of snakemake syntax. "
+                "It must be the coder's fault, not the user's. "
+                "So please report this to the developers with the code so we can fix it: "
+                "https://github.com/snakemake/snakefmt/issues"
+            )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@snakefmt/formatter.py` around lines 198 - 204, The assert guarding that all
directives were consumed (the assertion checking self.keywords) must not rely on
Python's assert (which can be disabled with -O); replace the assert with an
explicit runtime check that raises an AssertionError (or RuntimeError) with the
same message when self.keywords is truthy. Locate the assertion involving
self.keywords in snakefmt/formatter.py and change it to an if self.keywords:
raise AssertionError("All directives should have been consumed; if not, this is
a bug in snakefmt's handling of snakemake syntax. It must be the coder's fault,
not the user's. So please report this to the developers with the code so we can
fix it: https://github.com/snakemake/snakefmt/issues") so the invariant always
fails loudly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@snakefmt/formatter.py`:
- Around line 127-133: Replace the runtime-conditional assert on re_rematch with
an explicit exception to ensure the invariant is enforced even when Python is
optimized; locate the assert re_rematch in formatter.py (the block that checks
the regex match after formatting) and raise a clear exception type (e.g.,
RuntimeError or ValueError) with the same descriptive message about a bug in
snakefmt's handling of snakemake syntax and the report URL so failures cannot be
silently skipped under -O/-OO.
- Around line 198-204: The assert guarding that all directives were consumed
(the assertion checking self.keywords) must not rely on Python's assert (which
can be disabled with -O); replace the assert with an explicit runtime check that
raises an AssertionError (or RuntimeError) with the same message when
self.keywords is truthy. Locate the assertion involving self.keywords in
snakefmt/formatter.py and change it to an if self.keywords: raise
AssertionError("All directives should have been consumed; if not, this is a bug
in snakefmt's handling of snakemake syntax. It must be the coder's fault, not
the user's. So please report this to the developers with the code so we can fix
it: https://github.com/snakemake/snakefmt/issues") so the invariant always fails
loudly.

In `@snakefmt/parser/parser.py`:
- Around line 49-64: The zip in split_token_lines can silently drop mismatched
sequences; update the return to use zip(..., strict=True) so
split_token_lines(token: tokenize.TokenInfo) raises if the range of line numbers
(range(token.start[0], token.end[0] + 1)) and
token.line.splitlines(keepends=True) lengths differ, preventing silent
mismatches during development.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c369a3db-6bf1-451b-a3d0-38f876d35d03

📥 Commits

Reviewing files that changed from the base of the PR and between a418503 and 2ed3f9d.

📒 Files selected for processing (2)
  • snakefmt/formatter.py
  • snakefmt/parser/parser.py

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
tests/test_formatter.py (2)

2103-2110: Consider renaming unused loop variable to _formatted.

The formatted variable is intentionally unused here because # fmt: off at the start means the original code should remain unformatted. Renaming to _formatted would satisfy the linter and signal intent.

-        for code, formatted in (
+        for code, _formatted in (
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_formatter.py` around lines 2103 - 2110, In test_fmt_off_at_start
rename the intentionally unused loop variable formatted to _formatted in the for
loop (for code, _formatted in (...)) so the linter knows it’s unused; update the
tuple unpack in the test_fmt_off_at_start function and leave all logic unchanged
(references: test_fmt_off_at_start, variables code and formatted/_formatted).

2144-2156: Same unused variable pattern here.

Same situation as test_fmt_off_at_start — the formatted variable is intentionally unused because the test verifies that formatting is disabled when # fmt: off is present.

-        for code, formatted in (
+        for code, _formatted in (
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_formatter.py` around lines 2144 - 2156, In test_fmt_off_not_on the
loop unpacks into (code, formatted) but never uses formatted; change the unpack
to use a throwaway name (e.g., for code, _ in ...) or iterate only over the
first element (for code in ...) so the unused variable is removed; update the
loop header in test_fmt_off_not_on to reference the throwaway "_" or iterate
just over TestSimpleParamFormatting.example_* entries.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@snakefmt/parser/parser.py`:
- Around line 49-64: In split_token_lines, prevent silent truncation by calling
zip with strict=True so mismatched lengths between range(token.start[0],
token.end[0] + 1) and token.line.splitlines(keepends=True) raise instead of
dropping interior lines; update the zip invocation in split_token_lines to
include strict=True and preserve the existing use of
token.line.splitlines(keepends=True) so you can then handle or assert the
mismatch explicitly (this ensures split_token_lines will surface cases where
tokenize.TokenInfo.line does not contain all physical lines).

---

Nitpick comments:
In `@tests/test_formatter.py`:
- Around line 2103-2110: In test_fmt_off_at_start rename the intentionally
unused loop variable formatted to _formatted in the for loop (for code,
_formatted in (...)) so the linter knows it’s unused; update the tuple unpack in
the test_fmt_off_at_start function and leave all logic unchanged (references:
test_fmt_off_at_start, variables code and formatted/_formatted).
- Around line 2144-2156: In test_fmt_off_not_on the loop unpacks into (code,
formatted) but never uses formatted; change the unpack to use a throwaway name
(e.g., for code, _ in ...) or iterate only over the first element (for code in
...) so the unused variable is removed; update the loop header in
test_fmt_off_not_on to reference the throwaway "_" or iterate just over
TestSimpleParamFormatting.example_* entries.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b91bb32a-c9e4-4b93-9aa3-6e67cbe457a5

📥 Commits

Reviewing files that changed from the base of the PR and between 2ed3f9d and 41ea8c3.

📒 Files selected for processing (4)
  • README.md
  • snakefmt/formatter.py
  • snakefmt/parser/parser.py
  • tests/test_formatter.py
✅ Files skipped from review due to trivial changes (1)
  • README.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • snakefmt/formatter.py

Copy link
Copy Markdown
Member

@mbhall88 mbhall88 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks so much for tackling this @Hocnonsense! And great start.

I have added some comments mostly to the tests as these are the only part I really care about. There are some changes I don't like, such as extracting some of the test cases out of the relevant functions into variables outside the tests. I understand this has been done to be able to reuse them in later tests, but I would rather copy and past them into the later tests so that each test can be read completely in isolation and understand what is being tested and whether the formatting makes sense. I can't do this with the updated structure.

We should also support # fmt: skip to skip formatting a single line (see black's docs).

How does the indenting work in the example where the # fmt: on is indented closer to 0 than the previous # fmt: off? Or does the unindentation implicitly just turn the formatting back on, so the # fat: on has no effect? Might be nice to have a test for this.

There is intermittent test code cases where you aren't using explicit visual tab indication. e.g., f"{TAB * 1}..." etc. And also cases where you have f"{TAB}{TAB}.." should be changed to f"{TAB * 2}..."

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
tests/test_formatter.py (1)

2097-2103: Unused loop variable formatted should be renamed to _formatted.

The loop unpacks (code, formatted) but only uses code. Renaming the unused variable to _formatted follows Python convention for intentionally unused variables and silences the Ruff B007 warning.

-        for code, formatted in (
+        for code, _formatted in (

This also applies to line 2153.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_formatter.py` around lines 2097 - 2103, Rename the unused loop
variable formatted to _formatted in the test loops that unpack (code, formatted)
— specifically around the uses of
TestSimpleParamFormatting.example_shell_newline,
TestSimpleParamFormatting.example_params_newline,
TestSimpleParamFormatting.example_input_threads_newline and the second
occurrence around line 2153 — so the for loop becomes for code, _formatted in
(...) and satisfies the Ruff B007 convention; no other logic changes are needed,
test helpers like setup_formatter() remain unchanged.
snakefmt/formatter.py (1)

238-241: Redundant self.fmt_off check on line 240.

The condition if self.fmt_off and not self.result.endswith("\n\n\n") is inside an if self.fmt_off: block, making the inner self.fmt_off check redundant.

Suggested fix
         if self.fmt_off:
             if self.fmt_off[0] == 0 and not self.no_formatting_yet:
-                if self.fmt_off and not self.result.endswith("\n\n\n"):
+                if not self.result.endswith("\n\n\n"):
                     self.result += "\n\n"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@snakefmt/formatter.py` around lines 238 - 241, The inner redundant check `if
self.fmt_off` inside the outer `if self.fmt_off:` block should be removed:
inside the method where you manipulate formatting state (the block referencing
self.fmt_off, self.no_formatting_yet and self.result.endswith), simplify the
nested condition so it only tests `if self.fmt_off[0] == 0 and not
self.no_formatting_yet:` then `if not self.result.endswith("\n\n\n"):` before
appending `"\n\n"`; update the condition references in that block (the ones
using self.fmt_off, self.no_formatting_yet, and self.result.endswith)
accordingly to eliminate the redundant check.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@snakefmt/formatter.py`:
- Around line 238-241: The inner redundant check `if self.fmt_off` inside the
outer `if self.fmt_off:` block should be removed: inside the method where you
manipulate formatting state (the block referencing self.fmt_off,
self.no_formatting_yet and self.result.endswith), simplify the nested condition
so it only tests `if self.fmt_off[0] == 0 and not self.no_formatting_yet:` then
`if not self.result.endswith("\n\n\n"):` before appending `"\n\n"`; update the
condition references in that block (the ones using self.fmt_off,
self.no_formatting_yet, and self.result.endswith) accordingly to eliminate the
redundant check.

In `@tests/test_formatter.py`:
- Around line 2097-2103: Rename the unused loop variable formatted to _formatted
in the test loops that unpack (code, formatted) — specifically around the uses
of TestSimpleParamFormatting.example_shell_newline,
TestSimpleParamFormatting.example_params_newline,
TestSimpleParamFormatting.example_input_threads_newline and the second
occurrence around line 2153 — so the for loop becomes for code, _formatted in
(...) and satisfies the Ruff B007 convention; no other logic changes are needed,
test helpers like setup_formatter() remain unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 737314a3-8707-4aaa-93ed-615d62df92c3

📥 Commits

Reviewing files that changed from the base of the PR and between 41ea8c3 and bfa6158.

📒 Files selected for processing (3)
  • snakefmt/formatter.py
  • snakefmt/parser/parser.py
  • tests/test_formatter.py

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
tests/test_formatter.py (1)

2369-2385: ⚠️ Potential issue | 🟡 Minor

Pipeline failures: unused variables and f-strings without placeholders.

This test has several issues flagged by CI:

  1. formatter is assigned but never used (line 2370)
  2. expected is assigned but never used (line 2376)
  3. f-strings without placeholders (lines 2372-2374)

The test assertion is commented out with a TODO, leaving dead code. Either complete the test implementation or remove the unreachable code to fix the pipeline.

🔧 Proposed fix: Either implement the test or mark it as skipped

Option 1 - Mark as skipped until implemented:

+    `@pytest.mark.skip`(reason="# fmt: skip in directives not yet supported")
     def test_fmt_skip_in_directive(self):
-        formatter = setup_formatter(
+        snakecode = (
             "rule a:\n"
-            f" params:\n"
-            f"  x = [ 1,2,3] # fmt: skip\n"
-            f" input: a= 'sth'   # fmt: skip\n"
+            " params:\n"
+            "  x = [ 1,2,3] # fmt: skip\n"
+            " input: a= 'sth'   # fmt: skip\n"
         )
         expected = (
             "rule a:\n"
             f"{TAB}params:\n"
             f"{TAB * 2}x=[1, 2, 3],  # fmt: skip\n"
             f"{TAB}input:\n"
             f'{TAB * 2}a="sth",  # fmt: skip\n'
         )
-        # TODO: currently `# fmt: skip` in directives is not supported
-        # assert formatter.get_formatted() == expected
+        # TODO: currently `# fmt: skip` in directives is not supported
+        # assert setup_formatter(snakecode).get_formatted() == expected

Option 2 - Remove the test entirely until the feature is implemented.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_formatter.py` around lines 2369 - 2385, The test function
test_fmt_skip_in_directive contains dead/unusable code: it creates formatter via
setup_formatter, builds expected using f-strings without placeholders and leaves
the assertion commented out, causing unused-variable and f-string lint errors;
fix by either implementing the test properly (use non-f-string literals or put
placeholders in those f-strings, call formatter.get_formatted() and assert
equality with expected) or mark the whole test as skipped/removed: update
test_fmt_skip_in_directive to call formatter.get_formatted() and assert against
expected (ensuring TAB is a real variable or replace f-strings with plain
strings), or replace the function with a pytest.skip() decorator or remove it
completely to eliminate unused-variable and f-string issues.
🧹 Nitpick comments (2)
tests/test_formatter.py (2)

2096-2103: Unused loop variable formatted should be renamed to _formatted.

The loop unpacks tuples but only uses code. Rename the unused variable to suppress the B007 warning.

♻️ Proposed fix
     def test_fmt_off_at_start(self):
-        for code, formatted in (
+        for code, _formatted in (
             TestSimpleParamFormatting.example_shell_newline,
             TestSimpleParamFormatting.example_params_newline,
             TestSimpleParamFormatting.example_input_threads_newline,
         ):
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_formatter.py` around lines 2096 - 2103, In test_fmt_off_at_start,
the for-loop unpacks tuples into (code, formatted) but never uses formatted;
rename the unused variable to _formatted to silence the B007 warning. Update the
loop header in test_fmt_off_at_start to use (code, _formatted) when iterating
over TestSimpleParamFormatting.example_shell_newline,
TestSimpleParamFormatting.example_params_newline, and
TestSimpleParamFormatting.example_input_threads_newline so the assert using
setup_formatter(expected).get_formatted() remains unchanged.

2147-2164: Same issue: unused loop variable formatted.

♻️ Proposed fix
     def test_fmt_off_not_on(self):
         """`
         - `# fmt: on` at a deeper indentation level than `# fmt: off` has no effect
         - `# fmt: off` keeps the rest of the code unformatted until a same-indent
             `# fmt: on` found
         """
-        for code, formatted in (
+        for code, _formatted in (
             TestSimpleParamFormatting.example_shell_newline,
             TestSimpleParamFormatting.example_params_newline,
             TestSimpleParamFormatting.example_input_threads_newline,
         ):
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_formatter.py` around lines 2147 - 2164, The test
test_fmt_off_not_on declares an unused loop variable "formatted" by iterating
with "for code, formatted in (TestSimpleParamFormatting.example_shell_newline,
...)" even though each tuple element is a single string; fix by iterating only
over codes (use "for code in (TestSimpleParamFormatting.example_shell_newline,
TestSimpleParamFormatting.example_params_newline,
TestSimpleParamFormatting.example_input_threads_newline):") so the loop unpacks
correctly and "formatted" is not required, ensuring
setup_formatter(expected).get_formatted() compares as intended; update the loop
in test_fmt_off_not_on and keep references to setup_formatter and the
TestSimpleParamFormatting example attributes intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@tests/test_formatter.py`:
- Around line 2369-2385: The test function test_fmt_skip_in_directive contains
dead/unusable code: it creates formatter via setup_formatter, builds expected
using f-strings without placeholders and leaves the assertion commented out,
causing unused-variable and f-string lint errors; fix by either implementing the
test properly (use non-f-string literals or put placeholders in those f-strings,
call formatter.get_formatted() and assert equality with expected) or mark the
whole test as skipped/removed: update test_fmt_skip_in_directive to call
formatter.get_formatted() and assert against expected (ensuring TAB is a real
variable or replace f-strings with plain strings), or replace the function with
a pytest.skip() decorator or remove it completely to eliminate unused-variable
and f-string issues.

---

Nitpick comments:
In `@tests/test_formatter.py`:
- Around line 2096-2103: In test_fmt_off_at_start, the for-loop unpacks tuples
into (code, formatted) but never uses formatted; rename the unused variable to
_formatted to silence the B007 warning. Update the loop header in
test_fmt_off_at_start to use (code, _formatted) when iterating over
TestSimpleParamFormatting.example_shell_newline,
TestSimpleParamFormatting.example_params_newline, and
TestSimpleParamFormatting.example_input_threads_newline so the assert using
setup_formatter(expected).get_formatted() remains unchanged.
- Around line 2147-2164: The test test_fmt_off_not_on declares an unused loop
variable "formatted" by iterating with "for code, formatted in
(TestSimpleParamFormatting.example_shell_newline, ...)" even though each tuple
element is a single string; fix by iterating only over codes (use "for code in
(TestSimpleParamFormatting.example_shell_newline,
TestSimpleParamFormatting.example_params_newline,
TestSimpleParamFormatting.example_input_threads_newline):") so the loop unpacks
correctly and "formatted" is not required, ensuring
setup_formatter(expected).get_formatted() compares as intended; update the loop
in test_fmt_off_not_on and keep references to setup_formatter and the
TestSimpleParamFormatting example attributes intact.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 85a20e19-6853-4b35-88e5-647f90ed36af

📥 Commits

Reviewing files that changed from the base of the PR and between bfa6158 and 04bd28a.

📒 Files selected for processing (3)
  • README.md
  • snakefmt/formatter.py
  • tests/test_formatter.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • snakefmt/formatter.py

@Hocnonsense Hocnonsense requested a review from mbhall88 March 31, 2026 03:08
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/test_formatter.py`:
- Around line 2097-2101: The loop unpacks tuples like (code, formatted) using
TestSimpleParamFormatting.example_shell_newline,
TestSimpleParamFormatting.example_params_newline, and
TestSimpleParamFormatting.example_input_threads_newline but never uses the
formatted variable, causing Ruff B007; rename the unused variable to _formatted
(or _) in the for-loop header (e.g., change "for code, formatted in (...):" to
"for code, _formatted in (...):") in the loops at the occurrences around the
TestSimpleParamFormatting examples (also update the similar loop around lines
with the same examples at the second location).
- Around line 2369-2386: The test test_fmt_skip_in_directive does not actually
assert the formatted output against the expected string (it uses a no-op "assert
expected"), so update the assertion to compare the two: replace the current
assertions with a single assertion that formatter.get_formatted() == expected;
keep or update the TODO comment about unsupported `# fmt: skip` in directives if
desired, but ensure the primary check uses formatter.get_formatted() and
expected so the test fails on regressions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 46995c87-d1b6-4406-a5f6-11769c16db04

📥 Commits

Reviewing files that changed from the base of the PR and between 04bd28a and bddeae1.

📒 Files selected for processing (1)
  • tests/test_formatter.py

Comment on lines +2097 to +2101
for code, formatted in (
TestSimpleParamFormatting.example_shell_newline,
TestSimpleParamFormatting.example_params_newline,
TestSimpleParamFormatting.example_input_threads_newline,
):
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Unused loop variable formatted triggers Ruff B007 warnings.

formatted is unpacked but unused in these loops. Rename to _formatted (or _) to keep lint clean.

Suggested fix
-        for code, formatted in (
+        for code, _formatted in (
             TestSimpleParamFormatting.example_shell_newline,
             TestSimpleParamFormatting.example_params_newline,
             TestSimpleParamFormatting.example_input_threads_newline,
         ):

-        for code, formatted in (
+        for code, _formatted in (
             TestSimpleParamFormatting.example_shell_newline,
             TestSimpleParamFormatting.example_params_newline,
             TestSimpleParamFormatting.example_input_threads_newline,
         ):

Also applies to: 2153-2157

🧰 Tools
🪛 Ruff (0.15.7)

[warning] 2097-2097: Loop control variable formatted not used within loop body

Rename unused formatted to _formatted

(B007)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_formatter.py` around lines 2097 - 2101, The loop unpacks tuples
like (code, formatted) using TestSimpleParamFormatting.example_shell_newline,
TestSimpleParamFormatting.example_params_newline, and
TestSimpleParamFormatting.example_input_threads_newline but never uses the
formatted variable, causing Ruff B007; rename the unused variable to _formatted
(or _) in the for-loop header (e.g., change "for code, formatted in (...):" to
"for code, _formatted in (...):") in the loops at the occurrences around the
TestSimpleParamFormatting examples (also update the similar loop around lines
with the same examples at the second location).

@Hocnonsense
Copy link
Copy Markdown
Contributor Author

@mbhall88 Thanks for the detailed review! Here is a summary of what has been addressed:

  • Test structure: I've kept the shared class variables for example_* and sorting_* cases, as they express the invariant that # fmt: off suppresses exactly the formatting that would otherwise apply.
    I've added inline comments to the progressive asserts in test_fmt_off_sort to make the intent of each step clear.
    Happy to inline everything if you feel strongly about it.
  • # fmt: skip: Implemented for plain Python lines, matching Black's behaviour.
    Support for Snakemake directive lines (e.g. input:, output:) is not yet implemented, where the parameter parsing path (ParameterSyntax) would require a separate, more involved change.
    A commented-out test and a note in the README document this limitation.
  • # fmt: on at deeper indent: The behaviour is now documented in the test_fmt_off_not_on docstring:
    • a # fmt: on at a deeper indentation level than the matching # fmt: off has no effect;
    • formatting remains disabled until a # fmt: on at the same level is found,
    • or until the code dedents past the # fmt: off level.
  • # fmt: off[sort] between directives: Updated both the implementation and the test to match your expected behaviour:
    • the region between # fmt: off[sort] and # fmt: on[sort] stays in place,
    • with directives above and below sorted independently.
  • TAB consistency: Unified to {TAB * N} style.

@mbhall88
Copy link
Copy Markdown
Member

mbhall88 commented Apr 1, 2026

Great work. Two things from me:

  1. I tested out the skip functionality but maybe I am not understanding something?
rule:
    input:
        "test.txt"
    output:
        "test.out"
    params:
        test_param="test",
        long_param=lambda wildcards, input: "this is a really long line that should get formatted {}".format(input[0])  # fmt: skip
    shell:
        "echo {input} > {output}"

gets formatted to

rule:
    input:
        "test.txt",
    output:
        "test.out",
    params:
        test_param="test",
        long_param=lambda wildcards, input: "this is a really long line that should get formatted {}".format(
            input[0]
        ),  # fmt: skip
    shell:
        "echo {input} > {output}"

I would expect that long line to remain as it is.

  1. do the # fmt: off lines have to be on their own separate lines, or can they be inline, say
rule:
    input:  # fmt: off

and if they can be inline, do they apple to the line they are on, or just subsequent lines?

@Hocnonsense
Copy link
Copy Markdown
Contributor Author

@mbhall88 Thanks for reviewing!

  1. You've hit exactly the known limitation documented in the README:
    # fmt: skip currently only applies to plain Python lines, because those are passed through to Black which handles the directive natively.
    Parameter lines inside Snakemake directives (e.g. rule:, conda:) go through a separate parsing path (ParameterSyntax) that does not yet keep original python lines or honour # fmt: skip.
    I'd prefer to land the current partial support now and address directive-level # fmt: skip in a follow-up PR (but happy to do it in this PR if you'd rather have it complete).

  2. # fmt: off and # fmt: on must appear on their own line (i.e. as a standalone comment, not inline after code).
    An inline occurrence such as input: # fmt: off is treated as a plain comment and has no effect.
    This matches Black's behaviour.

@mbhall88
Copy link
Copy Markdown
Member

mbhall88 commented Apr 1, 2026

1. You've hit exactly the known limitation documented in the README:
   `# fmt: skip` currently only applies to plain Python lines, because those are passed through to Black which handles the directive natively.
   Parameter lines inside Snakemake directives (e.g. `rule:`, `conda:`) go through a separate parsing path (`ParameterSyntax`) that does not yet keep original python lines or honour `# fmt: skip`.
   I'd prefer to land the current partial support now and address directive-level `# fmt: skip` in a follow-up PR (but happy to do it in this PR if you'd rather have it complete).

Ah okay, I see. It would be nice to have the full support straight away, but I am happy to merge this PR for now, and then we work on adding skip before the next release? I had hoped we would be able to just pass the comment straight through to black for skip as it is a little more manageable to do it inline rather than the more complex block commenting

2. `# fmt: off` and `# fmt: on` must appear on their own line (i.e. as a standalone comment, not inline after code).
   An inline occurrence such as `input:  # fmt: off` is treated as a plain comment and has no effect.
   This matches Black's behaviour.

Okay, good to know. Might be good to make this explicit in the README

@Hocnonsense
Copy link
Copy Markdown
Contributor Author

Thanks! Happy to merge now, and I'm happy to follow up with full directive-level # fmt: skip support.

Regarding passing it through to Black: the challenge is that parameter lines inside rules go through ParameterSyntax rather than Black directly, so there is no clean passthrough path for now. But agreed it is worth solving properly.

@Hocnonsense Hocnonsense requested a review from mbhall88 April 1, 2026 11:56
@mbhall88 mbhall88 merged commit e7201e1 into master Apr 7, 2026
10 of 12 checks passed
@mbhall88 mbhall88 deleted the fmtoff branch April 7, 2026 01:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request help wanted Extra attention is needed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

introduce # fmt: off and # fmt: on tags to skip formatting within files

2 participants