├── .rustfmt.toml ├── src └── pyproject_fmt_rust │ ├── py.typed │ ├── __init__.py │ └── _lib.pyi ├── rust-toolchain.toml ├── .github ├── FUNDING.yml ├── release.yml ├── dependabot.yml ├── SECURITY.md └── workflows │ ├── check.yaml │ └── release.yaml ├── rust └── src │ ├── helpers │ ├── mod.rs │ ├── string.rs │ ├── create.rs │ ├── pep508.rs │ ├── array.rs │ └── table.rs │ ├── data │ ├── ruff-21.start.toml │ ├── ruff-21.expected.toml │ ├── ruff-order.start.toml │ └── ruff-order.expected.toml │ ├── build_system.rs │ ├── global.rs │ ├── ruff.rs │ ├── main.rs │ └── project.rs ├── .gitignore ├── .pre-commit-hooks.yaml ├── README.md ├── LICENSE.txt ├── Cargo.toml ├── .pre-commit-config.yaml ├── CONTRIBUTING.md ├── tox.ini ├── tests └── test_main.py ├── CODE_OF_CONDUCT.md ├── pyproject.toml └── Cargo.lock /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 120 2 | -------------------------------------------------------------------------------- /src/pyproject_fmt_rust/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.81" 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | tidelift: "pypi/pyproject-fmt-rust" 2 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | authors: 4 | - dependabot 5 | - pre-commit-ci 6 | -------------------------------------------------------------------------------- /rust/src/helpers/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod array; 2 | pub mod create; 3 | pub mod pep508; 4 | pub mod string; 5 | pub mod table; 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | -------------------------------------------------------------------------------- /src/pyproject_fmt_rust/__init__.py: -------------------------------------------------------------------------------- 1 | """Format your pyproject.toml.""" 2 | 3 | from __future__ import annotations 4 | 5 | from ._lib import Settings, format_toml 6 | 7 | __all__ = [ 8 | "Settings", 9 | "format_toml", 10 | ] 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info/ 2 | .tox/ 3 | .*_cache 4 | __pycache__ 5 | **.pyc 6 | dist 7 | 8 | /target 9 | /pyproject-*.toml 10 | /src/pyproject_fmt_rust/_lib.abi3* 11 | /tarpaulin-report.html 12 | /build_rs_cov.profraw 13 | /.cargo/config.toml 14 | -------------------------------------------------------------------------------- /.pre-commit-hooks.yaml: -------------------------------------------------------------------------------- 1 | - id: pyproject-fmt 2 | name: pyproject-fmt 3 | description: apply a consistent format to `pyproject.toml` files 4 | entry: pyproject-fmt 5 | language: python 6 | language_version: python3 7 | files: '(^|/)pyproject\.toml$' 8 | types: [file, text, toml] 9 | -------------------------------------------------------------------------------- /rust/src/data/ruff-21.start.toml: -------------------------------------------------------------------------------- 1 | [tool.ruff.lint] 2 | select = ["ALL"] 3 | 4 | ignore = [ 5 | # We do not annotate the type of 'self'. 6 | "ANN101", 7 | ] 8 | 9 | # Do not automatically remove commented out code. 10 | # We comment out code during development, and with VSCode auto-save, this code 11 | # is sometimes annoyingly removed. 12 | unfixable = ["ERA001"] 13 | -------------------------------------------------------------------------------- /rust/src/data/ruff-21.expected.toml: -------------------------------------------------------------------------------- 1 | [tool.ruff] 2 | lint.select = [ 3 | "ALL", 4 | ] 5 | lint.ignore = [ 6 | # We do not annotate the type of 'self'. 7 | "ANN101", 8 | ] 9 | # Do not automatically remove commented out code. 10 | # We comment out code during development, and with VSCode auto-save, this code 11 | # is sometimes annoyingly removed. 12 | lint.unfixable = [ 13 | "ERA001", 14 | ] 15 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ------- | ------------------ | 7 | | 1.0 + | :white_check_mark: | 8 | | < 1.0 | :x: | 9 | 10 | ## Reporting a Vulnerability 11 | 12 | To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift 13 | will coordinate the fix and disclosure. 14 | -------------------------------------------------------------------------------- /src/pyproject_fmt_rust/_lib.pyi: -------------------------------------------------------------------------------- 1 | class Settings: 2 | def __init__( 3 | self, 4 | *, 5 | column_width: int, 6 | indent: int, 7 | keep_full_version: bool, 8 | max_supported_python: tuple[int, int], 9 | min_supported_python: tuple[int, int], 10 | ) -> None: ... 11 | @property 12 | def column_width(self) -> int: ... 13 | @property 14 | def indent(self) -> int: ... 15 | @property 16 | def keep_full_version(self) -> bool: ... 17 | @property 18 | def max_supported_python(self) -> tuple[int, int]: ... 19 | @property 20 | def min_supported_python(self) -> tuple[int, int]: ... 21 | 22 | def format_toml(content: str, settings: Settings) -> str: ... 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pyproject-fmt-rust 2 | 3 | Migrated into the [toml-fmt](https://github.com/gaborbernat/toml-fmt) repository. 4 | 5 | [![PyPI](https://img.shields.io/pypi/v/pyproject-fmt-rust?style=flat-square)](https://pypi.org/project/pyproject-fmt-rust) 6 | [![PyPI - Implementation](https://img.shields.io/pypi/implementation/pyproject-fmt-rust?style=flat-square)](https://pypi.org/project/pyproject-fmt-rust) 7 | [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pyproject-fmt-rust?style=flat-square)](https://pypi.org/project/pyproject-fmt-rust) 8 | [![Downloads](https://static.pepy.tech/badge/pyproject-fmt-rust/month)](https://pepy.tech/project/pyproject-fmt-rust) 9 | [![PyPI - License](https://img.shields.io/pypi/l/pyproject-fmt-rust?style=flat-square)](https://opensource.org/licenses/MIT) 10 | [![check](https://github.com/tox-dev/pyproject-fmt-rust/actions/workflows/check.yaml/badge.svg)](https://github.com/tox-dev/pyproject-fmt-rust/actions/workflows/check.yaml) 11 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any person obtaining a 2 | copy of this software and associated documentation files (the 3 | "Software"), to deal in the Software without restriction, including 4 | without limitation the rights to use, copy, modify, merge, publish, 5 | distribute, sublicense, and/or sell copies of the Software, and to 6 | permit persons to whom the Software is furnished to do so, subject to 7 | the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included 10 | in all copies or substantial portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 13 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 14 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 15 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 16 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 17 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 18 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pyproject-fmt-rust" 3 | version = "1.2.1" 4 | description = "Format pyproject.toml files" 5 | repository = "https://github.com/tox-dev/pyproject-fmt" 6 | readme = "README.md" 7 | license = "MIT" 8 | edition = "2021" 9 | 10 | [lib] 11 | name = "_lib" 12 | path = "rust/src/main.rs" 13 | crate-type = ["cdylib"] 14 | 15 | [dependencies] 16 | taplo = { version = "0.13.0" } # formatter 17 | pyo3 = { version = "0.21.2", features = ["abi3-py38"] } # integration with Python 18 | pep440_rs = { version = "0.6.0" } 19 | pep508_rs = { version = "0.6.0" } 20 | lexical-sort = { version = "0.3.1" } 21 | regex = { version = "1.10.4" } 22 | 23 | [features] 24 | extension-module = ["pyo3/extension-module"] 25 | default = ["extension-module"] 26 | 27 | [lints.clippy] 28 | all = "warn" 29 | pedantic = "warn" 30 | nursery = "warn" 31 | 32 | [lints.rust] 33 | unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tarpaulin_include)'] } 34 | 35 | [dev-dependencies] 36 | rstest = { version = "0.19.0" } # parametrized tests 37 | indoc = { version = "2.0.5" } # dedented test cases for literal strings 38 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v5.0.0 4 | hooks: 5 | - id: end-of-file-fixer 6 | - id: trailing-whitespace 7 | - repo: https://github.com/python-jsonschema/check-jsonschema 8 | rev: 0.29.4 9 | hooks: 10 | - id: check-github-workflows 11 | args: ["--verbose"] 12 | - repo: https://github.com/codespell-project/codespell 13 | rev: v2.3.0 14 | hooks: 15 | - id: codespell 16 | additional_dependencies: ["tomli>=2.0.1"] 17 | - repo: https://github.com/tox-dev/tox-ini-fmt 18 | rev: "1.4.1" 19 | hooks: 20 | - id: tox-ini-fmt 21 | args: ["-p", "fix"] 22 | - repo: https://github.com/tox-dev/pyproject-fmt 23 | rev: "2.3.0" 24 | hooks: 25 | - id: pyproject-fmt 26 | - repo: https://github.com/astral-sh/ruff-pre-commit 27 | rev: "v0.6.9" 28 | hooks: 29 | - id: ruff-format 30 | - id: ruff 31 | args: ["--fix", "--unsafe-fixes", "--exit-non-zero-on-fix"] 32 | - repo: https://github.com/rbubley/mirrors-prettier 33 | rev: "v3.3.3" 34 | hooks: 35 | - id: prettier 36 | additional_dependencies: 37 | - prettier@3.3.3 38 | - "@prettier/plugin-xml@3.4.1" 39 | - repo: meta 40 | hooks: 41 | - id: check-hooks-apply 42 | - id: check-useless-excludes 43 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to pyproject-fmt-rust 2 | 3 | Thank you for your interest in contributing to pyproject-fmt-rust! There are 4 | many ways to contribute, and we appreciate all of them. As a reminder, all 5 | contributors are expected to follow our [Code of Conduct](CODE_OF_CONDUCT.md). 6 | 7 | ## Development Setup 8 | 9 | ### Building the Project 10 | 11 | To work on the project: 12 | 13 | 1. Install Rust (preferably through [rustup](https://rustup.rs)). 14 | 2. Clone the repository. 15 | 3. Build the project and run the unit tests: 16 | ```bash 17 | cargo test 18 | ``` 19 | 20 | ## License 21 | 22 | By contributing to pyproject-rust-format, you agree that your contributions 23 | will be licensed under the [MIT License](LICENSE). 24 | 25 | Thank you for your contributions! If you have any questions or need further 26 | assistance, feel free to reach out via GitHub issues. 27 | 28 | ## Tips 29 | 30 | ### Always recompiling PyO3 31 | 32 | If you find PyO3 constantly recompiling (such as if you are running 33 | rust-analyser in your IDE and cargo test in a terminal), the problem is that 34 | PyO3 has a `build.rs` that looks for Python, and it will recompile if it is run 35 | with a different PATH. To fix it, put the following in `.cargo/config.toml`: 36 | 37 | ```toml 38 | [env] 39 | PYO3_PYTHON = "./.venv/bin/python" 40 | ``` 41 | 42 | And make sure you have a `.venv` folder with Python in it. This will ensure all 43 | runs use the same Python and don't reconfigure. 44 | -------------------------------------------------------------------------------- /rust/src/helpers/string.rs: -------------------------------------------------------------------------------- 1 | use taplo::syntax::SyntaxKind::{IDENT, MULTI_LINE_STRING, MULTI_LINE_STRING_LITERAL, STRING, STRING_LITERAL}; 2 | use taplo::syntax::{SyntaxElement, SyntaxKind, SyntaxNode}; 3 | 4 | use crate::helpers::create::make_string_node; 5 | 6 | pub fn load_text(value: &str, kind: SyntaxKind) -> String { 7 | let mut chars = value.chars(); 8 | let offset = if [STRING, STRING_LITERAL].contains(&kind) { 9 | 1 10 | } else if kind == IDENT { 11 | 0 12 | } else { 13 | 3 14 | }; 15 | for _ in 0..offset { 16 | chars.next(); 17 | } 18 | for _ in 0..offset { 19 | chars.next_back(); 20 | } 21 | let mut res = chars.as_str().to_string(); 22 | if kind == STRING { 23 | res = res.replace("\\\"", "\""); 24 | } 25 | res 26 | } 27 | 28 | pub fn update_content(entry: &SyntaxNode, transform: F) 29 | where 30 | F: Fn(&str) -> String, 31 | { 32 | let (mut to_insert, mut count) = (Vec::::new(), 0); 33 | let mut changed = false; 34 | for mut child in entry.children_with_tokens() { 35 | count += 1; 36 | let kind = child.kind(); 37 | if [STRING, STRING_LITERAL, MULTI_LINE_STRING, MULTI_LINE_STRING_LITERAL].contains(&kind) { 38 | let found_str_value = load_text(child.as_token().unwrap().text(), kind); 39 | let output = transform(found_str_value.as_str()); 40 | 41 | changed = output != found_str_value || kind != STRING; 42 | if changed { 43 | child = make_string_node(output.as_str()); 44 | } 45 | } 46 | to_insert.push(child); 47 | } 48 | if changed { 49 | entry.splice_children(0..count, to_insert); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | requires = 3 | tox>=4.2 4 | tox-uv>=1.11.3 5 | env_list = 6 | fix 7 | 3.13 8 | 3.12 9 | 3.11 10 | 3.10 11 | 3.9 12 | type 13 | pkg_meta 14 | 15 | [testenv] 16 | description = run the unit tests with pytest under {base_python} 17 | package = wheel 18 | wheel_build_env = .pkg 19 | extras = 20 | graphviz 21 | test 22 | pass_env = 23 | PYTEST_* 24 | set_env = 25 | COVERAGE_FILE = {work_dir}/.coverage.{env_name} 26 | commands = 27 | python -m pytest {tty:--color=yes} {posargs: \ 28 | --cov {env_site_packages_dir}{/}pyproject_fmt_rust --cov {tox_root}{/}tests \ 29 | --cov-config=pyproject.toml --no-cov-on-fail --cov-report term-missing:skip-covered --cov-context=test \ 30 | --cov-report html:{env_tmp_dir}{/}htmlcov --cov-report xml:{work_dir}{/}coverage.{env_name}.xml \ 31 | --junitxml {work_dir}{/}junit.{env_name}.xml \ 32 | tests} 33 | 34 | [testenv:fix] 35 | description = format the code base to adhere to our styles, and complain about what we cannot do automatically 36 | skip_install = true 37 | deps = 38 | pre-commit-uv>=4.1 39 | commands = 40 | pre-commit run --all-files --show-diff-on-failure 41 | 42 | [testenv:type] 43 | description = run type check on code base 44 | deps = 45 | mypy==1.11.2 46 | commands = 47 | mypy src 48 | mypy tests 49 | 50 | [testenv:pkg_meta] 51 | description = check that the long description is valid 52 | skip_install = true 53 | deps = 54 | check-wheel-contents>=0.6 55 | twine>=5.1.1 56 | uv>=0.4.10 57 | commands = 58 | uv build --sdist --wheel --out-dir {env_tmp_dir} . 59 | twine check {env_tmp_dir}{/}* 60 | check-wheel-contents --no-config {env_tmp_dir} 61 | 62 | [testenv:dev] 63 | description = generate a DEV environment 64 | package = editable 65 | commands = 66 | uv pip tree 67 | python -c 'import sys; print(sys.executable)' 68 | -------------------------------------------------------------------------------- /tests/test_main.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from textwrap import dedent 4 | 5 | import pytest 6 | 7 | from pyproject_fmt_rust import Settings, format_toml 8 | 9 | 10 | @pytest.mark.parametrize( 11 | ("start", "expected"), 12 | [ 13 | pytest.param( 14 | """ 15 | [project] 16 | keywords = [ 17 | "A", 18 | ] 19 | classifiers = [ 20 | "Programming Language :: Python :: 3 :: Only", 21 | ] 22 | dynamic = [ 23 | "B", 24 | ] 25 | dependencies = [ 26 | "requests>=2.0", 27 | ] 28 | """, 29 | """\ 30 | [project] 31 | keywords = [ 32 | "A", 33 | ] 34 | classifiers = [ 35 | "Programming Language :: Python :: 3 :: Only", 36 | "Programming Language :: Python :: 3.7", 37 | "Programming Language :: Python :: 3.8", 38 | ] 39 | dynamic = [ 40 | "B", 41 | ] 42 | dependencies = [ 43 | "requests>=2.0", 44 | ] 45 | """, 46 | id="expanded", 47 | ), 48 | pytest.param( 49 | """ 50 | [project] 51 | keywords = ["A"] 52 | classifiers = ["Programming Language :: Python :: 3 :: Only"] 53 | dynamic = ["B"] 54 | dependencies = ["requests>=2.0"] 55 | """, 56 | """\ 57 | [project] 58 | keywords = [ "A" ] 59 | classifiers = [ 60 | "Programming Language :: Python :: 3 :: Only", 61 | "Programming Language :: Python :: 3.7", 62 | "Programming Language :: Python :: 3.8", 63 | ] 64 | dynamic = [ "B" ] 65 | dependencies = [ "requests>=2.0" ] 66 | """, 67 | id="collapsed", 68 | ), 69 | ], 70 | ) 71 | def test_format_toml(start: str, expected: str) -> None: 72 | settings = Settings( 73 | column_width=120, 74 | indent=4, 75 | keep_full_version=True, 76 | min_supported_python=(3, 7), 77 | max_supported_python=(3, 8), 78 | ) 79 | res = format_toml(dedent(start), settings) 80 | assert res == dedent(expected) 81 | -------------------------------------------------------------------------------- /.github/workflows/check.yaml: -------------------------------------------------------------------------------- 1 | name: Check 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: ["main"] 6 | tags: ["*"] 7 | pull_request: 8 | concurrency: 9 | group: check-${{ github.ref }} 10 | cancel-in-progress: true 11 | jobs: 12 | test: 13 | name: test ${{ matrix.py }} ${{ matrix.os }} 14 | runs-on: ubuntu-latest 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | py: 19 | - "3.13" 20 | - "3.12" 21 | - "3.11" 22 | - "3.10" 23 | - "3.9" 24 | os: 25 | - ubuntu-latest 26 | - windows-latest 27 | - macos-latest 28 | steps: 29 | - uses: actions/checkout@v4 30 | with: 31 | fetch-depth: 0 32 | - name: Install the latest version of uv 33 | uses: astral-sh/setup-uv@v3 34 | with: 35 | enable-cache: true 36 | cache-dependency-glob: "pyproject.toml" 37 | - name: Install tox 38 | run: uv tool install --python-preference only-managed --python 3.13 tox --with tox-uv 39 | - name: Install Python 40 | if: matrix.py != '3.13' 41 | run: uv python install --python-preference only-managed ${{ matrix.env }} 42 | - uses: moonrepo/setup-rust@v1 43 | with: 44 | cache-base: main 45 | bins: cargo-tarpaulin 46 | env: 47 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 48 | - name: setup test suite 49 | run: tox run -vv --notest --skip-missing-interpreters false -e ${{ matrix.py }} 50 | - name: run test suite 51 | run: tox run --skip-pkg-install -e ${{ matrix.py }} 52 | env: 53 | PYTEST_ADDOPTS: "-vv --durations=20" 54 | 55 | check: 56 | name: tox env ${{ matrix.env }} 57 | runs-on: ubuntu-latest 58 | strategy: 59 | fail-fast: false 60 | matrix: 61 | env: 62 | - type 63 | - dev 64 | - pkg_meta 65 | steps: 66 | - uses: actions/checkout@v4 67 | with: 68 | fetch-depth: 0 69 | - name: Install the latest version of uv 70 | uses: astral-sh/setup-uv@v3 71 | with: 72 | enable-cache: true 73 | cache-dependency-glob: "pyproject.toml" 74 | github-token: ${{ secrets.GITHUB_TOKEN }} 75 | - name: Install tox 76 | run: uv tool install --python-preference only-managed --python 3.13 tox --with tox-uv 77 | - name: Setup test suite 78 | run: tox run -vv --notest --skip-missing-interpreters false -e ${{ matrix.env }} 79 | - name: Run test suite 80 | run: tox run --skip-pkg-install -e ${{ matrix.env }} 81 | env: 82 | PYTEST_ADDOPTS: "-vv --durations=20" 83 | 84 | rust-check: 85 | runs-on: ubuntu-latest 86 | steps: 87 | - uses: actions/checkout@v4 88 | - uses: actions-rust-lang/setup-rust-toolchain@v1 89 | - name: Rustfmt Check 90 | uses: actions-rust-lang/rustfmt@v1 91 | - name: Lint 92 | run: cargo clippy --all-targets -- -D warnings 93 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making 6 | participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, 7 | disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, 8 | religion, or sexual identity and orientation. 9 | 10 | ## Our Standards 11 | 12 | Examples of behavior that contributes to creating a positive environment include: 13 | 14 | - Using welcoming and inclusive language 15 | - Being respectful of differing viewpoints and experiences 16 | - Gracefully accepting constructive criticism 17 | - Focusing on what is best for the community 18 | - Showing empathy towards other community members 19 | 20 | Examples of unacceptable behavior by participants include: 21 | 22 | - The use of sexualized language or imagery and unwelcome sexual attention or advances 23 | - Trolling, insulting/derogatory comments, and personal or political attacks 24 | - Public or private harassment 25 | - Publishing others' private information, such as a physical or electronic address, without explicit permission 26 | - Other conduct which could reasonably be considered inappropriate in a professional setting 27 | 28 | ## Our Responsibilities 29 | 30 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take 31 | appropriate and fair corrective action in response to any instances of unacceptable behavior. 32 | 33 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, 34 | issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any 35 | contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 36 | 37 | ## Scope 38 | 39 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the 40 | project or its community. Examples of representing a project or community include using an official project e-mail 41 | address, posting via an official social media account, or acting as an appointed representative at an online or offline 42 | event. Representation of a project may be further defined and clarified by project maintainers. 43 | 44 | ## Enforcement 45 | 46 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at 47 | gaborbernat@python.org. The project team will review and investigate all complaints, and will respond in a way that it 48 | deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the 49 | reporter of an incident. Further details of specific enforcement policies may be posted separately. 50 | 51 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent 52 | repercussions as determined by other members of the project's leadership. 53 | 54 | ## Attribution 55 | 56 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at 57 | [https://www.contributor-covenant.org/version/1/4/code-of-conduct.html][version] 58 | 59 | [homepage]: https://www.contributor-covenant.org/ 60 | [version]: https://www.contributor-covenant.org/version/1/4/ 61 | -------------------------------------------------------------------------------- /rust/src/build_system.rs: -------------------------------------------------------------------------------- 1 | use crate::helpers::array::{sort, transform}; 2 | use crate::helpers::pep508::{format_requirement, get_canonic_requirement_name}; 3 | use crate::helpers::table::{for_entries, reorder_table_keys, Tables}; 4 | 5 | pub fn fix(tables: &Tables, keep_full_version: bool) { 6 | let table_element = tables.get("build-system"); 7 | if table_element.is_none() { 8 | return; 9 | } 10 | let table = &mut table_element.unwrap().first().unwrap().borrow_mut(); 11 | for_entries(table, &mut |key, entry| match key.as_str() { 12 | "requires" => { 13 | transform(entry, &|s| format_requirement(s, keep_full_version)); 14 | sort(entry, |e| get_canonic_requirement_name(e).to_lowercase()); 15 | } 16 | "backend-path" => { 17 | sort(entry, str::to_lowercase); 18 | } 19 | _ => {} 20 | }); 21 | reorder_table_keys(table, &["", "build-backend", "requires", "backend-path"]); 22 | } 23 | 24 | #[cfg(test)] 25 | mod tests { 26 | use indoc::indoc; 27 | use rstest::rstest; 28 | use taplo::formatter::{format_syntax, Options}; 29 | use taplo::parser::parse; 30 | use taplo::syntax::SyntaxElement; 31 | 32 | use crate::build_system::fix; 33 | use crate::helpers::table::Tables; 34 | 35 | fn evaluate(start: &str, keep_full_version: bool) -> String { 36 | let root_ast = parse(start).into_syntax().clone_for_update(); 37 | let count = root_ast.children_with_tokens().count(); 38 | let tables = Tables::from_ast(&root_ast); 39 | fix(&tables, keep_full_version); 40 | let entries = tables 41 | .table_set 42 | .iter() 43 | .flat_map(|e| e.borrow().clone()) 44 | .collect::>(); 45 | root_ast.splice_children(0..count, entries); 46 | let opt = Options { 47 | column_width: 1, 48 | ..Options::default() 49 | }; 50 | format_syntax(root_ast, opt) 51 | } 52 | 53 | #[rstest] 54 | #[case::no_build_system( 55 | indoc ! {r""}, 56 | "\n", 57 | false 58 | )] 59 | #[case::build_system_requires_no_keep( 60 | indoc ! {r#" 61 | [build-system] 62 | requires=["a>=1.0.0", "b.c>=1.5.0"] 63 | "#}, 64 | indoc ! {r#" 65 | [build-system] 66 | requires = [ 67 | "a>=1", 68 | "b-c>=1.5", 69 | ] 70 | "#}, 71 | false 72 | )] 73 | #[case::build_system_requires_keep( 74 | indoc ! {r#" 75 | [build-system] 76 | requires=["a>=1.0.0", "b.c>=1.5.0"] 77 | "#}, 78 | indoc ! {r#" 79 | [build-system] 80 | requires = [ 81 | "a>=1.0.0", 82 | "b-c>=1.5.0", 83 | ] 84 | "#}, 85 | true 86 | )] 87 | #[case::join( 88 | indoc ! {r#" 89 | [build-system] 90 | requires=["a"] 91 | [build-system] 92 | build-backend = "hatchling.build" 93 | [[build-system.a]] 94 | name = "Hammer" 95 | [[build-system.a]] # empty table within the array 96 | [[build-system.a]] 97 | name = "Nail" 98 | "#}, 99 | indoc ! {r#" 100 | [build-system] 101 | build-backend = "hatchling.build" 102 | requires = [ 103 | "a", 104 | ] 105 | [[build-system.a]] 106 | name = "Hammer" 107 | [[build-system.a]] # empty table within the array 108 | [[build-system.a]] 109 | name = "Nail" 110 | "#}, 111 | false 112 | )] 113 | fn test_format_build_systems(#[case] start: &str, #[case] expected: &str, #[case] keep_full_version: bool) { 114 | assert_eq!(evaluate(start, keep_full_version), expected); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /rust/src/data/ruff-order.start.toml: -------------------------------------------------------------------------------- 1 | [tool.ruff] 2 | builtins = ['Bar', 'ALPHA'] 3 | cache-dir = '~/a' 4 | exclude = ['Bar', 'ALPHA'] 5 | extend = '../pyproject.toml' 6 | extend-exclude = ['Bar', 'ALPHA'] 7 | extend-include = ['Bar', 'ALPHA'] 8 | fix = true 9 | fix-only = true 10 | force-exclude = true 11 | include = ['Bar', 'ALPHA'] 12 | indent-width = 2 13 | line-length = 120 14 | namespace-packages = ['Bar', 'ALPHA'] 15 | output-format = 'grouped' 16 | preview = true 17 | required-version = '>=0.0.193' 18 | respect-gitignore = false 19 | show-fixes = true 20 | show-source = true 21 | src = ['Bar', 'ALPHA'] 22 | tab-size = 2 23 | target-version = 'py37' 24 | unsafe-fixes = true 25 | [tool.ruff.format] 26 | docstring-code-format = true 27 | docstring-code-line-length = 60 28 | exclude = ['Bar', 'ALPHA'] 29 | indent-style = 'tab' 30 | line-ending = 'lf' 31 | preview = true 32 | more = true 33 | extra = true 34 | quote-style = 'single' 35 | skip-magic-trailing-comma = true 36 | [tool.ruff.lint] 37 | allowed-confusables = ['−', 'ρ', '∗'] 38 | dummy-variable-rgx = '^_$' 39 | exclude = ['Bar', 'ALPHA'] 40 | explicit-preview-rules = true 41 | extend-fixable = ['Bar', 'ALPHA'] 42 | extend-ignore = ['Bar', 'ALPHA'] 43 | extend-safe-fixes = ['Bar', 'ALPHA'] 44 | extend-select = ['Bar', 'ALPHA'] 45 | extend-unsafe-fixes = ['Bar', 'ALPHA'] 46 | external = ['Bar', 'ALPHA'] 47 | fixable = ['Bar', 'ALPHA'] 48 | select = ['Bar', 'ALPHA'] 49 | task-tags = ['Bar', 'ALPHA'] 50 | typing-modules = ['Bar', 'ALPHA'] 51 | unfixable = ['Bar', 'ALPHA'] 52 | [tool.ruff.lint.extend-per-file-ignores] 53 | 'Magic.py' = ['Bar', 'ALPHA'] 54 | "alpha.py" = ['Bar', 'ALPHA'] 55 | [tool.ruff.lint.per-file-ignores] 56 | 'Magic.py' = ['Bar', 'ALPHA'] 57 | "alpha.py" = ['Bar', 'ALPHA'] 58 | [tool.ruff.lint.flake8-annotations] 59 | suppress-none-returning = true 60 | [tool.ruff.lint.flake8-bandit] 61 | hardcoded-tmp-directory = ['Bar', 'ALPHA'] 62 | [tool.ruff.lint.flake8-boolean-trap] 63 | extend-allowed-calls = ['Bar', 'ALPHA'] 64 | [tool.ruff.lint.flake8-bugbear] 65 | extend-immutable-calls = ['Bar', 'ALPHA'] 66 | [tool.ruff.lint.flake8-builtins] 67 | builtins-ignorelist = ['Bar', 'ALPHA'] 68 | [tool.ruff.lint.flake8-comprehensions] 69 | allow-dict-calls-with-keyword-arguments = true 70 | [tool.ruff.lint.flake8-copyright] 71 | author = 'Ruff' 72 | notice-rgx = '(?i)Copyright \\(C\\) \\d{4}' 73 | [tool.ruff.lint.flake8-errmsg] 74 | max-string-length = 20 75 | [tool.ruff.lint.flake8-gettext] 76 | extend-function-names = ['Bar', 'ALPHA'] 77 | [tool.ruff.lint.flake8-implicit-str-concat] 78 | allow-multiline = false 79 | [tool.ruff.lint.flake8-import-conventions.aliases] 80 | altair = "alt" 81 | numpy = "np" 82 | [tool.ruff.lint.flake8-pytest-style] 83 | parametrize-names-type = 'list' 84 | raises-extend-require-match-for = ['Bar', 'ALPHA'] 85 | [tool.ruff.lint.flake8-quotes] 86 | docstring-quotes = 'single' 87 | [tool.ruff.lint.flake8-self] 88 | extend-ignore-names = ['Bar', 'ALPHA'] 89 | [tool.ruff.lint.flake8-tidy-imports] 90 | banned-module-level-imports = ['Bar', 'ALPHA'] 91 | [tool.ruff.lint.flake8-type-checking] 92 | exempt-modules = ['Bar', 'ALPHA'] 93 | [tool.ruff.lint.flake8-unused-arguments] 94 | ignore-variadic-names = true 95 | [tool.ruff.lint.isort] 96 | section-order = ['Bar', 'ALPHA'] 97 | [tool.ruff.lint.mccabe] 98 | max-complexity = 5 99 | [tool.ruff.lint.pep8-naming] 100 | classmethod-decorators = ['Bar', 'ALPHA'] 101 | [tool.ruff.lint.pycodestyle] 102 | max-line-length = 100 103 | [tool.ruff.lint.pydocstyle] 104 | convention = 'google' 105 | [tool.ruff.lint.pyflakes] 106 | extend-generics = ['Bar', 'ALPHA'] 107 | [tool.ruff.lint.pylint] 108 | allow-dunder-method-names = ['Bar', 'ALPHA'] 109 | [tool.ruff.lint.more] 110 | ok = 1 111 | [tool.ruff.lint.extra] 112 | ok = 1 113 | [tool.ruff.lint.pyupgrade] 114 | keep-runtime-typing = true 115 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = "maturin" 3 | requires = [ 4 | "maturin>=1.7.1", 5 | ] 6 | 7 | [project] 8 | name = "pyproject-fmt-rust" 9 | description = "Format your pyproject.toml file" 10 | readme = "README.md" 11 | keywords = [ 12 | "format", 13 | "pyproject", 14 | ] 15 | license.file = "LICENSE.txt" 16 | authors = [ 17 | { name = "Bernat Gabor", email = "gaborjbernat@gmail.com" }, 18 | ] 19 | requires-python = ">=3.9" 20 | classifiers = [ 21 | "License :: OSI Approved :: MIT License", 22 | "Operating System :: OS Independent", 23 | "Programming Language :: Python", 24 | "Programming Language :: Python :: 3 :: Only", 25 | "Programming Language :: Python :: 3.9", 26 | "Programming Language :: Python :: 3.10", 27 | "Programming Language :: Python :: 3.11", 28 | "Programming Language :: Python :: 3.12", 29 | "Programming Language :: Python :: 3.13", 30 | ] 31 | dynamic = [ 32 | "version", 33 | ] 34 | dependencies = [ 35 | ] 36 | optional-dependencies.test = [ 37 | "covdefaults>=2.3", 38 | "pytest>=8.3.2", 39 | "pytest-cov>=5", 40 | ] 41 | urls."Bug Tracker" = "https://github.com/tox-dev/pyproject-fmt-rust/issues" 42 | urls."Changelog" = "https://github.com/tox-dev/pyproject-fmt-rust/releases" 43 | urls.Documentation = "https://github.com/tox-dev/pyproject-fmt-rust/" 44 | urls."Source Code" = "https://github.com/tox-dev/pyproject-fmt-rust" 45 | 46 | [tool.maturin] 47 | bindings = "pyo3" 48 | manifest-path = "Cargo.toml" 49 | module-name = "pyproject_fmt_rust._lib" 50 | python-source = "src" 51 | strip = true 52 | include = [ 53 | "rust-toolchain.toml", 54 | ] 55 | 56 | [tool.cibuildwheel] 57 | skip = [ 58 | "pp*", 59 | "*musl*", 60 | ] 61 | 62 | [tool.ruff] 63 | target-version = "py38" 64 | line-length = 120 65 | format.preview = true 66 | format.docstring-code-line-length = 100 67 | format.docstring-code-format = true 68 | lint.select = [ 69 | "ALL", 70 | ] 71 | lint.ignore = [ 72 | "ANN101", # no type annotation for self 73 | "ANN401", # allow Any as type annotation 74 | "COM812", # Conflict with formatter 75 | "CPY", # No copyright statements 76 | "D203", # `one-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible 77 | "D212", # `multi-line-summary-first-line` (D212) and `multi-line-summary-second-line` (D213) are incompatible 78 | "ISC001", # Conflict with formatter 79 | "S104", # Possible binding to all interface 80 | ] 81 | lint.per-file-ignores."tests/**/*.py" = [ 82 | "D", # don"t care about documentation in tests 83 | "FBT", # don"t care about booleans as positional arguments in tests 84 | "INP001", # no implicit namespace 85 | "PLC2701", # private import 86 | "PLR0913", # any number of arguments in tests 87 | "PLR0917", # any number of arguments in tests 88 | "PLR2004", # Magic value used in comparison, consider replacing with a constant variable 89 | "S101", # asserts allowed in tests... 90 | "S603", # `subprocess` call: check for execution of untrusted input 91 | ] 92 | lint.isort = { known-first-party = [ 93 | "pyproject_fmt_rust", 94 | ], required-imports = [ 95 | "from __future__ import annotations", 96 | ] } 97 | lint.preview = true 98 | 99 | [tool.codespell] 100 | builtin = "clear,usage,en-GB_to_en-US" 101 | count = true 102 | 103 | [tool.pyproject-fmt] 104 | max_supported_python = "3.13" 105 | 106 | [tool.pytest] 107 | ini_options.testpaths = [ 108 | "tests", 109 | ] 110 | 111 | [tool.coverage] 112 | html.show_contexts = true 113 | html.skip_covered = false 114 | paths.source = [ 115 | "src", 116 | ".tox/*/.venv/lib/*/site-packages", 117 | ".tox\\*\\.venv\\Lib\\site-packages", 118 | ".tox/*/lib/*/site-packages", 119 | ".tox\\*\\Lib\\site-packages", 120 | "**/src", 121 | "**\\src", 122 | ] 123 | report.fail_under = 100 124 | run.parallel = true 125 | run.plugins = [ 126 | "covdefaults", 127 | ] 128 | 129 | [tool.mypy] 130 | show_error_codes = true 131 | strict = true 132 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: ["main"] 6 | tags: ["*"] 7 | pull_request: 8 | schedule: 9 | - cron: "0 8 * * *" 10 | concurrency: 11 | group: build-${{ github.ref }} 12 | cancel-in-progress: true 13 | 14 | permissions: 15 | contents: read 16 | 17 | jobs: 18 | linux: 19 | runs-on: ${{ matrix.platform.runner }} 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | platform: 24 | - runner: ubuntu-latest 25 | target: x86_64 26 | interpreter: "3.8 pypy3.8 pypy3.9 pypy3.10" 27 | - runner: ubuntu-latest 28 | target: x86 29 | - runner: ubuntu-latest 30 | target: x86_64-unknown-linux-musl 31 | manylinux: musllinux_1_1 32 | - runner: ubuntu-latest 33 | target: i686-unknown-linux-musl 34 | manylinux: musllinux_1_1 35 | - runner: ubuntu-latest 36 | target: aarch64 37 | - runner: ubuntu-latest 38 | target: armv7 39 | - runner: ubuntu-latest 40 | target: s390x 41 | - runner: ubuntu-latest 42 | target: ppc64le 43 | steps: 44 | - uses: actions/checkout@v4 45 | - name: Build wheels 46 | uses: PyO3/maturin-action@v1 47 | with: 48 | target: ${{ matrix.platform.target }} 49 | args: --release --out dist --interpreter ${{ matrix.platform.interpreter || '3.8' }} 50 | sccache: "true" 51 | manylinux: ${{ matrix.platform.manylinux || 'auto' }} 52 | - name: Upload wheels 53 | uses: actions/upload-artifact@v4 54 | with: 55 | name: wheels-linux-${{ matrix.platform.target }} 56 | path: dist 57 | 58 | windows: 59 | runs-on: ${{ matrix.platform.runner }} 60 | strategy: 61 | matrix: 62 | platform: 63 | - runner: windows-latest 64 | target: x64 65 | - runner: windows-latest 66 | target: x86 67 | steps: 68 | - uses: actions/checkout@v4 69 | - name: Build wheels 70 | uses: PyO3/maturin-action@v1 71 | with: 72 | target: ${{ matrix.platform.target }} 73 | args: --release --out dist --interpreter ${{ matrix.platform.interpreter || '3.8' }} 74 | sccache: "true" 75 | - name: Upload wheels 76 | uses: actions/upload-artifact@v4 77 | with: 78 | name: wheels-windows-${{ matrix.platform.target }} 79 | path: dist 80 | 81 | macos: 82 | runs-on: ${{ matrix.platform.runner }} 83 | strategy: 84 | matrix: 85 | platform: 86 | - runner: macos-latest 87 | target: x86_64 88 | - runner: macos-14 89 | target: aarch64 90 | steps: 91 | - uses: actions/checkout@v4 92 | - name: Build wheels 93 | uses: PyO3/maturin-action@v1 94 | with: 95 | target: ${{ matrix.platform.target }} 96 | args: --release --out dist --interpreter "3.8 pypy3.8 pypy3.9 pypy3.10" 97 | sccache: "true" 98 | - name: Upload wheels 99 | uses: actions/upload-artifact@v4 100 | with: 101 | name: wheels-macos-${{ matrix.platform.target }} 102 | path: dist 103 | 104 | sdist: 105 | runs-on: ubuntu-latest 106 | steps: 107 | - uses: actions/checkout@v4 108 | - name: Build sdist 109 | uses: PyO3/maturin-action@v1 110 | with: 111 | command: sdist 112 | args: --out dist 113 | - name: Upload sdist 114 | uses: actions/upload-artifact@v4 115 | with: 116 | name: wheels-sdist 117 | path: dist 118 | 119 | release: 120 | name: Release 121 | runs-on: ubuntu-latest 122 | environment: 123 | name: release 124 | url: https://pypi.org/project/pyproject-fmt-rust/${{ github.ref_name }} 125 | permissions: 126 | id-token: write 127 | if: "startsWith(github.ref, 'refs/tags/')" 128 | needs: [linux, windows, macos, sdist] 129 | steps: 130 | - uses: actions/download-artifact@v4 131 | - name: Publish to PyPI 132 | uses: PyO3/maturin-action@v1 133 | with: 134 | command: upload 135 | args: --non-interactive --skip-existing wheels-*/* 136 | -------------------------------------------------------------------------------- /rust/src/data/ruff-order.expected.toml: -------------------------------------------------------------------------------- 1 | [tool.ruff] 2 | required-version = ">=0.0.193" 3 | extend = "../pyproject.toml" 4 | target-version = "py37" 5 | line-length = 120 6 | indent-width = 2 7 | tab-size = 2 8 | builtins = [ 9 | "ALPHA", 10 | "Bar", 11 | ] 12 | namespace-packages = [ 13 | "ALPHA", 14 | "Bar", 15 | ] 16 | src = [ 17 | "ALPHA", 18 | "Bar", 19 | ] 20 | include = [ 21 | "ALPHA", 22 | "Bar", 23 | ] 24 | extend-include = [ 25 | "ALPHA", 26 | "Bar", 27 | ] 28 | exclude = [ 29 | "ALPHA", 30 | "Bar", 31 | ] 32 | extend-exclude = [ 33 | "ALPHA", 34 | "Bar", 35 | ] 36 | force-exclude = true 37 | respect-gitignore = false 38 | preview = true 39 | fix = true 40 | unsafe-fixes = true 41 | fix-only = true 42 | show-fixes = true 43 | show-source = true 44 | output-format = "grouped" 45 | cache-dir = "~/a" 46 | format.preview = true 47 | format.indent-style = "tab" 48 | format.quote-style = "single" 49 | format.line-ending = "lf" 50 | format.skip-magic-trailing-comma = true 51 | format.docstring-code-line-length = 60 52 | format.exclude = [ 53 | "ALPHA", 54 | "Bar", 55 | ] 56 | format.docstring-code-format = true 57 | format.extra = true 58 | format.more = true 59 | lint.select = [ 60 | "ALPHA", 61 | "Bar", 62 | ] 63 | lint.extend-select = [ 64 | "ALPHA", 65 | "Bar", 66 | ] 67 | lint.explicit-preview-rules = true 68 | lint.exclude = [ 69 | "ALPHA", 70 | "Bar", 71 | ] 72 | lint.extend-ignore = [ 73 | "ALPHA", 74 | "Bar", 75 | ] 76 | lint.per-file-ignores.'Magic.py' = [ 77 | "ALPHA", 78 | "Bar", 79 | ] 80 | lint.per-file-ignores."alpha.py" = [ 81 | "ALPHA", 82 | "Bar", 83 | ] 84 | lint.extend-per-file-ignores.'Magic.py' = [ 85 | "ALPHA", 86 | "Bar", 87 | ] 88 | lint.extend-per-file-ignores."alpha.py" = [ 89 | "ALPHA", 90 | "Bar", 91 | ] 92 | lint.fixable = [ 93 | "ALPHA", 94 | "Bar", 95 | ] 96 | lint.extend-fixable = [ 97 | "ALPHA", 98 | "Bar", 99 | ] 100 | lint.unfixable = [ 101 | "ALPHA", 102 | "Bar", 103 | ] 104 | lint.extend-safe-fixes = [ 105 | "ALPHA", 106 | "Bar", 107 | ] 108 | lint.extend-unsafe-fixes = [ 109 | "ALPHA", 110 | "Bar", 111 | ] 112 | lint.typing-modules = [ 113 | "ALPHA", 114 | "Bar", 115 | ] 116 | lint.allowed-confusables = [ 117 | "−", 118 | "∗", 119 | "ρ", 120 | ] 121 | lint.dummy-variable-rgx = "^_$" 122 | lint.external = [ 123 | "ALPHA", 124 | "Bar", 125 | ] 126 | lint.task-tags = [ 127 | "ALPHA", 128 | "Bar", 129 | ] 130 | lint.flake8-annotations.suppress-none-returning = true 131 | lint.flake8-bandit.hardcoded-tmp-directory = [ 132 | "ALPHA", 133 | "Bar", 134 | ] 135 | lint.flake8-boolean-trap.extend-allowed-calls = [ 136 | "ALPHA", 137 | "Bar", 138 | ] 139 | lint.flake8-bugbear.extend-immutable-calls = [ 140 | "ALPHA", 141 | "Bar", 142 | ] 143 | lint.flake8-builtins.builtins-ignorelist = [ 144 | "ALPHA", 145 | "Bar", 146 | ] 147 | lint.flake8-comprehensions.allow-dict-calls-with-keyword-arguments = true 148 | lint.flake8-copyright.author = "Ruff" 149 | lint.flake8-copyright.notice-rgx = "(?i)Copyright \\(C\\) \\d{4}" 150 | lint.flake8-errmsg.max-string-length = 20 151 | lint.flake8-gettext.extend-function-names = [ 152 | "ALPHA", 153 | "Bar", 154 | ] 155 | lint.flake8-implicit-str-concat.allow-multiline = false 156 | lint.flake8-import-conventions.aliases.altair = "alt" 157 | lint.flake8-import-conventions.aliases.numpy = "np" 158 | lint.flake8-pytest-style.parametrize-names-type = "list" 159 | lint.flake8-pytest-style.raises-extend-require-match-for = [ 160 | "ALPHA", 161 | "Bar", 162 | ] 163 | lint.flake8-quotes.docstring-quotes = "single" 164 | lint.flake8-self.extend-ignore-names = [ 165 | "ALPHA", 166 | "Bar", 167 | ] 168 | lint.flake8-tidy-imports.banned-module-level-imports = [ 169 | "ALPHA", 170 | "Bar", 171 | ] 172 | lint.flake8-type-checking.exempt-modules = [ 173 | "ALPHA", 174 | "Bar", 175 | ] 176 | lint.flake8-unused-arguments.ignore-variadic-names = true 177 | lint.isort.section-order = [ 178 | "Bar", 179 | "ALPHA", 180 | ] 181 | lint.mccabe.max-complexity = 5 182 | lint.pep8-naming.classmethod-decorators = [ 183 | "ALPHA", 184 | "Bar", 185 | ] 186 | lint.pycodestyle.max-line-length = 100 187 | lint.pydocstyle.convention = "google" 188 | lint.pyflakes.extend-generics = [ 189 | "ALPHA", 190 | "Bar", 191 | ] 192 | lint.pylint.allow-dunder-method-names = [ 193 | "ALPHA", 194 | "Bar", 195 | ] 196 | lint.pyupgrade.keep-runtime-typing = true 197 | lint.extra.ok = 1 198 | lint.more.ok = 1 199 | -------------------------------------------------------------------------------- /rust/src/global.rs: -------------------------------------------------------------------------------- 1 | use taplo::rowan::SyntaxNode; 2 | use taplo::syntax::Lang; 3 | 4 | use crate::helpers::table::Tables; 5 | 6 | pub fn reorder_tables(root_ast: &SyntaxNode, tables: &Tables) { 7 | tables.reorder( 8 | root_ast, 9 | &[ 10 | "", 11 | "build-system", 12 | "project", 13 | // Build backends 14 | "tool.poetry", 15 | "tool.poetry-dynamic-versioning", 16 | "tool.pdm", 17 | "tool.setuptools", 18 | "tool.distutils", 19 | "tool.setuptools_scm", 20 | "tool.hatch", 21 | "tool.flit", 22 | "tool.scikit-build", 23 | "tool.meson-python", 24 | "tool.maturin", 25 | "tool.whey", 26 | "tool.py-build-cmake", 27 | "tool.sphinx-theme-builder", 28 | // Builders 29 | "tool.cibuildwheel", 30 | // Formatters and linters 31 | "tool.autopep8", 32 | "tool.black", 33 | "tool.ruff", 34 | "tool.isort", 35 | "tool.flake8", 36 | "tool.pycln", 37 | "tool.nbqa", 38 | "tool.pylint", 39 | "tool.repo-review", 40 | "tool.codespell", 41 | "tool.docformatter", 42 | "tool.pydoclint", 43 | "tool.tomlsort", 44 | "tool.check-manifest", 45 | "tool.check-sdist", 46 | "tool.check-wheel-contents", 47 | "tool.deptry", 48 | "tool.pyproject-fmt", 49 | // Testing 50 | "tool.pytest", 51 | "tool.pytest_env", 52 | "tool.pytest-enabler", 53 | "tool.coverage", 54 | // Runners 55 | "tool.doit", 56 | "tool.spin", 57 | "tool.tox", 58 | // Releasers/bumpers 59 | "tool.bumpversion", 60 | "tool.jupyter-releaser", 61 | "tool.tbump", 62 | "tool.towncrier", 63 | "tool.vendoring", 64 | // Type checking 65 | "tool.mypy", 66 | "tool.pyright", 67 | ], 68 | ); 69 | } 70 | 71 | #[cfg(test)] 72 | mod tests { 73 | use indoc::indoc; 74 | use rstest::rstest; 75 | use taplo::formatter::{format_syntax, Options}; 76 | use taplo::parser::parse; 77 | 78 | use crate::global::reorder_tables; 79 | use crate::helpers::table::Tables; 80 | 81 | #[rstest] 82 | #[case::reorder( 83 | indoc ! {r#" 84 | # comment 85 | a= "b" 86 | [project] 87 | name="alpha" 88 | dependencies=["e"] 89 | [build-system] 90 | build-backend="backend" 91 | requires=["c", "d"] 92 | [tool.mypy] 93 | mk="mv" 94 | [tool.ruff.test] 95 | mrt="vrt" 96 | [extra] 97 | ek = "ev" 98 | [tool.undefined] 99 | mu="mu" 100 | [tool.ruff] 101 | mr="vr" 102 | [demo] 103 | ed = "ed" 104 | [tool.coverage.report] 105 | cd="de" 106 | [tool.coverage] 107 | aa = "bb" 108 | [tool.coverage.paths] 109 | ab="bc" 110 | [tool.coverage.run] 111 | ef="fg" 112 | [tool.pytest] 113 | mk="mv" 114 | "#}, 115 | indoc ! {r#" 116 | # comment 117 | a = "b" 118 | 119 | [build-system] 120 | build-backend = "backend" 121 | requires = [ 122 | "c", 123 | "d", 124 | ] 125 | 126 | [project] 127 | name = "alpha" 128 | dependencies = [ 129 | "e", 130 | ] 131 | 132 | [tool.ruff] 133 | mr = "vr" 134 | [tool.ruff.test] 135 | mrt = "vrt" 136 | 137 | [tool.pytest] 138 | mk = "mv" 139 | 140 | [tool.coverage] 141 | aa = "bb" 142 | [tool.coverage.report] 143 | cd = "de" 144 | [tool.coverage.paths] 145 | ab = "bc" 146 | [tool.coverage.run] 147 | ef = "fg" 148 | 149 | [tool.mypy] 150 | mk = "mv" 151 | 152 | [extra] 153 | ek = "ev" 154 | 155 | [tool.undefined] 156 | mu = "mu" 157 | 158 | [demo] 159 | ed = "ed" 160 | "#}, 161 | )] 162 | fn test_reorder_table(#[case] start: &str, #[case] expected: &str) { 163 | let root_ast = parse(start).into_syntax().clone_for_update(); 164 | let tables = Tables::from_ast(&root_ast); 165 | reorder_tables(&root_ast, &tables); 166 | let opt = Options { 167 | column_width: 1, 168 | ..Options::default() 169 | }; 170 | let got = format_syntax(root_ast, opt); 171 | assert_eq!(got, expected); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /rust/src/helpers/create.rs: -------------------------------------------------------------------------------- 1 | use taplo::parser::parse; 2 | use taplo::syntax::SyntaxElement; 3 | use taplo::syntax::SyntaxKind::{ARRAY, COMMA, ENTRY, KEY, NEWLINE, STRING, VALUE}; 4 | 5 | pub fn make_string_node(text: &str) -> SyntaxElement { 6 | let expr = &format!("a = \"{}\"", text.replace('"', "\\\"")); 7 | for root in parse(expr) 8 | .into_syntax() 9 | .clone_for_update() 10 | .first_child() 11 | .unwrap() 12 | .children_with_tokens() 13 | { 14 | if root.kind() == VALUE { 15 | for entries in root.as_node().unwrap().children_with_tokens() { 16 | if entries.kind() == STRING { 17 | return entries; 18 | } 19 | } 20 | } 21 | } 22 | panic!("Could not create string element for {text:?}") 23 | } 24 | 25 | pub fn make_empty_newline() -> SyntaxElement { 26 | for root in parse("\n\n").into_syntax().clone_for_update().children_with_tokens() { 27 | if root.kind() == NEWLINE { 28 | return root; 29 | } 30 | } 31 | panic!("Could not create empty newline"); 32 | } 33 | 34 | pub fn make_newline() -> SyntaxElement { 35 | for root in parse("\n").into_syntax().clone_for_update().children_with_tokens() { 36 | if root.kind() == NEWLINE { 37 | return root; 38 | } 39 | } 40 | panic!("Could not create newline"); 41 | } 42 | 43 | pub fn make_comma() -> SyntaxElement { 44 | for root in parse("a=[1,2]").into_syntax().clone_for_update().children_with_tokens() { 45 | if root.kind() == ENTRY { 46 | for value in root.as_node().unwrap().children_with_tokens() { 47 | if value.kind() == VALUE { 48 | for array in value.as_node().unwrap().children_with_tokens() { 49 | if array.kind() == ARRAY { 50 | for e in array.as_node().unwrap().children_with_tokens() { 51 | if e.kind() == COMMA { 52 | return e; 53 | } 54 | } 55 | } 56 | } 57 | } 58 | } 59 | } 60 | } 61 | panic!("Could not create comma"); 62 | } 63 | 64 | pub fn make_key(text: &str) -> SyntaxElement { 65 | for root in parse(format!("{text}=1").as_str()) 66 | .into_syntax() 67 | .clone_for_update() 68 | .children_with_tokens() 69 | { 70 | if root.kind() == ENTRY { 71 | for value in root.as_node().unwrap().children_with_tokens() { 72 | if value.kind() == KEY { 73 | return value; 74 | } 75 | } 76 | } 77 | } 78 | panic!("Could not create key {text}"); 79 | } 80 | 81 | pub fn make_array(key: &str) -> SyntaxElement { 82 | let txt = format!("{key} = []"); 83 | for root in parse(txt.as_str()) 84 | .into_syntax() 85 | .clone_for_update() 86 | .children_with_tokens() 87 | { 88 | if root.kind() == ENTRY { 89 | return root; 90 | } 91 | } 92 | panic!("Could not create array"); 93 | } 94 | 95 | pub fn make_array_entry(key: &str) -> SyntaxElement { 96 | let txt = format!("a = [\"{key}\"]"); 97 | for root in parse(txt.as_str()) 98 | .into_syntax() 99 | .clone_for_update() 100 | .children_with_tokens() 101 | { 102 | if root.kind() == ENTRY { 103 | for value in root.as_node().unwrap().children_with_tokens() { 104 | if value.kind() == VALUE { 105 | for array in value.as_node().unwrap().children_with_tokens() { 106 | if array.kind() == ARRAY { 107 | for e in array.as_node().unwrap().children_with_tokens() { 108 | if e.kind() == VALUE { 109 | return e; 110 | } 111 | } 112 | } 113 | } 114 | } 115 | } 116 | } 117 | } 118 | panic!("Could not create array"); 119 | } 120 | 121 | pub fn make_entry_of_string(key: &String, value: &String) -> SyntaxElement { 122 | let txt = format!("{key} = \"{value}\"\n"); 123 | for root in parse(txt.as_str()) 124 | .into_syntax() 125 | .clone_for_update() 126 | .children_with_tokens() 127 | { 128 | if root.kind() == ENTRY { 129 | return root; 130 | } 131 | } 132 | panic!("Could not create entry of string"); 133 | } 134 | 135 | pub fn make_table_entry(key: &str) -> Vec { 136 | let txt = format!("[{key}]\n"); 137 | let mut res = Vec::::new(); 138 | for root in parse(txt.as_str()) 139 | .into_syntax() 140 | .clone_for_update() 141 | .children_with_tokens() 142 | { 143 | res.push(root); 144 | } 145 | res 146 | } 147 | -------------------------------------------------------------------------------- /rust/src/helpers/pep508.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Write; 2 | use std::str::FromStr; 3 | 4 | use pep440_rs::Operator; 5 | use pep508_rs::{MarkerTree, Requirement, VersionOrUrl}; 6 | 7 | pub fn format_requirement(value: &str, keep_full_version: bool) -> String { 8 | let req = Requirement::from_str(value).unwrap(); 9 | let mut result = req.name.to_string(); 10 | if !req.extras.is_empty() { 11 | write!(&mut result, "[").unwrap(); 12 | let extra_count = req.extras.len() - 1; 13 | for (at, extra) in req.extras.iter().enumerate() { 14 | write!(&mut result, "{extra}").unwrap(); 15 | if extra_count != at { 16 | write!(&mut result, ",").unwrap(); 17 | } 18 | } 19 | write!(&mut result, "]").unwrap(); 20 | } 21 | if let Some(version_or_url) = req.version_or_url { 22 | match version_or_url { 23 | VersionOrUrl::VersionSpecifier(v) => { 24 | let extra_count = v.len() - 1; 25 | for (at, spec) in v.iter().enumerate() { 26 | let mut spec_repr = format!("{spec}"); 27 | if !keep_full_version && spec.operator() != &Operator::TildeEqual { 28 | loop { 29 | let propose = spec_repr.strip_suffix(".0"); 30 | if propose.is_none() { 31 | break; 32 | } 33 | spec_repr = propose.unwrap().to_string(); 34 | } 35 | } 36 | write!(&mut result, "{spec_repr}").unwrap(); 37 | if extra_count != at { 38 | write!(&mut result, ",").unwrap(); 39 | } 40 | } 41 | } 42 | VersionOrUrl::Url(u) => { 43 | write!(&mut result, " @ {u}").unwrap(); 44 | } 45 | } 46 | } 47 | if let Some(marker) = req.marker { 48 | write!(&mut result, "; ").unwrap(); 49 | handle_marker(&marker, &mut result, false); 50 | } 51 | 52 | result 53 | } 54 | 55 | fn handle_marker(marker: &MarkerTree, result: &mut String, nested: bool) { 56 | match marker { 57 | MarkerTree::Expression(e) => { 58 | write!(result, "{}{}{}", e.l_value, e.operator, e.r_value).unwrap(); 59 | } 60 | MarkerTree::And(a) => { 61 | handle_tree(result, nested, a, " and "); 62 | } 63 | MarkerTree::Or(a) => { 64 | handle_tree(result, nested, a, " or "); 65 | } 66 | } 67 | } 68 | 69 | fn handle_tree(result: &mut String, nested: bool, elements: &[MarkerTree], x: &str) { 70 | let len = elements.len() - 1; 71 | if nested && len > 0 { 72 | write!(result, "(").unwrap(); 73 | } 74 | for (at, e) in elements.iter().enumerate() { 75 | handle_marker(e, result, true); 76 | if at != len { 77 | write!(result, "{x}").unwrap(); 78 | } 79 | } 80 | if nested && len > 0 { 81 | write!(result, ")").unwrap(); 82 | } 83 | } 84 | 85 | pub fn get_canonic_requirement_name(value: &str) -> String { 86 | let req = Requirement::from_str(value).unwrap(); 87 | req.name.to_string() 88 | } 89 | 90 | #[cfg(test)] 91 | mod tests { 92 | use rstest::rstest; 93 | 94 | use crate::helpers::pep508::{format_requirement, get_canonic_requirement_name}; 95 | 96 | #[rstest] 97 | #[case::lowercase("A", "a")] 98 | #[case::replace_dot_with_dash("a.b", "a-b")] 99 | fn test_get_canonic_requirement_name(#[case] start: &str, #[case] expected: &str) { 100 | assert_eq!(get_canonic_requirement_name(start), expected); 101 | } 102 | #[rstest] 103 | #[case::strip_version( 104 | r#"requests [security , tests] >= 2.0.0, == 2.8.* ; (os_name=="a" or os_name=='b') and os_name=='c' and python_version > "3.8""#, 105 | "requests[security,tests]>=2,==2.8.*; (os_name=='a' or os_name=='b') and os_name=='c' and python_version>'3.8'", 106 | false 107 | )] 108 | #[case::keep_version( 109 | r#"requests [security , tests] >= 2.0.0, == 2.8.* ; (os_name=="a" or os_name=='b') and os_name=='c' and python_version > "3.8""#, 110 | "requests[security,tests]>=2.0.0,==2.8.*; (os_name=='a' or os_name=='b') and os_name=='c' and python_version>'3.8'", 111 | true 112 | )] 113 | #[case::do_not_strip_tilda("a~=3.0.0", "a~=3.0.0", false)] 114 | #[case::url( 115 | " pip @ https://github.com/pypa/pip/archive/1.3.1.zip#sha1=da9234ee9982d4bbb3c72346a6de940a148ea686 ", 116 | "pip @ https://github.com/pypa/pip/archive/1.3.1.zip#sha1=da9234ee9982d4bbb3c72346a6de940a148ea686", 117 | true 118 | )] 119 | fn test_format_requirement(#[case] start: &str, #[case] expected: &str, #[case] keep_full_version: bool) { 120 | let got = format_requirement(start, keep_full_version); 121 | assert_eq!(got, expected); 122 | // formatting remains stable 123 | assert_eq!(format_requirement(got.as_str(), keep_full_version), expected); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /rust/src/ruff.rs: -------------------------------------------------------------------------------- 1 | use crate::helpers::array::{sort, transform}; 2 | use crate::helpers::string::update_content; 3 | use crate::helpers::table::{collapse_sub_tables, for_entries, reorder_table_keys, Tables}; 4 | 5 | #[allow(clippy::too_many_lines)] 6 | pub fn fix(tables: &mut Tables) { 7 | collapse_sub_tables(tables, "tool.ruff"); 8 | let table_element = tables.get("tool.ruff"); 9 | if table_element.is_none() { 10 | return; 11 | } 12 | let table = &mut table_element.unwrap().first().unwrap().borrow_mut(); 13 | for_entries(table, &mut |key, entry| match key.as_str() { 14 | "target-version" 15 | | "cache-dir" 16 | | "extend" 17 | | "required-version" 18 | | "output-format" 19 | | "format.indent-style" 20 | | "format.line-ending" 21 | | "format.quote-style" 22 | | "lint.dummy-variable-rgx" 23 | | "lint.flake8-copyright.author" 24 | | "lint.flake8-copyright.notice-rgx" 25 | | "lint.flake8-pytest-style.parametrize-names-type" 26 | | "lint.flake8-pytest-style.parametrize-values-row-type" 27 | | "lint.flake8-pytest-style.parametrize-values-type" 28 | | "lint.flake8-quotes.docstring-quotes" 29 | | "lint.flake8-quotes.multiline-quotes" 30 | | "lint.flake8-quotes.inline-quotes" 31 | | "lint.flake8-tidy-imports.ban-relative-imports" 32 | | "lint.isort.known-first-party" 33 | | "lint.isort.known-third-party" 34 | | "lint.isort.relative-imports-order" 35 | | "lint.pydocstyle.convention" => { 36 | update_content(entry, |s| String::from(s)); 37 | } 38 | "exclude" 39 | | "extend-exclude" 40 | | "builtins" 41 | | "include" 42 | | "extend-include" 43 | | "namespace-packages" 44 | | "src" 45 | | "format.exclude" 46 | | "lint.allowed-confusables" 47 | | "lint.exclude" 48 | | "lint.extend-fixable" 49 | | "lint.extend-ignore" 50 | | "lint.extend-safe-fixes" 51 | | "lint.extend-select" 52 | | "lint.extend-unsafe-fixes" 53 | | "lint.external" 54 | | "lint.fixable" 55 | | "lint.ignore" 56 | | "lint.logger-objects" 57 | | "lint.select" 58 | | "lint.task-tags" 59 | | "lint.typing-modules" 60 | | "lint.unfixable" 61 | | "lint.flake8-bandit.hardcoded-tmp-directory" 62 | | "lint.flake8-bandit.hardcoded-tmp-directory-extend" 63 | | "lint.flake8-boolean-trap.extend-allowed-calls" 64 | | "lint.flake8-bugbear.extend-immutable-calls" 65 | | "lint.flake8-builtins.builtins-ignorelist" 66 | | "lint.flake8-gettext.extend-function-names" 67 | | "lint.flake8-gettext.function-names" 68 | | "lint.flake8-import-conventions.banned-from" 69 | | "lint.flake8-pytest-style.raises-extend-require-match-for" 70 | | "lint.flake8-pytest-style.raises-require-match-for" 71 | | "lint.flake8-self.extend-ignore-names" 72 | | "lint.flake8-self.ignore-names" 73 | | "lint.flake8-tidy-imports.banned-module-level-imports" 74 | | "lint.flake8-type-checking.exempt-modules" 75 | | "lint.flake8-type-checking.runtime-evaluated-base-classes" 76 | | "lint.flake8-type-checking.runtime-evaluated-decorators" 77 | | "lint.isort.constants" 78 | | "lint.isort.default-section" 79 | | "lint.isort.extra-standard-library" 80 | | "lint.isort.forced-separate" 81 | | "lint.isort.no-lines-before" 82 | | "lint.isort.required-imports" 83 | | "lint.isort.single-line-exclusions" 84 | | "lint.isort.variables" 85 | | "lint.pep8-naming.classmethod-decorators" 86 | | "lint.pep8-naming.extend-ignore-names" 87 | | "lint.pep8-naming.ignore-names" 88 | | "lint.pep8-naming.staticmethod-decorators" 89 | | "lint.pydocstyle.ignore-decorators" 90 | | "lint.pydocstyle.property-decorators" 91 | | "lint.pyflakes.extend-generics" 92 | | "lint.pylint.allow-dunder-method-names" 93 | | "lint.pylint.allow-magic-value-types" => { 94 | transform(entry, &|s| String::from(s)); 95 | sort(entry, str::to_lowercase); 96 | } 97 | "lint.isort.section-order" => { 98 | transform(entry, &|s| String::from(s)); 99 | } 100 | _ => { 101 | if key.starts_with("lint.extend-per-file-ignores.") || key.starts_with("lint.per-file-ignores.") { 102 | transform(entry, &|s| String::from(s)); 103 | sort(entry, str::to_lowercase); 104 | } 105 | } 106 | }); 107 | reorder_table_keys( 108 | table, 109 | &[ 110 | "", 111 | "required-version", 112 | "extend", 113 | "target-version", 114 | "line-length", 115 | "indent-width", 116 | "tab-size", 117 | "builtins", 118 | "namespace-packages", 119 | "src", 120 | "include", 121 | "extend-include", 122 | "exclude", 123 | "extend-exclude", 124 | "force-exclude", 125 | "respect-gitignore", 126 | "preview", 127 | "fix", 128 | "unsafe-fixes", 129 | "fix-only", 130 | "show-fixes", 131 | "show-source", 132 | "output-format", 133 | "cache-dir", 134 | "format.preview", 135 | "format.indent-style", 136 | "format.quote-style", 137 | "format.line-ending", 138 | "format.skip-magic-trailing-comma", 139 | "format.docstring-code-line-length", 140 | "format.docstring-code-format ", 141 | "format.exclude", 142 | "format", 143 | "lint.select", 144 | "lint.extend-select", 145 | "lint.ignore", 146 | "lint.explicit-preview-rules", 147 | "lint.exclude", 148 | "lint.extend-ignore", 149 | "lint.per-file-ignores", 150 | "lint.extend-per-file-ignores", 151 | "lint.fixable", 152 | "lint.extend-fixable", 153 | "lint.unfixable", 154 | "lint.extend-safe-fixes", 155 | "lint.extend-unsafe-fixes", 156 | "lint.typing-modules", 157 | "lint.allowed-confusables", 158 | "lint.dummy-variable-rgx", 159 | "lint.external", 160 | "lint.task-tags", 161 | "lint.flake8-annotations", 162 | "lint.flake8-bandit", 163 | "lint.flake8-boolean-trap", 164 | "lint.flake8-bugbear", 165 | "lint.flake8-builtins", 166 | "lint.flake8-comprehensions", 167 | "lint.flake8-copyright", 168 | "lint.flake8-errmsg", 169 | "lint.flake8-gettext", 170 | "lint.flake8-implicit-str-concat", 171 | "lint.flake8-import-conventions", 172 | "lint.flake8-pytest-style", 173 | "lint.flake8-quotes", 174 | "lint.flake8-self", 175 | "lint.flake8-tidy-imports", 176 | "lint.flake8-type-checking", 177 | "lint.flake8-unused-arguments", 178 | "lint.isort", 179 | "lint.mccabe", 180 | "lint.pep8-naming", 181 | "lint.pycodestyle", 182 | "lint.pydocstyle", 183 | "lint.pyflakes", 184 | "lint.pylint", 185 | "lint.pyupgrade", 186 | "lint", 187 | ], 188 | ); 189 | } 190 | 191 | #[cfg(test)] 192 | mod tests { 193 | use std::fs::read_to_string; 194 | use std::path::{Path, PathBuf}; 195 | 196 | use rstest::{fixture, rstest}; 197 | use taplo::formatter::{format_syntax, Options}; 198 | use taplo::parser::parse; 199 | use taplo::syntax::SyntaxElement; 200 | 201 | use crate::helpers::table::Tables; 202 | use crate::ruff::fix; 203 | 204 | fn evaluate(start: &str) -> String { 205 | let root_ast = parse(start).into_syntax().clone_for_update(); 206 | let count = root_ast.children_with_tokens().count(); 207 | let mut tables = Tables::from_ast(&root_ast); 208 | fix(&mut tables); 209 | let entries = tables 210 | .table_set 211 | .iter() 212 | .flat_map(|e| e.borrow().clone()) 213 | .collect::>(); 214 | root_ast.splice_children(0..count, entries); 215 | let opt = Options { 216 | column_width: 1, 217 | ..Options::default() 218 | }; 219 | format_syntax(root_ast, opt) 220 | } 221 | #[fixture] 222 | fn data() -> PathBuf { 223 | Path::new(env!("CARGO_MANIFEST_DIR")) 224 | .join("rust") 225 | .join("src") 226 | .join("data") 227 | } 228 | 229 | #[rstest] 230 | fn test_order_ruff(data: PathBuf) { 231 | let start = read_to_string(data.join("ruff-order.start.toml")).unwrap(); 232 | let got = evaluate(start.as_str()); 233 | let expected = read_to_string(data.join("ruff-order.expected.toml")).unwrap(); 234 | assert_eq!(got, expected); 235 | } 236 | 237 | #[rstest] 238 | fn test_ruff_comment_21(data: PathBuf) { 239 | let start = read_to_string(data.join("ruff-21.start.toml")).unwrap(); 240 | let got = evaluate(start.as_str()); 241 | let expected = read_to_string(data.join("ruff-21.expected.toml")).unwrap(); 242 | assert_eq!(got, expected); 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /rust/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::string::String; 2 | 3 | use pyo3::prelude::PyModule; 4 | use pyo3::{pyclass, pyfunction, pymethods, pymodule, wrap_pyfunction, Bound, PyResult}; 5 | use taplo::formatter::{format_syntax, Options}; 6 | use taplo::parser::parse; 7 | 8 | use crate::global::reorder_tables; 9 | use crate::helpers::table::Tables; 10 | 11 | mod build_system; 12 | mod project; 13 | 14 | mod global; 15 | mod helpers; 16 | mod ruff; 17 | 18 | #[pyclass(frozen, get_all)] 19 | pub struct Settings { 20 | column_width: usize, 21 | indent: usize, 22 | keep_full_version: bool, 23 | max_supported_python: (u8, u8), 24 | min_supported_python: (u8, u8), 25 | } 26 | 27 | #[pymethods] 28 | impl Settings { 29 | #[new] 30 | #[pyo3(signature = (*, column_width, indent, keep_full_version, max_supported_python, min_supported_python ))] 31 | const fn new( 32 | column_width: usize, 33 | indent: usize, 34 | keep_full_version: bool, 35 | max_supported_python: (u8, u8), 36 | min_supported_python: (u8, u8), 37 | ) -> Self { 38 | Self { 39 | column_width, 40 | indent, 41 | keep_full_version, 42 | max_supported_python, 43 | min_supported_python, 44 | } 45 | } 46 | } 47 | 48 | /// Format toml file 49 | #[must_use] 50 | #[pyfunction] 51 | pub fn format_toml(content: &str, opt: &Settings) -> String { 52 | let root_ast = parse(content).into_syntax().clone_for_update(); 53 | let mut tables = Tables::from_ast(&root_ast); 54 | 55 | build_system::fix(&tables, opt.keep_full_version); 56 | project::fix( 57 | &mut tables, 58 | opt.keep_full_version, 59 | opt.max_supported_python, 60 | opt.min_supported_python, 61 | ); 62 | ruff::fix(&mut tables); 63 | reorder_tables(&root_ast, &tables); 64 | 65 | let options = Options { 66 | align_entries: false, // do not align by = 67 | align_comments: true, // align inline comments 68 | align_single_comments: true, // align comments after entries 69 | array_trailing_comma: true, // ensure arrays finish with trailing comma 70 | array_auto_expand: true, // arrays go to multi line when too long 71 | array_auto_collapse: false, // do not collapse for easier diffs 72 | compact_arrays: false, // leave whitespace 73 | compact_inline_tables: false, // leave whitespace 74 | compact_entries: false, // leave whitespace 75 | column_width: opt.column_width, 76 | indent_tables: false, 77 | indent_entries: false, 78 | inline_table_expand: true, 79 | trailing_newline: true, 80 | allowed_blank_lines: 1, // one blank line to separate 81 | indent_string: " ".repeat(opt.indent), 82 | reorder_keys: false, // respect custom order 83 | reorder_arrays: false, // for natural sorting we need to this ourselves 84 | crlf: false, 85 | }; 86 | format_syntax(root_ast, options) 87 | } 88 | 89 | /// # Errors 90 | /// 91 | /// Will return `PyErr` if an error is raised during formatting. 92 | #[pymodule] 93 | #[pyo3(name = "_lib")] 94 | #[cfg(not(tarpaulin_include))] 95 | pub fn _lib(m: &Bound<'_, PyModule>) -> PyResult<()> { 96 | m.add_function(wrap_pyfunction!(format_toml, m)?)?; 97 | m.add_class::()?; 98 | Ok(()) 99 | } 100 | 101 | #[cfg(test)] 102 | mod tests { 103 | use std::fs::read_to_string; 104 | use std::path::{Path, PathBuf}; 105 | 106 | use indoc::indoc; 107 | use rstest::{fixture, rstest}; 108 | 109 | use crate::{format_toml, Settings}; 110 | 111 | #[rstest] 112 | #[case::simple( 113 | indoc ! {r#" 114 | # comment 115 | a= "b" 116 | [project] 117 | name="alpha" 118 | dependencies=[" e >= 1.5.0"] 119 | [build-system] 120 | build-backend="backend" 121 | requires=[" c >= 1.5.0", "d == 2.0.0"] 122 | [tool.mypy] 123 | mk="mv" 124 | "#}, 125 | indoc ! {r#" 126 | # comment 127 | a = "b" 128 | 129 | [build-system] 130 | build-backend = "backend" 131 | requires = [ 132 | "c>=1.5", 133 | "d==2", 134 | ] 135 | 136 | [project] 137 | name = "alpha" 138 | classifiers = [ 139 | "Programming Language :: Python :: 3 :: Only", 140 | "Programming Language :: Python :: 3.9", 141 | "Programming Language :: Python :: 3.10", 142 | "Programming Language :: Python :: 3.11", 143 | "Programming Language :: Python :: 3.12", 144 | "Programming Language :: Python :: 3.13", 145 | ] 146 | dependencies = [ 147 | "e>=1.5", 148 | ] 149 | 150 | [tool.mypy] 151 | mk = "mv" 152 | "#}, 153 | 2, 154 | false, 155 | (3, 13), 156 | )] 157 | #[case::empty( 158 | indoc ! {r""}, 159 | "\n", 160 | 2, 161 | true, 162 | (3, 13) 163 | )] 164 | #[case::scripts( 165 | indoc ! {r#" 166 | [project.scripts] 167 | c = "d" 168 | a = "b" 169 | "#}, 170 | indoc ! {r#" 171 | [project] 172 | classifiers = [ 173 | "Programming Language :: Python :: 3 :: Only", 174 | "Programming Language :: Python :: 3.9", 175 | ] 176 | scripts.a = "b" 177 | scripts.c = "d" 178 | "#}, 179 | 2, 180 | true, 181 | (3, 9) 182 | )] 183 | #[case::subsubtable( 184 | indoc ! {r" 185 | [project] 186 | [tool.coverage.report] 187 | a = 2 188 | [tool.coverage] 189 | a = 0 190 | [tool.coverage.paths] 191 | a = 1 192 | [tool.coverage.run] 193 | a = 3 194 | "}, 195 | indoc ! {r#" 196 | [project] 197 | classifiers = [ 198 | "Programming Language :: Python :: 3 :: Only", 199 | "Programming Language :: Python :: 3.9", 200 | ] 201 | 202 | [tool.coverage] 203 | a = 0 204 | [tool.coverage.report] 205 | a = 2 206 | [tool.coverage.paths] 207 | a = 1 208 | [tool.coverage.run] 209 | a = 3 210 | "#}, 211 | 2, 212 | true, 213 | (3, 9) 214 | )] 215 | #[case::array_of_tables( 216 | indoc ! {r#" 217 | [tool.commitizen] 218 | name = "cz_customize" 219 | 220 | [tool.commitizen.customize] 221 | message_template = "" 222 | 223 | [[tool.commitizen.customize.questions]] 224 | type = "list" 225 | [[tool.commitizen.customize.questions]] 226 | type = "input" 227 | "#}, 228 | indoc ! {r#" 229 | [tool.commitizen] 230 | name = "cz_customize" 231 | 232 | [tool.commitizen.customize] 233 | message_template = "" 234 | 235 | [[tool.commitizen.customize.questions]] 236 | type = "list" 237 | 238 | [[tool.commitizen.customize.questions]] 239 | type = "input" 240 | "#}, 241 | 2, 242 | true, 243 | (3, 9) 244 | )] 245 | #[case::unstable_issue_18( 246 | indoc ! {r#" 247 | [project] 248 | requires-python = "==3.12" 249 | classifiers = [ 250 | "Programming Language :: Python :: 3 :: Only", 251 | "Programming Language :: Python :: 3.12", 252 | ] 253 | [project.urls] 254 | Source = "https://github.com/VWS-Python/vws-python-mock" 255 | 256 | [tool.setuptools] 257 | zip-safe = false 258 | "#}, 259 | indoc ! {r#" 260 | [project] 261 | requires-python = "==3.12" 262 | classifiers = [ 263 | "Programming Language :: Python :: 3 :: Only", 264 | "Programming Language :: Python :: 3.12", 265 | ] 266 | urls.Source = "https://github.com/VWS-Python/vws-python-mock" 267 | 268 | [tool.setuptools] 269 | zip-safe = false 270 | "#}, 271 | 2, 272 | true, 273 | (3, 9) 274 | )] 275 | fn test_format_toml( 276 | #[case] start: &str, 277 | #[case] expected: &str, 278 | #[case] indent: usize, 279 | #[case] keep_full_version: bool, 280 | #[case] max_supported_python: (u8, u8), 281 | ) { 282 | let settings = Settings { 283 | column_width: 1, 284 | indent, 285 | keep_full_version, 286 | max_supported_python, 287 | min_supported_python: (3, 9), 288 | }; 289 | let got = format_toml(start, &settings); 290 | assert_eq!(got, expected); 291 | let second = format_toml(got.as_str(), &settings); 292 | assert_eq!(second, got); 293 | } 294 | 295 | #[fixture] 296 | fn data() -> PathBuf { 297 | Path::new(env!("CARGO_MANIFEST_DIR")) 298 | .join("rust") 299 | .join("src") 300 | .join("data") 301 | } 302 | 303 | #[rstest] 304 | fn test_issue_24(data: PathBuf) { 305 | let start = read_to_string(data.join("ruff-order.start.toml")).unwrap(); 306 | let settings = Settings { 307 | column_width: 1, 308 | indent: 2, 309 | keep_full_version: false, 310 | max_supported_python: (3, 9), 311 | min_supported_python: (3, 9), 312 | }; 313 | let got = format_toml(start.as_str(), &settings); 314 | let expected = read_to_string(data.join("ruff-order.expected.toml")).unwrap(); 315 | assert_eq!(got, expected); 316 | let second = format_toml(got.as_str(), &settings); 317 | assert_eq!(second, got); 318 | } 319 | 320 | /// Test that the column width is respected, 321 | /// and that arrays are neither exploded nor collapsed without reason 322 | #[rstest] 323 | fn test_column_width() { 324 | let start = indoc! {r#" 325 | [build-system] 326 | build-backend = "backend" 327 | requires = ["c>=1.5", "d == 2" ] 328 | 329 | [project] 330 | name = "beta" 331 | dependencies = [ 332 | "e>=1.5", 333 | ] 334 | "#}; 335 | let settings = Settings { 336 | column_width: 80, 337 | indent: 4, 338 | keep_full_version: false, 339 | max_supported_python: (3, 13), 340 | min_supported_python: (3, 13), 341 | }; 342 | let got = format_toml(start, &settings); 343 | let expected = indoc! {r#" 344 | [build-system] 345 | build-backend = "backend" 346 | requires = [ "c>=1.5", "d==2" ] 347 | 348 | [project] 349 | name = "beta" 350 | classifiers = [ 351 | "Programming Language :: Python :: 3 :: Only", 352 | "Programming Language :: Python :: 3.13", 353 | ] 354 | dependencies = [ 355 | "e>=1.5", 356 | ] 357 | "#}; 358 | assert_eq!(got, expected); 359 | let second = format_toml(got.as_str(), &settings); 360 | assert_eq!(second, got); 361 | } 362 | } 363 | -------------------------------------------------------------------------------- /rust/src/helpers/array.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::collections::HashMap; 3 | 4 | use lexical_sort::{natural_lexical_cmp, StringSort}; 5 | use taplo::syntax::SyntaxKind::{ARRAY, COMMA, NEWLINE, STRING, VALUE, WHITESPACE}; 6 | use taplo::syntax::{SyntaxElement, SyntaxKind, SyntaxNode}; 7 | 8 | use crate::helpers::create::{make_comma, make_newline}; 9 | use crate::helpers::string::{load_text, update_content}; 10 | 11 | pub fn transform(node: &SyntaxNode, transform: &F) 12 | where 13 | F: Fn(&str) -> String, 14 | { 15 | for array in node.children_with_tokens() { 16 | if array.kind() == ARRAY { 17 | for array_entry in array.as_node().unwrap().children_with_tokens() { 18 | if array_entry.kind() == VALUE { 19 | update_content(array_entry.as_node().unwrap(), transform); 20 | } 21 | } 22 | } 23 | } 24 | } 25 | 26 | #[allow(clippy::range_plus_one, clippy::too_many_lines)] 27 | pub fn sort(node: &SyntaxNode, transform: F) 28 | where 29 | F: Fn(&str) -> String, 30 | { 31 | for array in node.children_with_tokens() { 32 | if array.kind() == ARRAY { 33 | let array_node = array.as_node().unwrap(); 34 | let has_trailing_comma = array_node 35 | .children_with_tokens() 36 | .map(|x| x.kind()) 37 | .filter(|x| *x == COMMA || *x == VALUE) 38 | .last() 39 | == Some(COMMA); 40 | let multiline = array_node.children_with_tokens().any(|e| e.kind() == NEWLINE); 41 | let mut value_set = Vec::>::new(); 42 | let entry_set = RefCell::new(Vec::::new()); 43 | let mut key_to_pos = HashMap::::new(); 44 | 45 | let mut add_to_value_set = |entry: String| { 46 | let mut entry_set_borrow = entry_set.borrow_mut(); 47 | if !entry_set_borrow.is_empty() { 48 | key_to_pos.insert(entry, value_set.len()); 49 | value_set.push(entry_set_borrow.clone()); 50 | entry_set_borrow.clear(); 51 | } 52 | }; 53 | let mut entries = Vec::::new(); 54 | let mut has_value = false; 55 | let mut previous_is_bracket_open = false; 56 | let mut entry_value = String::new(); 57 | let mut count = 0; 58 | 59 | for entry in array_node.children_with_tokens() { 60 | count += 1; 61 | if previous_is_bracket_open { 62 | // make sure ends with trailing comma 63 | if entry.kind() == NEWLINE || entry.kind() == WHITESPACE { 64 | continue; 65 | } 66 | previous_is_bracket_open = false; 67 | } 68 | match &entry.kind() { 69 | SyntaxKind::BRACKET_START => { 70 | entries.push(entry); 71 | if multiline { 72 | entries.push(make_newline()); 73 | } 74 | previous_is_bracket_open = true; 75 | } 76 | SyntaxKind::BRACKET_END => { 77 | if has_value { 78 | add_to_value_set(entry_value.clone()); 79 | } else { 80 | entries.extend(entry_set.borrow_mut().clone()); 81 | } 82 | entries.push(entry); 83 | } 84 | VALUE => { 85 | if has_value { 86 | if multiline { 87 | entry_set.borrow_mut().push(make_newline()); 88 | } 89 | add_to_value_set(entry_value.clone()); 90 | } 91 | has_value = true; 92 | let value_node = entry.as_node().unwrap(); 93 | let mut found_string = false; 94 | for child in value_node.children_with_tokens() { 95 | let kind = child.kind(); 96 | if kind == STRING { 97 | entry_value = transform(load_text(child.as_token().unwrap().text(), STRING).as_str()); 98 | found_string = true; 99 | break; 100 | } 101 | } 102 | if !found_string { 103 | // abort if not correct types 104 | return; 105 | } 106 | entry_set.borrow_mut().push(entry); 107 | entry_set.borrow_mut().push(make_comma()); 108 | } 109 | NEWLINE => { 110 | entry_set.borrow_mut().push(entry); 111 | if has_value { 112 | add_to_value_set(entry_value.clone()); 113 | has_value = false; 114 | } 115 | } 116 | COMMA => {} 117 | _ => { 118 | entry_set.borrow_mut().push(entry); 119 | } 120 | } 121 | } 122 | 123 | let mut order: Vec = key_to_pos.clone().into_keys().collect(); 124 | order.string_sort_unstable(natural_lexical_cmp); 125 | let end = entries.split_off(if multiline { 2 } else { 1 }); 126 | for key in order { 127 | entries.extend(value_set[key_to_pos[&key]].clone()); 128 | } 129 | entries.extend(end); 130 | array_node.splice_children(0..count, entries); 131 | if !has_trailing_comma { 132 | if let Some((i, _)) = array_node 133 | .children_with_tokens() 134 | .enumerate() 135 | .filter(|(_, x)| x.kind() == COMMA) 136 | .last() 137 | { 138 | array_node.splice_children(i..i + 1, vec![]); 139 | } 140 | } 141 | } 142 | } 143 | } 144 | 145 | #[cfg(test)] 146 | mod tests { 147 | use indoc::indoc; 148 | use rstest::rstest; 149 | use taplo::formatter::{format_syntax, Options}; 150 | use taplo::parser::parse; 151 | use taplo::syntax::SyntaxKind::{ENTRY, VALUE}; 152 | 153 | use crate::helpers::array::{sort, transform}; 154 | use crate::helpers::pep508::format_requirement; 155 | 156 | #[rstest] 157 | #[case::strip_micro_no_keep( 158 | indoc ! {r#" 159 | a=["maturin >= 1.5.0"] 160 | "#}, 161 | indoc ! {r#" 162 | a = ["maturin>=1.5"] 163 | "#}, 164 | false 165 | )] 166 | #[case::strip_micro_keep( 167 | indoc ! {r#" 168 | a=["maturin >= 1.5.0"] 169 | "#}, 170 | indoc ! {r#" 171 | a = ["maturin>=1.5.0"] 172 | "#}, 173 | true 174 | )] 175 | #[case::no_change( 176 | indoc ! {r#" 177 | a = [ 178 | "maturin>=1.5.3",# comment here 179 | # a comment afterwards 180 | ] 181 | "#}, 182 | indoc ! {r#" 183 | a = [ 184 | "maturin>=1.5.3", # comment here 185 | # a comment afterwards 186 | ] 187 | "#}, 188 | false 189 | )] 190 | #[case::ignore_non_string( 191 | indoc ! {r#" 192 | a=[{key="maturin>=1.5.0"}] 193 | "#}, 194 | indoc ! {r#" 195 | a = [{ key = "maturin>=1.5.0" }] 196 | "#}, 197 | false 198 | )] 199 | #[case::has_double_quote( 200 | indoc ! {r#" 201 | a=['importlib-metadata>=7.0.0;python_version<"3.8"'] 202 | "#}, 203 | indoc ! {r#" 204 | a = ["importlib-metadata>=7; python_version<'3.8'"] 205 | "#}, 206 | false 207 | )] 208 | fn test_normalize_requirement(#[case] start: &str, #[case] expected: &str, #[case] keep_full_version: bool) { 209 | let root_ast = parse(start).into_syntax().clone_for_update(); 210 | for children in root_ast.children_with_tokens() { 211 | if children.kind() == ENTRY { 212 | for entry in children.as_node().unwrap().children_with_tokens() { 213 | if entry.kind() == VALUE { 214 | transform(entry.as_node().unwrap(), &|s| format_requirement(s, keep_full_version)); 215 | } 216 | } 217 | } 218 | } 219 | let res = format_syntax(root_ast, Options::default()); 220 | assert_eq!(expected, res); 221 | } 222 | 223 | #[rstest] 224 | #[case::empty( 225 | indoc ! {r" 226 | a = [] 227 | "}, 228 | indoc ! {r" 229 | a = [] 230 | "} 231 | )] 232 | #[case::single( 233 | indoc ! {r#" 234 | a = ["A"] 235 | "#}, 236 | indoc ! {r#" 237 | a = ["A"] 238 | "#} 239 | )] 240 | #[case::newline_single( 241 | indoc ! {r#" 242 | a = ["A"] 243 | "#}, 244 | indoc ! {r#" 245 | a = ["A"] 246 | "#} 247 | )] 248 | #[case::newline_single_comment( 249 | indoc ! {r#" 250 | a = [ # comment 251 | "A" 252 | ] 253 | "#}, 254 | indoc ! {r#" 255 | a = [ 256 | # comment 257 | "A", 258 | ] 259 | "#} 260 | )] 261 | #[case::double( 262 | indoc ! {r#" 263 | a = ["A", "B"] 264 | "#}, 265 | indoc ! {r#" 266 | a = ["A", "B"] 267 | "#} 268 | )] 269 | #[case::increasing( 270 | indoc ! {r#" 271 | a=["B", "D", 272 | # C comment 273 | "C", # C trailing 274 | # A comment 275 | "A" # A trailing 276 | # extra 277 | ] # array comment 278 | "#}, 279 | indoc ! {r#" 280 | a = [ 281 | # A comment 282 | "A", # A trailing 283 | "B", 284 | # C comment 285 | "C", # C trailing 286 | "D", 287 | # extra 288 | ] # array comment 289 | "#} 290 | )] 291 | fn test_order_array(#[case] start: &str, #[case] expected: &str) { 292 | let root_ast = parse(start).into_syntax().clone_for_update(); 293 | for children in root_ast.children_with_tokens() { 294 | if children.kind() == ENTRY { 295 | for entry in children.as_node().unwrap().children_with_tokens() { 296 | if entry.kind() == VALUE { 297 | sort(entry.as_node().unwrap(), str::to_lowercase); 298 | } 299 | } 300 | } 301 | } 302 | let opt = Options { 303 | column_width: 120, 304 | ..Options::default() 305 | }; 306 | let res = format_syntax(root_ast, opt); 307 | assert_eq!(res, expected); 308 | } 309 | 310 | #[rstest] 311 | #[case::reorder_no_trailing_comma( 312 | indoc ! {r#"a=["B","A"]"#}, 313 | indoc ! {r#"a=["A","B"]"#} 314 | )] 315 | fn test_reorder_no_trailing_comma(#[case] start: &str, #[case] expected: &str) { 316 | let root_ast = parse(start).into_syntax().clone_for_update(); 317 | for children in root_ast.children_with_tokens() { 318 | if children.kind() == ENTRY { 319 | for entry in children.as_node().unwrap().children_with_tokens() { 320 | if entry.kind() == VALUE { 321 | sort(entry.as_node().unwrap(), str::to_lowercase); 322 | } 323 | } 324 | } 325 | } 326 | let mut res = root_ast.to_string(); 327 | res.retain(|x| !x.is_whitespace()); 328 | assert_eq!(res, expected); 329 | } 330 | } 331 | -------------------------------------------------------------------------------- /rust/src/helpers/table.rs: -------------------------------------------------------------------------------- 1 | use std::cell::{RefCell, RefMut}; 2 | use std::collections::HashMap; 3 | use std::iter::zip; 4 | use std::ops::Index; 5 | 6 | use taplo::syntax::SyntaxKind::{ENTRY, IDENT, KEY, NEWLINE, TABLE_ARRAY_HEADER, TABLE_HEADER, VALUE}; 7 | use taplo::syntax::{SyntaxElement, SyntaxNode}; 8 | use taplo::HashSet; 9 | 10 | use crate::helpers::create::{make_empty_newline, make_key, make_newline, make_table_entry}; 11 | use crate::helpers::string::load_text; 12 | 13 | #[derive(Debug)] 14 | pub struct Tables { 15 | pub header_to_pos: HashMap>, 16 | pub table_set: Vec>>, 17 | } 18 | 19 | impl Tables { 20 | pub(crate) fn get(&self, key: &str) -> Option>>> { 21 | if self.header_to_pos.contains_key(key) { 22 | let mut res = Vec::<&RefCell>>::new(); 23 | for pos in &self.header_to_pos[key] { 24 | res.push(&self.table_set[*pos]); 25 | } 26 | Some(res) 27 | } else { 28 | None 29 | } 30 | } 31 | 32 | pub fn from_ast(root_ast: &SyntaxNode) -> Self { 33 | let mut header_to_pos = HashMap::>::new(); 34 | let mut table_set = Vec::>>::new(); 35 | let entry_set = RefCell::new(Vec::::new()); 36 | let mut table_kind = TABLE_HEADER; 37 | let mut add_to_table_set = |kind| { 38 | let mut entry_set_borrow = entry_set.borrow_mut(); 39 | if !entry_set_borrow.is_empty() { 40 | let table_name = get_table_name(&entry_set_borrow[0]); 41 | let indexes = header_to_pos.entry(table_name).or_default(); 42 | if kind == TABLE_ARRAY_HEADER || (kind == TABLE_HEADER && indexes.is_empty()) { 43 | indexes.push(table_set.len()); 44 | table_set.push(RefCell::new(entry_set_borrow.clone())); 45 | } else if kind == TABLE_HEADER && !indexes.is_empty() { 46 | // join tables 47 | let pos = indexes.first().unwrap(); 48 | let mut res = table_set.index(*pos).borrow_mut(); 49 | let mut new = entry_set_borrow.clone(); 50 | if let Some(last_non_trailing_newline_index) = new.iter().rposition(|x| x.kind() != NEWLINE) { 51 | new.truncate(last_non_trailing_newline_index + 1); 52 | } 53 | if res.last().unwrap().kind() != NEWLINE { 54 | res.push(make_newline()); 55 | } 56 | res.extend( 57 | new.into_iter() 58 | .skip_while(|x| [NEWLINE, TABLE_HEADER].contains(&x.kind())), 59 | ); 60 | } 61 | entry_set_borrow.clear(); 62 | } 63 | }; 64 | for c in root_ast.children_with_tokens() { 65 | if [TABLE_ARRAY_HEADER, TABLE_HEADER].contains(&c.kind()) { 66 | add_to_table_set(table_kind); 67 | table_kind = c.kind(); 68 | } 69 | entry_set.borrow_mut().push(c); 70 | } 71 | add_to_table_set(table_kind); 72 | Self { 73 | header_to_pos, 74 | table_set, 75 | } 76 | } 77 | 78 | pub fn reorder(&self, root_ast: &SyntaxNode, order: &[&str]) { 79 | let mut to_insert = Vec::::new(); 80 | let order = calculate_order(&self.header_to_pos, &self.table_set, order); 81 | let mut next = order.clone(); 82 | if !next.is_empty() { 83 | next.remove(0); 84 | } 85 | next.push(String::new()); 86 | for (name, next_name) in zip(order.iter(), next.iter()) { 87 | for entries in self.get(name).unwrap() { 88 | let got = entries.borrow_mut(); 89 | if !got.is_empty() { 90 | let last = got.last().unwrap(); 91 | if name.is_empty() && last.kind() == NEWLINE && got.len() == 1 { 92 | continue; 93 | } 94 | let mut add = got.clone(); 95 | if get_key(name) != get_key(next_name) { 96 | if last.kind() == NEWLINE { 97 | // replace existing newline to ensure single newline 98 | add.pop(); 99 | } 100 | add.push(make_empty_newline()); 101 | } 102 | to_insert.extend(add); 103 | } 104 | } 105 | } 106 | root_ast.splice_children(0..root_ast.children_with_tokens().count(), to_insert); 107 | } 108 | } 109 | fn calculate_order( 110 | header_to_pos: &HashMap>, 111 | table_set: &[RefCell>], 112 | ordering: &[&str], 113 | ) -> Vec { 114 | let max_ordering = ordering.len() * 2; 115 | let key_to_pos = ordering 116 | .iter() 117 | .enumerate() 118 | .map(|(k, v)| (v, k * 2)) 119 | .collect::>(); 120 | 121 | let mut header_pos: Vec<(String, usize)> = header_to_pos 122 | .clone() 123 | .into_iter() 124 | .filter(|(_k, v)| v.iter().any(|p| !table_set.get(*p).unwrap().borrow().is_empty())) 125 | .map(|(k, v)| (k, *v.iter().min().unwrap())) 126 | .collect(); 127 | 128 | header_pos.sort_by_cached_key(|(k, file_pos)| -> (usize, usize) { 129 | let key = get_key(k); 130 | let pos = key_to_pos.get(&key.as_str()); 131 | 132 | ( 133 | if let Some(&pos) = pos { 134 | let offset = usize::from(key != *k); 135 | pos + offset 136 | } else { 137 | max_ordering 138 | }, 139 | *file_pos, 140 | ) 141 | }); 142 | header_pos.into_iter().map(|(k, _)| k).collect() 143 | } 144 | 145 | fn get_key(k: &str) -> String { 146 | let parts: Vec<&str> = k.splitn(3, '.').collect(); 147 | if !parts.is_empty() { 148 | return if parts[0] == "tool" && parts.len() >= 2 { 149 | parts[0..2].join(".") 150 | } else { 151 | String::from(parts[0]) 152 | }; 153 | } 154 | String::from(k) 155 | } 156 | 157 | pub fn reorder_table_keys(table: &mut RefMut>, order: &[&str]) { 158 | let (size, mut to_insert) = (table.len(), Vec::::new()); 159 | let (key_to_position, key_set) = load_keys(table); 160 | let mut handled_positions = HashSet::::new(); 161 | for current_key in order { 162 | let mut matching_keys = key_to_position 163 | .iter() 164 | .filter(|(checked_key, position)| { 165 | !handled_positions.contains(position) 166 | && (current_key == checked_key 167 | || (checked_key.starts_with(current_key) 168 | && checked_key.len() > current_key.len() 169 | && checked_key.chars().nth(current_key.len()).unwrap() == '.')) 170 | }) 171 | .map(|(key, _)| key) 172 | .clone() 173 | .collect::>(); 174 | matching_keys.sort_by_key(|key| key.to_lowercase().replace('"', "")); 175 | for key in matching_keys { 176 | let position = key_to_position[key]; 177 | to_insert.extend(key_set[position].clone()); 178 | handled_positions.insert(position); 179 | } 180 | } 181 | for (position, entries) in key_set.into_iter().enumerate() { 182 | if !handled_positions.contains(&position) { 183 | to_insert.extend(entries); 184 | } 185 | } 186 | table.splice(0..size, to_insert); 187 | } 188 | 189 | fn load_keys(table: &[SyntaxElement]) -> (HashMap, Vec>) { 190 | let mut key_to_pos = HashMap::::new(); 191 | let mut key_set = Vec::>::new(); 192 | let entry_set = RefCell::new(Vec::::new()); 193 | let mut add_to_key_set = |k| { 194 | let mut entry_set_borrow = entry_set.borrow_mut(); 195 | if !entry_set_borrow.is_empty() { 196 | key_to_pos.insert(k, key_set.len()); 197 | key_set.push(entry_set_borrow.clone()); 198 | entry_set_borrow.clear(); 199 | } 200 | }; 201 | let mut key = String::new(); 202 | let mut cutoff = false; 203 | for element in table { 204 | let kind = element.kind(); 205 | if kind == ENTRY { 206 | if cutoff { 207 | add_to_key_set(key.clone()); 208 | cutoff = false; 209 | } 210 | for e in element.as_node().unwrap().children_with_tokens() { 211 | if e.kind() == KEY { 212 | key = e.as_node().unwrap().text().to_string().trim().to_string(); 213 | break; 214 | } 215 | } 216 | } 217 | if [ENTRY, TABLE_HEADER, TABLE_ARRAY_HEADER].contains(&kind) { 218 | cutoff = true; 219 | } 220 | entry_set.borrow_mut().push(element.clone()); 221 | if cutoff && kind == NEWLINE { 222 | add_to_key_set(key.clone()); 223 | cutoff = false; 224 | } 225 | } 226 | add_to_key_set(key); 227 | (key_to_pos, key_set) 228 | } 229 | 230 | pub fn get_table_name(entry: &SyntaxElement) -> String { 231 | if [TABLE_HEADER, TABLE_ARRAY_HEADER].contains(&entry.kind()) { 232 | for child in entry.as_node().unwrap().children_with_tokens() { 233 | if child.kind() == KEY { 234 | return child.as_node().unwrap().text().to_string().trim().to_string(); 235 | } 236 | } 237 | } 238 | String::new() 239 | } 240 | 241 | pub fn for_entries(table: &[SyntaxElement], f: &mut F) 242 | where 243 | F: FnMut(String, &SyntaxNode), 244 | { 245 | let mut key = String::new(); 246 | for table_entry in table { 247 | if table_entry.kind() == ENTRY { 248 | for entry in table_entry.as_node().unwrap().children_with_tokens() { 249 | if entry.kind() == KEY { 250 | key = entry.as_node().unwrap().text().to_string().trim().to_string(); 251 | } else if entry.kind() == VALUE { 252 | f(key.clone(), entry.as_node().unwrap()); 253 | } 254 | } 255 | } 256 | } 257 | } 258 | 259 | pub fn collapse_sub_tables(tables: &mut Tables, name: &str) { 260 | let h2p = tables.header_to_pos.clone(); 261 | let sub_name_prefix = format!("{name}."); 262 | let sub_table_keys: Vec<&String> = h2p.keys().filter(|s| s.starts_with(sub_name_prefix.as_str())).collect(); 263 | if sub_table_keys.is_empty() { 264 | return; 265 | } 266 | if !tables.header_to_pos.contains_key(name) { 267 | tables 268 | .header_to_pos 269 | .insert(String::from(name), vec![tables.table_set.len()]); 270 | tables.table_set.push(RefCell::new(make_table_entry(name))); 271 | } 272 | let main_positions = tables.header_to_pos[name].clone(); 273 | if main_positions.len() != 1 { 274 | return; 275 | } 276 | let mut main = tables.table_set[*main_positions.first().unwrap()].borrow_mut(); 277 | for key in sub_table_keys { 278 | let sub_positions = tables.header_to_pos[key].clone(); 279 | if sub_positions.len() != 1 { 280 | continue; 281 | } 282 | let mut sub = tables.table_set[*sub_positions.first().unwrap()].borrow_mut(); 283 | let sub_name = key.strip_prefix(sub_name_prefix.as_str()).unwrap(); 284 | let mut header = false; 285 | for child in sub.iter() { 286 | let kind = child.kind(); 287 | if kind == TABLE_HEADER { 288 | header = true; 289 | continue; 290 | } 291 | if header && kind == NEWLINE { 292 | continue; 293 | } 294 | if kind == ENTRY { 295 | let mut to_insert = Vec::::new(); 296 | let child_node = child.as_node().unwrap(); 297 | for mut entry in child_node.children_with_tokens() { 298 | if entry.kind() == KEY { 299 | for array_entry_value in entry.as_node().unwrap().children_with_tokens() { 300 | if array_entry_value.kind() == IDENT { 301 | let txt = load_text(array_entry_value.as_token().unwrap().text(), IDENT); 302 | entry = make_key(format!("{sub_name}.{txt}").as_str()); 303 | break; 304 | } 305 | } 306 | } 307 | to_insert.push(entry); 308 | } 309 | child_node.splice_children(0..to_insert.len(), to_insert); 310 | } 311 | if main.last().unwrap().kind() != NEWLINE { 312 | main.push(make_newline()); 313 | } 314 | main.push(child.clone()); 315 | } 316 | sub.clear(); 317 | } 318 | } 319 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "ahash" 7 | version = "0.8.11" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" 10 | dependencies = [ 11 | "cfg-if", 12 | "getrandom", 13 | "once_cell", 14 | "version_check", 15 | "zerocopy", 16 | ] 17 | 18 | [[package]] 19 | name = "aho-corasick" 20 | version = "1.1.3" 21 | source = "registry+https://github.com/rust-lang/crates.io-index" 22 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 23 | dependencies = [ 24 | "memchr", 25 | ] 26 | 27 | [[package]] 28 | name = "any_ascii" 29 | version = "0.1.7" 30 | source = "registry+https://github.com/rust-lang/crates.io-index" 31 | checksum = "70033777eb8b5124a81a1889416543dddef2de240019b674c81285a2635a7e1e" 32 | 33 | [[package]] 34 | name = "arc-swap" 35 | version = "1.7.1" 36 | source = "registry+https://github.com/rust-lang/crates.io-index" 37 | checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" 38 | 39 | [[package]] 40 | name = "autocfg" 41 | version = "1.3.0" 42 | source = "registry+https://github.com/rust-lang/crates.io-index" 43 | checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" 44 | 45 | [[package]] 46 | name = "beef" 47 | version = "0.5.2" 48 | source = "registry+https://github.com/rust-lang/crates.io-index" 49 | checksum = "3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1" 50 | 51 | [[package]] 52 | name = "bitflags" 53 | version = "2.6.0" 54 | source = "registry+https://github.com/rust-lang/crates.io-index" 55 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 56 | 57 | [[package]] 58 | name = "bstr" 59 | version = "1.10.0" 60 | source = "registry+https://github.com/rust-lang/crates.io-index" 61 | checksum = "40723b8fb387abc38f4f4a37c09073622e41dd12327033091ef8950659e6dc0c" 62 | dependencies = [ 63 | "memchr", 64 | "serde", 65 | ] 66 | 67 | [[package]] 68 | name = "cfg-if" 69 | version = "1.0.0" 70 | source = "registry+https://github.com/rust-lang/crates.io-index" 71 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 72 | 73 | [[package]] 74 | name = "countme" 75 | version = "3.0.1" 76 | source = "registry+https://github.com/rust-lang/crates.io-index" 77 | checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636" 78 | 79 | [[package]] 80 | name = "deranged" 81 | version = "0.3.11" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" 84 | dependencies = [ 85 | "powerfmt", 86 | ] 87 | 88 | [[package]] 89 | name = "derivative" 90 | version = "2.2.0" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" 93 | dependencies = [ 94 | "proc-macro2", 95 | "quote", 96 | "syn 1.0.109", 97 | ] 98 | 99 | [[package]] 100 | name = "either" 101 | version = "1.13.0" 102 | source = "registry+https://github.com/rust-lang/crates.io-index" 103 | checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" 104 | 105 | [[package]] 106 | name = "fnv" 107 | version = "1.0.7" 108 | source = "registry+https://github.com/rust-lang/crates.io-index" 109 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 110 | 111 | [[package]] 112 | name = "form_urlencoded" 113 | version = "1.2.1" 114 | source = "registry+https://github.com/rust-lang/crates.io-index" 115 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 116 | dependencies = [ 117 | "percent-encoding", 118 | ] 119 | 120 | [[package]] 121 | name = "futures" 122 | version = "0.3.30" 123 | source = "registry+https://github.com/rust-lang/crates.io-index" 124 | checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" 125 | dependencies = [ 126 | "futures-channel", 127 | "futures-core", 128 | "futures-executor", 129 | "futures-io", 130 | "futures-sink", 131 | "futures-task", 132 | "futures-util", 133 | ] 134 | 135 | [[package]] 136 | name = "futures-channel" 137 | version = "0.3.30" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" 140 | dependencies = [ 141 | "futures-core", 142 | "futures-sink", 143 | ] 144 | 145 | [[package]] 146 | name = "futures-core" 147 | version = "0.3.30" 148 | source = "registry+https://github.com/rust-lang/crates.io-index" 149 | checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" 150 | 151 | [[package]] 152 | name = "futures-executor" 153 | version = "0.3.30" 154 | source = "registry+https://github.com/rust-lang/crates.io-index" 155 | checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" 156 | dependencies = [ 157 | "futures-core", 158 | "futures-task", 159 | "futures-util", 160 | ] 161 | 162 | [[package]] 163 | name = "futures-io" 164 | version = "0.3.30" 165 | source = "registry+https://github.com/rust-lang/crates.io-index" 166 | checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" 167 | 168 | [[package]] 169 | name = "futures-macro" 170 | version = "0.3.30" 171 | source = "registry+https://github.com/rust-lang/crates.io-index" 172 | checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" 173 | dependencies = [ 174 | "proc-macro2", 175 | "quote", 176 | "syn 2.0.77", 177 | ] 178 | 179 | [[package]] 180 | name = "futures-sink" 181 | version = "0.3.30" 182 | source = "registry+https://github.com/rust-lang/crates.io-index" 183 | checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" 184 | 185 | [[package]] 186 | name = "futures-task" 187 | version = "0.3.30" 188 | source = "registry+https://github.com/rust-lang/crates.io-index" 189 | checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" 190 | 191 | [[package]] 192 | name = "futures-timer" 193 | version = "3.0.3" 194 | source = "registry+https://github.com/rust-lang/crates.io-index" 195 | checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" 196 | 197 | [[package]] 198 | name = "futures-util" 199 | version = "0.3.30" 200 | source = "registry+https://github.com/rust-lang/crates.io-index" 201 | checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" 202 | dependencies = [ 203 | "futures-channel", 204 | "futures-core", 205 | "futures-io", 206 | "futures-macro", 207 | "futures-sink", 208 | "futures-task", 209 | "memchr", 210 | "pin-project-lite", 211 | "pin-utils", 212 | "slab", 213 | ] 214 | 215 | [[package]] 216 | name = "getrandom" 217 | version = "0.2.15" 218 | source = "registry+https://github.com/rust-lang/crates.io-index" 219 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 220 | dependencies = [ 221 | "cfg-if", 222 | "libc", 223 | "wasi", 224 | ] 225 | 226 | [[package]] 227 | name = "glob" 228 | version = "0.3.1" 229 | source = "registry+https://github.com/rust-lang/crates.io-index" 230 | checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" 231 | 232 | [[package]] 233 | name = "globset" 234 | version = "0.4.14" 235 | source = "registry+https://github.com/rust-lang/crates.io-index" 236 | checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1" 237 | dependencies = [ 238 | "aho-corasick", 239 | "bstr", 240 | "log", 241 | "regex-automata", 242 | "regex-syntax 0.8.4", 243 | ] 244 | 245 | [[package]] 246 | name = "hashbrown" 247 | version = "0.14.5" 248 | source = "registry+https://github.com/rust-lang/crates.io-index" 249 | checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 250 | 251 | [[package]] 252 | name = "heck" 253 | version = "0.4.1" 254 | source = "registry+https://github.com/rust-lang/crates.io-index" 255 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 256 | 257 | [[package]] 258 | name = "idna" 259 | version = "0.5.0" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" 262 | dependencies = [ 263 | "unicode-bidi", 264 | "unicode-normalization", 265 | ] 266 | 267 | [[package]] 268 | name = "indoc" 269 | version = "2.0.5" 270 | source = "registry+https://github.com/rust-lang/crates.io-index" 271 | checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" 272 | 273 | [[package]] 274 | name = "itertools" 275 | version = "0.10.5" 276 | source = "registry+https://github.com/rust-lang/crates.io-index" 277 | checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" 278 | dependencies = [ 279 | "either", 280 | ] 281 | 282 | [[package]] 283 | name = "itoa" 284 | version = "1.0.11" 285 | source = "registry+https://github.com/rust-lang/crates.io-index" 286 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 287 | 288 | [[package]] 289 | name = "lexical-sort" 290 | version = "0.3.1" 291 | source = "registry+https://github.com/rust-lang/crates.io-index" 292 | checksum = "c09e4591611e231daf4d4c685a66cb0410cc1e502027a20ae55f2bb9e997207a" 293 | dependencies = [ 294 | "any_ascii", 295 | ] 296 | 297 | [[package]] 298 | name = "libc" 299 | version = "0.2.158" 300 | source = "registry+https://github.com/rust-lang/crates.io-index" 301 | checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" 302 | 303 | [[package]] 304 | name = "lock_api" 305 | version = "0.4.12" 306 | source = "registry+https://github.com/rust-lang/crates.io-index" 307 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 308 | dependencies = [ 309 | "autocfg", 310 | "scopeguard", 311 | ] 312 | 313 | [[package]] 314 | name = "log" 315 | version = "0.4.22" 316 | source = "registry+https://github.com/rust-lang/crates.io-index" 317 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 318 | 319 | [[package]] 320 | name = "logos" 321 | version = "0.12.1" 322 | source = "registry+https://github.com/rust-lang/crates.io-index" 323 | checksum = "bf8b031682c67a8e3d5446840f9573eb7fe26efe7ec8d195c9ac4c0647c502f1" 324 | dependencies = [ 325 | "logos-derive", 326 | ] 327 | 328 | [[package]] 329 | name = "logos-derive" 330 | version = "0.12.1" 331 | source = "registry+https://github.com/rust-lang/crates.io-index" 332 | checksum = "a1d849148dbaf9661a6151d1ca82b13bb4c4c128146a88d05253b38d4e2f496c" 333 | dependencies = [ 334 | "beef", 335 | "fnv", 336 | "proc-macro2", 337 | "quote", 338 | "regex-syntax 0.6.29", 339 | "syn 1.0.109", 340 | ] 341 | 342 | [[package]] 343 | name = "memchr" 344 | version = "2.7.4" 345 | source = "registry+https://github.com/rust-lang/crates.io-index" 346 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 347 | 348 | [[package]] 349 | name = "memoffset" 350 | version = "0.9.1" 351 | source = "registry+https://github.com/rust-lang/crates.io-index" 352 | checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" 353 | dependencies = [ 354 | "autocfg", 355 | ] 356 | 357 | [[package]] 358 | name = "num-conv" 359 | version = "0.1.0" 360 | source = "registry+https://github.com/rust-lang/crates.io-index" 361 | checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 362 | 363 | [[package]] 364 | name = "once_cell" 365 | version = "1.19.0" 366 | source = "registry+https://github.com/rust-lang/crates.io-index" 367 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 368 | 369 | [[package]] 370 | name = "parking_lot" 371 | version = "0.12.3" 372 | source = "registry+https://github.com/rust-lang/crates.io-index" 373 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 374 | dependencies = [ 375 | "lock_api", 376 | "parking_lot_core", 377 | ] 378 | 379 | [[package]] 380 | name = "parking_lot_core" 381 | version = "0.9.10" 382 | source = "registry+https://github.com/rust-lang/crates.io-index" 383 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 384 | dependencies = [ 385 | "cfg-if", 386 | "libc", 387 | "redox_syscall", 388 | "smallvec", 389 | "windows-targets", 390 | ] 391 | 392 | [[package]] 393 | name = "pep440_rs" 394 | version = "0.6.6" 395 | source = "registry+https://github.com/rust-lang/crates.io-index" 396 | checksum = "466eada3179c2e069ca897b99006cbb33f816290eaeec62464eea907e22ae385" 397 | dependencies = [ 398 | "once_cell", 399 | "unicode-width", 400 | "unscanny", 401 | ] 402 | 403 | [[package]] 404 | name = "pep508_rs" 405 | version = "0.6.1" 406 | source = "registry+https://github.com/rust-lang/crates.io-index" 407 | checksum = "3f8877489a99ccc80012333123e434f84e645fe1ede3b30e9d3b815887a12979" 408 | dependencies = [ 409 | "derivative", 410 | "once_cell", 411 | "pep440_rs", 412 | "regex", 413 | "thiserror", 414 | "unicode-width", 415 | "url", 416 | "urlencoding", 417 | ] 418 | 419 | [[package]] 420 | name = "percent-encoding" 421 | version = "2.3.1" 422 | source = "registry+https://github.com/rust-lang/crates.io-index" 423 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 424 | 425 | [[package]] 426 | name = "pin-project-lite" 427 | version = "0.2.14" 428 | source = "registry+https://github.com/rust-lang/crates.io-index" 429 | checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" 430 | 431 | [[package]] 432 | name = "pin-utils" 433 | version = "0.1.0" 434 | source = "registry+https://github.com/rust-lang/crates.io-index" 435 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 436 | 437 | [[package]] 438 | name = "portable-atomic" 439 | version = "1.7.0" 440 | source = "registry+https://github.com/rust-lang/crates.io-index" 441 | checksum = "da544ee218f0d287a911e9c99a39a8c9bc8fcad3cb8db5959940044ecfc67265" 442 | 443 | [[package]] 444 | name = "powerfmt" 445 | version = "0.2.0" 446 | source = "registry+https://github.com/rust-lang/crates.io-index" 447 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 448 | 449 | [[package]] 450 | name = "proc-macro2" 451 | version = "1.0.86" 452 | source = "registry+https://github.com/rust-lang/crates.io-index" 453 | checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" 454 | dependencies = [ 455 | "unicode-ident", 456 | ] 457 | 458 | [[package]] 459 | name = "pyo3" 460 | version = "0.21.2" 461 | source = "registry+https://github.com/rust-lang/crates.io-index" 462 | checksum = "a5e00b96a521718e08e03b1a622f01c8a8deb50719335de3f60b3b3950f069d8" 463 | dependencies = [ 464 | "cfg-if", 465 | "indoc", 466 | "libc", 467 | "memoffset", 468 | "parking_lot", 469 | "portable-atomic", 470 | "pyo3-build-config", 471 | "pyo3-ffi", 472 | "pyo3-macros", 473 | "unindent", 474 | ] 475 | 476 | [[package]] 477 | name = "pyo3-build-config" 478 | version = "0.21.2" 479 | source = "registry+https://github.com/rust-lang/crates.io-index" 480 | checksum = "7883df5835fafdad87c0d888b266c8ec0f4c9ca48a5bed6bbb592e8dedee1b50" 481 | dependencies = [ 482 | "once_cell", 483 | "target-lexicon", 484 | ] 485 | 486 | [[package]] 487 | name = "pyo3-ffi" 488 | version = "0.21.2" 489 | source = "registry+https://github.com/rust-lang/crates.io-index" 490 | checksum = "01be5843dc60b916ab4dad1dca6d20b9b4e6ddc8e15f50c47fe6d85f1fb97403" 491 | dependencies = [ 492 | "libc", 493 | "pyo3-build-config", 494 | ] 495 | 496 | [[package]] 497 | name = "pyo3-macros" 498 | version = "0.21.2" 499 | source = "registry+https://github.com/rust-lang/crates.io-index" 500 | checksum = "77b34069fc0682e11b31dbd10321cbf94808394c56fd996796ce45217dfac53c" 501 | dependencies = [ 502 | "proc-macro2", 503 | "pyo3-macros-backend", 504 | "quote", 505 | "syn 2.0.77", 506 | ] 507 | 508 | [[package]] 509 | name = "pyo3-macros-backend" 510 | version = "0.21.2" 511 | source = "registry+https://github.com/rust-lang/crates.io-index" 512 | checksum = "08260721f32db5e1a5beae69a55553f56b99bd0e1c3e6e0a5e8851a9d0f5a85c" 513 | dependencies = [ 514 | "heck", 515 | "proc-macro2", 516 | "pyo3-build-config", 517 | "quote", 518 | "syn 2.0.77", 519 | ] 520 | 521 | [[package]] 522 | name = "pyproject-fmt-rust" 523 | version = "1.2.0" 524 | dependencies = [ 525 | "indoc", 526 | "lexical-sort", 527 | "pep440_rs", 528 | "pep508_rs", 529 | "pyo3", 530 | "regex", 531 | "rstest", 532 | "taplo", 533 | ] 534 | 535 | [[package]] 536 | name = "quote" 537 | version = "1.0.37" 538 | source = "registry+https://github.com/rust-lang/crates.io-index" 539 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 540 | dependencies = [ 541 | "proc-macro2", 542 | ] 543 | 544 | [[package]] 545 | name = "redox_syscall" 546 | version = "0.5.3" 547 | source = "registry+https://github.com/rust-lang/crates.io-index" 548 | checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" 549 | dependencies = [ 550 | "bitflags", 551 | ] 552 | 553 | [[package]] 554 | name = "regex" 555 | version = "1.10.6" 556 | source = "registry+https://github.com/rust-lang/crates.io-index" 557 | checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" 558 | dependencies = [ 559 | "aho-corasick", 560 | "memchr", 561 | "regex-automata", 562 | "regex-syntax 0.8.4", 563 | ] 564 | 565 | [[package]] 566 | name = "regex-automata" 567 | version = "0.4.7" 568 | source = "registry+https://github.com/rust-lang/crates.io-index" 569 | checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" 570 | dependencies = [ 571 | "aho-corasick", 572 | "memchr", 573 | "regex-syntax 0.8.4", 574 | ] 575 | 576 | [[package]] 577 | name = "regex-syntax" 578 | version = "0.6.29" 579 | source = "registry+https://github.com/rust-lang/crates.io-index" 580 | checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" 581 | 582 | [[package]] 583 | name = "regex-syntax" 584 | version = "0.8.4" 585 | source = "registry+https://github.com/rust-lang/crates.io-index" 586 | checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" 587 | 588 | [[package]] 589 | name = "relative-path" 590 | version = "1.9.3" 591 | source = "registry+https://github.com/rust-lang/crates.io-index" 592 | checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" 593 | 594 | [[package]] 595 | name = "rowan" 596 | version = "0.15.16" 597 | source = "registry+https://github.com/rust-lang/crates.io-index" 598 | checksum = "0a542b0253fa46e632d27a1dc5cf7b930de4df8659dc6e720b647fc72147ae3d" 599 | dependencies = [ 600 | "countme", 601 | "hashbrown", 602 | "rustc-hash", 603 | "text-size", 604 | ] 605 | 606 | [[package]] 607 | name = "rstest" 608 | version = "0.19.0" 609 | source = "registry+https://github.com/rust-lang/crates.io-index" 610 | checksum = "9d5316d2a1479eeef1ea21e7f9ddc67c191d497abc8fc3ba2467857abbb68330" 611 | dependencies = [ 612 | "futures", 613 | "futures-timer", 614 | "rstest_macros", 615 | "rustc_version", 616 | ] 617 | 618 | [[package]] 619 | name = "rstest_macros" 620 | version = "0.19.0" 621 | source = "registry+https://github.com/rust-lang/crates.io-index" 622 | checksum = "04a9df72cc1f67020b0d63ad9bfe4a323e459ea7eb68e03bd9824db49f9a4c25" 623 | dependencies = [ 624 | "cfg-if", 625 | "glob", 626 | "proc-macro2", 627 | "quote", 628 | "regex", 629 | "relative-path", 630 | "rustc_version", 631 | "syn 2.0.77", 632 | "unicode-ident", 633 | ] 634 | 635 | [[package]] 636 | name = "rustc-hash" 637 | version = "1.1.0" 638 | source = "registry+https://github.com/rust-lang/crates.io-index" 639 | checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" 640 | 641 | [[package]] 642 | name = "rustc_version" 643 | version = "0.4.1" 644 | source = "registry+https://github.com/rust-lang/crates.io-index" 645 | checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" 646 | dependencies = [ 647 | "semver", 648 | ] 649 | 650 | [[package]] 651 | name = "ryu" 652 | version = "1.0.18" 653 | source = "registry+https://github.com/rust-lang/crates.io-index" 654 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 655 | 656 | [[package]] 657 | name = "scopeguard" 658 | version = "1.2.0" 659 | source = "registry+https://github.com/rust-lang/crates.io-index" 660 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 661 | 662 | [[package]] 663 | name = "semver" 664 | version = "1.0.23" 665 | source = "registry+https://github.com/rust-lang/crates.io-index" 666 | checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" 667 | 668 | [[package]] 669 | name = "serde" 670 | version = "1.0.210" 671 | source = "registry+https://github.com/rust-lang/crates.io-index" 672 | checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" 673 | dependencies = [ 674 | "serde_derive", 675 | ] 676 | 677 | [[package]] 678 | name = "serde_derive" 679 | version = "1.0.210" 680 | source = "registry+https://github.com/rust-lang/crates.io-index" 681 | checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" 682 | dependencies = [ 683 | "proc-macro2", 684 | "quote", 685 | "syn 2.0.77", 686 | ] 687 | 688 | [[package]] 689 | name = "serde_json" 690 | version = "1.0.128" 691 | source = "registry+https://github.com/rust-lang/crates.io-index" 692 | checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" 693 | dependencies = [ 694 | "itoa", 695 | "memchr", 696 | "ryu", 697 | "serde", 698 | ] 699 | 700 | [[package]] 701 | name = "slab" 702 | version = "0.4.9" 703 | source = "registry+https://github.com/rust-lang/crates.io-index" 704 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 705 | dependencies = [ 706 | "autocfg", 707 | ] 708 | 709 | [[package]] 710 | name = "smallvec" 711 | version = "1.13.2" 712 | source = "registry+https://github.com/rust-lang/crates.io-index" 713 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 714 | 715 | [[package]] 716 | name = "syn" 717 | version = "1.0.109" 718 | source = "registry+https://github.com/rust-lang/crates.io-index" 719 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 720 | dependencies = [ 721 | "proc-macro2", 722 | "quote", 723 | "unicode-ident", 724 | ] 725 | 726 | [[package]] 727 | name = "syn" 728 | version = "2.0.77" 729 | source = "registry+https://github.com/rust-lang/crates.io-index" 730 | checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" 731 | dependencies = [ 732 | "proc-macro2", 733 | "quote", 734 | "unicode-ident", 735 | ] 736 | 737 | [[package]] 738 | name = "taplo" 739 | version = "0.13.2" 740 | source = "registry+https://github.com/rust-lang/crates.io-index" 741 | checksum = "010941ac4171eaf12f1e26dfc11dadaf78619ea2330940fef01fe6bb0442d14d" 742 | dependencies = [ 743 | "ahash", 744 | "arc-swap", 745 | "either", 746 | "globset", 747 | "itertools", 748 | "logos", 749 | "once_cell", 750 | "rowan", 751 | "serde", 752 | "serde_json", 753 | "thiserror", 754 | "time", 755 | "tracing", 756 | ] 757 | 758 | [[package]] 759 | name = "target-lexicon" 760 | version = "0.12.16" 761 | source = "registry+https://github.com/rust-lang/crates.io-index" 762 | checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" 763 | 764 | [[package]] 765 | name = "text-size" 766 | version = "1.1.1" 767 | source = "registry+https://github.com/rust-lang/crates.io-index" 768 | checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233" 769 | 770 | [[package]] 771 | name = "thiserror" 772 | version = "1.0.63" 773 | source = "registry+https://github.com/rust-lang/crates.io-index" 774 | checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" 775 | dependencies = [ 776 | "thiserror-impl", 777 | ] 778 | 779 | [[package]] 780 | name = "thiserror-impl" 781 | version = "1.0.63" 782 | source = "registry+https://github.com/rust-lang/crates.io-index" 783 | checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" 784 | dependencies = [ 785 | "proc-macro2", 786 | "quote", 787 | "syn 2.0.77", 788 | ] 789 | 790 | [[package]] 791 | name = "time" 792 | version = "0.3.36" 793 | source = "registry+https://github.com/rust-lang/crates.io-index" 794 | checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" 795 | dependencies = [ 796 | "deranged", 797 | "itoa", 798 | "num-conv", 799 | "powerfmt", 800 | "serde", 801 | "time-core", 802 | "time-macros", 803 | ] 804 | 805 | [[package]] 806 | name = "time-core" 807 | version = "0.1.2" 808 | source = "registry+https://github.com/rust-lang/crates.io-index" 809 | checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" 810 | 811 | [[package]] 812 | name = "time-macros" 813 | version = "0.2.18" 814 | source = "registry+https://github.com/rust-lang/crates.io-index" 815 | checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" 816 | dependencies = [ 817 | "num-conv", 818 | "time-core", 819 | ] 820 | 821 | [[package]] 822 | name = "tinyvec" 823 | version = "1.8.0" 824 | source = "registry+https://github.com/rust-lang/crates.io-index" 825 | checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" 826 | dependencies = [ 827 | "tinyvec_macros", 828 | ] 829 | 830 | [[package]] 831 | name = "tinyvec_macros" 832 | version = "0.1.1" 833 | source = "registry+https://github.com/rust-lang/crates.io-index" 834 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 835 | 836 | [[package]] 837 | name = "tracing" 838 | version = "0.1.40" 839 | source = "registry+https://github.com/rust-lang/crates.io-index" 840 | checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" 841 | dependencies = [ 842 | "pin-project-lite", 843 | "tracing-attributes", 844 | "tracing-core", 845 | ] 846 | 847 | [[package]] 848 | name = "tracing-attributes" 849 | version = "0.1.27" 850 | source = "registry+https://github.com/rust-lang/crates.io-index" 851 | checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" 852 | dependencies = [ 853 | "proc-macro2", 854 | "quote", 855 | "syn 2.0.77", 856 | ] 857 | 858 | [[package]] 859 | name = "tracing-core" 860 | version = "0.1.32" 861 | source = "registry+https://github.com/rust-lang/crates.io-index" 862 | checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" 863 | dependencies = [ 864 | "once_cell", 865 | ] 866 | 867 | [[package]] 868 | name = "unicode-bidi" 869 | version = "0.3.15" 870 | source = "registry+https://github.com/rust-lang/crates.io-index" 871 | checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" 872 | 873 | [[package]] 874 | name = "unicode-ident" 875 | version = "1.0.12" 876 | source = "registry+https://github.com/rust-lang/crates.io-index" 877 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 878 | 879 | [[package]] 880 | name = "unicode-normalization" 881 | version = "0.1.23" 882 | source = "registry+https://github.com/rust-lang/crates.io-index" 883 | checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" 884 | dependencies = [ 885 | "tinyvec", 886 | ] 887 | 888 | [[package]] 889 | name = "unicode-width" 890 | version = "0.1.13" 891 | source = "registry+https://github.com/rust-lang/crates.io-index" 892 | checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" 893 | 894 | [[package]] 895 | name = "unindent" 896 | version = "0.2.3" 897 | source = "registry+https://github.com/rust-lang/crates.io-index" 898 | checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce" 899 | 900 | [[package]] 901 | name = "unscanny" 902 | version = "0.1.0" 903 | source = "registry+https://github.com/rust-lang/crates.io-index" 904 | checksum = "e9df2af067a7953e9c3831320f35c1cc0600c30d44d9f7a12b01db1cd88d6b47" 905 | 906 | [[package]] 907 | name = "url" 908 | version = "2.5.2" 909 | source = "registry+https://github.com/rust-lang/crates.io-index" 910 | checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" 911 | dependencies = [ 912 | "form_urlencoded", 913 | "idna", 914 | "percent-encoding", 915 | ] 916 | 917 | [[package]] 918 | name = "urlencoding" 919 | version = "2.1.3" 920 | source = "registry+https://github.com/rust-lang/crates.io-index" 921 | checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" 922 | 923 | [[package]] 924 | name = "version_check" 925 | version = "0.9.5" 926 | source = "registry+https://github.com/rust-lang/crates.io-index" 927 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 928 | 929 | [[package]] 930 | name = "wasi" 931 | version = "0.11.0+wasi-snapshot-preview1" 932 | source = "registry+https://github.com/rust-lang/crates.io-index" 933 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 934 | 935 | [[package]] 936 | name = "windows-targets" 937 | version = "0.52.6" 938 | source = "registry+https://github.com/rust-lang/crates.io-index" 939 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 940 | dependencies = [ 941 | "windows_aarch64_gnullvm", 942 | "windows_aarch64_msvc", 943 | "windows_i686_gnu", 944 | "windows_i686_gnullvm", 945 | "windows_i686_msvc", 946 | "windows_x86_64_gnu", 947 | "windows_x86_64_gnullvm", 948 | "windows_x86_64_msvc", 949 | ] 950 | 951 | [[package]] 952 | name = "windows_aarch64_gnullvm" 953 | version = "0.52.6" 954 | source = "registry+https://github.com/rust-lang/crates.io-index" 955 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 956 | 957 | [[package]] 958 | name = "windows_aarch64_msvc" 959 | version = "0.52.6" 960 | source = "registry+https://github.com/rust-lang/crates.io-index" 961 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 962 | 963 | [[package]] 964 | name = "windows_i686_gnu" 965 | version = "0.52.6" 966 | source = "registry+https://github.com/rust-lang/crates.io-index" 967 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 968 | 969 | [[package]] 970 | name = "windows_i686_gnullvm" 971 | version = "0.52.6" 972 | source = "registry+https://github.com/rust-lang/crates.io-index" 973 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 974 | 975 | [[package]] 976 | name = "windows_i686_msvc" 977 | version = "0.52.6" 978 | source = "registry+https://github.com/rust-lang/crates.io-index" 979 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 980 | 981 | [[package]] 982 | name = "windows_x86_64_gnu" 983 | version = "0.52.6" 984 | source = "registry+https://github.com/rust-lang/crates.io-index" 985 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 986 | 987 | [[package]] 988 | name = "windows_x86_64_gnullvm" 989 | version = "0.52.6" 990 | source = "registry+https://github.com/rust-lang/crates.io-index" 991 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 992 | 993 | [[package]] 994 | name = "windows_x86_64_msvc" 995 | version = "0.52.6" 996 | source = "registry+https://github.com/rust-lang/crates.io-index" 997 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 998 | 999 | [[package]] 1000 | name = "zerocopy" 1001 | version = "0.7.35" 1002 | source = "registry+https://github.com/rust-lang/crates.io-index" 1003 | checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" 1004 | dependencies = [ 1005 | "zerocopy-derive", 1006 | ] 1007 | 1008 | [[package]] 1009 | name = "zerocopy-derive" 1010 | version = "0.7.35" 1011 | source = "registry+https://github.com/rust-lang/crates.io-index" 1012 | checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" 1013 | dependencies = [ 1014 | "proc-macro2", 1015 | "quote", 1016 | "syn 2.0.77", 1017 | ] 1018 | -------------------------------------------------------------------------------- /rust/src/project.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefMut; 2 | 3 | use regex::Regex; 4 | use taplo::syntax::SyntaxKind::{ 5 | ARRAY, BRACKET_END, BRACKET_START, COMMA, ENTRY, IDENT, INLINE_TABLE, KEY, NEWLINE, STRING, VALUE, 6 | }; 7 | use taplo::syntax::{SyntaxElement, SyntaxNode}; 8 | use taplo::util::StrExt; 9 | use taplo::HashSet; 10 | 11 | use crate::helpers::array::{sort, transform}; 12 | use crate::helpers::create::{make_array, make_array_entry, make_comma, make_entry_of_string, make_newline}; 13 | use crate::helpers::pep508::{format_requirement, get_canonic_requirement_name}; 14 | use crate::helpers::string::{load_text, update_content}; 15 | use crate::helpers::table::{collapse_sub_tables, for_entries, reorder_table_keys, Tables}; 16 | 17 | pub fn fix( 18 | tables: &mut Tables, 19 | keep_full_version: bool, 20 | max_supported_python: (u8, u8), 21 | min_supported_python: (u8, u8), 22 | ) { 23 | collapse_sub_tables(tables, "project"); 24 | let table_element = tables.get("project"); 25 | if table_element.is_none() { 26 | return; 27 | } 28 | let table = &mut table_element.unwrap().first().unwrap().borrow_mut(); 29 | let re = Regex::new(r" \.(\W)").unwrap(); 30 | expand_entry_points_inline_tables(table); 31 | for_entries(table, &mut |key, entry| match key.split('.').next().unwrap() { 32 | "name" => { 33 | update_content(entry, get_canonic_requirement_name); 34 | } 35 | "version" | "readme" | "license-files" | "scripts" | "entry-points" | "gui-scripts" => { 36 | update_content(entry, |s| String::from(s)); 37 | } 38 | "description" => { 39 | update_content(entry, |s| { 40 | re.replace_all( 41 | &s.trim() 42 | .lines() 43 | .map(|part| { 44 | part.split_whitespace() 45 | .filter(|part| !part.trim().is_empty()) 46 | .collect::>() 47 | .join(" ") 48 | }) 49 | .collect::>() 50 | .join(" "), 51 | ".$1", 52 | ) 53 | .to_string() 54 | }); 55 | } 56 | "requires-python" => { 57 | update_content(entry, |s| s.split_whitespace().collect()); 58 | } 59 | "dependencies" | "optional-dependencies" => { 60 | transform(entry, &|s| format_requirement(s, keep_full_version)); 61 | sort(entry, |e| { 62 | get_canonic_requirement_name(e).to_lowercase() + " " + &format_requirement(e, keep_full_version) 63 | }); 64 | } 65 | "dynamic" | "keywords" => { 66 | transform(entry, &|s| String::from(s)); 67 | sort(entry, str::to_lowercase); 68 | } 69 | "classifiers" => { 70 | transform(entry, &|s| String::from(s)); 71 | sort(entry, str::to_lowercase); 72 | } 73 | _ => {} 74 | }); 75 | 76 | generate_classifiers(table, max_supported_python, min_supported_python); 77 | for_entries(table, &mut |key, entry| { 78 | if key.as_str() == "classifiers" { 79 | sort(entry, str::to_lowercase); 80 | } 81 | }); 82 | reorder_table_keys( 83 | table, 84 | &[ 85 | "", 86 | "name", 87 | "version", 88 | "description", 89 | "readme", 90 | "keywords", 91 | "license", 92 | "license-files", 93 | "maintainers", 94 | "authors", 95 | "requires-python", 96 | "classifiers", 97 | "dynamic", 98 | "dependencies", 99 | // these go at the end as they may be inline or exploded 100 | "optional-dependencies", 101 | "urls", 102 | "scripts", 103 | "gui-scripts", 104 | "entry-points", 105 | ], 106 | ); 107 | } 108 | 109 | fn expand_entry_points_inline_tables(table: &mut RefMut>) { 110 | let (mut to_insert, mut count, mut key) = (Vec::::new(), 0, String::new()); 111 | for s_table_entry in table.iter() { 112 | count += 1; 113 | if s_table_entry.kind() == ENTRY { 114 | let mut has_inline_table = false; 115 | for s_in_table in s_table_entry.as_node().unwrap().children_with_tokens() { 116 | if s_in_table.kind() == KEY { 117 | key = s_in_table.as_node().unwrap().text().to_string().trim().to_string(); 118 | } else if key.starts_with("entry-points.") && s_in_table.kind() == VALUE { 119 | for s_in_value in s_in_table.as_node().unwrap().children_with_tokens() { 120 | if s_in_value.kind() == INLINE_TABLE { 121 | has_inline_table = true; 122 | for s_in_inline_table in s_in_value.as_node().unwrap().children_with_tokens() { 123 | if s_in_inline_table.kind() == ENTRY { 124 | let mut with_key = String::new(); 125 | for s_in_entry in s_in_inline_table.as_node().unwrap().children_with_tokens() { 126 | if s_in_entry.kind() == KEY { 127 | for s_in_key in s_in_entry.as_node().unwrap().children_with_tokens() { 128 | if s_in_key.kind() == IDENT { 129 | with_key = load_text(s_in_key.as_token().unwrap().text(), IDENT); 130 | with_key = String::from(with_key.strip_quotes()); 131 | break; 132 | } 133 | } 134 | } else if s_in_entry.kind() == VALUE { 135 | for s_in_b_value in s_in_entry.as_node().unwrap().children_with_tokens() { 136 | if s_in_b_value.kind() == STRING { 137 | let value = 138 | load_text(s_in_b_value.as_token().unwrap().text(), STRING); 139 | if to_insert.last().unwrap().kind() != NEWLINE { 140 | to_insert.push(make_newline()); 141 | } 142 | let new_key = format!("{key}.{with_key}"); 143 | let got = make_entry_of_string(&new_key, &value); 144 | to_insert.push(got); 145 | break; 146 | } 147 | } 148 | } 149 | } 150 | } 151 | } 152 | } 153 | } 154 | } 155 | } 156 | if !has_inline_table { 157 | to_insert.push(s_table_entry.clone()); 158 | } 159 | } else { 160 | to_insert.push(s_table_entry.clone()); 161 | } 162 | } 163 | table.splice(0..count, to_insert); 164 | } 165 | 166 | fn generate_classifiers( 167 | table: &mut RefMut>, 168 | max_supported_python: (u8, u8), 169 | min_supported_python: (u8, u8), 170 | ) { 171 | let (min, max, omit, classifiers) = 172 | get_python_requires_with_classifier(table, max_supported_python, min_supported_python); 173 | match classifiers { 174 | None => { 175 | let entry = make_array("classifiers"); 176 | generate_classifiers_to_entry(entry.as_node().unwrap(), min, max, &omit, &HashSet::new()); 177 | table.push(entry); 178 | } 179 | Some(c) => { 180 | let mut key_value = String::new(); 181 | for table_row in table.iter() { 182 | if table_row.kind() == ENTRY { 183 | for entry in table_row.as_node().unwrap().children_with_tokens() { 184 | if entry.kind() == KEY { 185 | key_value = entry.as_node().unwrap().text().to_string().trim().to_string(); 186 | } else if entry.kind() == VALUE && key_value == "classifiers" { 187 | generate_classifiers_to_entry(table_row.as_node().unwrap(), min, max, &omit, &c); 188 | } 189 | } 190 | } 191 | } 192 | } 193 | }; 194 | } 195 | 196 | fn generate_classifiers_to_entry( 197 | node: &SyntaxNode, 198 | min: (u8, u8), 199 | max: (u8, u8), 200 | omit: &[u8], 201 | existing: &HashSet, 202 | ) { 203 | for array in node.children_with_tokens() { 204 | if array.kind() == VALUE { 205 | for root_value in array.as_node().unwrap().children_with_tokens() { 206 | if root_value.kind() == ARRAY { 207 | let mut must_have: HashSet = HashSet::new(); 208 | must_have.insert(String::from("Programming Language :: Python :: 3 :: Only")); 209 | must_have.extend( 210 | (min.1..=max.1) 211 | .filter(|i| !omit.contains(i)) 212 | .map(|i| format!("Programming Language :: Python :: 3.{i}")), 213 | ); 214 | 215 | let mut count = 0; 216 | let delete = existing 217 | .iter() 218 | .filter(|e| e.starts_with("Programming Language :: Python :: 3") && !must_have.contains(*e)) 219 | .collect::>(); 220 | let mut to_insert = Vec::::new(); 221 | let mut delete_mode = false; 222 | for array_entry in root_value.as_node().unwrap().children_with_tokens() { 223 | count += 1; 224 | let kind = array_entry.kind(); 225 | if delete_mode & [NEWLINE, BRACKET_END].contains(&kind) { 226 | delete_mode = false; 227 | if kind == NEWLINE { 228 | continue; 229 | } 230 | } else if kind == VALUE { 231 | for array_entry_value in array_entry.as_node().unwrap().children_with_tokens() { 232 | if array_entry_value.kind() == STRING { 233 | let txt = load_text(array_entry_value.as_token().unwrap().text(), STRING); 234 | delete_mode = delete.contains(&txt); 235 | if delete_mode { 236 | // delete from previous comma/start until next newline 237 | let mut remove_count = to_insert.len(); 238 | for (at, v) in to_insert.iter().rev().enumerate() { 239 | if [COMMA, BRACKET_START].contains(&v.kind()) { 240 | remove_count = at; 241 | for (i, e) in to_insert.iter().enumerate().skip(to_insert.len() - at) { 242 | if e.kind() == NEWLINE { 243 | remove_count = i + 1; 244 | break; 245 | } 246 | } 247 | break; 248 | } 249 | } 250 | to_insert.truncate(remove_count); 251 | } 252 | break; 253 | } 254 | } 255 | } 256 | if !delete_mode { 257 | to_insert.push(array_entry); 258 | } 259 | } 260 | let to_add: HashSet<_> = must_have.difference(existing).collect(); 261 | if !to_add.is_empty() { 262 | // make sure we have a comma 263 | let mut trail_at = 0; 264 | for (at, v) in to_insert.iter().rev().enumerate() { 265 | trail_at = to_insert.len() - at; 266 | if v.kind() == COMMA { 267 | for (i, e) in to_insert.iter().enumerate().skip(trail_at) { 268 | if e.kind() == NEWLINE || e.kind() == BRACKET_END { 269 | trail_at = i; 270 | break; 271 | } 272 | } 273 | break; 274 | } else if v.kind() == BRACKET_START { 275 | break; 276 | } else if v.kind() == VALUE { 277 | to_insert.insert(trail_at, make_comma()); 278 | trail_at += 1; 279 | break; 280 | } 281 | } 282 | let trail = to_insert.split_off(trail_at); 283 | for add in to_add { 284 | to_insert.push(make_array_entry(add)); 285 | to_insert.push(make_comma()); 286 | } 287 | to_insert.extend(trail); 288 | } 289 | root_value.as_node().unwrap().splice_children(0..count, to_insert); 290 | } 291 | } 292 | } 293 | } 294 | } 295 | 296 | type MaxMinPythonWithClassifier = ((u8, u8), (u8, u8), Vec, Option>); 297 | 298 | fn get_python_requires_with_classifier( 299 | table: &[SyntaxElement], 300 | max_supported_python: (u8, u8), 301 | min_supported_python: (u8, u8), 302 | ) -> MaxMinPythonWithClassifier { 303 | let mut classifiers: Option> = None; 304 | let mut mins: Vec = vec![]; 305 | let mut maxs: Vec = vec![]; 306 | let mut omit: Vec = vec![]; 307 | assert_eq!(max_supported_python.0, 3, "for now only Python 3 supported"); 308 | assert_eq!(min_supported_python.0, 3, "for now only Python 3 supported"); 309 | 310 | for_entries(table, &mut |key, entry| { 311 | if key == "requires-python" { 312 | for child in entry.children_with_tokens() { 313 | if child.kind() == STRING { 314 | let found_str_value = load_text(child.as_token().unwrap().text(), STRING); 315 | let re = Regex::new(r"^(?<|<=|==|!=|>=|>)3[.](?\d+)").unwrap(); 316 | for part in found_str_value.split(',') { 317 | let capture = re.captures(part); 318 | if capture.is_some() { 319 | let caps = capture.unwrap(); 320 | let minor = caps["minor"].parse::().unwrap(); 321 | match &caps["op"] { 322 | "==" => { 323 | mins.push(minor); 324 | maxs.push(minor); 325 | } 326 | ">=" => { 327 | mins.push(minor); 328 | } 329 | ">" => { 330 | mins.push(minor + 1); 331 | } 332 | "<=" => { 333 | maxs.push(minor); 334 | } 335 | "<" => { 336 | maxs.push(minor - 1); 337 | } 338 | "!=" => { 339 | omit.push(minor); 340 | } 341 | _ => {} 342 | } 343 | } 344 | } 345 | } 346 | } 347 | } else if key == "classifiers" { 348 | for child in entry.children_with_tokens() { 349 | if child.kind() == ARRAY { 350 | let mut found_elements = HashSet::::new(); 351 | for array in child.as_node().unwrap().children_with_tokens() { 352 | if array.kind() == VALUE { 353 | for value in array.as_node().unwrap().children_with_tokens() { 354 | if value.kind() == STRING { 355 | let found = value.as_token().unwrap().text(); 356 | let found_str_value: String = String::from(&found[1..found.len() - 1]); 357 | found_elements.insert(found_str_value); 358 | } 359 | } 360 | } 361 | } 362 | classifiers = Some(found_elements); 363 | } 364 | } 365 | } 366 | }); 367 | let min_py = (3, *mins.iter().max().unwrap_or(&min_supported_python.1)); 368 | let max_py = (3, *maxs.iter().min().unwrap_or(&max_supported_python.1)); 369 | (min_py, max_py, omit, classifiers) 370 | } 371 | 372 | #[cfg(test)] 373 | mod tests { 374 | use indoc::indoc; 375 | use rstest::rstest; 376 | use taplo::formatter::{format_syntax, Options}; 377 | use taplo::parser::parse; 378 | use taplo::syntax::SyntaxElement; 379 | 380 | use crate::helpers::table::Tables; 381 | use crate::project::fix; 382 | 383 | fn evaluate(start: &str, keep_full_version: bool, max_supported_python: (u8, u8)) -> String { 384 | let root_ast = parse(start).into_syntax().clone_for_update(); 385 | let count = root_ast.children_with_tokens().count(); 386 | let mut tables = Tables::from_ast(&root_ast); 387 | fix(&mut tables, keep_full_version, max_supported_python, (3, 9)); 388 | let entries = tables 389 | .table_set 390 | .iter() 391 | .flat_map(|e| e.borrow().clone()) 392 | .collect::>(); 393 | root_ast.splice_children(0..count, entries); 394 | let opt = Options { 395 | column_width: 1, 396 | ..Options::default() 397 | }; 398 | format_syntax(root_ast, opt) 399 | } 400 | 401 | #[rstest] 402 | #[case::no_project( 403 | indoc ! {r""}, 404 | "\n", 405 | false, 406 | (3, 9), 407 | )] 408 | #[case::project_requires_no_keep( 409 | indoc ! {r#" 410 | [project] 411 | dependencies=["a>=1.0.0", "b.c>=1.5.0"] 412 | "#}, 413 | indoc ! {r#" 414 | [project] 415 | classifiers = [ 416 | "Programming Language :: Python :: 3 :: Only", 417 | "Programming Language :: Python :: 3.9", 418 | ] 419 | dependencies = [ 420 | "a>=1", 421 | "b-c>=1.5", 422 | ] 423 | "#}, 424 | false, 425 | (3, 9), 426 | )] 427 | #[case::project_requires_keep( 428 | indoc ! {r#" 429 | [project] 430 | dependencies=["a>=1.0.0", "b.c>=1.5.0"] 431 | "#}, 432 | indoc ! {r#" 433 | [project] 434 | classifiers = [ 435 | "Programming Language :: Python :: 3 :: Only", 436 | "Programming Language :: Python :: 3.9", 437 | ] 438 | dependencies = [ 439 | "a>=1.0.0", 440 | "b-c>=1.5.0", 441 | ] 442 | "#}, 443 | true, 444 | (3, 9), 445 | )] 446 | #[case::project_requires_ge( 447 | indoc ! {r#" 448 | [project] 449 | requires-python = " >= 3.9" 450 | classifiers = [ 451 | # comment license inline 1 452 | # comment license inline 2 453 | "License :: OSI Approved :: MIT License", # comment license post 454 | # comment 3.12 inline 1 455 | # comment 3.12 inline 2 456 | "Programming Language :: Python :: 3.12", # comment 3.12 post 457 | # comment 3.10 inline 458 | "Programming Language :: Python :: 3.10" # comment 3.10 post 459 | # extra 1 460 | # extra 2 461 | # extra 3 462 | ] 463 | "#}, 464 | indoc ! {r#" 465 | [project] 466 | requires-python = ">=3.9" 467 | classifiers = [ 468 | # comment license inline 1 469 | # comment license inline 2 470 | "License :: OSI Approved :: MIT License", # comment license post 471 | "Programming Language :: Python :: 3 :: Only", 472 | "Programming Language :: Python :: 3.9", 473 | # comment 3.10 inline 474 | "Programming Language :: Python :: 3.10", # comment 3.10 post 475 | # extra 1 476 | # extra 2 477 | # extra 3 478 | ] 479 | "#}, 480 | true, 481 | (3, 10), 482 | )] 483 | #[case::project_requires_gt( 484 | indoc ! {r#" 485 | [project] 486 | requires-python = " > 3.8" 487 | "#}, 488 | indoc ! {r#" 489 | [project] 490 | requires-python = ">3.8" 491 | classifiers = [ 492 | "Programming Language :: Python :: 3 :: Only", 493 | "Programming Language :: Python :: 3.9", 494 | ] 495 | "#}, 496 | true, 497 | (3, 9), 498 | )] 499 | #[case::project_requires_eq( 500 | indoc ! {r#" 501 | [project] 502 | requires-python = " == 3.12" 503 | "#}, 504 | indoc ! {r#" 505 | [project] 506 | requires-python = "==3.12" 507 | classifiers = [ 508 | "Programming Language :: Python :: 3 :: Only", 509 | "Programming Language :: Python :: 3.12", 510 | ] 511 | "#}, 512 | true, 513 | (3, 9), 514 | )] 515 | #[case::project_sort_keywords( 516 | indoc ! {r#" 517 | [project] 518 | keywords = ["b", "A", "a-c", " c"] 519 | "#}, 520 | indoc ! {r#" 521 | [project] 522 | keywords = [ 523 | " c", 524 | "A", 525 | "a-c", 526 | "b", 527 | ] 528 | classifiers = [ 529 | "Programming Language :: Python :: 3 :: Only", 530 | "Programming Language :: Python :: 3.9", 531 | ] 532 | "#}, 533 | true, 534 | (3, 9), 535 | )] 536 | #[case::project_sort_dynamic( 537 | indoc ! {r#" 538 | [project] 539 | dynamic = ["b", "A", "a-c", " c", "a10", "a2"] 540 | "#}, 541 | indoc ! {r#" 542 | [project] 543 | classifiers = [ 544 | "Programming Language :: Python :: 3 :: Only", 545 | "Programming Language :: Python :: 3.9", 546 | ] 547 | dynamic = [ 548 | " c", 549 | "A", 550 | "a-c", 551 | "a2", 552 | "a10", 553 | "b", 554 | ] 555 | "#}, 556 | true, 557 | (3, 9), 558 | )] 559 | #[case::project_name_norm( 560 | indoc ! {r#" 561 | [project] 562 | name = "a.b.c" 563 | "#}, 564 | indoc ! {r#" 565 | [project] 566 | name = "a-b-c" 567 | classifiers = [ 568 | "Programming Language :: Python :: 3 :: Only", 569 | "Programming Language :: Python :: 3.9", 570 | ] 571 | "#}, 572 | true, 573 | (3, 9), 574 | )] 575 | #[case::project_name_literal( 576 | indoc ! {r" 577 | [project] 578 | name = 'a.b.c' 579 | "}, 580 | indoc ! {r#" 581 | [project] 582 | name = "a-b-c" 583 | classifiers = [ 584 | "Programming Language :: Python :: 3 :: Only", 585 | "Programming Language :: Python :: 3.9", 586 | ] 587 | "#}, 588 | true, 589 | (3, 9), 590 | )] 591 | #[case::project_requires_gt_old( 592 | indoc ! {r#" 593 | [project] 594 | requires-python = " > 3.7" 595 | "#}, 596 | indoc ! {r#" 597 | [project] 598 | requires-python = ">3.7" 599 | classifiers = [ 600 | "Programming Language :: Python :: 3 :: Only", 601 | "Programming Language :: Python :: 3.8", 602 | "Programming Language :: Python :: 3.9", 603 | ] 604 | "#}, 605 | true, 606 | (3, 9), 607 | )] 608 | #[case::project_requires_range( 609 | indoc ! {r#" 610 | [project] 611 | requires-python=">=3.7,<3.13" 612 | "#}, 613 | indoc ! {r#" 614 | [project] 615 | requires-python = ">=3.7,<3.13" 616 | classifiers = [ 617 | "Programming Language :: Python :: 3 :: Only", 618 | "Programming Language :: Python :: 3.7", 619 | "Programming Language :: Python :: 3.8", 620 | "Programming Language :: Python :: 3.9", 621 | "Programming Language :: Python :: 3.10", 622 | "Programming Language :: Python :: 3.11", 623 | "Programming Language :: Python :: 3.12", 624 | ] 625 | "#}, 626 | true, 627 | (3, 9), 628 | )] 629 | #[case::project_requires_high_range( 630 | indoc ! {r#" 631 | [project] 632 | requires-python = "<=3.13,>3.10" 633 | "#}, 634 | indoc ! {r#" 635 | [project] 636 | requires-python = "<=3.13,>3.10" 637 | classifiers = [ 638 | "Programming Language :: Python :: 3 :: Only", 639 | "Programming Language :: Python :: 3.11", 640 | "Programming Language :: Python :: 3.12", 641 | "Programming Language :: Python :: 3.13", 642 | ] 643 | "#}, 644 | true, 645 | (3, 9), 646 | )] 647 | #[case::project_requires_range_neq( 648 | indoc ! {r#" 649 | [project] 650 | requires-python = "<=3.10,!=3.9,>=3.8" 651 | "#}, 652 | indoc ! {r#" 653 | [project] 654 | requires-python = "<=3.10,!=3.9,>=3.8" 655 | classifiers = [ 656 | "Programming Language :: Python :: 3 :: Only", 657 | "Programming Language :: Python :: 3.8", 658 | "Programming Language :: Python :: 3.10", 659 | ] 660 | "#}, 661 | true, 662 | (3, 13), 663 | )] 664 | #[case::project_description_whitespace( 665 | "[project]\ndescription = ' A magic stuff \t is great\t\t.\r\n Like really . Works on .rst and .NET :)\t\'\nrequires-python = '==3.12'", 666 | indoc ! {r#" 667 | [project] 668 | description = "A magic stuff is great. Like really. Works on .rst and .NET :)" 669 | requires-python = "==3.12" 670 | classifiers = [ 671 | "Programming Language :: Python :: 3 :: Only", 672 | "Programming Language :: Python :: 3.12", 673 | ] 674 | "#}, 675 | true, 676 | (3, 13), 677 | )] 678 | #[case::project_description_multiline( 679 | indoc ! {r#" 680 | [project] 681 | requires-python = "==3.12" 682 | description = """ 683 | A magic stuff is great. 684 | Like really. 685 | """ 686 | "#}, 687 | indoc ! {r#" 688 | [project] 689 | description = "A magic stuff is great. Like really." 690 | requires-python = "==3.12" 691 | classifiers = [ 692 | "Programming Language :: Python :: 3 :: Only", 693 | "Programming Language :: Python :: 3.12", 694 | ] 695 | "#}, 696 | true, 697 | (3, 13), 698 | )] 699 | #[case::project_dependencies_with_double_quotes( 700 | indoc ! {r#" 701 | [project] 702 | dependencies = [ 703 | 'packaging>=20.0;python_version>"3.4"', 704 | "appdirs" 705 | ] 706 | requires-python = "==3.12" 707 | "#}, 708 | indoc ! {r#" 709 | [project] 710 | requires-python = "==3.12" 711 | classifiers = [ 712 | "Programming Language :: Python :: 3 :: Only", 713 | "Programming Language :: Python :: 3.12", 714 | ] 715 | dependencies = [ 716 | "appdirs", 717 | "packaging>=20.0; python_version>'3.4'", 718 | ] 719 | "#}, 720 | true, 721 | (3, 13), 722 | )] 723 | #[case::project_platform_dependencies( 724 | indoc ! {r#" 725 | [project] 726 | dependencies = [ 727 | 'pyperclip; platform_system == "Darwin"', 728 | 'pyperclip; platform_system == "Windows"', 729 | "appdirs" 730 | ] 731 | requires-python = "==3.12" 732 | "#}, 733 | indoc ! {r#" 734 | [project] 735 | requires-python = "==3.12" 736 | classifiers = [ 737 | "Programming Language :: Python :: 3 :: Only", 738 | "Programming Language :: Python :: 3.12", 739 | ] 740 | dependencies = [ 741 | "appdirs", 742 | "pyperclip; platform_system=='Darwin'", 743 | "pyperclip; platform_system=='Windows'", 744 | ] 745 | "#}, 746 | true, 747 | (3, 13), 748 | )] 749 | #[case::project_opt_inline_dependencies( 750 | indoc ! {r#" 751 | [project] 752 | dependencies = ["packaging>=24"] 753 | optional-dependencies.test = ["pytest>=8.1.1", "covdefaults>=2.3"] 754 | optional-dependencies.docs = ["sphinx-argparse-cli>=1.15", "Sphinx>=7.3.7"] 755 | requires-python = "==3.12" 756 | "#}, 757 | indoc ! {r#" 758 | [project] 759 | requires-python = "==3.12" 760 | classifiers = [ 761 | "Programming Language :: Python :: 3 :: Only", 762 | "Programming Language :: Python :: 3.12", 763 | ] 764 | dependencies = [ 765 | "packaging>=24", 766 | ] 767 | optional-dependencies.docs = [ 768 | "sphinx>=7.3.7", 769 | "sphinx-argparse-cli>=1.15", 770 | ] 771 | optional-dependencies.test = [ 772 | "covdefaults>=2.3", 773 | "pytest>=8.1.1", 774 | ] 775 | "#}, 776 | true, 777 | (3, 13), 778 | )] 779 | #[case::project_opt_dependencies( 780 | indoc ! {r#" 781 | [project.optional-dependencies] 782 | test = ["pytest>=8.1.1", "covdefaults>=2.3"] 783 | docs = ["sphinx-argparse-cli>=1.15", "Sphinx>=7.3.7"] 784 | "#}, 785 | indoc ! {r#" 786 | [project] 787 | classifiers = [ 788 | "Programming Language :: Python :: 3 :: Only", 789 | "Programming Language :: Python :: 3.9", 790 | ] 791 | optional-dependencies.docs = [ 792 | "sphinx>=7.3.7", 793 | "sphinx-argparse-cli>=1.15", 794 | ] 795 | optional-dependencies.test = [ 796 | "covdefaults>=2.3", 797 | "pytest>=8.1.1", 798 | ] 799 | "#}, 800 | true, 801 | (3, 9), 802 | )] 803 | #[case::project_scripts_collapse( 804 | indoc ! {r#" 805 | [project.scripts] 806 | c = 'd' 807 | a = "b" 808 | "#}, 809 | indoc ! {r#" 810 | [project] 811 | classifiers = [ 812 | "Programming Language :: Python :: 3 :: Only", 813 | "Programming Language :: Python :: 3.9", 814 | ] 815 | scripts.a = "b" 816 | scripts.c = "d" 817 | "#}, 818 | true, 819 | (3, 9), 820 | )] 821 | #[case::project_entry_points_collapse( 822 | indoc ! {r#" 823 | [project] 824 | entry-points.tox = {"tox-uv" = "tox_uv.plugin", "tox" = "tox.plugin"} 825 | [project.scripts] 826 | virtualenv = "virtualenv.__main__:run_with_catch" 827 | [project.gui-scripts] 828 | hello-world = "timmins:hello_world" 829 | [project.entry-points."virtualenv.activate"] 830 | bash = "virtualenv.activation.bash:BashActivator" 831 | [project.entry-points] 832 | B = {base = "vehicle_crash_prevention.main:VehicleBase"} 833 | [project.entry-points."no_crashes.vehicle"] 834 | base = "vehicle_crash_prevention.main:VehicleBase" 835 | [project.entry-points.plugin-namespace] 836 | plugin-name1 = "pkg.subpkg1" 837 | plugin-name2 = "pkg.subpkg2:func" 838 | "#}, 839 | indoc ! {r#" 840 | [project] 841 | classifiers = [ 842 | "Programming Language :: Python :: 3 :: Only", 843 | "Programming Language :: Python :: 3.9", 844 | ] 845 | scripts.virtualenv = "virtualenv.__main__:run_with_catch" 846 | gui-scripts.hello-world = "timmins:hello_world" 847 | entry-points.B.base = "vehicle_crash_prevention.main:VehicleBase" 848 | entry-points."no_crashes.vehicle".base = "vehicle_crash_prevention.main:VehicleBase" 849 | entry-points.plugin-namespace.plugin-name1 = "pkg.subpkg1" 850 | entry-points.plugin-namespace.plugin-name2 = "pkg.subpkg2:func" 851 | entry-points.tox.tox = "tox.plugin" 852 | entry-points.tox.tox-uv = "tox_uv.plugin" 853 | entry-points."virtualenv.activate".bash = "virtualenv.activation.bash:BashActivator" 854 | "#}, 855 | true, 856 | (3, 9), 857 | )] 858 | #[case::project_preserve_implementation_classifiers( 859 | indoc ! {r#" 860 | [project] 861 | requires-python = ">=3.8" 862 | classifiers = [ 863 | "License :: OSI Approved :: MIT License", 864 | "Topic :: Software Development :: Libraries :: Python Modules", 865 | "Programming Language :: Python :: Implementation :: CPython", 866 | "Programming Language :: Python :: Implementation :: PyPy", 867 | ] 868 | "#}, 869 | indoc ! {r#" 870 | [project] 871 | requires-python = ">=3.8" 872 | classifiers = [ 873 | "License :: OSI Approved :: MIT License", 874 | "Programming Language :: Python :: 3 :: Only", 875 | "Programming Language :: Python :: 3.8", 876 | "Programming Language :: Python :: 3.9", 877 | "Programming Language :: Python :: 3.10", 878 | "Programming Language :: Python :: Implementation :: CPython", 879 | "Programming Language :: Python :: Implementation :: PyPy", 880 | "Topic :: Software Development :: Libraries :: Python Modules", 881 | ] 882 | "#}, 883 | true, 884 | (3, 10), 885 | )] 886 | fn test_format_project( 887 | #[case] start: &str, 888 | #[case] expected: &str, 889 | #[case] keep_full_version: bool, 890 | #[case] max_supported_python: (u8, u8), 891 | ) { 892 | assert_eq!(evaluate(start, keep_full_version, max_supported_python), expected); 893 | } 894 | } 895 | --------------------------------------------------------------------------------