From 40968ba613846ca050215767ad843fa18566a6dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Mon, 5 Aug 2024 12:08:07 -0700 Subject: [PATCH] Create first version (#1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Bernát Gábor --- .github/FUNDING.yml | 1 + .github/SECURITY.md | 13 ++++ .github/dependabot.yml | 6 ++ .github/release.yml | 5 ++ .github/workflows/check.yml | 78 ++++++++++++++++++++ .github/workflows/release.yml | 27 +++++++ .gitignore | 6 ++ .pre-commit-config.yaml | 35 +++++++++ CODE_OF_CONDUCT.md | 60 +++++++++++++++ LICENSE.txt | 18 +++++ README.md | 11 ++- pyproject.toml | 134 ++++++++++++++++++++++++++++++++++ src/pre_commit_uv/__init__.py | 64 ++++++++++++++++ src/pre_commit_uv/py.typed | 0 src/pre_commit_uv_patch.pth | 1 + tests/test_main.py | 37 ++++++++++ tox.ini | 71 ++++++++++++++++++ 17 files changed, 566 insertions(+), 1 deletion(-) create mode 100644 .github/FUNDING.yml create mode 100644 .github/SECURITY.md create mode 100644 .github/dependabot.yml create mode 100644 .github/release.yml create mode 100644 .github/workflows/check.yml create mode 100644 .github/workflows/release.yml create mode 100644 .gitignore create mode 100644 .pre-commit-config.yaml create mode 100644 CODE_OF_CONDUCT.md create mode 100644 LICENSE.txt create mode 100644 pyproject.toml create mode 100644 src/pre_commit_uv/__init__.py create mode 100644 src/pre_commit_uv/py.typed create mode 100644 src/pre_commit_uv_patch.pth create mode 100644 tests/test_main.py create mode 100644 tox.ini diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..c079697 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +tidelift: "pypi/pre-commit-uv" diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 0000000..b22dce0 --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,13 @@ +# Security Policy + +## Supported Versions + +| Version | Supported | +|---------| ------------------ | +| 0.1 + | :white_check_mark: | +| < 0.1 | :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/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..1230149 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 0000000..9d1e098 --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,5 @@ +changelog: + exclude: + authors: + - dependabot + - pre-commit-ci diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml new file mode 100644 index 0000000..1fe9ed1 --- /dev/null +++ b/.github/workflows/check.yml @@ -0,0 +1,78 @@ +name: check +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: + name: test ${{ matrix.py }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + py: + - "3.12" + - "3.11" + - "3.10" + - "3.9" + - "3.8" + steps: + - name: setup uv for tox + uses: yezz123/setup-uv@v4 + - name: setup python for tox + uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: install tox + run: uv pip install tox tox-uv --system + - uses: actions/checkout@v4 + - name: setup python for test ${{ matrix.py }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.py }} + - name: Pick environment to run + run: | + import codecs; import os; import sys + env = "TOXENV=py{}{}\n".format(*sys.version_info[0:2]) + print("Picked:\n{}for{}".format(env, sys.version)) + with codecs.open(os.environ["GITHUB_ENV"], "a", "utf-8") as file_handler: + file_handler.write(env) + shell: python + - name: setup test suite + run: tox -vv --notest + - name: run test suite + run: tox --skip-pkg-install + + check: + name: tox env ${{ matrix.tox_env }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + tox_env: + - type + - dev + - readme + steps: + - name: setup uv for tox + uses: yezz123/setup-uv@v4 + - uses: actions/checkout@v4 + - name: setup Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: install tox + run: uv pip install tox tox-uv --system + - name: run check for ${{ matrix.tox_env }} + run: python -m tox -e ${{ matrix.tox_env }} + env: + UPGRADE_ADVISORY: "yes" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..b80cff2 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,27 @@ +name: Release to PyPI +on: + push: + tags: ["*"] + +jobs: + release: + runs-on: ubuntu-latest + environment: + name: release + url: https://pypi.org/p/pre-commit-uv + permissions: + id-token: write + steps: + - name: Setup python to build package + uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Install build + run: python -m pip install build + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Build package + run: pyproject-build -s -w . -o dist + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@v1.9.0 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e08736f --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +*.egg-info/ +.tox/ +.*_cache +__pycache__ +**.pyc +dist diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..b57eca5 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,35 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: end-of-file-fixer + - id: trailing-whitespace + - repo: https://github.com/python-jsonschema/check-jsonschema + rev: 0.29.1 + hooks: + - id: check-github-workflows + args: [ "--verbose" ] + - repo: https://github.com/codespell-project/codespell + rev: v2.3.0 + hooks: + - id: codespell + args: ["--write-changes"] + - repo: https://github.com/tox-dev/tox-ini-fmt + rev: "1.3.1" + hooks: + - id: tox-ini-fmt + args: ["-p", "fix"] + - repo: https://github.com/tox-dev/pyproject-fmt + rev: "2.2.0" + hooks: + - id: pyproject-fmt + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: "v0.5.6" + hooks: + - id: ruff-format + - id: ruff + args: ["--fix", "--unsafe-fixes", "--exit-non-zero-on-fix"] + - repo: meta + hooks: + - id: check-hooks-apply + - id: check-useless-excludes diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..267eaab --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,60 @@ +# 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/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..3649823 --- /dev/null +++ b/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/README.md b/README.md index a864717..fceb863 100644 --- a/README.md +++ b/README.md @@ -1 +1,10 @@ -# pre-commit-uv \ No newline at end of file +# pre-commit-uv + +[![PyPI](https://img.shields.io/pypi/v/pre-commit-uv?style=flat-square)](https://pypi.org/project/pre-commit-uv) +[![PyPI - Implementation](https://img.shields.io/pypi/implementation/pre-commit-uv?style=flat-square)](https://pypi.org/project/pre-commit-uv) +[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pre-commit-uv?style=flat-square)](https://pypi.org/project/pre-commit-uv) +[![Downloads](https://static.pepy.tech/badge/pre-commit-uv/month)](https://pepy.tech/project/pre-commit-uv) +[![PyPI - License](https://img.shields.io/pypi/l/pre-commit-uv?style=flat-square)](https://opensource.org/licenses/MIT) +[![check](https://github.com/tox-dev/pre-commit-uv/actions/workflows/check.yml/badge.svg)](https://github.com/tox-dev/pre-commit-uv/actions/workflows/check.yml) + +Use `uv` to create virtual environments and install packages for `pre-commit`. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..869bb80 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,134 @@ +[build-system] +build-backend = "hatchling.build" +requires = [ + "hatch-vcs>=0.4", + "hatchling>=1.25", +] + +[project] +name = "pre-commit-uv" +description = "Run pre-commit with uv" +readme = "README.md" +keywords = [ + "format", + "pyproject", +] +license.file = "LICENSE.txt" +authors = [ + { name = "Bernat Gabor", email = "gaborjbernat@gmail.com" }, +] +requires-python = ">=3.9" +classifiers = [ + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", +] +dynamic = [ + "version", +] +dependencies = [ + "pre-commit>=3.8", + "uv>=0.2.33", +] +optional-dependencies.test = [ + "covdefaults>=2.3", + "pytest>=8.3.2", + "pytest-cov>=5", + "pytest-mock>=3.14", +] +urls."Bug Tracker" = "https://github.com/tox-dev/pre-commit-uv/issues" +urls."Changelog" = "https://github.com/tox-dev/pre-commit-uv/releases" +urls.Documentation = "https://github.com/tox-dev/pre-commit-uv/" +urls."Source Code" = "https://github.com/tox-dev/pre-commit-uv" + +[tool.hatch] +build.dev-mode-dirs = [ + "src", +] +build.targets.sdist.include = [ + "/src", + "/tests", + "tox.ini", +] +build.targets.wheel.force-include = { "src/pre_commit_uv_patch.pth" = "pre_commit_uv_patch.pth" } +version.source = "vcs" + +[tool.ruff] +target-version = "py38" +line-length = 120 +format.preview = true +format.docstring-code-line-length = 100 +format.docstring-code-format = true +lint.select = [ + "ALL", +] +lint.ignore = [ + "ANN101", # no type annotation for self + "ANN401", # allow Any as type annotation + "COM812", # Conflict with formatter + "CPY", # No copyright statements + "D203", # `one-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible + "D212", # `multi-line-summary-first-line` (D212) and `multi-line-summary-second-line` (D213) are incompatible + "DOC501", # not working with Sphinx + "ISC001", # Conflict with formatter + "S104", # Possible binding to all interfaces +] +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 + "INP001", # no implicit namespace + "PLC2701", # private import + "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 +] +lint.isort = { known-first-party = [ + "pre_commit_uv", +], required-imports = [ + "from __future__ import annotations", +] } +lint.preview = true + +[tool.codespell] +builtin = "clear,usage,en-GB_to_en-US" +count = true + +[tool.pytest] +ini_options.testpaths = [ + "tests", +] + +[tool.coverage] +html.show_contexts = true +html.skip_covered = false +paths.source = [ + "src", + ".tox/*/.venv/lib/*/site-packages", + ".tox\\*\\.venv\\Lib\\site-packages", + ".tox/*/lib/*/site-packages", + ".tox\\*\\Lib\\site-packages", + "**/src", + "**\\src", +] +report.fail_under = 54 +run.parallel = true +run.plugins = [ + "covdefaults", +] + +[tool.mypy] +show_error_codes = true +strict = true +overrides = [ + { module = [ + "pre_commit.*", + ], ignore_missing_imports = true }, +] diff --git a/src/pre_commit_uv/__init__.py b/src/pre_commit_uv/__init__.py new file mode 100644 index 0000000..c2f8ae0 --- /dev/null +++ b/src/pre_commit_uv/__init__.py @@ -0,0 +1,64 @@ +"""Package root.""" + +from __future__ import annotations + +import logging +from functools import cache +from importlib.metadata import version as _metadata_version +from typing import TYPE_CHECKING, Sequence, cast + +from pre_commit import lang_base, main +from pre_commit.languages import python +from pre_commit.languages.python import in_env, norm_version +from pre_commit.util import CalledProcessError, cmd_output, cmd_output_b +from uv import find_uv_bin + +if TYPE_CHECKING: + from pre_commit.prefix import Prefix + + +__version__ = _metadata_version("pre-commit-uv") +_uv_version = _metadata_version("uv") +_original_main = main.main + + +def _patch() -> None: + main.main = _new_main + + +def _new_main(argv: Sequence[str] | None = None) -> int: + python.install_environment = _install_environment + python._version_info = _version_info # noqa: SLF001 + return cast(int, _original_main(argv)) + + +def _install_environment( + prefix: Prefix, + version: str, + additional_dependencies: Sequence[str], +) -> None: + logging.getLogger("pre_commit").info("Using pre-commit with uv %s via pre-commit-uv %s", _uv_version, __version__) + uv = find_uv_bin() + + venv_cmd = [uv, "venv", lang_base.environment_dir(prefix, python.ENVIRONMENT_DIR, version)] + py = norm_version(version) + if py is not None: + venv_cmd.extend(("-p", py)) + cmd_output_b(*venv_cmd, cwd="/") + + with in_env(prefix, version): + lang_base.setup_cmd(prefix, (uv, "pip", "install", ".", *additional_dependencies)) + + +@cache +def _version_info(exe: str) -> str: + prog = 'import sys;print(".".join(str(p) for p in sys.version_info[0:3]))' + try: + return cast(str, cmd_output(exe, "-S", "-c", prog)[1].strip()) + except CalledProcessError: + return f"<>" + + +__all__ = [ + "__version__", +] diff --git a/src/pre_commit_uv/py.typed b/src/pre_commit_uv/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/src/pre_commit_uv_patch.pth b/src/pre_commit_uv_patch.pth new file mode 100644 index 0000000..4f7a66b --- /dev/null +++ b/src/pre_commit_uv_patch.pth @@ -0,0 +1 @@ +import pre_commit_uv; pre_commit_uv._patch() diff --git a/tests/test_main.py b/tests/test_main.py new file mode 100644 index 0000000..6ea48d2 --- /dev/null +++ b/tests/test_main.py @@ -0,0 +1,37 @@ +from __future__ import annotations + +from importlib.metadata import version +from textwrap import dedent +from typing import TYPE_CHECKING + +from pre_commit import main + +if TYPE_CHECKING: + from pathlib import Path + + import pytest + + +def test_install(tmp_path: Path, caplog: pytest.LogCaptureFixture, monkeypatch: pytest.MonkeyPatch) -> None: + conf = """ + repos: + - repo: https://github.com/tox-dev/pyproject-fmt + rev: "2.2.0" + hooks: + - id: pyproject-fmt + """ + conf_file = tmp_path / ".pre-commit-config.yaml" + conf_file.write_text(dedent(conf)) + monkeypatch.setenv("PRE_COMMIT_HOME", str(tmp_path / "store")) + + main.main(["install-hooks", "-c", str(conf_file)]) + + uv = version("uv") + self = version("pre-commit-uv") + assert caplog.messages == [ + "Initializing environment for https://github.com/tox-dev/pyproject-fmt.", + "Installing environment for https://github.com/tox-dev/pyproject-fmt.", + "Once installed this environment will be reused.", + "This may take a few minutes...", + f"Using pre-commit with uv {uv} via pre-commit-uv {self}", + ] diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..09bfdf8 --- /dev/null +++ b/tox.ini @@ -0,0 +1,71 @@ +[tox] +requires = + tox>=4.2 +env_list = + fix + py312 + py311 + py310 + py39 + type + readme +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 = {toxworkdir}/.coverage.{envname} + COVERAGE_PROCESS_START = {toxinidir}/setup.cfg + _COVERAGE_SRC = {envsitepackagesdir}/sphinx_argparse_cli +commands = + python -m pytest {tty:--color=yes} {posargs: \ + --junitxml {toxworkdir}{/}junit.{envname}.xml --cov {envsitepackagesdir}{/}pre_commit_uv \ + --cov {toxinidir}{/}tests \ + --cov-config=pyproject.toml --no-cov-on-fail --cov-report term-missing:skip-covered --cov-context=test \ + --cov-report html:{envtmpdir}{/}htmlcov --cov-report xml:{toxworkdir}{/}coverage.{envname}.xml \ + tests} + +[testenv:fix] +description = run static analysis and style check using flake8 +skip_install = true +deps = + pre-commit>=3.8 +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.1 +set_env = + {tty:MYPY_FORCE_COLOR = 1} +commands = + mypy src + mypy tests + +[testenv:readme] +description = check that the long description is valid +skip_install = true +deps = + build[virtualenv]>=1.2.1 + twine>=5.1.1 +commands = + python -m build --sdist --wheel -o {envtmpdir} . + twine check {envtmpdir}/* + +[testenv:dev] +description = generate a DEV environment +package = editable +extras = + docs + test +commands = + python -c "import shutil; shutil.copy2('src{/}_pre_commit_uv_patch.pth', '{env_site_packages_dir}{/}_pre_commit_uv_patch.pth')" + python -m pip list --format=columns + python -c 'import sys; print(sys.executable)' +uv_seed = true