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.
@@ -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,11 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .ruff_cache/
8
+ .pytest_cache/
9
+ .env
10
+ .env.local
11
+ *.pypi-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
@@ -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
+ [![PyPI](https://img.shields.io/pypi/v/sectionise.svg)](https://pypi.org/project/sectionise/)
21
+ [![Python](https://img.shields.io/pypi/pyversions/sectionise.svg)](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
+ [![PyPI](https://img.shields.io/pypi/v/sectionise.svg)](https://pypi.org/project/sectionise/)
4
+ [![Python](https://img.shields.io/pypi/pyversions/sectionise.svg)](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())