diff --git a/.github/workflows/common.yaml b/.github/workflows/common.yaml index 8be2ff2..3783ec3 100644 --- a/.github/workflows/common.yaml +++ b/.github/workflows/common.yaml @@ -3,7 +3,9 @@ on: workflow_dispatch: push: branches: ["main"] + paths: ["common/**", ".github/workflows/common.yaml"] pull_request: + paths: ["common/**", ".github/workflows/common.yaml"] concurrency: group: ${{ github.workflow }}-${{ github.ref }} diff --git a/.github/workflows/pyproject_fmt_build.yaml b/.github/workflows/pyproject_fmt_build.yaml index 2dc8191..11c3873 100644 --- a/.github/workflows/pyproject_fmt_build.yaml +++ b/.github/workflows/pyproject_fmt_build.yaml @@ -14,7 +14,9 @@ on: - major push: branches: ["main"] + paths: ["common/**", "pyproject-fmt/**", ".github/workflows/pyproject_fmt_build.yaml"] pull_request: + paths: ["common/**", "pyproject-fmt/**", ".github/workflows/pyproject_fmt_build.yaml"] schedule: - cron: "0 8 * * *" diff --git a/.github/workflows/pyproject_fmt_test.yaml b/.github/workflows/pyproject_fmt_test.yaml index 1a923ee..13075c1 100644 --- a/.github/workflows/pyproject_fmt_test.yaml +++ b/.github/workflows/pyproject_fmt_test.yaml @@ -3,7 +3,9 @@ on: workflow_dispatch: push: branches: ["main"] + paths: ["common/**", "pyproject-fmt/**", ".github/workflows/pyproject_fmt_test.yaml"] pull_request: + paths: ["common/**", "pyproject-fmt/**", ".github/workflows/pyproject_fmt_test.yaml"] concurrency: group: ${{ github.workflow }}-${{ github.ref }} diff --git a/.github/workflows/tox_toml_fmt_build.yaml b/.github/workflows/tox_toml_fmt_build.yaml new file mode 100644 index 0000000..94c987d --- /dev/null +++ b/.github/workflows/tox_toml_fmt_build.yaml @@ -0,0 +1,245 @@ +name: Build tox-toml-fmt +on: + workflow_dispatch: + inputs: + release: + description: "Cut a release (select semver bump)?" + required: true + default: "no" + type: choice + options: + - "no" + - patch + - minor + - major + push: + branches: ["main"] + paths: ["common/**", "tox-toml-fmt/**", ".github/workflows/tox_toml_fmt_build.yaml"] + pull_request: + paths: ["common/**", "tox-toml-fmt/**", ".github/workflows/tox_toml_fmt_build.yaml"] + schedule: + - cron: "0 8 * * *" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event.inputs.release == 'no' || github.event.inputs.release == null }} + +permissions: + contents: read + +jobs: + bump: + runs-on: ubuntu-latest + permissions: + pull-requests: read + outputs: + version: ${{ steps.get-version.outputs.version }} + changelog: ${{ steps.get-version.outputs.changelog }} + steps: + - uses: actions-rust-lang/setup-rust-toolchain@v1 + - name: Install cargo-edit from crates.io + uses: baptiste0928/cargo-install@v3 + with: + crate: cargo-edit + - uses: actions/checkout@v4 + - name: Bump version + run: cargo set-version -p tox-toml-fmt --bump '${{ github.event.inputs.release == 'no' || github.event.inputs.release == null && 'patch' || github.event.inputs.release }}' + - name: Install the latest version of uv + uses: astral-sh/setup-uv@v3 + with: + enable-cache: true + cache-dependency-glob: "tasks/changelog.py" + - name: Generate changelog + id: get-version + run: uv run tasks/changelog.py tox-toml-fmt "${{ github.event.number }}" "${{ github.event.pull_request.base.sha }}" + env: + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - uses: pre-commit/action@v3.0.1 + continue-on-error: true + - name: Show changes to the repository + run: git diff HEAD -u + - name: Store the patched distribution + uses: actions/upload-artifact@v4 + with: + name: source + path: . + compression-level: 9 + retention-days: 1 + if-no-files-found: "error" + + linux: + needs: [bump] + runs-on: ${{ matrix.platform.runner }} + strategy: + fail-fast: false + matrix: + platform: + - runner: ubuntu-latest + target: x86_64 + interpreter: "3.8 pypy3.8 pypy3.9 pypy3.10" + - runner: ubuntu-latest + target: x86 + - runner: ubuntu-latest + target: x86_64-unknown-linux-musl + manylinux: musllinux_1_1 + - runner: ubuntu-latest + target: i686-unknown-linux-musl + manylinux: musllinux_1_1 + - runner: ubuntu-latest + target: aarch64 + - runner: ubuntu-latest + target: armv7 + - runner: ubuntu-latest + target: s390x + - runner: ubuntu-latest + target: ppc64le + steps: + - name: Download the code + uses: actions/download-artifact@v4 + with: + name: source + - name: Build wheels + uses: PyO3/maturin-action@ea5bac0f1ccd0ab11c805e2b804bfcb65dac2eab + + with: + target: ${{ matrix.platform.target }} + args: -m tox-toml-fmt/Cargo.toml --release --out dist --interpreter ${{ matrix.platform.interpreter || '3.8' }} --target-dir target + sccache: "true" + manylinux: ${{ matrix.platform.manylinux || 'auto' }} + - name: Upload wheels + uses: actions/upload-artifact@v4 + with: + name: wheels-linux-${{ matrix.platform.target }} + path: dist + + windows: + needs: [bump] + runs-on: ${{ matrix.platform.runner }} + strategy: + matrix: + platform: + - runner: windows-latest + target: x64 + - runner: windows-latest + target: x86 + steps: + - name: Download the code + uses: actions/download-artifact@v4 + with: + name: source + - name: Build wheels + uses: PyO3/maturin-action@ea5bac0f1ccd0ab11c805e2b804bfcb65dac2eab + + with: + target: ${{ matrix.platform.target }} + args: -m tox-toml-fmt/Cargo.toml --release --out dist --interpreter ${{ matrix.platform.interpreter || '3.8' }} --target-dir target + sccache: "true" + - name: Upload wheels + uses: actions/upload-artifact@v4 + with: + name: wheels-windows-${{ matrix.platform.target }} + path: dist + + macos: + needs: [bump] + runs-on: ${{ matrix.platform.runner }} + strategy: + matrix: + platform: + - runner: macos-latest + target: x86_64 + - runner: macos-14 + target: aarch64 + steps: + - name: Download the code + uses: actions/download-artifact@v4 + with: + name: source + - name: Build wheels + uses: PyO3/maturin-action@ea5bac0f1ccd0ab11c805e2b804bfcb65dac2eab + + with: + target: ${{ matrix.platform.target }} + args: -m tox-toml-fmt/Cargo.toml --release --out dist --interpreter "3.8 pypy3.8 pypy3.9 pypy3.10" --target-dir target + sccache: "true" + - name: Upload wheels + uses: actions/upload-artifact@v4 + with: + name: wheels-macos-${{ matrix.platform.target }} + path: dist + + sdist: + needs: [bump] + runs-on: ubuntu-latest + steps: + - name: Download the code + uses: actions/download-artifact@v4 + with: + name: source + - name: Build sdist + uses: PyO3/maturin-action@ea5bac0f1ccd0ab11c805e2b804bfcb65dac2eab + + with: + command: sdist + args: -m tox-toml-fmt/Cargo.toml --out dist + - name: Upload sdist + uses: actions/upload-artifact@v4 + with: + name: wheels-sdist + path: dist + + release: + name: Release + runs-on: ubuntu-latest + environment: + name: release + url: https://pypi.org/project/tox-toml-fmt/${{ needs.bump.outputs.version }} + permissions: + id-token: write + contents: write + if: github.event.inputs.release != 'no' && github.event.inputs.release != null && github.ref == 'refs/heads/main' + needs: [bump, sdist, linux, macos, windows] + steps: + - uses: actions/checkout@v4 + - name: Download source + uses: actions/download-artifact@v4 + with: + name: source + path: . + - name: Show changes to the repository + run: git diff HEAD -u + - name: Commit changes + run: | + git config --global user.name 'Bernat Gabor' + git config --global user.email 'gaborbernat@users.noreply.github.com' + git commit -am "Release tox-toml-fmt ${{needs.bump.outputs.version}}" + - name: Tag release + run: git tag tox-toml-fmt/${{needs.bump.outputs.version}} + - name: Download wheels + uses: actions/download-artifact@v4 + with: + pattern: wheels-* + path: dist + merge-multiple: "true" + - name: Show wheels + run: ls -lth dist + - name: Publish to PyPI + uses: PyO3/maturin-action@ea5bac0f1ccd0ab11c805e2b804bfcb65dac2eab + with: + command: upload + args: --non-interactive --skip-existing dist/* + - name: Push release commit+tag and create GitHub release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + git push + git push --tags + gh release create "tox-toml-fmt/${{needs.bump.outputs.version}}" \ + --title="tox-toml-fmt/${{needs.bump.outputs.version}}" --verify-tag \ + --notes "$(cat << 'EOM' + ${{ needs.bump.outputs.changelog }} + EOM + )" diff --git a/.github/workflows/tox_toml_fmt_test.yaml b/.github/workflows/tox_toml_fmt_test.yaml new file mode 100644 index 0000000..d49f42f --- /dev/null +++ b/.github/workflows/tox_toml_fmt_test.yaml @@ -0,0 +1,130 @@ +name: Test tox-toml-fmt +on: + workflow_dispatch: + push: + branches: ["main"] + paths: ["common/**", "tox-toml-fmt/**", ".github/workflows/tox_toml_fmt_test.yaml"] + pull_request: + paths: ["common/**", "tox-toml-fmt/**", ".github/workflows/tox_toml_fmt_test.yaml"] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + rust-lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + components: clippy + - name: Lint rust code with clippy + run: cargo clippy -p tox-toml-fmt --all-targets -- -D warnings + + rust-fmt: + runs-on: ubuntu-latest + defaults: + run: + working-directory: tox-toml-fmt + steps: + - uses: actions/checkout@v4 + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + components: rustfmt + - name: Rust formatting check + run: cargo fmt -p tox-toml-fmt --check + + rust-build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions-rust-lang/setup-rust-toolchain@v1 + - name: Build library + run: cargo build -p tox-toml-fmt + + rust-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions-rust-lang/setup-rust-toolchain@v1 + - run: cargo test -p tox-toml-fmt + + py-test: + name: test ${{ matrix.py }} ${{ matrix.os }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + py: + - "3.13" + - "3.12" + - "3.11" + - "3.10" + - "3.9" + os: + - ubuntu-latest + - windows-latest + - macos-latest + defaults: + run: + working-directory: tox-toml-fmt + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Install the latest version of uv + uses: astral-sh/setup-uv@v3 + with: + enable-cache: true + cache-dependency-glob: "pyproject.toml" + - name: Install tox + run: uv tool install --python-preference only-managed --python 3.13 tox --with tox-uv + - name: Install Python + if: matrix.py != '3.13' + run: uv python install --python-preference only-managed ${{ matrix.env }} + - uses: moonrepo/setup-rust@v1 + with: + cache-base: main + bins: cargo-tarpaulin + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup test suite + run: tox run -vv --notest --skip-missing-interpreters false -e ${{ matrix.py }} + - name: Run test suite + run: tox run --skip-pkg-install -e ${{ matrix.py }} + env: + PYTEST_ADDOPTS: "-vv --durations=20" + + py-check: + name: tox env ${{ matrix.env }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + env: + - type + - dev + - pkg_meta + defaults: + run: + working-directory: tox-toml-fmt + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Install the latest version of uv + uses: astral-sh/setup-uv@v3 + with: + enable-cache: true + cache-dependency-glob: "pyproject.toml" + github-token: ${{ secrets.GITHUB_TOKEN }} + - name: Install tox + run: uv tool install --python-preference only-managed --python 3.13 tox --with tox-uv + - name: Setup test suite + run: tox run -vv --notest --skip-missing-interpreters false -e ${{ matrix.env }} + - name: Run test suite + run: tox run --skip-pkg-install -e ${{ matrix.env }} + env: + PYTEST_ADDOPTS: "-vv --durations=20" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9b6eff4..da3fba3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,8 +14,12 @@ repos: hooks: - id: codespell additional_dependencies: ["tomli>=2.0.1"] + - repo: https://github.com/abravalheri/validate-pyproject + rev: v0.22 + hooks: + - id: validate-pyproject - repo: https://github.com/tox-dev/pyproject-fmt - rev: "v2.4.3" + rev: "v2.5.0" hooks: - id: pyproject-fmt - repo: https://github.com/astral-sh/ruff-pre-commit diff --git a/Cargo.lock b/Cargo.lock index a737793..cbdf294 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -818,6 +818,18 @@ dependencies = [ "winnow", ] +[[package]] +name = "tox-toml-fmt" +version = "2.5.0" +dependencies = [ + "common", + "indoc", + "lexical-sort", + "pyo3", + "regex", + "rstest", +] + [[package]] name = "tracing" version = "0.1.40" diff --git a/Cargo.toml b/Cargo.toml index 1ef9677..f3c5b9e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ members = [ "common", "pyproject-fmt", + "tox-toml-fmt" ] resolver = "2" diff --git a/README.md b/README.md index 462a44e..494716f 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,6 @@ # Python TOML formatters -This project includes the: +This project includes the following TOML formatters for the Python ecosystem: -- pyproject-fmt, -- ruff-toml-fmt, -- tox-toml-fmt, - -projects formatting your TOML files in the Python world. +- [`pyproject-fmt`](./pyproject-fmt), +- [`tox-toml-fmt`](./tox-toml-fmt). diff --git a/pyproject-fmt/docs/conf.py b/pyproject-fmt/docs/conf.py index 6042ced..e2b7ee5 100644 --- a/pyproject-fmt/docs/conf.py +++ b/pyproject-fmt/docs/conf.py @@ -22,6 +22,7 @@ "sphinx.ext.intersphinx", "sphinx_argparse_cli", "sphinx_autodoc_typehints", + "sphinx_inline_tabs", "sphinx_copybutton", ] @@ -34,8 +35,7 @@ } extlinks = { - "issue": (f"https://github.com/{company}/{name}/issues/%s", "#%s"), - "user": ("https://github.com/%s", "@%s"), + "pypi": ("https://pypi.org/project/%s", "%s"), "gh": ("https://github.com/%s", "%s"), } intersphinx_mapping = {"python": ("https://docs.python.org/3", None)} diff --git a/pyproject-fmt/docs/index.rst b/pyproject-fmt/docs/index.rst index 78c395b..6c0bb15 100644 --- a/pyproject-fmt/docs/index.rst +++ b/pyproject-fmt/docs/index.rst @@ -1,16 +1,15 @@ pyproject-fmt ============= -Apply a consistent format to your ``pyproject.toml`` file with comment support. -See `changelog here `_. +Apply a consistent format to your ``pyproject.toml`` file with comment support. See +`changelog here `_. Philosophy ---------- -This tool aims to be an *opinionated formatter*, with similar objectives to -`black `_. This means it deliberately does not support -a wide variety of configuration settings. In return, you get consistency, predictability, -and smaller diffs. +This tool aims to be an *opinionated formatter*, with similar objectives to `black `_. +This means it deliberately does not support a wide variety of configuration settings. In return, you get consistency, +predictability, and smaller diffs. Use --- @@ -18,11 +17,39 @@ Use Via ``CLI`` ~~~~~~~~~~~ -Use `pipx `_ to install the project: +:pypi:`pyproject-fmt` is a CLI tool that needs a Python interpreter (version 3.7 or higher) to run. We recommend either +:pypi:`pipx` or :pypi:`uv` to install tox into an isolated environment. This has the added benefit that later you'll +be able to upgrade tox without affecting other parts of the system. We provide method for ``pip`` too here but we +discourage that path if you can: -.. code-block:: shell +.. tab:: uv - pipx install pyproject-fmt + .. code-block:: bash + + # install uv per https://docs.astral.sh/uv/#getting-started + uv tool install pyproject-fmt + pyproject-fmt --help + + +.. tab:: pipx + + .. code-block:: bash + + python -m pip install pipx-in-pipx --user + pipx install pyproject-fmt + pyproject-fmt --help + +.. tab:: pip + + .. code-block:: bash + + python -m pip install --user pyproject-fmt + pyproject-fmt --help + + You can install it within the global Python interpreter itself (perhaps as a user package via the + ``--user`` flag). Be cautious if you are using a Python installation that is managed by your operating system or + another package manager. ``pip`` might not coordinate with those tools, and may leave your system in an inconsistent + state. Note, if you go down this path you need to ensure pip is new enough per the subsections below Via ``pre-commit`` hook @@ -33,7 +60,7 @@ See :gh:`pre-commit/pre-commit` for instructions, sample ``.pre-commit-config.ya .. code-block:: yaml - repo: https://github.com/tox-dev/pyproject-fmt - rev: "2.0.4" + rev: "v2.5.0" hooks: - id: pyproject-fmt diff --git a/pyproject-fmt/pyproject.toml b/pyproject-fmt/pyproject.toml index b9f5468..410238f 100644 --- a/pyproject-fmt/pyproject.toml +++ b/pyproject-fmt/pyproject.toml @@ -40,6 +40,42 @@ urls.Documentation = "https://github.com/tox-dev/toml-fmt/" urls."Source Code" = "https://github.com/tox-dev/toml-fmt" scripts.pyproject-fmt = "pyproject_fmt.__main__:runner" +[dependency-groups] +dev = [ + { include-group = "docs" }, + { include-group = "fix" }, + { include-group = "test" }, + { include-group = "type" }, +] +test = [ + "covdefaults>=2.3", + "pytest>=8.3.2", + "pytest-cov>=5", + "pytest-mock>=3.14", +] +type = [ + "mypy==1.11.2", + "types-cachetools>=5.5.0.20240820", + "types-chardet>=5.0.4.6", + { include-group = "test" }, +] +docs = [ + "furo>=2024.8.6", + "sphinx>=8.0.2", + "sphinx-argparse-cli>=1.18.2", + "sphinx-autodoc-typehints>=2.4.4", + "sphinx-copybutton>=0.5.2", + "sphinx-inline-tabs>=2023.4.21", +] +fix = [ + "pre-commit-uv>=4.1.3", +] +pkg-meta = [ + "check-wheel-contents>=0.6", + "twine>=5.1.1", + "uv>=0.4.17", +] + [tool.maturin] bindings = "pyo3" manifest-path = "Cargo.toml" @@ -85,43 +121,3 @@ run.plugins = [ [tool.mypy] show_error_codes = true strict = true - -[dependency-groups] -dev = [ - { include-group = "docs" }, - { include-group = "fix" }, - { include-group = "tasks" }, - { include-group = "test" }, - { include-group = "type" }, -] -docs = [ - "furo>=2024.8.6", - "sphinx>=8.0.2", - "sphinx-argparse-cli>=1.18.2", - "sphinx-autodoc-typehints>=2.4.4", - "sphinx-copybutton>=0.5.2", -] -fix = [ - "pre-commit-uv>=4.1.3", -] -pkg-meta = [ - "check-wheel-contents>=0.6", - "twine>=5.1.1", - "uv>=0.4.17", -] -tasks = [ - "gitpython>=3.1.43", - "pygithub>=2.4", -] -test = [ - "covdefaults>=2.3", - "pytest>=8.3.2", - "pytest-cov>=5", - "pytest-mock>=3.14", -] -type = [ - "mypy==1.11.2", - "types-cachetools>=5.5.0.20240820", - "types-chardet>=5.0.4.6", - { include-group = "test" }, -] diff --git a/pyproject-fmt/tests/test_main.py b/pyproject-fmt/tests/test_main.py index b26b262..a0f5b92 100644 --- a/pyproject-fmt/tests/test_main.py +++ b/pyproject-fmt/tests/test_main.py @@ -19,6 +19,7 @@ True, False, ], + ids=("in_place", "print"), ) @pytest.mark.parametrize( "check", @@ -26,6 +27,7 @@ True, False, ], + ids=["check", "no_check"], ) @pytest.mark.parametrize( "cwd", @@ -33,20 +35,23 @@ True, False, ], + ids=["cwd", "absolute"], ) @pytest.mark.parametrize( ("start", "outcome", "output"), [ - ( + pytest.param( '[build-system]\nrequires = [\n "hatchling>=0.14",\n]\n', '[build-system]\nrequires = [\n "hatchling>=0.14",\n]\n', "no change for {0}\n", + id="formatted", ), - ( + pytest.param( '[build-system]\nrequires = ["hatchling>=0.14.0"]', '[build-system]\nrequires = [ "hatchling>=0.14" ]\n', "--- {0}\n\n+++ {0}\n\n@@ -1,2 +1,2 @@\n\n [build-system]\n-requires = " '["hatchling>=0.14.0"]\n+requires = [ "hatchling>=0.14" ]\n', + id="format", ), ], ) diff --git a/tasks/changelog.py b/tasks/changelog.py index c5e57d5..86579e5 100644 --- a/tasks/changelog.py +++ b/tasks/changelog.py @@ -78,7 +78,7 @@ def run() -> None: def parse_cli() -> Options: parser = ArgumentParser() - parser.add_argument("project", choices=["pyproject-fmt"]) + parser.add_argument("project", choices=["pyproject-fmt", "tox-toml-fmt"]) parser.add_argument("pr", type=lambda s: int(s) if s else None) parser.add_argument("base", type=str) options = Options() diff --git a/tox-toml-fmt/.readthedocs.yaml b/tox-toml-fmt/.readthedocs.yaml new file mode 100644 index 0000000..cbe9b7a --- /dev/null +++ b/tox-toml-fmt/.readthedocs.yaml @@ -0,0 +1,10 @@ +version: 2 +build: + os: ubuntu-lts-latest + tools: + python: "3.12" + rust: "latest" + commands: + - pip install tox-uv + - tox -c tox-toml-fmt/tox.toml run -e docs -vv --notest + - tox -c tox-toml-fmt/tox.toml run -e docs --skip-pkg-install -- diff --git a/tox-toml-fmt/CHANGELOG.md b/tox-toml-fmt/CHANGELOG.md new file mode 100644 index 0000000..e69de29 diff --git a/tox-toml-fmt/Cargo.toml b/tox-toml-fmt/Cargo.toml new file mode 100644 index 0000000..21f99a2 --- /dev/null +++ b/tox-toml-fmt/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "tox-toml-fmt" +version = "2.5.0" +description = "Format pyproject.toml files" +repository = "https://github.com/tox-dev/tox-toml-fmt" +readme = "README.md" +license = "MIT" +edition = "2021" + +[lib] +name = "_lib" +path = "rust/src/main.rs" +crate-type = ["cdylib"] + +[dependencies] +common = {path = "../common" } +regex = { version = "1.11.1" } +pyo3 = { version = "0.22.5", features = ["abi3-py38"] } # integration with Python +lexical-sort = { version = "0.3.1" } + +[features] +extension-module = ["pyo3/extension-module"] +default = ["extension-module"] + +[dev-dependencies] +rstest = { version = "0.23.0" } # parametrized tests +indoc = { version = "2.0.5" } # dedented test cases for literal strings diff --git a/tox-toml-fmt/LICENSE.txt b/tox-toml-fmt/LICENSE.txt new file mode 100644 index 0000000..3649823 --- /dev/null +++ b/tox-toml-fmt/LICENSE.txt @@ -0,0 +1,18 @@ +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/tox-toml-fmt/README.md b/tox-toml-fmt/README.md new file mode 100644 index 0000000..2bb977a --- /dev/null +++ b/tox-toml-fmt/README.md @@ -0,0 +1,9 @@ +# tox-toml-fmt + +[![PyPI](https://img.shields.io/pypi/v/tox-toml-fmt?style=flat-square)](https://pypi.org/project/tox-toml-fmt) +[![PyPI - Implementation](https://img.shields.io/pypi/implementation/tox-toml-fmt?style=flat-square)](https://pypi.org/project/tox-toml-fmt) +[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/tox-toml-fmt?style=flat-square)](https://pypi.org/project/tox-toml-fmt) +[![Downloads](https://static.pepy.tech/badge/tox-toml-fmt/month)](https://pepy.tech/project/tox-toml-fmt) +[![PyPI - License](https://img.shields.io/pypi/l/tox-toml-fmt?style=flat-square)](https://opensource.org/licenses/MIT) +[![Build tox-toml-fmt](https://github.com/tox-dev/toml-fmt/actions/workflows/tox_toml_fmt_build.yaml/badge.svg)](https://github.com/tox-dev/toml-fmt/actions/workflows/tox_toml_fmt_build.yaml) +[![Test tox-toml-fmt](https://github.com/tox-dev/toml-fmt/actions/workflows/tox_toml_fmt_test.yaml/badge.svg)](https://github.com/tox-dev/toml-fmt/actions/workflows/tox_toml_fmt_test.yaml) diff --git a/tox-toml-fmt/docs/conf.py b/tox-toml-fmt/docs/conf.py new file mode 100644 index 0000000..c817925 --- /dev/null +++ b/tox-toml-fmt/docs/conf.py @@ -0,0 +1,43 @@ +"""Configuration for documentation build.""" # noqa: INP001 + +from __future__ import annotations + +from datetime import datetime, timezone +from importlib.metadata import version as metadata_version + +company, name = "tox-dev", "tox-toml-fmt" +ver = metadata_version("tox-toml-fmt") +release, version = ver, ".".join(ver.split(".")[:2]) +now = datetime.now(tz=timezone.utc) +project_copyright = f"2022-{now.year}, {company}" +master_doc, source_suffix = "index", ".rst" + +html_theme = "furo" +html_title, html_last_updated_fmt = name, now.isoformat() +pygments_style, pygments_dark_style = "sphinx", "monokai" + +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.extlinks", + "sphinx.ext.intersphinx", + "sphinx_argparse_cli", + "sphinx_autodoc_typehints", + "sphinx_inline_tabs", + "sphinx_copybutton", +] + +exclude_patterns = ["_build", "changelog/*", "_draft.rst"] +autoclass_content, autodoc_member_order, autodoc_typehints = "class", "bysource", "none" +autodoc_default_options = { + "member-order": "bysource", + "undoc-members": True, + "show-inheritance": True, +} + +extlinks = { + "pypi": ("https://pypi.org/project/%s", "%s"), + "gh": ("https://github.com/%s", "%s"), +} +intersphinx_mapping = {"python": ("https://docs.python.org/3", None)} +nitpicky = True +nitpick_ignore = [] diff --git a/tox-toml-fmt/docs/index.rst b/tox-toml-fmt/docs/index.rst new file mode 100644 index 0000000..a9964ee --- /dev/null +++ b/tox-toml-fmt/docs/index.rst @@ -0,0 +1,101 @@ +tox-toml-fmt +============= + +Apply a consistent format to your ``pyproject.toml`` file with comment support. See +`changelog here `_. + + +Philosophy +---------- +This tool aims to be an *opinionated formatter*, with similar objectives to `black `_. +This means it deliberately does not support a wide variety of configuration settings. In return, you get consistency, +predictability, and smaller diffs. + +Use +--- + +Via ``CLI`` +~~~~~~~~~~~ + +:pypi:`tox-toml-fmt` is a CLI tool that needs a Python interpreter (version 3.7 or higher) to run. We recommend either +:pypi:`pipx` or :pypi:`uv` to install tox into an isolated environment. This has the added benefit that later you'll +be able to upgrade tox without affecting other parts of the system. We provide method for ``pip`` too here but we +discourage that path if you can: + +.. tab:: uv + + .. code-block:: bash + + # install uv per https://docs.astral.sh/uv/#getting-started + uv tool install tox-toml-fmt + tox-toml-fmt --help + + +.. tab:: pipx + + .. code-block:: bash + + python -m pip install pipx-in-pipx --user + pipx install tox-toml-fmt + tox-toml-fmt --help + +.. tab:: pip + + .. code-block:: bash + + python -m pip install --user tox-toml-fmt + tox-toml-fmt --help + + You can install it within the global Python interpreter itself (perhaps as a user package via the + ``--user`` flag). Be cautious if you are using a Python installation that is managed by your operating system or + another package manager. ``pip`` might not coordinate with those tools, and may leave your system in an inconsistent + state. Note, if you go down this path you need to ensure pip is new enough per the subsections below + + +Via ``pre-commit`` hook +~~~~~~~~~~~~~~~~~~~~~~~ + +See :gh:`pre-commit/pre-commit` for instructions, sample ``.pre-commit-config.yaml``: + +.. code-block:: yaml + + - repo: https://github.com/tox-dev/tox-toml-fmt + rev: "v1.0.0" + hooks: + - id: tox-toml-fmt + +Via Python +~~~~~~~~~~ + +.. automodule:: tox_toml_fmt + :members: + +.. toctree:: + :hidden: + + self + +Configuration via file +---------------------- + +The ``tox-toml-fmt`` table is used when present in the ``tox.toml`` file: + +.. code-block:: toml + + [tox-toml-fmt] + + # after how many column width split arrays/dicts into multiple lines, 1 will force always + column_width = 120 + + # how many spaces use for indentation + indent = 2 + +If not set they will default to values from the CLI, the example above shows the defaults. + +Command line interface +---------------------- +.. sphinx_argparse_cli:: + :module: tox_toml_fmt.__main__ + :func: _build_our_cli + :prog: tox-toml-fmt + :title: diff --git a/tox-toml-fmt/pyproject.toml b/tox-toml-fmt/pyproject.toml new file mode 100644 index 0000000..e035b47 --- /dev/null +++ b/tox-toml-fmt/pyproject.toml @@ -0,0 +1,123 @@ +[build-system] +build-backend = "maturin" +requires = [ + "maturin>=1.7.4", +] + +[project] +name = "tox-toml-fmt" +description = "Format your pyproject.toml file" +readme = "README.md" +keywords = [ + "format", + "pyproject", +] +license.file = "LICENSE.txt" +authors = [ + { name = "Bernat Gabor", email = "gaborjbernat@gmail.com" }, +] +requires-python = ">=3.9" +classifiers = [ + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", +] +dynamic = [ + "version", +] +dependencies = [ + "toml-fmt-common==1.0.1", +] +urls."Bug Tracker" = "https://github.com/tox-dev/toml-fmt/issues" +urls."Changelog" = "https://github.com/tox-dev/toml-fmt/blob/main/tox-toml-fmt/CHANGELOG.md" +urls.Documentation = "https://github.com/tox-dev/toml-fmt/" +urls."Source Code" = "https://github.com/tox-dev/toml-fmt" +scripts.tox-toml-fmt = "tox_toml_fmt.__main__:runner" + +[dependency-groups] +dev = [ + { include-group = "docs" }, + { include-group = "fix" }, + { include-group = "test" }, + { include-group = "type" }, +] +test = [ + "covdefaults>=2.3", + "pytest>=8.3.2", + "pytest-cov>=5", + "pytest-mock>=3.14", +] +type = [ + "mypy==1.11.2", + "types-cachetools>=5.5.0.20240820", + "types-chardet>=5.0.4.6", + { include-group = "test" }, +] +docs = [ + "furo>=2024.8.6", + "sphinx>=8.0.2", + "sphinx-argparse-cli>=1.18.2", + "sphinx-autodoc-typehints>=2.4.4", + "sphinx-copybutton>=0.5.2", + "sphinx-inline-tabs>=2023.4.21", +] +fix = [ + "pre-commit-uv>=4.1.3", +] +pkg-meta = [ + "check-wheel-contents>=0.6", + "twine>=5.1.1", + "uv>=0.4.17", +] + +[tool.maturin] +bindings = "pyo3" +manifest-path = "Cargo.toml" +module-name = "tox_toml_fmt._lib" +python-source = "src" +strip = true +include = [ + "rust-toolchain.toml", +] + +[tool.cibuildwheel] +skip = [ + "pp*", + "*musl*", +] + +[tool.pyproject-fmt] +max_supported_python = "3.13" + +[tool.pytest] +ini_options.testpaths = [ + "tests", +] + +[tool.coverage] +html.show_contexts = true +html.skip_covered = false +paths.source = [ + "src", + ".tox/*/.venv/lib/*/site-packages", + ".tox\\*\\.venv\\Lib\\site-packages", + ".tox/*/lib/*/site-packages", + ".tox\\*\\Lib\\site-packages", + "**/src", + "**\\src", +] +report.fail_under = 100 +run.parallel = true +run.plugins = [ + "covdefaults", +] + +[tool.mypy] +show_error_codes = true +strict = true diff --git a/tox-toml-fmt/rust/src/global.rs b/tox-toml-fmt/rust/src/global.rs new file mode 100644 index 0000000..ec85f89 --- /dev/null +++ b/tox-toml-fmt/rust/src/global.rs @@ -0,0 +1,8 @@ +use common::taplo::rowan::SyntaxNode; +use common::taplo::syntax::Lang; + +use common::table::Tables; + +pub fn reorder_tables(root_ast: &SyntaxNode, tables: &Tables) { + tables.reorder(root_ast, &["", "env_run_base", "env"]); +} diff --git a/tox-toml-fmt/rust/src/main.rs b/tox-toml-fmt/rust/src/main.rs new file mode 100644 index 0000000..c115123 --- /dev/null +++ b/tox-toml-fmt/rust/src/main.rs @@ -0,0 +1,74 @@ +use std::string::String; + +use common::taplo::formatter::{format_syntax, Options}; +use common::taplo::parser::parse; +use pyo3::prelude::{PyModule, PyModuleMethods}; +use pyo3::{pyclass, pyfunction, pymethods, pymodule, wrap_pyfunction, Bound, PyResult}; + +use crate::global::reorder_tables; +use common::table::Tables; +mod global; +#[cfg(test)] +mod tests; + +#[pyclass(frozen, get_all)] +pub struct Settings { + column_width: usize, + indent: usize, +} + +#[pymethods] +impl Settings { + #[new] + #[pyo3(signature = (*, column_width, indent ))] + const fn new(column_width: usize, indent: usize) -> Self { + Self { + column_width, + indent, + } + } +} + +/// Format toml file +#[must_use] +#[pyfunction] +pub fn format_toml(content: &str, opt: &Settings) -> String { + let root_ast = parse(content).into_syntax().clone_for_update(); + let tables = Tables::from_ast(&root_ast); + + reorder_tables(&root_ast, &tables); + + let options = Options { + align_entries: false, // do not align by = + align_comments: true, // align inline comments + align_single_comments: true, // align comments after entries + array_trailing_comma: true, // ensure arrays finish with trailing comma + array_auto_expand: true, // arrays go to multi line when too long + array_auto_collapse: false, // do not collapse for easier diffs + compact_arrays: false, // leave whitespace + compact_inline_tables: false, // leave whitespace + compact_entries: false, // leave whitespace + column_width: opt.column_width, + indent_tables: false, + indent_entries: false, + inline_table_expand: true, + trailing_newline: true, + allowed_blank_lines: 1, // one blank line to separate + indent_string: " ".repeat(opt.indent), + reorder_keys: false, // respect custom order + reorder_arrays: false, // for natural sorting we need to this ourselves + crlf: false, + }; + format_syntax(root_ast, options) +} + +/// # Errors +/// +/// Will return `PyErr` if an error is raised during formatting. +#[pymodule] +#[pyo3(name = "_lib")] +pub fn _lib(m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_function(wrap_pyfunction!(format_toml, m)?)?; + m.add_class::()?; + Ok(()) +} diff --git a/tox-toml-fmt/rust/src/tests/global_tests.rs b/tox-toml-fmt/rust/src/tests/global_tests.rs new file mode 100644 index 0000000..5c2c84a --- /dev/null +++ b/tox-toml-fmt/rust/src/tests/global_tests.rs @@ -0,0 +1,55 @@ +use common::taplo::formatter::{format_syntax, Options}; +use common::taplo::parser::parse; +use indoc::indoc; +use rstest::rstest; + +use crate::global::reorder_tables; +use common::table::Tables; + +#[rstest] +#[case::reorder( + indoc ! {r#" + # comment + requires = ["tox>=4.22"] + + [demo] + desc = "demo" + + [env.type] + description = "type" + + [env.docs] + description = "docs" + + [env_run_base] + description = "base" + + "#}, + indoc ! {r#" + # comment + requires = ["tox>=4.22"] + + [env_run_base] + description = "base" + + [env.type] + description = "type" + + [env.docs] + description = "docs" + + [demo] + desc = "demo" + "#}, +)] +fn test_reorder_table(#[case] start: &str, #[case] expected: &str) { + let root_ast = parse(start).into_syntax().clone_for_update(); + let tables = Tables::from_ast(&root_ast); + reorder_tables(&root_ast, &tables); + let opt = Options { + column_width: 120, + ..Options::default() + }; + let got = format_syntax(root_ast, opt); + assert_eq!(got, expected); +} diff --git a/tox-toml-fmt/rust/src/tests/main_tests.rs b/tox-toml-fmt/rust/src/tests/main_tests.rs new file mode 100644 index 0000000..556d8e5 --- /dev/null +++ b/tox-toml-fmt/rust/src/tests/main_tests.rs @@ -0,0 +1,88 @@ +use indoc::indoc; +use rstest::{rstest}; + +use crate::{format_toml, Settings}; + +#[rstest] +#[case::simple( + indoc ! {r#" + requires = ["tox>=4.22"] + env_list = ["3.13", "3.12"] + skip_missing_interpreters = true + + [env_run_base] + description = "run the tests with pytest under {env_name}" + commands = [ ["pytest"] ] + + [env.type] + description = "run type check on code base" + commands = [["mypy", "src{/}tox_toml_fmt"], ["mypy", "tests"]] + "#}, + indoc ! {r#" + requires = [ "tox>=4.22" ] + env_list = [ "3.13", "3.12" ] + skip_missing_interpreters = true + + [env_run_base] + description = "run the tests with pytest under {env_name}" + commands = [ [ "pytest" ] ] + + [env.type] + description = "run type check on code base" + commands = [ [ "mypy", "src{/}tox_toml_fmt" ], [ "mypy", "tests" ] ] + "#}, + 2, +)] +#[case::empty( + indoc ! {r""}, + "\n", + 2, +)] +fn test_format_toml( + #[case] start: &str, + #[case] expected: &str, + #[case] indent: usize, +) { + let settings = Settings { + column_width: 80, + indent, + }; + let got = format_toml(start, &settings); + assert_eq!(got, expected); + let second = format_toml(got.as_str(), &settings); + assert_eq!(second, got); +} + +/// Test that the column width is respected, +/// and that arrays are neither exploded nor collapsed without reason +#[test] +fn test_column_width() { + let start = indoc! {r#" + # comment + requires = ["tox>=4.22"] + env_list = ["fix", "3.13", "3.12", "3.11", "3.10", "3.9", "type", "docs", "pkg_meta"] + "#}; + let settings = Settings { + column_width: 50, + indent: 4, + }; + let got = format_toml(start, &settings); + let expected = indoc! {r#" + # comment + requires = [ "tox>=4.22" ] + env_list = [ + "fix", + "3.13", + "3.12", + "3.11", + "3.10", + "3.9", + "type", + "docs", + "pkg_meta", + ] + "#}; + assert_eq!(got, expected); + let second = format_toml(got.as_str(), &settings); + assert_eq!(second, got); +} diff --git a/tox-toml-fmt/rust/src/tests/mod.rs b/tox-toml-fmt/rust/src/tests/mod.rs new file mode 100644 index 0000000..68e908a --- /dev/null +++ b/tox-toml-fmt/rust/src/tests/mod.rs @@ -0,0 +1,2 @@ +mod global_tests; +mod main_tests; diff --git a/tox-toml-fmt/src/tox_toml_fmt/__init__.py b/tox-toml-fmt/src/tox_toml_fmt/__init__.py new file mode 100644 index 0000000..ceef8d6 --- /dev/null +++ b/tox-toml-fmt/src/tox_toml_fmt/__init__.py @@ -0,0 +1,9 @@ +"""Format pyproject.toml files.""" + +from __future__ import annotations + +from .__main__ import runner as run + +__all__ = [ + "run", +] diff --git a/tox-toml-fmt/src/tox_toml_fmt/__main__.py b/tox-toml-fmt/src/tox_toml_fmt/__main__.py new file mode 100644 index 0000000..42775de --- /dev/null +++ b/tox-toml-fmt/src/tox_toml_fmt/__main__.py @@ -0,0 +1,82 @@ +"""Main entry point for the formatter.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Sequence + +from toml_fmt_common import ArgumentGroup, FmtNamespace, TOMLFormatter, _build_cli, run # noqa: PLC2701 + +from ._lib import Settings, format_toml + +if TYPE_CHECKING: + from argparse import ArgumentParser + + +class PyProjectFmtNamespace(FmtNamespace): + """Formatting arguments.""" + + +class ToxTOMLFormatter(TOMLFormatter[PyProjectFmtNamespace]): + """Format pyproject.toml.""" + + def __init__(self) -> None: + """Create a formatter.""" + super().__init__(PyProjectFmtNamespace()) + + @property + def prog(self) -> str: + """:return: program name""" + return "tox-toml-fmt" + + @property + def filename(self) -> str: + """:return: filename operating on""" + return "pyproject.toml" + + def add_format_flags(self, parser: ArgumentGroup) -> None: + """ + Additional formatter config. + + :param parser: parser to operate on. + """ + + @property + def override_cli_from_section(self) -> tuple[str, ...]: + """:return: path where config overrides live""" + return ("tox-toml-fmt",) + + def format(self, text: str, opt: PyProjectFmtNamespace) -> str: # noqa: PLR6301 + """ + Perform the formatting. + + :param text: content to operate on + :param opt: formatter config + :return: formatted text + """ + settings = Settings( + column_width=opt.column_width, + indent=opt.indent, + ) + return format_toml(text, settings) + + +def runner(args: Sequence[str] | None = None) -> int: + """ + Run the formatter. + + :param args: CLI arguments + :return: exit code + """ + return run(ToxTOMLFormatter(), args) + + +def _build_our_cli() -> ArgumentParser: + return _build_cli(ToxTOMLFormatter())[0] # pragma: no cover + + +__all__ = [ + "runner", +] + +if __name__ == "__main__": + raise SystemExit(runner()) diff --git a/tox-toml-fmt/src/tox_toml_fmt/_lib.pyi b/tox-toml-fmt/src/tox_toml_fmt/_lib.pyi new file mode 100644 index 0000000..56e571f --- /dev/null +++ b/tox-toml-fmt/src/tox_toml_fmt/_lib.pyi @@ -0,0 +1,13 @@ +class Settings: + def __init__( + self, + *, + column_width: int, + indent: int, + ) -> None: ... + @property + def column_width(self) -> int: ... + @property + def indent(self) -> int: ... + +def format_toml(content: str, settings: Settings) -> str: ... diff --git a/tox-toml-fmt/src/tox_toml_fmt/py.typed b/tox-toml-fmt/src/tox_toml_fmt/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/tox-toml-fmt/tests/test_lib.py b/tox-toml-fmt/tests/test_lib.py new file mode 100644 index 0000000..7f13131 --- /dev/null +++ b/tox-toml-fmt/tests/test_lib.py @@ -0,0 +1,46 @@ +from __future__ import annotations + +from textwrap import dedent + +import pytest +from tox_toml_fmt._lib import Settings, format_toml + + +@pytest.mark.parametrize( + ("start", "expected"), + [ + pytest.param( + """\ + requires = ["tox>=4.22"] + env_list = ["3.13", "3.12"] + skip_missing_interpreters = true + + [env_run_base] + description = "run the tests with pytest under {env_name}" + commands = [ ["pytest"] ] + + [env.type] + description = "run type check on code base" + commands = [["mypy", "src{/}tox_toml_fmt"], ["mypy", "tests"]] + """, + """\ + requires = [ "tox>=4.22" ] + env_list = [ "3.13", "3.12" ] + skip_missing_interpreters = true + + [env_run_base] + description = "run the tests with pytest under {env_name}" + commands = [ [ "pytest" ] ] + + [env.type] + description = "run type check on code base" + commands = [ [ "mypy", "src{/}tox_toml_fmt" ], [ "mypy", "tests" ] ] + """, + id="example", + ), + ], +) +def test_format_toml(start: str, expected: str) -> None: + settings = Settings(column_width=120, indent=4) + res = format_toml(dedent(start), settings) + assert res == dedent(expected) diff --git a/tox-toml-fmt/tests/test_main.py b/tox-toml-fmt/tests/test_main.py new file mode 100644 index 0000000..9b55276 --- /dev/null +++ b/tox-toml-fmt/tests/test_main.py @@ -0,0 +1,149 @@ +from __future__ import annotations + +from textwrap import dedent +from typing import TYPE_CHECKING + +import pytest +from tox_toml_fmt.__main__ import runner as run + +if TYPE_CHECKING: + from pathlib import Path + + from pytest_mock import MockerFixture + + +@pytest.mark.parametrize( + "in_place", + [ + True, + False, + ], + ids=("in_place", "print"), +) +@pytest.mark.parametrize( + "check", + [ + True, + False, + ], + ids=["check", "no_check"], +) +@pytest.mark.parametrize( + "cwd", + [ + True, + False, + ], + ids=["cwd", "absolute"], +) +@pytest.mark.parametrize( + ("start", "outcome", "output"), + [ + pytest.param( + 'requires = [ "tox>=4.22" ]\n', + 'requires = [ "tox>=4.22" ]\n', + "no change for {0}\n", + id="formatted", + ), + pytest.param( + 'requires = ["tox>=4.22"]\n', + 'requires = [ "tox>=4.22" ]\n', + '--- {0}\n\n+++ {0}\n\n@@ -1 +1 @@\n\n-requires = ["tox>=4.22"]\n+requires = [ "tox>=4.22" ]\n', + id="format", + ), + ], +) +def test_main( + tmp_path: Path, + capsys: pytest.CaptureFixture[str], + in_place: bool, + start: str, + outcome: str, + output: str, + monkeypatch: pytest.MonkeyPatch, + mocker: MockerFixture, + cwd: bool, + check: bool, +) -> None: + mocker.patch("toml_fmt_common._color_diff", lambda t: t) + if cwd: + monkeypatch.chdir(tmp_path) + pyproject_toml = tmp_path / "tox.toml" + pyproject_toml.write_text(start) + args = [str(pyproject_toml)] + if not in_place: + args.append("--stdout") + + if check: + args.append("--check") + + if not in_place: + with pytest.raises(SystemExit): + run(args) + assert pyproject_toml.read_text() == start + return + + result = run(args) + assert result == (0 if start == outcome else 1) + + out, err = capsys.readouterr() + assert not err + + if check: + assert pyproject_toml.read_text() == start + elif in_place: + name = "tox.toml" if cwd else str(tmp_path / "tox.toml") + output = output.format(name) + assert pyproject_toml.read_text() == outcome + assert out == output + else: + assert out == outcome + + +@pytest.mark.parametrize("indent", [0, 2, 4]) +def test_indent(tmp_path: Path, indent: int) -> None: + start = """\ + requires = [ + "a" + ] + """ + + expected = f"""\ + requires = [ + {" " * indent}"a", + ] + """ + pyproject_toml = tmp_path / "tox.toml" + pyproject_toml.write_text(dedent(start)) + args = [str(pyproject_toml), "--indent", str(indent)] + run(args) + output = pyproject_toml.read_text() + assert output == dedent(expected) + + +def test_tox_toml_config(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None: + txt = """\ + requires = [ + "a", + ] + + [tox-toml-fmt] + indent = 6 + """ + filename = tmp_path / "tox.toml" + filename.write_text(dedent(txt)) + run([str(filename)]) + + expected = """\ + requires = [ + "a", + ] + + [tox-toml-fmt] + indent = 6 + """ + got = filename.read_text() + assert got == dedent(expected) + out, err = capsys.readouterr() + assert out + assert not err diff --git a/tox-toml-fmt/tests/test_pyproject_toml_fmt.py b/tox-toml-fmt/tests/test_pyproject_toml_fmt.py new file mode 100644 index 0000000..3e9eb74 --- /dev/null +++ b/tox-toml-fmt/tests/test_pyproject_toml_fmt.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +import subprocess # noqa: S404 +import sys +from pathlib import Path + + +def test_help_invocation_as_module() -> None: + subprocess.check_call([sys.executable, "-m", "tox_toml_fmt", "--help"]) + + +def test_help_invocation_as_script() -> None: + subprocess.check_call( + [str(Path(sys.executable).parent / "tox-toml-fmt"), "--help"], + ) diff --git a/tox-toml-fmt/tox.toml b/tox-toml-fmt/tox.toml new file mode 100644 index 0000000..f758e8f --- /dev/null +++ b/tox-toml-fmt/tox.toml @@ -0,0 +1,102 @@ +requires = ["tox>=4.22"] +env_list = ["fix", "3.13", "3.12", "3.11", "3.10", "3.9", "type", "docs", "pkg_meta"] +skip_missing_interpreters = true + +[env_run_base] +description = "run the tests with pytest under {env_name}" +package = "wheel" +wheel_build_env = ".pkg" +dependency_groups = ["test"] +pass_env = ["PYTEST_*", "SSL_CERT_FILE"] +set_env.COVERAGE_FILE = { replace = "env", name = "COVERAGE_FILE", default = "{work_dir}{/}.coverage.{env_name}" } +set_env.COVERAGE_FILECOVERAGE_PROCESS_START = "{tox_root}{/}pyproject.toml" +commands = [ + [ + "pytest", + { replace = "posargs", extend = true, default = [ + "--durations", + "5", + "--junitxml", + "{work_dir}{/}junit.{env_name}.xml", + "--no-cov-on-fail", + "--cov", + "{env_site_packages_dir}{/}tox_toml_fmt", + "--cov", + "{tox_root}{/}tests", + "--cov-config", + "{tox_root}{/}pyproject.toml", + "--cov-context", + "test", + "--cov-report", + "term-missing:skip-covered", + "--cov-report", + "html:{env_tmp_dir}{/}htmlcov", + "--cov-report", + "xml:{work_dir}{/}coverage.{env_name}.xml", + "tests", + ] }, + ], +] + +[env.type] +description = "run type check on code base" +dependency_groups = ["type"] +commands = [["mypy", "src{/}tox_toml_fmt"], ["mypy", "tests"]] + +[env.docs] +description = "build documentation" +dependency_groups = ["docs"] +commands = [ + [ + "sphinx-build", + "-d", + "{env_tmp_dir}{/}docs_tree", + "docs", + "{env:READTHEDOCS_OUTPUT:{work_dir}{/}docs_out}/html", + "--color", + "-b", + "html", + { replace = "posargs", default = [ + "-b", + "linkcheck", + ], extend = true }, + "-W", + ], + [ + "python", + "-c", + 'print(r"documentation available under file://{work_dir}{/}docs_out{/}index.html")', + ], +] + +[env.pkg_meta] +description = "check that the long description is valid" +skip_install = true +dependency_groups = ["pkg_meta"] +commands = [ + [ + "uv", + "build", + "--sdist", + "--wheel", + "--out-dir", + "{env_tmp_dir}", + ".", + ], + [ + "twine", + "check", + "{env_tmp_dir}{/}*", + ], + [ + "check-wheel-contents", + "--no-config", + "{env_tmp_dir}", + ], +] + +[env.dev] +description = "dev environment with all deps at {envdir}" +package = "editable" +dependency_groups = ["dev"] +commands = [["uv", "pip", "tree"], ["python", "-c", 'print(r"{env_python}")']]