pypfmt 0.1.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.
- pypfmt-0.1.0/.gitignore +145 -0
- pypfmt-0.1.0/LICENSE +22 -0
- pypfmt-0.1.0/PKG-INFO +132 -0
- pypfmt-0.1.0/README.md +103 -0
- pypfmt-0.1.0/pyproject.toml +199 -0
- pypfmt-0.1.0/src/pypfmt/__init__.py +3 -0
- pypfmt-0.1.0/src/pypfmt/__main__.py +5 -0
- pypfmt-0.1.0/src/pypfmt/cli.py +205 -0
- pypfmt-0.1.0/src/pypfmt/config.py +336 -0
- pypfmt-0.1.0/src/pypfmt/formatter.py +54 -0
- pypfmt-0.1.0/src/pypfmt/pipeline.py +67 -0
- pypfmt-0.1.0/src/pypfmt/py.typed +0 -0
- pypfmt-0.1.0/src/pypfmt/sorter.py +58 -0
pypfmt-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
*.manifest
|
|
31
|
+
*.spec
|
|
32
|
+
|
|
33
|
+
# Installer logs
|
|
34
|
+
pip-log.txt
|
|
35
|
+
pip-delete-this-directory.txt
|
|
36
|
+
|
|
37
|
+
# Unit test / coverage reports
|
|
38
|
+
htmlcov/
|
|
39
|
+
.tox/
|
|
40
|
+
.nox/
|
|
41
|
+
.coverage
|
|
42
|
+
.coverage.*
|
|
43
|
+
.cache
|
|
44
|
+
nosetests.xml
|
|
45
|
+
coverage.xml
|
|
46
|
+
*.cover
|
|
47
|
+
*.py,cover
|
|
48
|
+
.hypothesis/
|
|
49
|
+
.pytest_cache/
|
|
50
|
+
|
|
51
|
+
# Translations
|
|
52
|
+
*.mo
|
|
53
|
+
*.pot
|
|
54
|
+
|
|
55
|
+
# Django stuff:
|
|
56
|
+
*.log
|
|
57
|
+
local_settings.py
|
|
58
|
+
db.sqlite3
|
|
59
|
+
db.sqlite3-journal
|
|
60
|
+
|
|
61
|
+
# Flask stuff:
|
|
62
|
+
instance/
|
|
63
|
+
.webassets-cache
|
|
64
|
+
|
|
65
|
+
# Scrapy stuff:
|
|
66
|
+
.scrapy
|
|
67
|
+
|
|
68
|
+
# Sphinx documentation
|
|
69
|
+
docs/_build/
|
|
70
|
+
|
|
71
|
+
# PyBuilder
|
|
72
|
+
.pybuilder/
|
|
73
|
+
target/
|
|
74
|
+
|
|
75
|
+
# Jupyter Notebook
|
|
76
|
+
.ipynb_checkpoints
|
|
77
|
+
|
|
78
|
+
# IPython
|
|
79
|
+
profile_default/
|
|
80
|
+
ipython_config.py
|
|
81
|
+
|
|
82
|
+
# pyenv
|
|
83
|
+
.python-version
|
|
84
|
+
|
|
85
|
+
# pipenv
|
|
86
|
+
Pipfile.lock
|
|
87
|
+
|
|
88
|
+
# PEP 582
|
|
89
|
+
__pypackages__/
|
|
90
|
+
|
|
91
|
+
# Celery stuff
|
|
92
|
+
celerybeat-schedule
|
|
93
|
+
celerybeat.pid
|
|
94
|
+
|
|
95
|
+
# SageMath parsed files
|
|
96
|
+
*.sage.py
|
|
97
|
+
|
|
98
|
+
# Environments
|
|
99
|
+
.env
|
|
100
|
+
.venv
|
|
101
|
+
env/
|
|
102
|
+
venv/
|
|
103
|
+
ENV/
|
|
104
|
+
env.bak/
|
|
105
|
+
venv.bak/
|
|
106
|
+
|
|
107
|
+
# Spyder project settings
|
|
108
|
+
.spyderproject
|
|
109
|
+
.spyproject
|
|
110
|
+
|
|
111
|
+
# Rope project settings
|
|
112
|
+
.ropeproject
|
|
113
|
+
|
|
114
|
+
# mkdocs documentation
|
|
115
|
+
/site
|
|
116
|
+
|
|
117
|
+
# mypy
|
|
118
|
+
.mypy_cache/
|
|
119
|
+
.dmypy.json
|
|
120
|
+
dmypy.json
|
|
121
|
+
|
|
122
|
+
# Pyre type checker
|
|
123
|
+
.pyre/
|
|
124
|
+
|
|
125
|
+
# ruff
|
|
126
|
+
.ruff_cache/
|
|
127
|
+
|
|
128
|
+
# ty
|
|
129
|
+
.ty_cache/
|
|
130
|
+
|
|
131
|
+
# IDE
|
|
132
|
+
.idea/
|
|
133
|
+
.vscode/
|
|
134
|
+
*.swp
|
|
135
|
+
*.swo
|
|
136
|
+
*~
|
|
137
|
+
|
|
138
|
+
# OS
|
|
139
|
+
.DS_Store
|
|
140
|
+
Thumbs.db
|
|
141
|
+
|
|
142
|
+
# Project specific
|
|
143
|
+
*.log
|
|
144
|
+
.env.*
|
|
145
|
+
!.env.example
|
pypfmt-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
|
|
2
|
+
MIT License
|
|
3
|
+
|
|
4
|
+
Copyright (c) 2026 Jamie McGregor Nelson
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
pypfmt-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pypfmt
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Python package to sort and format pyproject.toml
|
|
5
|
+
Project-URL: Changelog, https://github.com/bitflight-devops/pyproject-fmt/blob/main/CHANGELOG.md
|
|
6
|
+
Project-URL: Documentation, https://bitflight-devops.github.io/pyproject-fmt
|
|
7
|
+
Project-URL: Homepage, https://github.com/bitflight-devops/pyproject-fmt
|
|
8
|
+
Project-URL: Issues, https://github.com/bitflight-devops/pyproject-fmt/issues
|
|
9
|
+
Project-URL: Repository, https://github.com/bitflight-devops/pyproject-fmt
|
|
10
|
+
Author-email: Jamie McGregor Nelson <jamie@bitflight.io>
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.11
|
|
25
|
+
Requires-Dist: taplo>=0.9.0
|
|
26
|
+
Requires-Dist: toml-sort<0.25,>=0.24.0
|
|
27
|
+
Requires-Dist: typer>=0.12.0
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# pypfmt
|
|
31
|
+
|
|
32
|
+
[](https://github.com/bitflight-devops/pyproject-fmt/actions/workflows/ci.yml)
|
|
33
|
+
[](https://badge.fury.io/py/pypfmt)
|
|
34
|
+
[](https://codecov.io/gh/bitflight-devops/pyproject-fmt)
|
|
35
|
+
[](https://www.python.org/downloads/)
|
|
36
|
+
[](https://github.com/astral-sh/uv)
|
|
37
|
+
[](https://github.com/astral-sh/ruff)
|
|
38
|
+
[](https://github.com/astral-sh/ty)
|
|
39
|
+
[](https://github.com/bitflight-devops/pyproject-fmt/blob/main/LICENSE)
|
|
40
|
+
|
|
41
|
+
A Python package to sort and format pyproject.toml
|
|
42
|
+
|
|
43
|
+
## Features
|
|
44
|
+
|
|
45
|
+
- Fast and modern Python toolchain using Astral's tools (uv, ruff, ty)
|
|
46
|
+
- Type-safe with full type annotations
|
|
47
|
+
- Command-line interface built with Typer
|
|
48
|
+
- Comprehensive documentation with MkDocs — [View Docs](https://bitflight-devops.github.io/pyproject-fmt/)
|
|
49
|
+
|
|
50
|
+
## Installation
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install pypfmt
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Or using uv (recommended):
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
uv add pypfmt
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Quick Start
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
import pypfmt
|
|
66
|
+
|
|
67
|
+
print(pypfmt.__version__)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### CLI Usage
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
# Show version
|
|
74
|
+
pypfmt --version
|
|
75
|
+
|
|
76
|
+
# Say hello
|
|
77
|
+
pypfmt hello World
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Development
|
|
81
|
+
|
|
82
|
+
### Prerequisites
|
|
83
|
+
|
|
84
|
+
- Python 3.11+
|
|
85
|
+
- [uv](https://docs.astral.sh/uv/) for package management
|
|
86
|
+
|
|
87
|
+
### Setup
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
git clone https://github.com/bitflight-devops/pyproject-fmt.git
|
|
91
|
+
cd pyproject-fmt
|
|
92
|
+
uv sync --all-groups
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Running Tests
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
uv run poe test
|
|
99
|
+
|
|
100
|
+
# With coverage
|
|
101
|
+
uv run poe test-cov
|
|
102
|
+
|
|
103
|
+
# Across all Python versions
|
|
104
|
+
uv run poe test-matrix
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Code Quality
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
# Run all checks (lint, format, type-check)
|
|
111
|
+
uv run poe verify
|
|
112
|
+
|
|
113
|
+
# Auto-fix lint and format issues
|
|
114
|
+
uv run poe fix
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Prek
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
prek install
|
|
121
|
+
prek run --all-files
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Documentation
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
uv run poe docs-serve
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## License
|
|
131
|
+
|
|
132
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
pypfmt-0.1.0/README.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# pypfmt
|
|
2
|
+
|
|
3
|
+
[](https://github.com/bitflight-devops/pyproject-fmt/actions/workflows/ci.yml)
|
|
4
|
+
[](https://badge.fury.io/py/pypfmt)
|
|
5
|
+
[](https://codecov.io/gh/bitflight-devops/pyproject-fmt)
|
|
6
|
+
[](https://www.python.org/downloads/)
|
|
7
|
+
[](https://github.com/astral-sh/uv)
|
|
8
|
+
[](https://github.com/astral-sh/ruff)
|
|
9
|
+
[](https://github.com/astral-sh/ty)
|
|
10
|
+
[](https://github.com/bitflight-devops/pyproject-fmt/blob/main/LICENSE)
|
|
11
|
+
|
|
12
|
+
A Python package to sort and format pyproject.toml
|
|
13
|
+
|
|
14
|
+
## Features
|
|
15
|
+
|
|
16
|
+
- Fast and modern Python toolchain using Astral's tools (uv, ruff, ty)
|
|
17
|
+
- Type-safe with full type annotations
|
|
18
|
+
- Command-line interface built with Typer
|
|
19
|
+
- Comprehensive documentation with MkDocs — [View Docs](https://bitflight-devops.github.io/pyproject-fmt/)
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install pypfmt
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Or using uv (recommended):
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
uv add pypfmt
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Quick Start
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
import pypfmt
|
|
37
|
+
|
|
38
|
+
print(pypfmt.__version__)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### CLI Usage
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# Show version
|
|
45
|
+
pypfmt --version
|
|
46
|
+
|
|
47
|
+
# Say hello
|
|
48
|
+
pypfmt hello World
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Development
|
|
52
|
+
|
|
53
|
+
### Prerequisites
|
|
54
|
+
|
|
55
|
+
- Python 3.11+
|
|
56
|
+
- [uv](https://docs.astral.sh/uv/) for package management
|
|
57
|
+
|
|
58
|
+
### Setup
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
git clone https://github.com/bitflight-devops/pyproject-fmt.git
|
|
62
|
+
cd pyproject-fmt
|
|
63
|
+
uv sync --all-groups
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Running Tests
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
uv run poe test
|
|
70
|
+
|
|
71
|
+
# With coverage
|
|
72
|
+
uv run poe test-cov
|
|
73
|
+
|
|
74
|
+
# Across all Python versions
|
|
75
|
+
uv run poe test-matrix
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Code Quality
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
# Run all checks (lint, format, type-check)
|
|
82
|
+
uv run poe verify
|
|
83
|
+
|
|
84
|
+
# Auto-fix lint and format issues
|
|
85
|
+
uv run poe fix
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Prek
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
prek install
|
|
92
|
+
prek run --all-files
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Documentation
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
uv run poe docs-serve
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## License
|
|
102
|
+
|
|
103
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "pypfmt"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "A Python package to sort and format pyproject.toml"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Jamie McGregor Nelson", email = "jamie@bitflight.io" },
|
|
8
|
+
]
|
|
9
|
+
license = "MIT"
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 3 - Alpha",
|
|
12
|
+
"Intended Audience :: Developers",
|
|
13
|
+
"License :: OSI Approved :: MIT License",
|
|
14
|
+
"Operating System :: OS Independent",
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3.10",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Programming Language :: Python :: 3.13",
|
|
20
|
+
"Programming Language :: Python :: 3.14",
|
|
21
|
+
"Typing :: Typed",
|
|
22
|
+
]
|
|
23
|
+
keywords = []
|
|
24
|
+
requires-python = ">=3.11"
|
|
25
|
+
dependencies = [
|
|
26
|
+
"taplo>=0.9.0",
|
|
27
|
+
"toml-sort>=0.24.0,<0.25",
|
|
28
|
+
"typer>=0.12.0",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.scripts]
|
|
32
|
+
pypfmt = "pypfmt.cli:app"
|
|
33
|
+
|
|
34
|
+
[project.urls]
|
|
35
|
+
Changelog = "https://github.com/bitflight-devops/pyproject-fmt/blob/main/CHANGELOG.md"
|
|
36
|
+
Documentation = "https://bitflight-devops.github.io/pyproject-fmt"
|
|
37
|
+
Homepage = "https://github.com/bitflight-devops/pyproject-fmt"
|
|
38
|
+
Issues = "https://github.com/bitflight-devops/pyproject-fmt/issues"
|
|
39
|
+
Repository = "https://github.com/bitflight-devops/pyproject-fmt"
|
|
40
|
+
|
|
41
|
+
[build-system]
|
|
42
|
+
requires = ["hatchling"]
|
|
43
|
+
build-backend = "hatchling.build"
|
|
44
|
+
|
|
45
|
+
[dependency-groups]
|
|
46
|
+
dev = [
|
|
47
|
+
"hatch>=1.16.3",
|
|
48
|
+
"poethepoet>=0.32.0",
|
|
49
|
+
"prek>=0.1.0",
|
|
50
|
+
"pysentry-rs>=0.1.0",
|
|
51
|
+
"pytest-cov>=7.0.0",
|
|
52
|
+
"pytest>=9.0.0",
|
|
53
|
+
"ruff>=0.14.14",
|
|
54
|
+
"ty>=0.0.14",
|
|
55
|
+
]
|
|
56
|
+
docs = [
|
|
57
|
+
"mkdocs-material>=9.7.0",
|
|
58
|
+
"mkdocs>=1.6.0",
|
|
59
|
+
"mkdocstrings-python>=2.0.1",
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
[tool.coverage.report]
|
|
63
|
+
exclude_lines = [
|
|
64
|
+
"@abstractmethod",
|
|
65
|
+
"def __repr__",
|
|
66
|
+
"if TYPE_CHECKING:",
|
|
67
|
+
"if __name__ == .__main__.:",
|
|
68
|
+
"pragma: no cover",
|
|
69
|
+
"raise AssertionError",
|
|
70
|
+
"raise NotImplementedError",
|
|
71
|
+
]
|
|
72
|
+
show_missing = true
|
|
73
|
+
|
|
74
|
+
[tool.coverage.run]
|
|
75
|
+
branch = true
|
|
76
|
+
parallel = true
|
|
77
|
+
source = ["src/pypfmt"]
|
|
78
|
+
|
|
79
|
+
[tool.git-cliff]
|
|
80
|
+
config = "cliff.toml"
|
|
81
|
+
|
|
82
|
+
[tool.hatch.build]
|
|
83
|
+
|
|
84
|
+
[tool.hatch.build.targets.sdist]
|
|
85
|
+
include = [
|
|
86
|
+
"/src",
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
[tool.hatch.build.targets.wheel]
|
|
90
|
+
packages = ["src/pypfmt"]
|
|
91
|
+
|
|
92
|
+
# Matrix testing across Python versions
|
|
93
|
+
[tool.hatch.envs.test]
|
|
94
|
+
|
|
95
|
+
[[tool.hatch.envs.test.matrix]]
|
|
96
|
+
python = ["3.10", "3.11", "3.12", "3.13", "3.14"]
|
|
97
|
+
|
|
98
|
+
[tool.pytest.ini_options]
|
|
99
|
+
addopts = [
|
|
100
|
+
"--strict-config",
|
|
101
|
+
"--strict-markers",
|
|
102
|
+
"-q",
|
|
103
|
+
"-ra",
|
|
104
|
+
]
|
|
105
|
+
markers = [
|
|
106
|
+
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
|
|
107
|
+
]
|
|
108
|
+
pythonpath = ["src"]
|
|
109
|
+
testpaths = ["tests"]
|
|
110
|
+
|
|
111
|
+
[tool.ruff]
|
|
112
|
+
line-length = 88
|
|
113
|
+
src = ["src", "tests"]
|
|
114
|
+
target-version = "py311"
|
|
115
|
+
|
|
116
|
+
[tool.ruff.lint]
|
|
117
|
+
select = [
|
|
118
|
+
"ARG", # flake8-unused-arguments
|
|
119
|
+
"B", # flake8-bugbear
|
|
120
|
+
"C4", # flake8-comprehensions
|
|
121
|
+
"E", # pycodestyle errors
|
|
122
|
+
"ERA", # eradicate
|
|
123
|
+
"F", # Pyflakes
|
|
124
|
+
"I", # isort
|
|
125
|
+
"PTH", # flake8-use-pathlib
|
|
126
|
+
"RUF", # Ruff-specific rules
|
|
127
|
+
"SIM", # flake8-simplify
|
|
128
|
+
"TCH", # flake8-type-checking
|
|
129
|
+
"UP", # pyupgrade
|
|
130
|
+
"W", # pycodestyle warnings
|
|
131
|
+
]
|
|
132
|
+
|
|
133
|
+
[tool.ruff.lint.isort]
|
|
134
|
+
known-first-party = ["pypfmt"]
|
|
135
|
+
|
|
136
|
+
[tool.ruff.lint.per-file-ignores]
|
|
137
|
+
"tests/**/*.py" = ["ARG001"]
|
|
138
|
+
|
|
139
|
+
[tool.ty.src]
|
|
140
|
+
include = ["**/*.py"]
|
|
141
|
+
|
|
142
|
+
[tool.poe.tasks.lint]
|
|
143
|
+
cmd = "ruff check ."
|
|
144
|
+
help = "Run ruff linter"
|
|
145
|
+
|
|
146
|
+
[tool.poe.tasks.format-check]
|
|
147
|
+
cmd = "ruff format --check ."
|
|
148
|
+
help = "Check code formatting"
|
|
149
|
+
|
|
150
|
+
[tool.poe.tasks.format]
|
|
151
|
+
cmd = "ruff format ."
|
|
152
|
+
help = "Format code"
|
|
153
|
+
|
|
154
|
+
[tool.poe.tasks.type-check]
|
|
155
|
+
cmd = "ty check"
|
|
156
|
+
help = "Run ty type checker"
|
|
157
|
+
|
|
158
|
+
[tool.poe.tasks.verify]
|
|
159
|
+
sequence = ["lint", "format-check", "type-check"]
|
|
160
|
+
help = "Run all checks (lint, format-check, type-check)"
|
|
161
|
+
|
|
162
|
+
[tool.poe.tasks.fix]
|
|
163
|
+
sequence = [
|
|
164
|
+
{ cmd = "ruff check --fix ." },
|
|
165
|
+
{ cmd = "ruff format ." },
|
|
166
|
+
]
|
|
167
|
+
help = "Auto-fix lint and format issues"
|
|
168
|
+
|
|
169
|
+
[tool.poe.tasks.install]
|
|
170
|
+
cmd = "uv sync --all-groups"
|
|
171
|
+
help = "Install all dependencies"
|
|
172
|
+
|
|
173
|
+
[tool.poe.tasks.test]
|
|
174
|
+
cmd = "pytest tests/ -v"
|
|
175
|
+
help = "Run tests"
|
|
176
|
+
|
|
177
|
+
[tool.poe.tasks.test-cov]
|
|
178
|
+
cmd = "pytest --cov --cov-report=xml --cov-report=term-missing"
|
|
179
|
+
help = "Run tests with coverage"
|
|
180
|
+
|
|
181
|
+
[tool.poe.tasks.test-matrix]
|
|
182
|
+
cmd = "hatch test"
|
|
183
|
+
help = "Run tests across all Python versions"
|
|
184
|
+
|
|
185
|
+
[tool.poe.tasks.test-matrix-cov]
|
|
186
|
+
cmd = "hatch test --cover"
|
|
187
|
+
help = "Run tests with coverage across all Python versions"
|
|
188
|
+
|
|
189
|
+
[tool.poe.tasks.pysentry]
|
|
190
|
+
cmd = "pysentry-rs"
|
|
191
|
+
help = "Run dependency vulnerability scanning"
|
|
192
|
+
|
|
193
|
+
[tool.poe.tasks.docs]
|
|
194
|
+
cmd = "mkdocs build"
|
|
195
|
+
help = "Build documentation"
|
|
196
|
+
|
|
197
|
+
[tool.poe.tasks.docs-serve]
|
|
198
|
+
cmd = "mkdocs serve"
|
|
199
|
+
help = "Serve documentation locally"
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""Command-line interface for pypfmt."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import difflib
|
|
6
|
+
import sys
|
|
7
|
+
import tomllib
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Annotated
|
|
10
|
+
|
|
11
|
+
import typer
|
|
12
|
+
|
|
13
|
+
from pypfmt import __version__
|
|
14
|
+
from pypfmt.config import (
|
|
15
|
+
MergedConfig,
|
|
16
|
+
check_config_conflict,
|
|
17
|
+
load_config,
|
|
18
|
+
merge_config,
|
|
19
|
+
)
|
|
20
|
+
from pypfmt.pipeline import format_pyproject
|
|
21
|
+
|
|
22
|
+
_RED = "\033[31m"
|
|
23
|
+
_GREEN = "\033[32m"
|
|
24
|
+
_CYAN = "\033[36m"
|
|
25
|
+
_RESET = "\033[0m"
|
|
26
|
+
|
|
27
|
+
app = typer.Typer(
|
|
28
|
+
name="pypfmt",
|
|
29
|
+
help="Sort and format pyproject.toml files.",
|
|
30
|
+
add_completion=False,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _version_callback(value: bool) -> None:
|
|
35
|
+
"""Print version and exit."""
|
|
36
|
+
if value:
|
|
37
|
+
typer.echo(f"pypfmt {__version__}")
|
|
38
|
+
raise typer.Exit()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _print_diff(original: str, formatted: str, filename: str) -> None:
|
|
42
|
+
"""Print unified diff, colored if stdout is a terminal."""
|
|
43
|
+
diff_lines = difflib.unified_diff(
|
|
44
|
+
original.splitlines(keepends=True),
|
|
45
|
+
formatted.splitlines(keepends=True),
|
|
46
|
+
fromfile=f"a/{filename}",
|
|
47
|
+
tofile=f"b/{filename}",
|
|
48
|
+
)
|
|
49
|
+
use_color = sys.stdout.isatty()
|
|
50
|
+
for line in diff_lines:
|
|
51
|
+
if use_color:
|
|
52
|
+
if line.startswith(("---", "+++", "@@")):
|
|
53
|
+
sys.stdout.write(f"{_CYAN}{line}{_RESET}")
|
|
54
|
+
elif line.startswith("-"):
|
|
55
|
+
sys.stdout.write(f"{_RED}{line}{_RESET}")
|
|
56
|
+
elif line.startswith("+"):
|
|
57
|
+
sys.stdout.write(f"{_GREEN}{line}{_RESET}")
|
|
58
|
+
else:
|
|
59
|
+
sys.stdout.write(line)
|
|
60
|
+
else:
|
|
61
|
+
sys.stdout.write(line)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _load_and_warn(text: str) -> MergedConfig | None:
|
|
65
|
+
"""Load config from text, emit conflict warning, return merged config.
|
|
66
|
+
|
|
67
|
+
Returns a ``MergedConfig`` 5-tuple when ``[tool.pypfmt]`` is
|
|
68
|
+
present, or ``None`` so the pipeline uses its hardcoded defaults.
|
|
69
|
+
|
|
70
|
+
If the TOML is invalid, returns ``None`` -- the pipeline's own
|
|
71
|
+
validation will catch and report the parse error.
|
|
72
|
+
"""
|
|
73
|
+
try:
|
|
74
|
+
warning = check_config_conflict(text)
|
|
75
|
+
except tomllib.TOMLDecodeError:
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
if warning is not None:
|
|
79
|
+
typer.echo(warning, err=True)
|
|
80
|
+
|
|
81
|
+
user_config = load_config(text)
|
|
82
|
+
if user_config is None:
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
return merge_config(user_config)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _format_with_config(text: str, merged: MergedConfig | None) -> str:
|
|
89
|
+
"""Run ``format_pyproject`` with optional merged config."""
|
|
90
|
+
if merged is None:
|
|
91
|
+
return format_pyproject(text)
|
|
92
|
+
sort_cfg, overrides, comment_cfg, format_cfg, taplo_opts = merged
|
|
93
|
+
return format_pyproject(
|
|
94
|
+
text,
|
|
95
|
+
sort_config=sort_cfg,
|
|
96
|
+
sort_overrides=overrides,
|
|
97
|
+
comment_config=comment_cfg,
|
|
98
|
+
format_config=format_cfg,
|
|
99
|
+
taplo_options=taplo_opts,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _process_file(filepath: str, *, check: bool, diff: bool) -> int:
|
|
104
|
+
"""Process a single file through the formatting pipeline.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
0 on success (or no changes needed), 1 on error or check failure.
|
|
108
|
+
"""
|
|
109
|
+
try:
|
|
110
|
+
text = Path(filepath).read_text(encoding="utf-8")
|
|
111
|
+
except FileNotFoundError:
|
|
112
|
+
typer.echo(f"error: {filepath}: file not found", err=True)
|
|
113
|
+
return 1
|
|
114
|
+
except PermissionError:
|
|
115
|
+
typer.echo(f"error: {filepath}: permission denied", err=True)
|
|
116
|
+
return 1
|
|
117
|
+
|
|
118
|
+
merged = _load_and_warn(text)
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
result = _format_with_config(text, merged)
|
|
122
|
+
except tomllib.TOMLDecodeError as exc:
|
|
123
|
+
typer.echo(f"error: {filepath}: {exc}", err=True)
|
|
124
|
+
return 1
|
|
125
|
+
|
|
126
|
+
if text == result:
|
|
127
|
+
return 0
|
|
128
|
+
|
|
129
|
+
# File needs changes
|
|
130
|
+
if check and diff:
|
|
131
|
+
_print_diff(text, result, filepath)
|
|
132
|
+
return 1
|
|
133
|
+
if check:
|
|
134
|
+
typer.echo(f"error: {filepath}: not properly formatted", err=True)
|
|
135
|
+
return 1
|
|
136
|
+
if diff:
|
|
137
|
+
_print_diff(text, result, filepath)
|
|
138
|
+
return 0
|
|
139
|
+
|
|
140
|
+
# Fix mode: write back
|
|
141
|
+
Path(filepath).write_text(result, encoding="utf-8")
|
|
142
|
+
typer.echo(f"{filepath}: reformatted", err=True)
|
|
143
|
+
return 0
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@app.command()
|
|
147
|
+
def main(
|
|
148
|
+
files: Annotated[
|
|
149
|
+
list[str] | None,
|
|
150
|
+
typer.Argument(help="pyproject.toml files to format"),
|
|
151
|
+
] = None,
|
|
152
|
+
check: Annotated[
|
|
153
|
+
bool,
|
|
154
|
+
typer.Option(
|
|
155
|
+
"--check", help="Check if files are formatted, exit non-zero if not"
|
|
156
|
+
),
|
|
157
|
+
] = False,
|
|
158
|
+
diff: Annotated[
|
|
159
|
+
bool,
|
|
160
|
+
typer.Option("--diff", help="Show unified diff of changes"),
|
|
161
|
+
] = False,
|
|
162
|
+
version: Annotated[ # noqa: ARG001
|
|
163
|
+
bool | None,
|
|
164
|
+
typer.Option(
|
|
165
|
+
"--version",
|
|
166
|
+
"-v",
|
|
167
|
+
callback=_version_callback,
|
|
168
|
+
is_eager=True,
|
|
169
|
+
),
|
|
170
|
+
] = None,
|
|
171
|
+
) -> None:
|
|
172
|
+
"""Sort and format pyproject.toml files."""
|
|
173
|
+
if not files:
|
|
174
|
+
# Stdin mode
|
|
175
|
+
text = sys.stdin.read()
|
|
176
|
+
merged = _load_and_warn(text)
|
|
177
|
+
try:
|
|
178
|
+
result = _format_with_config(text, merged)
|
|
179
|
+
except tomllib.TOMLDecodeError as exc:
|
|
180
|
+
typer.echo(f"error: stdin: {exc}", err=True)
|
|
181
|
+
raise typer.Exit(code=1) from None
|
|
182
|
+
|
|
183
|
+
if check and diff:
|
|
184
|
+
if text != result:
|
|
185
|
+
_print_diff(text, result, "stdin")
|
|
186
|
+
raise typer.Exit(code=0 if text == result else 1)
|
|
187
|
+
if check:
|
|
188
|
+
raise typer.Exit(code=0 if text == result else 1)
|
|
189
|
+
if diff:
|
|
190
|
+
if text != result:
|
|
191
|
+
_print_diff(text, result, "stdin")
|
|
192
|
+
raise typer.Exit()
|
|
193
|
+
# Fix mode: write formatted output to stdout
|
|
194
|
+
typer.echo(result, nl=False)
|
|
195
|
+
else:
|
|
196
|
+
# File mode
|
|
197
|
+
exit_code = 0
|
|
198
|
+
for filepath in files:
|
|
199
|
+
code = _process_file(filepath, check=check, diff=diff)
|
|
200
|
+
exit_code = max(exit_code, code)
|
|
201
|
+
raise typer.Exit(code=exit_code)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
if __name__ == "__main__":
|
|
205
|
+
app()
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
"""Sort and format configuration for pypfmt.
|
|
2
|
+
|
|
3
|
+
Hardcoded defaults plus optional user overrides from [tool.pypfmt].
|
|
4
|
+
Override pattern modeled on ruff: extend-* adds to defaults, plain key replaces.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import dataclasses
|
|
10
|
+
import os
|
|
11
|
+
import tomllib
|
|
12
|
+
|
|
13
|
+
from toml_sort.tomlsort import (
|
|
14
|
+
CommentConfiguration,
|
|
15
|
+
FormattingConfiguration,
|
|
16
|
+
SortConfiguration,
|
|
17
|
+
SortOverrideConfiguration,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
# Maps [tool.pypfmt] TOML keys to the config field they control.
|
|
21
|
+
# Serves as documentation and reference for error messages.
|
|
22
|
+
SORT_KEY_MAP: dict[str, str] = {
|
|
23
|
+
"sort-first": "first (replace)",
|
|
24
|
+
"extend-sort-first": "first (extend)",
|
|
25
|
+
"sort-tables": "tables",
|
|
26
|
+
"sort-table-keys": "table_keys",
|
|
27
|
+
"sort-inline-tables": "inline_tables",
|
|
28
|
+
"sort-inline-arrays": "inline_arrays",
|
|
29
|
+
"ignore-case": "ignore_case",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
COMMENT_KEY_MAP: dict[str, str] = {
|
|
33
|
+
"comments-header": "header",
|
|
34
|
+
"comments-footer": "footer",
|
|
35
|
+
"comments-inline": "inline",
|
|
36
|
+
"comments-block": "block",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
FORMAT_KEY_MAP: dict[str, str] = {
|
|
40
|
+
"spaces-before-inline-comment": "spaces_before_inline_comment",
|
|
41
|
+
"spaces-indent-inline-array": "spaces_indent_inline_array",
|
|
42
|
+
"trailing-comma-inline-array": "trailing_comma_inline_array",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def get_sort_config() -> SortConfiguration:
|
|
47
|
+
"""Return the global sort configuration with locked defaults.
|
|
48
|
+
|
|
49
|
+
Global inline_arrays=False preserves array element order by default.
|
|
50
|
+
Only arrays with explicit inline_arrays=True overrides are sorted
|
|
51
|
+
alphabetically (classifiers, extend-select, ignore, dependency-groups).
|
|
52
|
+
|
|
53
|
+
Global table_keys=False preserves key order within tables by default.
|
|
54
|
+
Tables needing first-list ordering (project, build-system) must have
|
|
55
|
+
table_keys=True on their override for the first list to take effect.
|
|
56
|
+
|
|
57
|
+
The root first list controls only ROOT-level table ordering.
|
|
58
|
+
Sub-table ordering (e.g., tool.ruff vs tool.pytest) is controlled
|
|
59
|
+
by the first list on the PARENT table's override (e.g., "tool" override).
|
|
60
|
+
This mirrors how toml-sort CLI's parse_sort_first decomposes dotted keys.
|
|
61
|
+
"""
|
|
62
|
+
return SortConfiguration(
|
|
63
|
+
tables=True,
|
|
64
|
+
table_keys=False,
|
|
65
|
+
inline_tables=False,
|
|
66
|
+
inline_arrays=False,
|
|
67
|
+
ignore_case=False,
|
|
68
|
+
first=[
|
|
69
|
+
"build-system",
|
|
70
|
+
"project",
|
|
71
|
+
"dependency-groups",
|
|
72
|
+
],
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def get_sort_overrides() -> dict[str, SortOverrideConfiguration]:
|
|
77
|
+
"""Return per-table sort override configurations.
|
|
78
|
+
|
|
79
|
+
Global defaults: inline_arrays=False (preserve order), table_keys=False
|
|
80
|
+
(preserve key order). Overrides selectively enable sorting where needed.
|
|
81
|
+
|
|
82
|
+
The first list on parent-table overrides controls sub-table ordering.
|
|
83
|
+
This mirrors how toml-sort CLI's parse_sort_first decomposes dotted
|
|
84
|
+
keys: "tool.ruff" becomes first=["ruff"] on the "tool" override.
|
|
85
|
+
|
|
86
|
+
- table_keys=True on build-system/project enables first-list key ordering
|
|
87
|
+
- first list on "tool" override controls tool sub-table ordering
|
|
88
|
+
- first=["*"] on sub-tool overrides ensures sub-sub-tables sort correctly
|
|
89
|
+
- inline_arrays=True on specific array paths enables alphabetical sorting
|
|
90
|
+
- tool.tomlsort overrides explicitly preserve its own config section
|
|
91
|
+
"""
|
|
92
|
+
return {
|
|
93
|
+
# Tables needing first-list key ordering (table_keys=True required
|
|
94
|
+
# for the first list to work with global table_keys=False)
|
|
95
|
+
"build-system": SortOverrideConfiguration(
|
|
96
|
+
table_keys=True,
|
|
97
|
+
first=["requires", "build-backend"],
|
|
98
|
+
),
|
|
99
|
+
"project": SortOverrideConfiguration(
|
|
100
|
+
table_keys=True,
|
|
101
|
+
first=[
|
|
102
|
+
"name",
|
|
103
|
+
"dynamic",
|
|
104
|
+
"description",
|
|
105
|
+
"readme",
|
|
106
|
+
"authors",
|
|
107
|
+
"maintainers",
|
|
108
|
+
"license",
|
|
109
|
+
"classifiers",
|
|
110
|
+
"keywords",
|
|
111
|
+
"requires-python",
|
|
112
|
+
"dependencies",
|
|
113
|
+
"*",
|
|
114
|
+
],
|
|
115
|
+
),
|
|
116
|
+
# Tool sub-table ordering (mirrors parse_sort_first decomposition)
|
|
117
|
+
"tool": SortOverrideConfiguration(
|
|
118
|
+
first=[
|
|
119
|
+
"git-cliff",
|
|
120
|
+
"pypis_delivery_service",
|
|
121
|
+
"ty",
|
|
122
|
+
"uv",
|
|
123
|
+
"ruff",
|
|
124
|
+
"mypy",
|
|
125
|
+
"pyright",
|
|
126
|
+
"basedpyright",
|
|
127
|
+
"pylint",
|
|
128
|
+
"isort",
|
|
129
|
+
"black",
|
|
130
|
+
"pytest",
|
|
131
|
+
"coverage",
|
|
132
|
+
"semantic_release",
|
|
133
|
+
"hatch",
|
|
134
|
+
"*",
|
|
135
|
+
"tomlsort",
|
|
136
|
+
],
|
|
137
|
+
),
|
|
138
|
+
# Sub-tool table ordering matching golden file specification.
|
|
139
|
+
# toml-sort sorts sub-tables alphabetically by default (tables=True).
|
|
140
|
+
# These first lists override to match the golden file order.
|
|
141
|
+
"tool.ruff.lint": SortOverrideConfiguration(
|
|
142
|
+
first=["per-file-ignores", "pycodestyle", "pydocstyle", "mccabe"],
|
|
143
|
+
),
|
|
144
|
+
"tool.coverage": SortOverrideConfiguration(
|
|
145
|
+
first=["run", "report"],
|
|
146
|
+
),
|
|
147
|
+
"tool.hatch": SortOverrideConfiguration(
|
|
148
|
+
first=["version", "build"],
|
|
149
|
+
),
|
|
150
|
+
# Arrays that SHOULD be sorted alphabetically
|
|
151
|
+
"project.classifiers": SortOverrideConfiguration(inline_arrays=True),
|
|
152
|
+
"tool.ruff.lint.extend-select": SortOverrideConfiguration(inline_arrays=True),
|
|
153
|
+
"tool.ruff.lint.ignore": SortOverrideConfiguration(inline_arrays=True),
|
|
154
|
+
# Dependency groups: sort array elements alphabetically
|
|
155
|
+
"dependency-groups.*": SortOverrideConfiguration(inline_arrays=True),
|
|
156
|
+
# tomlsort section: preserve as-is (no sorting)
|
|
157
|
+
"tool.tomlsort": SortOverrideConfiguration(
|
|
158
|
+
table_keys=False,
|
|
159
|
+
inline_arrays=False,
|
|
160
|
+
first=["*"],
|
|
161
|
+
),
|
|
162
|
+
"tool.tomlsort.*": SortOverrideConfiguration(
|
|
163
|
+
table_keys=False,
|
|
164
|
+
inline_arrays=False,
|
|
165
|
+
),
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def get_comment_config() -> CommentConfiguration:
|
|
170
|
+
"""Return comment preservation configuration."""
|
|
171
|
+
return CommentConfiguration(
|
|
172
|
+
header=True,
|
|
173
|
+
footer=True,
|
|
174
|
+
inline=True,
|
|
175
|
+
block=True,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def get_format_config() -> FormattingConfiguration:
|
|
180
|
+
"""Return formatting configuration aligned with taplo options.
|
|
181
|
+
|
|
182
|
+
spaces_indent_inline_array=4 matches taplo indent_string (4 spaces).
|
|
183
|
+
trailing_comma_inline_array=True matches taplo array_trailing_comma=true.
|
|
184
|
+
"""
|
|
185
|
+
return FormattingConfiguration(
|
|
186
|
+
spaces_before_inline_comment=2,
|
|
187
|
+
spaces_indent_inline_array=4,
|
|
188
|
+
trailing_comma_inline_array=True,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
TAPLO_OPTIONS: tuple[str, ...] = (
|
|
193
|
+
"reorder_keys=false",
|
|
194
|
+
"indent_string= ",
|
|
195
|
+
"array_auto_collapse=false",
|
|
196
|
+
"array_auto_expand=true",
|
|
197
|
+
"array_trailing_comma=true",
|
|
198
|
+
"align_comments=true",
|
|
199
|
+
"column_width=80",
|
|
200
|
+
"allowed_blank_lines=2",
|
|
201
|
+
)
|
|
202
|
+
"""taplo CLI -o key=value pairs for formatting."""
|
|
203
|
+
|
|
204
|
+
_CONFLICT_WARNING = (
|
|
205
|
+
"warning: [tool.tomlsort] and [tool.pypfmt] both present. "
|
|
206
|
+
"toml-sort should not be used against pyproject.toml files when also "
|
|
207
|
+
"using pypfmt, since results and ordering will be outside of "
|
|
208
|
+
"pypfmt's control."
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def load_config(text: str) -> dict | None:
|
|
213
|
+
"""Extract ``[tool.pypfmt]`` from TOML text.
|
|
214
|
+
|
|
215
|
+
Pure extraction -- no merging logic. Returns the raw dict when the
|
|
216
|
+
section exists, ``None`` when it doesn't.
|
|
217
|
+
"""
|
|
218
|
+
data = tomllib.loads(text)
|
|
219
|
+
return data.get("tool", {}).get("pypfmt", None)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def check_config_conflict(text: str) -> str | None:
|
|
223
|
+
"""Return a warning string if both tomlsort and pypfmt config exist.
|
|
224
|
+
|
|
225
|
+
Returns ``None`` when there is no conflict or when the warning is
|
|
226
|
+
suppressed via the ``PPF_HIDE_CONFLICT_WARNING`` environment variable.
|
|
227
|
+
"""
|
|
228
|
+
data = tomllib.loads(text)
|
|
229
|
+
tool = data.get("tool", {})
|
|
230
|
+
if (
|
|
231
|
+
"tomlsort" in tool
|
|
232
|
+
and "pypfmt" in tool
|
|
233
|
+
and not os.environ.get("PPF_HIDE_CONFLICT_WARNING")
|
|
234
|
+
):
|
|
235
|
+
return _CONFLICT_WARNING
|
|
236
|
+
return None
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _merge_sort_config(default: SortConfiguration, user: dict) -> SortConfiguration:
|
|
240
|
+
"""Apply user overrides to the default SortConfiguration."""
|
|
241
|
+
replacements: dict[str, object] = {}
|
|
242
|
+
if "sort-first" in user:
|
|
243
|
+
replacements["first"] = list(user["sort-first"])
|
|
244
|
+
elif "extend-sort-first" in user:
|
|
245
|
+
replacements["first"] = list(default.first) + list(user["extend-sort-first"])
|
|
246
|
+
if "sort-tables" in user:
|
|
247
|
+
replacements["tables"] = user["sort-tables"]
|
|
248
|
+
if "sort-table-keys" in user:
|
|
249
|
+
replacements["table_keys"] = user["sort-table-keys"]
|
|
250
|
+
if "sort-inline-tables" in user:
|
|
251
|
+
replacements["inline_tables"] = user["sort-inline-tables"]
|
|
252
|
+
if "sort-inline-arrays" in user:
|
|
253
|
+
replacements["inline_arrays"] = user["sort-inline-arrays"]
|
|
254
|
+
if "ignore-case" in user:
|
|
255
|
+
replacements["ignore_case"] = user["ignore-case"]
|
|
256
|
+
return dataclasses.replace(default, **replacements) if replacements else default
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _merge_sort_overrides(
|
|
260
|
+
default: dict[str, SortOverrideConfiguration], user: dict
|
|
261
|
+
) -> dict[str, SortOverrideConfiguration]:
|
|
262
|
+
"""Apply user overrides to the per-table sort overrides."""
|
|
263
|
+
if "overrides" in user:
|
|
264
|
+
# Replace: start fresh from user dict only
|
|
265
|
+
return {
|
|
266
|
+
path: SortOverrideConfiguration(**cfg)
|
|
267
|
+
for path, cfg in user["overrides"].items()
|
|
268
|
+
}
|
|
269
|
+
if "extend-overrides" in user:
|
|
270
|
+
# Extend: copy defaults, then update with user entries
|
|
271
|
+
merged = dict(default)
|
|
272
|
+
for path, cfg in user["extend-overrides"].items():
|
|
273
|
+
merged[path] = SortOverrideConfiguration(**cfg)
|
|
274
|
+
return merged
|
|
275
|
+
return default
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _merge_comment_config(
|
|
279
|
+
default: CommentConfiguration, user: dict
|
|
280
|
+
) -> CommentConfiguration:
|
|
281
|
+
"""Apply user overrides to CommentConfiguration."""
|
|
282
|
+
replacements: dict[str, object] = {}
|
|
283
|
+
for toml_key, field_name in COMMENT_KEY_MAP.items():
|
|
284
|
+
if toml_key in user:
|
|
285
|
+
replacements[field_name] = user[toml_key]
|
|
286
|
+
return dataclasses.replace(default, **replacements) if replacements else default
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _merge_format_config(
|
|
290
|
+
default: FormattingConfiguration, user: dict
|
|
291
|
+
) -> FormattingConfiguration:
|
|
292
|
+
"""Apply user overrides to FormattingConfiguration."""
|
|
293
|
+
replacements: dict[str, object] = {}
|
|
294
|
+
for toml_key, field_name in FORMAT_KEY_MAP.items():
|
|
295
|
+
if toml_key in user:
|
|
296
|
+
replacements[field_name] = user[toml_key]
|
|
297
|
+
return dataclasses.replace(default, **replacements) if replacements else default
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _merge_taplo_options(default: tuple[str, ...], user: dict) -> tuple[str, ...]:
|
|
301
|
+
"""Apply user overrides to taplo options."""
|
|
302
|
+
if "taplo-options" in user:
|
|
303
|
+
return tuple(user["taplo-options"])
|
|
304
|
+
if "extend-taplo-options" in user:
|
|
305
|
+
return default + tuple(user["extend-taplo-options"])
|
|
306
|
+
return default
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
MergedConfig = tuple[
|
|
310
|
+
SortConfiguration,
|
|
311
|
+
dict[str, SortOverrideConfiguration],
|
|
312
|
+
CommentConfiguration,
|
|
313
|
+
FormattingConfiguration,
|
|
314
|
+
tuple[str, ...],
|
|
315
|
+
]
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def merge_config(user: dict) -> MergedConfig:
|
|
319
|
+
"""Merge user overrides with hardcoded defaults.
|
|
320
|
+
|
|
321
|
+
Takes the raw dict from ``load_config()`` and returns a 5-tuple of
|
|
322
|
+
merged config objects. Defaults are never mutated -- new instances
|
|
323
|
+
are created via ``dataclasses.replace()``.
|
|
324
|
+
"""
|
|
325
|
+
default_sort = get_sort_config()
|
|
326
|
+
default_overrides = get_sort_overrides()
|
|
327
|
+
default_comment = get_comment_config()
|
|
328
|
+
default_format = get_format_config()
|
|
329
|
+
|
|
330
|
+
return (
|
|
331
|
+
_merge_sort_config(default_sort, user),
|
|
332
|
+
_merge_sort_overrides(default_overrides, user),
|
|
333
|
+
_merge_comment_config(default_comment, user),
|
|
334
|
+
_merge_format_config(default_format, user),
|
|
335
|
+
_merge_taplo_options(TAPLO_OPTIONS, user),
|
|
336
|
+
)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""TOML formatting via taplo subprocess.
|
|
2
|
+
|
|
3
|
+
Wraps the taplo CLI binary to apply whitespace and style formatting.
|
|
4
|
+
This is the second stage of the pipeline: format after sorting.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import shutil
|
|
10
|
+
import subprocess
|
|
11
|
+
|
|
12
|
+
from pypfmt.config import TAPLO_OPTIONS
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def format_toml(
|
|
16
|
+
text: str,
|
|
17
|
+
taplo_options: tuple[str, ...] | None = None,
|
|
18
|
+
) -> str:
|
|
19
|
+
"""Format a TOML string using taplo subprocess.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
text: Valid TOML content as a string.
|
|
23
|
+
taplo_options: taplo -o key=value pairs, or None for defaults.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
The formatted TOML string with consistent whitespace,
|
|
27
|
+
indentation, and style.
|
|
28
|
+
|
|
29
|
+
Raises:
|
|
30
|
+
RuntimeError: If taplo binary is not found or formatting fails.
|
|
31
|
+
"""
|
|
32
|
+
options = taplo_options if taplo_options is not None else TAPLO_OPTIONS
|
|
33
|
+
|
|
34
|
+
taplo_bin = shutil.which("taplo")
|
|
35
|
+
if taplo_bin is None:
|
|
36
|
+
msg = "taplo binary not found. Install via: pip install taplo"
|
|
37
|
+
raise RuntimeError(msg)
|
|
38
|
+
|
|
39
|
+
cmd: list[str] = [taplo_bin, "format", "--no-auto-config"]
|
|
40
|
+
for option in options:
|
|
41
|
+
cmd.extend(["-o", option])
|
|
42
|
+
cmd.append("-")
|
|
43
|
+
|
|
44
|
+
result = subprocess.run(
|
|
45
|
+
cmd,
|
|
46
|
+
input=text,
|
|
47
|
+
capture_output=True,
|
|
48
|
+
text=True,
|
|
49
|
+
check=False,
|
|
50
|
+
)
|
|
51
|
+
if result.returncode != 0:
|
|
52
|
+
msg = f"taplo format failed: {result.stderr}"
|
|
53
|
+
raise RuntimeError(msg)
|
|
54
|
+
return result.stdout
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Pipeline orchestrator: validate -> sort -> format.
|
|
2
|
+
|
|
3
|
+
Chains TOML validation, sorting, and formatting into a single
|
|
4
|
+
str -> str transformation. This is the public API for pypfmt.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import tomllib
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
from pypfmt.formatter import format_toml
|
|
13
|
+
from pypfmt.sorter import sort_toml
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from toml_sort.tomlsort import (
|
|
17
|
+
CommentConfiguration,
|
|
18
|
+
FormattingConfiguration,
|
|
19
|
+
SortConfiguration,
|
|
20
|
+
SortOverrideConfiguration,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def format_pyproject(
|
|
25
|
+
text: str,
|
|
26
|
+
sort_config: SortConfiguration | None = None,
|
|
27
|
+
sort_overrides: dict[str, SortOverrideConfiguration] | None = None,
|
|
28
|
+
comment_config: CommentConfiguration | None = None,
|
|
29
|
+
format_config: FormattingConfiguration | None = None,
|
|
30
|
+
taplo_options: tuple[str, ...] | None = None,
|
|
31
|
+
) -> str:
|
|
32
|
+
"""Format a pyproject.toml string through the full pipeline.
|
|
33
|
+
|
|
34
|
+
Validates TOML syntax, sorts tables and keys via toml-sort,
|
|
35
|
+
then formats whitespace and style via taplo.
|
|
36
|
+
|
|
37
|
+
When config parameters are ``None``, hardcoded defaults are used.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
text: Raw pyproject.toml content as a string.
|
|
41
|
+
sort_config: Global sort configuration, or None for defaults.
|
|
42
|
+
sort_overrides: Per-table sort overrides, or None for defaults.
|
|
43
|
+
comment_config: Comment handling configuration, or None for defaults.
|
|
44
|
+
format_config: Formatting configuration, or None for defaults.
|
|
45
|
+
taplo_options: taplo -o key=value pairs, or None for defaults.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
The sorted and formatted TOML string.
|
|
49
|
+
|
|
50
|
+
Raises:
|
|
51
|
+
tomllib.TOMLDecodeError: If the input is not valid TOML.
|
|
52
|
+
RuntimeError: If taplo binary is not found or formatting fails.
|
|
53
|
+
"""
|
|
54
|
+
# Validate input -- let TOMLDecodeError propagate naturally
|
|
55
|
+
tomllib.loads(text)
|
|
56
|
+
|
|
57
|
+
# Stage 1: Sort tables and keys
|
|
58
|
+
sorted_text = sort_toml(
|
|
59
|
+
text,
|
|
60
|
+
sort_config=sort_config,
|
|
61
|
+
sort_overrides=sort_overrides,
|
|
62
|
+
comment_config=comment_config,
|
|
63
|
+
format_config=format_config,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Stage 2: Format whitespace and style
|
|
67
|
+
return format_toml(sorted_text, taplo_options=taplo_options)
|
|
File without changes
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""TOML sorting via toml-sort library API.
|
|
2
|
+
|
|
3
|
+
Wraps TomlSort with configuration to produce a sorted TOML string.
|
|
4
|
+
This is the first stage of the pipeline: sort tables and keys.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
from toml_sort import TomlSort
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from toml_sort.tomlsort import (
|
|
15
|
+
CommentConfiguration,
|
|
16
|
+
FormattingConfiguration,
|
|
17
|
+
SortConfiguration,
|
|
18
|
+
SortOverrideConfiguration,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
from pypfmt.config import (
|
|
22
|
+
get_comment_config,
|
|
23
|
+
get_format_config,
|
|
24
|
+
get_sort_config,
|
|
25
|
+
get_sort_overrides,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def sort_toml(
|
|
30
|
+
text: str,
|
|
31
|
+
sort_config: SortConfiguration | None = None,
|
|
32
|
+
sort_overrides: dict[str, SortOverrideConfiguration] | None = None,
|
|
33
|
+
comment_config: CommentConfiguration | None = None,
|
|
34
|
+
format_config: FormattingConfiguration | None = None,
|
|
35
|
+
) -> str:
|
|
36
|
+
"""Sort a TOML string using toml-sort.
|
|
37
|
+
|
|
38
|
+
When config parameters are ``None``, hardcoded defaults are used.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
text: Valid TOML content as a string.
|
|
42
|
+
sort_config: Global sort configuration, or None for defaults.
|
|
43
|
+
sort_overrides: Per-table sort overrides, or None for defaults.
|
|
44
|
+
comment_config: Comment handling configuration, or None for defaults.
|
|
45
|
+
format_config: Formatting configuration, or None for defaults.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
The sorted TOML string with tables and keys reordered
|
|
49
|
+
according to the configuration.
|
|
50
|
+
"""
|
|
51
|
+
sorter = TomlSort(
|
|
52
|
+
input_toml=text,
|
|
53
|
+
sort_config=sort_config or get_sort_config(),
|
|
54
|
+
comment_config=comment_config or get_comment_config(),
|
|
55
|
+
format_config=format_config or get_format_config(),
|
|
56
|
+
sort_config_overrides=sort_overrides or get_sort_overrides(),
|
|
57
|
+
)
|
|
58
|
+
return sorter.sorted()
|