From 59c43a7aeb26704d9b4c461332cbd07191a42105 Mon Sep 17 00:00:00 2001 From: Andrew Halberstadt Date: Mon, 26 Aug 2024 16:18:02 -0400 Subject: [PATCH] feat(python): switch Python spec from poetry to uv --- README.md | 9 +++-- STANDARD.md | 11 +++--- reps/hooks.py | 37 +++++++++++-------- reps/templates/python/cookiecutter.json | 3 +- .../pyproject.toml | 18 +++++---- .../taskcluster/docker/python/Dockerfile | 15 ++------ .../taskcluster/kinds/codecov/kind.yml | 3 +- .../taskcluster/kinds/docker-image/kind.yml | 4 +- .../taskcluster/kinds/test/kind.yml | 6 +-- .../taskcluster/scripts/poetry-setup | 8 ---- .../taskcluster/scripts/pyenv-setup | 26 ------------- .../{{cookiecutter.__project_slug}}/tox.ini | 19 ++++------ test/test_template_python.py | 9 ++--- 13 files changed, 65 insertions(+), 103 deletions(-) delete mode 100755 reps/templates/python/{{cookiecutter.__project_slug}}/taskcluster/scripts/poetry-setup delete mode 100755 reps/templates/python/{{cookiecutter.__project_slug}}/taskcluster/scripts/pyenv-setup diff --git a/README.md b/README.md index 39d9bd9..ac60ddd 100644 --- a/README.md +++ b/README.md @@ -23,18 +23,19 @@ the REPS definition. The `reps-new` tool can be used to bootstrap new projects that conform to this standard. It is recommended to install and run it with -[pipx](https://github.com/pypa/pipx) (so the most up to date version is always -used): +[uvx](https://docs.astral.sh/uv/guides/tools/) (so the most up to date version +is always used). First [install +uv](https://docs.astral.sh/uv/getting-started/installation/), then run: ```bash -pipx run reps-new +uvx reps-new ``` and fill out the prompts. By default, the `python` project template is used. You may optionally specify a different template to use with the `-t/--template` flag: ```bash -pipx run reps-new -t base +uvx reps-new -t base ``` Available templates can be found in the diff --git a/STANDARD.md b/STANDARD.md index 006df12..82c8729 100644 --- a/STANDARD.md +++ b/STANDARD.md @@ -281,11 +281,11 @@ standards. ### Packaging -Projects should use [Poetry] to manage dependencies, run builds and publish -packages. Running `poetry init` should be sufficient to generate the initial +Projects should use [uv] to manage dependencies, virtualenvs and publish +packages. Running `uv init` should be sufficient to generate the initial configuration in the top-level `pyproject.toml` file. -[Poetry]: https://python-poetry.org/ +[uv]: https://docs.astral.sh/uv/ ### Testing @@ -467,15 +467,14 @@ type-check: cwd: '{checkout}' cache-dotcache: true command: >- - poetry install --only main --only type && - poetry run pyright + uv run pyright ``` While it's possible to run as a [pre-commit hook], this method isn't recommended as Pyright needs to run in an environment where the project's dependencies are installed. This means either the dependencies need to be listed a second time in `pre-commit-config.yaml`, or Pyright needs to be -explicitly told about Poetry's virtualenv (which varies from person to person +explicitly told about uv's virtualenv (which varies from person to person and shouldn't be committed in the config file). [Pyright]: https://github.com/Microsoft/pyright diff --git a/reps/hooks.py b/reps/hooks.py index 0c911f6..755e1d0 100644 --- a/reps/hooks.py +++ b/reps/hooks.py @@ -86,20 +86,20 @@ def merge_pre_commit(items: CookiecutterContext): @hook("post-gen-py") -def add_poetry_dependencies(items: CookiecutterContext): +def add_uv_dependencies(items: CookiecutterContext): # Build constraints to ensure we don't try to add versions # that are incompatible with the minimum Python. min_python = items["min_python_version"] constraints = defaultdict(dict) - constraints["coverage"] = {"3.7": "coverage@<7.3.0"} - constraints["tox"] = {"3.7": "tox@<4.9.0"} + constraints["coverage"] = {"3.7": "coverage<7.3.0"} + constraints["tox"] = {"3.7": "tox<4.9.0"} constraints["sphinx-book-theme"] = { - "3.7": "sphinx-book-theme@<=1.0.1", - "3.8": "sphinx-book-theme@<=1.0.1", + "3.7": "sphinx-book-theme<=1.0.1", + "3.8": "sphinx-book-theme<=1.0.1", } constraints["sphinx-autobuild"] = { - "3.7": "sphinx-autobuild@<=2021.3.14", - "3.8": "sphinx-autobuild@<=2021.3.14", + "3.7": "sphinx-autobuild<=2021.3.14", + "3.8": "sphinx-autobuild<=2021.3.14", } def build_specifiers(*packages: str) -> Generator[str, None, None]: @@ -107,17 +107,24 @@ def build_specifiers(*packages: str) -> Generator[str, None, None]: yield constraints[p].get(min_python, p) run( - ["poetry", "add", "--group=test"] + # Until the pyproject.toml spec and/or uv supports dependency groups, we + # need to add these all to "dev-dependencies" + # See: https://github.com/astral-sh/uv/issues/5632 + ["uv", "add", "--dev"] + list( - build_specifiers("coverage", "pytest", "pytest-mock", "responses", "tox") + build_specifiers( + "coverage", + "pyright", + "pytest", + "pytest-mock", + "responses", + "sphinx<7", + "sphinx-autobuild", + "sphinx-book-theme", + "tox", + ) ) ) - run( - ["poetry", "add", "--group=docs"] - + list(build_specifiers("sphinx<7", "sphinx-autobuild", "sphinx-book-theme")) - ) - - run(["poetry", "add", "--group=type"] + list(build_specifiers("pyright"))) @hook("post-gen-py") diff --git a/reps/templates/python/cookiecutter.json b/reps/templates/python/cookiecutter.json index ce6b347..fcaac92 100644 --- a/reps/templates/python/cookiecutter.json +++ b/reps/templates/python/cookiecutter.json @@ -3,7 +3,8 @@ "__project_slug": "{{cookiecutter.project_name|lower|replace(' ', '-')}}", "__package_name": "{{cookiecutter.project_name|lower|replace(' ', '_')|replace('-', '_')}}", "short_description": "", - "author": "Mozilla Release Engineering ", + "author_name": "Mozilla Release Engineering", + "author_email": "release@mozilla.com", "github_slug": "mozilla-releng/{{cookiecutter.__project_slug}}", "__github_org": "{{cookiecutter.github_slug[:cookiecutter.github_slug.find('/')]}}", "__github_project": "{{cookiecutter.github_slug[cookiecutter.github_slug.find('/')+1:]}}", diff --git a/reps/templates/python/{{cookiecutter.__project_slug}}/pyproject.toml b/reps/templates/python/{{cookiecutter.__project_slug}}/pyproject.toml index adcafcb..137f9a2 100644 --- a/reps/templates/python/{{cookiecutter.__project_slug}}/pyproject.toml +++ b/reps/templates/python/{{cookiecutter.__project_slug}}/pyproject.toml @@ -1,14 +1,16 @@ -[tool.poetry] +[project] name = "{{cookiecutter.__project_slug}}" version = "0.1.0" description = "{{cookiecutter.short_description}}" -authors = ["{{cookiecutter.author}}"] -license = "MPL-2.0" +requires-python = ">={{cookiecutter.min_python_version}}" +authors = [ + { name = "{{cookiecutter.author_name}}", email = "{{cookiecutter.author_email}}" } +] +classifiers = [ + "License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)", +] readme = "README.md" -[tool.poetry.dependencies] -python = "^{{cookiecutter.min_python_version}}" - [tool.pytest.ini_options] xfail_strict = true @@ -42,5 +44,5 @@ include = ["src/{{cookiecutter.__package_name}}"] reportUnknownParameterType = "error" [build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" +requires = ["hatchling"] +build-backend = "hatchling.build" diff --git a/reps/templates/python/{{cookiecutter.__project_slug}}/taskcluster/docker/python/Dockerfile b/reps/templates/python/{{cookiecutter.__project_slug}}/taskcluster/docker/python/Dockerfile index 3344187..d3d7237 100644 --- a/reps/templates/python/{{cookiecutter.__project_slug}}/taskcluster/docker/python/Dockerfile +++ b/reps/templates/python/{{cookiecutter.__project_slug}}/taskcluster/docker/python/Dockerfile @@ -22,17 +22,10 @@ ENV SHELL=/bin/bash \ VOLUME /builds/worker/checkouts VOLUME /builds/worker/.cache -# pyenv -# %ARG PYENV_VERSIONS -ENV PYENV_ROOT=/builds/worker/.pyenv \ - PATH=/builds/worker/.pyenv/bin:/builds/worker/.pyenv/shims:$PATH -# %include taskcluster/scripts/pyenv-setup -ADD topsrcdir/taskcluster/scripts/pyenv-setup /builds/worker/pyenv-setup -RUN /builds/worker/pyenv-setup "$PYENV_VERSIONS" - -# %include taskcluster/scripts/poetry-setup -ADD topsrcdir/taskcluster/scripts/poetry-setup /builds/worker/poetry-setup -RUN /builds/worker/poetry-setup +# uv +COPY --from=ghcr.io/astral-sh/uv:0.3.5 /uv /bin/uv +# %ARG PYTHON_VERSIONS +RUN uv python install $PYTHON_VERSIONS RUN chown -R worker:worker /builds/worker diff --git a/reps/templates/python/{{cookiecutter.__project_slug}}/taskcluster/kinds/codecov/kind.yml b/reps/templates/python/{{cookiecutter.__project_slug}}/taskcluster/kinds/codecov/kind.yml index 295ad1e..fb61859 100644 --- a/reps/templates/python/{{cookiecutter.__project_slug}}/taskcluster/kinds/codecov/kind.yml +++ b/reps/templates/python/{{cookiecutter.__project_slug}}/taskcluster/kinds/codecov/kind.yml @@ -35,5 +35,4 @@ tasks: using: run-task cwd: '{checkout}' command: >- - poetry install --only test && - poetry run python taskcluster/scripts/codecov-upload.py + uv run python taskcluster/scripts/codecov-upload.py diff --git a/reps/templates/python/{{cookiecutter.__project_slug}}/taskcluster/kinds/docker-image/kind.yml b/reps/templates/python/{{cookiecutter.__project_slug}}/taskcluster/kinds/docker-image/kind.yml index d2f5dd4..58b3323 100644 --- a/reps/templates/python/{{cookiecutter.__project_slug}}/taskcluster/kinds/docker-image/kind.yml +++ b/reps/templates/python/{{cookiecutter.__project_slug}}/taskcluster/kinds/docker-image/kind.yml @@ -1,7 +1,7 @@ {%- set pylist -%} {%- for i in range(cookiecutter.__max_tox_python_version[1:]|int, cookiecutter.__min_tox_python_version[1:]|int - 1, -1) -%} 3.{{i}} -{%- if not loop.last %},{% endif -%} +{%- if not loop.last %} {% endif -%} {%- endfor -%} {%- endset -%} --- @@ -16,4 +16,4 @@ tasks: fetch: {} python: args: - PYENV_VERSIONS: "{{pylist}}" + PYTHON_VERSIONS: "{{pylist}}" diff --git a/reps/templates/python/{{cookiecutter.__project_slug}}/taskcluster/kinds/test/kind.yml b/reps/templates/python/{{cookiecutter.__project_slug}}/taskcluster/kinds/test/kind.yml index 1de30c6..fc41b95 100644 --- a/reps/templates/python/{{cookiecutter.__project_slug}}/taskcluster/kinds/test/kind.yml +++ b/reps/templates/python/{{cookiecutter.__project_slug}}/taskcluster/kinds/test/kind.yml @@ -32,8 +32,7 @@ tasks: TOX_PARALLEL_NO_SPINNER: "1" run: command: >- - poetry install --only test && - poetry run tox --parallel + uv run tox --parallel type-check: description: "Run pyright type checking against code base" @@ -41,5 +40,4 @@ tasks: max-run-time: 300 run: command: >- - poetry install --only main --only type && - poetry run pyright + uv run pyright diff --git a/reps/templates/python/{{cookiecutter.__project_slug}}/taskcluster/scripts/poetry-setup b/reps/templates/python/{{cookiecutter.__project_slug}}/taskcluster/scripts/poetry-setup deleted file mode 100755 index 0f242de..0000000 --- a/reps/templates/python/{{cookiecutter.__project_slug}}/taskcluster/scripts/poetry-setup +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bash - -set -vx - -# Use pipx to avoid Poetry's curl | bash installer. -python3 -mpip install --user pipx -python3 -mpipx ensurepath -python3 -mpipx install poetry~=1.5.1 diff --git a/reps/templates/python/{{cookiecutter.__project_slug}}/taskcluster/scripts/pyenv-setup b/reps/templates/python/{{cookiecutter.__project_slug}}/taskcluster/scripts/pyenv-setup deleted file mode 100755 index 3e0a423..0000000 --- a/reps/templates/python/{{cookiecutter.__project_slug}}/taskcluster/scripts/pyenv-setup +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env bash -set -ex - -curl -L "https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer" -o /builds/worker/pyenv-installer -chmod +x /builds/worker/pyenv-installer -/builds/worker/pyenv-installer -rm /builds/worker/pyenv-installer -cat << EOF >> /builds/worker/.bashrc -eval "\$(pyenv init --path)" -eval "\$(pyenv init -)" -eval "\$(pyenv virtualenv-init -)" -EOF -source /builds/worker/.bashrc - -# Log some debugging info -pyenv --version -git -C "$(pyenv root)" rev-parse HEAD - -for i in ${1//,/ } -do - version=$(pyenv latest --known $i) - pyenv install $version -done -chmod 777 /builds/worker/.pyenv/shims -pyenv rehash -pyenv global ${1//,/ } diff --git a/reps/templates/python/{{cookiecutter.__project_slug}}/tox.ini b/reps/templates/python/{{cookiecutter.__project_slug}}/tox.ini index 0addb49..21c3a92 100644 --- a/reps/templates/python/{{cookiecutter.__project_slug}}/tox.ini +++ b/reps/templates/python/{{cookiecutter.__project_slug}}/tox.ini @@ -10,27 +10,24 @@ envlist = clean,{{pylist}},report [testenv] -allowlist_externals = poetry +allowlist_externals = uv parallel_show_output = true depends = {{pylist}}: clean report: {{pylist}} commands = - poetry install --with test - poetry run python --version - poetry run coverage run --context={envname} -p -m pytest -vv {posargs} + uv run python --version + uv run coverage run --context={envname} -p -m pytest -vv {posargs} [testenv:report] -allowlist_externals = poetry +allowlist_externals = uv passenv = COVERAGE_REPORT_COMMAND parallel_show_output = true commands = - poetry install --only test - poetry run coverage combine - poetry run {env:COVERAGE_REPORT_COMMAND:coverage report} + uv run coverage combine + uv run {env:COVERAGE_REPORT_COMMAND:coverage report} [testenv:clean] -allowlist_externals = poetry +allowlist_externals = uv commands = - poetry install --only test - poetry run coverage erase + uv run coverage erase diff --git a/test/test_template_python.py b/test/test_template_python.py index 822714f..1cef03f 100644 --- a/test/test_template_python.py +++ b/test/test_template_python.py @@ -26,7 +26,6 @@ def test_generated_files(reps_new): "docs/reference/index.rst", "docs/tutorials/index.rst", "pyproject.toml", - "poetry.lock", f"src/{name}/__init__.py", "taskcluster/config.yml", "taskcluster/docker/fetch/Dockerfile", @@ -36,17 +35,16 @@ def test_generated_files(reps_new): "taskcluster/kinds/fetch/kind.yml", "taskcluster/kinds/test/kind.yml", "taskcluster/scripts/codecov-upload.py", - "taskcluster/scripts/pyenv-setup", - "taskcluster/scripts/poetry-setup", "test/conftest.py", f"test/test_{name}.py", "tox.ini", + "uv.lock", ] project = reps_new(name, "python") actual = [] - ignore = ("__pycache__", ".git/", ".pyc") + ignore = ("__pycache__", ".git/", ".pyc", ".venv") for path in project.rglob("*"): if path.is_dir() or any(i in str(path) for i in ignore): continue @@ -64,7 +62,8 @@ def test_generated_files(reps_new): "__project_slug": "my-package", "__package_name": "my_package", "short_description": "", - "author": "Mozilla Release Engineering ", + "author_name": "Mozilla Release Engineering", + "author_email": "release@mozilla.com", "github_slug": "mozilla-releng/my-package", "min_python_version": "3.8", "__min_tox_python_version": "38",