sectionise 0.1.1__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.1.1/.env.example +12 -0
- sectionise-0.1.1/.github/workflows/publish.yml +19 -0
- sectionise-0.1.1/.gitignore +11 -0
- sectionise-0.1.1/.pre-commit-config.yaml +37 -0
- sectionise-0.1.1/.pre-commit-hooks.yaml +8 -0
- sectionise-0.1.1/.python-version +1 -0
- sectionise-0.1.1/LICENSE +21 -0
- sectionise-0.1.1/PKG-INFO +146 -0
- sectionise-0.1.1/README.md +129 -0
- sectionise-0.1.1/pyproject.toml +54 -0
- sectionise-0.1.1/sectionise/__init__.py +0 -0
- sectionise-0.1.1/sectionise/cli.py +150 -0
- sectionise-0.1.1/sectionise/core.py +386 -0
- sectionise-0.1.1/tests/test_core.py +142 -0
- sectionise-0.1.1/uv.lock +253 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Local publishing to PyPI. Copy this file to .env and paste your token in.
|
|
2
|
+
# .env is gitignored, so the token is never committed.
|
|
3
|
+
#
|
|
4
|
+
# cp .env.example .env
|
|
5
|
+
# # edit .env, then:
|
|
6
|
+
# set -a && source .env && set +a
|
|
7
|
+
# uv build && uv publish dist/*
|
|
8
|
+
#
|
|
9
|
+
# Create a token at https://pypi.org/manage/account/token/ (scope it to the
|
|
10
|
+
# sectionise project once it exists). In CI, this same value lives in the
|
|
11
|
+
# PYPI_API_TOKEN GitHub Actions secret instead.
|
|
12
|
+
UV_PUBLISH_TOKEN=pypi-your-token-here
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
name: publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "*.*.*"
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
publish:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
with:
|
|
14
|
+
fetch-depth: 0 # full history + tags so hatch-vcs can read the version
|
|
15
|
+
- uses: astral-sh/setup-uv@v5
|
|
16
|
+
- run: uv build
|
|
17
|
+
- run: uv publish dist/*
|
|
18
|
+
env:
|
|
19
|
+
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
repos:
|
|
2
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
3
|
+
rev: v0.14.9
|
|
4
|
+
hooks:
|
|
5
|
+
- id: ruff-check
|
|
6
|
+
args: [--fix]
|
|
7
|
+
stages: [pre-commit]
|
|
8
|
+
- id: ruff-format
|
|
9
|
+
stages: [pre-commit]
|
|
10
|
+
|
|
11
|
+
- repo: https://github.com/tox-dev/pyproject-fmt
|
|
12
|
+
rev: v2.8.0
|
|
13
|
+
hooks:
|
|
14
|
+
- id: pyproject-fmt
|
|
15
|
+
stages: [pre-commit]
|
|
16
|
+
|
|
17
|
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
18
|
+
rev: v6.0.0
|
|
19
|
+
hooks:
|
|
20
|
+
- id: check-merge-conflict
|
|
21
|
+
stages: [pre-commit]
|
|
22
|
+
- id: end-of-file-fixer
|
|
23
|
+
stages: [pre-commit]
|
|
24
|
+
- id: trailing-whitespace
|
|
25
|
+
stages: [pre-commit]
|
|
26
|
+
- id: check-added-large-files
|
|
27
|
+
stages: [pre-commit]
|
|
28
|
+
|
|
29
|
+
# Dogfood: standardise this repo's own section headers.
|
|
30
|
+
- repo: local
|
|
31
|
+
hooks:
|
|
32
|
+
- id: sectionise
|
|
33
|
+
name: Section header banners
|
|
34
|
+
entry: sectionise
|
|
35
|
+
language: system
|
|
36
|
+
types: [text]
|
|
37
|
+
stages: [pre-commit]
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
- id: sectionise
|
|
2
|
+
name: Section header banners
|
|
3
|
+
description: >
|
|
4
|
+
Standardise section-header comment banners to one canonical style. Reads
|
|
5
|
+
settings from [tool.sectionise] in the repo's pyproject.toml; flags override.
|
|
6
|
+
entry: sectionise
|
|
7
|
+
language: python
|
|
8
|
+
types: [text]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12
|
sectionise-0.1.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mitchell Needham
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sectionise
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Standardise section-header comment banners
|
|
5
|
+
Project-URL: Homepage, https://github.com/MitchellNeedham/sectionise
|
|
6
|
+
Project-URL: Repository, https://github.com/MitchellNeedham/sectionise
|
|
7
|
+
Author-email: Mitchell Needham <contact@mitchellneedham.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: comments,formatter,linter,pre-commit,section-headers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
|
+
Requires-Python: >=3.12
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# sectionise
|
|
19
|
+
|
|
20
|
+
[](https://pypi.org/project/sectionise/)
|
|
21
|
+
[](https://pypi.org/project/sectionise/)
|
|
22
|
+
|
|
23
|
+
Standardise section-header comment banners across a codebase to one canonical
|
|
24
|
+
style. Runs as a pre-commit hook or a CLI, and is configured from
|
|
25
|
+
`pyproject.toml` (with flag overrides).
|
|
26
|
+
|
|
27
|
+
A section header is a comment styled as a banner: a title framed by a run of
|
|
28
|
+
fill characters. Real code accumulates many variants; `sectionise` detects them
|
|
29
|
+
and rewrites each to the same shape.
|
|
30
|
+
|
|
31
|
+
## What it standardises
|
|
32
|
+
|
|
33
|
+
| Variant | Example in |
|
|
34
|
+
| --- | --- |
|
|
35
|
+
| Single-line, framed both sides | `# ------- Loading models -------` |
|
|
36
|
+
| Single-line, filled one side | `# Ancillary functions ----------` |
|
|
37
|
+
| Unicode fill (em/en dash, box rules) | `# ——— Loading models ———` |
|
|
38
|
+
| Three-line box | a rule line, a title comment, a rule line |
|
|
39
|
+
|
|
40
|
+
All of them normalise to the configured style, for example the single-line form:
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
# ---------------------------- Loading models -----------------------------
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Detection is conservative: only full-line comments whose content is framed by a
|
|
47
|
+
fill run of at least `min_run` characters are touched. Ordinary comments,
|
|
48
|
+
trailing comments, and commented-out code (`# print("=== run ===")`) are left
|
|
49
|
+
alone. Title-less rules (`# --------`) are ignored unless `dividers` is enabled.
|
|
50
|
+
|
|
51
|
+
A title too long to fit the chosen style is reported as an error suggesting a
|
|
52
|
+
shorter title or the multi-line `box` style, rather than being silently
|
|
53
|
+
overflowed.
|
|
54
|
+
|
|
55
|
+
## Installation
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pip install sectionise
|
|
59
|
+
# or: uv add sectionise (add to a project)
|
|
60
|
+
# or: uvx sectionise ... (run without installing)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Use as a pre-commit hook
|
|
64
|
+
|
|
65
|
+
```yaml
|
|
66
|
+
- repo: https://github.com/MitchellNeedham/sectionise
|
|
67
|
+
rev: 0.1.0
|
|
68
|
+
hooks:
|
|
69
|
+
- id: sectionise
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
It fixes in place and fails the commit if it changed anything (re-stage and
|
|
73
|
+
commit again), the same way `ruff-format` behaves.
|
|
74
|
+
|
|
75
|
+
## Use as a CLI
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
sectionise path/to/file.py # fix in place
|
|
79
|
+
sectionise --check path/to/file.py # report only, no writes
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Configuration
|
|
83
|
+
|
|
84
|
+
Settings resolve with the precedence **flags > `[tool.sectionise]` > defaults**.
|
|
85
|
+
Put shared settings in each repo's `pyproject.toml`:
|
|
86
|
+
|
|
87
|
+
```toml
|
|
88
|
+
[tool.sectionise]
|
|
89
|
+
width = 88
|
|
90
|
+
style = "single" # or "box"
|
|
91
|
+
fill = "-"
|
|
92
|
+
detect_chars = "-=*_~#—–─═"
|
|
93
|
+
min_run = 3
|
|
94
|
+
require_both_sides = false
|
|
95
|
+
dividers = false
|
|
96
|
+
# max_title = 60 # optional hard cap on title length
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
| Setting | Flag | Default | Meaning |
|
|
100
|
+
| --- | --- | --- | --- |
|
|
101
|
+
| `width` | `--width` | `88` | Target total line length. |
|
|
102
|
+
| `style` | `--style` | `single` | Output form: `single` line or 3-line `box`. |
|
|
103
|
+
| `fill` | `--fill` | `-` | Output fill character. |
|
|
104
|
+
| `detect_chars` | `--detect-chars` | `-=*_~#—–─═` | Characters recognised as fill in input. |
|
|
105
|
+
| `min_run` | `--min-run` | `3` | Minimum fill-run length to count as a banner side. |
|
|
106
|
+
| `require_both_sides` | `--require-both-sides` | `false` | Only treat both-sided comments as banners. |
|
|
107
|
+
| `dividers` | `--dividers` | `false` | Also standardise title-less rules. |
|
|
108
|
+
| `max_title` | `--max-title` | none | Hard cap on title length, on top of the width fit. |
|
|
109
|
+
|
|
110
|
+
## Development
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
uv sync
|
|
114
|
+
uv run pytest
|
|
115
|
+
uv run ruff check
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Publishing
|
|
119
|
+
|
|
120
|
+
Releases go to [PyPI](https://pypi.org/project/sectionise/) on a version tag.
|
|
121
|
+
|
|
122
|
+
- **Set the token once.** In the GitHub repo, add a repository secret named
|
|
123
|
+
`PYPI_API_TOKEN` holding a PyPI API token
|
|
124
|
+
(Settings > Secrets and variables > Actions).
|
|
125
|
+
- **Release.** Tag a version and push it; the publish workflow builds and
|
|
126
|
+
uploads:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
git tag 0.1.0
|
|
130
|
+
git push origin 0.1.0
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
The version comes from the tag via `hatch-vcs`, so the tag is the single
|
|
134
|
+
source of truth.
|
|
135
|
+
|
|
136
|
+
To publish from your machine instead, copy `.env.example` to `.env`, paste your
|
|
137
|
+
token, and run:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
set -a && source .env && set +a
|
|
141
|
+
uv build && uv publish dist/*
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## License
|
|
145
|
+
|
|
146
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# sectionise
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/sectionise/)
|
|
4
|
+
[](https://pypi.org/project/sectionise/)
|
|
5
|
+
|
|
6
|
+
Standardise section-header comment banners across a codebase to one canonical
|
|
7
|
+
style. Runs as a pre-commit hook or a CLI, and is configured from
|
|
8
|
+
`pyproject.toml` (with flag overrides).
|
|
9
|
+
|
|
10
|
+
A section header is a comment styled as a banner: a title framed by a run of
|
|
11
|
+
fill characters. Real code accumulates many variants; `sectionise` detects them
|
|
12
|
+
and rewrites each to the same shape.
|
|
13
|
+
|
|
14
|
+
## What it standardises
|
|
15
|
+
|
|
16
|
+
| Variant | Example in |
|
|
17
|
+
| --- | --- |
|
|
18
|
+
| Single-line, framed both sides | `# ------- Loading models -------` |
|
|
19
|
+
| Single-line, filled one side | `# Ancillary functions ----------` |
|
|
20
|
+
| Unicode fill (em/en dash, box rules) | `# ——— Loading models ———` |
|
|
21
|
+
| Three-line box | a rule line, a title comment, a rule line |
|
|
22
|
+
|
|
23
|
+
All of them normalise to the configured style, for example the single-line form:
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
# ---------------------------- Loading models -----------------------------
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Detection is conservative: only full-line comments whose content is framed by a
|
|
30
|
+
fill run of at least `min_run` characters are touched. Ordinary comments,
|
|
31
|
+
trailing comments, and commented-out code (`# print("=== run ===")`) are left
|
|
32
|
+
alone. Title-less rules (`# --------`) are ignored unless `dividers` is enabled.
|
|
33
|
+
|
|
34
|
+
A title too long to fit the chosen style is reported as an error suggesting a
|
|
35
|
+
shorter title or the multi-line `box` style, rather than being silently
|
|
36
|
+
overflowed.
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install sectionise
|
|
42
|
+
# or: uv add sectionise (add to a project)
|
|
43
|
+
# or: uvx sectionise ... (run without installing)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Use as a pre-commit hook
|
|
47
|
+
|
|
48
|
+
```yaml
|
|
49
|
+
- repo: https://github.com/MitchellNeedham/sectionise
|
|
50
|
+
rev: 0.1.0
|
|
51
|
+
hooks:
|
|
52
|
+
- id: sectionise
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
It fixes in place and fails the commit if it changed anything (re-stage and
|
|
56
|
+
commit again), the same way `ruff-format` behaves.
|
|
57
|
+
|
|
58
|
+
## Use as a CLI
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
sectionise path/to/file.py # fix in place
|
|
62
|
+
sectionise --check path/to/file.py # report only, no writes
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Configuration
|
|
66
|
+
|
|
67
|
+
Settings resolve with the precedence **flags > `[tool.sectionise]` > defaults**.
|
|
68
|
+
Put shared settings in each repo's `pyproject.toml`:
|
|
69
|
+
|
|
70
|
+
```toml
|
|
71
|
+
[tool.sectionise]
|
|
72
|
+
width = 88
|
|
73
|
+
style = "single" # or "box"
|
|
74
|
+
fill = "-"
|
|
75
|
+
detect_chars = "-=*_~#—–─═"
|
|
76
|
+
min_run = 3
|
|
77
|
+
require_both_sides = false
|
|
78
|
+
dividers = false
|
|
79
|
+
# max_title = 60 # optional hard cap on title length
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
| Setting | Flag | Default | Meaning |
|
|
83
|
+
| --- | --- | --- | --- |
|
|
84
|
+
| `width` | `--width` | `88` | Target total line length. |
|
|
85
|
+
| `style` | `--style` | `single` | Output form: `single` line or 3-line `box`. |
|
|
86
|
+
| `fill` | `--fill` | `-` | Output fill character. |
|
|
87
|
+
| `detect_chars` | `--detect-chars` | `-=*_~#—–─═` | Characters recognised as fill in input. |
|
|
88
|
+
| `min_run` | `--min-run` | `3` | Minimum fill-run length to count as a banner side. |
|
|
89
|
+
| `require_both_sides` | `--require-both-sides` | `false` | Only treat both-sided comments as banners. |
|
|
90
|
+
| `dividers` | `--dividers` | `false` | Also standardise title-less rules. |
|
|
91
|
+
| `max_title` | `--max-title` | none | Hard cap on title length, on top of the width fit. |
|
|
92
|
+
|
|
93
|
+
## Development
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
uv sync
|
|
97
|
+
uv run pytest
|
|
98
|
+
uv run ruff check
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Publishing
|
|
102
|
+
|
|
103
|
+
Releases go to [PyPI](https://pypi.org/project/sectionise/) on a version tag.
|
|
104
|
+
|
|
105
|
+
- **Set the token once.** In the GitHub repo, add a repository secret named
|
|
106
|
+
`PYPI_API_TOKEN` holding a PyPI API token
|
|
107
|
+
(Settings > Secrets and variables > Actions).
|
|
108
|
+
- **Release.** Tag a version and push it; the publish workflow builds and
|
|
109
|
+
uploads:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
git tag 0.1.0
|
|
113
|
+
git push origin 0.1.0
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
The version comes from the tag via `hatch-vcs`, so the tag is the single
|
|
117
|
+
source of truth.
|
|
118
|
+
|
|
119
|
+
To publish from your machine instead, copy `.env.example` to `.env`, paste your
|
|
120
|
+
token, and run:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
set -a && source .env && set +a
|
|
124
|
+
uv build && uv publish dist/*
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## License
|
|
128
|
+
|
|
129
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
build-backend = "hatchling.build"
|
|
3
|
+
requires = [ "hatch-vcs", "hatchling" ]
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "sectionise"
|
|
7
|
+
description = "Standardise section-header comment banners"
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
license = "MIT"
|
|
10
|
+
authors = [ { name = "Mitchell Needham", email = "contact@mitchellneedham.com" } ]
|
|
11
|
+
requires-python = ">=3.12"
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
14
|
+
"Programming Language :: Python :: 3.12",
|
|
15
|
+
"Programming Language :: Python :: 3.13",
|
|
16
|
+
"Programming Language :: Python :: 3.14",
|
|
17
|
+
]
|
|
18
|
+
dynamic = [ "version" ]
|
|
19
|
+
dependencies = [ ]
|
|
20
|
+
keywords = [ "comments", "formatter", "linter", "pre-commit", "section-headers" ]
|
|
21
|
+
|
|
22
|
+
urls.Homepage = "https://github.com/MitchellNeedham/sectionise"
|
|
23
|
+
urls.Repository = "https://github.com/MitchellNeedham/sectionise"
|
|
24
|
+
|
|
25
|
+
scripts.sectionise = "sectionise.cli:main"
|
|
26
|
+
|
|
27
|
+
[dependency-groups]
|
|
28
|
+
dev = [
|
|
29
|
+
"pre-commit>=4.5",
|
|
30
|
+
"pytest>=8",
|
|
31
|
+
"ruff>=0.14",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[tool.hatch.version]
|
|
35
|
+
source = "vcs"
|
|
36
|
+
|
|
37
|
+
[tool.ruff]
|
|
38
|
+
lint.select = [ "E", "F", "I", "W" ]
|
|
39
|
+
lint.extend-select = [ "D" ]
|
|
40
|
+
lint.ignore = [ "D105", "D107", "D413", "E501" ]
|
|
41
|
+
lint.per-file-ignores."**/*.py" = [ "E731", "F403", "F405" ]
|
|
42
|
+
lint.per-file-ignores."**/{__init__,conftest}.py" = [ "F401", "F403" ]
|
|
43
|
+
lint.per-file-ignores."**/__init__.py" = [ "D" ]
|
|
44
|
+
lint.per-file-ignores."tests/**/*.py" = [ "D" ]
|
|
45
|
+
lint.pydocstyle.convention = "google"
|
|
46
|
+
|
|
47
|
+
[tool.pytest.ini_options]
|
|
48
|
+
testpaths = [ "tests" ]
|
|
49
|
+
|
|
50
|
+
# Dogfood: sectionise standardises its own section headers with these settings.
|
|
51
|
+
[tool.sectionise]
|
|
52
|
+
width = 88
|
|
53
|
+
style = "single"
|
|
54
|
+
fill = "-"
|
|
File without changes
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""Command-line entry point for sectionise.
|
|
2
|
+
|
|
3
|
+
Resolves settings with the precedence flags > `[tool.sectionise]` in the nearest
|
|
4
|
+
`pyproject.toml` > built-in defaults, then lints or autofixes the given files.
|
|
5
|
+
Exits non-zero when it changed anything or hit an over-long title, matching the
|
|
6
|
+
pre-commit formatter convention.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import tomllib
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from . import core
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _find_pyproject(start: Path) -> Path | None:
|
|
17
|
+
"""Return the nearest `pyproject.toml` at or above `start`, else `None`."""
|
|
18
|
+
start = start.resolve()
|
|
19
|
+
for parent in (start, *start.parents):
|
|
20
|
+
candidate = parent / "pyproject.toml"
|
|
21
|
+
if candidate.is_file():
|
|
22
|
+
return candidate
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _load_config(path: Path | None) -> dict:
|
|
27
|
+
"""Read the `[tool.sectionise]` table from a `pyproject.toml`.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
path: The file to read, or `None`.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
The table as a dict, or an empty dict when absent or unreadable.
|
|
34
|
+
"""
|
|
35
|
+
if path is None or not path.is_file():
|
|
36
|
+
return {}
|
|
37
|
+
try:
|
|
38
|
+
with open(path, "rb") as handle:
|
|
39
|
+
data = tomllib.load(handle)
|
|
40
|
+
except (OSError, tomllib.TOMLDecodeError):
|
|
41
|
+
return {}
|
|
42
|
+
tool = data.get("tool", {})
|
|
43
|
+
section = tool.get("sectionise", {})
|
|
44
|
+
return section if isinstance(section, dict) else {}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
48
|
+
"""Build the argument parser.
|
|
49
|
+
|
|
50
|
+
Overridable options default to `None` so an unset flag falls through to
|
|
51
|
+
`pyproject.toml` and then the built-in default.
|
|
52
|
+
"""
|
|
53
|
+
parser = argparse.ArgumentParser(
|
|
54
|
+
prog="sectionise",
|
|
55
|
+
description="Standardise section-header comment banners.",
|
|
56
|
+
)
|
|
57
|
+
parser.add_argument("filenames", nargs="*", help="Files to process.")
|
|
58
|
+
parser.add_argument("--config", type=Path, default=None, help="pyproject.toml to read.")
|
|
59
|
+
parser.add_argument("--width", type=int, default=None, help="Target line length.")
|
|
60
|
+
parser.add_argument("--fill", default=None, help="Output fill character.")
|
|
61
|
+
parser.add_argument(
|
|
62
|
+
"--detect-chars", default=None, help="Characters recognised as banner fill."
|
|
63
|
+
)
|
|
64
|
+
parser.add_argument("--min-run", type=int, default=None, help="Minimum fill-run length.")
|
|
65
|
+
parser.add_argument(
|
|
66
|
+
"--style", choices=("single", "box"), default=None, help="Output form."
|
|
67
|
+
)
|
|
68
|
+
parser.add_argument(
|
|
69
|
+
"--max-title", type=int, default=None, help="Hard cap on title length."
|
|
70
|
+
)
|
|
71
|
+
parser.add_argument(
|
|
72
|
+
"--require-both-sides",
|
|
73
|
+
action=argparse.BooleanOptionalAction,
|
|
74
|
+
default=None,
|
|
75
|
+
help="Only treat comments with fill on both sides as banners.",
|
|
76
|
+
)
|
|
77
|
+
parser.add_argument(
|
|
78
|
+
"--dividers",
|
|
79
|
+
action=argparse.BooleanOptionalAction,
|
|
80
|
+
default=None,
|
|
81
|
+
help="Also standardise stand-alone title-less rules.",
|
|
82
|
+
)
|
|
83
|
+
parser.add_argument(
|
|
84
|
+
"--check", action="store_true", help="Report only; do not rewrite files."
|
|
85
|
+
)
|
|
86
|
+
return parser
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _resolve_style(args: argparse.Namespace, config: dict) -> core.Style:
|
|
90
|
+
"""Merge flags over `pyproject.toml` over defaults into a `Style`."""
|
|
91
|
+
|
|
92
|
+
def pick(flag_value, key, default):
|
|
93
|
+
if flag_value is not None:
|
|
94
|
+
return flag_value
|
|
95
|
+
return config.get(key, default)
|
|
96
|
+
|
|
97
|
+
return core.Style(
|
|
98
|
+
width=pick(args.width, "width", core.DEFAULT_WIDTH),
|
|
99
|
+
fill=pick(args.fill, "fill", core.DEFAULT_FILL),
|
|
100
|
+
detect_chars=pick(args.detect_chars, "detect_chars", core.DEFAULT_DETECT_CHARS),
|
|
101
|
+
min_run=pick(args.min_run, "min_run", core.DEFAULT_MIN_RUN),
|
|
102
|
+
require_both_sides=pick(args.require_both_sides, "require_both_sides", False),
|
|
103
|
+
dividers=pick(args.dividers, "dividers", False),
|
|
104
|
+
style=pick(args.style, "style", core.DEFAULT_STYLE),
|
|
105
|
+
max_title=pick(args.max_title, "max_title", None),
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def main(argv: list[str] | None = None) -> int:
|
|
110
|
+
"""Lint or autofix section-header banners in the given files.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
argv: Argument list; defaults to `sys.argv[1:]`.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
`1` if any file was (or would be) changed or a title was too long, else
|
|
117
|
+
`0`.
|
|
118
|
+
"""
|
|
119
|
+
args = _build_parser().parse_args(argv)
|
|
120
|
+
config = _load_config(args.config or _find_pyproject(Path.cwd()))
|
|
121
|
+
style = _resolve_style(args, config)
|
|
122
|
+
|
|
123
|
+
changed_files: list[str] = []
|
|
124
|
+
all_errors: list[str] = []
|
|
125
|
+
for name in args.filenames:
|
|
126
|
+
path = Path(name)
|
|
127
|
+
syntax = core.syntax_for(path.suffix)
|
|
128
|
+
if syntax is None:
|
|
129
|
+
continue
|
|
130
|
+
try:
|
|
131
|
+
text = path.read_text(encoding="utf-8")
|
|
132
|
+
except (OSError, UnicodeDecodeError):
|
|
133
|
+
continue
|
|
134
|
+
new_text, changed, errors = core.process_text(text, syntax, style, name)
|
|
135
|
+
all_errors.extend(errors)
|
|
136
|
+
if changed:
|
|
137
|
+
changed_files.append(name)
|
|
138
|
+
if not args.check:
|
|
139
|
+
path.write_text(new_text, encoding="utf-8")
|
|
140
|
+
|
|
141
|
+
verb = "would reformat" if args.check else "reformatted"
|
|
142
|
+
for name in changed_files:
|
|
143
|
+
print(f"{verb} section headers in {name}")
|
|
144
|
+
for error in all_errors:
|
|
145
|
+
print(error)
|
|
146
|
+
return 1 if changed_files or all_errors else 0
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
if __name__ == "__main__":
|
|
150
|
+
raise SystemExit(main())
|