snakefmt 2.0.2__tar.gz → 2.0.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {snakefmt-2.0.2 → snakefmt-2.0.3}/CHANGELOG.md +7 -0
- {snakefmt-2.0.2 → snakefmt-2.0.3}/PKG-INFO +1 -1
- {snakefmt-2.0.2 → snakefmt-2.0.3}/pyproject.toml +1 -1
- {snakefmt-2.0.2 → snakefmt-2.0.3}/snakefmt/formatter.py +30 -29
- {snakefmt-2.0.2 → snakefmt-2.0.3}/snakefmt/parser/parser.py +56 -35
- {snakefmt-2.0.2 → snakefmt-2.0.3}/tests/test_formatter.py +40 -0
- {snakefmt-2.0.2 → snakefmt-2.0.3}/uv.lock +1 -1
- {snakefmt-2.0.2 → snakefmt-2.0.3}/.flake8 +0 -0
- {snakefmt-2.0.2 → snakefmt-2.0.3}/.github/CODEOWNERS +0 -0
- {snakefmt-2.0.2 → snakefmt-2.0.3}/.github/workflows/ci.yaml +0 -0
- {snakefmt-2.0.2 → snakefmt-2.0.3}/.github/workflows/conventional-prs.yaml +0 -0
- {snakefmt-2.0.2 → snakefmt-2.0.3}/.github/workflows/release-please.yaml +0 -0
- {snakefmt-2.0.2 → snakefmt-2.0.3}/.gitignore +0 -0
- {snakefmt-2.0.2 → snakefmt-2.0.3}/.pre-commit-hooks.yaml +0 -0
- {snakefmt-2.0.2 → snakefmt-2.0.3}/CITATION.cff +0 -0
- {snakefmt-2.0.2 → snakefmt-2.0.3}/CONTRIBUTING.md +0 -0
- {snakefmt-2.0.2 → snakefmt-2.0.3}/LICENSE +0 -0
- {snakefmt-2.0.2 → snakefmt-2.0.3}/Makefile +0 -0
- {snakefmt-2.0.2 → snakefmt-2.0.3}/README.md +0 -0
- {snakefmt-2.0.2 → snakefmt-2.0.3}/codecov.yml +0 -0
- {snakefmt-2.0.2 → snakefmt-2.0.3}/docs/adr/0001-shell-formatter-distribution.md +0 -0
- {snakefmt-2.0.2 → snakefmt-2.0.3}/docs/adr/README.md +0 -0
- {snakefmt-2.0.2 → snakefmt-2.0.3}/docs/editor_integration.md +0 -0
- {snakefmt-2.0.2 → snakefmt-2.0.3}/docs/plans/pr-295-heredoc-masking.md +0 -0
- {snakefmt-2.0.2 → snakefmt-2.0.3}/docs/plans/pr-295-review-response.md +0 -0
- {snakefmt-2.0.2 → snakefmt-2.0.3}/docs/plans/shell-formatting-trailing-newline-bugfix.md +0 -0
- {snakefmt-2.0.2 → snakefmt-2.0.3}/lua/snakefmt/config.lua +0 -0
- {snakefmt-2.0.2 → snakefmt-2.0.3}/lua/snakefmt/init.lua +0 -0
- {snakefmt-2.0.2 → snakefmt-2.0.3}/lua/snakefmt/paths.lua +0 -0
- {snakefmt-2.0.2 → snakefmt-2.0.3}/plugin/snakefmt.vim +0 -0
- {snakefmt-2.0.2 → snakefmt-2.0.3}/snakefmt/__init__.py +0 -0
- {snakefmt-2.0.2 → snakefmt-2.0.3}/snakefmt/__main__.py +0 -0
- {snakefmt-2.0.2 → snakefmt-2.0.3}/snakefmt/config.py +0 -0
- {snakefmt-2.0.2 → snakefmt-2.0.3}/snakefmt/diff.py +0 -0
- {snakefmt-2.0.2 → snakefmt-2.0.3}/snakefmt/exceptions.py +0 -0
- {snakefmt-2.0.2 → snakefmt-2.0.3}/snakefmt/logging.py +0 -0
- {snakefmt-2.0.2 → snakefmt-2.0.3}/snakefmt/parser/grammar.py +0 -0
- {snakefmt-2.0.2 → snakefmt-2.0.3}/snakefmt/parser/syntax.py +0 -0
- {snakefmt-2.0.2 → snakefmt-2.0.3}/snakefmt/shell_formatter.py +0 -0
- {snakefmt-2.0.2 → snakefmt-2.0.3}/snakefmt/snakefmt.py +0 -0
- {snakefmt-2.0.2 → snakefmt-2.0.3}/snakefmt/types.py +0 -0
- {snakefmt-2.0.2 → snakefmt-2.0.3}/test.config.toml +0 -0
- {snakefmt-2.0.2 → snakefmt-2.0.3}/tests/__init__.py +0 -0
- {snakefmt-2.0.2 → snakefmt-2.0.3}/tests/conftest.py +0 -0
- {snakefmt-2.0.2 → snakefmt-2.0.3}/tests/test_config.py +0 -0
- {snakefmt-2.0.2 → snakefmt-2.0.3}/tests/test_diff.py +0 -0
- {snakefmt-2.0.2 → snakefmt-2.0.3}/tests/test_grammar.py +0 -0
- {snakefmt-2.0.2 → snakefmt-2.0.3}/tests/test_logging.py +0 -0
- {snakefmt-2.0.2 → snakefmt-2.0.3}/tests/test_main.py +0 -0
- {snakefmt-2.0.2 → snakefmt-2.0.3}/tests/test_parser.py +0 -0
- {snakefmt-2.0.2 → snakefmt-2.0.3}/tests/test_shell_formatter.py +0 -0
- {snakefmt-2.0.2 → snakefmt-2.0.3}/tests/test_snakefmt.py +0 -0
- {snakefmt-2.0.2 → snakefmt-2.0.3}/tests/test_v8_v9_features.py +0 -0
|
@@ -6,6 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
|
6
6
|
this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
8
|
|
|
9
|
+
## [2.0.3](https://github.com/snakemake/snakefmt/compare/v2.0.2...v2.0.3) (2026-06-25)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
### Bug Fixes
|
|
13
|
+
|
|
14
|
+
* skip fmt-directive and indent processing for inline comments ([#305](https://github.com/snakemake/snakefmt/issues/305)) ([#307](https://github.com/snakemake/snakefmt/issues/307)) ([d3a06b2](https://github.com/snakemake/snakefmt/commit/d3a06b27cfb8e1995871dcef37201e65c353527d))
|
|
15
|
+
|
|
9
16
|
## [2.0.2](https://github.com/snakemake/snakefmt/compare/v2.0.1...v2.0.2) (2026-06-11)
|
|
10
17
|
|
|
11
18
|
|
|
@@ -80,6 +80,34 @@ def indent_docstring(docstring: str, target_indent: int) -> str:
|
|
|
80
80
|
return textwrap.indent(docstring, TAB * target_indent)
|
|
81
81
|
|
|
82
82
|
|
|
83
|
+
def align_strings(string: str, target_indent: int) -> str:
|
|
84
|
+
"""
|
|
85
|
+
Takes an ensemble of strings and indents/reindents it
|
|
86
|
+
"""
|
|
87
|
+
used_indent = TAB * target_indent
|
|
88
|
+
split_string = split_code_string(string)
|
|
89
|
+
if len(split_string) == 1:
|
|
90
|
+
return textwrap.indent(split_string[0], used_indent)
|
|
91
|
+
|
|
92
|
+
# First, masks all multi-line strings
|
|
93
|
+
mask_string = "`~!@#$%^&*|?"
|
|
94
|
+
while mask_string in string:
|
|
95
|
+
mask_string += mask_string
|
|
96
|
+
mask_string = f'"""{mask_string}"""'
|
|
97
|
+
fakewrap = textwrap.indent(
|
|
98
|
+
"".join(mask_string if i % 2 else s for i, s in enumerate(split_string)),
|
|
99
|
+
used_indent,
|
|
100
|
+
)
|
|
101
|
+
split_code = fakewrap.split(mask_string)
|
|
102
|
+
|
|
103
|
+
# After indenting, we put those strings back exactly as they were
|
|
104
|
+
indented = "".join(
|
|
105
|
+
s.replace("\t", TAB) if i % 2 else split_code[i // 2]
|
|
106
|
+
for i, s in enumerate(split_string)
|
|
107
|
+
)
|
|
108
|
+
return indented
|
|
109
|
+
|
|
110
|
+
|
|
83
111
|
class Formatter(Parser):
|
|
84
112
|
def __init__(
|
|
85
113
|
self,
|
|
@@ -136,7 +164,7 @@ class Formatter(Parser):
|
|
|
136
164
|
# string, so indent the whole docstring directly. See issue #303.
|
|
137
165
|
formatted = indent_docstring(formatted, self.keyword_indent)
|
|
138
166
|
else:
|
|
139
|
-
formatted =
|
|
167
|
+
formatted = align_strings(formatted, self.keyword_indent)
|
|
140
168
|
else:
|
|
141
169
|
# Invalid python syntax, eg lone 'else:' between two rules, can occur.
|
|
142
170
|
# Below constructs valid code statements and formats them.
|
|
@@ -400,33 +428,6 @@ class Formatter(Parser):
|
|
|
400
428
|
fmted = textwrap.dedent(s)
|
|
401
429
|
return fmted
|
|
402
430
|
|
|
403
|
-
def align_strings(self, string: str, target_indent: int) -> str:
|
|
404
|
-
"""
|
|
405
|
-
Takes an ensemble of strings and indents/reindents it
|
|
406
|
-
"""
|
|
407
|
-
used_indent = TAB * target_indent
|
|
408
|
-
split_string = split_code_string(string)
|
|
409
|
-
if len(split_string) == 1:
|
|
410
|
-
return textwrap.indent(split_string[0], used_indent)
|
|
411
|
-
|
|
412
|
-
# First, masks all multi-line strings
|
|
413
|
-
mask_string = "`~!@#$%^&*|?"
|
|
414
|
-
while mask_string in string:
|
|
415
|
-
mask_string += mask_string
|
|
416
|
-
mask_string = f'"""{mask_string}"""'
|
|
417
|
-
fakewrap = textwrap.indent(
|
|
418
|
-
"".join(mask_string if i % 2 else s for i, s in enumerate(split_string)),
|
|
419
|
-
used_indent,
|
|
420
|
-
)
|
|
421
|
-
split_code = fakewrap.split(mask_string)
|
|
422
|
-
|
|
423
|
-
# After indenting, we put those strings back exactly as they were
|
|
424
|
-
indented = "".join(
|
|
425
|
-
s.replace("\t", TAB) if i % 2 else split_code[i // 2]
|
|
426
|
-
for i, s in enumerate(split_string)
|
|
427
|
-
)
|
|
428
|
-
return indented
|
|
429
|
-
|
|
430
431
|
def format_param(
|
|
431
432
|
self,
|
|
432
433
|
parameter: Parameter,
|
|
@@ -498,7 +499,7 @@ class Formatter(Parser):
|
|
|
498
499
|
else:
|
|
499
500
|
val = content
|
|
500
501
|
|
|
501
|
-
val =
|
|
502
|
+
val = align_strings(val, target_indent)
|
|
502
503
|
|
|
503
504
|
result = ""
|
|
504
505
|
if not inline_formatting:
|
|
@@ -756,41 +756,62 @@ class Parser(ABC):
|
|
|
756
756
|
token, block_indent, self.cur_indent, buffer, True, pythonable
|
|
757
757
|
)
|
|
758
758
|
elif token.type == tokenize.COMMENT:
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
759
|
+
is_inline = (
|
|
760
|
+
prev_token is not None
|
|
761
|
+
and prev_token.type
|
|
762
|
+
not in {
|
|
763
|
+
tokenize.NL,
|
|
764
|
+
tokenize.NEWLINE,
|
|
765
|
+
tokenize.INDENT,
|
|
766
|
+
tokenize.DEDENT,
|
|
767
|
+
}
|
|
768
|
+
and prev_token.end[0] == token.start[0]
|
|
769
|
+
)
|
|
770
|
+
if not is_inline:
|
|
771
|
+
fmt_dir = FMT_DIRECTIVE.from_token(token)
|
|
772
|
+
if (
|
|
773
|
+
fmt_dir
|
|
774
|
+
and col_nb(token) == 0
|
|
775
|
+
and not (fmt_dir.disable and "next" in fmt_dir.modifiers)
|
|
776
|
+
):
|
|
777
|
+
# col-0 comments report cur_indent=0 to trigger context_exit;
|
|
778
|
+
# fmt directives at other columns report actual cur_indent.
|
|
779
|
+
return Status(token, block_indent, 0, buffer, False, pythonable)
|
|
780
|
+
# Comments arrive in the token stream *before* any following
|
|
781
|
+
# INDENT/DEDENT tokens, so self.cur_indent still reflects the
|
|
782
|
+
# previous (potentially higher) level. Delegate to
|
|
783
|
+
# _determine_comment_indent which peeks ahead and applies the
|
|
784
|
+
# two snapping rules.
|
|
785
|
+
effective_indent = self._determine_comment_indent(token)
|
|
786
|
+
self.syntax.cur_indent = effective_indent
|
|
787
|
+
if effective_indent < max(self.keyword_indent, self.block_indent):
|
|
788
|
+
return Status(
|
|
789
|
+
token,
|
|
790
|
+
block_indent,
|
|
791
|
+
effective_indent,
|
|
792
|
+
buffer,
|
|
793
|
+
False,
|
|
794
|
+
pythonable,
|
|
795
|
+
)
|
|
796
|
+
# Plain `# fmt: off` is parser-level only in global context;
|
|
797
|
+
# in other Python contexts it is handled by Black.
|
|
798
|
+
if (
|
|
799
|
+
fmt_dir
|
|
800
|
+
and fmt_dir.disable
|
|
801
|
+
and (
|
|
802
|
+
"next" in fmt_dir.modifiers
|
|
803
|
+
or "sort" in fmt_dir.modifiers
|
|
804
|
+
or (not fmt_dir.modifiers and self.in_global_context)
|
|
805
|
+
)
|
|
806
|
+
):
|
|
807
|
+
return Status(
|
|
808
|
+
token,
|
|
809
|
+
block_indent,
|
|
810
|
+
effective_indent,
|
|
811
|
+
buffer,
|
|
812
|
+
False,
|
|
813
|
+
pythonable,
|
|
814
|
+
)
|
|
794
815
|
|
|
795
816
|
elif is_newline(token):
|
|
796
817
|
self.queriable, newline = True, True
|
|
@@ -12,6 +12,7 @@ import black.parsing
|
|
|
12
12
|
import pytest
|
|
13
13
|
|
|
14
14
|
from snakefmt.exceptions import InvalidPython
|
|
15
|
+
from snakefmt.formatter import align_strings
|
|
15
16
|
from snakefmt.parser.grammar import SingleParam, SnakeGlobal
|
|
16
17
|
from snakefmt.parser.syntax import COMMENT_SPACING
|
|
17
18
|
from snakefmt.types import TAB
|
|
@@ -1070,6 +1071,27 @@ rule a:
|
|
|
1070
1071
|
formatter = setup_formatter(snakecode)
|
|
1071
1072
|
assert formatter.get_formatted() == expected
|
|
1072
1073
|
|
|
1074
|
+
def test_multiline_docstring_with_same_line_comment_inside_rule(self):
|
|
1075
|
+
"""Closing quote with same-line comment is formatted correctly"""
|
|
1076
|
+
snakecode = (
|
|
1077
|
+
"rule test:\n"
|
|
1078
|
+
f'{TAB * 1}"""\n'
|
|
1079
|
+
f"{TAB * 1}Comment\n"
|
|
1080
|
+
f'{TAB * 1}""" # comment\n'
|
|
1081
|
+
f"{TAB * 1}input:\n"
|
|
1082
|
+
f'{TAB * 2}"test.txt",\n'
|
|
1083
|
+
)
|
|
1084
|
+
expected = (
|
|
1085
|
+
"rule test:\n"
|
|
1086
|
+
f'{TAB * 1}"""\n'
|
|
1087
|
+
f"{TAB * 1}Comment\n"
|
|
1088
|
+
f'{TAB * 1}""" # comment\n'
|
|
1089
|
+
f"{TAB * 1}input:\n"
|
|
1090
|
+
f'{TAB * 2}"test.txt",\n'
|
|
1091
|
+
)
|
|
1092
|
+
formatter = setup_formatter(snakecode)
|
|
1093
|
+
assert formatter.get_formatted() == expected
|
|
1094
|
+
|
|
1073
1095
|
def test_collapsing_docstring_inside_rule_keeps_black_normalisation(self):
|
|
1074
1096
|
"""Black still collapses a single-line docstring; indentation is preserved"""
|
|
1075
1097
|
snakecode = (
|
|
@@ -2273,6 +2295,24 @@ class TestDocstringHelpers:
|
|
|
2273
2295
|
assert indent_docstring(docstring, 1) == expected
|
|
2274
2296
|
|
|
2275
2297
|
|
|
2298
|
+
class TestAlignStrings:
|
|
2299
|
+
"""Unit tests for the align_strings helper function."""
|
|
2300
|
+
|
|
2301
|
+
def test_align_strings_single_line(self):
|
|
2302
|
+
assert align_strings("param = 1", 1) == f"{TAB * 1}param = 1"
|
|
2303
|
+
assert align_strings("param = 1", 2) == f"{TAB * 2}param = 1"
|
|
2304
|
+
|
|
2305
|
+
def test_align_strings_multiline_code(self):
|
|
2306
|
+
code = "a = 1\nb = 2"
|
|
2307
|
+
expected = f"{TAB * 1}a = 1\n{TAB * 1}b = 2"
|
|
2308
|
+
assert align_strings(code, 1) == expected
|
|
2309
|
+
|
|
2310
|
+
def test_align_strings_preserves_triple_quoted_strings(self):
|
|
2311
|
+
code = 'x = """\nline1\nline2\n"""'
|
|
2312
|
+
expected = f'{TAB * 1}x = """\nline1\nline2\n"""'
|
|
2313
|
+
assert align_strings(code, 1) == expected
|
|
2314
|
+
|
|
2315
|
+
|
|
2276
2316
|
class TestFmtOffOn:
|
|
2277
2317
|
"""Tests for # fmt: off / # fmt: on directives."""
|
|
2278
2318
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|