From 0c36c6dcf7336106e4e32c07afe57d3be789094d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Fri, 18 Oct 2024 11:36:16 -0700 Subject: [PATCH] Extract common Python MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Bernát Gábor --- .gitignore | 10 +- common/README.md | 4 +- py-common/CHANGELOG.md | 0 py-common/LICENSE.txt | 18 ++ py-common/README.md | 5 + py-common/pyproject.toml | 97 ++++++ py-common/src/toml_fmt_common/__init__.py | 292 ++++++++++++++++++ py-common/src/toml_fmt_common/py.typed | 0 py-common/tests/test_app.py | 250 +++++++++++++++ py-common/tox.toml | 76 +++++ pyproject-fmt/.github/FUNDING.yml | 1 - pyproject-fmt/.github/SECURITY.md | 13 - pyproject-fmt/.github/dependabot.yml | 6 - pyproject-fmt/.github/release.yml | 5 - pyproject-fmt/.github/workflows/check.yaml | 93 ------ pyproject-fmt/.github/workflows/release.yaml | 135 -------- pyproject-fmt/.gitignore | 13 - pyproject-fmt/.rustfmt.toml | 1 - pyproject-fmt/pyproject.toml | 1 + pyproject-fmt/src/pyproject_fmt/__init__.py | 1 + pyproject-fmt/src/pyproject_fmt/__main__.py | 137 ++++---- pyproject-fmt/src/pyproject_fmt/cli.py | 208 ------------- .../src/pyproject_fmt/toml_fmt_common | 1 + pyproject-fmt/tox.toml | 128 ++++---- tox.toml | 16 + 25 files changed, 893 insertions(+), 618 deletions(-) create mode 100644 py-common/CHANGELOG.md create mode 100644 py-common/LICENSE.txt create mode 100644 py-common/README.md create mode 100644 py-common/pyproject.toml create mode 100644 py-common/src/toml_fmt_common/__init__.py create mode 100644 py-common/src/toml_fmt_common/py.typed create mode 100644 py-common/tests/test_app.py create mode 100644 py-common/tox.toml delete mode 100644 pyproject-fmt/.github/FUNDING.yml delete mode 100644 pyproject-fmt/.github/SECURITY.md delete mode 100644 pyproject-fmt/.github/dependabot.yml delete mode 100644 pyproject-fmt/.github/release.yml delete mode 100644 pyproject-fmt/.github/workflows/check.yaml delete mode 100644 pyproject-fmt/.github/workflows/release.yaml delete mode 100644 pyproject-fmt/.gitignore delete mode 100644 pyproject-fmt/.rustfmt.toml delete mode 100644 pyproject-fmt/src/pyproject_fmt/cli.py create mode 120000 pyproject-fmt/src/pyproject_fmt/toml_fmt_common create mode 100644 tox.toml 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/py-common/CHANGELOG.md b/py-common/CHANGELOG.md new file mode 100644 index 0000000..e69de29 diff --git a/py-common/LICENSE.txt b/py-common/LICENSE.txt new file mode 100644 index 0000000..3649823 --- /dev/null +++ b/py-common/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/py-common/README.md b/py-common/README.md new file mode 100644 index 0000000..4c04b97 --- /dev/null +++ b/py-common/README.md @@ -0,0 +1,5 @@ +# toml-fmt-common + +Contains Python code common to all formatters under the `toml-fmt` umbrella. + +[![Test common](https://github.com/tox-dev/toml-fmt/actions/workflows/py-common.yaml/badge.svg)](https://github.com/tox-dev/toml-fmt/actions/workflows/py-common.yaml) diff --git a/py-common/pyproject.toml b/py-common/pyproject.toml new file mode 100644 index 0000000..897cce9 --- /dev/null +++ b/py-common/pyproject.toml @@ -0,0 +1,97 @@ +[build-system] +build-backend = "hatchling.build" +requires = [ + "hatchling>=1.25", +] + +[project] +name = "toml-fmt-common" +version = "1.0.0" +description = "Common logic to the TOML formatter." +readme = "README.md" +license = "MIT" +maintainers = [ + { name = "Bernát Gábor", email = "gaborjbernat@gmail.com" }, +] +requires-python = ">=3.9" +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "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", + "Topic :: Internet", + "Topic :: Software Development :: Libraries", + "Topic :: System", +] +dependencies = [ + "tomli>=2.0.2; python_version<'3.11'", +] +urls."Bug Tracker" = "https://github.com/tox-dev/toml-fmt/issues" +urls."Changelog" = "https://github.com/tox-dev/toml-fmt/blob/main/py-common/CHANGELOG.md" +urls.Documentation = "https://github.com/tox-dev/toml-fmt/" +urls."Source Code" = "https://github.com/tox-dev/toml-fmt" + +[tool.ruff] +extend = "../pyproject.toml" +lint.isort = { known-first-party = [ + "toml_fmt_common", + "tests", +], required-imports = [ + "from __future__ import annotations", +] } + +[tool.pyproject-fmt] +max_supported_python = "3.13" + +[tool.coverage] +html.show_contexts = true +html.skip_covered = false +paths.source = [ + "src", + ".tox/*/lib/*/site-packages", + ".tox\\*\\Lib\\site-packages", + "**/src", + "**\\src", +] +paths.other = [ + ".", + "*/toml_fmt_common", + "*\\toml_fmt_common", +] +report.fail_under = 100 +run.parallel = true +run.plugins = [ + "covdefaults", +] + +[tool.mypy] +show_error_codes = true +strict = true + +[dependency-groups] +dev = [ + { include-group = "test" }, + { include-group = "type" }, +] +pkg-meta = [ + "check-wheel-contents>=0.6", + "twine>=5.1.1", + "uv>=0.4.17", +] +test = [ + "covdefaults>=2.3", + "pytest>=8.3.2", + "pytest-cov>=5", + "pytest-mock>=3.14", +] +type = [ + "mypy==1.11.2", + { include-group = "test" }, +] diff --git a/py-common/src/toml_fmt_common/__init__.py b/py-common/src/toml_fmt_common/__init__.py new file mode 100644 index 0000000..1745aa8 --- /dev/null +++ b/py-common/src/toml_fmt_common/__init__.py @@ -0,0 +1,292 @@ +"""Common logic for a TOML formatter.""" + +from __future__ import annotations + +import difflib +import os +import sys +from abc import ABC, abstractmethod +from argparse import ArgumentDefaultsHelpFormatter, ArgumentParser, ArgumentTypeError, Namespace +from collections import deque +from copy import deepcopy +from dataclasses import dataclass +from functools import partial +from importlib.metadata import version +from pathlib import Path +from typing import TYPE_CHECKING, Any, Generic, TypeVar + +if TYPE_CHECKING: + from collections.abc import Iterable, Sequence + +if sys.version_info >= (3, 11): # pragma: >=3.11 cover + import tomllib +else: # pragma: <3.11 cover + import tomli as tomllib + + +class FmtNamespace(Namespace): + """Options for pyproject-fmt tool.""" + + inputs: list[Path] + stdout: bool + check: bool + no_print_diff: bool + + column_width: int + indent: int + + +T = TypeVar("T", bound=FmtNamespace) + + +class TOMLFormatter(ABC, Generic[T]): + """API for a TOML formatter.""" + + def __init__(self, opt: T) -> None: + self.opt: T = opt + + @property + @abstractmethod + def prog(self) -> str: + """:returns: name of the application (must be same as the package name)""" + raise NotImplementedError + + @property + @abstractmethod + def filename(self) -> str: + """:returns: name of the file type it formats""" + raise NotImplementedError + + @abstractmethod + def add_format_flags(self, parser: ArgumentParser) -> None: + """ + Add any additional flags to configure the formatter. + + :param parser: the parser to operate on + """ + raise NotImplementedError + + @property + @abstractmethod + def override_cli_from_section(self) -> tuple[str, ...]: + """ + Allow overriding CLI defaults from within the TOML files this section. + + :returns: the section path + """ + raise NotImplementedError + + @abstractmethod + def format(self, text: str, opt: T) -> str: + """ + Run the formatter. + + :param text: the TOML text to format + :param opt: the flags to format with + :returns: the formatted TOML text + """ + raise NotImplementedError + + + + +def run(info: TOMLFormatter[T], args: Sequence[str] | None = None) -> int: + """ + Run the formatter. + + :param info: information specific to the current formatter + :param args: command line arguments, by default use sys.argv[1:] + :return: exit code - 0 means already formatted correctly, otherwise 1 + """ + configs = _cli_args(info, sys.argv[1:] if args is None else args) + results = [_handle_one(info, config) for config in configs] + return 1 if any(results) else 0 # exit with non success on change + + +@dataclass(frozen=True) +class _Config(Generic[T]): + """Configuration flags for the formatting.""" + + toml_filename: 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 + opt: T + + +def _cli_args(info: TOMLFormatter[T], args: Sequence[str]) -> list[_Config[T]]: + """ + Load the tools options. + + :param info: information + :param args: CLI arguments + :return: the parsed options + """ + parser = _build_cli(info) + parser.parse_args(namespace=info.opt, args=args) + res = [] + for pyproject_toml in info.opt.inputs: + raw_pyproject_toml = sys.stdin.read() if pyproject_toml is None else pyproject_toml.read_text(encoding="utf-8") + config: dict[str, Any] | None = tomllib.loads(raw_pyproject_toml) + + parts = deque(info.override_cli_from_section) + while parts: # pragma: no branch + part = parts.popleft() + if not isinstance(config, dict) or part not in config: + config = None + break + config = config[part] + override_opt = deepcopy(info.opt) + if isinstance(config, dict): + for key in set(vars(override_opt).keys()) - {"inputs", "stdout", "check", "no_print_diff"}: + if key in config: + setattr(override_opt, key, config[key]) + res.append( + _Config( + toml_filename=pyproject_toml, + toml=raw_pyproject_toml, + stdout=info.opt.stdout, + check=info.opt.check, + no_print_diff=info.opt.no_print_diff, + opt=override_opt, + ) + ) + + return res + + +def _build_cli(of: TOMLFormatter[T]) -> ArgumentParser: + parser = ArgumentParser( + formatter_class=ArgumentDefaultsHelpFormatter, + prog=of.prog, + ) + parser.add_argument( + "-V", + "--version", + action="version", + help="print package version of pyproject_fmt", + version=f"%(prog)s ({version(of.prog)})", + ) + + 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", + ) + of.add_format_flags(format_group) # type: ignore[arg-type] + msg = "pyproject.toml file(s) to format, use '-' to read from stdin" + parser.add_argument( + "inputs", + nargs="+", + type=partial(_toml_path_creator, of.filename), + help=msg, + ) + return parser + + +def _toml_path_creator(filename: str, argument: str) -> Path | None: + """ + Validate that toml can be formatted. + + :param filename: name of the toml file + :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 /= filename + 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 _handle_one(info: TOMLFormatter[T], config: _Config[T]) -> bool: + formatted = info.format(config.toml, config.opt) + before = config.toml + changed = before != formatted + if config.toml_filename 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.toml_filename.write_text(formatted, encoding="utf-8") + if config.no_print_diff: + return changed + try: + name = str(config.toml_filename.relative_to(Path.cwd())) + except ValueError: + name = str(config.toml_filename) + 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 + + +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 + + +__all__ = [ + "FmtNamespace", + "TOMLFormatter", + "run", +] diff --git a/py-common/src/toml_fmt_common/py.typed b/py-common/src/toml_fmt_common/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/py-common/tests/test_app.py b/py-common/tests/test_app.py new file mode 100644 index 0000000..dfc7ce9 --- /dev/null +++ b/py-common/tests/test_app.py @@ -0,0 +1,250 @@ +from __future__ import annotations + +import os +from io import StringIO +from typing import TYPE_CHECKING + +import pytest + +from toml_fmt_common import GREEN, RED, RESET, FmtNamespace, TOMLFormatter, run + +if TYPE_CHECKING: + from argparse import ArgumentParser + from pathlib import Path + + from pytest_mock import MockerFixture + + +class DumpNamespace(FmtNamespace): + extra: str + + +class Dumb(TOMLFormatter[DumpNamespace]): + + def __init__(self) -> None: + super().__init__(DumpNamespace()) + + @property + def prog(self) -> str: + return "toml-fmt-common" + + @property + def filename(self) -> str: + return "dumb.toml" + + @property + def override_cli_from_section(self) -> tuple[str, ...]: + return "start", "sub" + + def add_format_flags(self, parser: ArgumentParser) -> None: # noqa: PLR6301 + parser.add_argument("extra", help="this is something extra") + + def format(self, text: str, opt: DumpNamespace) -> str: # noqa: PLR6301 + return text if os.environ.get("NO_FMT") else f"{text}\nextras = {opt.extra!r}" + + +def test_dumb_help(capsys: pytest.CaptureFixture[str]) -> None: + with pytest.raises(SystemExit) as exc: + run(Dumb(), ["--help"]) + + assert exc.value.code == 0 + + out, err = capsys.readouterr() + assert not err + assert "this is something extra" in out + + +def test_dumb_format_with_override(capsys: pytest.CaptureFixture[str], tmp_path: Path) -> None: + dumb = tmp_path / "dumb.toml" + dumb.write_text("[start.sub]\nextra = 'B'") + + exit_code = run(Dumb(), ["E", str(dumb)]) + assert exit_code == 1 + + assert dumb.read_text() == "[start.sub]\nextra = 'B'\nextras = 'B'" + + out, err = capsys.readouterr() + assert not err + assert out.splitlines() == [ + f"{RED}--- {dumb}", + f"{RESET}", + f"{GREEN}+++ {dumb}", + f"{RESET}", + "@@ -1,2 +1,3 @@", + "", + " [start.sub]", + " extra = 'B'", + f"{GREEN}+extras = 'B'{RESET}", + ] + + +def test_dumb_format_no_print_diff(capsys: pytest.CaptureFixture[str], tmp_path: Path) -> None: + dumb = tmp_path / "dumb.toml" + dumb.write_text("[start.sub]\nextra = 'B'") + + exit_code = run(Dumb(), ["E", str(dumb), "--no-print-diff"]) + assert exit_code == 1 + + assert dumb.read_text() == "[start.sub]\nextra = 'B'\nextras = 'B'" + + out, err = capsys.readouterr() + assert not err + assert out.splitlines() == [] + + +def test_dumb_format_already_good( + capsys: pytest.CaptureFixture[str], tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + monkeypatch.setenv("NO_FMT", "1") + dumb = tmp_path / "dumb.toml" + dumb.write_text("[start.sub]\nextra = 'B'") + + exit_code = run(Dumb(), ["E", str(dumb)]) + assert exit_code == 0 + + assert dumb.read_text() == "[start.sub]\nextra = 'B'" + + out, err = capsys.readouterr() + assert not err + assert out.splitlines() == [f"no change for {dumb}"] + + +def test_dumb_format_via_folder( + capsys: pytest.CaptureFixture[str], tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + monkeypatch.chdir(tmp_path) + dumb = tmp_path / "dumb.toml" + dumb.write_text("") + + exit_code = run(Dumb(), ["E", "."]) + assert exit_code == 1 + + assert dumb.read_text() == "\nextras = 'E'" + + out, err = capsys.readouterr() + assert not err + assert out.splitlines() == [ + f"{RED}--- dumb.toml", + f"{RESET}", + f"{GREEN}+++ dumb.toml", + f"{RESET}", + "@@ -0,0 +1,2 @@", + "", + f"{GREEN}+{RESET}", + f"{GREEN}+extras = 'E'{RESET}", + ] + + +def test_dumb_format_override_non_dict_result(capsys: pytest.CaptureFixture[str], tmp_path: Path) -> None: + dumb = tmp_path / "dumb.toml" + dumb.write_text("[start]\nsub = 'B'") + + exit_code = run(Dumb(), ["E", str(dumb)]) + assert exit_code == 1 + + assert dumb.read_text() == "[start]\nsub = 'B'\nextras = 'E'" + + out, err = capsys.readouterr() + assert not err + assert out.splitlines() == [ + f"{RED}--- {dumb}", + f"{RESET}", + f"{GREEN}+++ {dumb}", + f"{RESET}", + "@@ -1,2 +1,3 @@", + "", + " [start]", + " sub = 'B'", + f"{GREEN}+extras = 'E'{RESET}", + ] + + +def test_dumb_format_override_non_dict_part(capsys: pytest.CaptureFixture[str], tmp_path: Path) -> None: + dumb = tmp_path / "dumb.toml" + dumb.write_text("start = 'B'") + + exit_code = run(Dumb(), ["E", str(dumb)]) + assert exit_code == 1 + + assert dumb.read_text() == "start = 'B'\nextras = 'E'" + + out, err = capsys.readouterr() + assert not err + assert out.splitlines() == [ + f"{RED}--- {dumb}", + f"{RESET}", + f"{GREEN}+++ {dumb}", + f"{RESET}", + "@@ -1 +1,2 @@", + "", + " start = 'B'", + f"{GREEN}+extras = 'E'{RESET}", + ] + + +def test_dumb_stdin(capsys: pytest.CaptureFixture[str], mocker: MockerFixture) -> None: + mocker.patch("sys.stdin", StringIO("ok = 1")) + + exit_code = run(Dumb(), ["E", "-"]) + assert exit_code == 1 + + out, err = capsys.readouterr() + assert not err + assert out.splitlines() == ["ok = 1", "extras = 'E'"] + + +def test_dumb_path_missing(capsys: pytest.CaptureFixture[str], tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.chdir(tmp_path) + + with pytest.raises(SystemExit): + run(Dumb(), ["E", "dumb.toml"]) + + out, err = capsys.readouterr() + assert "\ntoml-fmt-common: error: argument inputs: path does not exist\n" in err + assert not out + + +def test_dumb_path_is_folder(capsys: pytest.CaptureFixture[str], tmp_path: Path) -> None: + toml = tmp_path / "dumb.toml" + os.mkfifo(toml) + + with pytest.raises(SystemExit): + run(Dumb(), ["E", str(toml)]) + + out, err = capsys.readouterr() + assert "\ntoml-fmt-common: error: argument inputs: path is not a file\n" in err + assert not out + + +def test_dumb_path_no_read(capsys: pytest.CaptureFixture[str], tmp_path: Path) -> None: + toml = tmp_path / "dumb.toml" + toml.write_text("") + start = toml.stat().st_mode + toml.chmod(0o000) + + try: + with pytest.raises(SystemExit): + run(Dumb(), ["E", str(toml)]) + finally: + toml.chmod(start) + + out, err = capsys.readouterr() + assert "\ntoml-fmt-common: error: argument inputs: cannot read path\n" in err + assert not out + + +def test_dumb_path_no_write(capsys: pytest.CaptureFixture[str], tmp_path: Path) -> None: + toml = tmp_path / "dumb.toml" + toml.write_text("") + start = toml.stat().st_mode + toml.chmod(0o400) + + try: + with pytest.raises(SystemExit): + run(Dumb(), ["E", str(toml)]) + finally: + toml.chmod(start) + + out, err = capsys.readouterr() + assert "\ntoml-fmt-common: error: argument inputs: cannot write path\n" in err + assert not out diff --git a/py-common/tox.toml b/py-common/tox.toml new file mode 100644 index 0000000..347becb --- /dev/null +++ b/py-common/tox.toml @@ -0,0 +1,76 @@ +requires = ["tox>=4.22"] +env_list = ["3.13", "3.12", "3.11", "3.10", "3.9", "type", "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}{/}toml_fmt_common", + "--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{/}toml_fmt_common", "tests"]] + +[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}")']] 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/pyproject.toml b/pyproject-fmt/pyproject.toml index 8f0944b..0ff2030 100644 --- a/pyproject-fmt/pyproject.toml +++ b/pyproject-fmt/pyproject.toml @@ -45,6 +45,7 @@ bindings = "pyo3" manifest-path = "Cargo.toml" module-name = "pyproject_fmt._lib" python-source = "src" +python-packages = [ "pyproject_fmt/toml_fmt_common" ] strip = true include = [ "rust-toolchain.toml", diff --git a/pyproject-fmt/src/pyproject_fmt/__init__.py b/pyproject-fmt/src/pyproject_fmt/__init__.py index 578a4cf..408615a 100644 --- a/pyproject-fmt/src/pyproject_fmt/__init__.py +++ b/pyproject-fmt/src/pyproject_fmt/__init__.py @@ -4,6 +4,7 @@ from .__main__ import run + __all__ = [ "run", ] diff --git a/pyproject-fmt/src/pyproject_fmt/__main__.py b/pyproject-fmt/src/pyproject_fmt/__main__.py index 85a3046..f0f41e5 100644 --- a/pyproject-fmt/src/pyproject_fmt/__main__.py +++ b/pyproject-fmt/src/pyproject_fmt/__main__.py @@ -1,79 +1,72 @@ """Main entry point for the formatter.""" from __future__ import annotations +from __future__ import annotations + +from argparse import ArgumentParser, ArgumentTypeError +from typing import Sequence + +from .toml_fmt_common import TOMLFormatter, FmtNamespace, run +from ._lib import Settings, format_toml + + +class PyProjectFmtNamespace(FmtNamespace): + keep_full_version: bool + max_supported_python: tuple[int, int] + + +class PyProjectFormatter(TOMLFormatter[PyProjectFmtNamespace]): + + def __init__(self) -> None: + super().__init__(PyProjectFmtNamespace()) + + @property + def prog(self) -> str: + return "pyproject-fmt" + + @property + def filename(self) -> str: + return "pyproject.toml" + + def add_format_flags(self, parser: ArgumentParser) -> None: + 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 "tool", "pyproject-fmt" -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: - """ - Run the formatter. - - :param args: command line arguments, by default use sys.argv[1:] - :return: exit code - 0 means already formatted correctly, otherwise 1 - """ - 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 + def format(self, content: str, opt: PyProjectFmtNamespace) -> str: + 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 did not specify via requires-python + ) + return format_toml(content, settings) +def runner(args: Sequence[str] | None = None) -> int: + return run(PyProjectFormatter(), args) 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/src/pyproject_fmt/toml_fmt_common b/pyproject-fmt/src/pyproject_fmt/toml_fmt_common new file mode 120000 index 0000000..f382715 --- /dev/null +++ b/pyproject-fmt/src/pyproject_fmt/toml_fmt_common @@ -0,0 +1 @@ +../../../py-common/src/toml_fmt_common \ No newline at end of file diff --git a/pyproject-fmt/tox.toml b/pyproject-fmt/tox.toml index 92eb6aa..98fb49d 100644 --- a/pyproject-fmt/tox.toml +++ b/pyproject-fmt/tox.toml @@ -11,31 +11,31 @@ 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] @@ -54,26 +54,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 +81,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..c004755 --- /dev/null +++ b/tox.toml @@ -0,0 +1,16 @@ +requires = ["tox>=4.22"] +env_list = ["fix"] +skip_missing_interpreters = true + +[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 }]] + +[env.dev] +package = "skip" +description = "dev environment" +deps = ["pytest>=8.3.3", "-e pyproject-fmt"] +commands = [["uv", "pip", "tree"], ["python", "-c", 'print(r"{env_python}")']]