From 822db3e63adbc7dd845dc77bf0969b10ee395755 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Wed, 16 Oct 2024 22:47:16 -0700 Subject: [PATCH] Make it mirror repository MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Bernát Gábor --- .github/workflows/check.yaml | 1 - .github/workflows/mirror.yaml | 27 ++ .github/workflows/release.yaml | 48 --- .gitignore | 6 +- .pre-commit-config.yaml | 17 +- .readthedocs.yml | 9 - CODE_OF_CONDUCT.md | 60 ---- README.md | 17 +- docs/conf.py | 43 --- docs/index.rst | 91 ------ pyproject.toml | 75 ++--- src/pyproject_fmt/__init__.py | 11 - src/pyproject_fmt/__main__.py | 80 ----- src/pyproject_fmt/cli.py | 208 ------------ src/pyproject_fmt/py.typed => tasks/mirror.py | 0 tests/test_cli.py | 143 --------- tests/test_main.py | 302 ------------------ tests/test_mirror.py | 59 ++++ tests/test_pyproject_toml_fmt.py | 15 - tox.ini | 80 ----- tox.toml | 55 ++++ 21 files changed, 187 insertions(+), 1160 deletions(-) create mode 100644 .github/workflows/mirror.yaml delete mode 100644 .github/workflows/release.yaml delete mode 100644 .readthedocs.yml delete mode 100644 CODE_OF_CONDUCT.md delete mode 100644 docs/conf.py delete mode 100644 docs/index.rst delete mode 100644 src/pyproject_fmt/__init__.py delete mode 100644 src/pyproject_fmt/__main__.py delete mode 100644 src/pyproject_fmt/cli.py rename src/pyproject_fmt/py.typed => tasks/mirror.py (100%) delete mode 100644 tests/test_cli.py delete mode 100644 tests/test_main.py create mode 100644 tests/test_mirror.py delete mode 100644 tests/test_pyproject_toml_fmt.py delete mode 100644 tox.ini create mode 100644 tox.toml diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index e9b02b7..91fbecf 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -26,7 +26,6 @@ jobs: - "3.9" - type - dev - - pkg_meta steps: - uses: actions/checkout@v4 with: diff --git a/.github/workflows/mirror.yaml b/.github/workflows/mirror.yaml new file mode 100644 index 0000000..5d8a263 --- /dev/null +++ b/.github/workflows/mirror.yaml @@ -0,0 +1,27 @@ +name: Mirror +on: + workflow_dispatch: + push: + branches: ["main"] + tags-ignore: ["**"] + pull_request: + schedule: + - cron: "0 8 * * *" + +concurrency: + group: check-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + runs-on: ubuntu-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" + github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml deleted file mode 100644 index 11ccc68..0000000 --- a/.github/workflows/release.yaml +++ /dev/null @@ -1,48 +0,0 @@ -name: Release to PyPI -on: - push: - tags: ["*"] - -env: - dists-artifact-name: python-package-distributions - -jobs: - build: - runs-on: ubuntu-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" - github-token: ${{ secrets.GITHUB_TOKEN }} - - name: Build package - run: uv build --python 3.13 --python-preference only-managed --sdist --wheel . --out-dir dist - - name: Store the distribution packages - uses: actions/upload-artifact@v4 - with: - name: ${{ env.dists-artifact-name }} - path: dist/* - - release: - needs: - - build - runs-on: ubuntu-latest - environment: - name: release - url: https://pypi.org/project/pyproject-fmt/${{ github.ref_name }} - permissions: - id-token: write - steps: - - name: Download all the dists - uses: actions/download-artifact@v4 - with: - name: ${{ env.dists-artifact-name }} - path: dist/ - - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@v1.10.3 - with: - attestations: true diff --git a/.gitignore b/.gitignore index aae66af..4425c22 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,3 @@ -*.egg-info/ -.tox/ .*_cache +.tox __pycache__ -**.pyc -dist -/src/pyproject_fmt/_version.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5ab1c97..f63ae6c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,29 +13,24 @@ repos: rev: v2.3.0 hooks: - id: codespell - additional_dependencies: ["tomli>=2.0.2"] - - repo: https://github.com/tox-dev/tox-ini-fmt - rev: "1.4.1" - hooks: - - id: tox-ini-fmt - args: ["-p", "fix"] + additional_dependencies: ["tomli>=2.0.1"] - repo: https://github.com/tox-dev/pyproject-fmt - rev: "2.3.0" + rev: "2.3.1" hooks: - id: pyproject-fmt - repo: https://github.com/astral-sh/ruff-pre-commit rev: "v0.6.9" hooks: - id: ruff-format + args: ["--config", "pyproject.toml"] - id: ruff - args: ["--fix", "--unsafe-fixes", "--exit-non-zero-on-fix"] + args: ["--fix", "--unsafe-fixes", "--exit-non-zero-on-fix", "--config", "pyproject.toml"] - repo: https://github.com/rbubley/mirrors-prettier rev: "v3.3.3" hooks: - id: prettier - additional_dependencies: - - prettier@3.3.3 - - "@prettier/plugin-xml@3.4.1" + name: Prettier + args: ["--print-width=120", "--prose-wrap=always"] - repo: meta hooks: - id: check-hooks-apply diff --git a/.readthedocs.yml b/.readthedocs.yml deleted file mode 100644 index 68e7e75..0000000 --- a/.readthedocs.yml +++ /dev/null @@ -1,9 +0,0 @@ -version: 2 -build: - os: ubuntu-22.04 - tools: - python: "3.12" - commands: - - pip install tox-uv - - tox r -e docs -vv --notest - - tox r -e docs --skip-pkg-install -- "${READTHEDOCS_OUTPUT}"/html diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md deleted file mode 100644 index 267eaab..0000000 --- a/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,60 +0,0 @@ -# Contributor Covenant Code of Conduct - -## Our Pledge - -In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making -participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, -disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, -religion, or sexual identity and orientation. - -## Our Standards - -Examples of behavior that contributes to creating a positive environment include: - -- Using welcoming and inclusive language -- Being respectful of differing viewpoints and experiences -- Gracefully accepting constructive criticism -- Focusing on what is best for the community -- Showing empathy towards other community members - -Examples of unacceptable behavior by participants include: - -- The use of sexualized language or imagery and unwelcome sexual attention or advances -- Trolling, insulting/derogatory comments, and personal or political attacks -- Public or private harassment -- Publishing others' private information, such as a physical or electronic address, without explicit permission -- Other conduct which could reasonably be considered inappropriate in a professional setting - -## Our Responsibilities - -Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take -appropriate and fair corrective action in response to any instances of unacceptable behavior. - -Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, -issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any -contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. - -## Scope - -This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the -project or its community. Examples of representing a project or community include using an official project e-mail -address, posting via an official social media account, or acting as an appointed representative at an online or offline -event. Representation of a project may be further defined and clarified by project maintainers. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at -gaborbernat@python.org. The project team will review and investigate all complaints, and will respond in a way that it -deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the -reporter of an incident. Further details of specific enforcement policies may be posted separately. - -Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent -repercussions as determined by other members of the project's leadership. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at -[https://www.contributor-covenant.org/version/1/4/code-of-conduct.html][version] - -[homepage]: https://www.contributor-covenant.org/ -[version]: https://www.contributor-covenant.org/version/1/4/ diff --git a/README.md b/README.md index ab0d377..3c16204 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,5 @@ -# pyproject-fmt +# pyproject-fmt mirror -Migrated into the [toml-fmt](https://github.com/gaborbernat/toml-fmt) repository. - -[![PyPI](https://img.shields.io/pypi/v/pyproject-fmt?style=flat-square)](https://pypi.org/project/pyproject-fmt) -[![PyPI - Implementation](https://img.shields.io/pypi/implementation/pyproject-fmt?style=flat-square)](https://pypi.org/project/pyproject-fmt) -[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pyproject-fmt?style=flat-square)](https://pypi.org/project/pyproject-fmt) -[![Downloads](https://static.pepy.tech/badge/pyproject-fmt/month)](https://pepy.tech/project/pyproject-fmt) -[![PyPI - License](https://img.shields.io/pypi/l/pyproject-fmt?style=flat-square)](https://opensource.org/licenses/MIT) -[![check](https://github.com/tox-dev/pyproject-fmt/actions/workflows/check.yaml/badge.svg)](https://github.com/tox-dev/pyproject-fmt/actions/workflows/check.yaml) - -Apply a consistent format to the `pyproject.toml` files. - -[For more information on how to use or configure read the documentation here](https://pyproject-fmt.readthedocs.io/en/latest/). +The source code for this project now lives inside +[toml-fmt](https://github.com/tox-dev/toml-fmt/tree/main/pyproject-fmt), this repository serves only as a mirror for +pre-commit. diff --git a/docs/conf.py b/docs/conf.py deleted file mode 100644 index 01b2dce..0000000 --- a/docs/conf.py +++ /dev/null @@ -1,43 +0,0 @@ -"""Configuration for documentation build.""" # noqa: INP001 - -from __future__ import annotations - -from datetime import datetime, timezone - -from pyproject_fmt import __version__ - -company, name = "tox-dev", "pyproject-fmt" -release, version = __version__, ".".join(__version__.split(".")[:2]) -now = datetime.now(tz=timezone.utc) -copyright = f"2022-{now.year}, {company}" # noqa: A001 -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_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 = { - "issue": (f"https://github.com/{company}/{name}/issues/%s", "#%s"), - "user": ("https://github.com/%s", "@%s"), - "gh": ("https://github.com/%s", "%s"), -} -intersphinx_mapping = {"python": ("https://docs.python.org/3", None)} -nitpicky = True -nitpick_ignore = [] diff --git a/docs/index.rst b/docs/index.rst deleted file mode 100644 index b1c15d3..0000000 --- a/docs/index.rst +++ /dev/null @@ -1,91 +0,0 @@ -pyproject-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`` -~~~~~~~~~~~ - -Use `pipx `_ to install the project: - -.. code-block:: shell - - pipx install pyproject-fmt - - -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/pyproject-fmt - rev: "2.0.4" - hooks: - - id: pyproject-fmt - -Via Python -~~~~~~~~~~ - -.. automodule:: pyproject_fmt - :members: - -.. toctree:: - :hidden: - - self - -Configuration via file ----------------------- - -The ``tool.pyproject-fmt`` table is used when present in the ``pyproject.toml`` file: - -.. code-block:: toml - - [tool.pyproject-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 false will remove unnecessary trailing ``.0``'s from version specifiers - keep_full_version = false - - # maximum Python version to use when generating version specifiers - max_supported_python = "3.12" - -If not set they will default to values from the CLI, the example above shows the defaults. - -Command line interface ----------------------- -.. sphinx_argparse_cli:: - :module: pyproject_fmt.cli - :func: _build_cli - :prog: pyproject-fmt - :title: - -Python version classifiers --------------------------- - -This tool will automatically generate the ``Programming Language :: Python :: 3.X`` classifiers for you. To do so it -needs to know the range of Python interpreter versions you support: - -- The lower bound can be set via the ``requires-python`` key in the ``pyproject.toml`` configuration file (defaults to - the oldest non end of line CPython version at the time of the release). -- The upper bound, by default, will assume the latest stable release of CPython at the time of the release, but can be - changed via CLI flag or the config file. diff --git a/pyproject.toml b/pyproject.toml index 747fdae..1734ced 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,12 +1,12 @@ [build-system] build-backend = "hatchling.build" requires = [ - "hatch-vcs>=0.4", "hatchling>=1.25", ] [project] -name = "pyproject-fmt" +name = "pyproject-fmt-mirror" +version = "2.4.2" description = "Format your pyproject.toml file" readme = "README.md" keywords = [ @@ -29,43 +29,12 @@ classifiers = [ "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", ] -dynamic = [ - "version", -] dependencies = [ - "pyproject-fmt-rust==1.2.1", - "tomli>=2.0.2; python_version<'3.11'", -] -optional-dependencies.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", -] -optional-dependencies.test = [ - "covdefaults>=2.3", - "pytest>=8.3.3", - "pytest-cov>=5", - "pytest-mock>=3.14", + "pyproject-fmt==2.4.2", ] -urls."Bug Tracker" = "https://github.com/tox-dev/pyproject-fmt/issues" -urls."Changelog" = "https://github.com/tox-dev/pyproject-fmt/releases" -urls.Documentation = "https://github.com/tox-dev/pyproject-fmt/" -urls."Source Code" = "https://github.com/tox-dev/pyproject-fmt" -scripts.pyproject-fmt = "pyproject_fmt.__main__:run" [tool.hatch] -build.dev-mode-dirs = [ - "src", -] -build.hooks.vcs.version-file = "src/pyproject_fmt/_version.py" -build.targets.sdist.include = [ - "/src", - "/tests", - "tox.ini", -] -version.source = "vcs" +build.targets.wheel.bypass-selection = true [tool.ruff] line-length = 120 @@ -87,6 +56,10 @@ lint.ignore = [ "ISC001", # Conflict with formatter "S104", # Possible binding to all interfaces ] +lint.per-file-ignores."tasks/**/*.py" = [ + "D", # don't care about documentation in tsask files + "INP001", # not a package +] lint.per-file-ignores."tests/**/*.py" = [ "D", # don't care about documentation in tests "FBT", # don't care about booleans as positional arguments in tests @@ -95,12 +68,9 @@ lint.per-file-ignores."tests/**/*.py" = [ "PLR0913", # any number of arguments in tests "PLR0917", # any number of arguments in tests "PLR2004", # Magic value used in comparison, consider replacing with a constant variable - "S101", # asserts allowed in tests - "S603", # `subprocess` call: check for execution of untrusted input + "S", # no security concerns in tests ] -lint.isort = { known-first-party = [ - "pyproject_fmt", -], required-imports = [ +lint.isort = { known-first-party = [ ], required-imports = [ "from __future__ import annotations", ] } lint.preview = true @@ -116,6 +86,7 @@ max_supported_python = "3.13" ini_options.testpaths = [ "tests", ] +ini_options.verbosity_assertions = 2 [tool.coverage] html.show_contexts = true @@ -139,3 +110,27 @@ covdefaults.subtract_omit = "*/__main__.py" [tool.mypy] show_error_codes = true strict = true + +[dependency-groups] +dev = [ + { include-group = "tasks" }, + { include-group = "test" }, + { include-group = "type" }, +] +fix = [ + "pre-commit-uv>=4.1.3", +] +tasks = [ ] +test = [ + "covdefaults>=2.3", + "pygments>=2.18", + "pytest>=8.3.2", + "pytest-cov>=5", + { include-group = "fix" }, +] +type = [ + "mypy==1.11.2", + "types-cachetools>=5.5.0.20240820", + "types-chardet>=5.0.4.6", + { include-group = "test" }, +] diff --git a/src/pyproject_fmt/__init__.py b/src/pyproject_fmt/__init__.py deleted file mode 100644 index 3727ab5..0000000 --- a/src/pyproject_fmt/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -"""Package root.""" - -from __future__ import annotations - -from .__main__ import run -from ._version import __version__ - -__all__ = [ - "__version__", - "run", -] diff --git a/src/pyproject_fmt/__main__.py b/src/pyproject_fmt/__main__.py deleted file mode 100644 index 4b74ace..0000000 --- a/src/pyproject_fmt/__main__.py +++ /dev/null @@ -1,80 +0,0 @@ -"""Main entry point for the formatter.""" - -from __future__ import annotations - -import difflib -import sys -from pathlib import Path -from typing import TYPE_CHECKING - -from pyproject_fmt_rust 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 - - -if __name__ == "__main__": - raise SystemExit(run()) diff --git a/src/pyproject_fmt/cli.py b/src/pyproject_fmt/cli.py deleted file mode 100644 index 3cfc540..0000000 --- a/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 pyproject_fmt_rust 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/src/pyproject_fmt/py.typed b/tasks/mirror.py similarity index 100% rename from src/pyproject_fmt/py.typed rename to tasks/mirror.py diff --git a/tests/test_cli.py b/tests/test_cli.py deleted file mode 100644 index bbc3fa4..0000000 --- a/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/tests/test_main.py b/tests/test_main.py deleted file mode 100644 index ee3bc25..0000000 --- a/tests/test_main.py +++ /dev/null @@ -1,302 +0,0 @@ -from __future__ import annotations - -import difflib -from textwrap import dedent -from typing import TYPE_CHECKING, Any - -import pytest - -from pyproject_fmt import __version__ -from pyproject_fmt.__main__ import GREEN, RED, RESET, color_diff, run - -if TYPE_CHECKING: - from pathlib import Path - - from pytest_mock import MockerFixture - - -def test_version() -> None: - assert isinstance(__version__, str) - - -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", - [ - True, - False, - ], -) -@pytest.mark.parametrize( - "check", - [ - True, - False, - ], -) -@pytest.mark.parametrize( - "cwd", - [ - True, - False, - ], -) -@pytest.mark.parametrize( - ("start", "outcome", "output"), - [ - ( - '[build-system]\nrequires = [\n "hatchling>=0.14",\n]\n', - '[build-system]\nrequires = [\n "hatchling>=0.14",\n]\n', - "no change for {0}\n", - ), - ( - '[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', - ), - ], -) -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("pyproject_fmt.__main__.color_diff", no_color) - if cwd: - monkeypatch.chdir(tmp_path) - pyproject_toml = tmp_path / "pyproject.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 = "pyproject.toml" if cwd else str(tmp_path / "pyproject.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 = """\ - [build-system] - requires = [ - "A", - ] - """ - - expected = f"""\ - [build-system] - requires = [ - {" " * indent}"a", - ] - """ - pyproject_toml = tmp_path / "pyproject.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_keep_full_version_cli(tmp_path: Path) -> None: - start = """\ - [build-system] - requires = [ - "a==1.0.0", - ] - - [project] - classifiers = [ - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.9", - ] - dependencies = [ - "a==1.0.0", - ] - optional-dependencies.docs = [ - "b==2.0.0", - ] - """ - pyproject_toml = tmp_path / "pyproject.toml" - pyproject_toml.write_text(dedent(start)) - args = [str(pyproject_toml), "--keep-full-version", "--max-supported-python", "3.9"] - run(args) - output = pyproject_toml.read_text() - assert output == dedent(start) - - -def test_pyproject_toml_config(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None: - txt = """ - [project] - keywords = [ - "A", - ] - classifiers = [ - "Programming Language :: Python :: 3 :: Only", - ] - dynamic = [ - "B", - ] - dependencies = [ - "requests>=2.0", - ] - - [tool.pyproject-fmt] - column_width = 20 - indent = 4 - keep_full_version = true - max_supported_python = "3.11" - ignore_extra = true - """ - filename = tmp_path / "pyproject.toml" - filename.write_text(dedent(txt)) - run([str(filename)]) - - expected = """\ - [project] - keywords = [ - "A", - ] - classifiers = [ - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - ] - dynamic = [ - "B", - ] - dependencies = [ - "requests>=2.0", - ] - - [tool.pyproject-fmt] - column_width = 20 - indent = 4 - keep_full_version = true - max_supported_python = "3.11" - ignore_extra = true - """ - got = filename.read_text() - assert got == dedent(expected) - out, err = capsys.readouterr() - assert out - assert not err - - -def test_pyproject_ftm_api_changed(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None: - txt = """ - [project] - requires-python = "==3.12" - """ - filename = tmp_path / "pyproject.toml" - filename.write_text(dedent(txt)) - res = run([str(filename), "--no-print-diff", "--column-width", "20"]) - - assert res == 1 - - got = filename.read_text() - expected = """\ - [project] - requires-python = "==3.12" - classifiers = [ - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.12", - ] - """ - assert got == dedent(expected) - - out, err = capsys.readouterr() - assert not out - assert not err - - -def test_pyproject_ftm_api_no_change(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None: - txt = """\ - [project] - requires-python = "==3.12" - classifiers = [ - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.12", - ] - """ - filename = tmp_path / "pyproject.toml" - filename.write_text(dedent(txt)) - res = run([str(filename), "--no-print-diff"]) - - assert res == 0 - - got = filename.read_text() - - assert got == dedent(txt) - - out, err = capsys.readouterr() - assert not out - assert not err diff --git a/tests/test_mirror.py b/tests/test_mirror.py new file mode 100644 index 0000000..e1fa9ae --- /dev/null +++ b/tests/test_mirror.py @@ -0,0 +1,59 @@ +from __future__ import annotations + +import sys +from pathlib import Path +from subprocess import call, check_call, check_output +from textwrap import dedent + +import pytest + + +@pytest.fixture(scope="session") +def root() -> Path: + return Path(__file__).parents[1] + + +def test_last_tag(tmp_path: Path, root: Path, monkeypatch: pytest.MonkeyPatch) -> None: + # test this project against its latest tag + ver = check_output(["git", "describe", "--tags", "--abbrev=0"], text=True).strip() + print(f"Using version {ver}") # noqa: T201 + + pre_commit_home = tmp_path / "h" + pre_commit_home.mkdir() + monkeypatch.setenv("PRE_COMMIT_HOME", str(pre_commit_home)) + + project = tmp_path / "p" + project.mkdir() + monkeypatch.chdir(project) + (project / ".pre-commit-config.yaml").write_text( + dedent(f""" + repos: + - repo: file://{root} + rev: "{ver}" + hooks: + - id: pyproject-fmt + language_version: {sys.executable} + """) + ) + toml = project / "pyproject.toml" + toml.write_text("[project]\nrequires-python='>=3.13'") + check_call(["git", "init"]) + check_call(["git", "add", "."]) + monkeypatch.setenv("GIT_USER_EMAIL", "demo@magic.com") + monkeypatch.setenv("GIT_USER_NAME", "Demo Magic") + check_call(["git", "commit", "-m", "Initial commit"]) + check_call(["ls", "-alth"]) + + pre_commit = Path(sys.executable).parent / "pre-commit" + + check_call([pre_commit, "install-hooks"]) + assert list(pre_commit_home.iterdir()) + + code = call([pre_commit, "run", "--all-files"]) + assert code == 1 + + assert toml.read_text().splitlines() == [ + "[project]", + 'requires-python = ">=3.13"', + 'classifiers = [ "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.13" ]', + ] diff --git a/tests/test_pyproject_toml_fmt.py b/tests/test_pyproject_toml_fmt.py deleted file mode 100644 index a731ebe..0000000 --- a/tests/test_pyproject_toml_fmt.py +++ /dev/null @@ -1,15 +0,0 @@ -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", "pyproject_fmt", "--help"]) - - -def test_help_invocation_as_script() -> None: - subprocess.check_call( - [str(Path(sys.executable).parent / "pyproject-fmt"), "--help"], - ) diff --git a/tox.ini b/tox.ini deleted file mode 100644 index 6abd338..0000000 --- a/tox.ini +++ /dev/null @@ -1,80 +0,0 @@ -[tox] -requires = - tox>=4.2 - tox-uv>=1.11.3 -env_list = - fix - 3.13 - 3.12 - 3.11 - 3.10 - 3.9 - type - docs - pkg_meta -skip_missing_interpreters = true - -[testenv] -description = run the unit tests with pytest under {basepython} -package = wheel -wheel_build_env = .pkg -extras = - test -set_env = - COVERAGE_FILE = {work_dir}/.coverage.{env_name} -commands = - python -m pytest {tty:--color=yes} {posargs: \ - --junitxml {work_dir}{/}junit.{env_name}.xml --cov {env_site_packages_dir}{/}pyproject_fmt \ - --cov {tox_root}{/}tests --cov-fail-under=100 \ - --cov-config=pyproject.toml --no-cov-on-fail --cov-report term-missing:skip-covered --cov-context=test \ - --cov-report html:{env_tmp_dir}{/}htmlcov --cov-report xml:{work_dir}{/}coverage.{env_name}.xml \ - tests} - -[testenv:fix] -description = run static analysis and style check using flake8 -skip_install = true -deps = - pre-commit-uv>=4.1.3 -commands = - pre-commit run --all-files --show-diff-on-failure - python -c 'print("hint: run {envdir}/bin/pre-commit install to add checks as pre-commit hook")' - -[testenv:type] -description = run type check on code base -deps = - mypy==1.11.2 -commands = - mypy src - mypy tests - -[testenv:docs] -description = build documentation -extras = - docs -set_env = - DOCS_OUT = {posargs:{work_dir}{/}docs_out} -commands = - sphinx-build -d "{env_tmp_dir}{/}doc_tree" docs "{env:DOCS_OUT}" --color -b html - python -c 'print(r"documentation available under file://{env:DOCS_OUT}{/}index.html")' - -[testenv:pkg_meta] -description = check that the long description is valid -skip_install = true -deps = - check-wheel-contents>=0.6 - twine>=5.1.1 - uv>=0.4.18 -commands = - uv build --sdist --wheel --out-dir {env_tmp_dir} . - twine check {env_tmp_dir}{/}* - check-wheel-contents --no-config {env_tmp_dir} - -[testenv:dev] -description = generate a DEV environment -package = editable -extras = - docs - test -commands = - uv pip tree - python -c 'import sys; print(sys.executable)' diff --git a/tox.toml b/tox.toml new file mode 100644 index 0000000..9a08be3 --- /dev/null +++ b/tox.toml @@ -0,0 +1,55 @@ +requires = ["tox>=4.22"] +env_list = ["fix", "3.13", "3.12", "3.11", "3.10", "3.9", "type"] +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", + "{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"] +commands = [["mypy", "tasks", "tests"]] + +[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}")']]