diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index 4d89b67108..d228e96d03 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -3,11 +3,16 @@ on:
push:
paths:
- 'docs/**'
+ - 'CHANGELOG.md'
+ - 'mkdocs.yml'
+ - 'scripts/install.sh'
branches:
- main
+ workflow_dispatch:
jobs:
build:
+ if: github.repository == 'mitsuhiko/rye'
name: Deploy docs
runs-on: ubuntu-latest
steps:
@@ -16,7 +21,6 @@ jobs:
- uses: Swatinem/rust-cache@v2
- name: Deploy docs
uses: mhausenblas/mkdocs-deploy-gh-pages@master
- if: github.repository == 'mitsuhiko/rye'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CONFIG_FILE: mkdocs.yml
diff --git a/.github/workflows/sync-python-releases.yml b/.github/workflows/sync-python-releases.yml
new file mode 100644
index 0000000000..4a182a1649
--- /dev/null
+++ b/.github/workflows/sync-python-releases.yml
@@ -0,0 +1,39 @@
+# For this action to work you must explicitly allow GitHub Actions to create pull requests.
+# This setting can be found in a repository's settings under Actions > General > Workflow permissions.
+# For repositories belonging to an organization, this setting can be managed by
+# admins in organization settings under Actions > General > Workflow permissions.
+name: Sync Python Releases
+on:
+ workflow_dispatch:
+ schedule:
+ - cron: '0 0 * * *'
+
+permissions:
+ contents: write
+ pull-requests: write
+
+jobs:
+ sync:
+ if: github.repository == 'mitsuhiko/rye'
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Install Rye
+ uses: eifinger/setup-rye@v1
+ with:
+ enable-cache: true
+ - name: Sync Python Releases
+ run: make sync-python-releases
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ - name: Create PR
+ uses: peter-evans/create-pull-request@v6
+ with:
+ commit-message: "Sync latest Python releases"
+ add-paths: "rye/src/downloads.inc"
+ branch: "sync-python-releases"
+ title: "Sync Python Releases"
+ body: |
+ - Synced latest Python releases
+
+ Auto-generated by [sync-python-releases.yml](https://github.com/mitsuhiko/rye/blob/main/.github/workflows/sync-python-releases.yml)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cc6ac6d2fe..63a5e48b45 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,13 +3,33 @@
This file contains tracks the changes landing in Rye. It includes changes
that were not yet released.
-## 0.25.0
+## 0.26.0
_Unreleased_
+- Bumped `uv` to 0.1.6. #719
+
+- Bumped `ruff` to 0.2.2. #700
+
+- Prevent `rye toolchain remove` from removing the currently active toolchain. #693
+
+- Sync latest PyPy releases. #683
+
+- Fixes an issue where when `uv` is enabled, `add` did not honor custom sources. #720
+
+- When `uv` is enabled, rye will now automatically sync on `add` and `remove`. #677
+
+- Rename `rye tools list` flags: `-i, --include-scripts` to `-s, --include-scripts` and `-v, --version-show` to `-v, --include-version`. #722
+
+
+
+## 0.25.0
+
+Released on 2024-02-19
+
- Improved the error message if `config` is invoked without arguments. #660
-- Bump `uv` to 0.1.3. #665, #675
+- Bump `uv` to 0.1.5. #665, #675, #698
- When `uv` is enabled, `rye add` now uses `uv` instead of `unearth`
internally. #667
@@ -22,7 +42,13 @@ _Unreleased_
- Fixed the `-q` parameter not working for the `init` command. #686
-
+- `rye tools list` shows broken tools if the toolchain was removed. #692
+
+- Configure the ruff cache directory to be located within the workspace root. #689
+
+- Use default toolchain to install tools. #666
+
+- `rye --version` now shows if `uv` is enabled. #699
## 0.24.0
@@ -160,7 +186,7 @@ Released on 2024-01-15
- Fixed default generated script reference. #527
-- Correctly fall back to home folder if HOME is unset. #533
+- Correctly fall back to home folder if HOME is unset. #533
## 0.16.0
diff --git a/Cargo.lock b/Cargo.lock
index 0f032d6175..0d7c369080 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1070,9 +1070,9 @@ dependencies = [
[[package]]
name = "insta"
-version = "1.34.0"
+version = "1.35.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5d64600be34b2fcfc267740a243fa7744441bb4947a619ac4e5bb6507f35fbfc"
+checksum = "7c985c1bef99cf13c58fade470483d81a2bfe846ebde60ed28cc2dddec2df9e2"
dependencies = [
"console",
"lazy_static",
@@ -1085,9 +1085,9 @@ dependencies = [
[[package]]
name = "insta-cmd"
-version = "0.4.0"
+version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "809d3023d1d6e8d5c2206f199251f75cb26180e41f18cb0f22dd119161cb5127"
+checksum = "1980f17994b79f75670aa90cfc8d35edc4aa248f16aa48b5e27835b080e452a2"
dependencies = [
"insta",
"serde",
@@ -1834,7 +1834,7 @@ dependencies = [
[[package]]
name = "rye"
-version = "0.25.0"
+version = "0.26.0"
dependencies = [
"age",
"anyhow",
diff --git a/Makefile b/Makefile
index aa9a29fffc..82dfb76f71 100644
--- a/Makefile
+++ b/Makefile
@@ -31,3 +31,7 @@ lint:
.venv:
@rye sync
+
+.PHONY: sync-python-releases
+sync-python-releases: .venv
+ @rye run find-downloads > rye/src/downloads.inc
diff --git a/README.md b/README.md
index 932add08ea..d9dc3b305f 100644
--- a/README.md
+++ b/README.md
@@ -36,7 +36,7 @@ Rye picks and ships the right tools so you can get started in minutes:
* **Managing Virtualenvs:** it uses the well established virtualenv library under the hood.
* **Building Wheels:** it delegates that work largely to [build](https://pypi.org/project/build/).
* **Publishing:** its publish command uses [twine](https://pypi.org/project/twine/) to accomplish this task.
-* **Locking and Dependency Installation:** is today implemented by using [unearth](https://pypi.org/project/unearth/) and [pip-tools](https://github.com/jazzband/pip-tools/).
+* **Locking and Dependency Installation:** is today implemented by using [uv](https://github.com/astral-sh/uv) with a fallback to [unearth](https://pypi.org/project/unearth/) and [pip-tools](https://github.com/jazzband/pip-tools/).
* **Workspace support:** Rye lets you work with complex projects consisting
of multiple libraries.
diff --git a/docs/guide/commands/add.md b/docs/guide/commands/add.md
index 23cbb99aa8..816cd154a3 100644
--- a/docs/guide/commands/add.md
+++ b/docs/guide/commands/add.md
@@ -5,9 +5,13 @@ but provides additional helper arguments to make this process more user friendly
instance instead of passing git references within the requiement string, the `--git`
parameter can be used.
-After a dependency is added it's not automatically installed. To do that, you need to
-invoke the [`sync`](sync.md) command. To remove a dependency again use the [`remove`](remove.md)
-command.
+If auto sync is disabled, after a dependency is added it's not automatically
+installed. To do that, you need to invoke the [`sync`](sync.md) command or pass
+`--sync`. To remove a dependency again use the [`remove`](remove.md) command.
+
++++ 0.26.0
+
+ Added support for auto-sync and the `--sync` / `--no-sync` flags.
## Example
@@ -32,6 +36,13 @@ $ rye add flask --git https://github.com/pallets/flask
Added flask @ git+https://github.com/pallets/flask as regular dependency
```
+Add a local dependency:
+
+```
+$ rye add packagename --path path/to/packagename
+Added packagename @ file:///path/to/packagename as regular dependency
+```
+
## Arguments
* `...`: The package to add as PEP 508 requirement string. e.g. 'flask==2.2.3'
@@ -64,6 +75,10 @@ Added flask @ git+https://github.com/pallets/flask as regular dependency
* `--pin `: Overrides the pin operator [possible values: `equal`, `tilde-equal``, `greater-than-equal``]
+* `--sync`: Runs `sync` automatically even if auto-sync is disabled.
+
+* `--no-sync`: Does not run `sync` automatically even if auto-sync is enabled.
+
* `-v, --verbose`: Enables verbose diagnostics
* `-q, --quiet`: Turns off all output
diff --git a/docs/guide/commands/remove.md b/docs/guide/commands/remove.md
index 055f3980e0..8e10d7c003 100644
--- a/docs/guide/commands/remove.md
+++ b/docs/guide/commands/remove.md
@@ -3,6 +3,14 @@
Removes a package from this project. This removes a package from the `pyproject.toml`
dependency list.
+If auto sync is disabled, after a dependency is removed it's not automatically
+uninstalled. To do that, you need to invoke the [`sync`](sync.md) command or pass
+`--sync`.
+
++++ 0.26.0
+
+ Added support for auto-sync and the `--sync` / `--no-sync` flags.
+
## Example
```
@@ -20,6 +28,10 @@ Removed flask>=3.0.1
* `--optional `: Remove this from the optional dependency group
+* `--sync`: Runs `sync` automatically even if auto-sync is disabled.
+
+* `--no-sync`: Does not run `sync` automatically even if auto-sync is enabled.
+
* `-v, --verbose`: Enables verbose diagnostics
* `-q, --quiet`: Turns off all output
diff --git a/docs/guide/commands/toolchain/remove.md b/docs/guide/commands/toolchain/remove.md
index 56dca8755c..ff0ea52de2 100644
--- a/docs/guide/commands/toolchain/remove.md
+++ b/docs/guide/commands/toolchain/remove.md
@@ -15,4 +15,5 @@ Removed installed toolchain cpython@3.9.5
## Options
+* `-f, --force`: Force removal even if the toolchain is in use
* `-h, --help`: Print help (see a summary with '-h')
\ No newline at end of file
diff --git a/docs/guide/commands/tools/index.md b/docs/guide/commands/tools/index.md
index cb8ff88144..2c097c7bc0 100644
--- a/docs/guide/commands/tools/index.md
+++ b/docs/guide/commands/tools/index.md
@@ -5,3 +5,5 @@ Helper utility to manage global tool installations.
* [`install`](install.md): installs a tool globally.
* [`uninstall`](uninstall.md): uninstalls a globally installed tool.
+
+* [`list`](list.md): lists all globally installed tools.
diff --git a/docs/guide/commands/tools/list.md b/docs/guide/commands/tools/list.md
new file mode 100644
index 0000000000..3b266e9179
--- /dev/null
+++ b/docs/guide/commands/tools/list.md
@@ -0,0 +1,41 @@
+# `list`
+
+Lists all already installed global tools.
+
+For more information see [Tools](/guide/tools/).
+
+## Example
+
+List installed tools:
+
+```
+$ rye tools list
+pycowsay
+```
+
+List installed tools with version:
+
+```
+$ rye tools list --include-version
+pycowsay 0.0.0.2 (cpython@3.12.1)
+```
+
+## Arguments
+
+*no arguments*
+
+## Options
+
+* `-s, --include-scripts`: Show all the scripts installed by the tools
+
++/- 0.26.0
+
+ Renamed from `-i, --include-scripts` to `-s, --include-scripts`.
+
+* `-v, --include-version`: Show the version of tools
+
++/- 0.26.0
+
+ Renamed from `-v, --version-show` to `-v, --include-version`.
+
+* `-h, --help`: Print help
diff --git a/docs/guide/config.md b/docs/guide/config.md
index 933532f1a2..93ad8aae0b 100644
--- a/docs/guide/config.md
+++ b/docs/guide/config.md
@@ -85,6 +85,10 @@ global-python = false
# for pip-tools. Learn more about uv here: https://github.com/astral-sh/uv
use-uv = false
+# Enable or disable automatic `sync` after `add` and `remove`. This defaults
+# to `true` when uv is enabled and `false` otherwise.
+autosync = true
+
# Marks the managed .venv in a way that cloud based synchronization systems
# like Dropbox and iCloud Files will not upload it. This defaults to true
# as a .venv in cloud storage typically does not make sense. Set this to
diff --git a/docs/guide/installation.md b/docs/guide/installation.md
index aaa2a43bbf..36b4d0b6b6 100644
--- a/docs/guide/installation.md
+++ b/docs/guide/installation.md
@@ -66,7 +66,7 @@ opt-out, or you run a custom shell you will need to do this manually.
```
In some setups `.profile` is not sourced, in which case you can add it to your
- `.bashrc` instead:
+ `.bashrc`:
```bash
echo 'source "$HOME/.rye/env"' >> ~/.bashrc
@@ -81,7 +81,7 @@ opt-out, or you run a custom shell you will need to do this manually.
```
In some setups `.profile` is not sourced, in which case you can add it to your
- `.zprofile` instead:
+ `.zprofile`:
```bash
echo 'source "$HOME/.rye/env"' >> ~/.zprofile
@@ -89,7 +89,7 @@ opt-out, or you run a custom shell you will need to do this manually.
=== "Fish"
- Since fish does not support `env` files, you instead need to add
+ Since fish does not support `env` files, you need to add
the shims directly. This can be accomplished by running this
command once:
@@ -99,7 +99,7 @@ opt-out, or you run a custom shell you will need to do this manually.
=== "Nushell"
- Since nushell does not support `env` files, you instead need to add
+ Since nushell does not support `env` files, you need to add
the shims directly. This can be accomplished by adding this to your
`env.nu` file:
diff --git a/docs/guide/pyproject.md b/docs/guide/pyproject.md
index 21ea362cdd..832b6b0309 100644
--- a/docs/guide/pyproject.md
+++ b/docs/guide/pyproject.md
@@ -162,7 +162,7 @@ devserver = { cmd = "flask run --debug", env = { FLASK_APP = "./hello.py" } }
This is a special key that can be set instead of `cmd` to make a command invoke multiple
other commands. Each command will be executed one after another. If any of the commands
-fails the rest of the commands won't be executed and instead the chain fails.
+fails, the rest of the commands won't be executed and the chain fails.
```toml
[tool.rye.scripts]
diff --git a/docs/guide/sync.md b/docs/guide/sync.md
index 2e08bace51..6d100253ce 100644
--- a/docs/guide/sync.md
+++ b/docs/guide/sync.md
@@ -1,12 +1,18 @@
# Syncing and Locking
-Rye currently uses [pip-tools](https://github.com/jazzband/pip-tools) to download and install
-dependencies. For this purpose it creates two "lockfiles" (called `requirements.lock` and
-`requirements-dev.lock`). These are not real lockfiles but they fulfill a similar purpose
-until a better solution has been implemented.
+Rye supports two systems to manage dependencies:
+[uv](https://github.com/astral-sh/uv) and
+[pip-tools](https://github.com/jazzband/pip-tools). It currently defaults to
+`pip-tools` but will offer you the option to use `uv` instead. `uv` will become
+the default choice once it stabilzes as it offers significantly better performance.
-Whenever `rye sync` is called, it will update lockfiles as well as the virtualenv. If you only
-want to update the lockfiles, then `rye lock` can be used.
+In order to download dependencies rye creates two "lockfiles" (called
+`requirements.lock` and `requirements-dev.lock`). These are not real lockfiles
+but they fulfill a similar purpose until a better solution has been implemented.
+
+Whenever `rye sync` is called, it will update lockfiles as well as the
+virtualenv. If you only want to update the lockfiles, then `rye lock` can be
+used.
## Lock
@@ -93,3 +99,12 @@ lockfile (`requirements-dev.lock`).
```
rye sync --no-dev
```
+
+## Limitations
+
+Lockfiles depend on the platform they were generated on. This is a known limitation
+in pip-tools.
+
+For example, if your project relies on platform-specific packages and you generate
+lockfiles on Windows, these lockfiles will include Windows-specific projects.
+Consequently, they won't be compatible with other platforms like Linux or macOS.
diff --git a/docs/guide/toolchains/pypy.md b/docs/guide/toolchains/pypy.md
index 5f8bc6b4d1..39bd498ee3 100644
--- a/docs/guide/toolchains/pypy.md
+++ b/docs/guide/toolchains/pypy.md
@@ -15,8 +15,7 @@ target older Python packages.
## Sources
PyPy builds are downloaded from
-[downloads.python.org](https://downloads.python.org/pypy/). These downloads
-are not verified today.
+[downloads.python.org](https://downloads.python.org/pypy/).
## Usage
diff --git a/mkdocs.yml b/mkdocs.yml
index 2949d291ab..6b68ea9442 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -49,6 +49,7 @@ nav:
- Overview: guide/commands/tools/index.md
- install: guide/commands/tools/install.md
- uninstall: guide/commands/tools/uninstall.md
+ - list: guide/commands/tools/list.md
- self:
- Overview: guide/commands/self/index.md
- completion: guide/commands/self/completion.md
diff --git a/notes/metasrv.md b/notes/metasrv.md
index 2a05aae99e..d036e0131f 100644
--- a/notes/metasrv.md
+++ b/notes/metasrv.md
@@ -18,7 +18,7 @@ If you have ever hit that URL you will have realized that it's an enormous HTML
package very uploaded to PyPI. Yet this is still in some sense the canonical way to install
packages. If you for instance use `Rye` today you configure the index by pointing to that URL.
-With the use of a **meta server**, one would instead point it to a meta server instead. So for instance
+With the use of a **meta server**, one would instead point it to a meta server. So for instance
the meta server for `pypi.org` could be hosted at a different URL, say `https://meta.pypi.org/`.
That meta server URL fully replaces the existing index URL. Each meta server is supposed to target
a single index only. A package manager _only_ interfaces with the meta server and it's the meta
diff --git a/requirements-dev.lock b/requirements-dev.lock
index bc4c841d1c..9fd084536d 100644
--- a/requirements-dev.lock
+++ b/requirements-dev.lock
@@ -86,6 +86,8 @@ six==1.16.0
sniffio==1.3.0
# via anyio
# via httpx
+socksio==1.0.0
+ # via httpx
urllib3==2.0.2
# via requests
watchdog==3.0.0
diff --git a/requirements.lock b/requirements.lock
index 040b165533..4bf6f467c2 100644
--- a/requirements.lock
+++ b/requirements.lock
@@ -85,6 +85,8 @@ six==1.16.0
sniffio==1.3.0
# via anyio
# via httpx
+socksio==1.0.0
+ # via httpx
urllib3==2.0.2
# via requests
watchdog==3.0.0
diff --git a/rye-devtools/pyproject.toml b/rye-devtools/pyproject.toml
index 8dd51a15fc..5cc9abd2be 100644
--- a/rye-devtools/pyproject.toml
+++ b/rye-devtools/pyproject.toml
@@ -4,7 +4,7 @@ version = "1.0.0"
description = "Development tools for rye"
authors = [{ name = "Armin Ronacher", email = "armin.ronacher@active-4.com" }]
dependencies = [
- "httpx>=0.26.0",
+ "httpx[socks]>=0.26.0",
]
requires-python = ">= 3.11"
diff --git a/rye-devtools/src/rye_devtools/find_downloads.py b/rye-devtools/src/rye_devtools/find_downloads.py
index 6dc878489e..4f4a5e68e6 100644
--- a/rye-devtools/src/rye_devtools/find_downloads.py
+++ b/rye-devtools/src/rye_devtools/find_downloads.py
@@ -1,401 +1,488 @@
"""This script is used to generate rye/src/downloads.inc.
-It find the latest python-build-standalone releases, sorts them by
+It finds the latest Python releases, sorts them by
various factors (arch, platform, flavor) and generates download
-links to be included into rye at build time. In addition it maintains
-a manual list of pypy downloads to be included into rye at build
-time.
+links to be included into rye at build time.
"""
+import abc
import asyncio
import itertools
+import os
import re
import sys
import time
import unittest
from dataclasses import dataclass
from datetime import datetime, timezone
-from enum import Enum
-from itertools import chain
-from typing import Callable, Optional, Self
+from enum import StrEnum
+from typing import NamedTuple, Self
from urllib.parse import unquote
import httpx
+from httpx import HTTPStatusError
def log(*args, **kwargs):
print(*args, file=sys.stderr, **kwargs)
+
def batched(iterable, n):
"Batch data into tuples of length n. The last batch may be shorter."
# batched('ABCDEFG', 3) --> ABC DEF G
if n < 1:
- raise ValueError('n must be at least one')
+ raise ValueError("n must be at least one")
it = iter(iterable)
while batch := tuple(itertools.islice(it, n)):
yield batch
-TOKEN = open("token.txt").read().strip()
-RELEASE_URL = "https://api.github.com/repos/indygreg/python-build-standalone/releases"
-HEADERS = {
- "X-GitHub-Api-Version": "2022-11-28",
- "Authorization": "Bearer " + TOKEN,
-}
-FLAVOR_PREFERENCES = [
- "shared-pgo",
- "shared-noopt",
- "shared-noopt",
- "pgo+lto",
- "lto",
- "pgo",
-]
-HIDDEN_FLAVORS = [
- "debug",
- "noopt",
- "install_only",
-]
-SPECIAL_TRIPLES = {
- "macos": "x86_64-apple-darwin",
- "linux64": "x86_64-unknown-linux-gnu",
- "windows-amd64": "x86_64-pc-windows-msvc",
- "windows-x86-shared-pgo": "i686-pc-windows-msvc-shared-pgo",
- "windows-amd64-shared-pgo": "x86_64-pc-windows-msvc-shared-pgo",
- "windows-x86": "i686-pc-windows-msvc",
- "linux64-musl": "x86_64-unknown-linux-musl",
-}
-
-# matches these: https://doc.rust-lang.org/std/env/consts/constant.ARCH.html
-ARCH_MAPPING = {
- "x86_64": "x86_64",
- "x86": "x86",
- "i686": "x86",
- "aarch64": "aarch64",
-}
-
-# matches these: https://doc.rust-lang.org/std/env/consts/constant.OS.html
-PLATFORM_MAPPING = {
- "darwin": "macos",
- "windows": "windows",
- "linux": "linux",
-}
-
-ENV_MAPPING = {
- "gnu": "gnu",
- # We must ignore musl for now
- # "musl": "musl",
-}
-
-
-@dataclass(frozen=True)
-class PlatformTriple:
+
+class PlatformTriple(NamedTuple):
arch: str
platform: str
- environment: Optional[str]
- flavor: str
+ environment: str | None
+ flavor: str | None
+
+
+class PythonVersion(NamedTuple):
+ major: int
+ minor: int
+ patch: int
@classmethod
- def from_str(cls, triple: str) -> Optional[Self]:
- """Parse a triple into a PlatformTriple object."""
+ def from_str(cls, version: str) -> Self:
+ return cls(*map(int, version.split(".", 3)))
- # The parsing functions are all very similar and we could abstract them into a single function
- # but I think it's clearer to keep them separate.
- def match_flavor(triple):
- for flavor in FLAVOR_PREFERENCES + HIDDEN_FLAVORS:
- if flavor in triple:
- return flavor
- return ""
+ def __neg__(self) -> Self:
+ return PythonVersion(-self.major, -self.minor, -self.patch)
- def match_mapping(pieces: list[str], mapping: dict[str, str]):
- for i in reversed(range(0, len(pieces))):
- if pieces[i] in mapping:
- return mapping[pieces[i]], pieces[:i]
- return None, pieces
- # We split by '-' and match back to front to extract the flavor, env, platform and archk
- arch, platform, env, flavor = None, None, None, None
+class PythonImplementation(StrEnum):
+ CPYTHON = "cpython"
+ PYPY = "pypy"
- # Map, old, special triplets to proper triples for parsing, or
- # return the triple if it's not a special one
- triple = SPECIAL_TRIPLES.get(triple, triple)
- pieces = triple.split("-")
- flavor = match_flavor(triple)
- env, pieces = match_mapping(pieces, ENV_MAPPING)
- platform, pieces = match_mapping(pieces, PLATFORM_MAPPING)
- arch, pieces = match_mapping(pieces, ARCH_MAPPING)
- if flavor is None or arch is None or platform is None:
- return
+@dataclass
+class PythonDownload:
+ version: PythonVersion
+ triple: PlatformTriple
+ implementation: PythonImplementation
+ filename: str
+ url: str
+ sha256: str | None = None
- if env is None and platform == "linux":
- return
- return cls(arch, platform, env, flavor)
+async def fetch(client: httpx.AsyncClient, url: str) -> httpx.Response:
+ """Fetch from GitHub API with rate limit awareness."""
+ resp = await client.get(url, timeout=15)
+ if (
+ resp.status_code in [403, 429]
+ and resp.headers.get("x-ratelimit-remaining") == "0"
+ ):
+ # See https://docs.github.com/en/rest/using-the-rest-api/troubleshooting-the-rest-api?apiVersion=2022-11-28
+ if (retry_after := resp.headers.get("retry-after")) is not None:
+ log(f"Got retry-after header, retry in {retry_after} seconds.")
+ time.sleep(int(retry_after))
- def grouped(self) -> tuple[str, str]:
- # for now we only group by arch and platform, because rust's PythonVersion doesn't have a notion
- # of environment. Flavor will never be used to sort download choices and must not be included in grouping.
- return self.arch, self.platform
- # return self.arch, self.platform, self.environment or ""
+ return await fetch(client, url)
+ if (retry_at := resp.headers.get("x-ratelimit-reset")) is not None:
+ utc = datetime.now(timezone.utc).timestamp()
+ retry_after = max(int(retry_at) - int(utc), 0)
-@dataclass(frozen=True, order=True)
-class PythonVersion:
- major: int
- minor: int
- patch: int
+ log(f"Got x-ratelimit-reset header, retry in {retry_after} seconds.")
+ time.sleep(retry_after)
- @classmethod
- def from_str(cls, version: str) -> Self:
- return cls(*map(int, version.split(".", 3)))
+ return await fetch(client, url)
+ log("Got rate limited but no information how long, wait for 2 minutes.")
+ time.sleep(60 * 2)
+ return await fetch(client, url)
-@dataclass(frozen=True)
-class IndygregDownload:
- version: PythonVersion
- triple: PlatformTriple
- url: str
+ resp.raise_for_status()
+ return resp
+
+
+class Finder:
+ implementation: PythonImplementation
+
+ @abc.abstractmethod
+ async def find(self) -> list[PythonDownload]:
+ raise NotImplementedError
+
+
+class CPythonFinder(Finder):
+ implementation = PythonImplementation.CPYTHON
+
+ RELEASE_URL = (
+ "https://api.github.com/repos/indygreg/python-build-standalone/releases"
+ )
+
+ FLAVOR_PREFERENCES = [
+ "shared-pgo",
+ "shared-noopt",
+ "shared-noopt",
+ "pgo+lto",
+ "pgo",
+ "lto",
+ ]
+ HIDDEN_FLAVORS = [
+ "debug",
+ "noopt",
+ "install_only",
+ ]
+ SPECIAL_TRIPLES = {
+ "macos": "x86_64-apple-darwin",
+ "linux64": "x86_64-unknown-linux-gnu",
+ "windows-amd64": "x86_64-pc-windows-msvc",
+ "windows-x86-shared-pgo": "i686-pc-windows-msvc-shared-pgo",
+ "windows-amd64-shared-pgo": "x86_64-pc-windows-msvc-shared-pgo",
+ "windows-x86": "i686-pc-windows-msvc",
+ "linux64-musl": "x86_64-unknown-linux-musl",
+ }
+
+ # matches these: https://doc.rust-lang.org/std/env/consts/constant.ARCH.html
+ ARCH_MAPPING = {
+ "x86_64": "x86_64",
+ "x86": "x86",
+ "i686": "x86",
+ "aarch64": "aarch64",
+ }
+
+ # matches these: https://doc.rust-lang.org/std/env/consts/constant.OS.html
+ PLATFORM_MAPPING = {
+ "darwin": "macos",
+ "windows": "windows",
+ "linux": "linux",
+ }
+
+ ENV_MAPPING = {
+ "gnu": "gnu",
+ # We must ignore musl for now
+ # "musl": "musl",
+ }
FILENAME_RE = re.compile(
r"""(?x)
- ^
- cpython-(?P\d+\.\d+\.\d+?)
- (?:\+\d+)?
- -(?P.*?)
- (?:-[\dT]+)?\.tar\.(?:gz|zst)
- $
- """
+ ^
+ cpython-(?P\d+\.\d+\.\d+?)
+ (?:\+\d+)?
+ -(?P.*?)
+ (?:-[\dT]+)?\.tar\.(?:gz|zst)
+ $
+"""
)
+ def __init__(self, client: httpx.AsyncClient):
+ self.client = client
+
+ async def find(self) -> list[PythonDownload]:
+ downloads = await self.fetch_indygreg_downloads()
+ await self.fetch_indygreg_checksums(downloads, n=20)
+ return downloads
+
+ async def fetch_indygreg_downloads(self, pages: int = 100) -> list[PythonDownload]:
+ """Fetch all the indygreg downloads from the release API."""
+ results: dict[PythonVersion, dict[tuple[str, str], list[PythonDownload]]] = {}
+
+ for page in range(1, pages):
+ log(f"Fetching indygreg release page {page}")
+ resp = await fetch(self.client, "%s?page=%d" % (self.RELEASE_URL, page))
+ rows = resp.json()
+ if not rows:
+ break
+ for row in rows:
+ for asset in row["assets"]:
+ url = asset["browser_download_url"]
+ download = self.parse_download_url(url)
+ if download is not None:
+ (
+ results.setdefault(download.version, {})
+ # For now, we only group by arch and platform, because Rust's PythonVersion doesn't have a notion
+ # of environment. Flavor will never be used to sort download choices and must not be included in grouping.
+ .setdefault(
+ (download.triple.arch, download.triple.platform), []
+ )
+ .append(download)
+ )
+
+ downloads = []
+ for version, platform_downloads in results.items():
+ for flavors in platform_downloads.values():
+ best = self.pick_best_download(flavors)
+ if best is not None:
+ downloads.append(best)
+ return downloads
+
@classmethod
- def from_url(cls, url) -> Optional[Self]:
- base_name = unquote(url.rsplit("/")[-1])
- if base_name.endswith(".sha256"):
+ def parse_download_url(cls, url: str) -> PythonDownload | None:
+ """Parse an indygreg download URL into a PythonDownload object."""
+ # The URL looks like this:
+ # https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.12.1%2B20240107-aarch64-unknown-linux-gnu-lto-full.tar.zst
+ filename = unquote(url.rsplit("/", maxsplit=1)[-1])
+ if filename.endswith(".sha256"):
return
- match = cls.FILENAME_RE.match(base_name)
+ match = cls.FILENAME_RE.match(filename)
if match is None:
return
- # Parse version string and triplet string
version_str, triple_str = match.groups()
version = PythonVersion.from_str(version_str)
- triple = PlatformTriple.from_str(triple_str)
+ triple = cls.parse_triple(triple_str)
if triple is None:
return
- return cls(version, triple, url)
-
- async def sha256(self, client: httpx.AsyncClient) -> Optional[str]:
- """We only fetch the sha256 when needed. This generally is AFTER we have
- decided that the download will be part of rye's download set"""
- resp = await fetch(client, self.url + ".sha256", headers=HEADERS)
- if 200 <= resp.status_code < 400:
- return resp.text.strip()
- return None
+ return PythonDownload(
+ version=version,
+ triple=triple,
+ implementation=PythonImplementation.CPYTHON,
+ filename=filename,
+ url=url,
+ )
+ @classmethod
+ def parse_triple(cls, triple: str) -> PlatformTriple | None:
+ """Parse a triple into a PlatformTriple object."""
-async def fetch(client: httpx.AsyncClient, page: str, headers: dict[str, str]):
- """Fetch a page from GitHub API with ratelimit awareness."""
- resp = await client.get(page, headers=headers, timeout=90)
- if (
- resp.status_code in [403, 429]
- and resp.headers.get("x-ratelimit-remaining") == "0"
- ):
- # See https://docs.github.com/en/rest/using-the-rest-api/troubleshooting-the-rest-api?apiVersion=2022-11-28
- if (retry_after := resp.headers.get("retry-after")) is not None:
- log("got retry-after header. retrying in {retry_after} seconds.")
- time.sleep(int(retry_after))
+ def match_flavor(triple: str) -> str | None:
+ for flavor in cls.FLAVOR_PREFERENCES + cls.HIDDEN_FLAVORS:
+ if flavor in triple:
+ return flavor
+ return None
- return await fetch(client, page, headers)
+ def match_mapping(
+ pieces: list[str], mapping: dict[str, str]
+ ) -> tuple[str | None, list[str]]:
+ for i in reversed(range(0, len(pieces))):
+ if pieces[i] in mapping:
+ return mapping[pieces[i]], pieces[:i]
+ return None, pieces
- if (retry_at := resp.headers.get("x-ratelimit-reset")) is not None:
- utc = datetime.now(timezone.utc).timestamp()
- retry_after = int(retry_at) - int(utc)
+ # Map, old, special triplets to proper triples for parsing, or
+ # return the triple if it's not a special one
+ triple = cls.SPECIAL_TRIPLES.get(triple, triple)
+ pieces = triple.split("-")
+ flavor = match_flavor(triple)
+ env, pieces = match_mapping(pieces, cls.ENV_MAPPING)
+ platform, pieces = match_mapping(pieces, cls.PLATFORM_MAPPING)
+ arch, pieces = match_mapping(pieces, cls.ARCH_MAPPING)
- log("got x-ratelimit-reset header. retrying in {retry_after} seconds.")
- time.sleep(max(int(retry_at) - int(utc), 0))
+ if arch is None or platform is None:
+ return
- return await fetch(client, page, headers)
+ if env is None and platform == "linux":
+ return
- log("got rate limited but no information how long. waiting for 2 minutes")
- time.sleep(60 * 2)
- return await fetch(client, page, headers)
- return resp
+ return PlatformTriple(arch, platform, env, flavor)
+ @classmethod
+ def pick_best_download(
+ cls, downloads: list[PythonDownload]
+ ) -> PythonDownload | None:
+ """Pick the best download from the list of downloads."""
+
+ def preference(download: PythonDownload) -> int:
+ try:
+ return cls.FLAVOR_PREFERENCES.index(download.triple.flavor)
+ except ValueError:
+ return len(cls.FLAVOR_PREFERENCES) + 1
+
+ downloads.sort(key=preference)
+ return downloads[0] if downloads else None
+
+ async def fetch_indygreg_checksums(
+ self, downloads: list[PythonDownload], n: int = 10
+ ) -> None:
+ """Fetch the checksums for the given downloads."""
+ checksums_url = set()
+ for download in downloads:
+ release_url = download.url.rsplit("/", maxsplit=1)[0]
+ checksum_url = release_url + "/SHA256SUMS"
+ checksums_url.add(checksum_url)
+
+ async def fetch_checksums(url: str):
+ try:
+ resp = await fetch(self.client, url)
+ except HTTPStatusError as e:
+ if e.response.status_code != 404:
+ raise
+ return None
+ return resp
+
+ completed = 0
+ tasks = []
+ for batch in batched(checksums_url, n):
+ log(f"Fetching indygreg checksums: {completed}/{len(checksums_url)}")
+ async with asyncio.TaskGroup() as tg:
+ for url in batch:
+ task = tg.create_task(fetch_checksums(url))
+ tasks.append(task)
+ completed += n
+
+ checksums = {}
+ for task in tasks:
+ resp = task.result()
+ if resp is None:
+ continue
+ lines = resp.text.splitlines()
+ for line in lines:
+ checksum, filename = line.split(" ", maxsplit=1)
+ filename = filename.strip()
+ checksums[filename] = checksum
+
+ for download in downloads:
+ download.sha256 = checksums.get(download.filename)
+
+
+class PyPyFinder(Finder):
+ implementation = PythonImplementation.PYPY
+
+ RELEASE_URL = "https://raw.githubusercontent.com/pypy/pypy/main/pypy/tool/release/versions.json"
+ CHECKSUM_URL = (
+ "https://raw.githubusercontent.com/pypy/pypy.org/main/pages/checksums.rst"
+ )
+ CHECKSUM_RE = re.compile(
+ r"^\s*(?P\w{64})\s+(?Ppypy.+)$", re.MULTILINE
+ )
-async def fetch_indiygreg_downloads(
- client: httpx.AsyncClient,
- pages: int = 100,
-) -> dict[PythonVersion, dict[PlatformTriple, list[IndygregDownload]]]:
- """Fetch all the indygreg downloads from the release API."""
- results = {}
+ ARCH_MAPPING = {
+ "x64": "x86_64",
+ "i686": "x86",
+ "aarch64": "aarch64",
+ "arm64": "aarch64",
+ }
+
+ PLATFORM_MAPPING = {
+ "darwin": "macos",
+ "win64": "windows",
+ "linux": "linux",
+ }
+
+ def __init__(self, client: httpx.AsyncClient):
+ self.client = client
+
+ async def find(self) -> list[PythonDownload]:
+ downloads = await self.fetch_downloads()
+ await self.fetch_checksums(downloads)
+ return downloads
+
+ async def fetch_downloads(self) -> list[PythonDownload]:
+ log("Fetching pypy downloads...")
+ resp = await fetch(self.client, self.RELEASE_URL)
+ versions = resp.json()
+
+ results = {}
+ for version in versions:
+ if not version["stable"]:
+ continue
+ python_version = PythonVersion.from_str(version["python_version"])
+ if python_version < (3, 7, 0):
+ continue
+ for file in version["files"]:
+ arch = self.ARCH_MAPPING.get(file["arch"])
+ platform = self.PLATFORM_MAPPING.get(file["platform"])
+ if arch is None or platform is None:
+ continue
+ environment = "gnu" if platform == "linux" else None
+ download = PythonDownload(
+ version=python_version,
+ triple=PlatformTriple(
+ arch=arch,
+ platform=platform,
+ environment=environment,
+ flavor=None,
+ ),
+ implementation=PythonImplementation.PYPY,
+ filename=file["filename"],
+ url=file["download_url"],
+ )
+ # Only keep the latest pypy version of each arch/platform
+ if (python_version, arch, platform) not in results:
+ results[(python_version, arch, platform)] = download
+
+ return list(results.values())
+
+ async def fetch_checksums(self, downloads: list[PythonDownload]) -> None:
+ log("Fetching pypy checksums...")
+ resp = await fetch(self.client, self.CHECKSUM_URL)
+ text = resp.text
+
+ checksums = {}
+ for match in self.CHECKSUM_RE.finditer(text):
+ checksums[match.group("filename")] = match.group("checksum")
+
+ for download in downloads:
+ download.sha256 = checksums.get(download.filename)
+
+
+def render(downloads: list[PythonDownload]):
+ """Render downloads.inc."""
+
+ def sort_key(download: PythonDownload) -> tuple[int, PythonVersion, PlatformTriple]:
+ # Sort by implementation, version (latest first), and then by triple.
+ impl_order = [PythonImplementation.PYPY, PythonImplementation.CPYTHON]
+ return (
+ impl_order.index(download.implementation),
+ -download.version,
+ download.triple,
+ )
+
+ downloads.sort(key=sort_key)
+
+ print("// Generated by rye-devtools. DO NOT EDIT.")
+ print(
+ "// To regenerate, run `rye run find-downloads > rye/src/downloads.inc` from the root of the repository."
+ )
+ print("use std::borrow::Cow;")
+ print("pub const PYTHON_VERSIONS: &[(PythonVersion, &str, Option<&str>)] = &[")
- for page in range(1, pages):
- log(f"Fetching page {page}")
- resp = await fetch(client, "%s?page=%d" % (RELEASE_URL, page), headers=HEADERS)
- rows = resp.json()
- if not rows:
- break
- for row in rows:
- for asset in row["assets"]:
- url = asset["browser_download_url"]
- if (download := IndygregDownload.from_url(url)) is not None:
- results.setdefault(download.version, {}).setdefault(download.triple.grouped(), []).append(download)
- return results
+ for download in downloads:
+ triple = download.triple
+ version = download.version
+ sha256 = f'Some("{download.sha256}")' if download.sha256 else "None"
+ print(
+ f' (PythonVersion {{ name: Cow::Borrowed("{download.implementation}"), arch: Cow::Borrowed("{triple.arch}"), os: Cow::Borrowed("{triple.platform}"), major: {version.major}, minor: {version.minor}, patch: {version.patch}, suffix: None }}, "{download.url}", {sha256}),'
+ )
+ print("];")
-def pick_best_download(downloads: list[IndygregDownload]) -> Optional[IndygregDownload]:
- """Pick the best download from the list of downloads."""
- def preference(download: IndygregDownload) -> int:
+async def async_main():
+ token = os.environ.get("GITHUB_TOKEN")
+ if not token:
try:
- return FLAVOR_PREFERENCES.index(download.triple.flavor)
- except ValueError:
- return len(FLAVOR_PREFERENCES) + 1
-
- downloads.sort(key=preference)
- return downloads[0] if downloads else None
-
-async def fetch_sha256s(
- client: httpx.AsyncClient,
- indys: dict[PythonVersion, list[IndygregDownload]],
- n: int = 10,
-) -> dict[str, str]:
- # flatten
- downloads = (download for downloads in indys.values() for download in downloads)
- length = sum([len(downloads) for downloads in indys.values()])
-
- completed = 0
- tasks = []
- for batch in batched(downloads, n=n):
- log(f"fetching {n} sha256s: {completed}/{length} completed")
- async with asyncio.TaskGroup() as tg:
- for download in batch:
- task = tg.create_task(download.sha256(client))
- tasks.append((download.url, task))
- completed += n
- return {url: task.result() for url, task in tasks}
-
-def render(
- indys: dict[PythonVersion, list[IndygregDownload]],
- pypy: dict[PythonVersion, dict[PlatformTriple, str]],
- sha256s: dict[str, str]
-):
- """Render downloads.inc"""
- log("Generating code and fetching sha256 of all cpython downloads.")
- log("This can be slow......")
-
- print("// generated code, do not edit")
- print("use std::borrow::Cow;")
- print("pub const PYTHON_VERSIONS: &[(PythonVersion, &str, Option<&str>)] = &[")
+ token = open("token.txt").read().strip()
+ except Exception:
+ pass
- for version, downloads in sorted(pypy.items(), key=lambda v: v[0], reverse=True):
- for triple, url in sorted(downloads.items(), key=lambda v: v[0].grouped()):
- print(
- f' (PythonVersion {{ name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("{triple.arch}"), os: Cow::Borrowed("{triple.platform}"), major: {version.major}, minor: {version.minor}, patch: {version.patch}, suffix: None }}, "{url}", None),'
- )
-
- for version, downloads in sorted(indys.items(), key=lambda v: v[0], reverse=True):
- for download in sorted(downloads, key=lambda v: v.triple.grouped()):
- if (sha256 := sha256s.get(download.url)) is not None:
- sha256_str = f'Some("{sha256}")'
- else:
- sha256_str = "None"
- print(
- f' (PythonVersion {{ name: Cow::Borrowed("cpython"), arch: Cow::Borrowed("{download.triple.arch}"), os: Cow::Borrowed("{download.triple.platform}"), major: {version.major}, minor: {version.minor}, patch: {version.patch}, suffix: None }}, "{download.url}", {sha256_str}),'
- )
- print("];")
+ if not token:
+ log("Please set GITHUB_TOKEN environment variable or create a token.txt file.")
+ sys.exit(1)
+ headers = {
+ "X-GitHub-Api-Version": "2022-11-28",
+ "Authorization": "Bearer " + token,
+ }
+ client = httpx.AsyncClient(follow_redirects=True, headers=headers)
+
+ finders = [
+ CPythonFinder(client),
+ PyPyFinder(client),
+ ]
+ downloads = []
+
+ log("Fetching all Python downloads and generating code.")
+ async with client:
+ for finder in finders:
+ log(f"Finding {finder.implementation} downloads...")
+ downloads.extend(await finder.find())
+
+ render(downloads)
-async def async_main():
- log("Rye download creator started.")
- log("Fetching indygreg downloads...")
-
- indys = {}
- # For every version, pick the best download per triple
- # and store it in the results
- async with httpx.AsyncClient(follow_redirects=True) as client:
- downloads = await fetch_indiygreg_downloads(client, 100)
- for version, download_choices in downloads.items():
- # Create a dict[PlatformTriple, list[IndygregDownload]]]
- # for each version
- for triple, choices in download_choices.items():
- if (best_download := pick_best_download(choices)) is not None:
- indys.setdefault(version, []).append(best_download)
-
- sha256s = await fetch_sha256s(client, indys, n=25)
- render(indys, PYPY_DOWNLOADS, sha256s)
def main():
asyncio.run(async_main())
-# These are manually maintained for now
-PYPY_DOWNLOADS = {
- PythonVersion(3, 10, 12): {
- PlatformTriple(
- arch="x86_64", platform="linux", environment="gnu", flavor=""
- ): "https://downloads.python.org/pypy/pypy3.10-v7.3.12-linux64.tar.bz2",
- PlatformTriple(
- arch="aarch64", platform="linux", environment="gnu", flavor=""
- ): "https://downloads.python.org/pypy/pypy3.10-v7.3.12-aarch64.tar.bz2",
- PlatformTriple(
- arch="x86_64", platform="macos", environment=None, flavor=""
- ): "https://downloads.python.org/pypy/pypy3.10-v7.3.12-macos_x86_64.tar.bz2",
- PlatformTriple(
- arch="aarch64", platform="macos", environment=None, flavor=""
- ): "https://downloads.python.org/pypy/pypy3.10-v7.3.12-macos_arm64.tar.bz2",
- PlatformTriple(
- arch="x86_64", platform="windows", environment=None, flavor=""
- ): "https://downloads.python.org/pypy/pypy3.10-v7.3.12-win64.zip",
- },
- PythonVersion(3, 9, 16): {
- PlatformTriple(
- arch="x86_64", platform="linux", environment="gnu", flavor=""
- ): "https://downloads.python.org/pypy/pypy3.9-v7.3.11-linux64.tar.bz2",
- PlatformTriple(
- arch="aarch64", platform="linux", environment="gnu", flavor=""
- ): "https://downloads.python.org/pypy/pypy3.9-v7.3.11-aarch64.tar.bz2",
- PlatformTriple(
- arch="x86_64", platform="macos", environment=None, flavor=""
- ): "https://downloads.python.org/pypy/pypy3.9-v7.3.11-macos_x86_64.tar.bz2",
- PlatformTriple(
- arch="aarch64", platform="macos", environment=None, flavor=""
- ): "https://downloads.python.org/pypy/pypy3.9-v7.3.11-macos_arm64.tar.bz2",
- PlatformTriple(
- arch="x86_64", platform="windows", environment=None, flavor=""
- ): "https://downloads.python.org/pypy/pypy3.9-v7.3.11-win64.zip",
- },
- PythonVersion(3, 8, 16): {
- PlatformTriple(
- arch="x86_64", platform="linux", environment="gnu", flavor=""
- ): "https://downloads.python.org/pypy/pypy3.8-v7.3.11-linux64.tar.bz2",
- PlatformTriple(
- arch="aarch64", platform="linux", environment="gnu", flavor=""
- ): "https://downloads.python.org/pypy/pypy3.8-v7.3.11-aarch64.tar.bz2",
- PlatformTriple(
- arch="x86_64", platform="macos", environment=None, flavor=""
- ): "https://downloads.python.org/pypy/pypy3.8-v7.3.11-macos_x86_64.tar.bz2",
- PlatformTriple(
- arch="aarch64", platform="macos", environment=None, flavor=""
- ): "https://downloads.python.org/pypy/pypy3.8-v7.3.11-macos_arm64.tar.bz2",
- PlatformTriple(
- arch="x86_64", platform="windows", environment=None, flavor=""
- ): "https://downloads.python.org/pypy/pypy3.8-v7.3.11-win64.zip",
- },
- PythonVersion(3, 7, 13): {
- PlatformTriple(
- arch="x86_64", platform="linux", environment="gnu", flavor=""
- ): "https://downloads.python.org/pypy/pypy3.7-v7.3.9-linux64.tar.bz2",
- PlatformTriple(
- arch="aarch64", platform="linux", environment="gnu", flavor=""
- ): "https://downloads.python.org/pypy/pypy3.7-v7.3.9-aarch64.tar.bz2",
- PlatformTriple(
- arch="x86_64", platform="macos", environment=None, flavor=""
- ): "https://downloads.python.org/pypy/pypy3.7-v7.3.9-osx64.tar.bz2",
- PlatformTriple(
- arch="x86_64", platform="windows", environment=None, flavor=""
- ): "https://downloads.python.org/pypy/pypy3.7-v7.3.9-win64.zip",
- },
-}
if __name__ == "__main__":
main()
@@ -417,7 +504,7 @@ def test_parse_triplets(self):
"x86_64-unknown-linux-gnu-debug": PlatformTriple(
"x86_64", "linux", "gnu", "debug"
),
- "linux64": PlatformTriple("x86_64", "linux", "gnu", ""),
+ "linux64": PlatformTriple("x86_64", "linux", "gnu", None),
"ppc64le-unknown-linux-gnu-noopt-full": None,
"x86_64_v3-unknown-linux-gnu-lto": None,
"x86_64-pc-windows-msvc-shared-pgo": PlatformTriple(
@@ -426,4 +513,4 @@ def test_parse_triplets(self):
}
for input, expected in expected.items():
- self.assertEqual(PlatformTriple.from_str(input), expected, input)
+ self.assertEqual(CPythonFinder.parse_triple(input), expected, input)
diff --git a/rye/Cargo.toml b/rye/Cargo.toml
index 79d0752a4c..916733b17c 100644
--- a/rye/Cargo.toml
+++ b/rye/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "rye"
-version = "0.25.0"
+version = "0.26.0"
edition = "2021"
license = "MIT"
@@ -74,5 +74,5 @@ static_vcruntime = "2.0.0"
[dev-dependencies]
fslock = "0.2.1"
-insta = { version = "1.34.0", features = ["filters"] }
-insta-cmd = "0.4.0"
+insta = { version = "1.35.1", features = ["filters"] }
+insta-cmd = "0.5.0"
diff --git a/rye/find-downloads.py b/rye/find-downloads.py
deleted file mode 100644
index fed44deabb..0000000000
--- a/rye/find-downloads.py
+++ /dev/null
@@ -1,394 +0,0 @@
-"""This script is used to generate rye/src/downloads.inc.
-
-It find the latest python-build-standalone releases, sorts them by
-various factors (arch, platform, flavor) and generates download
-links to be included into rye at build time. In addition it maintains
-a manual list of pypy downloads to be included into rye at build
-time.
-"""
-import re
-import sys
-import time
-import unittest
-from dataclasses import dataclass
-from datetime import datetime, timezone
-from enum import Enum
-from itertools import chain
-from typing import Callable, Optional, Self
-from urllib.parse import unquote
-
-import requests
-
-
-def log(*args, **kwargs):
- print(*args, file=sys.stderr, **kwargs)
-
-
-SESSION = requests.Session()
-TOKEN = open("token.txt").read().strip()
-RELEASE_URL = "https://api.github.com/repos/indygreg/python-build-standalone/releases"
-HEADERS = {
- "X-GitHub-Api-Version": "2022-11-28",
- "Authorization": "Bearer " + TOKEN,
-}
-FLAVOR_PREFERENCES = [
- "shared-pgo",
- "shared-noopt",
- "shared-noopt",
- "pgo+lto",
- "lto",
- "pgo",
-]
-HIDDEN_FLAVORS = [
- "debug",
- "noopt",
- "install_only",
-]
-SPECIAL_TRIPLES = {
- "macos": "x86_64-apple-darwin",
- "linux64": "x86_64-unknown-linux-gnu",
- "windows-amd64": "x86_64-pc-windows-msvc",
- "windows-x86-shared-pgo": "i686-pc-windows-msvc-shared-pgo",
- "windows-amd64-shared-pgo": "x86_64-pc-windows-msvc-shared-pgo",
- "windows-x86": "i686-pc-windows-msvc",
- "linux64-musl": "x86_64-unknown-linux-musl",
-}
-
-# matches these: https://doc.rust-lang.org/std/env/consts/constant.ARCH.html
-ARCH_MAPPING = {
- "x86_64": "x86_64",
- "x86": "x86",
- "i686": "x86",
- "aarch64": "aarch64",
-}
-
-# matches these: https://doc.rust-lang.org/std/env/consts/constant.OS.html
-PLATFORM_MAPPING = {
- "darwin": "macos",
- "windows": "windows",
- "linux": "linux",
-}
-
-ENV_MAPPING = {
- "gnu": "gnu",
- # We must ignore musl for now
- # "musl": "musl",
-}
-
-
-@dataclass(frozen=True)
-class PlatformTriple:
- arch: str
- platform: str
- environment: Optional[str]
- flavor: str
-
- @classmethod
- def from_str(cls, triple: str) -> Optional[Self]:
- """Parse a triple into a PlatformTriple object."""
-
- # The parsing functions are all very similar and we could abstract them into a single function
- # but I think it's clearer to keep them separate.
- def match_flavor(triple):
- for flavor in FLAVOR_PREFERENCES + HIDDEN_FLAVORS:
- if flavor in triple:
- return flavor
- return ""
-
- def match_mapping(pieces: list[str], mapping: dict[str, str]):
- for i in reversed(range(0, len(pieces))):
- if pieces[i] in mapping:
- return mapping[pieces[i]], pieces[:i]
- return None, pieces
-
- # We split by '-' and match back to front to extract the flavor, env, platform and archk
- arch, platform, env, flavor = None, None, None, None
-
- # Map, old, special triplets to proper triples for parsing, or
- # return the triple if it's not a special one
- triple = SPECIAL_TRIPLES.get(triple, triple)
- pieces = triple.split("-")
- flavor = match_flavor(triple)
- env, pieces = match_mapping(pieces, ENV_MAPPING)
- platform, pieces = match_mapping(pieces, PLATFORM_MAPPING)
- arch, pieces = match_mapping(pieces, ARCH_MAPPING)
-
- if flavor is None or arch is None or platform is None:
- return
-
- if env is None and platform == "linux":
- return
-
- return cls(arch, platform, env, flavor)
-
- def grouped(self) -> tuple[str, str]:
- # for now we only group by arch and platform, because rust's PythonVersion doesn't have a notion
- # of environment. Flavor will never be used to sort download choices and must not be included in grouping.
- return self.arch, self.platform
- # return self.arch, self.platform, self.environment or ""
-
-
-@dataclass(frozen=True, order=True)
-class PythonVersion:
- major: int
- minor: int
- patch: int
-
- @classmethod
- def from_str(cls, version: str) -> Self:
- return cls(*map(int, version.split(".", 3)))
-
-
-@dataclass(frozen=True)
-class IndygregDownload:
- version: PythonVersion
- triple: PlatformTriple
- url: str
-
- FILENAME_RE = re.compile(
- r"""(?x)
- ^
- cpython-(?P\d+\.\d+\.\d+?)
- (?:\+\d+)?
- -(?P.*?)
- (?:-[\dT]+)?\.tar\.(?:gz|zst)
- $
- """
- )
-
- @classmethod
- def from_url(cls, url) -> Optional[Self]:
- base_name = unquote(url.rsplit("/")[-1])
- if base_name.endswith(".sha256"):
- return
-
- match = cls.FILENAME_RE.match(base_name)
- if match is None:
- return
-
- # Parse version string and triplet string
- version_str, triple_str = match.groups()
- version = PythonVersion.from_str(version_str)
- triple = PlatformTriple.from_str(triple_str)
- if triple is None:
- return
-
- return cls(version, triple, url)
-
- def sha256(self) -> Optional[str]:
- """We only fetch the sha256 when needed. This generally is AFTER we have
- decided that the download will be part of rye's download set"""
- resp = fetch(self.url + ".sha256", headers=HEADERS)
- if not resp.ok:
- return None
- return resp.text.strip()
-
-
-def fetch(page, headers):
- """Fetch a page from GitHub API with ratelimit awareness."""
- resp = SESSION.get(page, headers=headers, timeout=90)
- if (
- resp.status_code in [403, 429]
- and resp.headers.get("x-ratelimit-remaining") == "0"
- ):
- # See https://docs.github.com/en/rest/using-the-rest-api/troubleshooting-the-rest-api?apiVersion=2022-11-28
- if (retry_after := resp.headers.get("retry-after")) is not None:
- log("got retry-after header. retrying in {retry_after} seconds.")
- time.sleep(int(retry_after))
-
- return fetch(page, headers)
-
- if (retry_at := resp.headers.get("x-ratelimit-reset")) is not None:
- utc = datetime.now(timezone.utc).timestamp()
- retry_after = int(retry_at) - int(utc)
-
- log("got x-ratelimit-reset header. retrying in {retry_after} seconds.")
- time.sleep(max(int(retry_at) - int(utc), 0))
-
- return fetch(page, headers)
-
- log("got rate limited but no information how long. waiting for 2 minutes")
- time.sleep(60 * 2)
- return fetch(page, headers)
- return resp
-
-
-def fetch_indiygreg_downloads(
- pages: int = 100,
-) -> dict[PythonVersion, dict[PlatformTriple, list[IndygregDownload]]]:
- """Fetch all the indygreg downloads from the release API."""
- results = {}
-
- for page in range(1, pages):
- log(f"Fetching page {page}")
- resp = fetch("%s?page=%d" % (RELEASE_URL, page), headers=HEADERS)
- rows = resp.json()
- if not rows:
- break
- for row in rows:
- for asset in row["assets"]:
- url = asset["browser_download_url"]
- if (download := IndygregDownload.from_url(url)) is not None:
- results.setdefault(download.version, {}).setdefault(download.triple.grouped(), []).append(download)
- return results
-
-
-def pick_best_download(downloads: list[IndygregDownload]) -> Optional[IndygregDownload]:
- """Pick the best download from the list of downloads."""
-
- def preference(download: IndygregDownload) -> int:
- try:
- return FLAVOR_PREFERENCES.index(download.triple.flavor)
- except ValueError:
- return len(FLAVOR_PREFERENCES) + 1
-
- downloads.sort(key=preference)
- return downloads[0] if downloads else None
-
-
-def render(
- indys: dict[PythonVersion, list[IndygregDownload]],
- pypy: dict[PythonVersion, dict[PlatformTriple, str]],
-):
- """Render downloads.inc"""
- log("Generating code and fetching sha256 of all cpython downloads.")
- log("This can be slow......")
-
- print("// generated code, do not edit")
- print("use std::borrow::Cow;")
- print("pub const PYTHON_VERSIONS: &[(PythonVersion, &str, Option<&str>)] = &[")
-
- for version, downloads in sorted(pypy.items(), key=lambda v: v[0], reverse=True):
- for triple, url in sorted(downloads.items(), key=lambda v: v[0].grouped()):
- print(
- f' (PythonVersion {{ name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("{triple.arch}"), os: Cow::Borrowed("{triple.platform}"), major: {version.major}, minor: {version.minor}, patch: {version.patch}, suffix: None }}, "{url}", None),'
- )
-
- for version, downloads in sorted(indys.items(), key=lambda v: v[0], reverse=True):
- for download in sorted(downloads, key=lambda v: v.triple.grouped()):
- if (sha256 := download.sha256()) is not None:
- sha256_str = f'Some("{sha256}")'
- else:
- sha256_str = "None"
- print(
- f' (PythonVersion {{ name: Cow::Borrowed("cpython"), arch: Cow::Borrowed("{download.triple.arch}"), os: Cow::Borrowed("{download.triple.platform}"), major: {version.major}, minor: {version.minor}, patch: {version.patch}, suffix: None }}, "{download.url}", {sha256_str}),'
- )
- print("];")
-
-
-def main():
- log("Rye download creator started.")
- log("Fetching indygreg downloads...")
-
- indys = {}
- # For every version, pick the best download per triple
- # and store it in the results
- for version, download_choices in fetch_indiygreg_downloads(100).items():
- # Create a dict[PlatformTriple, list[IndygregDownload]]]
- # for each version
- for triple, choices in download_choices.items():
- if (best_download := pick_best_download(choices)) is not None:
- indys.setdefault(version, []).append(best_download)
-
- render(indys, PYPY_DOWNLOADS)
-
-
-# These are manually maintained for now
-PYPY_DOWNLOADS = {
- PythonVersion(3, 10, 12): {
- PlatformTriple(
- arch="x86_64", platform="linux", environment="gnu", flavor=""
- ): "https://downloads.python.org/pypy/pypy3.10-v7.3.12-linux64.tar.bz2",
- PlatformTriple(
- arch="aarch64", platform="linux", environment="gnu", flavor=""
- ): "https://downloads.python.org/pypy/pypy3.10-v7.3.12-aarch64.tar.bz2",
- PlatformTriple(
- arch="x86_64", platform="macos", environment=None, flavor=""
- ): "https://downloads.python.org/pypy/pypy3.10-v7.3.12-macos_x86_64.tar.bz2",
- PlatformTriple(
- arch="aarch64", platform="macos", environment=None, flavor=""
- ): "https://downloads.python.org/pypy/pypy3.10-v7.3.12-macos_arm64.tar.bz2",
- PlatformTriple(
- arch="x86_64", platform="windows", environment=None, flavor=""
- ): "https://downloads.python.org/pypy/pypy3.10-v7.3.12-win64.zip",
- },
- PythonVersion(3, 9, 16): {
- PlatformTriple(
- arch="x86_64", platform="linux", environment="gnu", flavor=""
- ): "https://downloads.python.org/pypy/pypy3.9-v7.3.11-linux64.tar.bz2",
- PlatformTriple(
- arch="aarch64", platform="linux", environment="gnu", flavor=""
- ): "https://downloads.python.org/pypy/pypy3.9-v7.3.11-aarch64.tar.bz2",
- PlatformTriple(
- arch="x86_64", platform="macos", environment=None, flavor=""
- ): "https://downloads.python.org/pypy/pypy3.9-v7.3.11-macos_x86_64.tar.bz2",
- PlatformTriple(
- arch="aarch64", platform="macos", environment=None, flavor=""
- ): "https://downloads.python.org/pypy/pypy3.9-v7.3.11-macos_arm64.tar.bz2",
- PlatformTriple(
- arch="x86_64", platform="windows", environment=None, flavor=""
- ): "https://downloads.python.org/pypy/pypy3.9-v7.3.11-win64.zip",
- },
- PythonVersion(3, 8, 16): {
- PlatformTriple(
- arch="x86_64", platform="linux", environment="gnu", flavor=""
- ): "https://downloads.python.org/pypy/pypy3.8-v7.3.11-linux64.tar.bz2",
- PlatformTriple(
- arch="aarch64", platform="linux", environment="gnu", flavor=""
- ): "https://downloads.python.org/pypy/pypy3.8-v7.3.11-aarch64.tar.bz2",
- PlatformTriple(
- arch="x86_64", platform="macos", environment=None, flavor=""
- ): "https://downloads.python.org/pypy/pypy3.8-v7.3.11-macos_x86_64.tar.bz2",
- PlatformTriple(
- arch="aarch64", platform="macos", environment=None, flavor=""
- ): "https://downloads.python.org/pypy/pypy3.8-v7.3.11-macos_arm64.tar.bz2",
- PlatformTriple(
- arch="x86_64", platform="windows", environment=None, flavor=""
- ): "https://downloads.python.org/pypy/pypy3.8-v7.3.11-win64.zip",
- },
- PythonVersion(3, 7, 13): {
- PlatformTriple(
- arch="x86_64", platform="linux", environment="gnu", flavor=""
- ): "https://downloads.python.org/pypy/pypy3.7-v7.3.9-linux64.tar.bz2",
- PlatformTriple(
- arch="aarch64", platform="linux", environment="gnu", flavor=""
- ): "https://downloads.python.org/pypy/pypy3.7-v7.3.9-aarch64.tar.bz2",
- PlatformTriple(
- arch="x86_64", platform="macos", environment=None, flavor=""
- ): "https://downloads.python.org/pypy/pypy3.7-v7.3.9-osx64.tar.bz2",
- PlatformTriple(
- arch="x86_64", platform="windows", environment=None, flavor=""
- ): "https://downloads.python.org/pypy/pypy3.7-v7.3.9-win64.zip",
- },
-}
-
-if __name__ == "__main__":
- main()
-
-
-class Tests(unittest.TestCase):
- def test_parse_triplets(self):
- expected = {
- "aarch64-apple-darwin-lto": PlatformTriple("aarch64", "macos", None, "lto"),
- "aarch64-unknown-linux-gnu-pgo+lto": PlatformTriple(
- "aarch64", "linux", "gnu", "pgo+lto"
- ),
- # "x86_64-unknown-linux-musl-debug": PlatformTriple(
- # "x86_64", "linux", "musl", "debug"
- # ),
- "aarch64-unknown-linux-gnu-debug-full": PlatformTriple(
- "aarch64", "linux", "gnu", "debug"
- ),
- "x86_64-unknown-linux-gnu-debug": PlatformTriple(
- "x86_64", "linux", "gnu", "debug"
- ),
- "linux64": PlatformTriple("x86_64", "linux", "gnu", ""),
- "ppc64le-unknown-linux-gnu-noopt-full": None,
- "x86_64_v3-unknown-linux-gnu-lto": None,
- "x86_64-pc-windows-msvc-shared-pgo": PlatformTriple(
- "x86_64", "windows", None, "shared-pgo"
- ),
- }
-
- for input, expected in expected.items():
- self.assertEqual(PlatformTriple.from_str(input), expected, input)
diff --git a/rye/src/bootstrap.rs b/rye/src/bootstrap.rs
index 317f123301..f3000a783b 100644
--- a/rye/src/bootstrap.rs
+++ b/rye/src/bootstrap.rs
@@ -19,7 +19,7 @@ use crate::platform::{
get_app_dir, get_canonical_py_path, get_toolchain_python_bin, list_known_toolchains,
symlinks_supported,
};
-use crate::pyproject::latest_available_python_version;
+use crate::pyproject::{latest_available_python_version, write_venv_marker};
use crate::sources::{get_download_url, PythonVersion, PythonVersionRequest};
use crate::utils::{
check_checksum, get_venv_python_bin, set_proxy_variables, symlink_file, unpack_archive,
@@ -37,7 +37,7 @@ pub const SELF_PYTHON_TARGET_VERSION: PythonVersionRequest = PythonVersionReques
suffix: None,
};
-const SELF_VERSION: u64 = 12;
+const SELF_VERSION: u64 = 14;
const SELF_REQUIREMENTS: &str = r#"
build==1.0.3
@@ -56,8 +56,8 @@ twine==4.0.2
unearth==0.14.0
urllib3==2.0.7
virtualenv==20.25.0
-ruff==0.1.14
-uv==0.1.3
+ruff==0.2.2
+uv==0.1.6
"#;
static FORCED_TO_UPDATE: AtomicBool = AtomicBool::new(false);
@@ -90,7 +90,7 @@ pub fn ensure_self_venv_with_toolchain(
return Ok(venv_dir);
} else {
if output != CommandOutput::Quiet {
- echo!("detected outdated rye internals. Refreshing");
+ echo!("Detected outdated rye internals. Refreshing");
}
fs::remove_dir_all(&venv_dir).context("could not remove self-venv for update")?;
if pip_tools_dir.is_dir() {
@@ -155,6 +155,8 @@ pub fn ensure_self_venv_with_toolchain(
bail!("failed to initialize virtualenv in {}", venv_dir.display());
}
+ write_venv_marker(&venv_dir, &version)?;
+
do_update(output, &venv_dir, app_dir)?;
fs::write(venv_dir.join("tool-version.txt"), SELF_VERSION.to_string())?;
@@ -386,6 +388,7 @@ fn ensure_specific_self_toolchain(
Ok(toolchain_version)
}
}
+
/// Fetches a version if missing.
pub fn fetch(
version: &PythonVersionRequest,
@@ -431,19 +434,22 @@ pub fn fetch(
if let Some(sha256) = sha256 {
if output != CommandOutput::Quiet {
- echo!("{}", style("Checking checksum").cyan());
+ echo!("{} {}", style("Checking").cyan(), "checksum");
}
check_checksum(&archive_buffer, sha256)
- .with_context(|| format!("hash check of {} failed", &url))?;
+ .with_context(|| format!("Checksum check of {} failed", &url))?;
} else if output != CommandOutput::Quiet {
echo!("Checksum check skipped (no hash available)");
}
+ if output != CommandOutput::Quiet {
+ echo!("{}", style("Unpacking").cyan());
+ }
unpack_archive(&archive_buffer, &target_dir, 1)
.with_context(|| format!("unpacking of downloaded tarball {} failed", &url))?;
if output != CommandOutput::Quiet {
- echo!("{} Downloaded {}", style("success:").green(), version);
+ echo!("{} {}", style("Downloaded").green(), version);
}
Ok(version)
diff --git a/rye/src/cli/add.rs b/rye/src/cli/add.rs
index 8c8787258c..1cf8edff70 100644
--- a/rye/src/cli/add.rs
+++ b/rye/src/cli/add.rs
@@ -16,7 +16,7 @@ use crate::config::Config;
use crate::consts::VENV_BIN;
use crate::pyproject::{BuildSystem, DependencyKind, ExpandedSources, PyProject};
use crate::sources::PythonVersion;
-use crate::sync::{sync, SyncOptions};
+use crate::sync::{autosync, sync, SyncOptions};
use crate::utils::{format_requirement, set_proxy_variables, CommandOutput};
const PACKAGE_FINDER_SCRIPT: &str = r#"
@@ -210,6 +210,12 @@ pub struct Args {
/// Overrides the pin operator
#[arg(long)]
pin: Option,
+ /// Runs `sync` even if auto-sync is disabled.
+ #[arg(long)]
+ sync: bool,
+ /// Does not run `sync` even if auto-sync is enabled.
+ #[arg(long, conflicts_with = "sync")]
+ no_sync: bool,
/// Enables verbose diagnostics.
#[arg(short, long)]
verbose: bool,
@@ -222,6 +228,7 @@ pub fn execute(cmd: Args) -> Result<(), Error> {
let output = CommandOutput::from_quiet_and_verbose(cmd.quiet, cmd.verbose);
let self_venv = ensure_self_venv(output).context("error bootstrapping venv")?;
let python_path = self_venv.join(VENV_BIN).join("python");
+ let cfg = Config::current();
let mut pyproject_toml = PyProject::discover()?;
let py_ver = pyproject_toml.venv_python_version()?;
@@ -251,7 +258,7 @@ pub fn execute(cmd: Args) -> Result<(), Error> {
}
if !cmd.excluded {
- if Config::current().use_uv() {
+ if cfg.use_uv() {
sync(SyncOptions::python_only().pyproject(None))
.context("failed to sync ahead of add")?;
resolve_requirements_with_uv(
@@ -294,6 +301,10 @@ pub fn execute(cmd: Args) -> Result<(), Error> {
}
}
+ if (cfg.autosync() && !cmd.no_sync) || cmd.sync {
+ autosync(&pyproject_toml, output)?;
+ }
+
Ok(())
}
@@ -453,6 +464,12 @@ fn resolve_requirements_with_uv(
if output == CommandOutput::Quiet {
cmd.arg("-q");
}
+ // this primarily exists for testing
+ if let Ok(dt) = env::var("__RYE_UV_EXCLUDE_NEWER") {
+ cmd.arg("--exclude-newer").arg(dt);
+ }
+ let sources = ExpandedSources::from_sources(&pyproject_toml.sources()?)?;
+ sources.add_as_pip_args(&mut cmd);
let mut child = cmd
.stdin(Stdio::piped())
.stdout(Stdio::piped())
diff --git a/rye/src/cli/fetch.rs b/rye/src/cli/fetch.rs
index 0f6f5d6816..0a6d337622 100644
--- a/rye/src/cli/fetch.rs
+++ b/rye/src/cli/fetch.rs
@@ -8,12 +8,12 @@ use crate::pyproject::PyProject;
use crate::sources::PythonVersionRequest;
use crate::utils::CommandOutput;
-/// Fetches a Python interpreter for the local machine.
+/// Fetches a Python interpreter for the local machine. This is an alias of `rye toolchain fetch`.
#[derive(Parser, Debug)]
pub struct Args {
/// The version of Python to fetch.
///
- /// If no version is provided, the requested version will be fetched.
+ /// If no version is provided, the requested version from local project or `.python-version` will be fetched.
version: Option,
/// Enables verbose diagnostics.
#[arg(short, long)]
diff --git a/rye/src/cli/fmt.rs b/rye/src/cli/fmt.rs
index 663a3b56ac..4d0e0a9413 100644
--- a/rye/src/cli/fmt.rs
+++ b/rye/src/cli/fmt.rs
@@ -1,85 +1,25 @@
-use std::ffi::OsString;
-use std::path::PathBuf;
-use std::process::Command;
-
use anyhow::Error;
use clap::Parser;
-use crate::bootstrap::ensure_self_venv;
-use crate::consts::VENV_BIN;
-use crate::pyproject::{locate_projects, PyProject};
-use crate::utils::{CommandOutput, QuietExit};
+use crate::utils::ruff;
/// Run the code formatter on the project.
///
/// This invokes ruff in format mode.
#[derive(Parser, Debug)]
pub struct Args {
- /// List of files or directories to format
- paths: Vec,
- /// Format all packages
- #[arg(short, long)]
- all: bool,
- /// Format a specific package
- #[arg(short, long)]
- package: Vec,
- /// Use this pyproject.toml file
- #[arg(long, value_name = "PYPROJECT_TOML")]
- pyproject: Option,
+ #[command(flatten)]
+ ruff: ruff::RuffArgs,
/// Run format in check mode
#[arg(long)]
check: bool,
- /// Enables verbose diagnostics.
- #[arg(short, long)]
- verbose: bool,
- /// Turns off all output.
- #[arg(short, long, conflicts_with = "verbose")]
- quiet: bool,
- /// Extra arguments to the formatter
- #[arg(last = true)]
- extra_args: Vec,
}
pub fn execute(cmd: Args) -> Result<(), Error> {
- let project = PyProject::load_or_discover(cmd.pyproject.as_deref())?;
- let output = CommandOutput::from_quiet_and_verbose(cmd.quiet, cmd.verbose);
- let venv = ensure_self_venv(output)?;
- let ruff = venv.join(VENV_BIN).join("ruff");
-
- let mut ruff_cmd = Command::new(ruff);
- ruff_cmd.arg("format");
- match output {
- CommandOutput::Normal => {}
- CommandOutput::Verbose => {
- ruff_cmd.arg("--verbose");
- }
- CommandOutput::Quiet => {
- ruff_cmd.arg("-q");
- }
- }
-
+ let mut args = Vec::new();
+ args.push("format");
if cmd.check {
- ruff_cmd.arg("--check");
- }
- ruff_cmd.args(cmd.extra_args);
-
- ruff_cmd.arg("--");
- if cmd.paths.is_empty() {
- let projects = locate_projects(project, cmd.all, &cmd.package[..])?;
- for project in projects {
- ruff_cmd.arg(project.root_path().as_os_str());
- }
- } else {
- for file in cmd.paths {
- ruff_cmd.arg(file.as_os_str());
- }
- }
-
- let status = ruff_cmd.status()?;
- if !status.success() {
- let code = status.code().unwrap_or(1);
- Err(QuietExit(code).into())
- } else {
- Ok(())
+ args.push("--check");
}
+ ruff::execute_ruff(cmd.ruff, &args)
}
diff --git a/rye/src/cli/install.rs b/rye/src/cli/install.rs
index 61e5a57d8c..82e03b4773 100644
--- a/rye/src/cli/install.rs
+++ b/rye/src/cli/install.rs
@@ -5,11 +5,12 @@ use clap::Parser;
use pep508_rs::Requirement;
use crate::cli::add::ReqExtras;
+use crate::config::Config;
use crate::installer::{install, resolve_local_requirement};
use crate::sources::PythonVersionRequest;
use crate::utils::CommandOutput;
-/// Installs a package as global tool.
+/// Installs a package as global tool. This is an alias of `rye tools install`.
#[derive(Parser, Debug)]
pub struct Args {
/// The name of the package to install.
@@ -53,15 +54,17 @@ pub fn execute(mut cmd: Args) -> Result<(), Error> {
let py_ver: PythonVersionRequest = match cmd.python {
Some(ref py) => py.parse()?,
- None => PythonVersionRequest {
- name: None,
- arch: None,
- os: None,
- major: 3,
- minor: None,
- patch: None,
- suffix: None,
- },
+ None => Config::current()
+ .default_toolchain()
+ .unwrap_or(PythonVersionRequest {
+ name: None,
+ arch: None,
+ os: None,
+ major: 3,
+ minor: None,
+ patch: None,
+ suffix: None,
+ }),
};
install(
diff --git a/rye/src/cli/lint.rs b/rye/src/cli/lint.rs
index 75b2ebafe8..6b07e85078 100644
--- a/rye/src/cli/lint.rs
+++ b/rye/src/cli/lint.rs
@@ -1,85 +1,25 @@
-use std::ffi::OsString;
-use std::path::PathBuf;
-use std::process::Command;
-
use anyhow::Error;
use clap::Parser;
-use crate::bootstrap::ensure_self_venv;
-use crate::consts::VENV_BIN;
-use crate::pyproject::{locate_projects, PyProject};
-use crate::utils::{CommandOutput, QuietExit};
+use crate::utils::ruff;
/// Run the linter on the project.
///
/// This invokes ruff in lint mode.
#[derive(Parser, Debug)]
pub struct Args {
- /// List of files or directories to lint
- paths: Vec,
- /// Lint all packages
- #[arg(short, long)]
- all: bool,
- /// Lint a specific package
- #[arg(short, long)]
- package: Vec,
- /// Use this pyproject.toml file
- #[arg(long, value_name = "PYPROJECT_TOML")]
- pyproject: Option,
+ #[command(flatten)]
+ ruff: ruff::RuffArgs,
/// Apply fixes.
#[arg(long)]
fix: bool,
- /// Enables verbose diagnostics.
- #[arg(short, long)]
- verbose: bool,
- /// Turns off all output.
- #[arg(short, long, conflicts_with = "verbose")]
- quiet: bool,
- /// Extra arguments to the linter
- #[arg(last = true)]
- extra_args: Vec,
}
pub fn execute(cmd: Args) -> Result<(), Error> {
- let project = PyProject::load_or_discover(cmd.pyproject.as_deref())?;
- let output = CommandOutput::from_quiet_and_verbose(cmd.quiet, cmd.verbose);
- let venv = ensure_self_venv(output)?;
- let ruff = venv.join(VENV_BIN).join("ruff");
-
- let mut ruff_cmd = Command::new(ruff);
- ruff_cmd.arg("check");
- match output {
- CommandOutput::Normal => {}
- CommandOutput::Verbose => {
- ruff_cmd.arg("--verbose");
- }
- CommandOutput::Quiet => {
- ruff_cmd.arg("-q");
- }
- }
-
+ let mut args = Vec::new();
+ args.push("check");
if cmd.fix {
- ruff_cmd.arg("--fix");
- }
- ruff_cmd.args(cmd.extra_args);
-
- ruff_cmd.arg("--");
- if cmd.paths.is_empty() {
- let projects = locate_projects(project, cmd.all, &cmd.package[..])?;
- for project in projects {
- ruff_cmd.arg(project.root_path().as_os_str());
- }
- } else {
- for file in cmd.paths {
- ruff_cmd.arg(file.as_os_str());
- }
- }
-
- let status = ruff_cmd.status()?;
- if !status.success() {
- let code = status.code().unwrap_or(1);
- Err(QuietExit(code).into())
- } else {
- Ok(())
+ args.push("--fix");
}
+ ruff::execute_ruff(cmd.ruff, &args)
}
diff --git a/rye/src/cli/lock.rs b/rye/src/cli/lock.rs
index aa63432de2..e12e0b6ccb 100644
--- a/rye/src/cli/lock.rs
+++ b/rye/src/cli/lock.rs
@@ -34,6 +34,9 @@ pub struct Args {
/// Set to true to lock with sources in the lockfile.
#[arg(long)]
with_sources: bool,
+ /// Reset prior lock options.
+ #[arg(long)]
+ reset: bool,
/// Use this pyproject.toml file
#[arg(long, value_name = "PYPROJECT_TOML")]
pyproject: Option,
@@ -51,6 +54,7 @@ pub fn execute(cmd: Args) -> Result<(), Error> {
features: cmd.features,
all_features: cmd.all_features,
with_sources: cmd.with_sources,
+ reset: cmd.reset,
},
pyproject: cmd.pyproject,
..SyncOptions::default()
diff --git a/rye/src/cli/mod.rs b/rye/src/cli/mod.rs
index 25a57e708e..c34a163de3 100644
--- a/rye/src/cli/mod.rs
+++ b/rye/src/cli/mod.rs
@@ -30,6 +30,7 @@ mod version;
use git_testament::git_testament;
use crate::bootstrap::SELF_PYTHON_TARGET_VERSION;
+use crate::config::Config;
use crate::platform::symlinks_supported;
git_testament!(TESTAMENT);
@@ -131,7 +132,7 @@ pub fn execute() -> Result<(), Error> {
Command::List(cmd) => list::execute(cmd),
Command::Shell(..) => {
bail!(
- "unknown command. The shell command was removed. Activate the virtualenv instead with '{}' instead.",
+ "unknown command. The shell command was removed. Activate the virtualenv with '{}' instead.",
if cfg!(windows) {
".venv\\Scripts\\activate"
} else {
@@ -152,5 +153,6 @@ fn print_version() -> Result<(), Error> {
);
echo!("self-python: {}", SELF_PYTHON_TARGET_VERSION);
echo!("symlink support: {}", symlinks_supported());
+ echo!("uv enabled: {}", Config::current().use_uv());
Ok(())
}
diff --git a/rye/src/cli/remove.rs b/rye/src/cli/remove.rs
index 17a5d1e9c2..73adaa2632 100644
--- a/rye/src/cli/remove.rs
+++ b/rye/src/cli/remove.rs
@@ -4,7 +4,9 @@ use anyhow::Error;
use clap::Parser;
use pep508_rs::Requirement;
+use crate::config::Config;
use crate::pyproject::{DependencyKind, PyProject};
+use crate::sync::autosync;
use crate::utils::{format_requirement, CommandOutput};
/// Removes a package from this project.
@@ -19,6 +21,12 @@ pub struct Args {
/// Remove this from an optional dependency group.
#[arg(long, conflicts_with = "dev")]
optional: Option,
+ /// Runs `sync` even if auto-sync is disabled.
+ #[arg(long)]
+ sync: bool,
+ /// Does not run `sync` even if auto-sync is enabled.
+ #[arg(long, conflicts_with = "sync")]
+ no_sync: bool,
/// Enables verbose diagnostics.
#[arg(short, long)]
verbose: bool,
@@ -56,5 +64,9 @@ pub fn execute(cmd: Args) -> Result<(), Error> {
}
}
+ if (Config::current().autosync() && !cmd.no_sync) || cmd.sync {
+ autosync(&pyproject_toml, output)?;
+ }
+
Ok(())
}
diff --git a/rye/src/cli/rye.rs b/rye/src/cli/rye.rs
index e5e3d00750..2431ed8199 100644
--- a/rye/src/cli/rye.rs
+++ b/rye/src/cli/rye.rs
@@ -344,6 +344,7 @@ fn uninstall(args: UninstallCommand) -> Result<(), Error> {
remove_dir_all_if_exists(&app_dir.join("self"))?;
remove_dir_all_if_exists(&app_dir.join("py"))?;
remove_dir_all_if_exists(&app_dir.join("pip-tools"))?;
+ remove_dir_all_if_exists(&app_dir.join("tools"))?;
// special deleting logic if we are placed in the app dir and the shim deletion
// did not succeed. This is likely the case on windows where we then use the
diff --git a/rye/src/cli/sync.rs b/rye/src/cli/sync.rs
index 17f5a06970..aaaea1cc82 100644
--- a/rye/src/cli/sync.rs
+++ b/rye/src/cli/sync.rs
@@ -46,6 +46,9 @@ pub struct Args {
/// Use this pyproject.toml file
#[arg(long, value_name = "PYPROJECT_TOML")]
pyproject: Option,
+ /// Do not reuse (reset) prior lock options.
+ #[arg(long)]
+ reset: bool,
}
pub fn execute(cmd: Args) -> Result<(), Error> {
@@ -67,6 +70,7 @@ pub fn execute(cmd: Args) -> Result<(), Error> {
features: cmd.features,
all_features: cmd.all_features,
with_sources: cmd.with_sources,
+ reset: cmd.reset,
},
pyproject: cmd.pyproject,
})?;
diff --git a/rye/src/cli/toolchain.rs b/rye/src/cli/toolchain.rs
index 6979bdfda7..6c009731ad 100644
--- a/rye/src/cli/toolchain.rs
+++ b/rye/src/cli/toolchain.rs
@@ -5,6 +5,8 @@ use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
+use crate::installer::list_installed_tools;
+use crate::piptools::get_pip_tools_venv_path;
use anyhow::{anyhow, bail, Context, Error};
use clap::Parser;
use clap::ValueEnum;
@@ -12,7 +14,8 @@ use console::style;
use serde::Deserialize;
use serde::Serialize;
-use crate::platform::{get_canonical_py_path, list_known_toolchains};
+use crate::platform::{get_app_dir, get_canonical_py_path, list_known_toolchains};
+use crate::pyproject::read_venv_marker;
use crate::sources::{iter_downloadable, PythonVersion};
use crate::utils::symlink_file;
@@ -60,6 +63,9 @@ pub struct RegisterCommand {
pub struct RemoveCommand {
/// Name and version of the toolchain.
version: String,
+ /// Force removal even if the toolchain is in use.
+ #[arg(short, long)]
+ force: bool,
}
/// List all registered toolchains
@@ -103,9 +109,40 @@ fn register(cmd: RegisterCommand) -> Result<(), Error> {
Ok(())
}
+/// Checks if a toolchain is still in use.
+fn check_in_use(ver: &PythonVersion) -> Result<(), Error> {
+ // Check if used by rye itself.
+ let app_dir = get_app_dir();
+ for venv in &[app_dir.join("self"), get_pip_tools_venv_path(ver)] {
+ let venv_marker = read_venv_marker(venv);
+ if let Some(ref venv_marker) = venv_marker {
+ if &venv_marker.python == ver {
+ bail!("toolchain {} is still in use by rye itself", ver);
+ }
+ }
+ }
+
+ // Check if used by any tool.
+ let installed_tools = list_installed_tools()?;
+ for (tool, info) in &installed_tools {
+ if let Some(ref venv_marker) = info.venv_marker {
+ if &venv_marker.python == ver {
+ bail!("toolchain {} is still in use by tool {}", ver, tool);
+ }
+ }
+ }
+
+ Ok(())
+}
+
pub fn remove(cmd: RemoveCommand) -> Result<(), Error> {
let ver: PythonVersion = cmd.version.parse()?;
let path = get_canonical_py_path(&ver)?;
+
+ if !cmd.force && path.exists() {
+ check_in_use(&ver)?;
+ }
+
if path.is_file() {
fs::remove_file(&path)?;
echo!("Removed toolchain link {}", &ver);
diff --git a/rye/src/cli/tools.rs b/rye/src/cli/tools.rs
index 1995eec674..4122ca45a7 100644
--- a/rye/src/cli/tools.rs
+++ b/rye/src/cli/tools.rs
@@ -14,12 +14,12 @@ pub struct Args {
/// List all registered tools
#[derive(Parser, Debug)]
pub struct ListCommand {
- /// Also how all the scripts installed by the tools.
- #[arg(short, long)]
+ /// Show all the scripts installed by the tools.
+ #[arg(short = 's', long)]
include_scripts: bool,
/// Show the version of tools.
- #[arg(short, long)]
- version_show: bool,
+ #[arg(short = 'v', long)]
+ include_version: bool,
}
#[derive(Parser, Debug)]
@@ -40,11 +40,19 @@ pub fn execute(cmd: Args) -> Result<(), Error> {
fn list_tools(cmd: ListCommand) -> Result<(), Error> {
let mut tools = list_installed_tools()?.into_iter().collect::>();
- tools.sort();
+ tools.sort_by_key(|(tool, _)| tool.clone());
for (tool, mut info) in tools {
- if cmd.version_show {
- echo!("{} {}", style(tool).cyan(), style(info.version).cyan());
+ if !info.valid {
+ echo!("{} ({})", style(tool).red(), style("seems broken").red());
+ continue;
+ }
+ if cmd.include_version {
+ if let Some(ref venv) = info.venv_marker {
+ echo!("{} {} ({})", style(tool).cyan(), info.version, venv.python);
+ } else {
+ echo!("{} {}", style(tool).cyan(), info.version);
+ }
} else {
echo!("{}", style(tool).cyan());
}
diff --git a/rye/src/cli/uninstall.rs b/rye/src/cli/uninstall.rs
index 1c76510e59..7039b30162 100644
--- a/rye/src/cli/uninstall.rs
+++ b/rye/src/cli/uninstall.rs
@@ -4,7 +4,7 @@ use clap::Parser;
use crate::installer::uninstall;
use crate::utils::CommandOutput;
-/// Uninstalls a global tool.
+/// Uninstalls a global tool. This is an alias of `rye tools uninstall`.
#[derive(Parser, Debug)]
pub struct Args {
/// The package to uninstall
diff --git a/rye/src/config.rs b/rye/src/config.rs
index dcb8a49768..f0c31d5842 100644
--- a/rye/src/config.rs
+++ b/rye/src/config.rs
@@ -247,6 +247,15 @@ impl Config {
Ok(rv)
}
+ /// Enable autosync.
+ pub fn autosync(&self) -> bool {
+ self.doc
+ .get("behavior")
+ .and_then(|x| x.get("autosync"))
+ .and_then(|x| x.as_bool())
+ .unwrap_or_else(|| self.use_uv())
+ }
+
/// Indicates if the experimental uv support should be used.
pub fn use_uv(&self) -> bool {
self.doc
diff --git a/rye/src/downloads.inc b/rye/src/downloads.inc
index 0f88451aea..b5d791f3d4 100644
--- a/rye/src/downloads.inc
+++ b/rye/src/downloads.inc
@@ -1,25 +1,94 @@
-// generated code, do not edit
+// Generated by rye-devtools. DO NOT EDIT.
+// To regenerate, run `rye run find-downloads > rye/src/downloads.inc` from the root of the repository.
use std::borrow::Cow;
pub const PYTHON_VERSIONS: &[(PythonVersion, &str, Option<&str>)] = &[
- (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("aarch64"), os: Cow::Borrowed("linux"), major: 3, minor: 10, patch: 12, suffix: None }, "https://downloads.python.org/pypy/pypy3.10-v7.3.12-aarch64.tar.bz2", None),
- (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("aarch64"), os: Cow::Borrowed("macos"), major: 3, minor: 10, patch: 12, suffix: None }, "https://downloads.python.org/pypy/pypy3.10-v7.3.12-macos_arm64.tar.bz2", None),
- (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86_64"), os: Cow::Borrowed("linux"), major: 3, minor: 10, patch: 12, suffix: None }, "https://downloads.python.org/pypy/pypy3.10-v7.3.12-linux64.tar.bz2", None),
- (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86_64"), os: Cow::Borrowed("macos"), major: 3, minor: 10, patch: 12, suffix: None }, "https://downloads.python.org/pypy/pypy3.10-v7.3.12-macos_x86_64.tar.bz2", None),
- (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86_64"), os: Cow::Borrowed("windows"), major: 3, minor: 10, patch: 12, suffix: None }, "https://downloads.python.org/pypy/pypy3.10-v7.3.12-win64.zip", None),
- (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("aarch64"), os: Cow::Borrowed("linux"), major: 3, minor: 9, patch: 16, suffix: None }, "https://downloads.python.org/pypy/pypy3.9-v7.3.11-aarch64.tar.bz2", None),
- (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("aarch64"), os: Cow::Borrowed("macos"), major: 3, minor: 9, patch: 16, suffix: None }, "https://downloads.python.org/pypy/pypy3.9-v7.3.11-macos_arm64.tar.bz2", None),
- (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86_64"), os: Cow::Borrowed("linux"), major: 3, minor: 9, patch: 16, suffix: None }, "https://downloads.python.org/pypy/pypy3.9-v7.3.11-linux64.tar.bz2", None),
- (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86_64"), os: Cow::Borrowed("macos"), major: 3, minor: 9, patch: 16, suffix: None }, "https://downloads.python.org/pypy/pypy3.9-v7.3.11-macos_x86_64.tar.bz2", None),
- (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86_64"), os: Cow::Borrowed("windows"), major: 3, minor: 9, patch: 16, suffix: None }, "https://downloads.python.org/pypy/pypy3.9-v7.3.11-win64.zip", None),
- (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("aarch64"), os: Cow::Borrowed("linux"), major: 3, minor: 8, patch: 16, suffix: None }, "https://downloads.python.org/pypy/pypy3.8-v7.3.11-aarch64.tar.bz2", None),
- (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("aarch64"), os: Cow::Borrowed("macos"), major: 3, minor: 8, patch: 16, suffix: None }, "https://downloads.python.org/pypy/pypy3.8-v7.3.11-macos_arm64.tar.bz2", None),
- (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86_64"), os: Cow::Borrowed("linux"), major: 3, minor: 8, patch: 16, suffix: None }, "https://downloads.python.org/pypy/pypy3.8-v7.3.11-linux64.tar.bz2", None),
- (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86_64"), os: Cow::Borrowed("macos"), major: 3, minor: 8, patch: 16, suffix: None }, "https://downloads.python.org/pypy/pypy3.8-v7.3.11-macos_x86_64.tar.bz2", None),
- (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86_64"), os: Cow::Borrowed("windows"), major: 3, minor: 8, patch: 16, suffix: None }, "https://downloads.python.org/pypy/pypy3.8-v7.3.11-win64.zip", None),
- (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("aarch64"), os: Cow::Borrowed("linux"), major: 3, minor: 7, patch: 13, suffix: None }, "https://downloads.python.org/pypy/pypy3.7-v7.3.9-aarch64.tar.bz2", None),
- (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86_64"), os: Cow::Borrowed("linux"), major: 3, minor: 7, patch: 13, suffix: None }, "https://downloads.python.org/pypy/pypy3.7-v7.3.9-linux64.tar.bz2", None),
- (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86_64"), os: Cow::Borrowed("macos"), major: 3, minor: 7, patch: 13, suffix: None }, "https://downloads.python.org/pypy/pypy3.7-v7.3.9-osx64.tar.bz2", None),
- (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86_64"), os: Cow::Borrowed("windows"), major: 3, minor: 7, patch: 13, suffix: None }, "https://downloads.python.org/pypy/pypy3.7-v7.3.9-win64.zip", None),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("aarch64"), os: Cow::Borrowed("linux"), major: 3, minor: 10, patch: 13, suffix: None }, "https://downloads.python.org/pypy/pypy3.10-v7.3.15-aarch64.tar.bz2", Some("52146fccaf64e87e71d178dda8de63c01577ec3923073dc69e1519622bcacb74")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("aarch64"), os: Cow::Borrowed("macos"), major: 3, minor: 10, patch: 13, suffix: None }, "https://downloads.python.org/pypy/pypy3.10-v7.3.15-macos_arm64.tar.bz2", Some("d927c5105ea7880f7596fe459183e35cc17c853ef5105678b2ad62a8d000a548")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86"), os: Cow::Borrowed("linux"), major: 3, minor: 10, patch: 13, suffix: None }, "https://downloads.python.org/pypy/pypy3.10-v7.3.15-linux32.tar.bz2", Some("75dd58c9abd8b9d78220373148355bc3119febcf27a2c781d64ad85e7232c4aa")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86_64"), os: Cow::Borrowed("linux"), major: 3, minor: 10, patch: 13, suffix: None }, "https://downloads.python.org/pypy/pypy3.10-v7.3.15-linux64.tar.bz2", Some("33c584e9a70a71afd0cb7dd8ba9996720b911b3b8ed0156aea298d4487ad22c3")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86_64"), os: Cow::Borrowed("macos"), major: 3, minor: 10, patch: 13, suffix: None }, "https://downloads.python.org/pypy/pypy3.10-v7.3.15-macos_x86_64.tar.bz2", Some("559b61ba7e7c5a5c23cef5370f1fab47ccdb939ac5d2b42b4bef091abe3f6964")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86_64"), os: Cow::Borrowed("windows"), major: 3, minor: 10, patch: 13, suffix: None }, "https://downloads.python.org/pypy/pypy3.10-v7.3.15-win64.zip", Some("b378b3ab1c3719aee0c3e5519e7bff93ff67b2d8aa987fe4f088b54382db676c")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("aarch64"), os: Cow::Borrowed("linux"), major: 3, minor: 10, patch: 12, suffix: None }, "https://downloads.python.org/pypy/pypy3.10-v7.3.12-aarch64.tar.bz2", Some("26208b5a134d9860a08f74cce60960005758e82dc5f0e3566a48ed863a1f16a1")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("aarch64"), os: Cow::Borrowed("macos"), major: 3, minor: 10, patch: 12, suffix: None }, "https://downloads.python.org/pypy/pypy3.10-v7.3.12-macos_arm64.tar.bz2", Some("45671b1e9437f95ccd790af10dbeb57733cca1ed9661463b727d3c4f5caa7ba0")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86"), os: Cow::Borrowed("linux"), major: 3, minor: 10, patch: 12, suffix: None }, "https://downloads.python.org/pypy/pypy3.10-v7.3.12-linux32.tar.bz2", Some("811667825ae58ada4b7c3d8bc1b5055b9f9d6a377e51aedfbe0727966603f60e")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86_64"), os: Cow::Borrowed("linux"), major: 3, minor: 10, patch: 12, suffix: None }, "https://downloads.python.org/pypy/pypy3.10-v7.3.12-linux64.tar.bz2", Some("6c577993160b6f5ee8cab73cd1a807affcefafe2f7441c87bd926c10505e8731")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86_64"), os: Cow::Borrowed("macos"), major: 3, minor: 10, patch: 12, suffix: None }, "https://downloads.python.org/pypy/pypy3.10-v7.3.12-macos_x86_64.tar.bz2", Some("dbc15d8570560d5f79366883c24bc42231a92855ac19a0f28cb0adeb11242666")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86_64"), os: Cow::Borrowed("windows"), major: 3, minor: 10, patch: 12, suffix: None }, "https://downloads.python.org/pypy/pypy3.10-v7.3.12-win64.zip", Some("8c3b1d34fb99100e230e94560410a38d450dc844effbee9ea183518e4aff595c")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("aarch64"), os: Cow::Borrowed("linux"), major: 3, minor: 9, patch: 18, suffix: None }, "https://downloads.python.org/pypy/pypy3.9-v7.3.15-aarch64.tar.bz2", Some("03e35fcba290454bb0ccf7ee57fb42d1e63108d10d593776a382c0a2fe355de0")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("aarch64"), os: Cow::Borrowed("macos"), major: 3, minor: 9, patch: 18, suffix: None }, "https://downloads.python.org/pypy/pypy3.9-v7.3.15-macos_arm64.tar.bz2", Some("300541c32125767a91b182b03d9cc4257f04971af32d747ecd4d62549d72acfd")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86"), os: Cow::Borrowed("linux"), major: 3, minor: 9, patch: 18, suffix: None }, "https://downloads.python.org/pypy/pypy3.9-v7.3.15-linux32.tar.bz2", Some("c6209380977066c9e8b96e8258821c70f996004ce1bc8659ae83d4fd5a89ff5c")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86_64"), os: Cow::Borrowed("linux"), major: 3, minor: 9, patch: 18, suffix: None }, "https://downloads.python.org/pypy/pypy3.9-v7.3.15-linux64.tar.bz2", Some("f062be307200bde434817e1620cebc13f563d6ab25309442c5f4d0f0d68f0912")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86_64"), os: Cow::Borrowed("macos"), major: 3, minor: 9, patch: 18, suffix: None }, "https://downloads.python.org/pypy/pypy3.9-v7.3.15-macos_x86_64.tar.bz2", Some("18ad7c9cb91c5e8ef9d40442b2fd1f6392ae113794c5b6b7d3a45e04f19edec6")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86_64"), os: Cow::Borrowed("windows"), major: 3, minor: 9, patch: 18, suffix: None }, "https://downloads.python.org/pypy/pypy3.9-v7.3.15-win64.zip", Some("a156dad8b58570597eaaabe05663f00f80c60bc11df4a9c46d0953b6c5eb9209")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("aarch64"), os: Cow::Borrowed("linux"), major: 3, minor: 9, patch: 17, suffix: None }, "https://downloads.python.org/pypy/pypy3.9-v7.3.12-aarch64.tar.bz2", Some("e9327fb9edaf2ad91935d5b8563ec5ff24193bddb175c1acaaf772c025af1824")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("aarch64"), os: Cow::Borrowed("macos"), major: 3, minor: 9, patch: 17, suffix: None }, "https://downloads.python.org/pypy/pypy3.9-v7.3.12-macos_arm64.tar.bz2", Some("0e8a1a3468b9790c734ac698f5b00cc03fc16899ccc6ce876465fac0b83980e3")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86"), os: Cow::Borrowed("linux"), major: 3, minor: 9, patch: 17, suffix: None }, "https://downloads.python.org/pypy/pypy3.9-v7.3.12-linux32.tar.bz2", Some("aa04370d38f451683ccc817d76c2b3e0f471dbb879e0bd618d9affbdc9cd37a4")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86_64"), os: Cow::Borrowed("linux"), major: 3, minor: 9, patch: 17, suffix: None }, "https://downloads.python.org/pypy/pypy3.9-v7.3.12-linux64.tar.bz2", Some("84c89b966fab2b58f451a482ee30ca7fec3350435bd0b9614615c61dc6da2390")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86_64"), os: Cow::Borrowed("macos"), major: 3, minor: 9, patch: 17, suffix: None }, "https://downloads.python.org/pypy/pypy3.9-v7.3.12-macos_x86_64.tar.bz2", Some("64f008ffa070c407e5ef46c8256b2e014de7196ea5d858385861254e7959f4eb")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86_64"), os: Cow::Borrowed("windows"), major: 3, minor: 9, patch: 17, suffix: None }, "https://downloads.python.org/pypy/pypy3.9-v7.3.12-win64.zip", Some("0996054207b401aeacace1aa11bad82cfcb463838a1603c5f263626c47bbe0e6")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("aarch64"), os: Cow::Borrowed("linux"), major: 3, minor: 9, patch: 16, suffix: None }, "https://downloads.python.org/pypy/pypy3.9-v7.3.11-aarch64.tar.bz2", Some("09175dc652ed895d98e9ad63d216812bf3ee7e398d900a9bf9eb2906ba8302b9")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("aarch64"), os: Cow::Borrowed("macos"), major: 3, minor: 9, patch: 16, suffix: None }, "https://downloads.python.org/pypy/pypy3.9-v7.3.11-macos_arm64.tar.bz2", Some("91ad7500f1a39531dbefa0b345a3dcff927ff9971654e8d2e9ef7c5ae311f57e")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86"), os: Cow::Borrowed("linux"), major: 3, minor: 9, patch: 16, suffix: None }, "https://downloads.python.org/pypy/pypy3.9-v7.3.11-linux32.tar.bz2", Some("0099d72c2897b229057bff7e2c343624aeabdc60d6fb43ca882bff082f1ffa48")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86_64"), os: Cow::Borrowed("linux"), major: 3, minor: 9, patch: 16, suffix: None }, "https://downloads.python.org/pypy/pypy3.9-v7.3.11-linux64.tar.bz2", Some("d506172ca11071274175d74e9c581c3166432d0179b036470e3b9e8d20eae581")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86_64"), os: Cow::Borrowed("macos"), major: 3, minor: 9, patch: 16, suffix: None }, "https://downloads.python.org/pypy/pypy3.9-v7.3.11-macos_x86_64.tar.bz2", Some("d33f40b207099872585afd71873575ca6ea638a27d823bc621238c5ae82542ed")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86_64"), os: Cow::Borrowed("windows"), major: 3, minor: 9, patch: 16, suffix: None }, "https://downloads.python.org/pypy/pypy3.9-v7.3.11-win64.zip", Some("57faad132d42d3e7a6406fcffafffe0b4f390cf0e2966abb8090d073c6edf405")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("aarch64"), os: Cow::Borrowed("linux"), major: 3, minor: 9, patch: 15, suffix: None }, "https://downloads.python.org/pypy/pypy3.9-v7.3.10-aarch64.tar.bz2", Some("657a04fd9a5a992a2f116a9e7e9132ea0c578721f59139c9fb2083775f71e514")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("aarch64"), os: Cow::Borrowed("macos"), major: 3, minor: 9, patch: 15, suffix: None }, "https://downloads.python.org/pypy/pypy3.9-v7.3.10-macos_arm64.tar.bz2", Some("e2a6bec7408e6497c7de8165aa4a1b15e2416aec4a72f2578f793fb06859ccba")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86"), os: Cow::Borrowed("linux"), major: 3, minor: 9, patch: 15, suffix: None }, "https://downloads.python.org/pypy/pypy3.9-v7.3.10-linux32.tar.bz2", Some("b6db59613b9a1c0c1ab87bc103f52ee95193423882dc8a848b68850b8ba59cc5")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86_64"), os: Cow::Borrowed("linux"), major: 3, minor: 9, patch: 15, suffix: None }, "https://downloads.python.org/pypy/pypy3.9-v7.3.10-linux64.tar.bz2", Some("95cf99406179460d63ddbfe1ec870f889d05f7767ce81cef14b88a3a9e127266")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86_64"), os: Cow::Borrowed("macos"), major: 3, minor: 9, patch: 15, suffix: None }, "https://downloads.python.org/pypy/pypy3.9-v7.3.10-macos_x86_64.tar.bz2", Some("f90c8619b41e68ec9ffd7d5e913fe02e60843da43d3735b1c1bc75bcfe638d97")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86_64"), os: Cow::Borrowed("windows"), major: 3, minor: 9, patch: 15, suffix: None }, "https://downloads.python.org/pypy/pypy3.9-v7.3.10-win64.zip", Some("07e18b7b24c74af9730dfaab16e24b22ef94ea9a4b64cbb2c0d80610a381192a")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("aarch64"), os: Cow::Borrowed("linux"), major: 3, minor: 9, patch: 12, suffix: None }, "https://downloads.python.org/pypy/pypy3.9-v7.3.9-aarch64.tar.bz2", Some("2e1ae193d98bc51439642a7618d521ea019f45b8fb226940f7e334c548d2b4b9")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86"), os: Cow::Borrowed("linux"), major: 3, minor: 9, patch: 12, suffix: None }, "https://downloads.python.org/pypy/pypy3.9-v7.3.9-linux32.tar.bz2", Some("0de4b9501cf28524cdedcff5052deee9ea4630176a512bdc408edfa30914bae7")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86_64"), os: Cow::Borrowed("linux"), major: 3, minor: 9, patch: 12, suffix: None }, "https://downloads.python.org/pypy/pypy3.9-v7.3.9-linux64.tar.bz2", Some("46818cb3d74b96b34787548343d266e2562b531ddbaf330383ba930ff1930ed5")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86_64"), os: Cow::Borrowed("macos"), major: 3, minor: 9, patch: 12, suffix: None }, "https://downloads.python.org/pypy/pypy3.9-v7.3.9-osx64.tar.bz2", Some("59c8852168b2b1ba1f0211ff043c678760380d2f9faf2f95042a8878554dbc25")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86_64"), os: Cow::Borrowed("windows"), major: 3, minor: 9, patch: 12, suffix: None }, "https://downloads.python.org/pypy/pypy3.9-v7.3.9-win64.zip", Some("be48ab42f95c402543a7042c999c9433b17e55477c847612c8733a583ca6dff5")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("aarch64"), os: Cow::Borrowed("linux"), major: 3, minor: 9, patch: 10, suffix: None }, "https://downloads.python.org/pypy/pypy3.9-v7.3.8-aarch64-portable.tar.bz2", Some("b7282bc4484bceae5bc4cc04e05ee4faf51cb624c8fc7a69d92e5fdf0d0c96aa")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86"), os: Cow::Borrowed("linux"), major: 3, minor: 9, patch: 10, suffix: None }, "https://downloads.python.org/pypy/pypy3.9-v7.3.8-linux32.tar.bz2", Some("a0d18e4e73cc655eb02354759178b8fb161d3e53b64297d05e2fff91f7cf862d")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86_64"), os: Cow::Borrowed("linux"), major: 3, minor: 9, patch: 10, suffix: None }, "https://downloads.python.org/pypy/pypy3.9-v7.3.8-linux64.tar.bz2", Some("129a055032bba700cd1d0acacab3659cf6b7180e25b1b2f730e792f06d5b3010")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86_64"), os: Cow::Borrowed("macos"), major: 3, minor: 9, patch: 10, suffix: None }, "https://downloads.python.org/pypy/pypy3.9-v7.3.8-osx64.tar.bz2", Some("95bd88ac8d6372cd5b7b5393de7b7d5c615a0c6e42fdb1eb67f2d2d510965aee")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86_64"), os: Cow::Borrowed("windows"), major: 3, minor: 9, patch: 10, suffix: None }, "https://downloads.python.org/pypy/pypy3.9-v7.3.8-win64.zip", Some("c1b2e4cde2dcd1208d41ef7b7df8e5c90564a521e7a5db431673da335a1ba697")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("aarch64"), os: Cow::Borrowed("linux"), major: 3, minor: 8, patch: 16, suffix: None }, "https://downloads.python.org/pypy/pypy3.8-v7.3.11-aarch64.tar.bz2", Some("9a2fa0b8d92b7830aa31774a9a76129b0ff81afbd22cd5c41fbdd9119e859f55")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("aarch64"), os: Cow::Borrowed("macos"), major: 3, minor: 8, patch: 16, suffix: None }, "https://downloads.python.org/pypy/pypy3.8-v7.3.11-macos_arm64.tar.bz2", Some("78cdc79ff964c4bfd13eb45a7d43a011cbe8d8b513323d204891f703fdc4fa1a")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86"), os: Cow::Borrowed("linux"), major: 3, minor: 8, patch: 16, suffix: None }, "https://downloads.python.org/pypy/pypy3.8-v7.3.11-linux32.tar.bz2", Some("a79b31fce8f5bc1f9940b6777134189a1d3d18bda4b1c830384cda90077c9176")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86_64"), os: Cow::Borrowed("linux"), major: 3, minor: 8, patch: 16, suffix: None }, "https://downloads.python.org/pypy/pypy3.8-v7.3.11-linux64.tar.bz2", Some("470330e58ac105c094041aa07bb05676b06292bc61409e26f5c5593ebb2292d9")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86_64"), os: Cow::Borrowed("macos"), major: 3, minor: 8, patch: 16, suffix: None }, "https://downloads.python.org/pypy/pypy3.8-v7.3.11-macos_x86_64.tar.bz2", Some("194ca0b4d91ae409a9cb1a59eb7572d7affa8a451ea3daf26539aa515443433a")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86_64"), os: Cow::Borrowed("windows"), major: 3, minor: 8, patch: 16, suffix: None }, "https://downloads.python.org/pypy/pypy3.8-v7.3.11-win64.zip", Some("0f46fb6df32941ea016f77cfd7e9b426d5ac25a2af2453414df66103941c8435")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("aarch64"), os: Cow::Borrowed("linux"), major: 3, minor: 8, patch: 15, suffix: None }, "https://downloads.python.org/pypy/pypy3.8-v7.3.10-aarch64.tar.bz2", Some("e4caa1a545f22cfee87d5b9aa6f8852347f223643ad7d2562e0b2a2f4663ad98")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("aarch64"), os: Cow::Borrowed("macos"), major: 3, minor: 8, patch: 15, suffix: None }, "https://downloads.python.org/pypy/pypy3.8-v7.3.10-macos_arm64.tar.bz2", Some("6cb1429371e4854b718148a509d80143f801e3abfc72fef58d88aeeee1e98f9e")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86"), os: Cow::Borrowed("linux"), major: 3, minor: 8, patch: 15, suffix: None }, "https://downloads.python.org/pypy/pypy3.8-v7.3.10-linux32.tar.bz2", Some("b70ed7fdc73a74ebdc04f07439f7bad1a849aaca95e26b4a74049d0e483f071c")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86_64"), os: Cow::Borrowed("linux"), major: 3, minor: 8, patch: 15, suffix: None }, "https://downloads.python.org/pypy/pypy3.8-v7.3.10-linux64.tar.bz2", Some("ceef6496fd4ab1c99e3ec22ce657b8f10f8bb77a32427fadfb5e1dd943806011")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86_64"), os: Cow::Borrowed("macos"), major: 3, minor: 8, patch: 15, suffix: None }, "https://downloads.python.org/pypy/pypy3.8-v7.3.10-macos_x86_64.tar.bz2", Some("399eb1ce4c65f62f6a096b7c273536601b7695e3c0dc0457393a659b95b7615b")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86_64"), os: Cow::Borrowed("windows"), major: 3, minor: 8, patch: 15, suffix: None }, "https://downloads.python.org/pypy/pypy3.8-v7.3.10-win64.zip", Some("362dd624d95bd64743190ea2539b97452ecb3d53ea92ceb2fbe9f48dc60e6b8f")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("aarch64"), os: Cow::Borrowed("linux"), major: 3, minor: 8, patch: 13, suffix: None }, "https://downloads.python.org/pypy/pypy3.8-v7.3.9-aarch64.tar.bz2", Some("5e124455e207425e80731dff317f0432fa0aba1f025845ffca813770e2447e32")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86"), os: Cow::Borrowed("linux"), major: 3, minor: 8, patch: 13, suffix: None }, "https://downloads.python.org/pypy/pypy3.8-v7.3.9-linux32.tar.bz2", Some("4b261516c6c59078ab0c8bd7207327a1b97057b4ec1714ed5e79a026f9efd492")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86_64"), os: Cow::Borrowed("linux"), major: 3, minor: 8, patch: 13, suffix: None }, "https://downloads.python.org/pypy/pypy3.8-v7.3.9-linux64.tar.bz2", Some("08be25ec82fc5d23b78563eda144923517daba481a90af0ace7a047c9c9a3c34")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86_64"), os: Cow::Borrowed("macos"), major: 3, minor: 8, patch: 13, suffix: None }, "https://downloads.python.org/pypy/pypy3.8-v7.3.9-osx64.tar.bz2", Some("91a5c2c1facd5a4931a8682b7d792f7cf4f2ba25cd2e7e44e982139a6d5e4840")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86_64"), os: Cow::Borrowed("windows"), major: 3, minor: 8, patch: 13, suffix: None }, "https://downloads.python.org/pypy/pypy3.8-v7.3.9-win64.zip", Some("05022baaa55db2b60880f2422312d9e4025e1267303ac57f33e8253559d0be88")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("aarch64"), os: Cow::Borrowed("linux"), major: 3, minor: 8, patch: 12, suffix: None }, "https://downloads.python.org/pypy/pypy3.8-v7.3.8-aarch64-portable.tar.bz2", Some("0210536e9f1841ba283c13b04783394050837bb3e6f4091c9f1bd9c7f2b94b55")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86"), os: Cow::Borrowed("linux"), major: 3, minor: 8, patch: 12, suffix: None }, "https://downloads.python.org/pypy/pypy3.8-v7.3.8-linux32.tar.bz2", Some("bea4b275decd492af6462157d293dd6fcf08a949859f8aec0959537b40afd032")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86_64"), os: Cow::Borrowed("linux"), major: 3, minor: 8, patch: 12, suffix: None }, "https://downloads.python.org/pypy/pypy3.8-v7.3.8-linux64.tar.bz2", Some("089f8e3e357d6130815964ddd3507c13bd53e4976ccf0a89b5c36a9a6775a188")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86_64"), os: Cow::Borrowed("macos"), major: 3, minor: 8, patch: 12, suffix: None }, "https://downloads.python.org/pypy/pypy3.8-v7.3.8-osx64.tar.bz2", Some("de1b283ff112d76395c0162a1cf11528e192bdc230ee3f1b237f7694c7518dee")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86_64"), os: Cow::Borrowed("windows"), major: 3, minor: 8, patch: 12, suffix: None }, "https://downloads.python.org/pypy/pypy3.8-v7.3.8-win64.zip", Some("0894c468e7de758c509a602a28ef0ba4fbf197ccdf946c7853a7283d9bb2a345")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("aarch64"), os: Cow::Borrowed("linux"), major: 3, minor: 7, patch: 13, suffix: None }, "https://downloads.python.org/pypy/pypy3.7-v7.3.9-aarch64.tar.bz2", Some("dfc62f2c453fb851d10a1879c6e75c31ffebbf2a44d181bb06fcac4750d023fc")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86"), os: Cow::Borrowed("linux"), major: 3, minor: 7, patch: 13, suffix: None }, "https://downloads.python.org/pypy/pypy3.7-v7.3.9-linux32.tar.bz2", Some("3398cece0167b81baa219c9cd54a549443d8c0a6b553ec8ec13236281e0d86cd")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86_64"), os: Cow::Borrowed("linux"), major: 3, minor: 7, patch: 13, suffix: None }, "https://downloads.python.org/pypy/pypy3.7-v7.3.9-linux64.tar.bz2", Some("c58195124d807ecc527499ee19bc511ed753f4f2e418203ca51bc7e3b124d5d1")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86_64"), os: Cow::Borrowed("macos"), major: 3, minor: 7, patch: 13, suffix: None }, "https://downloads.python.org/pypy/pypy3.7-v7.3.9-osx64.tar.bz2", Some("12d92f578a200d50959e55074b20f29f93c538943e9a6e6522df1a1cc9cef542")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86_64"), os: Cow::Borrowed("windows"), major: 3, minor: 7, patch: 13, suffix: None }, "https://downloads.python.org/pypy/pypy3.7-v7.3.9-win64.zip", Some("8acb184b48fb3c854de0662e4d23a66b90e73b1ab73a86695022c12c745d8b00")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("aarch64"), os: Cow::Borrowed("linux"), major: 3, minor: 7, patch: 12, suffix: None }, "https://downloads.python.org/pypy/pypy3.7-v7.3.8-aarch64-portable.tar.bz2", Some("639c76f128a856747aee23a34276fa101a7a157ea81e76394fbaf80b97dcf2f2")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86"), os: Cow::Borrowed("linux"), major: 3, minor: 7, patch: 12, suffix: None }, "https://downloads.python.org/pypy/pypy3.7-v7.3.8-linux32.tar.bz2", Some("38429ec6ea1aca391821ee4fbda7358ae86de4600146643f2af2fe2c085af839")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86_64"), os: Cow::Borrowed("linux"), major: 3, minor: 7, patch: 12, suffix: None }, "https://downloads.python.org/pypy/pypy3.7-v7.3.8-linux64.tar.bz2", Some("409085db79a6d90bfcf4f576dca1538498e65937acfbe03bd4909bdc262ff378")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86_64"), os: Cow::Borrowed("macos"), major: 3, minor: 7, patch: 12, suffix: None }, "https://downloads.python.org/pypy/pypy3.7-v7.3.8-osx64.tar.bz2", Some("76b8eef5b059a7e478f525615482d2a6e9feb83375e3f63c16381d80521a693f")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86_64"), os: Cow::Borrowed("windows"), major: 3, minor: 7, patch: 12, suffix: None }, "https://downloads.python.org/pypy/pypy3.7-v7.3.8-win64.zip", Some("96df67492bc8d62b2e71dddf5f6c58965a26cac9799c5f4081401af0494b3bcc")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("aarch64"), os: Cow::Borrowed("linux"), major: 3, minor: 7, patch: 10, suffix: None }, "https://downloads.python.org/pypy/pypy3.7-v7.3.5-aarch64.tar.bz2", Some("85d83093b3ef5b863f641bc4073d057cc98bb821e16aa9361a5ff4898e70e8ee")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86"), os: Cow::Borrowed("linux"), major: 3, minor: 7, patch: 10, suffix: None }, "https://downloads.python.org/pypy/pypy3.7-v7.3.5-linux32.tar.bz2", Some("3dd8b565203d372829e53945c599296fa961895130342ea13791b17c84ed06c4")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86_64"), os: Cow::Borrowed("linux"), major: 3, minor: 7, patch: 10, suffix: None }, "https://downloads.python.org/pypy/pypy3.7-v7.3.5-linux64.tar.bz2", Some("9000db3e87b54638e55177e68cbeb30a30fe5d17b6be48a9eb43d65b3ebcfc26")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86_64"), os: Cow::Borrowed("macos"), major: 3, minor: 7, patch: 10, suffix: None }, "https://downloads.python.org/pypy/pypy3.7-v7.3.5-osx64.tar.bz2", Some("b3a7d3099ad83de7c267bb79ae609d5ce73b01800578ffd91ba7e221b13f80db")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86_64"), os: Cow::Borrowed("windows"), major: 3, minor: 7, patch: 10, suffix: None }, "https://downloads.python.org/pypy/pypy3.7-v7.3.5-win64.zip", Some("072bd22427178dc4e65d961f50281bd2f56e11c4e4d9f16311c703f69f46ae24")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("aarch64"), os: Cow::Borrowed("linux"), major: 3, minor: 7, patch: 9, suffix: None }, "https://downloads.python.org/pypy/pypy3.7-v7.3.3-aarch64.tar.bz2", Some("ee4aa041558b58de6063dd6df93b3def221c4ca4c900d6a9db5b1b52135703a8")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86"), os: Cow::Borrowed("linux"), major: 3, minor: 7, patch: 9, suffix: None }, "https://downloads.python.org/pypy/pypy3.7-v7.3.3-linux32.tar.bz2", Some("7d81b8e9fcd07c067cfe2f519ab770ec62928ee8787f952cadf2d2786246efc8")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86_64"), os: Cow::Borrowed("linux"), major: 3, minor: 7, patch: 9, suffix: None }, "https://downloads.python.org/pypy/pypy3.7-v7.3.3-linux64.tar.bz2", Some("37e2804c4661c86c857d709d28c7de716b000d31e89766599fdf5a98928b7096")),
+ (PythonVersion { name: Cow::Borrowed("pypy"), arch: Cow::Borrowed("x86_64"), os: Cow::Borrowed("macos"), major: 3, minor: 7, patch: 9, suffix: None }, "https://downloads.python.org/pypy/pypy3.7-v7.3.3-osx64.tar.bz2", Some("d72b27d5bb60813273f14f07378a08822186a66e216c5d1a768ad295b582438d")),
(PythonVersion { name: Cow::Borrowed("cpython"), arch: Cow::Borrowed("aarch64"), os: Cow::Borrowed("linux"), major: 3, minor: 12, patch: 1, suffix: None }, "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.12.1%2B20240107-aarch64-unknown-linux-gnu-lto-full.tar.zst", Some("3621be2cd8b5686e10a022f04869911cad9197a3ef77b30879fe25e792d7c249")),
(PythonVersion { name: Cow::Borrowed("cpython"), arch: Cow::Borrowed("aarch64"), os: Cow::Borrowed("macos"), major: 3, minor: 12, patch: 1, suffix: None }, "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.12.1%2B20240107-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", Some("61e51e3490537b800fcefad718157cf775de41044e95aa538b63ab599f66f3a9")),
(PythonVersion { name: Cow::Borrowed("cpython"), arch: Cow::Borrowed("x86"), os: Cow::Borrowed("windows"), major: 3, minor: 12, patch: 1, suffix: None }, "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.12.1%2B20240107-i686-pc-windows-msvc-shared-pgo-full.tar.zst", Some("22866d35fdf58e90e75d6ba9aa78c288b452ea7041fa9bc5549eca9daa431883")),
diff --git a/rye/src/installer.rs b/rye/src/installer.rs
index b051bc1bcf..84c2a8a48e 100644
--- a/rye/src/installer.rs
+++ b/rye/src/installer.rs
@@ -15,9 +15,9 @@ use crate::bootstrap::{ensure_self_venv, fetch};
use crate::config::Config;
use crate::consts::VENV_BIN;
use crate::platform::get_app_dir;
-use crate::pyproject::{normalize_package_name, ExpandedSources};
+use crate::pyproject::{normalize_package_name, read_venv_marker, ExpandedSources};
use crate::sources::PythonVersionRequest;
-use crate::sync::create_virtualenv;
+use crate::sync::{create_virtualenv, VenvMarker};
use crate::utils::{
get_short_executable_name, get_venv_python_bin, is_executable, symlink_file, CommandOutput,
};
@@ -68,15 +68,27 @@ print(json.dumps(result))
static SUCCESSFULLY_DOWNLOADED_RE: Lazy =
Lazy::new(|| Regex::new("(?m)^Successfully downloaded (.*?)$").unwrap());
-#[derive(Ord, PartialOrd, Eq, PartialEq)]
+#[derive(Eq, PartialEq)]
pub struct ToolInfo {
pub version: String,
pub scripts: Vec,
+ pub venv_marker: Option,
+ pub valid: bool,
}
impl ToolInfo {
- pub fn new(version: String, scripts: Vec) -> Self {
- Self { version, scripts }
+ pub fn new(
+ version: String,
+ scripts: Vec,
+ venv_marker: Option,
+ valid: bool,
+ ) -> Self {
+ Self {
+ version,
+ scripts,
+ venv_marker,
+ valid,
+ }
}
}
@@ -335,8 +347,9 @@ pub fn list_installed_tools() -> Result, Error> {
}
let tool_name = folder.file_name().to_string_lossy().to_string();
let target_venv_bin_path = folder.path().join(VENV_BIN);
- let mut scripts = Vec::new();
+ let venv_marker = read_venv_marker(&folder.path());
+ let mut scripts = Vec::new();
for script in fs::read_dir(target_venv_bin_path.clone())? {
let script = script?;
let script_path = script.path();
@@ -347,17 +360,23 @@ pub fn list_installed_tools() -> Result, Error> {
}
}
}
- let tool_version = Command::new(target_venv_bin_path.join("python"))
+
+ let output = Command::new(target_venv_bin_path.join("python"))
.arg("-c")
.arg(TOOL_VERSION_SCRIPT)
.arg(tool_name.clone())
.stdout(Stdio::piped())
- .output()?;
- let tool_version = String::from_utf8_lossy(&tool_version.stdout)
- .trim()
- .to_string();
-
- rv.insert(tool_name, ToolInfo::new(tool_version, scripts));
+ .output();
+ let valid = output.is_ok();
+ let tool_version = match output {
+ Ok(output) => String::from_utf8_lossy(&output.stdout).trim().to_string(),
+ Err(_) => String::new(),
+ };
+
+ rv.insert(
+ tool_name,
+ ToolInfo::new(tool_version, scripts, venv_marker, valid),
+ );
}
Ok(rv)
diff --git a/rye/src/lock.rs b/rye/src/lock.rs
index 92f760cec4..1922546125 100644
--- a/rye/src/lock.rs
+++ b/rye/src/lock.rs
@@ -32,12 +32,14 @@ static REQUIREMENTS_HEADER: &str = r#"# generated by rye
# use `rye lock` or `rye sync` to update this lockfile
#
# last locked with the following flags:
-# pre: {{ lock_options.pre }}
-# features: {{ lock_options.features }}
-# all-features: {{ lock_options.all_features }}
-# with-sources: {{ lock_options.with_sources }}
+# pre: {{ lock_options.pre|tojson }}
+# features: {{ lock_options.features|tojson }}
+# all-features: {{ lock_options.all_features|tojson }}
+# with-sources: {{ lock_options.with_sources|tojson }}
"#;
+static PARAM_RE: Lazy =
+ Lazy::new(|| Regex::new(r"^# (pre|features|all-features|with_sources):\s*(.*?)$").unwrap());
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum LockMode {
@@ -73,6 +75,55 @@ pub struct LockOptions {
pub all_features: bool,
/// Should locking happen with sources?
pub with_sources: bool,
+ /// Do not reuse (reset) prior lock options.
+ pub reset: bool,
+}
+
+impl LockOptions {
+ /// Writes the lock options as header.
+ pub fn write_header(&self, mut w: W) -> Result<(), Error> {
+ writeln!(w, "{}", render!(REQUIREMENTS_HEADER, lock_options => self))?;
+ Ok(())
+ }
+
+ /// Restores lock options from a requirements file.
+ ///
+ /// This also applies overrides from the command line.
+ pub fn restore<'o>(s: &str, opts: &'o LockOptions) -> Result, Error> {
+ // nothing to do here
+ if opts.reset {
+ return Ok(Cow::Borrowed(opts));
+ }
+
+ let mut rv = opts.clone();
+ for line in s
+ .lines()
+ .skip_while(|x| *x != "# last locked with the following flags:")
+ {
+ if let Some(m) = PARAM_RE.captures(line) {
+ let value = &m[2];
+ match &m[1] {
+ "pre" => rv.pre = rv.pre || serde_json::from_str(value)?,
+ "features" => {
+ if rv.features.is_empty() {
+ rv.features = serde_json::from_str(value)?;
+ }
+ }
+ "all-features" => {
+ rv.all_features = rv.all_features || serde_json::from_str(value)?
+ }
+ "with-sources" => rv.with_sources = serde_json::from_str(value)?,
+ _ => unreachable!(),
+ }
+ }
+ }
+
+ if rv.all_features {
+ rv.features = Vec::new();
+ }
+
+ Ok(Cow::Owned(rv))
+ }
}
/// Creates lockfiles for all projects in the workspace.
@@ -310,11 +361,14 @@ fn generate_lockfile(
) -> Result<(), Error> {
let scratch = tempfile::tempdir()?;
let requirements_file = scratch.path().join("requirements.txt");
- if lockfile.is_file() {
- fs::copy(lockfile, &requirements_file)?;
+ let lock_options = if lockfile.is_file() {
+ let requirements = fs::read_to_string(lockfile)?;
+ fs::write(&requirements_file, &requirements)?;
+ LockOptions::restore(&requirements, lock_options)?
} else {
fs::write(&requirements_file, b"")?;
- }
+ Cow::Borrowed(lock_options)
+ };
let mut cmd = if Config::current().use_uv() {
let self_venv = ensure_self_venv(output)?;
@@ -331,6 +385,9 @@ fn generate_lockfile(
} else if output == CommandOutput::Quiet {
cmd.arg("-q");
}
+ if lock_options.pre {
+ cmd.arg("--prerelease=allow");
+ }
// this primarily exists for testing
if let Ok(dt) = env::var("__RYE_UV_EXCLUDE_NEWER") {
cmd.arg("--exclude-newer").arg(dt);
@@ -359,6 +416,9 @@ fn generate_lockfile(
} else {
"-q"
});
+ if lock_options.pre {
+ cmd.arg("--pre");
+ }
cmd
};
@@ -376,9 +436,6 @@ fn generate_lockfile(
if lock_options.update_all {
cmd.arg("--upgrade");
}
- if lock_options.pre {
- cmd.arg("--pre");
- }
sources.add_as_pip_args(&mut cmd);
set_proxy_variables(&mut cmd);
let status = cmd.status().context("unable to run pip-compile")?;
@@ -392,7 +449,7 @@ fn generate_lockfile(
workspace_path,
exclusions,
sources,
- lock_options,
+ &lock_options,
)?;
Ok(())
@@ -407,7 +464,7 @@ fn finalize_lockfile(
lock_options: &LockOptions,
) -> Result<(), Error> {
let mut rv = BufWriter::new(fs::File::create(out)?);
- writeln!(rv, "{}", render!(REQUIREMENTS_HEADER, lock_options))?;
+ lock_options.write_header(&mut rv)?;
// only if we are asked to include sources we do that.
if lock_options.with_sources {
diff --git a/rye/src/pyproject.rs b/rye/src/pyproject.rs
index 06398f41e3..5bedfa47a8 100644
--- a/rye/src/pyproject.rs
+++ b/rye/src/pyproject.rs
@@ -1068,6 +1068,19 @@ pub fn read_venv_marker(venv_path: &Path) -> Option {
serde_json::from_slice(&contents).ok()
}
+pub fn write_venv_marker(venv_path: &Path, py_ver: &PythonVersion) -> Result<(), Error> {
+ fs::write(
+ venv_path.join("rye-venv.json"),
+ serde_json::to_string_pretty(&VenvMarker {
+ python: py_ver.clone(),
+ venv_path: Some(venv_path.into()),
+ })?,
+ )
+ .context("failed writing venv marker file")?;
+
+ Ok(())
+}
+
pub fn get_current_venv_python_version(venv_path: &Path) -> Option {
read_venv_marker(venv_path).map(|x| x.python)
}
diff --git a/rye/src/sync.rs b/rye/src/sync.rs
index c899c5b93a..67a337f0c7 100644
--- a/rye/src/sync.rs
+++ b/rye/src/sync.rs
@@ -17,7 +17,7 @@ use crate::lock::{
};
use crate::piptools::{get_pip_sync, get_pip_tools_venv_path};
use crate::platform::get_toolchain_python_bin;
-use crate::pyproject::{read_venv_marker, ExpandedSources, PyProject};
+use crate::pyproject::{read_venv_marker, write_venv_marker, ExpandedSources, PyProject};
use crate::sources::PythonVersion;
use crate::utils::{
get_venv_python_bin, mark_path_sync_ignore, set_proxy_variables, symlink_dir, CommandOutput,
@@ -72,7 +72,7 @@ impl SyncOptions {
}
/// Config written into the virtualenv for sync purposes.
-#[derive(Serialize, Deserialize, Debug)]
+#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
pub struct VenvMarker {
pub python: PythonVersion,
pub venv_path: Option,
@@ -170,14 +170,6 @@ pub fn sync(mut cmd: SyncOptions) -> Result<(), Error> {
let prompt = pyproject.name().unwrap_or("venv");
create_virtualenv(output, &self_venv, &py_ver, &venv, prompt)
.context("failed creating virtualenv ahead of sync")?;
- fs::write(
- venv.join("rye-venv.json"),
- serde_json::to_string_pretty(&VenvMarker {
- python: py_ver.clone(),
- venv_path: Some(venv.clone().into()),
- })?,
- )
- .context("failed writing venv marker file")?;
}
// prepare necessary utilities for pip-sync. This is a super crude
@@ -318,6 +310,19 @@ pub fn sync(mut cmd: SyncOptions) -> Result<(), Error> {
Ok(())
}
+/// Performs an autosync.
+pub fn autosync(pyproject: &PyProject, output: CommandOutput) -> Result<(), Error> {
+ sync(SyncOptions {
+ output,
+ dev: true,
+ mode: SyncMode::Regular,
+ force: false,
+ no_lock: false,
+ lock_options: LockOptions::default(),
+ pyproject: Some(pyproject.toml_path().to_path_buf()),
+ })
+}
+
pub fn create_virtualenv(
output: CommandOutput,
self_venv: &Path,
@@ -370,6 +375,8 @@ pub fn create_virtualenv(
bail!("failed to initialize virtualenv");
}
+ write_venv_marker(venv, py_ver)?;
+
// uv can only do it now
if Config::current().use_uv() {
update_venv_sync_marker(output, venv);
diff --git a/rye/src/utils/mod.rs b/rye/src/utils/mod.rs
index 77e4a8e7cb..9d712f192e 100644
--- a/rye/src/utils/mod.rs
+++ b/rye/src/utils/mod.rs
@@ -34,6 +34,7 @@ pub(crate) mod windows;
#[cfg(unix)]
pub(crate) mod unix;
+pub(crate) mod ruff;
pub(crate) mod toml;
#[cfg(windows)]
diff --git a/rye/src/utils/ruff.rs b/rye/src/utils/ruff.rs
new file mode 100644
index 0000000000..39e2ffe44e
--- /dev/null
+++ b/rye/src/utils/ruff.rs
@@ -0,0 +1,82 @@
+use std::env;
+use std::ffi::OsString;
+use std::path::PathBuf;
+use std::process::Command;
+
+use anyhow::Error;
+use clap::Parser;
+
+use crate::bootstrap::ensure_self_venv;
+use crate::consts::VENV_BIN;
+use crate::pyproject::{locate_projects, PyProject};
+use crate::utils::{CommandOutput, QuietExit};
+
+#[derive(Parser, Debug)]
+pub struct RuffArgs {
+ /// List of files or directories to limit the operation to
+ paths: Vec,
+ /// Perform the operation on all packages
+ #[arg(short, long)]
+ all: bool,
+ /// Perform the operation on a specific package
+ #[arg(short, long)]
+ package: Vec,
+ /// Use this pyproject.toml file
+ #[arg(long, value_name = "PYPROJECT_TOML")]
+ pyproject: Option,
+ /// Enables verbose diagnostics.
+ #[arg(short, long)]
+ verbose: bool,
+ /// Turns off all output.
+ #[arg(short, long, conflicts_with = "verbose")]
+ quiet: bool,
+ /// Extra arguments to ruff
+ #[arg(last = true)]
+ extra_args: Vec,
+}
+
+pub fn execute_ruff(args: RuffArgs, extra_args: &[&str]) -> Result<(), Error> {
+ let project = PyProject::load_or_discover(args.pyproject.as_deref())?;
+ let output = CommandOutput::from_quiet_and_verbose(args.quiet, args.verbose);
+ let venv = ensure_self_venv(output)?;
+ let ruff = venv.join(VENV_BIN).join("ruff");
+
+ let mut ruff_cmd = Command::new(ruff);
+ if env::var_os("RUFF_CACHE_DIR").is_none() {
+ ruff_cmd.env(
+ "RUFF_CACHE_DIR",
+ project.workspace_path().join(".ruff_cache"),
+ );
+ }
+ match output {
+ CommandOutput::Normal => {}
+ CommandOutput::Verbose => {
+ ruff_cmd.arg("--verbose");
+ }
+ CommandOutput::Quiet => {
+ ruff_cmd.arg("-q");
+ }
+ }
+ ruff_cmd.args(extra_args);
+ ruff_cmd.args(args.extra_args);
+
+ ruff_cmd.arg("--");
+ if args.paths.is_empty() {
+ let projects = locate_projects(project, args.all, &args.package[..])?;
+ for project in projects {
+ ruff_cmd.arg(project.root_path().as_os_str());
+ }
+ } else {
+ for file in args.paths {
+ ruff_cmd.arg(file.as_os_str());
+ }
+ }
+
+ let status = ruff_cmd.status()?;
+ if !status.success() {
+ let code = status.code().unwrap_or(1);
+ Err(QuietExit(code).into())
+ } else {
+ Ok(())
+ }
+}
diff --git a/rye/tests/common/mod.rs b/rye/tests/common/mod.rs
index 06bf8adfcb..67178c7391 100644
--- a/rye/tests/common/mod.rs
+++ b/rye/tests/common/mod.rs
@@ -9,6 +9,7 @@ use tempfile::TempDir;
// Exclude any packages uploaded after this date.
pub static EXCLUDE_NEWER: &str = "2023-11-18T12:00:00Z";
+#[allow(unused)]
pub const INSTA_FILTERS: &[(&str, &str)] = &[
// general temp folders
(
@@ -31,7 +32,7 @@ fn marked_tempdir() -> TempDir {
}
fn bootstrap_test_rye() -> PathBuf {
- let home = get_cargo_bin("rye").parent().unwrap().join("rye-test-home");
+ let home = get_bin().parent().unwrap().join("rye-test-home");
fs::create_dir_all(&home).ok();
let lock_path = home.join("lock");
let mut lock = fslock::LockFile::open(&lock_path).unwrap();
@@ -130,6 +131,38 @@ impl Space {
self.cmd(get_bin())
}
+ #[allow(unused)]
+ pub fn edit_toml, R, F: FnOnce(&mut toml_edit::Document) -> R>(
+ &self,
+ path: P,
+ f: F,
+ ) -> R {
+ let p = self.project_path().join(path.as_ref());
+ let mut doc = if p.is_file() {
+ std::fs::read_to_string(&p).unwrap().parse().unwrap()
+ } else {
+ toml_edit::Document::default()
+ };
+ let rv = f(&mut doc);
+ fs::create_dir_all(p.parent().unwrap()).ok();
+ fs::write(p, doc.to_string()).unwrap();
+ rv
+ }
+
+ #[allow(unused)]
+ pub fn write, B: AsRef<[u8]>>(&self, path: P, contents: B) {
+ let p = self.project_path().join(path.as_ref());
+ fs::create_dir_all(p.parent().unwrap()).ok();
+ fs::write(p, contents).unwrap();
+ }
+
+ #[allow(unused)]
+ pub fn read_string>(&self, path: P) -> String {
+ let p = self.project_path().join(path.as_ref());
+ fs::read_to_string(p).unwrap()
+ }
+
+ #[allow(unused)]
pub fn init(&self, name: &str) {
let status = self
.cmd(get_bin())
@@ -150,6 +183,13 @@ impl Space {
pub fn project_path(&self) -> &Path {
&self.project_dir
}
+
+ #[allow(unused)]
+ pub fn lock_rye_home(&self) -> fslock::LockFile {
+ let mut lock = fslock::LockFile::open(&self.rye_home().join("lock")).unwrap();
+ lock.lock().unwrap();
+ lock
+ }
}
#[allow(unused_macros)]
diff --git a/rye/tests/test_add.rs b/rye/tests/test_add.rs
new file mode 100644
index 0000000000..1b388ffbb2
--- /dev/null
+++ b/rye/tests/test_add.rs
@@ -0,0 +1,92 @@
+use toml_edit::{value, ArrayOfTables, Table};
+
+use crate::common::{rye_cmd_snapshot, Space};
+
+mod common;
+
+#[test]
+fn test_add_flask() {
+ let space = Space::new();
+ space.init("my-project");
+ // add colorama to ensure we have this as a dependency on all platforms
+ rye_cmd_snapshot!(space.rye_cmd().arg("add").arg("flask==3.0.0").arg("colorama"), @r###"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+ Initializing new virtualenv in [TEMP_PATH]/project/.venv
+ Python version: cpython@3.12.1
+ Added colorama>=0.4.6 as regular dependency
+ Added flask>=3.0.0 as regular dependency
+ Reusing already existing virtualenv
+ Generating production lockfile: [TEMP_PATH]/project/requirements.lock
+ Generating dev lockfile: [TEMP_PATH]/project/requirements-dev.lock
+ Installing dependencies
+ Done!
+
+ ----- stderr -----
+ warning: Requirements file [TEMP_FILE] does not contain any dependencies
+ Built 1 editable in [EXECUTION_TIME]
+ Resolved 9 packages in [EXECUTION_TIME]
+ warning: Requirements file [TEMP_FILE] does not contain any dependencies
+ Built 1 editable in [EXECUTION_TIME]
+ Resolved 9 packages in [EXECUTION_TIME]
+ Built 1 editable in [EXECUTION_TIME]
+ Resolved 8 packages in [EXECUTION_TIME]
+ Downloaded 8 packages in [EXECUTION_TIME]
+ Installed 9 packages in [EXECUTION_TIME]
+ + blinker==1.7.0
+ + click==8.1.7
+ + colorama==0.4.6
+ + flask==3.0.0
+ + itsdangerous==2.1.2
+ + jinja2==3.1.2
+ + markupsafe==2.1.3
+ + my-project==0.1.0 (from file:[TEMP_PATH]/project)
+ + werkzeug==3.0.1
+ "###);
+}
+
+#[test]
+fn test_add_from_find_links() {
+ let space = Space::new();
+ space.init("my-project");
+ space.edit_toml("pyproject.toml", |doc| {
+ let mut source = Table::new();
+ source["name"] = value("extra");
+ source["type"] = value("find-links");
+ source["url"] = value("https://download.pytorch.org/whl/torch_stable.html");
+ let mut sources = ArrayOfTables::new();
+ sources.push(source);
+ doc["tool"]["rye"]["sources"] = value(sources.into_array());
+ });
+
+ rye_cmd_snapshot!(space.rye_cmd().arg("add").arg("tqdm").arg("colorama"), @r###"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+ Initializing new virtualenv in [TEMP_PATH]/project/.venv
+ Python version: cpython@3.12.1
+ Added colorama>=0.4.6 as regular dependency
+ Added tqdm>=4.66.1 as regular dependency
+ Reusing already existing virtualenv
+ Generating production lockfile: [TEMP_PATH]/project/requirements.lock
+ Generating dev lockfile: [TEMP_PATH]/project/requirements-dev.lock
+ Installing dependencies
+ Done!
+
+ ----- stderr -----
+ warning: Requirements file [TEMP_FILE] does not contain any dependencies
+ Built 1 editable in [EXECUTION_TIME]
+ Resolved 3 packages in [EXECUTION_TIME]
+ warning: Requirements file [TEMP_FILE] does not contain any dependencies
+ Built 1 editable in [EXECUTION_TIME]
+ Resolved 3 packages in [EXECUTION_TIME]
+ Built 1 editable in [EXECUTION_TIME]
+ Resolved 2 packages in [EXECUTION_TIME]
+ Downloaded 2 packages in [EXECUTION_TIME]
+ Installed 3 packages in [EXECUTION_TIME]
+ + colorama==0.4.6
+ + my-project==0.1.0 (from file:[TEMP_PATH]/project)
+ + tqdm==4.66.1
+ "###);
+}
diff --git a/rye/tests/test_ruff.rs b/rye/tests/test_ruff.rs
new file mode 100644
index 0000000000..1d2a9027bd
--- /dev/null
+++ b/rye/tests/test_ruff.rs
@@ -0,0 +1,72 @@
+use insta::assert_snapshot;
+
+use crate::common::{rye_cmd_snapshot, Space};
+
+mod common;
+
+#[test]
+fn test_lint_and_format() {
+ let space = Space::new();
+ space.init("my-project");
+ space.write(
+ "src/my_project/__init__.py",
+ r#"import os
+
+def hello():
+
+
+ return "Hello World";
+"#,
+ );
+
+ // start with lint
+ rye_cmd_snapshot!(space.rye_cmd().arg("lint"), @r###"
+ success: false
+ exit_code: 1
+ ----- stdout -----
+ src/my_project/__init__.py:1:8: F401 [*] `os` imported but unused
+ src/my_project/__init__.py:6:25: E703 [*] Statement ends with an unnecessary semicolon
+ Found 2 errors.
+ [*] 2 fixable with the `--fix` option.
+
+ ----- stderr -----
+ "###);
+ rye_cmd_snapshot!(space.rye_cmd().arg("lint").arg("--fix"), @r###"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+ Found 2 errors (2 fixed, 0 remaining).
+
+ ----- stderr -----
+ "###);
+ assert_snapshot!(space.read_string("src/my_project/__init__.py"), @r###"
+
+ def hello():
+
+
+ return "Hello World"
+ "###);
+
+ // fmt next
+ rye_cmd_snapshot!(space.rye_cmd().arg("fmt").arg("--check"), @r###"
+ success: false
+ exit_code: 1
+ ----- stdout -----
+ Would reformat: src/my_project/__init__.py
+ 1 file would be reformatted
+
+ ----- stderr -----
+ "###);
+ rye_cmd_snapshot!(space.rye_cmd().arg("fmt"), @r###"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+ 1 file reformatted
+
+ ----- stderr -----
+ "###);
+ assert_snapshot!(space.read_string("src/my_project/__init__.py"), @r###"
+ def hello():
+ return "Hello World"
+ "###);
+}
diff --git a/rye/tests/test_self.rs b/rye/tests/test_self.rs
new file mode 100644
index 0000000000..7fcd228a49
--- /dev/null
+++ b/rye/tests/test_self.rs
@@ -0,0 +1,44 @@
+use crate::common::Space;
+mod common;
+
+// This test is self-destructive, making other tests slow, ignore it by default.
+#[test]
+#[ignore]
+fn test_self_uninstall() {
+ let space = Space::new();
+ let _guard = space.lock_rye_home();
+
+ // install a global tool to ensure tools directory is created
+ space
+ .rye_cmd()
+ .arg("install")
+ .arg("pycowsay")
+ .arg("-f")
+ .status()
+ .unwrap();
+
+ assert!(space.rye_home().join("self").is_dir());
+ assert!(space.rye_home().join("py").is_dir());
+ assert!(space.rye_home().join("tools").is_dir());
+
+ let status = space
+ .rye_cmd()
+ .arg("self")
+ .arg("uninstall")
+ .arg("--yes")
+ .status()
+ .unwrap();
+ assert!(status.success());
+
+ let may_left = &["env", "config.toml", "lock"];
+ let leftovers: Vec<_> = space
+ .rye_home()
+ .read_dir()
+ .unwrap()
+ .filter(|x| {
+ let x = x.as_ref().unwrap();
+ !may_left.contains(&x.file_name().to_str().unwrap())
+ })
+ .collect();
+ assert!(leftovers.is_empty(), "leftovers: {:?}", leftovers);
+}
diff --git a/rye/tests/test_sync.rs b/rye/tests/test_sync.rs
index ebd404c545..19372f8ae4 100644
--- a/rye/tests/test_sync.rs
+++ b/rye/tests/test_sync.rs
@@ -31,11 +31,12 @@ fn test_empty_sync() {
}
#[test]
-fn test_add_and_sync() {
+fn test_add_and_sync_no_auto_sync() {
let space = Space::new();
space.init("my-project");
+
// add colorama to ensure we have this as a dependency on all platforms
- rye_cmd_snapshot!(space.rye_cmd().arg("add").arg("flask==3.0.0").arg("colorama"), @r###"
+ rye_cmd_snapshot!(space.rye_cmd().arg("add").arg("flask==3.0.0").arg("colorama").arg("--no-sync"), @r###"
success: true
exit_code: 0
----- stdout -----
@@ -78,3 +79,45 @@ fn test_add_and_sync() {
+ werkzeug==3.0.1
"###);
}
+
+#[test]
+fn test_add_autosync() {
+ let space = Space::new();
+ space.init("my-project");
+ // add colorama to ensure we have this as a dependency on all platforms
+ rye_cmd_snapshot!(space.rye_cmd().arg("add").arg("flask==3.0.0").arg("colorama"), @r###"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+ Initializing new virtualenv in [TEMP_PATH]/project/.venv
+ Python version: cpython@3.12.1
+ Added colorama>=0.4.6 as regular dependency
+ Added flask>=3.0.0 as regular dependency
+ Reusing already existing virtualenv
+ Generating production lockfile: [TEMP_PATH]/project/requirements.lock
+ Generating dev lockfile: [TEMP_PATH]/project/requirements-dev.lock
+ Installing dependencies
+ Done!
+
+ ----- stderr -----
+ warning: Requirements file [TEMP_FILE] does not contain any dependencies
+ Built 1 editable in [EXECUTION_TIME]
+ Resolved 9 packages in [EXECUTION_TIME]
+ warning: Requirements file [TEMP_FILE] does not contain any dependencies
+ Built 1 editable in [EXECUTION_TIME]
+ Resolved 9 packages in [EXECUTION_TIME]
+ Built 1 editable in [EXECUTION_TIME]
+ Resolved 8 packages in [EXECUTION_TIME]
+ Downloaded 8 packages in [EXECUTION_TIME]
+ Installed 9 packages in [EXECUTION_TIME]
+ + blinker==1.7.0
+ + click==8.1.7
+ + colorama==0.4.6
+ + flask==3.0.0
+ + itsdangerous==2.1.2
+ + jinja2==3.1.2
+ + markupsafe==2.1.3
+ + my-project==0.1.0 (from file:[TEMP_PATH]/project)
+ + werkzeug==3.0.1
+ "###);
+}
diff --git a/rye/tests/test_tools.rs b/rye/tests/test_tools.rs
new file mode 100644
index 0000000000..789a0c06ee
--- /dev/null
+++ b/rye/tests/test_tools.rs
@@ -0,0 +1,96 @@
+use std::env::consts::EXE_EXTENSION;
+use std::fs;
+
+use crate::common::{rye_cmd_snapshot, Space};
+
+mod common;
+
+#[test]
+fn test_basic_tool_behavior() {
+ let space = Space::new();
+
+ // in case we left things behind from last run.
+ fs::remove_dir_all(space.rye_home().join("tools")).ok();
+ fs::remove_file(
+ space
+ .rye_home()
+ .join("shims")
+ .join("pycowsay")
+ .with_extension(EXE_EXTENSION),
+ )
+ .ok();
+
+ rye_cmd_snapshot!(
+ space.rye_cmd()
+ .arg("tools")
+ .arg("install")
+ .arg("pycowsay")
+ .arg("-p")
+ .arg("cpython@3.11"), @r###"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+
+ Installed scripts:
+ - pycowsay
+
+ ----- stderr -----
+ Resolved 1 package in [EXECUTION_TIME]
+ Downloaded 1 package in [EXECUTION_TIME]
+ Installed 1 package in [EXECUTION_TIME]
+ + pycowsay==0.0.0.2
+ "###);
+
+ rye_cmd_snapshot!(
+ space.rye_cmd()
+ .arg("tools")
+ .arg("list"), @r###"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+ pycowsay
+
+ ----- stderr -----
+ "###);
+
+ rye_cmd_snapshot!(
+ space.rye_cmd()
+ .arg("tools")
+ .arg("list")
+ .arg("--include-version"), @r###"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+ pycowsay 0.0.0.2 (cpython@3.11.7)
+
+ ----- stderr -----
+ "###);
+
+ rye_cmd_snapshot!(
+ space.rye_cmd()
+ .arg("toolchain")
+ .arg("remove")
+ .arg("cpython@3.11.7"), @r###"
+ success: false
+ exit_code: 1
+ ----- stdout -----
+
+ ----- stderr -----
+ error: toolchain cpython@3.11.7 is still in use by tool pycowsay
+ "###);
+
+ rye_cmd_snapshot!(
+ space.rye_cmd()
+ .arg("tools")
+ .arg("uninstall")
+ .arg("pycowsay"), @r###"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+ Uninstalled pycowsay
+
+ ----- stderr -----
+ "###);
+
+ assert!(!space.rye_home().join("tools").join("pycowsay").is_dir());
+}