diff --git a/.gitignore b/.gitignore index 7aadb5b..4b67d0e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ -/common/target -/target -/pyproject-fmt/.tox -/pyproject-fmt/dist -/dist +.tox +target +dist +__pycache__ +_lib.abi3.* diff --git a/common/README.md b/common/README.md index 08bcebf..ef60369 100644 --- a/common/README.md +++ b/common/README.md @@ -1,5 +1,5 @@ -# toml-fmt common +# toml-fmt-common -Contains code common to all formatters. +Contains Rust code common to all formatters under the `toml-fmt` umbrella. [![Test common](https://github.com/tox-dev/toml-fmt/actions/workflows/common.yaml/badge.svg)](https://github.com/tox-dev/toml-fmt/actions/workflows/common.yaml) diff --git a/pyproject-fmt/.github/FUNDING.yml b/pyproject-fmt/.github/FUNDING.yml deleted file mode 100644 index 423b664..0000000 --- a/pyproject-fmt/.github/FUNDING.yml +++ /dev/null @@ -1 +0,0 @@ -tidelift: "pypi/pyproject-fmt-rust" diff --git a/pyproject-fmt/.github/SECURITY.md b/pyproject-fmt/.github/SECURITY.md deleted file mode 100644 index f9506b7..0000000 --- a/pyproject-fmt/.github/SECURITY.md +++ /dev/null @@ -1,13 +0,0 @@ -# Security Policy - -## Supported Versions - -| Version | Supported | -| ------- | ------------------ | -| 1.0 + | :white_check_mark: | -| < 1.0 | :x: | - -## Reporting a Vulnerability - -To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift -will coordinate the fix and disclosure. diff --git a/pyproject-fmt/.github/dependabot.yml b/pyproject-fmt/.github/dependabot.yml deleted file mode 100644 index 1230149..0000000 --- a/pyproject-fmt/.github/dependabot.yml +++ /dev/null @@ -1,6 +0,0 @@ -version: 2 -updates: - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "daily" diff --git a/pyproject-fmt/.github/release.yml b/pyproject-fmt/.github/release.yml deleted file mode 100644 index 9d1e098..0000000 --- a/pyproject-fmt/.github/release.yml +++ /dev/null @@ -1,5 +0,0 @@ -changelog: - exclude: - authors: - - dependabot - - pre-commit-ci diff --git a/pyproject-fmt/.github/workflows/check.yaml b/pyproject-fmt/.github/workflows/check.yaml deleted file mode 100644 index 66d685b..0000000 --- a/pyproject-fmt/.github/workflows/check.yaml +++ /dev/null @@ -1,93 +0,0 @@ -name: Check -on: - workflow_dispatch: - push: - branches: ["main"] - tags: ["*"] - pull_request: -concurrency: - group: check-${{ github.ref }} - cancel-in-progress: true -jobs: - 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 - 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" - - check: - name: tox env ${{ matrix.env }} - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - env: - - type - - dev - - pkg_meta - 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" - - rust-check: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions-rust-lang/setup-rust-toolchain@v1 - - name: Rustfmt Check - uses: actions-rust-lang/rustfmt@v1 - - name: Lint - run: cargo clippy --all-targets -- -D warnings diff --git a/pyproject-fmt/.github/workflows/release.yaml b/pyproject-fmt/.github/workflows/release.yaml deleted file mode 100644 index ca7bdc6..0000000 --- a/pyproject-fmt/.github/workflows/release.yaml +++ /dev/null @@ -1,135 +0,0 @@ -name: Build -on: - workflow_dispatch: - push: - branches: ["main"] - tags: ["*"] - pull_request: - schedule: - - cron: "0 8 * * *" -concurrency: - group: build-${{ github.ref }} - cancel-in-progress: true - -permissions: - contents: read - -jobs: - linux: - 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: - - uses: actions/checkout@v4 - - name: Build wheels - uses: PyO3/maturin-action@v1 - with: - target: ${{ matrix.platform.target }} - args: --release --out dist --interpreter ${{ matrix.platform.interpreter || '3.8' }} - 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: - runs-on: ${{ matrix.platform.runner }} - strategy: - matrix: - platform: - - runner: windows-latest - target: x64 - - runner: windows-latest - target: x86 - steps: - - uses: actions/checkout@v4 - - name: Build wheels - uses: PyO3/maturin-action@v1 - with: - target: ${{ matrix.platform.target }} - args: --release --out dist --interpreter ${{ matrix.platform.interpreter || '3.8' }} - sccache: "true" - - name: Upload wheels - uses: actions/upload-artifact@v4 - with: - name: wheels-windows-${{ matrix.platform.target }} - path: dist - - macos: - runs-on: ${{ matrix.platform.runner }} - strategy: - matrix: - platform: - - runner: macos-latest - target: x86_64 - - runner: macos-14 - target: aarch64 - steps: - - uses: actions/checkout@v4 - - name: Build wheels - uses: PyO3/maturin-action@v1 - with: - target: ${{ matrix.platform.target }} - args: --release --out dist --interpreter "3.8 pypy3.8 pypy3.9 pypy3.10" - sccache: "true" - - name: Upload wheels - uses: actions/upload-artifact@v4 - with: - name: wheels-macos-${{ matrix.platform.target }} - path: dist - - sdist: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Build sdist - uses: PyO3/maturin-action@v1 - with: - command: sdist - args: --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/pyproject-fmt-rust/${{ github.ref_name }} - permissions: - id-token: write - if: "startsWith(github.ref, 'refs/tags/')" - needs: [linux, windows, macos, sdist] - steps: - - uses: actions/download-artifact@v4 - - name: Publish to PyPI - uses: PyO3/maturin-action@v1 - with: - command: upload - args: --non-interactive --skip-existing wheels-*/* diff --git a/pyproject-fmt/.gitignore b/pyproject-fmt/.gitignore deleted file mode 100644 index bb756a2..0000000 --- a/pyproject-fmt/.gitignore +++ /dev/null @@ -1,13 +0,0 @@ -*.egg-info/ -.tox/ -.*_cache -__pycache__ -**.pyc -dist - -/target -/pyproject-*.toml -/src/pyproject_fmt/_lib.abi3* -/tarpaulin-report.html -/build_rs_cov.profraw -/.cargo/config.toml diff --git a/pyproject-fmt/.rustfmt.toml b/pyproject-fmt/.rustfmt.toml deleted file mode 100644 index 7530651..0000000 --- a/pyproject-fmt/.rustfmt.toml +++ /dev/null @@ -1 +0,0 @@ -max_width = 120 diff --git a/pyproject-fmt/docs/index.rst b/pyproject-fmt/docs/index.rst index 63d2425..78c395b 100644 --- a/pyproject-fmt/docs/index.rst +++ b/pyproject-fmt/docs/index.rst @@ -74,8 +74,8 @@ If not set they will default to values from the CLI, the example above shows the Command line interface ---------------------- .. sphinx_argparse_cli:: - :module: pyproject_fmt.cli - :func: _build_cli + :module: pyproject_fmt.__main__ + :func: _build_our_cli :prog: pyproject-fmt :title: diff --git a/pyproject-fmt/pyproject.toml b/pyproject-fmt/pyproject.toml index 8f0944b..b9f5468 100644 --- a/pyproject-fmt/pyproject.toml +++ b/pyproject-fmt/pyproject.toml @@ -32,13 +32,13 @@ dynamic = [ "version", ] dependencies = [ - "tomli>=2.0.2; python_version<'3.11'", + "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/pyproject-fmt/CHANGELOG.md" 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__:run" +scripts.pyproject-fmt = "pyproject_fmt.__main__:runner" [tool.maturin] bindings = "pyo3" diff --git a/pyproject-fmt/src/pyproject_fmt/__init__.py b/pyproject-fmt/src/pyproject_fmt/__init__.py index 578a4cf..ceef8d6 100644 --- a/pyproject-fmt/src/pyproject_fmt/__init__.py +++ b/pyproject-fmt/src/pyproject_fmt/__init__.py @@ -2,7 +2,7 @@ from __future__ import annotations -from .__main__ import run +from .__main__ import runner as run __all__ = [ "run", diff --git a/pyproject-fmt/src/pyproject_fmt/__main__.py b/pyproject-fmt/src/pyproject_fmt/__main__.py index 85a3046..5217915 100644 --- a/pyproject-fmt/src/pyproject_fmt/__main__.py +++ b/pyproject-fmt/src/pyproject_fmt/__main__.py @@ -2,78 +2,106 @@ from __future__ import annotations -import difflib -import sys -from pathlib import Path -from typing import TYPE_CHECKING - -from pyproject_fmt._lib import format_toml -from pyproject_fmt.cli import cli_args - -if TYPE_CHECKING: - from collections.abc import Iterable, Sequence - - from pyproject_fmt.cli import Config - -GREEN = "\u001b[32m" -RED = "\u001b[31m" -RESET = "\u001b[0m" - - -def color_diff(diff: Iterable[str]) -> Iterable[str]: - """ - Visualize difference with colors. - - :param diff: the diff lines - """ - for line in diff: - if line.startswith("+"): - yield f"{GREEN}{line}{RESET}" - elif line.startswith("-"): - yield f"{RED}{line}{RESET}" - else: - yield line - - -def _handle_one(config: Config) -> bool: - formatted = format_toml(config.toml, config.settings) - before = config.toml - changed = before != formatted - if config.pyproject_toml is None or config.stdout: # when reading from stdin or writing to stdout, print new format - print(formatted, end="") # noqa: T201 - return changed - - if before != formatted and not config.check: - config.pyproject_toml.write_text(formatted, encoding="utf-8") - if config.no_print_diff: - return changed - try: - name = str(config.pyproject_toml.relative_to(Path.cwd())) - except ValueError: - name = str(config.pyproject_toml) - diff: Iterable[str] = [] - if changed: - diff = difflib.unified_diff(before.splitlines(), formatted.splitlines(), fromfile=name, tofile=name) - - if diff: - diff = color_diff(diff) - print("\n".join(diff)) # print diff on change # noqa: T201 - else: - print(f"no change for {name}") # noqa: T201 - return changed - - -def run(args: Sequence[str] | None = None) -> int: +from argparse import ArgumentParser, ArgumentTypeError +from typing import Sequence + +from toml_fmt_common import ArgumentGroup, FmtNamespace, TOMLFormatter, _build_cli, run # noqa: PLC2701 + +from ._lib import Settings, format_toml + + +class PyProjectFmtNamespace(FmtNamespace): + """Formatting arguments.""" + + keep_full_version: bool + max_supported_python: tuple[int, int] + + +class PyProjectFormatter(TOMLFormatter[PyProjectFmtNamespace]): + """Format pyproject.toml.""" + + def __init__(self) -> None: + """Create a formatter.""" + super().__init__(PyProjectFmtNamespace()) + + @property + def prog(self) -> str: + """:return: program name""" + return "pyproject-fmt" + + @property + def filename(self) -> str: + """:return: filename operating on""" + return "pyproject.toml" + + def add_format_flags(self, parser: ArgumentGroup) -> None: # noqa: PLR6301 + """ + Additional formatter config. + + :param parser: parser to operate on. + """ + msg = "keep full dependency versions - do not remove redundant .0 from versions" + parser.add_argument("--keep-full-version", action="store_true", help=msg) + + def _version_argument(got: str) -> tuple[int, int]: + parts = got.split(".") + if len(parts) != 2: # noqa: PLR2004 + err = f"invalid version: {got}, must be e.g. 3.13" + raise ArgumentTypeError(err) + try: + return int(parts[0]), int(parts[1]) + except ValueError as exc: + err = f"invalid version: {got} due {exc!r}, must be e.g. 3.13" + raise ArgumentTypeError(err) from exc + + parser.add_argument( + "--max-supported-python", + metavar="minor.major", + type=_version_argument, + default=(3, 13), + help="latest Python version the project supports (e.g. 3.13)", + ) + + @property + def override_cli_from_section(self) -> tuple[str, ...]: + """:return: path where config overrides live""" + return "tool", "pyproject-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, + keep_full_version=opt.keep_full_version, + max_supported_python=opt.max_supported_python, + min_supported_python=(3, 9), # default for when the user didn't specify via requires-python + ) + return format_toml(text, settings) + + +def runner(args: Sequence[str] | None = None) -> int: """ Run the formatter. - :param args: command line arguments, by default use sys.argv[1:] - :return: exit code - 0 means already formatted correctly, otherwise 1 + :param args: CLI arguments + :return: exit code """ - configs = cli_args(sys.argv[1:] if args is None else args) - results = [_handle_one(config) for config in configs] - return 1 if any(results) else 0 # exit with non success on change + return run(PyProjectFormatter(), args) + + +def _build_our_cli() -> ArgumentParser: + return _build_cli(PyProjectFormatter())[0] # pragma: no cover + +__all__ = [ + "runner", +] if __name__ == "__main__": - raise SystemExit(run()) + raise SystemExit(runner()) diff --git a/pyproject-fmt/src/pyproject_fmt/cli.py b/pyproject-fmt/src/pyproject_fmt/cli.py deleted file mode 100644 index d8e3521..0000000 --- a/pyproject-fmt/src/pyproject_fmt/cli.py +++ /dev/null @@ -1,208 +0,0 @@ -"""CLI interface parser.""" - -from __future__ import annotations - -import os -import sys -from argparse import ( - ArgumentDefaultsHelpFormatter, - ArgumentParser, - ArgumentTypeError, - Namespace, -) -from dataclasses import dataclass -from importlib.metadata import version -from pathlib import Path -from typing import TYPE_CHECKING - -from ._lib import Settings - -if TYPE_CHECKING: - from collections.abc import Sequence - -if sys.version_info >= (3, 11): # pragma: >=3.11 cover - import tomllib -else: # pragma: <3.11 cover - import tomli as tomllib - - -class PyProjectFmtNamespace(Namespace): - """Options for pyproject-fmt tool.""" - - inputs: list[Path] - stdout: bool - check: bool - no_print_diff: bool - - column_width: int - indent: int - keep_full_version: bool - max_supported_python: tuple[int, int] - - -@dataclass(frozen=True) -class Config: - """Configuration flags for the formatting.""" - - pyproject_toml: Path | None # path to the toml file or None if stdin - toml: str # the toml file content - stdout: bool # push to standard out, implied if reading from stdin - check: bool # check only - no_print_diff: bool # don't print diff - settings: Settings - - -def pyproject_toml_path_creator(argument: str) -> Path | None: - """ - Validate that pyproject.toml can be formatted. - - :param argument: the string argument passed in - :return: the pyproject.toml path or None if stdin - :raises ArgumentTypeError: invalid argument - """ - if argument == "-": - return None # stdin, no further validation needed - path = Path(argument).absolute() - if path.is_dir(): - path /= "pyproject.toml" - if not path.exists(): - msg = "path does not exist" - raise ArgumentTypeError(msg) - if not path.is_file(): - msg = "path is not a file" - raise ArgumentTypeError(msg) - if not os.access(path, os.R_OK): - msg = "cannot read path" - raise ArgumentTypeError(msg) - if not os.access(path, os.W_OK): - msg = "cannot write path" - raise ArgumentTypeError(msg) - return path - - -def _version_argument(got: str) -> tuple[int, int]: - parts = got.split(".") - if len(parts) != 2: # noqa: PLR2004 - msg = f"invalid version: {got}, must be e.g. 3.13" - raise ArgumentTypeError(msg) - try: - return int(parts[0]), int(parts[1]) - except ValueError as exc: - msg = f"invalid version: {got} due {exc!r}, must be e.g. 3.13" - raise ArgumentTypeError(msg) from exc - - -def _build_cli() -> ArgumentParser: - parser = ArgumentParser( - formatter_class=ArgumentDefaultsHelpFormatter, - prog="pyproject-fmt", - ) - parser.add_argument( - "-V", - "--version", - action="version", - help="print package version of pyproject_fmt", - version=f"%(prog)s ({version('pyproject-fmt')})", - ) - - mode_group = parser.add_argument_group("run mode") - mode = mode_group.add_mutually_exclusive_group() - msg = "print the formatted TOML to the stdout, implied if reading from stdin" - mode.add_argument("-s", "--stdout", action="store_true", help=msg) - msg = "check and fail if any input would be formatted, printing any diffs" - mode.add_argument("--check", action="store_true", help=msg) - mode_group.add_argument( - "-n", - "--no-print-diff", - action="store_true", - help="Flag indicating to print diff for the check mode", - ) - - format_group = parser.add_argument_group("formatting behavior") - format_group.add_argument( - "--column-width", - type=int, - default=120, - help="max column width in the TOML file", - metavar="count", - ) - format_group.add_argument( - "--indent", - type=int, - default=2, - help="number of spaces to use for indentation", - metavar="count", - ) - msg = "keep full dependency versions - do not remove redundant .0 from versions" - format_group.add_argument("--keep-full-version", action="store_true", help=msg) - format_group.add_argument( - "--max-supported-python", - metavar="minor.major", - type=_version_argument, - default=(3, 13), - help="latest Python version the project supports (e.g. 3.13)", - ) - - msg = "pyproject.toml file(s) to format, use '-' to read from stdin" - parser.add_argument( - "inputs", - nargs="+", - type=pyproject_toml_path_creator, - help=msg, - ) - return parser - - -def cli_args(args: Sequence[str]) -> list[Config]: - """ - Load the tools options. - - :param args: CLI arguments - :return: the parsed options - """ - parser = _build_cli() - opt = PyProjectFmtNamespace() - parser.parse_args(namespace=opt, args=args) - res = [] - for pyproject_toml in opt.inputs: - column_width = opt.column_width - indent = opt.indent - keep_full_version = opt.keep_full_version - max_supported_python = opt.max_supported_python - raw_pyproject_toml = sys.stdin.read() if pyproject_toml is None else pyproject_toml.read_text(encoding="utf-8") - config = tomllib.loads(raw_pyproject_toml) - if "tool" in config and "pyproject-fmt" in config["tool"]: - for key, entry in config["tool"]["pyproject-fmt"].items(): - if key == "column_width": - column_width = int(entry) - elif key == "indent": - indent = int(entry) - elif key == "keep_full_version": - keep_full_version = bool(entry) - elif key == "max_supported_python": - max_supported_python = _version_argument(entry) - res.append( - Config( - pyproject_toml=pyproject_toml, - toml=raw_pyproject_toml, - stdout=opt.stdout, - check=opt.check, - no_print_diff=opt.no_print_diff, - settings=Settings( - column_width=column_width, - indent=indent, - keep_full_version=keep_full_version, - max_supported_python=max_supported_python, - min_supported_python=(3, 9), # default for when the user did not specify via requires-python - ), - ) - ) - - return res - - -__all__ = [ - "Config", - "PyProjectFmtNamespace", - "cli_args", -] diff --git a/pyproject-fmt/tests/test_cli.py b/pyproject-fmt/tests/test_cli.py deleted file mode 100644 index bbc3fa4..0000000 --- a/pyproject-fmt/tests/test_cli.py +++ /dev/null @@ -1,143 +0,0 @@ -from __future__ import annotations - -import io -import os -import sys -from importlib.metadata import version -from stat import S_IREAD, S_IWRITE -from typing import TYPE_CHECKING - -import pytest - -from pyproject_fmt.cli import cli_args - -if TYPE_CHECKING: - from pathlib import Path - - from pytest_mock import MockerFixture - - -def test_cli_version(capsys: pytest.CaptureFixture[str]) -> None: - with pytest.raises(SystemExit) as context: - cli_args(["--version"]) - assert context.value.code == 0 - out, _err = capsys.readouterr() - assert out == f"pyproject-fmt ({version('pyproject-fmt')})\n" - - -def test_cli_invalid_version(capsys: pytest.CaptureFixture[str], tmp_path: Path) -> None: - path = tmp_path / "pyproject.toml" - path.write_text("") - with pytest.raises(SystemExit) as context: - cli_args([str(path), "--max-supported-python", "3"]) - assert context.value.code == 2 - out, err = capsys.readouterr() - assert not out - assert "error: argument --max-supported-python: invalid version: 3, must be e.g. 3.13\n" in err - - -def test_cli_invalid_version_value(capsys: pytest.CaptureFixture[str], tmp_path: Path) -> None: - path = tmp_path / "pyproject.toml" - path.write_text("") - with pytest.raises(SystemExit) as context: - cli_args([str(path), "--max-supported-python", "a.1"]) - assert context.value.code == 2 - out, err = capsys.readouterr() - assert not out - assert ( - "error: argument --max-supported-python: invalid version: a.1 due " - 'ValueError("invalid literal for int() with base 10:' - ) in err - - -def test_cli_pyproject_toml_ok(tmp_path: Path) -> None: - path = tmp_path / "tox.ini" - path.write_text("") - result = cli_args([str(path)]) - assert len(result) == 1 - assert result[0] - - -def test_cli_inputs_ok(tmp_path: Path) -> None: - paths = [] - for filename in ("tox.ini", "tox2.ini", "tox3.ini"): - path = tmp_path / filename - path.write_text("") - paths.append(path) - result = cli_args([*map(str, paths)]) - assert len(result) == 3 - - -def test_cli_pyproject_toml_stdin(mocker: MockerFixture) -> None: - mocker.patch("pyproject_fmt.cli.sys.stdin", io.StringIO("")) - result = cli_args(["-"]) - assert len(result) == 1 - assert result[0].pyproject_toml is None - assert not result[0].toml - - -def test_cli_pyproject_toml_not_exists( - tmp_path: Path, - capsys: pytest.CaptureFixture[str], -) -> None: - with pytest.raises(SystemExit) as context: - cli_args([str(tmp_path / "tox.ini")]) - assert context.value.code != 0 - out, err = capsys.readouterr() - assert not out - assert "argument inputs: path does not exist" in err - - -def test_cli_pyproject_toml_not_file( - tmp_path: Path, - capsys: pytest.CaptureFixture[str], -) -> None: - path = tmp_path / "temp" - os.mkfifo(path) - with pytest.raises(SystemExit) as context: - cli_args([str(path)]) - assert context.value.code != 0 - out, err = capsys.readouterr() - assert not out - assert "argument inputs: path is not a file" in err - - -@pytest.mark.parametrize(("flag", "error"), [(S_IREAD, "write"), (S_IWRITE, "read")]) -@pytest.mark.skipif( - sys.platform == "win32", - reason="On Windows files cannot be read only, only folders", -) -def test_cli_pyproject_toml_permission_fail( - tmp_path: Path, - capsys: pytest.CaptureFixture[str], - flag: int, - error: str, -) -> None: - path = tmp_path / "tox.ini" - path.write_text("") - path.chmod(flag) - try: - with pytest.raises(SystemExit) as context: - cli_args([str(path)]) - finally: - path.chmod(S_IWRITE | S_IREAD) - assert context.value.code != 0 - out, err = capsys.readouterr() - assert not out - assert f"argument inputs: cannot {error} path" in err - - -def test_pyproject_toml_resolved( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - monkeypatch.chdir(tmp_path) - path = tmp_path / "tox.ini" - path.write_text("") - result = cli_args(["tox.ini"]) - assert len(result) == 1 - - -def test_pyproject_toml_dir(tmp_path: Path) -> None: - (tmp_path / "pyproject.toml").write_text("") - cli_args([str(tmp_path)]) diff --git a/pyproject-fmt/tests/test_main.py b/pyproject-fmt/tests/test_main.py index d94c07f..b26b262 100644 --- a/pyproject-fmt/tests/test_main.py +++ b/pyproject-fmt/tests/test_main.py @@ -1,12 +1,11 @@ from __future__ import annotations -import difflib from textwrap import dedent -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING import pytest -from pyproject_fmt.__main__ import GREEN, RED, RESET, color_diff, run +from pyproject_fmt.__main__ import runner as run if TYPE_CHECKING: from pathlib import Path @@ -14,45 +13,6 @@ from pytest_mock import MockerFixture -def test_color_diff() -> None: - # Arrange - before = """ - abc - def - ghi -""" - after = """ - abc - abc - def -""" - diff = difflib.unified_diff(before.splitlines(), after.splitlines()) - expected_lines = f""" -{RED}--- -{RESET} -{GREEN}+++ -{RESET} -@@ -1,4 +1,4 @@ - - - abc -{GREEN}+ abc{RESET} - def -{RED}- ghi{RESET} -""".strip().splitlines() - - # Act - found_diff = color_diff(diff) - - # Assert - output_lines = [line.rstrip() for line in "\n".join(found_diff).splitlines()] - assert output_lines == expected_lines - - -def no_color(diff: Any) -> Any: - return diff - - @pytest.mark.parametrize( "in_place", [ @@ -102,7 +62,7 @@ def test_main( cwd: bool, check: bool, ) -> None: - mocker.patch("pyproject_fmt.__main__.color_diff", no_color) + mocker.patch("toml_fmt_common._color_diff", lambda t: t) if cwd: monkeypatch.chdir(tmp_path) pyproject_toml = tmp_path / "pyproject.toml" diff --git a/pyproject-fmt/tox.toml b/pyproject-fmt/tox.toml index 92eb6aa..beed11b 100644 --- a/pyproject-fmt/tox.toml +++ b/pyproject-fmt/tox.toml @@ -11,40 +11,33 @@ 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}{/}pyproject_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", - ] }, - ], + [ + "pytest", + { replace = "posargs", extend = true, default = [ + "--durations", + "5", + "--junitxml", + "{work_dir}{/}junit.{env_name}.xml", + "--no-cov-on-fail", + "--cov", + "{env_site_packages_dir}{/}pyproject_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.fix] -description = "format the code base to adhere to our styles, and complain about what we cannot do automatically" -skip_install = true -dependency_groups = ["fix"] -pass_env = [{ replace = "ref", of = ["env_run_base", "pass_env"], extend = true }, "PROGRAMDATA"] -commands = [["pre-commit", "run", "--all-files", "--show-diff-on-failure", { replace = "posargs", extend = true }]] - [env.type] description = "run type check on code base" dependency_groups = ["type"] @@ -54,26 +47,26 @@ commands = [["mypy", "src{/}pyproject_fmt"], ["mypy", "tests"]] 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")', - ], + [ + "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] @@ -81,25 +74,25 @@ 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}", - ], + [ + "uv", + "build", + "--sdist", + "--wheel", + "--out-dir", + "{env_tmp_dir}", + ".", + ], + [ + "twine", + "check", + "{env_tmp_dir}{/}*", + ], + [ + "check-wheel-contents", + "--no-config", + "{env_tmp_dir}", + ], ] [env.dev] diff --git a/tox.toml b/tox.toml new file mode 100644 index 0000000..5691176 --- /dev/null +++ b/tox.toml @@ -0,0 +1,9 @@ +requires = ["tox>=4.22"] +env_list = ["fix"] + +[env.fix] +description = "format the code base to adhere to our styles, and complain about what we cannot do automatically" +skip_install = true +deps = ["pre-commit-uv>=4.1.3"] +pass_env = [{ replace = "ref", of = ["env_run_base", "pass_env"], extend = true }, "PROGRAMDATA"] +commands = [["pre-commit", "run", "--all-files", "--show-diff-on-failure", { replace = "posargs", extend = true }]]