sectionise 0.2.0__tar.gz → 0.3.0__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.
- {sectionise-0.2.0 → sectionise-0.3.0}/.pre-commit-config.yaml +4 -0
- {sectionise-0.2.0 → sectionise-0.3.0}/CHANGELOG.md +21 -0
- {sectionise-0.2.0 → sectionise-0.3.0}/PKG-INFO +67 -5
- {sectionise-0.2.0 → sectionise-0.3.0}/README.md +66 -4
- {sectionise-0.2.0 → sectionise-0.3.0}/sectionise/cli.py +24 -1
- {sectionise-0.2.0 → sectionise-0.3.0}/sectionise/core.py +146 -32
- {sectionise-0.2.0 → sectionise-0.3.0}/tests/test_cli.py +31 -8
- {sectionise-0.2.0 → sectionise-0.3.0}/tests/test_core.py +126 -9
- {sectionise-0.2.0 → sectionise-0.3.0}/.env.example +0 -0
- {sectionise-0.2.0 → sectionise-0.3.0}/.github/workflows/ci.yml +0 -0
- {sectionise-0.2.0 → sectionise-0.3.0}/.github/workflows/publish.yml +0 -0
- {sectionise-0.2.0 → sectionise-0.3.0}/.gitignore +0 -0
- {sectionise-0.2.0 → sectionise-0.3.0}/.gitleaks.toml +0 -0
- {sectionise-0.2.0 → sectionise-0.3.0}/.pre-commit-hooks.yaml +0 -0
- {sectionise-0.2.0 → sectionise-0.3.0}/.python-version +0 -0
- {sectionise-0.2.0 → sectionise-0.3.0}/LICENSE +0 -0
- {sectionise-0.2.0 → sectionise-0.3.0}/pyproject.toml +0 -0
- {sectionise-0.2.0 → sectionise-0.3.0}/sectionise/__init__.py +0 -0
- {sectionise-0.2.0 → sectionise-0.3.0}/uv.lock +0 -0
|
@@ -4,6 +4,27 @@ All notable changes to this project are documented here. The format follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project
|
|
5
5
|
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.3.0] - 2026-07-23
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `fill_mode` (`width` or `fixed`) and `fill_count`, so single-line banners can
|
|
12
|
+
carry a set number of fill characters per run (`# ----- Title -----`) instead
|
|
13
|
+
of always padding to the target width. `align` still chooses the sides, and
|
|
14
|
+
box style and dividers are unaffected.
|
|
15
|
+
- `bookend`, closing a single-line banner with a mirror of the comment opener
|
|
16
|
+
(`# --- Title --- #`, `// --- Title --- //`). A trailing opener is recognised
|
|
17
|
+
on input whether or not `bookend` is set.
|
|
18
|
+
- Multi-character `fill` (for example `-=`), tiled and truncated to the target
|
|
19
|
+
width. Each of its characters is added to `detect_chars` so the output is
|
|
20
|
+
recognised on the next run.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- Fill characters joined to the title text (`_function_name`, `#Nice`, `name_`)
|
|
25
|
+
are no longer stripped. Only stand-alone, whitespace-separated fill runs are
|
|
26
|
+
treated as decoration.
|
|
27
|
+
|
|
7
28
|
## [0.2.0] - 2026-07-23
|
|
8
29
|
|
|
9
30
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sectionise
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Standardise section-header comment banners
|
|
5
5
|
Project-URL: Homepage, https://github.com/MitchellNeedham/sectionise
|
|
6
6
|
Project-URL: Repository, https://github.com/MitchellNeedham/sectionise
|
|
@@ -135,7 +135,7 @@ pip install sectionise
|
|
|
135
135
|
|
|
136
136
|
```yaml
|
|
137
137
|
- repo: https://github.com/MitchellNeedham/sectionise
|
|
138
|
-
rev: 0.
|
|
138
|
+
rev: 0.3.0
|
|
139
139
|
hooks:
|
|
140
140
|
- id: sectionise
|
|
141
141
|
```
|
|
@@ -174,6 +174,9 @@ overrides are honoured.
|
|
|
174
174
|
style = "single" # or "box"
|
|
175
175
|
fill = "-"
|
|
176
176
|
align = "centre" # or "left"
|
|
177
|
+
fill_mode = "width" # or "fixed"
|
|
178
|
+
fill_count = 3 # fill characters per run when fill_mode = "fixed"
|
|
179
|
+
bookend = false # close a single-line banner with a mirror of the opener
|
|
177
180
|
detect_chars = "-=*_~#—–─═"
|
|
178
181
|
min_run = 3
|
|
179
182
|
require_both_sides = false
|
|
@@ -189,8 +192,11 @@ encoding = "utf-8"
|
|
|
189
192
|
| --- | --- | --- | --- |
|
|
190
193
|
| `width` | `--width` | per language | Target total line length. |
|
|
191
194
|
| `style` | `--style` | `single` | Output form: `single` line or 3-line `box`. |
|
|
192
|
-
| `fill` | `--fill` | `-` | Output fill character. |
|
|
195
|
+
| `fill` | `--fill` | `-` | Output fill. One or more characters; a multi-character fill is tiled to the width. |
|
|
193
196
|
| `align` | `--align` | `centre` | Single-line title placement: `centre` or `left`. |
|
|
197
|
+
| `fill_mode` | `--fill-mode` | `width` | Single-line fill: `width` (pad to `width`) or `fixed` (a set count per run). |
|
|
198
|
+
| `fill_count` | `--fill-count` | `3` | Fill characters per run when `fill_mode` is `fixed`. Must be at least `min_run`. |
|
|
199
|
+
| `bookend` | `--bookend` | `false` | Close a single-line banner with a mirror of the comment opener. Line comments only. |
|
|
194
200
|
| `detect_chars` | `--detect-chars` | `-=*_~#—–─═` | Characters recognised as fill in input. |
|
|
195
201
|
| `min_run` | `--min-run` | `3` | Minimum fill-run length to count as a banner side. |
|
|
196
202
|
| `require_both_sides` | `--require-both-sides` | `false` | Only treat both-sided comments as banners. |
|
|
@@ -230,7 +236,63 @@ align = "left"
|
|
|
230
236
|
# Loading models -----------------------------------------------------------------------
|
|
231
237
|
```
|
|
232
238
|
|
|
233
|
-
|
|
239
|
+
A fixed number of fill characters per run, so the line length follows the title
|
|
240
|
+
instead of the target width:
|
|
241
|
+
|
|
242
|
+
```toml
|
|
243
|
+
[tool.sectionise]
|
|
244
|
+
fill_mode = "fixed"
|
|
245
|
+
fill_count = 5
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
```
|
|
249
|
+
# ----- Loading models -----
|
|
250
|
+
# ----- Setup -----
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
`align` still decides the sides: `centre` (the default) puts a matching run on
|
|
254
|
+
both sides as above, while `left` keeps the trailing run only (`# Loading models
|
|
255
|
+
-----`). Box style and stand-alone dividers ignore `fill_mode` and always span
|
|
256
|
+
`width`.
|
|
257
|
+
|
|
258
|
+
Book-ended headers close with a mirror of the comment opener, so the line ends
|
|
259
|
+
the way it starts:
|
|
260
|
+
|
|
261
|
+
```toml
|
|
262
|
+
[tool.sectionise]
|
|
263
|
+
fill_mode = "fixed"
|
|
264
|
+
fill_count = 4
|
|
265
|
+
bookend = true
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
```
|
|
269
|
+
# ---- Loading models ---- #
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
The closing marker is always the opener, so it matches by construction: a `//`
|
|
273
|
+
comment closes with `//`, a `#` comment with `#`. Block comments already close
|
|
274
|
+
with their own token, so `bookend` leaves them alone. A trailing opener is
|
|
275
|
+
recognised on input whether or not `bookend` is set, so existing
|
|
276
|
+
`// --- x --- //` banners are picked up either way.
|
|
277
|
+
|
|
278
|
+
The fill can be more than one character; a multi-character fill is tiled and
|
|
279
|
+
truncated to reach the width:
|
|
280
|
+
|
|
281
|
+
```toml
|
|
282
|
+
[tool.sectionise]
|
|
283
|
+
fill = "=-"
|
|
284
|
+
fill_mode = "fixed"
|
|
285
|
+
fill_count = 8
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
```
|
|
289
|
+
# =-=-=-=- Loading models =-=-=-=-
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
A fill longer than the run it fills is truncated to its leading characters, so
|
|
293
|
+
in `fixed` mode a fill longer than `fill_count` is cut mid-motif.
|
|
294
|
+
|
|
295
|
+
Any non-space fill is valid, including an emoji:
|
|
234
296
|
|
|
235
297
|
```toml
|
|
236
298
|
[tool.sectionise]
|
|
@@ -243,7 +305,7 @@ width = 40
|
|
|
243
305
|
```
|
|
244
306
|
|
|
245
307
|
`width` counts characters, not display columns, so a double-width emoji fill
|
|
246
|
-
lands on target by count but
|
|
308
|
+
lands on target by count but is wider on screen.
|
|
247
309
|
|
|
248
310
|
### Per-language overrides
|
|
249
311
|
|
|
@@ -118,7 +118,7 @@ pip install sectionise
|
|
|
118
118
|
|
|
119
119
|
```yaml
|
|
120
120
|
- repo: https://github.com/MitchellNeedham/sectionise
|
|
121
|
-
rev: 0.
|
|
121
|
+
rev: 0.3.0
|
|
122
122
|
hooks:
|
|
123
123
|
- id: sectionise
|
|
124
124
|
```
|
|
@@ -157,6 +157,9 @@ overrides are honoured.
|
|
|
157
157
|
style = "single" # or "box"
|
|
158
158
|
fill = "-"
|
|
159
159
|
align = "centre" # or "left"
|
|
160
|
+
fill_mode = "width" # or "fixed"
|
|
161
|
+
fill_count = 3 # fill characters per run when fill_mode = "fixed"
|
|
162
|
+
bookend = false # close a single-line banner with a mirror of the opener
|
|
160
163
|
detect_chars = "-=*_~#—–─═"
|
|
161
164
|
min_run = 3
|
|
162
165
|
require_both_sides = false
|
|
@@ -172,8 +175,11 @@ encoding = "utf-8"
|
|
|
172
175
|
| --- | --- | --- | --- |
|
|
173
176
|
| `width` | `--width` | per language | Target total line length. |
|
|
174
177
|
| `style` | `--style` | `single` | Output form: `single` line or 3-line `box`. |
|
|
175
|
-
| `fill` | `--fill` | `-` | Output fill character. |
|
|
178
|
+
| `fill` | `--fill` | `-` | Output fill. One or more characters; a multi-character fill is tiled to the width. |
|
|
176
179
|
| `align` | `--align` | `centre` | Single-line title placement: `centre` or `left`. |
|
|
180
|
+
| `fill_mode` | `--fill-mode` | `width` | Single-line fill: `width` (pad to `width`) or `fixed` (a set count per run). |
|
|
181
|
+
| `fill_count` | `--fill-count` | `3` | Fill characters per run when `fill_mode` is `fixed`. Must be at least `min_run`. |
|
|
182
|
+
| `bookend` | `--bookend` | `false` | Close a single-line banner with a mirror of the comment opener. Line comments only. |
|
|
177
183
|
| `detect_chars` | `--detect-chars` | `-=*_~#—–─═` | Characters recognised as fill in input. |
|
|
178
184
|
| `min_run` | `--min-run` | `3` | Minimum fill-run length to count as a banner side. |
|
|
179
185
|
| `require_both_sides` | `--require-both-sides` | `false` | Only treat both-sided comments as banners. |
|
|
@@ -213,7 +219,63 @@ align = "left"
|
|
|
213
219
|
# Loading models -----------------------------------------------------------------------
|
|
214
220
|
```
|
|
215
221
|
|
|
216
|
-
|
|
222
|
+
A fixed number of fill characters per run, so the line length follows the title
|
|
223
|
+
instead of the target width:
|
|
224
|
+
|
|
225
|
+
```toml
|
|
226
|
+
[tool.sectionise]
|
|
227
|
+
fill_mode = "fixed"
|
|
228
|
+
fill_count = 5
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
```
|
|
232
|
+
# ----- Loading models -----
|
|
233
|
+
# ----- Setup -----
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
`align` still decides the sides: `centre` (the default) puts a matching run on
|
|
237
|
+
both sides as above, while `left` keeps the trailing run only (`# Loading models
|
|
238
|
+
-----`). Box style and stand-alone dividers ignore `fill_mode` and always span
|
|
239
|
+
`width`.
|
|
240
|
+
|
|
241
|
+
Book-ended headers close with a mirror of the comment opener, so the line ends
|
|
242
|
+
the way it starts:
|
|
243
|
+
|
|
244
|
+
```toml
|
|
245
|
+
[tool.sectionise]
|
|
246
|
+
fill_mode = "fixed"
|
|
247
|
+
fill_count = 4
|
|
248
|
+
bookend = true
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
```
|
|
252
|
+
# ---- Loading models ---- #
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
The closing marker is always the opener, so it matches by construction: a `//`
|
|
256
|
+
comment closes with `//`, a `#` comment with `#`. Block comments already close
|
|
257
|
+
with their own token, so `bookend` leaves them alone. A trailing opener is
|
|
258
|
+
recognised on input whether or not `bookend` is set, so existing
|
|
259
|
+
`// --- x --- //` banners are picked up either way.
|
|
260
|
+
|
|
261
|
+
The fill can be more than one character; a multi-character fill is tiled and
|
|
262
|
+
truncated to reach the width:
|
|
263
|
+
|
|
264
|
+
```toml
|
|
265
|
+
[tool.sectionise]
|
|
266
|
+
fill = "=-"
|
|
267
|
+
fill_mode = "fixed"
|
|
268
|
+
fill_count = 8
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
```
|
|
272
|
+
# =-=-=-=- Loading models =-=-=-=-
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
A fill longer than the run it fills is truncated to its leading characters, so
|
|
276
|
+
in `fixed` mode a fill longer than `fill_count` is cut mid-motif.
|
|
277
|
+
|
|
278
|
+
Any non-space fill is valid, including an emoji:
|
|
217
279
|
|
|
218
280
|
```toml
|
|
219
281
|
[tool.sectionise]
|
|
@@ -226,7 +288,7 @@ width = 40
|
|
|
226
288
|
```
|
|
227
289
|
|
|
228
290
|
`width` counts characters, not display columns, so a double-width emoji fill
|
|
229
|
-
lands on target by count but
|
|
291
|
+
lands on target by count but is wider on screen.
|
|
230
292
|
|
|
231
293
|
### Per-language overrides
|
|
232
294
|
|
|
@@ -141,7 +141,9 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
141
141
|
help="Name used to pick the comment syntax when reading stdin (default a .py name).",
|
|
142
142
|
)
|
|
143
143
|
parser.add_argument("--width", type=int, default=None, help="Target line length.")
|
|
144
|
-
parser.add_argument(
|
|
144
|
+
parser.add_argument(
|
|
145
|
+
"--fill", default=None, help="Output fill, one or more characters."
|
|
146
|
+
)
|
|
145
147
|
parser.add_argument(
|
|
146
148
|
"--detect-chars", default=None, help="Characters recognised as banner fill."
|
|
147
149
|
)
|
|
@@ -163,6 +165,24 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
163
165
|
default=None,
|
|
164
166
|
help="Single-line title placement (default centre).",
|
|
165
167
|
)
|
|
168
|
+
parser.add_argument(
|
|
169
|
+
"--fill-mode",
|
|
170
|
+
choices=("width", "fixed"),
|
|
171
|
+
default=None,
|
|
172
|
+
help="Single-line fill: pad to width, or a fixed count per run.",
|
|
173
|
+
)
|
|
174
|
+
parser.add_argument(
|
|
175
|
+
"--fill-count",
|
|
176
|
+
type=int,
|
|
177
|
+
default=None,
|
|
178
|
+
help="Fill characters per run when fill-mode is fixed (default 3).",
|
|
179
|
+
)
|
|
180
|
+
parser.add_argument(
|
|
181
|
+
"--bookend",
|
|
182
|
+
action=argparse.BooleanOptionalAction,
|
|
183
|
+
default=None,
|
|
184
|
+
help="Close a single-line banner with a mirror of the comment opener.",
|
|
185
|
+
)
|
|
166
186
|
parser.add_argument(
|
|
167
187
|
"--max-title", type=int, default=None, help="Hard cap on title length."
|
|
168
188
|
)
|
|
@@ -231,6 +251,9 @@ def _resolve_style(
|
|
|
231
251
|
boxes=pick(args.boxes, "boxes", True),
|
|
232
252
|
style=pick(args.style, "style", core.DEFAULT_STYLE),
|
|
233
253
|
align=pick(args.align, "align", core.DEFAULT_ALIGN),
|
|
254
|
+
fill_mode=pick(args.fill_mode, "fill_mode", core.DEFAULT_FILL_MODE),
|
|
255
|
+
fill_count=pick(args.fill_count, "fill_count", core.DEFAULT_FILL_COUNT),
|
|
256
|
+
bookend=pick(args.bookend, "bookend", False),
|
|
234
257
|
tab_width=pick(args.tab_width, "tab_width", core.DEFAULT_TAB_WIDTH),
|
|
235
258
|
max_title=pick(args.max_title, "max_title", None),
|
|
236
259
|
)
|
|
@@ -31,6 +31,8 @@ DEFAULT_DETECT_CHARS = "-=*_~#—–─═"
|
|
|
31
31
|
DEFAULT_MIN_RUN = 3
|
|
32
32
|
DEFAULT_STYLE = "single"
|
|
33
33
|
DEFAULT_ALIGN = "centre"
|
|
34
|
+
DEFAULT_FILL_MODE = "width"
|
|
35
|
+
DEFAULT_FILL_COUNT = 3
|
|
34
36
|
DEFAULT_TAB_WIDTH = 8
|
|
35
37
|
|
|
36
38
|
# Comment syntax as (opener, closer). The closer is empty for line comments and
|
|
@@ -129,7 +131,9 @@ class Style:
|
|
|
129
131
|
|
|
130
132
|
Attributes:
|
|
131
133
|
width: Target total line length for a rewritten banner or rule.
|
|
132
|
-
fill: The
|
|
134
|
+
fill: The fill used in output. Usually one character; a multi-character
|
|
135
|
+
fill is tiled and truncated to the exact width (`-=` gives
|
|
136
|
+
`-=-=-=-`), and each of its characters is added to `detect_chars`.
|
|
133
137
|
detect_chars: Characters recognised as banner fill in input.
|
|
134
138
|
min_run: Minimum identical-fill run length to count as a banner side.
|
|
135
139
|
require_both_sides: Only treat a comment as a banner when it has a fill
|
|
@@ -141,6 +145,19 @@ class Style:
|
|
|
141
145
|
align: Single-line title placement, `centre` (fill both sides) or `left`
|
|
142
146
|
(title first, fill trailing). The American spelling `center` is
|
|
143
147
|
accepted and normalised to `centre`.
|
|
148
|
+
fill_mode: How much fill a single-line banner carries, `width` (pad to
|
|
149
|
+
the target `width`) or `fixed` (`fill_count` characters per run, so
|
|
150
|
+
the line length grows with the title). `align` still decides the
|
|
151
|
+
sides: `centre` fills both, `left` fills only after the title. Box
|
|
152
|
+
style and stand-alone dividers are unaffected and always span
|
|
153
|
+
`width`.
|
|
154
|
+
fill_count: Fill characters per run when `fill_mode` is `fixed`. Must be
|
|
155
|
+
at least `min_run`, so a rendered banner is still recognised as one.
|
|
156
|
+
bookend: Close a single-line banner with a mirror of the comment opener,
|
|
157
|
+
so the line ends the way it starts (`# --- Title --- #`,
|
|
158
|
+
`// --- Title --- //`). Line comments only; block comments already
|
|
159
|
+
close with their own token, and box style is unaffected. A trailing
|
|
160
|
+
opener is always recognised on input regardless of this setting.
|
|
144
161
|
tab_width: Columns a leading tab occupies, so tab-indented banners reach
|
|
145
162
|
the target width.
|
|
146
163
|
max_title: Optional hard cap on title length, on top of the width fit.
|
|
@@ -155,6 +172,9 @@ class Style:
|
|
|
155
172
|
boxes: bool = True
|
|
156
173
|
style: str = DEFAULT_STYLE
|
|
157
174
|
align: str = DEFAULT_ALIGN
|
|
175
|
+
fill_mode: str = DEFAULT_FILL_MODE
|
|
176
|
+
fill_count: int = DEFAULT_FILL_COUNT
|
|
177
|
+
bookend: bool = False
|
|
158
178
|
tab_width: int = DEFAULT_TAB_WIDTH
|
|
159
179
|
max_title: int | None = None
|
|
160
180
|
|
|
@@ -180,9 +200,13 @@ class Style:
|
|
|
180
200
|
raise ValueError(
|
|
181
201
|
f"min_run must be a positive integer, got {self.min_run!r}"
|
|
182
202
|
)
|
|
183
|
-
if
|
|
203
|
+
if (
|
|
204
|
+
not isinstance(self.fill, str)
|
|
205
|
+
or not self.fill
|
|
206
|
+
or any(char.isspace() for char in self.fill)
|
|
207
|
+
):
|
|
184
208
|
raise ValueError(
|
|
185
|
-
f"fill must be a
|
|
209
|
+
f"fill must be a non-empty string without whitespace, got {self.fill!r}"
|
|
186
210
|
)
|
|
187
211
|
if not isinstance(self.detect_chars, str) or not self.detect_chars:
|
|
188
212
|
raise ValueError(
|
|
@@ -202,6 +226,25 @@ class Style:
|
|
|
202
226
|
object.__setattr__(self, "align", "centre")
|
|
203
227
|
if self.align not in ("centre", "left"):
|
|
204
228
|
raise ValueError(f"align must be 'centre' or 'left', got {self.align!r}")
|
|
229
|
+
if self.fill_mode not in ("width", "fixed"):
|
|
230
|
+
raise ValueError(
|
|
231
|
+
f"fill_mode must be 'width' or 'fixed', got {self.fill_mode!r}"
|
|
232
|
+
)
|
|
233
|
+
if (
|
|
234
|
+
not isinstance(self.fill_count, int)
|
|
235
|
+
or isinstance(self.fill_count, bool)
|
|
236
|
+
or self.fill_count < 1
|
|
237
|
+
):
|
|
238
|
+
raise ValueError(
|
|
239
|
+
f"fill_count must be a positive integer, got {self.fill_count!r}"
|
|
240
|
+
)
|
|
241
|
+
# A fixed run shorter than min_run would not be re-detected as a banner,
|
|
242
|
+
# so a freshly written header could not be re-fitted.
|
|
243
|
+
if self.fill_mode == "fixed" and self.fill_count < self.min_run:
|
|
244
|
+
raise ValueError(
|
|
245
|
+
f"fill_count must be at least min_run ({self.min_run}) in fixed "
|
|
246
|
+
f"mode, got {self.fill_count}"
|
|
247
|
+
)
|
|
205
248
|
if self.max_title is not None and (
|
|
206
249
|
not isinstance(self.max_title, int)
|
|
207
250
|
or isinstance(self.max_title, bool)
|
|
@@ -210,10 +253,14 @@ class Style:
|
|
|
210
253
|
raise ValueError(
|
|
211
254
|
f"max_title must be a positive integer or unset, got {self.max_title!r}"
|
|
212
255
|
)
|
|
213
|
-
#
|
|
214
|
-
# written banner would not be seen as one and could not be
|
|
215
|
-
|
|
216
|
-
|
|
256
|
+
# Every output fill character must be recognised on the next run, else a
|
|
257
|
+
# freshly written banner would not be seen as one and could not be
|
|
258
|
+
# re-fitted. A multi-character fill seeds each of its characters.
|
|
259
|
+
missing = "".join(
|
|
260
|
+
dict.fromkeys(c for c in self.fill if c not in self.detect_chars)
|
|
261
|
+
)
|
|
262
|
+
if missing:
|
|
263
|
+
object.__setattr__(self, "detect_chars", self.detect_chars + missing)
|
|
217
264
|
|
|
218
265
|
|
|
219
266
|
Syntax = tuple[str, str]
|
|
@@ -450,9 +497,41 @@ def _extract(content: str, syntax: tuple[str, str]) -> tuple[str, str] | None:
|
|
|
450
497
|
if not inner.rstrip().endswith(closer):
|
|
451
498
|
return None
|
|
452
499
|
inner = inner.rstrip()[: -len(closer)]
|
|
500
|
+
else:
|
|
501
|
+
# A line-comment banner may be book-ended with a mirror of the opener
|
|
502
|
+
# (`# --- x --- #`, `// --- x --- //`). Drop a stand-alone trailing
|
|
503
|
+
# opener so the title is recovered, whatever the `bookend` setting is,
|
|
504
|
+
# since the opener may not be a detectable fill character.
|
|
505
|
+
trimmed = inner.rstrip()
|
|
506
|
+
if trimmed.endswith(opener) and (
|
|
507
|
+
len(trimmed) == len(opener) or trimmed[-len(opener) - 1].isspace()
|
|
508
|
+
):
|
|
509
|
+
inner = trimmed[: -len(opener)]
|
|
453
510
|
return indent, inner
|
|
454
511
|
|
|
455
512
|
|
|
513
|
+
def _strip_edge_fill(text: str, detect_chars: str) -> str:
|
|
514
|
+
"""Drop whitespace-separated fill runs from each edge of `text`.
|
|
515
|
+
|
|
516
|
+
A run of a fill character that stands alone, bounded by whitespace or the
|
|
517
|
+
string edge, is decoration and is removed, so `title --- #` reduces to
|
|
518
|
+
`title`. A fill character joined to the title text with no space, as in
|
|
519
|
+
`_function_name` or `#Nice`, is part of the title and kept.
|
|
520
|
+
"""
|
|
521
|
+
s = text.strip()
|
|
522
|
+
while s and s[0] in detect_chars:
|
|
523
|
+
run = len(s) - len(s.lstrip(s[0]))
|
|
524
|
+
if run < len(s) and not s[run].isspace():
|
|
525
|
+
break # attached to the title text, so keep it
|
|
526
|
+
s = s[run:].lstrip()
|
|
527
|
+
while s and s[-1] in detect_chars:
|
|
528
|
+
run = len(s) - len(s.rstrip(s[-1]))
|
|
529
|
+
if run < len(s) and not s[-1 - run].isspace():
|
|
530
|
+
break # attached to the title text, so keep it
|
|
531
|
+
s = s[: len(s) - run].rstrip()
|
|
532
|
+
return s
|
|
533
|
+
|
|
534
|
+
|
|
456
535
|
def _side_runs(inner: str, style: Style) -> tuple[int, int, str]:
|
|
457
536
|
"""Measure the leading and trailing fill runs and the title between them.
|
|
458
537
|
|
|
@@ -466,20 +545,16 @@ def _side_runs(inner: str, style: Style) -> tuple[int, int, str]:
|
|
|
466
545
|
stripped = inner.strip()
|
|
467
546
|
if not stripped:
|
|
468
547
|
return 0, 0, ""
|
|
548
|
+
# A fill run is any maximal run of detect characters, not just one repeated
|
|
549
|
+
# character, so a multi-character fill motif (`-=-=-=`) reads as a single
|
|
550
|
+
# run and the tool's own output round-trips.
|
|
469
551
|
lead = 0
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
while lead < len(stripped) and stripped[lead] == char:
|
|
473
|
-
lead += 1
|
|
552
|
+
while lead < len(stripped) and stripped[lead] in style.detect_chars:
|
|
553
|
+
lead += 1
|
|
474
554
|
trail = 0
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
trail += 1
|
|
479
|
-
# Strip any residual fill characters left at the title edges, so decoration
|
|
480
|
-
# like a trailing lone `#` (`# --- title --- #`) cannot drag the real fill
|
|
481
|
-
# run into the title.
|
|
482
|
-
title = stripped[lead : len(stripped) - trail].strip(style.detect_chars + " \t")
|
|
555
|
+
while trail < len(stripped) and stripped[-1 - trail] in style.detect_chars:
|
|
556
|
+
trail += 1
|
|
557
|
+
title = _strip_edge_fill(stripped[lead : len(stripped) - trail], style.detect_chars)
|
|
483
558
|
return lead, trail, title
|
|
484
559
|
|
|
485
560
|
|
|
@@ -574,23 +649,53 @@ def _match_box(
|
|
|
574
649
|
return None
|
|
575
650
|
|
|
576
651
|
|
|
652
|
+
def _close_part(syntax: tuple[str, str], style: Style) -> str:
|
|
653
|
+
"""Return the trailing part of a single-line banner (space plus closer).
|
|
654
|
+
|
|
655
|
+
A block comment uses its own closer. A line comment has none, but closes
|
|
656
|
+
with a mirror of the opener when `bookend` is on, and is otherwise bare.
|
|
657
|
+
"""
|
|
658
|
+
opener, closer = syntax
|
|
659
|
+
if closer:
|
|
660
|
+
return f" {closer}"
|
|
661
|
+
return f" {opener}" if style.bookend else ""
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
def _tile(fill: str, count: int) -> str:
|
|
665
|
+
"""Return exactly `count` characters of the fill, tiling and truncating.
|
|
666
|
+
|
|
667
|
+
For a single-character fill this is just repetition; a multi-character fill
|
|
668
|
+
is repeated and cut to length, so `-=` at width 7 gives `-=-=-=-`.
|
|
669
|
+
"""
|
|
670
|
+
if count <= 0:
|
|
671
|
+
return ""
|
|
672
|
+
return (fill * (count // len(fill) + 1))[:count]
|
|
673
|
+
|
|
674
|
+
|
|
577
675
|
def _format_banner(
|
|
578
676
|
indent: str, syntax: tuple[str, str], title: str, style: Style
|
|
579
677
|
) -> str:
|
|
580
678
|
"""Render a canonical single-line banner, centred or left-aligned."""
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
679
|
+
open_part = f"{syntax[0]} "
|
|
680
|
+
close_part = _close_part(syntax, style)
|
|
681
|
+
if style.fill_mode == "fixed":
|
|
682
|
+
run = _tile(style.fill, style.fill_count)
|
|
683
|
+
if style.align == "left":
|
|
684
|
+
return f"{indent}{open_part}{title} {run}{close_part}"
|
|
685
|
+
return f"{indent}{open_part}{run} {title} {run}{close_part}"
|
|
584
686
|
indent_cols = _indent_width(indent, style.tab_width)
|
|
585
687
|
if style.align == "left":
|
|
586
688
|
used = indent_cols + len(open_part) + len(title) + 1 + len(close_part)
|
|
587
689
|
count = max(style.width - used, style.min_run)
|
|
588
|
-
return f"{indent}{open_part}{title} {style.fill
|
|
690
|
+
return f"{indent}{open_part}{title} {_tile(style.fill, count)}{close_part}"
|
|
589
691
|
fixed = indent_cols + len(open_part) + 1 + len(title) + 1 + len(close_part)
|
|
590
692
|
total = max(style.width - fixed, 2 * style.min_run)
|
|
591
693
|
left = total // 2
|
|
592
694
|
right = total - left
|
|
593
|
-
return
|
|
695
|
+
return (
|
|
696
|
+
f"{indent}{open_part}{_tile(style.fill, left)} {title} "
|
|
697
|
+
f"{_tile(style.fill, right)}{close_part}"
|
|
698
|
+
)
|
|
594
699
|
|
|
595
700
|
|
|
596
701
|
def _format_rule(indent: str, syntax: tuple[str, str], style: Style) -> str:
|
|
@@ -602,7 +707,7 @@ def _format_rule(indent: str, syntax: tuple[str, str], style: Style) -> str:
|
|
|
602
707
|
count = max(
|
|
603
708
|
style.width - indent_cols - len(open_part) - len(close_part), style.min_run
|
|
604
709
|
)
|
|
605
|
-
return f"{indent}{open_part}{style.fill
|
|
710
|
+
return f"{indent}{open_part}{_tile(style.fill, count)}{close_part}"
|
|
606
711
|
|
|
607
712
|
|
|
608
713
|
def _format_title_line(indent: str, syntax: tuple[str, str], title: str) -> str:
|
|
@@ -622,21 +727,30 @@ def _render(
|
|
|
622
727
|
return [_format_banner(indent, syntax, title, style)]
|
|
623
728
|
|
|
624
729
|
|
|
625
|
-
def _title_limit(indent: str, syntax: tuple[str, str], style: Style) -> int:
|
|
626
|
-
"""Return the maximum title length
|
|
730
|
+
def _title_limit(indent: str, syntax: tuple[str, str], style: Style) -> int | None:
|
|
731
|
+
"""Return the maximum title length for the style and cap, or `None`.
|
|
732
|
+
|
|
733
|
+
`None` means the title has no length limit, which is the case for a
|
|
734
|
+
fixed-fill single-line banner: its fill runs are a constant length, so the
|
|
735
|
+
line grows with the title and `width` imposes no bound. Only an explicit
|
|
736
|
+
`max_title` caps it.
|
|
737
|
+
"""
|
|
627
738
|
opener, closer = syntax
|
|
628
739
|
open_part = f"{opener} "
|
|
629
|
-
|
|
740
|
+
if style.style != "box" and style.fill_mode == "fixed":
|
|
741
|
+
return style.max_title
|
|
630
742
|
indent_cols = _indent_width(indent, style.tab_width)
|
|
631
743
|
if style.style == "box":
|
|
632
|
-
|
|
744
|
+
# Box is unaffected by bookend, so only its own block closer counts.
|
|
745
|
+
box_close = f" {closer}" if closer else ""
|
|
746
|
+
fit = style.width - indent_cols - len(open_part) - len(box_close)
|
|
633
747
|
elif style.align == "left":
|
|
634
748
|
fit = (
|
|
635
749
|
style.width
|
|
636
750
|
- indent_cols
|
|
637
751
|
- len(open_part)
|
|
638
752
|
- 1 # the space between the title and its trailing fill
|
|
639
|
-
- len(
|
|
753
|
+
- len(_close_part(syntax, style))
|
|
640
754
|
- style.min_run
|
|
641
755
|
)
|
|
642
756
|
else:
|
|
@@ -645,7 +759,7 @@ def _title_limit(indent: str, syntax: tuple[str, str], style: Style) -> int:
|
|
|
645
759
|
- indent_cols
|
|
646
760
|
- len(open_part)
|
|
647
761
|
- 2 # the two spaces framing the title
|
|
648
|
-
- len(
|
|
762
|
+
- len(_close_part(syntax, style))
|
|
649
763
|
- 2 * style.min_run
|
|
650
764
|
)
|
|
651
765
|
fit = max(fit, 1)
|
|
@@ -695,7 +809,7 @@ def _emit_unit(
|
|
|
695
809
|
"""
|
|
696
810
|
original = "".join(lines[start : end + 1])
|
|
697
811
|
limit = _title_limit(indent, syntax, style)
|
|
698
|
-
if len(title) > limit:
|
|
812
|
+
if limit is not None and len(title) > limit:
|
|
699
813
|
return original, False, _too_long_error(path, start + 1, title, limit, style)
|
|
700
814
|
|
|
701
815
|
rendered = _render(indent, syntax, title, style)
|
|
@@ -17,7 +17,7 @@ def write(path, text, encoding="utf-8"):
|
|
|
17
17
|
return path
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
#
|
|
20
|
+
# ------------------------------- Per-language defaults --------------------------------
|
|
21
21
|
@pytest.mark.parametrize(
|
|
22
22
|
"suffix, comment, expected_width",
|
|
23
23
|
[
|
|
@@ -33,7 +33,7 @@ def test_builtin_default_width_per_language(tmp_path, suffix, comment, expected_
|
|
|
33
33
|
assert len(f.read_text(encoding="utf-8").splitlines()[0]) == expected_width
|
|
34
34
|
|
|
35
35
|
|
|
36
|
-
#
|
|
36
|
+
# --------------------------------- Config precedence ----------------------------------
|
|
37
37
|
def test_global_and_per_language_override(tmp_path):
|
|
38
38
|
write(
|
|
39
39
|
tmp_path / "pyproject.toml",
|
|
@@ -55,7 +55,30 @@ def test_flag_overrides_everything(tmp_path):
|
|
|
55
55
|
assert len(f.read_text().splitlines()[0]) == 50
|
|
56
56
|
|
|
57
57
|
|
|
58
|
-
#
|
|
58
|
+
# ------------------------------------- Fill mode --------------------------------------
|
|
59
|
+
def test_fixed_fill_mode_via_flags(tmp_path):
|
|
60
|
+
f = write(tmp_path / "a.py", "# ------- Setup -------\n")
|
|
61
|
+
assert main(["--fill-mode", "fixed", "--fill-count", "5", str(f)]) == 1
|
|
62
|
+
assert f.read_text(encoding="utf-8") == "# ----- Setup -----\n"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_fixed_fill_mode_from_config(tmp_path):
|
|
66
|
+
write(
|
|
67
|
+
tmp_path / "pyproject.toml",
|
|
68
|
+
'[tool.sectionise]\nfill_mode = "fixed"\nfill_count = 4\n',
|
|
69
|
+
)
|
|
70
|
+
f = write(tmp_path / "a.py", "# --- Setup ---\n")
|
|
71
|
+
main([str(tmp_path)])
|
|
72
|
+
assert f.read_text(encoding="utf-8") == "# ---- Setup ----\n"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_bookend_mirrors_opener_via_flag(tmp_path):
|
|
76
|
+
f = write(tmp_path / "a.py", "# --- Setup ---\n")
|
|
77
|
+
assert main(["--bookend", "--fill-mode", "fixed", "--fill-count", "4", str(f)]) == 1
|
|
78
|
+
assert f.read_text(encoding="utf-8") == "# ---- Setup ---- #\n"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# ------------------------------------- Exit codes -------------------------------------
|
|
59
82
|
def test_exit_zero_when_clean(tmp_path, capsys):
|
|
60
83
|
f = write(tmp_path / "a.py", "# --- x ---\ncode = 1\n")
|
|
61
84
|
main([str(f)]) # normalise first
|
|
@@ -74,7 +97,7 @@ def test_exit_two_on_over_long_title(tmp_path, capsys):
|
|
|
74
97
|
assert "section title" in err # errors go to stderr
|
|
75
98
|
|
|
76
99
|
|
|
77
|
-
#
|
|
100
|
+
# ----------------------------------- Check and diff -----------------------------------
|
|
78
101
|
def test_check_does_not_write(tmp_path):
|
|
79
102
|
f = write(tmp_path / "a.py", "# --- x ---\n")
|
|
80
103
|
main(["--check", str(f)])
|
|
@@ -90,7 +113,7 @@ def test_diff_prints_and_does_not_write(tmp_path, capsys):
|
|
|
90
113
|
assert f.read_text() == "# --- x ---\n" # unchanged on disk
|
|
91
114
|
|
|
92
115
|
|
|
93
|
-
#
|
|
116
|
+
# --------------------------------------- Stdin ----------------------------------------
|
|
94
117
|
def test_stdin_writes_to_stdout(monkeypatch, capsys):
|
|
95
118
|
monkeypatch.setattr("sys.stdin", io.StringIO("# --- hi ---\n"))
|
|
96
119
|
rc = main(["--width", "40", "-"])
|
|
@@ -105,7 +128,7 @@ def test_stdin_filename_picks_syntax(monkeypatch, capsys):
|
|
|
105
128
|
assert capsys.readouterr().out.startswith("// ")
|
|
106
129
|
|
|
107
130
|
|
|
108
|
-
#
|
|
131
|
+
# ----------------------------------- Directory walk -----------------------------------
|
|
109
132
|
def test_directory_walk_skips_vendored_dirs(tmp_path):
|
|
110
133
|
write(tmp_path / "pkg" / "y.py", "# --- y ---\n")
|
|
111
134
|
write(tmp_path / ".venv" / "x.py", "# --- x ---\n")
|
|
@@ -125,7 +148,7 @@ def test_unsupported_explicit_file_warns(tmp_path, capsys):
|
|
|
125
148
|
assert "unsupported" in capsys.readouterr().err
|
|
126
149
|
|
|
127
150
|
|
|
128
|
-
#
|
|
151
|
+
# --------------------------------- Custom and shebang ---------------------------------
|
|
129
152
|
def test_custom_language_from_config(tmp_path):
|
|
130
153
|
write(
|
|
131
154
|
tmp_path / "pyproject.toml",
|
|
@@ -152,7 +175,7 @@ def test_shebang_extensionless_script(tmp_path):
|
|
|
152
175
|
assert len(f.read_text().splitlines()[1]) == 88 # python default width
|
|
153
176
|
|
|
154
177
|
|
|
155
|
-
#
|
|
178
|
+
# -------------------------------- Encoding and endings --------------------------------
|
|
156
179
|
def test_lf_stays_lf(tmp_path):
|
|
157
180
|
f = tmp_path / "a.py"
|
|
158
181
|
f.write_bytes(b"# --- x ---\ncode = 1\n")
|
|
@@ -20,7 +20,7 @@ C = core.syntax_for(".c") # both // and /* */
|
|
|
20
20
|
|
|
21
21
|
Case = namedtuple("Case", "name style syntax src want")
|
|
22
22
|
|
|
23
|
-
#
|
|
23
|
+
# ---------------------------------- Transform table -----------------------------------
|
|
24
24
|
CASES = [
|
|
25
25
|
Case(
|
|
26
26
|
"centre from both sides",
|
|
@@ -64,6 +64,90 @@ CASES = [
|
|
|
64
64
|
"# --- Setup ---\n",
|
|
65
65
|
"# Setup --------------------------------\n",
|
|
66
66
|
),
|
|
67
|
+
Case(
|
|
68
|
+
"fixed fill, centre, matching run both sides",
|
|
69
|
+
dict(fill_mode="fixed"),
|
|
70
|
+
HASH,
|
|
71
|
+
"# ------- Setup -------\n",
|
|
72
|
+
"# --- Setup ---\n",
|
|
73
|
+
),
|
|
74
|
+
Case(
|
|
75
|
+
"fixed fill honours fill_count",
|
|
76
|
+
dict(fill_mode="fixed", fill_count=5),
|
|
77
|
+
HASH,
|
|
78
|
+
"# --- HEADER ---\n",
|
|
79
|
+
"# ----- HEADER -----\n",
|
|
80
|
+
),
|
|
81
|
+
Case(
|
|
82
|
+
"fixed fill, left align, trailing run only",
|
|
83
|
+
dict(fill_mode="fixed", align="left"),
|
|
84
|
+
HASH,
|
|
85
|
+
"# ------- Setup -------\n",
|
|
86
|
+
"# Setup ---\n",
|
|
87
|
+
),
|
|
88
|
+
Case(
|
|
89
|
+
"fixed fill has no width bound so a long title fits",
|
|
90
|
+
dict(fill_mode="fixed", width=20),
|
|
91
|
+
HASH,
|
|
92
|
+
"# ===== a very long section title indeed =====\n",
|
|
93
|
+
"# --- a very long section title indeed ---\n",
|
|
94
|
+
),
|
|
95
|
+
Case(
|
|
96
|
+
"box ignores fill_mode and still spans the width",
|
|
97
|
+
dict(fill_mode="fixed", style="box", width=40),
|
|
98
|
+
HASH,
|
|
99
|
+
"# --- Setup ---\n",
|
|
100
|
+
"# --------------------------------------\n# Setup\n# --------------------------------------\n",
|
|
101
|
+
),
|
|
102
|
+
Case(
|
|
103
|
+
"bookend closes with a mirror of the opener",
|
|
104
|
+
dict(width=40, bookend=True),
|
|
105
|
+
HASH,
|
|
106
|
+
"# ------- Setup -------\n",
|
|
107
|
+
"# -------------- Setup --------------- #\n",
|
|
108
|
+
),
|
|
109
|
+
Case(
|
|
110
|
+
"bookend, fixed fill",
|
|
111
|
+
dict(fill_mode="fixed", fill_count=4, bookend=True),
|
|
112
|
+
HASH,
|
|
113
|
+
"# --- NICE ---\n",
|
|
114
|
+
"# ---- NICE ---- #\n",
|
|
115
|
+
),
|
|
116
|
+
Case(
|
|
117
|
+
"bookend mirrors a slash opener with hash fill",
|
|
118
|
+
dict(fill_mode="fixed", fill_count=5, fill="#", bookend=True),
|
|
119
|
+
SLASH,
|
|
120
|
+
"// --- HEADER ---\n",
|
|
121
|
+
"// ##### HEADER ##### //\n",
|
|
122
|
+
),
|
|
123
|
+
Case(
|
|
124
|
+
"bookend, left align",
|
|
125
|
+
dict(width=40, align="left", bookend=True),
|
|
126
|
+
HASH,
|
|
127
|
+
"# ------- Setup -------\n",
|
|
128
|
+
"# Setup ------------------------------ #\n",
|
|
129
|
+
),
|
|
130
|
+
Case(
|
|
131
|
+
"trailing opener marker recognised even with bookend off",
|
|
132
|
+
dict(width=40),
|
|
133
|
+
SLASH,
|
|
134
|
+
"// ##### HEADER ##### //\n",
|
|
135
|
+
"// -------------- HEADER ---------------\n",
|
|
136
|
+
),
|
|
137
|
+
Case(
|
|
138
|
+
"multi-character fill is tiled to the width",
|
|
139
|
+
dict(width=40, fill="=-"),
|
|
140
|
+
HASH,
|
|
141
|
+
"# ------- Setup -------\n",
|
|
142
|
+
"# =-=-=-=-=-=-=-= Setup =-=-=-=-=-=-=-=-\n",
|
|
143
|
+
),
|
|
144
|
+
Case(
|
|
145
|
+
"multi-character fill, fixed count",
|
|
146
|
+
dict(fill_mode="fixed", fill_count=6, fill="=-"),
|
|
147
|
+
HASH,
|
|
148
|
+
"# --- Setup ---\n",
|
|
149
|
+
"# =-=-=- Setup =-=-=-\n",
|
|
150
|
+
),
|
|
67
151
|
Case(
|
|
68
152
|
"box collapses to single",
|
|
69
153
|
dict(width=40, style="single"),
|
|
@@ -106,6 +190,27 @@ CASES = [
|
|
|
106
190
|
"# ---------------- model config ---------------- #\n",
|
|
107
191
|
"# ---------------------- model config ----------------------\n",
|
|
108
192
|
),
|
|
193
|
+
Case(
|
|
194
|
+
"leading fill char attached to title is kept",
|
|
195
|
+
dict(width=40),
|
|
196
|
+
HASH,
|
|
197
|
+
"# ------- _function_name -------\n",
|
|
198
|
+
"# ----------- _function_name -----------\n",
|
|
199
|
+
),
|
|
200
|
+
Case(
|
|
201
|
+
"hash prefix in title is kept",
|
|
202
|
+
dict(width=40),
|
|
203
|
+
HASH,
|
|
204
|
+
"# --- #Nice ---\n",
|
|
205
|
+
"# --------------- #Nice ----------------\n",
|
|
206
|
+
),
|
|
207
|
+
Case(
|
|
208
|
+
"trailing fill char attached to title is kept",
|
|
209
|
+
dict(width=40),
|
|
210
|
+
HASH,
|
|
211
|
+
"# --- name_ ---\n",
|
|
212
|
+
"# --------------- name_ ----------------\n",
|
|
213
|
+
),
|
|
109
214
|
Case(
|
|
110
215
|
"title-less divider left alone by default",
|
|
111
216
|
dict(width=40),
|
|
@@ -228,7 +333,7 @@ def test_changed_flag_matches_whether_output_differs(case):
|
|
|
228
333
|
assert bool(changed) == (case.want != case.src)
|
|
229
334
|
|
|
230
335
|
|
|
231
|
-
#
|
|
336
|
+
# ----------------------------------- Ignored inputs -----------------------------------
|
|
232
337
|
@pytest.mark.parametrize(
|
|
233
338
|
"src",
|
|
234
339
|
[
|
|
@@ -245,7 +350,7 @@ def test_left_alone(src):
|
|
|
245
350
|
assert errors == []
|
|
246
351
|
|
|
247
352
|
|
|
248
|
-
#
|
|
353
|
+
# --------------------------------- String protection ----------------------------------
|
|
249
354
|
def test_banner_inside_python_string_is_left_untouched():
|
|
250
355
|
text = 'x = """\n# ==== not a comment ====\nhello\n"""\n'
|
|
251
356
|
protected = core.protected_lines(text, ".py")
|
|
@@ -282,7 +387,7 @@ def test_backtick_template_literal_protects_banner():
|
|
|
282
387
|
assert changed == 0
|
|
283
388
|
|
|
284
389
|
|
|
285
|
-
#
|
|
390
|
+
# ------------------------------------ Box handling ------------------------------------
|
|
286
391
|
def test_boxes_disabled_leaves_box_untouched():
|
|
287
392
|
text = "# ======\n# Setup\n# ======\n"
|
|
288
393
|
out, changed, _ = core.process_text(text, HASH, Style(boxes=False, width=40))
|
|
@@ -320,7 +425,7 @@ def test_box_rule_never_shorter_than_min_run(min_run):
|
|
|
320
425
|
assert out.splitlines()[0] == "# " + "-" * min_run
|
|
321
426
|
|
|
322
427
|
|
|
323
|
-
#
|
|
428
|
+
# ---------------------------------- Over-long titles ----------------------------------
|
|
324
429
|
def test_too_long_single_title_errors_and_leaves_unchanged():
|
|
325
430
|
text = "# --- a very long section title indeed ---\n"
|
|
326
431
|
out, changed, errors = core.process_text(text, HASH, Style(width=20))
|
|
@@ -335,7 +440,7 @@ def test_box_style_accommodates_longer_titles():
|
|
|
335
440
|
assert errors == []
|
|
336
441
|
|
|
337
442
|
|
|
338
|
-
#
|
|
443
|
+
# ------------------------------------- Alignment --------------------------------------
|
|
339
444
|
def test_left_align_is_idempotent():
|
|
340
445
|
style = Style(width=40, align="left")
|
|
341
446
|
once = core._format_banner("", HASH, "Section name", style)
|
|
@@ -356,18 +461,22 @@ def test_tab_indent_counts_as_configured_columns():
|
|
|
356
461
|
assert len(line.expandtabs(8)) == 40
|
|
357
462
|
|
|
358
463
|
|
|
359
|
-
#
|
|
464
|
+
# ------------------------------------- Validation -------------------------------------
|
|
360
465
|
@pytest.mark.parametrize(
|
|
361
466
|
"kwargs, message",
|
|
362
467
|
[
|
|
363
468
|
(dict(width=0), "width"),
|
|
364
469
|
(dict(width=-5), "width"),
|
|
365
470
|
(dict(min_run=0), "min_run"),
|
|
366
|
-
(dict(fill="
|
|
471
|
+
(dict(fill=""), "fill"),
|
|
367
472
|
(dict(fill=" "), "fill"),
|
|
473
|
+
(dict(fill="- "), "fill"),
|
|
368
474
|
(dict(detect_chars=""), "detect_chars"),
|
|
369
475
|
(dict(style="banner"), "style"),
|
|
370
476
|
(dict(align="middle"), "align"),
|
|
477
|
+
(dict(fill_mode="stretch"), "fill_mode"),
|
|
478
|
+
(dict(fill_count=0), "fill_count"),
|
|
479
|
+
(dict(fill_mode="fixed", fill_count=2, min_run=3), "min_run"),
|
|
371
480
|
(dict(tab_width=0), "tab_width"),
|
|
372
481
|
(dict(max_title=0), "max_title"),
|
|
373
482
|
],
|
|
@@ -385,7 +494,15 @@ def test_fill_added_to_detect_chars_for_idempotency():
|
|
|
385
494
|
assert changed == 0
|
|
386
495
|
|
|
387
496
|
|
|
388
|
-
|
|
497
|
+
def test_multi_character_fill_seeds_each_char_and_round_trips():
|
|
498
|
+
style = Style(fill="<>", detect_chars="-=")
|
|
499
|
+
assert "<" in style.detect_chars and ">" in style.detect_chars
|
|
500
|
+
once = core._format_banner("", HASH, "Title", style)
|
|
501
|
+
_, changed, _ = core.process_text(once + "\n", HASH, style)
|
|
502
|
+
assert changed == 0
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
# --------------------------------- Language registry ----------------------------------
|
|
389
506
|
@pytest.mark.parametrize(
|
|
390
507
|
"suffix, language, width",
|
|
391
508
|
[
|
|
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
|