Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 81 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ design and specifications of [Black][black].
> `--diff` or `--check` options. See [Usage](#usage) for more details.

> [!IMPORTANT]
> **Recent Changes:**
> **Recent Changes:**
> 1. **Rule and module directives are now sorted by default:** `snakefmt` will automatically sort the order of directives inside rules (e.g. `input`, `output`, `shell`) and modules into a consistent order. You can opt out of this by using the `--no-sort` CLI flag.
> 2. **Black upgraded to v26:** The underlying `black` formatter has been upgraded to v26. You will see changes in how implicitly concatenated strings are wrapped (they are now collapsed onto a single line if they fit within the line limit) and other minor adjustments compared to previous versions.
>
>
> **Example of expected differences:**
> ```python
> # Before (Snakefmt older versions)
Expand All @@ -33,7 +33,7 @@ design and specifications of [Black][black].
> "b.txt",
> input:
> "a.txt",
>
>
> # After (Directives sorted, strings collapsed by Black 26)
> rule example:
> input:
Expand All @@ -51,18 +51,23 @@ design and specifications of [Black][black].
- [PyPi](#pypi)
- [Conda](#conda)
- [Containers](#containers)
- [Docker](#docker)
- [Singularity](#singularity)
- [Local](#local)
- [Example File](#example-file)
- [Usage](#usage)
- [Basic Usage](#basic-usage)
- [Full Usage](#full-usage)
- [Configuration](#configuration)
- [Directive Sorting](#directive-sorting)
- [Format Directives](#format-directives)
- [Integration](#integration)
- [Editor Integration](#editor-integration)
- [Version Control Integration](#version-control-integration)
- [Github Actions](#github-actions)
- [Editor Integration](#editor-integration)
- [Version Control Integration](#version-control-integration)
- [GitHub Actions](#github-actions)
- [Plug Us](#plug-us)
- [Markdown](#markdown)
- [ReStructuredText](#restructuredtext)
- [Changes](#changes)
- [Contributing](#contributing)
- [Cite](#cite)
Expand Down Expand Up @@ -313,6 +318,76 @@ This ordering ensures that the directives most frequently used in execution bloc

You can disable this feature using the `--no-sort` flag.

### 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.

```python
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.

```python
# 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.

```python
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",
```

#### Example

`pyproject.toml`
Expand Down
65 changes: 52 additions & 13 deletions snakefmt/formatter.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ def __init__(
self.result: str = ""
self.lagging_comments: str = ""
self.no_formatting_yet: bool = True
self.sort_directives = sort_directives
self.fmt_sort_off = None if sort_directives else -1
self.previous_result: str = ""
self.keyword_spec: list[str] = []
self.keywords: dict[str, str] = {} # cache to sort
Expand All @@ -90,10 +90,13 @@ def flush_buffer(
from_python: bool = False,
final_flush: bool = False,
in_global_context: bool = False,
exiting_keywords: bool = False,
) -> None:
if len(self.buffer) == 0 or self.buffer.isspace():
self.result += self.buffer
self.buffer = ""
if exiting_keywords and self.no_formatting_yet and self.result.rstrip("\n"):
self.no_formatting_yet = False
return

if not from_python:
Expand All @@ -103,6 +106,9 @@ def flush_buffer(
else:
# Invalid python syntax, eg lone 'else:' between two rules, can occur.
# Below constructs valid code statements and formats them.
if self.fmt_off_expected_index:
self.buffer += self.fmt_off_expected_index
self.fmt_off_expected_index = ""
re_match = contextual_matcher.match(self.buffer)
if re_match is not None:
callback_keyword = re_match.group(2)
Expand All @@ -119,11 +125,13 @@ def flush_buffer(
)
formatted = self.run_black_format_str(to_format, self.block_indent)
re_rematch = contextual_matcher.match(formatted)
if re_rematch is None:
raise ValueError(
"contextual_matcher failed to match for the given "
f"formatted string: {formatted}"
)
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 condition != "":
callback_keyword += re_rematch.group(3)
formatted = (
Expand Down Expand Up @@ -174,7 +182,7 @@ def process_keyword_param(
context=param_context,
)
param_formatted = self.format_params(param_context)
if self.sort_directives and not in_global_context and self.keyword_spec:
if self.fmt_sort_off is None and not in_global_context and self.keyword_spec:
self.keywords[param_context.keyword_name] = self.result + param_formatted
self.result = ""
else:
Expand All @@ -188,13 +196,45 @@ def post_process_keyword(self):
for keyword in self.keyword_spec:
res = self.keywords.pop(keyword, "")
self.previous_result += res
if self.keywords:
raise InvalidParameterSyntax(
"Unexpected keywords when sorted keywords: "
+ (", ".join(self.keywords))
)
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"
)
self.result = self.previous_result + self.result
self.previous_result = ""
if self.no_formatting_yet and self.result.rstrip("\n"):
self.no_formatting_yet = False

def handle_fmt_off_region(self, verbatim: str) -> None:
if self.no_formatting_yet:
self.result = self.result.lstrip("\n")
self.result += self.buffer
self.buffer = ""
if not verbatim:
return
# When fmt:off[next] is inside a Python block (e.g. `if 1:`), the
# directive ends up as a lagging_comment after flushing that block.
is_nested_next = self.fmt_off and self.fmt_off[1] == "next"
if self.lagging_comments:
# For nested fmt:off[next], add the same \n separator that
# process_keyword_context/add_newlines would normally provide
# before the first keyword inside the Python block.
if is_nested_next and not self.no_formatting_yet:
self.result += "\n"
self.result += self.lagging_comments
self.lagging_comments = ""
self.result += verbatim
# For fmt: off[next], mark that we've emitted content so the following
# block gets its normal blank-line separator.
# For fmt: off regions, treat verbatim as transparent to separator logic.
if is_nested_next:
self.no_formatting_yet = bool(self.lagging_comments)
else:
self.no_formatting_yet = True
self.last_recognised_keyword = ""

def run_black_format_str(
self,
Expand All @@ -216,7 +256,6 @@ def run_black_format_str(
and len(string.strip().splitlines()) > 1
and not no_nesting
)

if artificial_nest:
string = f"if x:\n{textwrap.indent(string, TAB)}"

Expand Down
Loading
Loading