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.
Files changed (53) hide show
  1. {snakefmt-2.0.2 → snakefmt-2.0.3}/CHANGELOG.md +7 -0
  2. {snakefmt-2.0.2 → snakefmt-2.0.3}/PKG-INFO +1 -1
  3. {snakefmt-2.0.2 → snakefmt-2.0.3}/pyproject.toml +1 -1
  4. {snakefmt-2.0.2 → snakefmt-2.0.3}/snakefmt/formatter.py +30 -29
  5. {snakefmt-2.0.2 → snakefmt-2.0.3}/snakefmt/parser/parser.py +56 -35
  6. {snakefmt-2.0.2 → snakefmt-2.0.3}/tests/test_formatter.py +40 -0
  7. {snakefmt-2.0.2 → snakefmt-2.0.3}/uv.lock +1 -1
  8. {snakefmt-2.0.2 → snakefmt-2.0.3}/.flake8 +0 -0
  9. {snakefmt-2.0.2 → snakefmt-2.0.3}/.github/CODEOWNERS +0 -0
  10. {snakefmt-2.0.2 → snakefmt-2.0.3}/.github/workflows/ci.yaml +0 -0
  11. {snakefmt-2.0.2 → snakefmt-2.0.3}/.github/workflows/conventional-prs.yaml +0 -0
  12. {snakefmt-2.0.2 → snakefmt-2.0.3}/.github/workflows/release-please.yaml +0 -0
  13. {snakefmt-2.0.2 → snakefmt-2.0.3}/.gitignore +0 -0
  14. {snakefmt-2.0.2 → snakefmt-2.0.3}/.pre-commit-hooks.yaml +0 -0
  15. {snakefmt-2.0.2 → snakefmt-2.0.3}/CITATION.cff +0 -0
  16. {snakefmt-2.0.2 → snakefmt-2.0.3}/CONTRIBUTING.md +0 -0
  17. {snakefmt-2.0.2 → snakefmt-2.0.3}/LICENSE +0 -0
  18. {snakefmt-2.0.2 → snakefmt-2.0.3}/Makefile +0 -0
  19. {snakefmt-2.0.2 → snakefmt-2.0.3}/README.md +0 -0
  20. {snakefmt-2.0.2 → snakefmt-2.0.3}/codecov.yml +0 -0
  21. {snakefmt-2.0.2 → snakefmt-2.0.3}/docs/adr/0001-shell-formatter-distribution.md +0 -0
  22. {snakefmt-2.0.2 → snakefmt-2.0.3}/docs/adr/README.md +0 -0
  23. {snakefmt-2.0.2 → snakefmt-2.0.3}/docs/editor_integration.md +0 -0
  24. {snakefmt-2.0.2 → snakefmt-2.0.3}/docs/plans/pr-295-heredoc-masking.md +0 -0
  25. {snakefmt-2.0.2 → snakefmt-2.0.3}/docs/plans/pr-295-review-response.md +0 -0
  26. {snakefmt-2.0.2 → snakefmt-2.0.3}/docs/plans/shell-formatting-trailing-newline-bugfix.md +0 -0
  27. {snakefmt-2.0.2 → snakefmt-2.0.3}/lua/snakefmt/config.lua +0 -0
  28. {snakefmt-2.0.2 → snakefmt-2.0.3}/lua/snakefmt/init.lua +0 -0
  29. {snakefmt-2.0.2 → snakefmt-2.0.3}/lua/snakefmt/paths.lua +0 -0
  30. {snakefmt-2.0.2 → snakefmt-2.0.3}/plugin/snakefmt.vim +0 -0
  31. {snakefmt-2.0.2 → snakefmt-2.0.3}/snakefmt/__init__.py +0 -0
  32. {snakefmt-2.0.2 → snakefmt-2.0.3}/snakefmt/__main__.py +0 -0
  33. {snakefmt-2.0.2 → snakefmt-2.0.3}/snakefmt/config.py +0 -0
  34. {snakefmt-2.0.2 → snakefmt-2.0.3}/snakefmt/diff.py +0 -0
  35. {snakefmt-2.0.2 → snakefmt-2.0.3}/snakefmt/exceptions.py +0 -0
  36. {snakefmt-2.0.2 → snakefmt-2.0.3}/snakefmt/logging.py +0 -0
  37. {snakefmt-2.0.2 → snakefmt-2.0.3}/snakefmt/parser/grammar.py +0 -0
  38. {snakefmt-2.0.2 → snakefmt-2.0.3}/snakefmt/parser/syntax.py +0 -0
  39. {snakefmt-2.0.2 → snakefmt-2.0.3}/snakefmt/shell_formatter.py +0 -0
  40. {snakefmt-2.0.2 → snakefmt-2.0.3}/snakefmt/snakefmt.py +0 -0
  41. {snakefmt-2.0.2 → snakefmt-2.0.3}/snakefmt/types.py +0 -0
  42. {snakefmt-2.0.2 → snakefmt-2.0.3}/test.config.toml +0 -0
  43. {snakefmt-2.0.2 → snakefmt-2.0.3}/tests/__init__.py +0 -0
  44. {snakefmt-2.0.2 → snakefmt-2.0.3}/tests/conftest.py +0 -0
  45. {snakefmt-2.0.2 → snakefmt-2.0.3}/tests/test_config.py +0 -0
  46. {snakefmt-2.0.2 → snakefmt-2.0.3}/tests/test_diff.py +0 -0
  47. {snakefmt-2.0.2 → snakefmt-2.0.3}/tests/test_grammar.py +0 -0
  48. {snakefmt-2.0.2 → snakefmt-2.0.3}/tests/test_logging.py +0 -0
  49. {snakefmt-2.0.2 → snakefmt-2.0.3}/tests/test_main.py +0 -0
  50. {snakefmt-2.0.2 → snakefmt-2.0.3}/tests/test_parser.py +0 -0
  51. {snakefmt-2.0.2 → snakefmt-2.0.3}/tests/test_shell_formatter.py +0 -0
  52. {snakefmt-2.0.2 → snakefmt-2.0.3}/tests/test_snakefmt.py +0 -0
  53. {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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: snakefmt
3
- Version: 2.0.2
3
+ Version: 2.0.3
4
4
  Summary: The uncompromising Snakemake code formatter
5
5
  Project-URL: Homepage, https://github.com/snakemake/snakefmt
6
6
  Project-URL: Repository, https://github.com/snakemake/snakefmt
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "snakefmt"
3
- version = "2.0.2"
3
+ version = "2.0.3"
4
4
  description = "The uncompromising Snakemake code formatter"
5
5
  authors = [
6
6
  { name = "Michael Hall", email = "mhaldini@icloud.com" },
@@ -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 = self.align_strings(formatted, self.keyword_indent)
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 = self.align_strings(val, target_indent)
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
- fmt_dir = FMT_DIRECTIVE.from_token(token)
760
- if (
761
- fmt_dir
762
- and col_nb(token) == 0
763
- and not (fmt_dir.disable and "next" in fmt_dir.modifiers)
764
- ):
765
- # col-0 comments report cur_indent=0 to trigger context_exit;
766
- # fmt directives at other columns report actual cur_indent.
767
- return Status(token, block_indent, 0, buffer, False, pythonable)
768
- # Comments arrive in the token stream *before* any following
769
- # INDENT/DEDENT tokens, so self.cur_indent still reflects the
770
- # previous (potentially higher) level. Delegate to
771
- # _determine_comment_indent which peeks ahead and applies the
772
- # two snapping rules.
773
- effective_indent = self._determine_comment_indent(token)
774
- self.syntax.cur_indent = effective_indent
775
- if effective_indent < max(self.keyword_indent, self.block_indent):
776
- return Status(
777
- token, block_indent, effective_indent, buffer, False, pythonable
778
- )
779
- # `# fmt: off[next]` always needs parser-level handling.
780
- # Plain `# fmt: off` is parser-level only in global context; in other
781
- # Python contexts it is handled by Black.
782
- if (
783
- fmt_dir
784
- and fmt_dir.disable
785
- and (
786
- "next" in fmt_dir.modifiers
787
- or "sort" in fmt_dir.modifiers
788
- or (not fmt_dir.modifiers and self.in_global_context)
789
- )
790
- ):
791
- return Status(
792
- token, block_indent, effective_indent, buffer, False, pythonable
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
 
@@ -1019,7 +1019,7 @@ wheels = [
1019
1019
 
1020
1020
  [[package]]
1021
1021
  name = "snakefmt"
1022
- version = "2.0.2"
1022
+ version = "2.0.3"
1023
1023
  source = { editable = "." }
1024
1024
  dependencies = [
1025
1025
  { name = "black" },
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