diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8313c1b599cf..044c438f5a70 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -108,37 +108,30 @@ jobs: - name: "Clippy" run: cargo clippy --workspace --all-targets --all-features --locked -- -D warnings - cargo-clippy-xwin: - # Do not set timeout below 15 minutes as uncached xwin Windows SDK download can take 10+ minutes - timeout-minutes: 20 + cargo-clippy-windows: + timeout-minutes: 15 needs: determine_changes if: ${{ github.repository == 'astral-sh/uv' && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }} - runs-on: ubuntu-latest + runs-on: + labels: "windows-latest-xlarge" name: "cargo clippy | windows" steps: - uses: actions/checkout@v4 - - name: Load xwin cache - uses: actions/cache@v4 - with: - path: "${{ github.workspace}}/.xwin" - key: cargo-xwin-x86_64 - - name: Load rust cache - uses: Swatinem/rust-cache@v2 - with: - save-if: ${{ github.ref == 'refs/heads/main' }} + - uses: Swatinem/rust-cache@v2 + + - name: Create Dev Drive using ReFS + run: ${{ github.workspace }}/.github/workflows/setup-dev-drive.ps1 + + # actions/checkout does not let us clone into anywhere outside ${{ github.workspace }}, so we have to copy the clone... + - name: Copy Git Repo to Dev Drive + run: | + Copy-Item -Path "${{ github.workspace }}" -Destination "${{ env.UV_WORKSPACE }}" -Recurse + - name: "Install Rust toolchain" - run: rustup target add x86_64-pc-windows-msvc - - name: "Install cargo-xwin" - uses: taiki-e/install-action@v2 - with: - tool: cargo-xwin - - name: Install xwin dependencies - run: sudo apt-get install --no-install-recommends -y lld llvm clang cmake ninja-build + run: rustup component add clippy + - name: "Clippy" - run: cargo xwin clippy --target x86_64-pc-windows-msvc --workspace --all-targets --all-features --locked --profile fast-build -- -D warnings - env: - XWIN_ARCH: "x86_64" - XWIN_CACHE_DIR: "${{ github.workspace}}/.xwin" + run: cargo clippy --workspace --all-targets --all-features --locked -- -D warnings cargo-dev-generate-all: timeout-minutes: 10 @@ -303,7 +296,7 @@ jobs: # See https://github.com/astral-sh/uv/issues/6940 UV_LINK_MODE: copy run: | - cargo nextest run --no-default-features --features python,pypi --workspace --status-level skip --failure-output immediate-final --no-fail-fast -j 20 --final-status-level slow + cargo nextest run --no-default-features --features python,pypi,python-managed --workspace --status-level skip --failure-output immediate-final --no-fail-fast -j 20 --final-status-level slow - name: "Smoke test" working-directory: ${{ env.UV_WORKSPACE }} @@ -329,11 +322,10 @@ jobs: # Separate jobs for the nightly crate windows-trampoline-check: - # Do not set timeout below 15 minutes as uncached xwin Windows SDK download can take 10+ minutes - timeout-minutes: 20 + timeout-minutes: 15 needs: determine_changes if: ${{ github.repository == 'astral-sh/uv' && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }} - runs-on: ubuntu-latest + runs-on: windows-latest-xlarge name: "check windows trampoline | ${{ matrix.target-arch }}" strategy: fail-fast: false @@ -342,50 +334,45 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Load xwin cache - uses: actions/cache@v4 - with: - path: "${{ github.workspace }}/.xwin" - key: cargo-xwin-${{ matrix.target-arch }} + - name: Create Dev Drive using ReFS + run: ${{ github.workspace }}/.github/workflows/setup-dev-drive.ps1 - - uses: rui314/setup-mold@v1 + # actions/checkout does not let us clone into anywhere outside ${{ github.workspace }}, so we have to copy the clone... + - name: Copy Git Repo to Dev Drive + run: | + Copy-Item -Path "${{ github.workspace }}" -Destination "${{ env.UV_WORKSPACE }}" -Recurse - uses: Swatinem/rust-cache@v2 with: - workspaces: ${{ github.workspace }}/crates/uv-trampoline + workspaces: ${{ env.UV_WORKSPACE }}/crates/uv-trampoline - name: "Install Rust toolchain" - working-directory: ${{ github.workspace }}/crates/uv-trampoline + working-directory: ${{ env.UV_WORKSPACE }}/crates/uv-trampoline run: | rustup target add ${{ matrix.target-arch }}-pc-windows-msvc rustup component add rust-src --target ${{ matrix.target-arch }}-pc-windows-msvc - - name: "Install cargo-xwin and cargo-bloat" + - name: "Install cargo-bloat" uses: taiki-e/install-action@v2 with: - tool: cargo-xwin,cargo-bloat - - - name: "Install xwin dependencies" - run: sudo apt-get install --no-install-recommends -y lld llvm clang cmake ninja-build + tool: cargo-bloat - name: "Clippy" - working-directory: ${{ github.workspace }}/crates/uv-trampoline - if: matrix.target-arch == 'x86_64' - run: cargo xwin clippy --all-features --locked --target x86_64-pc-windows-msvc --tests -- -D warnings - env: - XWIN_ARCH: "x86_64" - XWIN_CACHE_DIR: "${{ github.workspace }}/.xwin" + working-directory: ${{ env.UV_WORKSPACE }}/crates/uv-trampoline + run: cargo clippy --all-features --locked --target x86_64-pc-windows-msvc --tests -- -D warnings - name: "Bloat Check" - working-directory: ${{ github.workspace }}/crates/uv-trampoline - if: matrix.target-arch == 'x86_64' + working-directory: ${{ env.UV_WORKSPACE }}/crates/uv-trampoline run: | - cargo xwin bloat --release --target x86_64-pc-windows-msvc | \ - grep -v -i -E 'core::fmt::write|core::fmt::getcount' | \ - grep -q -E 'core::fmt|std::panicking|std::backtrace_rs' && exit 1 || exit 0 - env: - XWIN_ARCH: "x86_64" - XWIN_CACHE_DIR: "${{ github.workspace }}/.xwin" + $output = cargo bloat --release --target x86_64-pc-windows-msvc + $filteredOutput = $output | Select-String -Pattern 'core::fmt::write|core::fmt::getcount' -NotMatch + $containsPatterns = $filteredOutput | Select-String -Pattern 'core::fmt|std::panicking|std::backtrace_rs' + + if ($containsPatterns) { + Exit 1 + } else { + Exit 0 + } # Separate jobs for the nightly crate windows-trampoline-test: @@ -397,6 +384,7 @@ jobs: strategy: fail-fast: false matrix: + # Note, we exclude `aarch64` because it's not supported by the GitHub runner target-arch: ["x86_64", "i686"] steps: - uses: actions/checkout@v4 @@ -408,6 +396,11 @@ jobs: - uses: Swatinem/rust-cache@v2 with: workspaces: ${{ github.workspace }}/crates/uv-trampoline + - name: "Test committed binaries" + working-directory: ${{ github.workspace }} + run: | + rustup target add ${{ matrix.target-arch }}-pc-windows-msvc + cargo test -p uv-trampoline-builder --target ${{ matrix.target-arch }}-pc-windows-msvc # Build and copy the new binaries - name: "Build" working-directory: ${{ github.workspace }}/crates/uv-trampoline @@ -415,9 +408,11 @@ jobs: cargo build --target ${{ matrix.target-arch }}-pc-windows-msvc cp target/${{ matrix.target-arch }}-pc-windows-msvc/debug/uv-trampoline-console.exe trampolines/uv-trampoline-${{ matrix.target-arch }}-console.exe cp target/${{ matrix.target-arch }}-pc-windows-msvc/debug/uv-trampoline-gui.exe trampolines/uv-trampoline-${{ matrix.target-arch }}-gui.exe - - name: "Test" - working-directory: ${{ github.workspace }}/crates/uv-trampoline - run: cargo test --target ${{ matrix.target-arch }}-pc-windows-msvc --test * + - name: "Test new binaries" + working-directory: ${{ github.workspace }} + run: | + # We turn off the default "production" test feature since these are debug binaries + cargo test -p uv-trampoline-builder --target ${{ matrix.target-arch }}-pc-windows-msvc --no-default-features typos: runs-on: ubuntu-latest @@ -707,7 +702,7 @@ jobs: - name: "Install free-threaded Python via uv" run: | - ./uv python install 3.13t + ./uv python install -v 3.13t ./uv venv -p 3.13t --python-preference only-managed - name: "Check version" @@ -779,7 +774,7 @@ jobs: run: chmod +x ./uv - name: "Install PyPy" - run: ./uv python install pypy3.9 + run: ./uv python install -v pypy3.9 - name: "Create a virtual environment" run: | @@ -1617,9 +1612,23 @@ jobs: - uses: actions/checkout@v4 - name: "Install pyenv" - uses: "gabrielfalcao/pyenv-action@v18" - with: - default: 3.9.7 + run: | + # Install pyenv + curl https://pyenv.run | bash + + # Set up environment variables for current step + export PYENV_ROOT="$HOME/.pyenv" + export PATH="$PYENV_ROOT/bin:$PATH" + eval "$(pyenv init -)" + + # Install Python 3.9 + pyenv install 3.9 + pyenv global 3.9 + + # Make environment variables persist across steps + echo "PYENV_ROOT=$HOME/.pyenv" >> $GITHUB_ENV + echo "$HOME/.pyenv/bin" >> $GITHUB_PATH + echo "$HOME/.pyenv/shims" >> $GITHUB_PATH - name: "Download binary" uses: actions/download-artifact@v4 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 70717ec89605..beac7f104ee2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,12 +7,12 @@ exclude: | repos: - repo: https://github.com/abravalheri/validate-pyproject - rev: v0.21 + rev: v0.22 hooks: - id: validate-pyproject - repo: https://github.com/crate-ci/typos - rev: v1.26.0 + rev: v1.26.8 hooks: - id: typos @@ -42,7 +42,7 @@ repos: types_or: [yaml, json5] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.7.0 + rev: v0.7.1 hooks: - id: ruff-format - id: ruff diff --git a/CHANGELOG.md b/CHANGELOG.md index 352bd4dc138a..45ae3f01faa2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,130 @@ # Changelog +## 0.4.29 + +### Enhancements + +- Sort errors during display in `uv python install` ([#8684](https://github.com/astral-sh/uv/pull/8684)) +- Update resolver to use disjointness checks instead of marker equality ([#8661](https://github.com/astral-sh/uv/pull/8661)) +- Add `riscv64` to supported Python platform tags ([#8660](https://github.com/astral-sh/uv/pull/8660)) + +### Bug fixes + +- Fix hard and soft float libc detection for managed Python distributions on ARM ([#8498](https://github.com/astral-sh/uv/pull/8498)) +- Handle cycles in `uv pip tree` ([#8689](https://github.com/astral-sh/uv/pull/8689)) +- Respect dependency group markers in `uv export` ([#8659](https://github.com/astral-sh/uv/pull/8659)) +- Support transitive dependencies in Git workspaces ([#8665](https://github.com/astral-sh/uv/pull/8665)) +- Use portable paths for subdirectories in lock URLs ([#8707](https://github.com/astral-sh/uv/pull/8707)) +- Update `uv init --virtual` to imply `--no-package` ([#8595](https://github.com/astral-sh/uv/pull/8595)) + +### Preview + +- Install versioned Python executables into the bin directory during `uv python install` (Unix only) ([#8458](https://github.com/astral-sh/uv/pull/8458)) + +### Documentation + +- Clarify relationship between specifiers and `requires-python` range ([#8688](https://github.com/astral-sh/uv/pull/8688)) +- Fix broken link in docs ([#8552](https://github.com/astral-sh/uv/pull/8552)) +- Fix outdated documentation on `Requires-Python` ([#8679](https://github.com/astral-sh/uv/pull/8679)) +- Add Google Artifact Registry index authentication guide ([#8579](https://github.com/astral-sh/uv/pull/8579)) + +## 0.4.28 + +### Enhancements + +- Add support for requesting free-threaded builds via `+freethreaded` ([#8645](https://github.com/astral-sh/uv/pull/8645)) +- Improve trusted publishing error messages ([#8633](https://github.com/astral-sh/uv/pull/8633)) +- Remove unneeded `return` from Maturin project template ([#8604](https://github.com/astral-sh/uv/pull/8604)) +- Skip Python interpreter discovery for `uv export` ([#8638](https://github.com/astral-sh/uv/pull/8638)) +- Hint about missing trusted publishing permission ([#8632](https://github.com/astral-sh/uv/pull/8632)) + +### Configuration + +- Add environment variable to disable progress output ([#8600](https://github.com/astral-sh/uv/pull/8600)) + +### Bug fixes + +- Fork when minimum Python version increases ([#8628](https://github.com/astral-sh/uv/pull/8628)) +- Ignore empty groups when validating lock ([#8598](https://github.com/astral-sh/uv/pull/8598)) +- Remove duplicate word in error message ([#8589](https://github.com/astral-sh/uv/pull/8589)) +- Support cyclic dependencies in `uv tree` ([#8564](https://github.com/astral-sh/uv/pull/8564)) +- Update `uv init` to imply `--package` when using `--build-backend` ([#8593](https://github.com/astral-sh/uv/pull/8593)) +- Restore use of `dev-dependencies` and `requires-dev` for lockfile compatibility ([#8599](https://github.com/astral-sh/uv/pull/8599)) + +### Documentation + +- Clarify `requires-python` requirement for dependencies ([#8619](https://github.com/astral-sh/uv/pull/8619)) +- Update CLI documentation for `--cache-dir` ([#8627](https://github.com/astral-sh/uv/pull/8627)) + +## 0.4.27 + +This release includes support for the `[dependency-groups]` table as recently standardized in [PEP 735](https://peps.python.org/pep-0735/). The table allows for declaration of optional dependency groups that are not published as part of the package metadata, unlike `[project.optional-dependencies]`. There are new `--group`, `--only-group`, and `--no-group` options throughout the uv interface. + +Previously, uv used a single `tool.uv.dev-dependencies` list for declaration of development dependencies. Now, uv supports declaring development dependencies in a standardized format and allows splitting development dependencies into multiple groups. + +For compatibility, and to simplify usage for people that do not need multiple groups, uv special-cases the group named `dev`. The `dev` group is equivalent to `tool.uv.dev-dependencies`. The contents of `tool.uv.dev-dependencies` will merged into the `dev` group in uv's resolver. The `--dev`, `--only-dev`, and `--no-dev` flags remain as aliases for the corresponding `--group` options. Support for `tool.uv.dev-dependencies` remains in this release, but will display warnings in a future release. + +uv syncs the `dev` group by default — this matches the exististing behavior for `tool.uv.dev-dependencies`. The default groups can be changed with the `tool.uv.default-groups` setting. + +Thank you to Stephen Rosen who authored PEP 735. + +### Enhancements + +- Support for PEP 735 ([#8272](https://github.com/astral-sh/uv/pull/8272)) +- Add support for `--dry-run` mode in `uv lock` ([#7783](https://github.com/astral-sh/uv/pull/7783)) +- Don't allow non-string email in authors ([#8520](https://github.com/astral-sh/uv/pull/8520)) +- Enforce lockfile schema versions ([#8509](https://github.com/astral-sh/uv/pull/8509)) + +### Bug fixes + +- Always attach URL to network errors ([#8444](https://github.com/astral-sh/uv/pull/8444)) +- Fix dangling non-platform dependencies in `uv tree` ([#8532](https://github.com/astral-sh/uv/pull/8532)) +- Prefer `lto` over `debug` free-threaded managed Python builds ([#8515](https://github.com/astral-sh/uv/pull/8515)) + +### Documentation + +- Add `tool.uv.sources` to the "Settings" reference ([#8543](https://github.com/astral-sh/uv/pull/8543)) +- Add reference to `uv build` and `uv publish` in the landing pages ([#8542](https://github.com/astral-sh/uv/pull/8542)) +- Avoid duplicate `[tool.uv]` header in TOML examples ([#8545](https://github.com/astral-sh/uv/pull/8545)) +- Document `.netrc` environment variable and path ([#8511](https://github.com/astral-sh/uv/pull/8511)) +- Fix `.netrc` typo in authentication docs ([#8521](https://github.com/astral-sh/uv/pull/8521)) +- Fix heading level of "Script support" on docs landing page ([#8544](https://github.com/astral-sh/uv/pull/8544)) +- Move the installation configuration docs to a separate page ([#8546](https://github.com/astral-sh/uv/pull/8546)) +- Update docs for `--publish-url` to avoid duplication. ([#8561](https://github.com/astral-sh/uv/pull/8561)) +- Fix typo ([#8554](https://github.com/astral-sh/uv/pull/8554)) +- Fix typo in description of `--strict` flag ([#8513](https://github.com/astral-sh/uv/pull/8513)) + +## 0.4.26 + +### Enhancements + +- Allow static dependency metadata entries for direct URL requirements ([#7846](https://github.com/astral-sh/uv/pull/7846)) +- Use reinstall report formatting for `uv python install --reinstall` ([#8487](https://github.com/astral-sh/uv/pull/8487)) +- Add support for system-level `uv.toml` configuration ([#7851](https://github.com/astral-sh/uv/pull/7851)) + +### Bug fixes + +- Apply `requires-python` narrowing with upper bounds ([#8403](https://github.com/astral-sh/uv/pull/8403)) +- Avoid rewriting `[[tool.uv.index]]` entries when credentials are provided ([#8502](https://github.com/astral-sh/uv/pull/8502)) +- Fix `uv add` comment handling for empty arrays ([#8504](https://github.com/astral-sh/uv/pull/8504)) +- Replace dashes with underscores in index credential variables ([#8452](https://github.com/astral-sh/uv/pull/8452)) +- Respect `--allow-insecure-host` in `uv publish` ([#8440](https://github.com/astral-sh/uv/pull/8440)) +- Allow arbitrary `--package` includes in `uv tree` ([#8507](https://github.com/astral-sh/uv/pull/8507)) +- Remove existing Python install after successful download in `uv python install` ([#8485](https://github.com/astral-sh/uv/pull/8485)) + +### Documentation + +- Add docs example for URLs with `[tool.uv.dependency-metadata]` ([#8484](https://github.com/astral-sh/uv/pull/8484)) +- Add help page for build failures ([#8286](https://github.com/astral-sh/uv/pull/8286)) +- Fix `cache-keys` typo in `tags = true` ([#8422](https://github.com/astral-sh/uv/pull/8422)) +- Add documentation examples for manual branch, rev, and tag Git dependencies ([#8497](https://github.com/astral-sh/uv/pull/8497)) + +### Error messages + +- Improve error message for cache info serialization ([#8500](https://github.com/astral-sh/uv/pull/8500)) +- Suggest `--from` command when executable is available for `uvx` ([#8473](https://github.com/astral-sh/uv/pull/8473)) +- Support `--with-editable` in `uv tool install` ([#8472](https://github.com/astral-sh/uv/pull/8472)) + ## 0.4.25 ### Enhancements @@ -1901,8 +2026,7 @@ and `uv tool run` ([#4717](https://github.com/astral-sh/uv/pull/4717)) - Add `uv tool uninstall` ([#4641](https://github.com/astral-sh/uv/pull/4641)) - Add support for specifying `name@version` in `uv tool run` ([#4572](https://github.com/astral-sh/uv/pull/4572)) - Allow `uv add` to specify optional dependency groups ([#4607](https://github.com/astral-sh/uv/pull/4607)) -- Allow the package spec to be passed positionally -in `uv tool install` ([#4564](https://github.com/astral-sh/uv/pull/4564)) +- Allow the package spec to be passed positionally in `uv tool install` ([#4564](https://github.com/astral-sh/uv/pull/4564)) - Avoid infinite loop for cyclic installs ([#4633](https://github.com/astral-sh/uv/pull/4633)) - Indent wheels like dependencies in the lockfile ([#4582](https://github.com/astral-sh/uv/pull/4582)) - Sync all packages in a virtual workspace ([#4636](https://github.com/astral-sh/uv/pull/4636)) diff --git a/Cargo.lock b/Cargo.lock index 267895cc8ac4..5b21956201c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -34,9 +34,9 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstream" -version = "0.6.15" +version = "0.6.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +checksum = "23a1e53f0f5d86382dafe1cf314783b2044280f406e7e1506368220ad11b1338" dependencies = [ "anstyle", "anstyle-parse", @@ -73,19 +73,19 @@ dependencies = [ [[package]] name = "anstyle-wincon" -version = "3.0.4" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" dependencies = [ "anstyle", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "anyhow" -version = "1.0.90" +version = "1.0.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37bf3594c4c988a53154954629820791dde498571819ae4ca50ca811e060cc95" +checksum = "c042108f3ed77fd83760a5fd79b53be043192bb3b9dba91d8c574c0ada7850c8" [[package]] name = "arrayref" @@ -269,9 +269,9 @@ dependencies = [ [[package]] name = "axoupdater" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa6f92ef9ab41a352f403f709ef0f09cda1f461795de52085cd46ed831ead02e" +checksum = "6fe17874ee40fb66fe1d2cb7871b14de4d298fbf77ea29a6cedb8027365e1a33" dependencies = [ "axoasset", "axoprocess", @@ -279,6 +279,7 @@ dependencies = [ "camino", "homedir", "miette", + "self-replace", "serde", "tempfile", "thiserror", @@ -1329,9 +1330,9 @@ dependencies = [ [[package]] name = "goblin" -version = "0.8.2" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b363a30c165f666402fe6a3024d3bec7ebc898f96a4a23bd1c99f8dbf3f4f47" +checksum = "53ab3f32d1d77146981dea5d6b1e8fe31eedcb7013e5e00d6ccd1259a4b4d923" dependencies = [ "log", "plain", @@ -1619,9 +1620,9 @@ checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" [[package]] name = "insta" -version = "1.40.0" +version = "1.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6593a41c7a73841868772495db7dc1e8ecab43bb5c0b6da2059246c4b506ab60" +checksum = "a1f72d3e19488cf7d8ea52d2fc0f8754fc933398b337cd3cbdb28aaeb35159ef" dependencies = [ "console", "lazy_static", @@ -1809,9 +1810,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.159" +version = "0.2.161" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" +checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" [[package]] name = "libmimalloc-sys" @@ -2467,13 +2468,37 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.88" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c3a7fc5db1e57d5a779a352c8cdb57b29aa4c40cc69c3a68a7fedc815fbf2f9" +checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" dependencies = [ "unicode-ident", ] +[[package]] +name = "procfs" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "731e0d9356b0c25f16f33b5be79b1c57b562f141ebfcdb0ad8ac2c13a24293b4" +dependencies = [ + "bitflags 2.6.0", + "flate2", + "hex", + "lazy_static", + "procfs-core", + "rustix", +] + +[[package]] +name = "procfs-core" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d3554923a69f4ce04c4a754260c338f505ce22642d3830e049a399fc2059a29" +dependencies = [ + "bitflags 2.6.0", + "hex", +] + [[package]] name = "ptr_meta" version = "0.3.0" @@ -2497,13 +2522,14 @@ dependencies = [ [[package]] name = "pubgrub" version = "0.2.1" -source = "git+https://github.com/astral-sh/pubgrub?rev=7243f4faf8e54837aa8a401a18406e7173de4ad5#7243f4faf8e54837aa8a401a18406e7173de4ad5" +source = "git+https://github.com/astral-sh/pubgrub?rev=95e1390399cdddee986b658be19587eb1fdb2d79#95e1390399cdddee986b658be19587eb1fdb2d79" dependencies = [ "indexmap", "log", "priority-queue", "rustc-hash", "thiserror", + "version-ranges", ] [[package]] @@ -2685,9 +2711,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", @@ -2783,7 +2809,7 @@ dependencies = [ "wasm-streams", "web-sys", "webpki-roots", - "windows-registry", + "windows-registry 0.2.0", ] [[package]] @@ -2977,9 +3003,9 @@ checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" [[package]] name = "rustix" -version = "0.38.37" +version = "0.38.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" +checksum = "aa260229e6538e52293eeb577aabd09945a09d6d9cc0fc550ed7529056c2e32a" dependencies = [ "bitflags 2.6.0", "errno", @@ -3161,6 +3187,17 @@ dependencies = [ "libc", ] +[[package]] +name = "self-replace" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ec815b5eab420ab893f63393878d89c90fdd94c0bcc44c07abb8ad95552fb7" +dependencies = [ + "fastrand", + "tempfile", + "windows-sys 0.52.0", +] + [[package]] name = "semver" version = "1.0.23" @@ -3169,9 +3206,9 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" -version = "1.0.210" +version = "1.0.213" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +checksum = "3ea7893ff5e2466df8d720bb615088341b295f849602c6956047f8f80f0e9bc1" dependencies = [ "serde_derive", ] @@ -3189,9 +3226,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.210" +version = "1.0.213" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +checksum = "7e85ad2009c50b58e87caa8cd6dac16bdf511bbfb7af6c33df902396aa480fa5" dependencies = [ "proc-macro2", "quote", @@ -3453,9 +3490,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.82" +version = "2.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83540f837a8afc019423a8edb95b52a8effe46957ee402287f4292fae35be021" +checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56" dependencies = [ "proc-macro2", "quote", @@ -3595,15 +3632,6 @@ dependencies = [ "syn", ] -[[package]] -name = "testing_logger" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d92b727cb45d33ae956f7f46b966b25f1bc712092aeef9dba5ac798fc89f720" -dependencies = [ - "log", -] - [[package]] name = "textwrap" version = "0.16.1" @@ -3617,18 +3645,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.64" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" +checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.64" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" +checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602" dependencies = [ "proc-macro2", "quote", @@ -3722,9 +3750,9 @@ source = "git+https://github.com/charliermarsh/tl.git?rev=6e25b2ee2513d75385101a [[package]] name = "tokio" -version = "1.40.0" +version = "1.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" +checksum = "145f3413504347a2be84393cc8a7d2fb4d863b375909ea59f2158261aa258bbb" dependencies = [ "backtrace", "bytes", @@ -3930,6 +3958,27 @@ dependencies = [ "tracing-serde", ] +[[package]] +name = "tracing-test" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "557b891436fe0d5e0e363427fc7f217abf9ccd510d5136549847bdcbcd011d68" +dependencies = [ + "tracing-core", + "tracing-subscriber", + "tracing-test-macro", +] + +[[package]] +name = "tracing-test-macro" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04659ddb06c87d233c566112c1c9c5b9e98256d9af50ec3bc9c8327f873a7568" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "tracing-tree" version = "0.4.0" @@ -4127,7 +4176,7 @@ checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" [[package]] name = "uv" -version = "0.4.25" +version = "0.4.29" dependencies = [ "anstream", "anyhow", @@ -4146,7 +4195,6 @@ dependencies = [ "futures", "http", "ignore", - "indexmap", "indicatif", "indoc", "insta", @@ -4154,11 +4202,13 @@ dependencies = [ "jiff", "miette", "owo-colors", + "petgraph", "predicates", "rayon", "regex", "reqwest", "rustc-hash", + "same-file", "serde", "serde_json", "similar", @@ -4290,9 +4340,9 @@ dependencies = [ "uv-normalize", "uv-pep440", "uv-pep508", - "uv-pubgrub", "uv-pypi-types", "uv-warnings", + "version-ranges", "walkdir", "zip", ] @@ -4335,8 +4385,6 @@ name = "uv-cache" version = "0.0.1" dependencies = [ "clap", - "directories", - "etcetera", "fs-err", "nanoid", "rmp-serde", @@ -4347,6 +4395,7 @@ dependencies = [ "url", "uv-cache-info", "uv-cache-key", + "uv-dirs", "uv-distribution-types", "uv-fs", "uv-normalize", @@ -4533,6 +4582,16 @@ dependencies = [ "walkdir", ] +[[package]] +name = "uv-dirs" +version = "0.0.1" +dependencies = [ + "directories", + "dirs-sys", + "etcetera", + "uv-static", +] + [[package]] name = "uv-dispatch" version = "0.0.1" @@ -4749,6 +4808,7 @@ dependencies = [ "uv-pep440", "uv-platform-tags", "uv-pypi-types", + "uv-trampoline-builder", "uv-warnings", "walkdir", "zip", @@ -4852,6 +4912,7 @@ dependencies = [ "tracing", "unicode-width", "unscanny", + "version-ranges", ] [[package]] @@ -4862,23 +4923,21 @@ dependencies = [ "indexmap", "insta", "itertools 0.13.0", - "log", - "pubgrub", "regex", "rustc-hash", "schemars", "serde", "serde_json", "smallvec", - "testing_logger", "thiserror", "tracing", + "tracing-test", "unicode-width", "url", "uv-fs", "uv-normalize", "uv-pep440", - "uv-pubgrub", + "version-ranges", ] [[package]] @@ -4906,16 +4965,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "uv-pubgrub" -version = "0.0.1" -dependencies = [ - "itertools 0.13.0", - "pubgrub", - "thiserror", - "uv-pep440", -] - [[package]] name = "uv-publish" version = "0.1.0" @@ -4934,15 +4983,17 @@ dependencies = [ "rustc-hash", "serde", "serde_json", - "sha2", "thiserror", "tokio", "tokio-util", "tracing", "url", + "uv-cache", "uv-client", "uv-configuration", "uv-distribution-filename", + "uv-distribution-types", + "uv-extract", "uv-fs", "uv-metadata", "uv-pypi-types", @@ -4990,6 +5041,7 @@ dependencies = [ "indoc", "itertools 0.13.0", "owo-colors", + "procfs", "regex", "reqwest", "reqwest-middleware", @@ -5011,6 +5063,7 @@ dependencies = [ "uv-cache-info", "uv-cache-key", "uv-client", + "uv-dirs", "uv-distribution-filename", "uv-extract", "uv-fs", @@ -5021,9 +5074,10 @@ dependencies = [ "uv-pypi-types", "uv-state", "uv-static", + "uv-trampoline-builder", "uv-warnings", "which", - "windows-registry", + "windows-registry 0.3.0", "windows-result 0.2.0", "windows-sys 0.59.0", ] @@ -5135,7 +5189,6 @@ dependencies = [ "uv-pep440", "uv-pep508", "uv-platform-tags", - "uv-pubgrub", "uv-pypi-types", "uv-python", "uv-requirements-txt", @@ -5212,10 +5265,9 @@ dependencies = [ name = "uv-state" version = "0.0.1" dependencies = [ - "directories", - "etcetera", "fs-err", "tempfile", + "uv-dirs", ] [[package]] @@ -5229,7 +5281,6 @@ dependencies = [ name = "uv-tool" version = "0.0.1" dependencies = [ - "dirs-sys", "fs-err", "pathdiff", "serde", @@ -5238,6 +5289,7 @@ dependencies = [ "toml_edit", "tracing", "uv-cache", + "uv-dirs", "uv-fs", "uv-install-wheel", "uv-installer", @@ -5251,6 +5303,20 @@ dependencies = [ "uv-virtualenv", ] +[[package]] +name = "uv-trampoline-builder" +version = "0.0.1" +dependencies = [ + "anyhow", + "assert_cmd", + "assert_fs", + "fs-err", + "thiserror", + "uv-fs", + "which", + "zip", +] + [[package]] name = "uv-types" version = "0.0.1" @@ -5273,7 +5339,7 @@ dependencies = [ [[package]] name = "uv-version" -version = "0.4.25" +version = "0.4.29" [[package]] name = "uv-virtualenv" @@ -5324,6 +5390,7 @@ dependencies = [ "toml_edit", "tracing", "url", + "uv-cache-key", "uv-distribution-types", "uv-fs", "uv-git", @@ -5343,6 +5410,14 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +[[package]] +name = "version-ranges" +version = "0.1.0" +source = "git+https://github.com/astral-sh/pubgrub?rev=95e1390399cdddee986b658be19587eb1fdb2d79#95e1390399cdddee986b658be19587eb1fdb2d79" +dependencies = [ + "smallvec", +] + [[package]] name = "version_check" version = "0.9.5" @@ -5594,7 +5669,7 @@ dependencies = [ "windows-implement 0.58.0", "windows-interface 0.58.0", "windows-result 0.2.0", - "windows-strings", + "windows-strings 0.1.0", "windows-targets 0.52.6", ] @@ -5649,7 +5724,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" dependencies = [ "windows-result 0.2.0", - "windows-strings", + "windows-strings 0.1.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-registry" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bafa604f2104cf5ae2cc2db1dee84b7e6a5d11b05f737b60def0ffdc398cbc0a" +dependencies = [ + "windows-result 0.2.0", + "windows-strings 0.2.0", "windows-targets 0.52.6", ] @@ -5681,6 +5767,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-strings" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978d65aedf914c664c510d9de43c8fd85ca745eaff1ed53edf409b479e441663" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.48.0" diff --git a/Cargo.toml b/Cargo.toml index fc2ecdc7e682..88511bcef40b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ uv-cli = { path = "crates/uv-cli" } uv-client = { path = "crates/uv-client" } uv-configuration = { path = "crates/uv-configuration" } uv-console = { path = "crates/uv-console" } +uv-dirs = { path = "crates/uv-dirs" } uv-dispatch = { path = "crates/uv-dispatch" } uv-distribution = { path = "crates/uv-distribution" } uv-distribution-filename = { path = "crates/uv-distribution-filename" } @@ -45,10 +46,9 @@ uv-metadata = { path = "crates/uv-metadata" } uv-normalize = { path = "crates/uv-normalize" } uv-once-map = { path = "crates/uv-once-map" } uv-options-metadata = { path = "crates/uv-options-metadata" } -uv-pep440 = { path = "crates/uv-pep440", features = ["tracing", "rkyv"] } +uv-pep440 = { path = "crates/uv-pep440", features = ["tracing", "rkyv", "version-ranges"] } uv-pep508 = { path = "crates/uv-pep508", features = ["non-pep508-extensions"] } uv-platform-tags = { path = "crates/uv-platform-tags" } -uv-pubgrub = { path = "crates/uv-pubgrub" } uv-publish = { path = "crates/uv-publish" } uv-pypi-types = { path = "crates/uv-pypi-types" } uv-python = { path = "crates/uv-python" } @@ -60,6 +60,7 @@ uv-settings = { path = "crates/uv-settings" } uv-shell = { path = "crates/uv-shell" } uv-state = { path = "crates/uv-state" } uv-static = { path = "crates/uv-static" } +uv-trampoline-builder = { path = "crates/uv-trampoline-builder" } uv-tool = { path = "crates/uv-tool" } uv-types = { path = "crates/uv-types" } uv-version = { path = "crates/uv-version" } @@ -102,7 +103,7 @@ fs2 = { version = "0.4.3" } futures = { version = "0.3.30" } glob = { version = "0.3.1" } globwalk = { version = "0.9.1" } -goblin = { version = "0.8.2", default-features = false, features = ["std", "elf32", "elf64", "endian_fd"] } +goblin = { version = "0.9.0", default-features = false, features = ["std", "elf32", "elf64", "endian_fd"] } hex = { version = "0.4.3" } home = { version = "0.5.9" } html-escape = { version = "0.2.13" } @@ -124,8 +125,10 @@ path-slash = { version = "0.2.1" } pathdiff = { version = "0.2.1" } petgraph = { version = "0.6.5" } platform-info = { version = "2.0.3" } +procfs = { version = "0.16.0" , default-features = false, features = ["flate2"] } proc-macro2 = { version = "1.0.86" } -pubgrub = { git = "https://github.com/astral-sh/pubgrub", rev = "7243f4faf8e54837aa8a401a18406e7173de4ad5" } +pubgrub = { git = "https://github.com/astral-sh/pubgrub", rev = "95e1390399cdddee986b658be19587eb1fdb2d79" } +version-ranges = { git = "https://github.com/astral-sh/pubgrub", rev = "95e1390399cdddee986b658be19587eb1fdb2d79" } quote = { version = "1.0.37" } rayon = { version = "1.10.0" } reflink-copy = { version = "0.1.19" } @@ -169,7 +172,7 @@ url = { version = "2.5.2" } urlencoding = { version = "2.1.3" } walkdir = { version = "2.5.0" } which = { version = "6.0.3", features = ["regex"] } -windows-registry = { version = "0.2.0" } +windows-registry = { version = "0.3.0" } windows-result = { version = "0.2.0" } windows-sys = { version = "0.59.0", features = ["Win32_Foundation", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Ioctl", "Win32_System_IO"] } winreg = { version = "0.52.0" } diff --git a/README.md b/README.md index 186c4af7e57d..f158a6dda23c 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,8 @@ An extremely fast Python package and project manager, written in Rust. ## Highlights -- 🚀 A single tool to replace `pip`, `pip-tools`, `pipx`, `poetry`, `pyenv`, `virtualenv`, and more. +- 🚀 A single tool to replace `pip`, `pip-tools`, `pipx`, `poetry`, `pyenv`, `twine`, `virtualenv`, + and more. - ⚡️ [10-100x faster](https://github.com/astral-sh/uv/blob/main/BENCHMARKS.md) than `pip`. - 🐍 [Installs and manages](#python-management) Python versions. - 🛠️ [Runs and installs](#tool-management) Python applications. @@ -101,6 +102,9 @@ All checks passed! See the [project documentation](https://docs.astral.sh/uv/guides/projects/) to get started. +uv also supports building and publishing projects, even if they're not managed with uv. See the +[publish guide](https://docs.astral.sh/uv/guides/publish/) to learn more. + ### Tool management uv executes and installs command-line tools provided by Python packages, similar to `pipx`. diff --git a/crates/uv-auth/src/credentials.rs b/crates/uv-auth/src/credentials.rs index a31419641007..e79dfac9278f 100644 --- a/crates/uv-auth/src/credentials.rs +++ b/crates/uv-auth/src/credentials.rs @@ -145,10 +145,9 @@ impl Credentials { /// /// For example, given a name of `"pytorch"`, search for `UV_INDEX_PYTORCH_USERNAME` and /// `UV_INDEX_PYTORCH_PASSWORD`. - pub fn from_env(name: &str) -> Option { - let name = name.to_uppercase(); - let username = std::env::var(EnvVars::index_username(&name)).ok(); - let password = std::env::var(EnvVars::index_password(&name)).ok(); + pub fn from_env(name: impl AsRef) -> Option { + let username = std::env::var(EnvVars::index_username(name.as_ref())).ok(); + let password = std::env::var(EnvVars::index_password(name.as_ref())).ok(); if username.is_none() && password.is_none() { None } else { diff --git a/crates/uv-auth/src/middleware.rs b/crates/uv-auth/src/middleware.rs index b2f67980bd19..046feaf20707 100644 --- a/crates/uv-auth/src/middleware.rs +++ b/crates/uv-auth/src/middleware.rs @@ -71,11 +71,11 @@ impl AuthMiddleware { } } - /// Configure the [`netrc::Netrc`] credential file to use. + /// Configure the [`Netrc`] credential file to use. /// /// `None` disables authentication via netrc. #[must_use] - pub fn with_netrc(mut self, netrc: Option) -> Self { + pub fn with_netrc(mut self, netrc: Option) -> Self { self.netrc = if let Some(netrc) = netrc { NetrcMode::Enabled(netrc) } else { diff --git a/crates/uv-build-backend/Cargo.toml b/crates/uv-build-backend/Cargo.toml index a6fa3d51c6a2..b1e924c0eef4 100644 --- a/crates/uv-build-backend/Cargo.toml +++ b/crates/uv-build-backend/Cargo.toml @@ -18,7 +18,6 @@ uv-fs = { workspace = true } uv-normalize = { workspace = true } uv-pep440 = { workspace = true } uv-pep508 = { workspace = true } -uv-pubgrub = { workspace = true } uv-pypi-types = { workspace = true } uv-warnings = { workspace = true } @@ -32,6 +31,7 @@ spdx = { workspace = true } thiserror = { workspace = true } toml = { workspace = true } tracing = { workspace = true } +version-ranges = { workspace = true } walkdir = { workspace = true } zip = { workspace = true } diff --git a/crates/uv-build-backend/src/metadata.rs b/crates/uv-build-backend/src/metadata.rs index e825a555142a..30219acb7328 100644 --- a/crates/uv-build-backend/src/metadata.rs +++ b/crates/uv-build-backend/src/metadata.rs @@ -11,9 +11,9 @@ use uv_fs::Simplified; use uv_normalize::{ExtraName, PackageName}; use uv_pep440::{Version, VersionSpecifiers}; use uv_pep508::{Requirement, VersionOrUrl}; -use uv_pubgrub::PubGrubSpecifier; use uv_pypi_types::{Metadata23, VerbatimParsedUrl}; use uv_warnings::warn_user_once; +use version_ranges::Ranges; #[derive(Debug, Error)] pub enum ValidationError { @@ -135,9 +135,9 @@ impl PyProjectToml { ); passed = false; } - PubGrubSpecifier::from_pep440_specifiers(specifier) - .ok() - .and_then(|specifier| Some(specifier.bounding_range()?.1 != Bound::Unbounded)) + Ranges::from(specifier.clone()) + .bounding_range() + .map(|bounding_range| bounding_range.1 != Bound::Unbounded) .unwrap_or(false) } }; @@ -605,14 +605,19 @@ enum License { /// The entry is derived from the email format of `John Doe `. You need to /// provide at least name or email. #[derive(Deserialize, Debug, Clone)] -#[serde(untagged, expecting = "a table with 'name' and/or 'email' keys")] +// deny_unknown_fields prevents using the name field when the email is not a string. +#[serde( + untagged, + deny_unknown_fields, + expecting = "a table with 'name' and/or 'email' keys" +)] enum Contact { + /// TODO(konsti): RFC 822 validation. + NameEmail { name: String, email: String }, /// TODO(konsti): RFC 822 validation. Name { name: String }, /// TODO(konsti): RFC 822 validation. Email { email: String }, - /// TODO(konsti): RFC 822 validation. - NameEmail { name: String, email: String }, } /// The `[build-system]` section of a pyproject.toml as specified in PEP 517. diff --git a/crates/uv-build-backend/src/metadata/tests.rs b/crates/uv-build-backend/src/metadata/tests.rs index e0aaa1c18fa7..abc4b9fd3000 100644 --- a/crates/uv-build-backend/src/metadata/tests.rs +++ b/crates/uv-build-backend/src/metadata/tests.rs @@ -104,35 +104,37 @@ fn valid() { let metadata = pyproject_toml.to_metadata(temp_dir.path()).unwrap(); assert_snapshot!(metadata.core_metadata_format(), @r###" - Metadata-Version: 2.3 - Name: hello-world - Version: 0.1.0 - Summary: A Python package - Keywords: demo,example,package - Author: Ferris the crab - License: THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, - INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A - PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF - CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE - OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - Classifier: Development Status :: 6 - Mature - Classifier: License :: OSI Approved :: MIT License - Classifier: License :: OSI Approved :: Apache Software License - Classifier: Programming Language :: Python - Requires-Dist: flask>=3,<4 - Requires-Dist: sqlalchemy[asyncio]>=2.0.35,<3 - Maintainer: Konsti - Project-URL: Homepage, https://github.com/astral-sh/uv - Project-URL: Repository, https://astral.sh - Provides-Extra: mysql - Provides-Extra: postgres - Description-Content-Type: text/markdown - - # Foo - - This is the foo library. - "###); + Metadata-Version: 2.3 + Name: hello-world + Version: 0.1.0 + Summary: A Python package + Keywords: demo,example,package + Author: Ferris the crab + Author-email: Ferris the crab + License: THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE + OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + Classifier: Development Status :: 6 - Mature + Classifier: License :: OSI Approved :: MIT License + Classifier: License :: OSI Approved :: Apache Software License + Classifier: Programming Language :: Python + Requires-Dist: flask>=3,<4 + Requires-Dist: sqlalchemy[asyncio]>=2.0.35,<3 + Maintainer: Konsti + Maintainer-email: Konsti + Project-URL: Homepage, https://github.com/astral-sh/uv + Project-URL: Repository, https://astral.sh + Provides-Extra: mysql + Provides-Extra: postgres + Description-Content-Type: text/markdown + + # Foo + + This is the foo library. + "###); assert_snapshot!(pyproject_toml.to_entry_points().unwrap().unwrap(), @r###" [console_scripts] diff --git a/crates/uv-build-frontend/src/lib.rs b/crates/uv-build-frontend/src/lib.rs index ca7eded39348..ee3bea5b89de 100644 --- a/crates/uv-build-frontend/src/lib.rs +++ b/crates/uv-build-frontend/src/lib.rs @@ -474,6 +474,7 @@ impl SourceBuild { let requires_dist = RequiresDist::from_project_maybe_workspace( requires_dist, install_path, + None, locations, source_strategy, LowerBound::Allow, @@ -927,6 +928,7 @@ async fn create_pep517_build_environment( let requires_dist = RequiresDist::from_project_maybe_workspace( requires_dist, install_path, + None, locations, source_strategy, LowerBound::Allow, diff --git a/crates/uv-cache-info/src/cache_info.rs b/crates/uv-cache-info/src/cache_info.rs index 7a62ded3da54..6780dc3acd47 100644 --- a/crates/uv-cache-info/src/cache_info.rs +++ b/crates/uv-cache-info/src/cache_info.rs @@ -18,7 +18,6 @@ pub enum CacheInfoError { /// timestamps of relevant files, the current commit of a repository, etc. #[derive(Default, Debug, Clone, Hash, PartialEq, Eq, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "kebab-case")] -#[serde(try_from = "CacheInfoWire")] pub struct CacheInfo { /// The timestamp of the most recent `ctime` of any relevant files, at the time of the build. /// The timestamp will typically be the maximum of the `ctime` values of the `pyproject.toml`, @@ -200,46 +199,6 @@ impl CacheInfo { } } -#[derive(Debug, serde::Deserialize)] -struct TimestampCommit { - #[serde(default)] - timestamp: Option, - #[serde(default)] - commit: Option, - #[serde(default)] - tags: Option, -} - -#[derive(Debug, serde::Deserialize)] -#[serde(untagged)] -enum CacheInfoWire { - /// For backwards-compatibility, enable deserializing [`CacheInfo`] structs that are solely - /// represented by a timestamp. - Timestamp(Timestamp), - /// A [`CacheInfo`] struct that includes both a timestamp and a commit. - TimestampCommit(TimestampCommit), -} - -impl From for CacheInfo { - fn from(wire: CacheInfoWire) -> Self { - match wire { - CacheInfoWire::Timestamp(timestamp) => Self { - timestamp: Some(timestamp), - ..Self::default() - }, - CacheInfoWire::TimestampCommit(TimestampCommit { - timestamp, - commit, - tags, - }) => Self { - timestamp, - commit, - tags, - }, - } - } -} - /// A `pyproject.toml` with an (optional) `[tool.uv]` section. #[derive(Debug, Deserialize)] #[serde(rename_all = "kebab-case")] diff --git a/crates/uv-cache/Cargo.toml b/crates/uv-cache/Cargo.toml index 8f33d81f0370..577a84cf5bf1 100644 --- a/crates/uv-cache/Cargo.toml +++ b/crates/uv-cache/Cargo.toml @@ -17,6 +17,7 @@ doctest = false workspace = true [dependencies] +uv-dirs = { workspace = true } uv-cache-info = { workspace = true } uv-cache-key = { workspace = true } uv-distribution-types = { workspace = true } @@ -26,8 +27,6 @@ uv-pypi-types = { workspace = true } uv-static = { workspace = true } clap = { workspace = true, features = ["derive", "env"], optional = true } -directories = { workspace = true } -etcetera = { workspace = true } fs-err = { workspace = true, features = ["tokio"] } nanoid = { workspace = true } rmp-serde = { workspace = true } diff --git a/crates/uv-cache/src/cli.rs b/crates/uv-cache/src/cli.rs index 781b740175cc..41e6917c3da6 100644 --- a/crates/uv-cache/src/cli.rs +++ b/crates/uv-cache/src/cli.rs @@ -4,8 +4,6 @@ use uv_static::EnvVars; use crate::Cache; use clap::Parser; -use directories::ProjectDirs; -use etcetera::BaseStrategy; use tracing::{debug, warn}; #[derive(Parser, Debug, Clone)] @@ -25,8 +23,10 @@ pub struct CacheArgs { /// Path to the cache directory. /// - /// Defaults to `$HOME/Library/Caches/uv` on macOS, `$XDG_CACHE_HOME/uv` or `$HOME/.cache/uv` on - /// Linux, and `%LOCALAPPDATA%\uv\cache` on Windows. + /// Defaults to `$XDG_CACHE_HOME/uv` or `$HOME/.cache/uv` on macOS and Linux, and + /// `%LOCALAPPDATA%\uv\cache` on Windows. + /// + /// To view the location of the cache directory, run `uv cache dir`. #[arg(global = true, long, env = EnvVars::UV_CACHE_DIR)] pub cache_dir: Option, } @@ -45,18 +45,13 @@ impl Cache { Self::temp() } else if let Some(cache_dir) = cache_dir { Ok(Self::from_path(cache_dir)) - } else if let Some(cache_dir) = ProjectDirs::from("", "", "uv") - .map(|dirs| dirs.cache_dir().to_path_buf()) - .filter(|dir| dir.exists()) + } else if let Some(cache_dir) = uv_dirs::legacy_user_cache_dir().filter(|dir| dir.exists()) { // If the user has an existing directory at (e.g.) `/Users/user/Library/Caches/uv`, // respect it for backwards compatibility. Otherwise, prefer the XDG strategy, even on // macOS. Ok(Self::from_path(cache_dir)) - } else if let Some(cache_dir) = etcetera::base_strategy::choose_base_strategy() - .ok() - .map(|dirs| dirs.cache_dir().join("uv")) - { + } else if let Some(cache_dir) = uv_dirs::user_cache_dir() { if cfg!(windows) { // On Windows, we append `cache` to the LocalAppData directory, i.e., prefer // `C:\Users\User\AppData\Local\uv\cache` over `C:\Users\User\AppData\Local\uv`. diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 865a04f6a195..1d5d57c46a9d 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -15,7 +15,7 @@ use uv_configuration::{ ProjectBuildBackend, TargetTriple, TrustedHost, TrustedPublishing, VersionControlSystem, }; use uv_distribution_types::{Index, IndexUrl, Origin, PipExtraIndex, PipFindLinks, PipIndex}; -use uv_normalize::{ExtraName, PackageName}; +use uv_normalize::{ExtraName, GroupName, PackageName}; use uv_pep508::Requirement; use uv_pypi_types::VerbatimParsedUrl; use uv_python::{PythonDownloads, PythonPreference, PythonVersion}; @@ -232,7 +232,7 @@ pub struct GlobalArgs { /// Hide all progress outputs. /// /// For example, spinners or progress bars. - #[arg(global = true, long)] + #[arg(global = true, long, env = EnvVars::UV_NO_PROGRESS, value_parser = clap::builder::BoolishValueParser::new())] pub no_progress: bool, /// Change to the given directory prior to running the command. @@ -945,7 +945,7 @@ pub struct PipCompileArgs { #[arg(long, short, env = EnvVars::UV_BUILD_CONSTRAINT, value_delimiter = ' ', value_parser = parse_maybe_file_path)] pub build_constraint: Vec>, - /// Include optional dependencies from the extra group name; may be provided more than once. + /// Include optional dependencies from the specified extra name; may be provided more than once. /// /// Only applies to `pyproject.toml`, `setup.py`, and `setup.cfg` sources. #[arg(long, conflicts_with = "all_extras", value_parser = extra_name_with_clap_error)] @@ -1423,7 +1423,7 @@ pub struct PipSyncArgs { #[arg(long)] pub python_platform: Option, - /// Validate the Python environment after completing the installation, to detect and with + /// Validate the Python environment after completing the installation, to detect packages with /// missing dependencies or other issues. #[arg(long, overrides_with("no_strict"))] pub strict: bool, @@ -1494,7 +1494,7 @@ pub struct PipInstallArgs { #[arg(long, short, env = EnvVars::UV_BUILD_CONSTRAINT, value_delimiter = ' ', value_parser = parse_maybe_file_path)] pub build_constraint: Vec>, - /// Include optional dependencies from the extra group name; may be provided more than once. + /// Include optional dependencies from the specified extra name; may be provided more than once. /// /// Only applies to `pyproject.toml`, `setup.py`, and `setup.cfg` sources. #[arg(long, conflicts_with = "all_extras", value_parser = extra_name_with_clap_error)] @@ -1711,7 +1711,7 @@ pub struct PipInstallArgs { #[arg(long, overrides_with("inexact"))] pub exact: bool, - /// Validate the Python environment after completing the installation, to detect and with + /// Validate the Python environment after completing the installation, to detect packages with /// missing dependencies or other issues. #[arg(long, overrides_with("no_strict"))] pub strict: bool, @@ -2473,7 +2473,7 @@ pub struct InitArgs { /// /// Defines a `[build-system]` for the project. /// - /// This is the default behavior when using `--lib`. + /// This is the default behavior when using `--lib` or `--build-backend`. /// /// When using `--app`, this will include a `[project.scripts]` entrypoint and use a `src/` /// project structure. @@ -2485,7 +2485,7 @@ pub struct InitArgs { /// Does not include a `[build-system]` for the project. /// /// This is the default behavior when using `--app`. - #[arg(long, overrides_with = "package", conflicts_with = "lib")] + #[arg(long, overrides_with = "package", conflicts_with_all = ["lib", "build_backend"])] pub r#no_package: bool, /// Create a project for an application. @@ -2515,7 +2515,7 @@ pub struct InitArgs { /// /// By default, adds a requirement on the system Python version; use `--python` to specify an /// alternative Python version requirement. - #[arg(long, alias="script", conflicts_with_all=["app", "lib", "package"])] + #[arg(long, alias="script", conflicts_with_all=["app", "lib", "package", "build_backend"])] pub r#script: bool, /// Initialize a version control system for the project. @@ -2526,6 +2526,8 @@ pub struct InitArgs { pub vcs: Option, /// Initialize a build-backend of choice for the project. + /// + /// Implicitly sets `--package`. #[arg(long, value_enum, conflicts_with_all=["script", "no_package"])] pub build_backend: Option, @@ -2573,7 +2575,7 @@ pub struct InitArgs { #[derive(Args)] #[allow(clippy::struct_excessive_bools)] pub struct RunArgs { - /// Include optional dependencies from the extra group name. + /// Include optional dependencies from the specified extra name. /// /// May be provided more than once. /// @@ -2596,30 +2598,56 @@ pub struct RunArgs { #[arg(long, overrides_with("all_extras"), hide = true)] pub no_all_extras: bool, - /// Include development dependencies. + /// Include the development dependency group. + /// + /// Development dependencies are defined via `dependency-groups.dev` or + /// `tool.uv.dev-dependencies` in a `pyproject.toml`. /// - /// Development dependencies are defined via `tool.uv.dev-dependencies` in a - /// `pyproject.toml`. + /// This option is an alias for `--group dev`. /// /// This option is only available when running in a project. #[arg(long, overrides_with("no_dev"), hide = true)] pub dev: bool, - /// Omit development dependencies. + /// Omit the development dependency group. + /// + /// This option is an alias of `--no-group dev`. /// /// This option is only available when running in a project. #[arg(long, overrides_with("dev"))] pub no_dev: bool, + /// Include dependencies from the specified dependency group. + /// + /// May be provided multiple times. + #[arg(long, conflicts_with("only_group"))] + pub group: Vec, + + /// Exclude dependencies from the specified dependency group. + /// + /// May be provided multiple times. + #[arg(long)] + pub no_group: Vec, + + /// Only include dependencies from the specified dependency group. + /// + /// May be provided multiple times. + /// + /// The project itself will also be omitted. + #[arg(long, conflicts_with("group"))] + pub only_group: Vec, + /// Run a Python module. /// /// Equivalent to `python -m `. #[arg(short, long, conflicts_with = "script")] pub module: bool, - /// Omit non-development dependencies. + /// Only include the development dependency group. /// - /// The project itself will also be omitted. + /// Omit other dependencies. The project itself will also be omitted. + /// + /// This option is an alias for `--only-group dev`. #[arg(long, conflicts_with("no_dev"))] pub only_dev: bool, @@ -2755,7 +2783,7 @@ pub struct RunArgs { #[derive(Args)] #[allow(clippy::struct_excessive_bools)] pub struct SyncArgs { - /// Include optional dependencies from the extra group name. + /// Include optional dependencies from the specified extra name. /// /// May be provided more than once. /// @@ -2774,20 +2802,46 @@ pub struct SyncArgs { #[arg(long, overrides_with("all_extras"), hide = true)] pub no_all_extras: bool, - /// Include development dependencies. + /// Include the development dependency group. + /// + /// This option is an alias for `--group dev`. #[arg(long, overrides_with("no_dev"), hide = true)] pub dev: bool, - /// Omit development dependencies. + /// Omit the development dependency group. + /// + /// This option is an alias for `--no-group dev`. #[arg(long, overrides_with("dev"))] pub no_dev: bool, - /// Omit non-development dependencies. + /// Only include the development dependency group. /// - /// The project itself will also be omitted. + /// Omit other dependencies. The project itself will also be omitted. + /// + /// This option is an alias for `--only-group dev`. #[arg(long, conflicts_with("no_dev"))] pub only_dev: bool, + /// Include dependencies from the specified dependency group. + /// + /// May be provided multiple times. + #[arg(long, conflicts_with("only_group"))] + pub group: Vec, + + /// Exclude dependencies from the specified dependency group. + /// + /// May be provided multiple times. + #[arg(long)] + pub no_group: Vec, + + /// Only include dependencies from the specified dependency group. + /// + /// May be provided multiple times. + /// + /// The project itself will also be omitted. + #[arg(long, conflicts_with("group"))] + pub only_group: Vec, + /// Install any editable dependencies, including the project and any workspace members, as /// non-editable. #[arg(long)] @@ -2903,6 +2957,13 @@ pub struct LockArgs { #[arg(long, env = EnvVars::UV_FROZEN, value_parser = clap::builder::BoolishValueParser::new(), conflicts_with = "locked")] pub frozen: bool, + /// Perform a dry run, without writing the lockfile. + /// + /// In dry-run mode, uv will resolve the project's dependencies and report on the resulting + /// changes, but will not write the lockfile to disk. + #[arg(long, conflicts_with = "frozen", conflicts_with = "locked")] + pub dry_run: bool, + #[command(flatten)] pub resolver: ResolverArgs, @@ -2945,20 +3006,27 @@ pub struct AddArgs { #[arg(long, short, group = "sources", value_parser = parse_file_path)] pub requirements: Vec, - /// Add the requirements as development dependencies. - #[arg(long, conflicts_with("optional"))] + /// Add the requirements to the development dependency group. + /// + /// This option is an alias for `--group dev`. + #[arg(long, conflicts_with("optional"), conflicts_with("group"))] pub dev: bool, - /// Add the requirements to the specified optional dependency group. + /// Add the requirements to the package's optional dependencies for the specified extra. /// /// The group may then be activated when installing the project with the /// `--extra` flag. /// - /// To enable an optional dependency group for this requirement instead, see - /// `--extra`. - #[arg(long, conflicts_with("dev"))] + /// To enable an optional extra for this requirement instead, see `--extra`. + #[arg(long, conflicts_with("dev"), conflicts_with("group"))] pub optional: Option, + /// Add the requirements to the specified dependency group. + /// + /// These requirements will not be included in the published metadata for the project. + #[arg(long, conflicts_with("dev"), conflicts_with("optional"))] + pub group: Option, + /// Add the requirements as editable. #[arg(long, overrides_with = "no_editable")] pub editable: bool, @@ -2996,8 +3064,7 @@ pub struct AddArgs { /// /// May be provided more than once. /// - /// To add this dependency to an optional group in the current project - /// instead, see `--optional`. + /// To add this dependency to an optional extra instead, see `--optional`. #[arg(long)] pub extra: Option>, @@ -3063,14 +3130,20 @@ pub struct RemoveArgs { #[arg(required = true)] pub packages: Vec, - /// Remove the packages from the development dependencies. - #[arg(long, conflicts_with("optional"))] + /// Remove the packages from the development dependency group. + /// + /// This option is an alias for `--group dev`. + #[arg(long, conflicts_with("optional"), conflicts_with("group"))] pub dev: bool, - /// Remove the packages from the specified optional dependency group. - #[arg(long, conflicts_with("dev"))] + /// Remove the packages from the project's optional dependencies for the specified extra. + #[arg(long, conflicts_with("dev"), conflicts_with("group"))] pub optional: Option, + /// Remove the packages from the specified dependency group. + #[arg(long, conflicts_with("dev"), conflicts_with("optional"))] + pub group: Option, + /// Avoid syncing the virtual environment after re-locking the project. #[arg(long, env = EnvVars::UV_NO_SYNC, value_parser = clap::builder::BoolishValueParser::new(), conflicts_with = "frozen")] pub no_sync: bool, @@ -3139,17 +3212,49 @@ pub struct TreeArgs { #[command(flatten)] pub tree: DisplayTreeArgs, - /// Include development dependencies. + /// Include the development dependency group. + /// + /// Development dependencies are defined via `dependency-groups.dev` or + /// `tool.uv.dev-dependencies` in a `pyproject.toml`. /// - /// Development dependencies are defined via `tool.uv.dev-dependencies` in a - /// `pyproject.toml`. + /// This option is an alias for `--group dev`. #[arg(long, overrides_with("no_dev"), hide = true)] pub dev: bool, - /// Omit development dependencies. + /// Only include the development dependency group. + /// + /// Omit other dependencies. The project itself will also be omitted. + /// + /// This option is an alias for `--only-group dev`. + #[arg(long, conflicts_with("no_dev"))] + pub only_dev: bool, + + /// Omit the development dependency group. + /// + /// This option is an alias for `--no-group dev`. #[arg(long, overrides_with("dev"), conflicts_with = "invert")] pub no_dev: bool, + /// Include dependencies from the specified dependency group. + /// + /// May be provided multiple times. + #[arg(long, conflicts_with("only_group"))] + pub group: Vec, + + /// Exclude dependencies from the specified dependency group. + /// + /// May be provided multiple times. + #[arg(long)] + pub no_group: Vec, + + /// Only include dependencies from the specified dependency group. + /// + /// May be provided multiple times. + /// + /// The project itself will also be omitted. + #[arg(long, conflicts_with("group"))] + pub only_group: Vec, + /// Assert that the `uv.lock` will remain unchanged. /// /// Requires that the lockfile is up-to-date. If the lockfile is missing or @@ -3224,7 +3329,7 @@ pub struct ExportArgs { #[arg(long)] pub package: Option, - /// Include optional dependencies from the extra group name. + /// Include optional dependencies from the specified extra name. /// /// May be provided more than once. #[arg(long, conflicts_with = "all_extras", value_parser = extra_name_with_clap_error)] @@ -3237,20 +3342,46 @@ pub struct ExportArgs { #[arg(long, overrides_with("all_extras"), hide = true)] pub no_all_extras: bool, - /// Include development dependencies. + /// Include the development dependency group. + /// + /// This option is an alias for `--group dev`. #[arg(long, overrides_with("no_dev"), hide = true)] pub dev: bool, - /// Omit development dependencies. + /// Omit the development dependency group. + /// + /// This option is an alias for `--no-group dev`. #[arg(long, overrides_with("dev"))] pub no_dev: bool, - /// Omit non-development dependencies. + /// Only include the development dependency group. /// - /// The project itself will also be omitted. + /// Omit other dependencies. The project itself will also be omitted. + /// + /// This option is an alias for `--only-group dev`. #[arg(long, conflicts_with("no_dev"))] pub only_dev: bool, + /// Include dependencies from the specified dependency group. + /// + /// May be provided multiple times. + #[arg(long, conflicts_with("only_group"))] + pub group: Vec, + + /// Exclude dependencies from the specified dependency group. + /// + /// May be provided multiple times. + #[arg(long)] + pub no_group: Vec, + + /// Only include dependencies from the specified dependency group. + /// + /// May be provided multiple times. + /// + /// The project itself will also be omitted. + #[arg(long, conflicts_with("group"))] + pub only_group: Vec, + /// Exclude the comment header at the top of the generated output file. #[arg(long, overrides_with("header"))] pub no_header: bool, @@ -3514,6 +3645,10 @@ pub struct ToolInstallArgs { #[arg(short, long)] pub editable: bool, + /// Include the given packages as editables. + #[arg(long, value_delimiter = ',')] + pub with_editable: Vec, + /// The package to install commands from. /// /// This option is provided for parity with `uv tool run`, but is redundant with `package`. @@ -3668,14 +3803,15 @@ pub enum PythonCommand { /// /// Multiple Python versions may be requested. /// - /// Supports CPython and PyPy. + /// Supports CPython and PyPy. CPython distributions are downloaded from the + /// `python-build-standalone` project. PyPy distributions are downloaded from `python.org`. /// - /// CPython distributions are downloaded from the `python-build-standalone` project. + /// Python versions are installed into the uv Python directory, which can be retrieved with `uv + /// python dir`. /// - /// Python versions are installed into the uv Python directory, which can be - /// retrieved with `uv python dir`. A `python` executable is not made - /// globally available, managed Python versions are only used in uv - /// commands or in active virtual environments. + /// A `python` executable is not made globally available, managed Python versions are only used + /// in uv commands or in active virtual environments. There is experimental support for + /// adding Python executables to the `PATH` — use the `--preview` flag to enable this behavior. /// /// See `uv help python` to view supported request formats. Install(PythonInstallArgs), @@ -3703,7 +3839,10 @@ pub enum PythonCommand { /// `%APPDATA%\uv\data\python` on Windows. /// /// The Python installation directory may be overridden with `$UV_PYTHON_INSTALL_DIR`. - Dir, + /// + /// To view the directory where uv installs Python executables instead, use the `--bin` flag. + /// Note that Python executables are only installed when preview mode is enabled. + Dir(PythonDirArgs), /// Uninstall Python versions. Uninstall(PythonUninstallArgs), @@ -3731,6 +3870,24 @@ pub struct PythonListArgs { pub only_installed: bool, } +#[derive(Args)] +#[allow(clippy::struct_excessive_bools)] +pub struct PythonDirArgs { + /// Show the directory into which `uv python` will install Python executables. + /// + /// Note that this directory is only used when installing Python with preview mode enabled. + /// + /// The Python executable directory is determined according to the XDG standard and is derived + /// from the following environment variables, in order of preference: + /// + /// - `$UV_PYTHON_BIN_DIR` + /// - `$XDG_BIN_HOME` + /// - `$XDG_DATA_HOME/../bin` + /// - `$HOME/.local/bin` + #[arg(long, verbatim_doc_comment)] + pub bin: bool, +} + #[derive(Args)] #[allow(clippy::struct_excessive_bools)] pub struct PythonInstallArgs { @@ -4603,8 +4760,6 @@ pub struct PublishArgs { /// and index upload. /// /// Defaults to PyPI's publish URL (). - /// - /// The default value is publish URL for PyPI (). #[arg(long, env = EnvVars::UV_PUBLISH_URL)] pub publish_url: Option, @@ -4664,6 +4819,22 @@ pub struct PublishArgs { value_parser = parse_insecure_host, )] pub allow_insecure_host: Option>>, + + /// Check an index URL for existing files to skip duplicate uploads. + /// + /// This option allows retrying publishing that failed after only some, but not all files have + /// been uploaded, and handles error due to parallel uploads of the same file. + /// + /// Before uploading, the index is checked. If the exact same file already exists in the index, + /// the file will not be uploaded. If an error occurred during the upload, the index is checked + /// again, to handle cases where the identical file was uploaded twice in parallel. + /// + /// The exact behavior will vary based on the index. When uploading to PyPI, uploading the same + /// file succeeds even without `--check-url`, while most other indexes error. + /// + /// The index must provide one of the supported hashes (SHA-256, SHA-384, or SHA-512). + #[arg(long,env = EnvVars::UV_PUBLISH_CHECK_URL)] + pub check_url: Option, } /// See [PEP 517](https://peps.python.org/pep-0517/) and diff --git a/crates/uv-client/src/cached_client.rs b/crates/uv-client/src/cached_client.rs index f365227e9d79..56266c6f88e0 100644 --- a/crates/uv-client/src/cached_client.rs +++ b/crates/uv-client/src/cached_client.rs @@ -468,9 +468,9 @@ impl CachedClient { .execute(req) .instrument(info_span!("revalidation_request", url = url.as_str())) .await - .map_err(ErrorKind::from)? + .map_err(|err| ErrorKind::from_reqwest_middleware(url.clone(), err))? .error_for_status() - .map_err(ErrorKind::from)?; + .map_err(|err| ErrorKind::from_reqwest(url.clone(), err))?; match cached .cache_policy .after_response(new_cache_policy_builder, &response) @@ -500,16 +500,17 @@ impl CachedClient { &self, req: Request, ) -> Result<(Response, Option>), Error> { - trace!("Sending fresh {} request for {}", req.method(), req.url()); + let url = req.url().clone(); + trace!("Sending fresh {} request for {}", req.method(), url); let cache_policy_builder = CachePolicyBuilder::new(&req); let response = self .0 - .for_host(req.url()) + .for_host(&url) .execute(req) .await - .map_err(ErrorKind::from)? + .map_err(|err| ErrorKind::from_reqwest_middleware(url.clone(), err))? .error_for_status() - .map_err(ErrorKind::from)?; + .map_err(|err| ErrorKind::from_reqwest(url.clone(), err))?; let cache_policy = cache_policy_builder.build(&response); let cache_policy = if cache_policy.to_archived().is_storable() { Some(Box::new(cache_policy)) diff --git a/crates/uv-client/src/error.rs b/crates/uv-client/src/error.rs index cded0db494b7..f55d351814e3 100644 --- a/crates/uv-client/src/error.rs +++ b/crates/uv-client/src/error.rs @@ -73,7 +73,7 @@ impl Error { // The server returned a "Method Not Allowed" error, indicating it doesn't support // HEAD requests, so we can't check for range requests. - ErrorKind::WrappedReqwestError(err) => { + ErrorKind::WrappedReqwestError(_url, err) => { if let Some(status) = err.status() { // If the server doesn't support HEAD requests, we can't check for range // requests. @@ -180,8 +180,8 @@ pub enum ErrorKind { MetadataNotFound(WheelFilename, String), /// An error that happened while making a request or in a reqwest middleware. - #[error(transparent)] - WrappedReqwestError(#[from] WrappedReqwestError), + #[error("Failed to fetch: `{0}`")] + WrappedReqwestError(Url, #[source] WrappedReqwestError), #[error("Received some unexpected JSON from {url}")] BadJson { source: serde_json::Error, url: Url }, @@ -208,7 +208,7 @@ pub enum ErrorKind { CacheWrite(#[source] std::io::Error), #[error(transparent)] - Io(#[from] std::io::Error), + Io(std::io::Error), #[error("Cache deserialization failed")] Decode(#[source] rmp_serde::decode::Error), @@ -235,21 +235,19 @@ pub enum ErrorKind { Offline(String), } -impl From for ErrorKind { - fn from(error: reqwest::Error) -> Self { - Self::WrappedReqwestError(WrappedReqwestError::from(error)) +impl ErrorKind { + pub(crate) fn from_reqwest(url: Url, error: reqwest::Error) -> Self { + Self::WrappedReqwestError(url, WrappedReqwestError::from(error)) } -} -impl From for ErrorKind { - fn from(err: reqwest_middleware::Error) -> Self { + pub(crate) fn from_reqwest_middleware(url: Url, err: reqwest_middleware::Error) -> Self { if let reqwest_middleware::Error::Middleware(ref underlying) = err { if let Some(err) = underlying.downcast_ref::() { return Self::Offline(err.url().to_string()); } } - Self::WrappedReqwestError(WrappedReqwestError(err)) + Self::WrappedReqwestError(url, WrappedReqwestError(err)) } } diff --git a/crates/uv-client/src/flat_index.rs b/crates/uv-client/src/flat_index.rs index 9b9d6fe618db..ef7caf7fd17d 100644 --- a/crates/uv-client/src/flat_index.rs +++ b/crates/uv-client/src/flat_index.rs @@ -160,14 +160,17 @@ impl<'a> FlatIndexClient<'a> { .header("Accept-Encoding", "gzip") .header("Accept", "text/html") .build() - .map_err(ErrorKind::from)?; + .map_err(|err| ErrorKind::from_reqwest(url.clone(), err))?; let parse_simple_response = |response: Response| { async { // Use the response URL, rather than the request URL, as the base for relative URLs. // This ensures that we handle redirects and other URL transformations correctly. let url = response.url().clone(); - let text = response.text().await.map_err(ErrorKind::from)?; + let text = response + .text() + .await + .map_err(|err| ErrorKind::from_reqwest(url.clone(), err))?; let SimpleHtml { base, files } = SimpleHtml::parse(&text, &url) .map_err(|err| Error::from_html_err(err, url.clone()))?; diff --git a/crates/uv-client/src/registry_client.rs b/crates/uv-client/src/registry_client.rs index 2930201ceefc..0052b92d0515 100644 --- a/crates/uv-client/src/registry_client.rs +++ b/crates/uv-client/src/registry_client.rs @@ -31,7 +31,7 @@ use crate::cached_client::CacheControl; use crate::html::SimpleHtml; use crate::remote_metadata::wheel_metadata_from_remote_zip; use crate::rkyvutil::OwnedArchive; -use crate::{CachedClient, CachedClientError, Error, ErrorKind}; +use crate::{BaseClient, CachedClient, CachedClientError, Error, ErrorKind}; /// A builder for an [`RegistryClient`]. #[derive(Debug, Clone)] @@ -143,6 +143,27 @@ impl<'a> RegistryClientBuilder<'a> { timeout, } } + + /// Share the underlying client between two different middleware configurations. + pub fn wrap_existing(self, existing: &BaseClient) -> RegistryClient { + // Wrap in any relevant middleware and handle connectivity. + let client = self.base_client_builder.wrap_existing(existing); + + let timeout = client.timeout(); + let connectivity = client.connectivity(); + + // Wrap in the cache middleware. + let client = CachedClient::new(client); + + RegistryClient { + index_urls: self.index_urls, + index_strategy: self.index_strategy, + cache: self.cache, + connectivity, + client, + timeout, + } + } } impl<'a> TryFrom> for RegistryClientBuilder<'a> { @@ -232,7 +253,7 @@ impl RegistryClient { } Err(err) => match err.into_kind() { // The package could not be found in the remote index. - ErrorKind::WrappedReqwestError(err) => match err.status() { + ErrorKind::WrappedReqwestError(url, err) => match err.status() { Some(StatusCode::NOT_FOUND) => {} Some(StatusCode::UNAUTHORIZED) => { capabilities.set_unauthorized(index.clone()); @@ -240,7 +261,7 @@ impl RegistryClient { Some(StatusCode::FORBIDDEN) => { capabilities.set_forbidden(index.clone()); } - _ => return Err(ErrorKind::from(err).into()), + _ => return Err(ErrorKind::WrappedReqwestError(url, err).into()), }, // The package is unavailable due to a lack of connectivity. @@ -323,7 +344,7 @@ impl RegistryClient { .header("Accept-Encoding", "gzip") .header("Accept", MediaType::accepts()) .build() - .map_err(ErrorKind::from)?; + .map_err(|err| ErrorKind::from_reqwest(url.clone(), err))?; let parse_simple_response = |response: Response| { async { // Use the response URL, rather than the request URL, as the base for relative URLs. @@ -347,14 +368,20 @@ impl RegistryClient { let unarchived = match media_type { MediaType::Json => { - let bytes = response.bytes().await.map_err(ErrorKind::from)?; + let bytes = response + .bytes() + .await + .map_err(|err| ErrorKind::from_reqwest(url.clone(), err))?; let data: SimpleJson = serde_json::from_slice(bytes.as_ref()) .map_err(|err| Error::from_json_err(err, url.clone()))?; SimpleMetadata::from_files(data.files, package_name, &url) } MediaType::Html => { - let text = response.text().await.map_err(ErrorKind::from)?; + let text = response + .text() + .await + .map_err(|err| ErrorKind::from_reqwest(url.clone(), err))?; SimpleMetadata::from_html(&text, package_name, &url)? } }; @@ -547,7 +574,10 @@ impl RegistryClient { }; let response_callback = |response: Response| async { - let bytes = response.bytes().await.map_err(ErrorKind::from)?; + let bytes = response + .bytes() + .await + .map_err(|err| ErrorKind::from_reqwest(url.clone(), err))?; info_span!("parse_metadata21") .in_scope(|| ResolutionMetadata::parse_metadata(bytes.as_ref())) @@ -563,7 +593,7 @@ impl RegistryClient { .uncached_client(&url) .get(url.clone()) .build() - .map_err(ErrorKind::from)?; + .map_err(|err| ErrorKind::from_reqwest(url.clone(), err))?; Ok(self .cached_client() .get_serde(req, &cache_entry, cache_control, response_callback) @@ -616,7 +646,7 @@ impl RegistryClient { http::HeaderValue::from_static("identity"), ) .build() - .map_err(ErrorKind::from)?; + .map_err(|err| ErrorKind::from_reqwest(url.clone(), err))?; // Copy authorization headers from the HEAD request to subsequent requests let mut headers = HeaderMap::default(); @@ -694,7 +724,7 @@ impl RegistryClient { reqwest::header::HeaderValue::from_static("identity"), ) .build() - .map_err(ErrorKind::from)?; + .map_err(|err| ErrorKind::from_reqwest(url.clone(), err))?; // Stream the file, searching for the METADATA. let read_metadata_stream = |response: Response| { diff --git a/crates/uv-client/src/tls.rs b/crates/uv-client/src/tls.rs index 41aef9613bca..8a807234b2fa 100644 --- a/crates/uv-client/src/tls.rs +++ b/crates/uv-client/src/tls.rs @@ -7,12 +7,15 @@ pub(crate) enum CertificateError { #[error(transparent)] Io(#[from] std::io::Error), #[error(transparent)] - Reqwest(#[from] reqwest::Error), + Reqwest(reqwest::Error), } /// Return the `Identity` from the provided file. pub(crate) fn read_identity(ssl_client_cert: &OsStr) -> Result { let mut buf = Vec::new(); fs_err::File::open(ssl_client_cert)?.read_to_end(&mut buf)?; - Ok(Identity::from_pem(&buf)?) + Identity::from_pem(&buf).map_err(|tls_err| { + debug_assert!(tls_err.is_builder(), "must be a rustls::Error internally"); + CertificateError::Reqwest(tls_err) + }) } diff --git a/crates/uv-configuration/src/dev.rs b/crates/uv-configuration/src/dev.rs index ab11c55fadca..1c55eaa14d9b 100644 --- a/crates/uv-configuration/src/dev.rs +++ b/crates/uv-configuration/src/dev.rs @@ -1,5 +1,7 @@ +use std::borrow::Cow; + use either::Either; -use uv_normalize::GroupName; +use uv_normalize::{GroupName, DEV_DEPENDENCIES}; #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] pub enum DevMode { @@ -13,41 +15,344 @@ pub enum DevMode { } impl DevMode { - /// Determine the [`DevMode`] policy from the command-line arguments. - pub fn from_args(dev: bool, no_dev: bool, only_dev: bool) -> Self { - if only_dev { - Self::Only + /// Returns `true` if the specification allows for production dependencies. + pub fn prod(&self) -> bool { + matches!(self, Self::Exclude | Self::Include) + } + + /// Returns `true` if the specification only includes development dependencies. + pub fn only(&self) -> bool { + matches!(self, Self::Only) + } + + /// Returns the flag that was used to request development dependencies. + pub fn as_flag(&self) -> &'static str { + match self { + Self::Exclude => "--no-dev", + Self::Include => "--dev", + Self::Only => "--only-dev", + } + } + + /// Iterate over the group names to include. + pub fn iter(&self) -> impl Iterator { + <&Self as IntoIterator>::into_iter(self) + } +} + +impl<'a> IntoIterator for &'a DevMode { + type Item = &'a GroupName; + type IntoIter = Either, std::iter::Once<&'a GroupName>>; + + fn into_iter(self) -> Self::IntoIter { + match self { + DevMode::Exclude => Either::Left(std::iter::empty()), + DevMode::Include | DevMode::Only => Either::Right(std::iter::once(&*DEV_DEPENDENCIES)), + } + } +} + +#[derive(Default, Debug, Clone)] +pub struct DevGroupsSpecification { + /// Legacy option for `dependency-group.dev` and `tool.uv.dev-dependencies`. + /// + /// Requested via the `--dev`, `--no-dev`, and `--only-dev` flags. + dev: Option, + + /// The groups to include. + /// + /// Requested via the `--group` and `--only-group` options. + groups: Option, +} + +#[derive(Debug, Clone)] +pub enum GroupsSpecification { + /// Include dependencies from the specified groups. + /// + /// The `include` list is guaranteed to omit groups in the `exclude` list (i.e., they have an + /// empty intersection). + Include { + include: Vec, + exclude: Vec, + }, + /// Only include dependencies from the specified groups, exclude all other dependencies. + /// + /// The `include` list is guaranteed to omit groups in the `exclude` list (i.e., they have an + /// empty intersection). + Only { + include: Vec, + exclude: Vec, + }, +} + +impl GroupsSpecification { + /// Create a [`GroupsSpecification`] that includes the given group. + pub fn from_group(group: GroupName) -> Self { + Self::Include { + include: vec![group], + exclude: Vec::new(), + } + } + + /// Returns `true` if the specification allows for production dependencies. + pub fn prod(&self) -> bool { + matches!(self, Self::Include { .. }) + } + + /// Returns `true` if the specification is limited to a select set of groups. + pub fn only(&self) -> bool { + matches!(self, Self::Only { .. }) + } + + /// Returns the option that was used to request the groups, if any. + pub fn as_flag(&self) -> Option> { + match self { + Self::Include { include, exclude } => match include.as_slice() { + [] => match exclude.as_slice() { + [] => None, + [group] => Some(Cow::Owned(format!("--no-group {group}"))), + [..] => Some(Cow::Borrowed("--no-group")), + }, + [group] => Some(Cow::Owned(format!("--group {group}"))), + [..] => Some(Cow::Borrowed("--group")), + }, + Self::Only { include, exclude } => match include.as_slice() { + [] => match exclude.as_slice() { + [] => None, + [group] => Some(Cow::Owned(format!("--no-group {group}"))), + [..] => Some(Cow::Borrowed("--no-group")), + }, + [group] => Some(Cow::Owned(format!("--only-group {group}"))), + [..] => Some(Cow::Borrowed("--only-group")), + }, + } + } + + /// Iterate over all groups referenced in the [`DevGroupsSpecification`]. + pub fn names(&self) -> impl Iterator { + match self { + GroupsSpecification::Include { include, exclude } + | GroupsSpecification::Only { include, exclude } => { + include.iter().chain(exclude.iter()) + } + } + } + + /// Iterate over the group names to include. + pub fn iter(&self) -> impl Iterator { + <&Self as IntoIterator>::into_iter(self) + } +} + +impl<'a> IntoIterator for &'a GroupsSpecification { + type Item = &'a GroupName; + type IntoIter = std::slice::Iter<'a, GroupName>; + + fn into_iter(self) -> Self::IntoIter { + match self { + GroupsSpecification::Include { + include, + exclude: _, + } + | GroupsSpecification::Only { + include, + exclude: _, + } => include.iter(), + } + } +} + +impl DevGroupsSpecification { + /// Determine the [`DevGroupsSpecification`] policy from the command-line arguments. + pub fn from_args( + dev: bool, + no_dev: bool, + only_dev: bool, + mut group: Vec, + no_group: Vec, + mut only_group: Vec, + ) -> Self { + let dev = if only_dev { + Some(DevMode::Only) } else if no_dev { - Self::Exclude + Some(DevMode::Exclude) } else if dev { - Self::Include + Some(DevMode::Include) + } else { + None + }; + + let groups = if !group.is_empty() { + if matches!(dev, Some(DevMode::Only)) { + unreachable!("cannot specify both `--only-dev` and `--group`") + }; + + // Ensure that `--no-group` and `--group` are mutually exclusive. + group.retain(|group| !no_group.contains(group)); + + Some(GroupsSpecification::Include { + include: group, + exclude: no_group, + }) + } else if !only_group.is_empty() { + if matches!(dev, Some(DevMode::Include)) { + unreachable!("cannot specify both `--dev` and `--only-group`") + }; + + // Ensure that `--no-group` and `--only-group` are mutually exclusive. + only_group.retain(|group| !no_group.contains(group)); + + Some(GroupsSpecification::Only { + include: only_group, + exclude: no_group, + }) + } else if !no_group.is_empty() { + Some(GroupsSpecification::Include { + include: Vec::new(), + exclude: no_group, + }) } else { - Self::default() + None + }; + + Self { dev, groups } + } + + /// Return a new [`DevGroupsSpecification`] with development dependencies included by default. + /// + /// This is appropriate in projects, where the `dev` group is synced by default. + #[must_use] + pub fn with_defaults(self, defaults: Vec) -> DevGroupsManifest { + DevGroupsManifest { + spec: self, + defaults, } } -} -#[derive(Debug, Copy, Clone)] -pub enum DevSpecification<'group> { - /// Include dev dependencies from the specified group. - Include(&'group [GroupName]), - /// Do not include dev dependencies. - Exclude, - /// Include dev dependencies from the specified group, and exclude all non-dev dependencies. - Only(&'group [GroupName]), -} + /// Returns `true` if the specification allows for production dependencies. + pub fn prod(&self) -> bool { + self.dev.as_ref().map_or(true, DevMode::prod) + && self.groups.as_ref().map_or(true, GroupsSpecification::prod) + } + + /// Returns `true` if the specification is limited to a select set of groups. + pub fn only(&self) -> bool { + self.dev.as_ref().is_some_and(DevMode::only) + || self.groups.as_ref().is_some_and(GroupsSpecification::only) + } + + /// Returns the flag that was used to request development dependencies, if specified. + pub fn dev_mode(&self) -> Option<&DevMode> { + self.dev.as_ref() + } + + /// Returns the list of groups to include, if specified. + pub fn groups(&self) -> Option<&GroupsSpecification> { + self.groups.as_ref() + } -impl<'group> DevSpecification<'group> { /// Returns an [`Iterator`] over the group names to include. pub fn iter(&self) -> impl Iterator { - match self { - Self::Exclude => Either::Left(std::iter::empty()), - Self::Include(groups) | Self::Only(groups) => Either::Right(groups.iter()), + <&Self as IntoIterator>::into_iter(self) + } +} + +impl<'a> IntoIterator for &'a DevGroupsSpecification { + type Item = &'a GroupName; + type IntoIter = std::iter::Chain< + std::iter::Flatten>, + std::iter::Flatten>, + >; + + fn into_iter(self) -> Self::IntoIter { + self.dev + .as_ref() + .into_iter() + .flatten() + .chain(self.groups.as_ref().into_iter().flatten()) + } +} + +impl From for DevGroupsSpecification { + fn from(dev: DevMode) -> Self { + Self { + dev: Some(dev), + groups: None, + } + } +} + +impl From for DevGroupsSpecification { + fn from(groups: GroupsSpecification) -> Self { + Self { + dev: None, + groups: Some(groups), + } + } +} + +/// The manifest of `dependency-groups` to include, taking into account the user-provided +/// [`DevGroupsSpecification`] and the project-specific default groups. +#[derive(Debug, Clone)] +pub struct DevGroupsManifest { + /// The specification for the development dependencies. + pub(crate) spec: DevGroupsSpecification, + /// The default groups to include. + pub(crate) defaults: Vec, +} + +impl DevGroupsManifest { + /// Returns a new [`DevGroupsManifest`] with the given default groups. + pub fn from_defaults(defaults: Vec) -> Self { + Self { + spec: DevGroupsSpecification::default(), + defaults, + } + } + + /// Returns a new [`DevGroupsManifest`] with the given specification. + pub fn from_spec(spec: DevGroupsSpecification) -> Self { + Self { + spec, + defaults: Vec::new(), } } /// Returns `true` if the specification allows for production dependencies. pub fn prod(&self) -> bool { - matches!(self, Self::Exclude | Self::Include(_)) + self.spec.prod() + } + + /// Returns an [`Iterator`] over the group names to include. + pub fn iter(&self) -> impl Iterator { + if self.spec.only() { + Either::Left(self.spec.iter()) + } else { + Either::Right( + self.spec + .iter() + .chain(self.defaults.iter().filter(|default| { + // If `--no-dev` was provided, exclude the `dev` group from the list of defaults. + if matches!(self.spec.dev_mode(), Some(DevMode::Exclude)) { + if *default == &*DEV_DEPENDENCIES { + return false; + }; + } + + // If `--no-group` was provided, exclude the group from the list of defaults. + if let Some(GroupsSpecification::Include { + include: _, + exclude, + }) = self.spec.groups() + { + if exclude.contains(default) { + return false; + } + } + + true + })), + ) + } } } diff --git a/crates/uv-dev/src/generate_cli_reference.rs b/crates/uv-dev/src/generate_cli_reference.rs index 854e5c5faafa..ec9a2eff5ccb 100644 --- a/crates/uv-dev/src/generate_cli_reference.rs +++ b/crates/uv-dev/src/generate_cli_reference.rs @@ -100,7 +100,14 @@ fn generate() -> String { generate_command(&mut output, &uv, &mut parents); for (value, replacement) in REPLACEMENTS { - output = output.replace(value, replacement); + assert_ne!( + value, replacement, + "`value` and `replacement` must be different, but both are `{value}`" + ); + let before = &output; + let after = output.replace(value, replacement); + assert_ne!(*before, after, "Could not find `{value}` in the output"); + output = after; } output diff --git a/crates/uv-dev/src/generate_env_vars_reference.rs b/crates/uv-dev/src/generate_env_vars_reference.rs index 5abf2c9ba74a..222c49cb9abd 100644 --- a/crates/uv-dev/src/generate_env_vars_reference.rs +++ b/crates/uv-dev/src/generate_env_vars_reference.rs @@ -83,7 +83,7 @@ fn generate() -> String { if i == 0 { line.to_string() } else { - format!(" {}", line) + format!(" {line}") } }) .collect::>() diff --git a/crates/uv-dev/src/generate_json_schema.rs b/crates/uv-dev/src/generate_json_schema.rs index 5e7ccfdf8ef8..fcf112587ca4 100644 --- a/crates/uv-dev/src/generate_json_schema.rs +++ b/crates/uv-dev/src/generate_json_schema.rs @@ -32,8 +32,8 @@ pub(crate) struct Args { } pub(crate) fn main(args: &Args) -> Result<()> { - let schema = schema_for!(CombinedOptions); - let schema_string = serde_json::to_string_pretty(&schema).unwrap(); + // Generate the schema. + let schema_string = generate(); let filename = "uv.schema.json"; let schema_path = PathBuf::from(ROOT_DIR).join(filename); @@ -79,5 +79,32 @@ pub(crate) fn main(args: &Args) -> Result<()> { Ok(()) } +const REPLACEMENTS: &[(&str, &str)] = &[ + // Use the fully-resolved URL rather than the relative Markdown path. + ( + "(../concepts/dependencies.md)", + "(https://docs.astral.sh/uv/concepts/dependencies/)", + ), +]; + +/// Generate the JSON schema for the combined options as a string. +fn generate() -> String { + let schema = schema_for!(CombinedOptions); + let mut output = serde_json::to_string_pretty(&schema).unwrap(); + + for (value, replacement) in REPLACEMENTS { + assert_ne!( + value, replacement, + "`value` and `replacement` must be different, but both are `{value}`" + ); + let before = &output; + let after = output.replace(value, replacement); + assert_ne!(*before, after, "Could not find `{value}` in the output"); + output = after; + } + + output +} + #[cfg(test)] mod tests; diff --git a/crates/uv-dev/src/generate_options_reference.rs b/crates/uv-dev/src/generate_options_reference.rs index 535243997cbc..2f97cca5c61a 100644 --- a/crates/uv-dev/src/generate_options_reference.rs +++ b/crates/uv-dev/src/generate_options_reference.rs @@ -267,7 +267,12 @@ fn emit_field(output: &mut String, name: &str, field: &OptionField, parents: &[S } => { output.push_str(&format_code( "pyproject.toml", - &format_header(field.scope, parents, ConfigurationFile::PyprojectToml), + &format_header( + field.scope, + field.example, + parents, + ConfigurationFile::PyprojectToml, + ), field.example, )); } @@ -277,12 +282,22 @@ fn emit_field(output: &mut String, name: &str, field: &OptionField, parents: &[S } => { output.push_str(&format_tab( "pyproject.toml", - &format_header(field.scope, parents, ConfigurationFile::PyprojectToml), + &format_header( + field.scope, + field.example, + parents, + ConfigurationFile::PyprojectToml, + ), field.example, )); output.push_str(&format_tab( "uv.toml", - &format_header(field.scope, parents, ConfigurationFile::UvToml), + &format_header( + field.scope, + field.example, + parents, + ConfigurationFile::UvToml, + ), field.example, )); } @@ -292,12 +307,20 @@ fn emit_field(output: &mut String, name: &str, field: &OptionField, parents: &[S } fn format_tab(tab_name: &str, header: &str, content: &str) -> String { - format!( - "=== \"{}\"\n\n ```toml\n {}\n{}\n ```\n", - tab_name, - header, - textwrap::indent(content, " ") - ) + if header.is_empty() { + format!( + "=== \"{}\"\n\n ```toml\n{}\n ```\n", + tab_name, + textwrap::indent(content, " ") + ) + } else { + format!( + "=== \"{}\"\n\n ```toml\n {}\n{}\n ```\n", + tab_name, + header, + textwrap::indent(content, " ") + ) + } } fn format_code(file_name: &str, header: &str, content: &str) -> String { @@ -307,7 +330,12 @@ fn format_code(file_name: &str, header: &str, content: &str) -> String { /// Format the TOML header for the example usage for a given option. /// /// For example: `[tool.uv.pip]`. -fn format_header(scope: Option<&str>, parents: &[Set], configuration: ConfigurationFile) -> String { +fn format_header( + scope: Option<&str>, + example: &str, + parents: &[Set], + configuration: ConfigurationFile, +) -> String { let tool_parent = match configuration { ConfigurationFile::PyprojectToml => Some("tool.uv"), ConfigurationFile::UvToml => None, @@ -319,6 +347,15 @@ fn format_header(scope: Option<&str>, parents: &[Set], configuration: Configurat .chain(scope) .join("."); + // Ex) `[[tool.uv.index]]` + if example.starts_with(&format!("[[{header}")) { + return String::new(); + } + // Ex) `[tool.uv.sources]` + if example.starts_with(&format!("[{header}")) { + return String::new(); + } + if header.is_empty() { String::new() } else { diff --git a/crates/uv-dirs/Cargo.toml b/crates/uv-dirs/Cargo.toml new file mode 100644 index 000000000000..d32a4a4e3ccf --- /dev/null +++ b/crates/uv-dirs/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "uv-dirs" +version = "0.0.1" +description = "Resolution of directories for storage of uv state" +edition = { workspace = true } +rust-version = { workspace = true } +homepage = { workspace = true } +documentation = { workspace = true } +repository = { workspace = true } +authors = { workspace = true } +license = { workspace = true } + +[lib] +doctest = false + +[lints] +workspace = true + +[dependencies] +uv-static = { workspace = true } + +dirs-sys = { workspace = true } +directories = { workspace = true } +etcetera = { workspace = true } diff --git a/crates/uv-dirs/src/lib.rs b/crates/uv-dirs/src/lib.rs new file mode 100644 index 000000000000..759a4df75dd2 --- /dev/null +++ b/crates/uv-dirs/src/lib.rs @@ -0,0 +1,72 @@ +use std::path::PathBuf; + +use etcetera::BaseStrategy; + +use uv_static::EnvVars; + +/// Returns an appropriate user-level directory for storing executables. +/// +/// This follows, in order: +/// +/// - `$OVERRIDE_VARIABLE` (if provided) +/// - `$XDG_BIN_HOME` +/// - `$XDG_DATA_HOME/../bin` +/// - `$HOME/.local/bin` +/// +/// On all platforms. +/// +/// Returns `None` if a directory cannot be found, i.e., if `$HOME` cannot be resolved. Does not +/// check if the directory exists. +pub fn user_executable_directory(override_variable: Option<&'static str>) -> Option { + override_variable + .and_then(std::env::var_os) + .and_then(dirs_sys::is_absolute_path) + .or_else(|| std::env::var_os(EnvVars::XDG_BIN_HOME).and_then(dirs_sys::is_absolute_path)) + .or_else(|| { + std::env::var_os(EnvVars::XDG_DATA_HOME) + .and_then(dirs_sys::is_absolute_path) + .map(|path| path.join("../bin")) + }) + .or_else(|| { + // See https://github.com/dirs-dev/dirs-rs/blob/50b50f31f3363b7656e5e63b3fa1060217cbc844/src/win.rs#L5C58-L5C78 + #[cfg(windows)] + let home_dir = dirs_sys::known_folder_profile(); + #[cfg(not(windows))] + let home_dir = dirs_sys::home_dir(); + home_dir.map(|path| path.join(".local").join("bin")) + }) +} + +/// Returns an appropriate user-level directory for storing the cache. +/// +/// Corresponds to `$XDG_CACHE_HOME/uv` on Unix. +pub fn user_cache_dir() -> Option { + etcetera::base_strategy::choose_base_strategy() + .ok() + .map(|dirs| dirs.cache_dir().join("uv")) +} + +/// Returns the legacy cache directory path. +/// +/// Uses `/Users/user/Library/Application Support/uv` on macOS, in contrast to the new preference +/// for using the XDG directories on all Unix platforms. +pub fn legacy_user_cache_dir() -> Option { + directories::ProjectDirs::from("", "", "uv").map(|dirs| dirs.cache_dir().to_path_buf()) +} + +/// Returns an appropriate user-level directory for storing application state. +/// +/// Corresponds to `$XDG_DATA_HOME/uv` on Unix. +pub fn user_state_dir() -> Option { + etcetera::base_strategy::choose_base_strategy() + .ok() + .map(|dirs| dirs.data_dir().join("uv")) +} + +/// Returns the legacy state directory path. +/// +/// Uses `/Users/user/Library/Application Support/uv` on macOS, in contrast to the new preference +/// for using the XDG directories on all Unix platforms. +pub fn legacy_user_state_dir() -> Option { + directories::ProjectDirs::from("", "", "uv").map(|dirs| dirs.data_dir().to_path_buf()) +} diff --git a/crates/uv-distribution-types/src/dependency_metadata.rs b/crates/uv-distribution-types/src/dependency_metadata.rs index f4d2e8bda0f9..2380cd8fadd4 100644 --- a/crates/uv-distribution-types/src/dependency_metadata.rs +++ b/crates/uv-distribution-types/src/dependency_metadata.rs @@ -1,5 +1,6 @@ use rustc_hash::FxHashMap; use serde::{Deserialize, Serialize}; +use tracing::{debug, warn}; use uv_normalize::{ExtraName, PackageName}; use uv_pep440::{Version, VersionSpecifiers}; use uv_pep508::Requirement; @@ -20,22 +21,57 @@ impl DependencyMetadata { } /// Retrieve a [`StaticMetadata`] entry by [`PackageName`] and [`Version`]. - pub fn get(&self, package: &PackageName, version: &Version) -> Option { + pub fn get( + &self, + package: &PackageName, + version: Option<&Version>, + ) -> Option { let versions = self.0.get(package)?; - // Search for an exact, then a global match. - let metadata = versions - .iter() - .find(|v| v.version.as_ref() == Some(version)) - .or_else(|| versions.iter().find(|v| v.version.is_none()))?; - - Some(ResolutionMetadata { - name: metadata.name.clone(), - version: version.clone(), - requires_dist: metadata.requires_dist.clone(), - requires_python: metadata.requires_python.clone(), - provides_extras: metadata.provides_extras.clone(), - }) + if let Some(version) = version { + // If a specific version was requested, search for an exact match, then a global match. + let metadata = versions + .iter() + .find(|v| v.version.as_ref() == Some(version)) + .inspect(|_| { + debug!("Found dependency metadata entry for `{package}=={version}`",); + }) + .or_else(|| versions.iter().find(|v| v.version.is_none())) + .inspect(|_| { + debug!("Found global metadata entry for `{package}`",); + }); + let Some(metadata) = metadata else { + warn!("No dependency metadata entry found for `{package}=={version}`"); + return None; + }; + debug!("Found dependency metadata entry for `{package}=={version}`",); + Some(ResolutionMetadata { + name: metadata.name.clone(), + version: version.clone(), + requires_dist: metadata.requires_dist.clone(), + requires_python: metadata.requires_python.clone(), + provides_extras: metadata.provides_extras.clone(), + }) + } else { + // If no version was requested (i.e., it's a direct URL dependency), allow a single + // versioned match. + let [metadata] = versions.as_slice() else { + warn!("Multiple dependency metadata entries found for `{package}`"); + return None; + }; + let Some(version) = metadata.version.clone() else { + warn!("No version found in dependency metadata entry for `{package}`"); + return None; + }; + debug!("Found dependency metadata entry for `{package}` (assuming: `{version}`)"); + Some(ResolutionMetadata { + name: metadata.name.clone(), + version, + requires_dist: metadata.requires_dist.clone(), + requires_python: metadata.requires_python.clone(), + provides_extras: metadata.provides_extras.clone(), + }) + } } /// Retrieve all [`StaticMetadata`] entries. diff --git a/crates/uv-distribution-types/src/index.rs b/crates/uv-distribution-types/src/index.rs index 0d04c9a517f9..3450757ab828 100644 --- a/crates/uv-distribution-types/src/index.rs +++ b/crates/uv-distribution-types/src/index.rs @@ -1,8 +1,11 @@ use std::str::FromStr; + use thiserror::Error; use url::Url; + use uv_auth::Credentials; +use crate::index_name::{IndexName, IndexNameError}; use crate::origin::Origin; use crate::{IndexUrl, IndexUrlError}; @@ -22,7 +25,7 @@ pub struct Index { /// [tool.uv.sources] /// torch = { index = "pytorch" } /// ``` - pub name: Option, + pub name: Option, /// The URL of the index. /// /// Expects to receive a URL (e.g., `https://pypi.org/simple`) or a local path. @@ -137,8 +140,8 @@ impl Index { /// Retrieve the credentials for the index, either from the environment, or from the URL itself. pub fn credentials(&self) -> Option { // If the index is named, and credentials are provided via the environment, prefer those. - if let Some(name) = self.name.as_deref() { - if let Some(credentials) = Credentials::from_env(name) { + if let Some(name) = self.name.as_ref() { + if let Some(credentials) = Credentials::from_env(name.to_env_var()) { return Some(credentials); } } @@ -154,17 +157,11 @@ impl FromStr for Index { fn from_str(s: &str) -> Result { // Determine whether the source is prefixed with a name, as in `name=https://pypi.org/simple`. if let Some((name, url)) = s.split_once('=') { - if name.is_empty() { - return Err(IndexSourceError::EmptyName); - } - - if name - .chars() - .all(|c| c.is_alphanumeric() || c == '-' || c == '_') - { + if !name.chars().any(|c| c == ':') { + let name = IndexName::from_str(name)?; let url = IndexUrl::from_str(url)?; return Ok(Self { - name: Some(name.to_string()), + name: Some(name), url, explicit: false, default: false, @@ -190,6 +187,8 @@ impl FromStr for Index { pub enum IndexSourceError { #[error(transparent)] Url(#[from] IndexUrlError), + #[error(transparent)] + IndexName(#[from] IndexNameError), #[error("Index included a name, but the name was empty")] EmptyName, } diff --git a/crates/uv-distribution-types/src/index_name.rs b/crates/uv-distribution-types/src/index_name.rs new file mode 100644 index 000000000000..017a1ca0424f --- /dev/null +++ b/crates/uv-distribution-types/src/index_name.rs @@ -0,0 +1,94 @@ +use std::ops::Deref; +use std::str::FromStr; + +use thiserror::Error; + +/// The normalized name of an index. +/// +/// Index names may contain letters, digits, hyphens, underscores, and periods, and must be ASCII. +#[derive(Debug, Clone, Hash, Eq, PartialEq, serde::Serialize)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +pub struct IndexName(String); + +impl IndexName { + /// Validates the given index name and returns [`IndexName`] if it's valid, or an error + /// otherwise. + pub fn new(name: String) -> Result { + for c in name.chars() { + match c { + 'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' | '.' => {} + c if c.is_ascii() => { + return Err(IndexNameError::UnsupportedCharacter(c, name)); + } + c => { + return Err(IndexNameError::NonAsciiName(c, name)); + } + } + } + Ok(Self(name)) + } + + /// Converts the index name to an environment variable name. + /// + /// For example, given `IndexName("foo-bar")`, this will return `"FOO_BAR"`. + pub fn to_env_var(&self) -> String { + self.0 + .chars() + .map(|c| { + if c.is_ascii_alphanumeric() { + c.to_ascii_uppercase() + } else { + '_' + } + }) + .collect::() + } +} + +impl FromStr for IndexName { + type Err = IndexNameError; + + fn from_str(s: &str) -> Result { + Self::new(s.to_string()) + } +} + +impl<'de> serde::de::Deserialize<'de> for IndexName { + fn deserialize(deserializer: D) -> Result + where + D: serde::de::Deserializer<'de>, + { + IndexName::new(String::deserialize(deserializer)?).map_err(serde::de::Error::custom) + } +} + +impl std::fmt::Display for IndexName { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl AsRef for IndexName { + fn as_ref(&self) -> &str { + &self.0 + } +} + +impl Deref for IndexName { + type Target = str; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +/// An error that can occur when parsing an [`IndexName`]. +#[derive(Error, Debug)] +pub enum IndexNameError { + #[error("Index included a name, but the name was empty")] + EmptyName, + #[error("Index names may only contain letters, digits, hyphens, underscores, and periods, but found unsupported character (`{0}`) in: `{1}`")] + UnsupportedCharacter(char, String), + #[error("Index names must be ASCII, but found non-ASCII character (`{0}`) in: `{1}`")] + NonAsciiName(char, String), +} diff --git a/crates/uv-distribution-types/src/lib.rs b/crates/uv-distribution-types/src/lib.rs index b8c050baf780..ce5427b7695e 100644 --- a/crates/uv-distribution-types/src/lib.rs +++ b/crates/uv-distribution-types/src/lib.rs @@ -58,6 +58,7 @@ pub use crate::file::*; pub use crate::hash::*; pub use crate::id::*; pub use crate::index::*; +pub use crate::index_name::*; pub use crate::index_url::*; pub use crate::installed::*; pub use crate::origin::*; @@ -79,6 +80,7 @@ mod file; mod hash; mod id; mod index; +mod index_name; mod index_url; mod installed; mod origin; diff --git a/crates/uv-distribution/src/distribution_database.rs b/crates/uv-distribution/src/distribution_database.rs index 05db4ce8da11..f96154f33057 100644 --- a/crates/uv-distribution/src/distribution_database.rs +++ b/crates/uv-distribution/src/distribution_database.rs @@ -87,7 +87,8 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { io::Error::new( io::ErrorKind::TimedOut, format!( - "Failed to download distribution due to network timeout. Try increasing UV_HTTP_TIMEOUT (current value: {}s).", self.client.unmanaged.timeout().as_secs() + "Failed to download distribution due to network timeout. Try increasing UV_HTTP_TIMEOUT (current value: {}s).", + self.client.unmanaged.timeout().as_secs() ), ) } else { @@ -354,7 +355,7 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { /// /// While hashes will be generated in some cases, hash-checking is _not_ enforced and should /// instead be enforced by the caller. - pub async fn get_wheel_metadata( + async fn get_wheel_metadata( &self, dist: &BuiltDist, hashes: HashPolicy<'_>, @@ -363,7 +364,7 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { if let Some(metadata) = self .build_context .dependency_metadata() - .get(dist.name(), dist.version()) + .get(dist.name(), Some(dist.version())) { return Ok(ArchiveMetadata::from_metadata23(metadata.clone())); } @@ -425,14 +426,16 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { ) -> Result { // If the metadata was provided by the user directly, prefer it. if let Some(dist) = source.as_dist() { - if let Some(version) = dist.version() { - if let Some(metadata) = self - .build_context - .dependency_metadata() - .get(dist.name(), version) - { - return Ok(ArchiveMetadata::from_metadata23(metadata.clone())); - } + if let Some(metadata) = self + .build_context + .dependency_metadata() + .get(dist.name(), dist.version()) + { + // If we skipped the build, we should still resolve any Git dependencies to precise + // commits. + self.builder.resolve_revision(source, &self.client).await?; + + return Ok(ArchiveMetadata::from_metadata23(metadata.clone())); } } diff --git a/crates/uv-distribution/src/metadata/lowering.rs b/crates/uv-distribution/src/metadata/lowering.rs index 2efa9030997a..71a093f9f279 100644 --- a/crates/uv-distribution/src/metadata/lowering.rs +++ b/crates/uv-distribution/src/metadata/lowering.rs @@ -1,12 +1,14 @@ -use either::Either; use std::collections::BTreeMap; use std::io; use std::path::{Path, PathBuf}; + +use either::Either; use thiserror::Error; use url::Url; + use uv_configuration::LowerBound; use uv_distribution_filename::DistExtension; -use uv_distribution_types::{Index, IndexLocations, Origin}; +use uv_distribution_types::{Index, IndexLocations, IndexName, Origin}; use uv_git::GitReference; use uv_normalize::PackageName; use uv_pep440::VersionSpecifiers; @@ -16,6 +18,8 @@ use uv_warnings::warn_user_once; use uv_workspace::pyproject::{PyProjectToml, Source, Sources}; use uv_workspace::Workspace; +use crate::metadata::GitWorkspaceMember; + #[derive(Debug, Clone)] pub struct LoweredRequirement(Requirement); @@ -38,6 +42,7 @@ impl LoweredRequirement { locations: &'data IndexLocations, workspace: &'data Workspace, lower_bound: LowerBound, + git_member: Option<&'data GitWorkspaceMember<'data>>, ) -> impl Iterator> + 'data { let (source, origin) = if let Some(source) = project_sources.get(&requirement.name) { (Some(source), RequirementOrigin::Project) @@ -216,7 +221,24 @@ impl LoweredRequirement { )) })?; - let source = if member.pyproject_toml().is_package() { + let source = if let Some(git_member) = &git_member { + // If the workspace comes from a git dependency, all workspace + // members need to be git deps, too. + let subdirectory = + uv_fs::relative_to(member.root(), git_member.fetch_root) + .expect("Workspace member must be relative"); + RequirementSource::Git { + repository: git_member.git_source.git.repository().clone(), + reference: git_member.git_source.git.reference().clone(), + precise: git_member.git_source.git.precise(), + subdirectory: if subdirectory == PathBuf::new() { + None + } else { + Some(subdirectory) + }, + url, + } + } else if member.pyproject_toml().is_package() { RequirementSource::Directory { install_path, url, @@ -398,7 +420,7 @@ pub enum LoweringError { #[error("Can only specify one of: `rev`, `tag`, or `branch`")] MoreThanOneGitRef, #[error("Package `{0}` references an undeclared index: `{1}`")] - MissingIndex(PackageName, String), + MissingIndex(PackageName, IndexName), #[error("Workspace members are not allowed in non-workspace contexts")] WorkspaceMember, #[error(transparent)] diff --git a/crates/uv-distribution/src/metadata/mod.rs b/crates/uv-distribution/src/metadata/mod.rs index 141f777bf0fc..05ed1ec6aa6e 100644 --- a/crates/uv-distribution/src/metadata/mod.rs +++ b/crates/uv-distribution/src/metadata/mod.rs @@ -4,10 +4,11 @@ use std::path::Path; use thiserror::Error; use uv_configuration::{LowerBound, SourceStrategy}; -use uv_distribution_types::IndexLocations; +use uv_distribution_types::{GitSourceUrl, IndexLocations}; use uv_normalize::{ExtraName, GroupName, PackageName}; use uv_pep440::{Version, VersionSpecifiers}; use uv_pypi_types::{HashDigest, ResolutionMetadata}; +use uv_workspace::dependency_groups::DependencyGroupError; use uv_workspace::WorkspaceError; pub use crate::metadata::lowering::LoweredRequirement; @@ -21,10 +22,12 @@ mod requires_dist; pub enum MetadataError { #[error(transparent)] Workspace(#[from] WorkspaceError), - #[error("Failed to parse entry for: `{0}`")] - LoweringError(PackageName, #[source] LoweringError), #[error(transparent)] - Lower(#[from] LoweringError), + DependencyGroup(#[from] DependencyGroupError), + #[error("Failed to parse entry: `{0}`")] + LoweringError(PackageName, #[source] Box), + #[error("Failed to parse entry in group `{0}`: `{1}`")] + GroupLoweringError(GroupName, PackageName, #[source] Box), } #[derive(Debug, Clone)] @@ -36,7 +39,7 @@ pub struct Metadata { pub requires_dist: Vec, pub requires_python: Option, pub provides_extras: Vec, - pub dev_dependencies: BTreeMap>, + pub dependency_groups: BTreeMap>, } impl Metadata { @@ -53,7 +56,7 @@ impl Metadata { .collect(), requires_python: metadata.requires_python, provides_extras: metadata.provides_extras, - dev_dependencies: BTreeMap::default(), + dependency_groups: BTreeMap::default(), } } @@ -62,23 +65,26 @@ impl Metadata { pub async fn from_workspace( metadata: ResolutionMetadata, install_path: &Path, + git_source: Option<&GitWorkspaceMember<'_>>, locations: &IndexLocations, sources: SourceStrategy, bounds: LowerBound, ) -> Result { // Lower the requirements. + let requires_dist = uv_pypi_types::RequiresDist { + name: metadata.name, + requires_dist: metadata.requires_dist, + provides_extras: metadata.provides_extras, + }; let RequiresDist { name, requires_dist, provides_extras, - dev_dependencies, + dependency_groups, } = RequiresDist::from_project_maybe_workspace( - uv_pypi_types::RequiresDist { - name: metadata.name, - requires_dist: metadata.requires_dist, - provides_extras: metadata.provides_extras, - }, + requires_dist, install_path, + git_source, locations, sources, bounds, @@ -92,7 +98,7 @@ impl Metadata { requires_dist, requires_python: metadata.requires_python, provides_extras, - dev_dependencies, + dependency_groups, }) } } @@ -130,3 +136,12 @@ impl From for ArchiveMetadata { } } } + +/// A workspace member from a checked-out Git repo. +#[derive(Debug, Clone)] +pub struct GitWorkspaceMember<'a> { + /// The root of the checkout, which may be the root of the workspace or may be above the + /// workspace root. + pub fetch_root: &'a Path, + pub git_source: &'a GitSourceUrl<'a>, +} diff --git a/crates/uv-distribution/src/metadata/requires_dist.rs b/crates/uv-distribution/src/metadata/requires_dist.rs index 730c287b1eba..8814d571950c 100644 --- a/crates/uv-distribution/src/metadata/requires_dist.rs +++ b/crates/uv-distribution/src/metadata/requires_dist.rs @@ -1,11 +1,12 @@ -use crate::metadata::{LoweredRequirement, MetadataError}; -use crate::Metadata; - use std::collections::BTreeMap; use std::path::Path; + +use crate::metadata::{GitWorkspaceMember, LoweredRequirement, MetadataError}; +use crate::Metadata; use uv_configuration::{LowerBound, SourceStrategy}; use uv_distribution_types::IndexLocations; use uv_normalize::{ExtraName, GroupName, PackageName, DEV_DEPENDENCIES}; +use uv_workspace::dependency_groups::FlatDependencyGroups; use uv_workspace::pyproject::ToolUvSources; use uv_workspace::{DiscoveryOptions, ProjectWorkspace}; @@ -14,7 +15,7 @@ pub struct RequiresDist { pub name: PackageName, pub requires_dist: Vec, pub provides_extras: Vec, - pub dev_dependencies: BTreeMap>, + pub dependency_groups: BTreeMap>, } impl RequiresDist { @@ -29,7 +30,7 @@ impl RequiresDist { .map(uv_pypi_types::Requirement::from) .collect(), provides_extras: metadata.provides_extras, - dev_dependencies: BTreeMap::default(), + dependency_groups: BTreeMap::default(), } } @@ -38,15 +39,27 @@ impl RequiresDist { pub async fn from_project_maybe_workspace( metadata: uv_pypi_types::RequiresDist, install_path: &Path, + git_member: Option<&GitWorkspaceMember<'_>>, locations: &IndexLocations, sources: SourceStrategy, lower_bound: LowerBound, ) -> Result { - // TODO(konsti): Limit discovery for Git checkouts to Git root. // TODO(konsti): Cache workspace discovery. + let discovery_options = if let Some(git_member) = &git_member { + DiscoveryOptions { + stop_discovery_at: Some( + git_member + .fetch_root + .parent() + .expect("git checkout has a parent"), + ), + ..Default::default() + } + } else { + DiscoveryOptions::default() + }; let Some(project_workspace) = - ProjectWorkspace::from_maybe_project_root(install_path, &DiscoveryOptions::default()) - .await? + ProjectWorkspace::from_maybe_project_root(install_path, &discovery_options).await? else { return Ok(Self::from_metadata23(metadata)); }; @@ -54,6 +67,7 @@ impl RequiresDist { Self::from_project_workspace( metadata, &project_workspace, + git_member, locations, sources, lower_bound, @@ -63,6 +77,7 @@ impl RequiresDist { fn from_project_workspace( metadata: uv_pypi_types::RequiresDist, project_workspace: &ProjectWorkspace, + git_member: Option<&GitWorkspaceMember<'_>>, locations: &IndexLocations, source_strategy: SourceStrategy, lower_bound: LowerBound, @@ -96,49 +111,90 @@ impl RequiresDist { SourceStrategy::Disabled => &empty, }; - let dev_dependencies = { + let dependency_groups = { + // First, collect `tool.uv.dev_dependencies` let dev_dependencies = project_workspace .current_project() .pyproject_toml() .tool .as_ref() .and_then(|tool| tool.uv.as_ref()) - .and_then(|uv| uv.dev_dependencies.as_ref()) - .into_iter() + .and_then(|uv| uv.dev_dependencies.as_ref()); + + // Then, collect `dependency-groups` + let dependency_groups = project_workspace + .current_project() + .pyproject_toml() + .dependency_groups + .iter() .flatten() - .cloned(); - let dev_dependencies = match source_strategy { - SourceStrategy::Enabled => dev_dependencies - .flat_map(|requirement| { - let requirement_name = requirement.name.clone(); - LoweredRequirement::from_requirement( - requirement, - &metadata.name, - project_workspace.project_root(), - project_sources, - project_indexes, - locations, - project_workspace.workspace(), - lower_bound, - ) - .map(move |requirement| match requirement { - Ok(requirement) => Ok(requirement.into_inner()), - Err(err) => { - Err(MetadataError::LoweringError(requirement_name.clone(), err)) - } - }) - }) - .collect::, _>>()?, - SourceStrategy::Disabled => dev_dependencies + .collect::>(); + + // Resolve any `include-group` entries in `dependency-groups`. + let dependency_groups = + FlatDependencyGroups::from_dependency_groups(&dependency_groups)? .into_iter() - .map(uv_pypi_types::Requirement::from) - .collect(), - }; - if dev_dependencies.is_empty() { - BTreeMap::default() - } else { - BTreeMap::from([(DEV_DEPENDENCIES.clone(), dev_dependencies)]) + .chain( + // Only add the `dev` group if `dev-dependencies` is defined. + dev_dependencies + .into_iter() + .map(|requirements| (DEV_DEPENDENCIES.clone(), requirements.clone())), + ) + .map(|(name, requirements)| { + let requirements = match source_strategy { + SourceStrategy::Enabled => requirements + .into_iter() + .flat_map(|requirement| { + let group_name = name.clone(); + let requirement_name = requirement.name.clone(); + LoweredRequirement::from_requirement( + requirement, + &metadata.name, + project_workspace.project_root(), + project_sources, + project_indexes, + locations, + project_workspace.workspace(), + lower_bound, + git_member, + ) + .map(move |requirement| { + match requirement { + Ok(requirement) => Ok(requirement.into_inner()), + Err(err) => Err(MetadataError::GroupLoweringError( + group_name.clone(), + requirement_name.clone(), + Box::new(err), + )), + } + }) + }) + .collect::, _>>(), + SourceStrategy::Disabled => Ok(requirements + .into_iter() + .map(uv_pypi_types::Requirement::from) + .collect()), + }?; + Ok::<(GroupName, Vec), MetadataError>(( + name, + requirements, + )) + }) + .collect::, _>>()?; + + // Merge any overlapping groups. + let mut map = BTreeMap::new(); + for (name, dependencies) in dependency_groups { + match map.entry(name) { + std::collections::btree_map::Entry::Vacant(entry) => { + entry.insert(dependencies); + } + std::collections::btree_map::Entry::Occupied(mut entry) => { + entry.get_mut().extend(dependencies); + } + } } + map }; let requires_dist = metadata.requires_dist.into_iter(); @@ -155,12 +211,14 @@ impl RequiresDist { locations, project_workspace.workspace(), lower_bound, + git_member, ) .map(move |requirement| match requirement { Ok(requirement) => Ok(requirement.into_inner()), - Err(err) => { - Err(MetadataError::LoweringError(requirement_name.clone(), err)) - } + Err(err) => Err(MetadataError::LoweringError( + requirement_name.clone(), + Box::new(err), + )), }) }) .collect::, _>>()?, @@ -173,7 +231,7 @@ impl RequiresDist { Ok(Self { name: metadata.name, requires_dist, - dev_dependencies, + dependency_groups, provides_extras: metadata.provides_extras, }) } @@ -185,7 +243,7 @@ impl From for RequiresDist { name: metadata.name, requires_dist: metadata.requires_dist, provides_extras: metadata.provides_extras, - dev_dependencies: metadata.dev_dependencies, + dependency_groups: metadata.dependency_groups, } } } @@ -224,6 +282,7 @@ mod test { Ok(RequiresDist::from_project_workspace( requires_dist, &project_workspace, + None, &IndexLocations::default(), SourceStrategy::default(), LowerBound::default(), @@ -255,7 +314,7 @@ mod test { "#}; assert_snapshot!(format_err(input).await, @r###" - error: Failed to parse entry for: `tqdm` + error: Failed to parse entry: `tqdm` Caused by: Can't combine URLs from both `project.dependencies` and `tool.uv.sources` "###); } @@ -422,7 +481,7 @@ mod test { "#}; assert_snapshot!(format_err(input).await, @r###" - error: Failed to parse entry for: `tqdm` + error: Failed to parse entry: `tqdm` Caused by: Can't combine URLs from both `project.dependencies` and `tool.uv.sources` "###); } @@ -441,7 +500,7 @@ mod test { "#}; assert_snapshot!(format_err(input).await, @r###" - error: Failed to parse entry for: `tqdm` + error: Failed to parse entry: `tqdm` Caused by: Package is not included as workspace package in `tool.uv.workspace` "###); } diff --git a/crates/uv-distribution/src/source/mod.rs b/crates/uv-distribution/src/source/mod.rs index a8e3d011339a..b3bbbe94a843 100644 --- a/crates/uv-distribution/src/source/mod.rs +++ b/crates/uv-distribution/src/source/mod.rs @@ -33,7 +33,7 @@ use zip::ZipArchive; use crate::distribution_database::ManagedClient; use crate::error::Error; -use crate::metadata::{ArchiveMetadata, Metadata}; +use crate::metadata::{ArchiveMetadata, GitWorkspaceMember, Metadata}; use crate::reporter::Facade; use crate::source::built_wheel_metadata::BuiltWheelMetadata; use crate::source::revision::Revision; @@ -389,6 +389,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { let requires_dist = RequiresDist::from_project_maybe_workspace( requires_dist, project_root, + None, self.build_context.locations(), self.build_context.sources(), self.build_context.bounds(), @@ -1083,6 +1084,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { Metadata::from_workspace( metadata, resource.install_path.as_ref(), + None, self.build_context.locations(), self.build_context.sources(), self.build_context.bounds(), @@ -1119,6 +1121,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { Metadata::from_workspace( metadata, resource.install_path.as_ref(), + None, self.build_context.locations(), self.build_context.sources(), self.build_context.bounds(), @@ -1150,6 +1153,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { Metadata::from_workspace( metadata, resource.install_path.as_ref(), + None, self.build_context.locations(), self.build_context.sources(), self.build_context.bounds(), @@ -1197,6 +1201,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { Metadata::from_workspace( metadata, resource.install_path.as_ref(), + None, self.build_context.locations(), self.build_context.sources(), self.build_context.bounds(), @@ -1378,10 +1383,15 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { if let Some(metadata) = Self::read_static_metadata(source, fetch.path(), resource.subdirectory).await? { + let git_member = GitWorkspaceMember { + fetch_root: fetch.path(), + git_source: resource, + }; return Ok(ArchiveMetadata::from( Metadata::from_workspace( metadata, &path, + Some(&git_member), self.build_context.locations(), self.build_context.sources(), self.build_context.bounds(), @@ -1410,6 +1420,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { Metadata::from_workspace( metadata, &path, + None, self.build_context.locations(), self.build_context.sources(), self.build_context.bounds(), @@ -1442,6 +1453,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { Metadata::from_workspace( metadata, &path, + None, self.build_context.locations(), self.build_context.sources(), self.build_context.bounds(), @@ -1489,6 +1501,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { Metadata::from_workspace( metadata, fetch.path(), + None, self.build_context.locations(), self.build_context.sources(), self.build_context.bounds(), @@ -1497,6 +1510,40 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { )) } + /// Resolve a source to a specific revision. + pub(crate) async fn resolve_revision( + &self, + source: &BuildableSource<'_>, + client: &ManagedClient<'_>, + ) -> Result<(), Error> { + match source { + BuildableSource::Dist(SourceDist::Git(source)) => { + self.build_context + .git() + .fetch( + &source.git, + client.unmanaged.uncached_client(&source.url).clone(), + self.build_context.cache().bucket(CacheBucket::Git), + self.reporter.clone().map(Facade::from), + ) + .await?; + } + BuildableSource::Url(SourceUrl::Git(source)) => { + self.build_context + .git() + .fetch( + source.git, + client.unmanaged.uncached_client(source.url).clone(), + self.build_context.cache().bucket(CacheBucket::Git), + self.reporter.clone().map(Facade::from), + ) + .await?; + } + _ => {} + } + Ok(()) + } + /// Heal a [`Revision`] for a local archive. async fn heal_archive_revision( &self, diff --git a/crates/uv-extract/src/hash.rs b/crates/uv-extract/src/hash.rs index ac1efb397d74..e7abdc668924 100644 --- a/crates/uv-extract/src/hash.rs +++ b/crates/uv-extract/src/hash.rs @@ -23,15 +23,6 @@ impl Hasher { Hasher::Sha512(hasher) => hasher.update(data), } } - - pub fn finalize(self) -> Vec { - match self { - Hasher::Md5(hasher) => hasher.finalize().to_vec(), - Hasher::Sha256(hasher) => hasher.finalize().to_vec(), - Hasher::Sha384(hasher) => hasher.finalize().to_vec(), - Hasher::Sha512(hasher) => hasher.finalize().to_vec(), - } - } } impl From for Hasher { diff --git a/crates/uv-extract/src/stream.rs b/crates/uv-extract/src/stream.rs index 2ff9278d85f9..483be2f50588 100644 --- a/crates/uv-extract/src/stream.rs +++ b/crates/uv-extract/src/stream.rs @@ -1,13 +1,15 @@ -use std::path::Path; +use std::path::{Component, Path, PathBuf}; use std::pin::Pin; -use crate::Error; use futures::StreamExt; use rustc_hash::FxHashSet; use tokio_util::compat::{FuturesAsyncReadCompatExt, TokioAsyncReadCompatExt}; use tracing::warn; + use uv_distribution_filename::SourceDistExtension; +use crate::Error; + const DEFAULT_BUF_SIZE: usize = 128 * 1024; /// Unpack a `.zip` archive into the target directory, without requiring `Seek`. @@ -19,6 +21,26 @@ pub async fn unzip( reader: R, target: impl AsRef, ) -> Result<(), Error> { + /// Ensure the file path is safe to use as a [`Path`]. + /// + /// See: + pub(crate) fn enclosed_name(file_name: &str) -> Option { + if file_name.contains('\0') { + return None; + } + let path = PathBuf::from(file_name); + let mut depth = 0usize; + for component in path.components() { + match component { + Component::Prefix(_) | Component::RootDir => return None, + Component::ParentDir => depth = depth.checked_sub(1)?, + Component::Normal(_) => depth += 1, + Component::CurDir => (), + } + } + Some(path) + } + let target = target.as_ref(); let mut reader = futures::io::BufReader::with_capacity(DEFAULT_BUF_SIZE, reader.compat()); let mut zip = async_zip::base::read::stream::ZipFileReader::new(&mut reader); @@ -28,6 +50,16 @@ pub async fn unzip( while let Some(mut entry) = zip.next_with_entry().await? { // Construct the (expected) path to the file on-disk. let path = entry.reader().entry().filename().as_str()?; + + // Sanitize the file name to prevent directory traversal attacks. + let Some(path) = enclosed_name(path) else { + warn!("Skipping unsafe file name: {path}"); + + // Close current file prior to proceeding, as per: + // https://docs.rs/async_zip/0.0.16/async_zip/base/read/stream/ + zip = entry.skip().await?; + continue; + }; let path = target.join(path); let is_dir = entry.reader().entry().dir()?; @@ -55,7 +87,7 @@ pub async fn unzip( tokio::io::copy(&mut reader, &mut writer).await?; } - // Close current file to get access to the next one. See docs: + // Close current file prior to proceeding, as per: // https://docs.rs/async_zip/0.0.16/async_zip/base/read/stream/ zip = entry.skip().await?; } @@ -84,6 +116,9 @@ pub async fn unzip( if has_any_executable_bit != 0 { // Construct the (expected) path to the file on-disk. let path = entry.filename().as_str()?; + let Some(path) = enclosed_name(path) else { + continue; + }; let path = target.join(path); let permissions = fs_err::tokio::metadata(&path).await?.permissions(); diff --git a/crates/uv-extract/src/sync.rs b/crates/uv-extract/src/sync.rs index 77a7945a30b9..4c1ad1a8ac58 100644 --- a/crates/uv-extract/src/sync.rs +++ b/crates/uv-extract/src/sync.rs @@ -3,6 +3,7 @@ use std::sync::Mutex; use rayon::prelude::*; use rustc_hash::FxHashSet; +use tracing::warn; use zip::ZipArchive; use crate::vendor::{CloneableSeekableReader, HasLength}; @@ -25,6 +26,7 @@ pub fn unzip( // Determine the path of the file within the wheel. let Some(enclosed_name) = file.enclosed_name() else { + warn!("Skipping unsafe file name: {}", file.name()); return Ok(()); }; diff --git a/crates/uv-fs/src/lib.rs b/crates/uv-fs/src/lib.rs index a7007f342a50..1cde77a9d14e 100644 --- a/crates/uv-fs/src/lib.rs +++ b/crates/uv-fs/src/lib.rs @@ -104,21 +104,22 @@ pub fn remove_symlink(path: impl AsRef) -> std::io::Result<()> { fs_err::remove_file(path.as_ref()) } -/// Create a symlink at `dst` pointing to `src` or, on Windows, copy `src` to `dst`. +/// Create a symlink at `dst` pointing to `src` on Unix or copy `src` to `dst` on Windows +/// +/// This does not replace an existing symlink or file at `dst`. +/// +/// This does not fallback to copying on Unix. /// /// This function should only be used for files. If targeting a directory, use [`replace_symlink`] /// instead; it will use a junction on Windows, which is more performant. -pub fn symlink_copy_fallback_file( - src: impl AsRef, - dst: impl AsRef, -) -> std::io::Result<()> { +pub fn symlink_or_copy_file(src: impl AsRef, dst: impl AsRef) -> std::io::Result<()> { #[cfg(windows)] { fs_err::copy(src.as_ref(), dst.as_ref())?; } #[cfg(unix)] { - std::os::unix::fs::symlink(src.as_ref(), dst.as_ref())?; + fs_err::os::unix::fs::symlink(src.as_ref(), dst.as_ref())?; } Ok(()) diff --git a/crates/uv-git/src/git.rs b/crates/uv-git/src/git.rs index 49a04af6e602..86af38a7e778 100644 --- a/crates/uv-git/src/git.rs +++ b/crates/uv-git/src/git.rs @@ -590,7 +590,7 @@ pub(crate) fn fetch( } } -/// Attempts to use `git` CLI installed on the system to fetch a repository,. +/// Attempts to use `git` CLI installed on the system to fetch a repository. fn fetch_with_cli( repo: &mut GitRepository, url: &str, diff --git a/crates/uv-git/src/resolver.rs b/crates/uv-git/src/resolver.rs index 4d34619833d0..e0636ec243d3 100644 --- a/crates/uv-git/src/resolver.rs +++ b/crates/uv-git/src/resolver.rs @@ -38,6 +38,50 @@ impl GitResolver { self.0.get(reference) } + /// Resolve a Git URL to a specific commit. + pub async fn resolve( + &self, + url: &GitUrl, + client: ClientWithMiddleware, + cache: PathBuf, + reporter: Option, + ) -> Result { + debug!("Resolving source distribution from Git: {url}"); + + let reference = RepositoryReference::from(url); + + // If we know the precise commit already, return it. + if let Some(precise) = self.get(&reference) { + return Ok(*precise); + } + + // Avoid races between different processes, too. + let lock_dir = cache.join("locks"); + fs::create_dir_all(&lock_dir).await?; + let repository_url = RepositoryUrl::new(url.repository()); + let _lock = LockedFile::acquire( + lock_dir.join(cache_digest(&repository_url)), + &repository_url, + ) + .await?; + + // Fetch the Git repository. + let source = if let Some(reporter) = reporter { + GitSource::new(url.clone(), client, cache).with_reporter(reporter) + } else { + GitSource::new(url.clone(), client, cache) + }; + let precise = tokio::task::spawn_blocking(move || source.resolve()) + .await? + .map_err(GitResolverError::Git)?; + + // Insert the resolved URL into the in-memory cache. This ensures that subsequent fetches + // resolve to the same precise commit. + self.insert(reference, precise); + + Ok(precise) + } + /// Fetch a remote Git repository. pub async fn fetch( &self, diff --git a/crates/uv-git/src/source.rs b/crates/uv-git/src/source.rs index bf3b7bd9bdb0..5b475af90fd6 100644 --- a/crates/uv-git/src/source.rs +++ b/crates/uv-git/src/source.rs @@ -51,6 +51,68 @@ impl GitSource { } } + /// Resolve a Git source to a specific revision. + #[instrument(skip(self), fields(repository = %self.git.repository, rev = ?self.git.precise))] + pub fn resolve(self) -> Result { + // Compute the canonical URL for the repository. + let canonical = RepositoryUrl::new(&self.git.repository); + + // The path to the repo, within the Git database. + let ident = cache_digest(&canonical); + let db_path = self.cache.join("db").join(&ident); + + // Authenticate the URL, if necessary. + let remote = if let Some(credentials) = GIT_STORE.get(&canonical) { + Cow::Owned(credentials.apply(self.git.repository.clone())) + } else { + Cow::Borrowed(&self.git.repository) + }; + + let remote = GitRemote::new(&remote); + let (db, actual_rev, task) = match (self.git.precise, remote.db_at(&db_path).ok()) { + // If we have a locked revision, and we have a preexisting database + // which has that revision, then no update needs to happen. + (Some(rev), Some(db)) if db.contains(rev.into()) => { + debug!("Using existing Git source `{}`", self.git.repository); + (db, rev, None) + } + + // ... otherwise we use this state to update the git database. Note + // that we still check for being offline here, for example in the + // situation that we have a locked revision but the database + // doesn't have it. + (locked_rev, db) => { + debug!("Updating Git source `{}`", self.git.repository); + + // Report the checkout operation to the reporter. + let task = self.reporter.as_ref().map(|reporter| { + reporter.on_checkout_start(remote.url(), self.git.reference.as_rev()) + }); + + let (db, actual_rev) = remote.checkout( + &db_path, + db, + &self.git.reference, + locked_rev.map(GitOid::from), + &self.client, + )?; + + (db, GitSha::from(actual_rev), task) + } + }; + + let short_id = db.to_short_id(actual_rev.into())?; + + // Report the checkout operation to the reporter. + if let Some(task) = task { + if let Some(reporter) = self.reporter.as_ref() { + reporter.on_checkout_complete(remote.url(), short_id.as_str(), task); + } + } + + Ok(actual_rev) + } + /// Fetch the underlying Git repository at the given revision. #[instrument(skip(self), fields(repository = %self.git.repository, rev = ?self.git.precise))] pub fn fetch(self) -> Result { diff --git a/crates/uv-install-wheel/Cargo.toml b/crates/uv-install-wheel/Cargo.toml index 97f5cf57a909..d7dcd8fd1db7 100644 --- a/crates/uv-install-wheel/Cargo.toml +++ b/crates/uv-install-wheel/Cargo.toml @@ -28,6 +28,7 @@ uv-normalize = { workspace = true } uv-pep440 = { workspace = true } uv-platform-tags = { workspace = true } uv-pypi-types = { workspace = true } +uv-trampoline-builder = { workspace = true } uv-warnings = { workspace = true } clap = { workspace = true, optional = true, features = ["derive"] } diff --git a/crates/uv-install-wheel/src/lib.rs b/crates/uv-install-wheel/src/lib.rs index 83300d42da76..69249229aab6 100644 --- a/crates/uv-install-wheel/src/lib.rs +++ b/crates/uv-install-wheel/src/lib.rs @@ -97,4 +97,6 @@ pub enum Error { MismatchedVersion(Version, Version), #[error("Invalid egg-link")] InvalidEggLink(PathBuf), + #[error(transparent)] + LauncherError(#[from] uv_trampoline_builder::Error), } diff --git a/crates/uv-install-wheel/src/wheel.rs b/crates/uv-install-wheel/src/wheel.rs index 559237e16769..cf640348e7fc 100644 --- a/crates/uv-install-wheel/src/wheel.rs +++ b/crates/uv-install-wheel/src/wheel.rs @@ -1,11 +1,8 @@ use std::collections::HashMap; -use std::io::{BufReader, Cursor, Read, Seek, Write}; +use std::io; +use std::io::{BufReader, Read, Seek, Write}; use std::path::{Path, PathBuf}; -use std::{env, io}; -use crate::record::RecordEntry; -use crate::script::Script; -use crate::{Error, Layout}; use data_encoding::BASE64URL_NOPAD; use fs_err as fs; use fs_err::{DirEntry, File}; @@ -13,39 +10,17 @@ use mailparse::parse_headers; use rustc_hash::FxHashMap; use sha2::{Digest, Sha256}; use tracing::{instrument, warn}; +use walkdir::WalkDir; + use uv_cache_info::CacheInfo; use uv_fs::{relative_to, Simplified}; use uv_normalize::PackageName; use uv_pypi_types::DirectUrl; -use walkdir::WalkDir; -use zip::write::FileOptions; -use zip::ZipWriter; - -const LAUNCHER_MAGIC_NUMBER: [u8; 4] = [b'U', b'V', b'U', b'V']; +use uv_trampoline_builder::windows_script_launcher; -#[cfg(all(windows, target_arch = "x86"))] -const LAUNCHER_I686_GUI: &[u8] = - include_bytes!("../../uv-trampoline/trampolines/uv-trampoline-i686-gui.exe"); - -#[cfg(all(windows, target_arch = "x86"))] -const LAUNCHER_I686_CONSOLE: &[u8] = - include_bytes!("../../uv-trampoline/trampolines/uv-trampoline-i686-console.exe"); - -#[cfg(all(windows, target_arch = "x86_64"))] -const LAUNCHER_X86_64_GUI: &[u8] = - include_bytes!("../../uv-trampoline/trampolines/uv-trampoline-x86_64-gui.exe"); - -#[cfg(all(windows, target_arch = "x86_64"))] -const LAUNCHER_X86_64_CONSOLE: &[u8] = - include_bytes!("../../uv-trampoline/trampolines/uv-trampoline-x86_64-console.exe"); - -#[cfg(all(windows, target_arch = "aarch64"))] -const LAUNCHER_AARCH64_GUI: &[u8] = - include_bytes!("../../uv-trampoline/trampolines/uv-trampoline-aarch64-gui.exe"); - -#[cfg(all(windows, target_arch = "aarch64"))] -const LAUNCHER_AARCH64_CONSOLE: &[u8] = - include_bytes!("../../uv-trampoline/trampolines/uv-trampoline-aarch64-console.exe"); +use crate::record::RecordEntry; +use crate::script::Script; +use crate::{Error, Layout}; /// Wrapper script template function /// @@ -158,87 +133,6 @@ fn format_shebang(executable: impl AsRef, os_name: &str, relocatable: bool format!("#!{executable}") } -/// A Windows script is a minimal .exe launcher binary with the python entrypoint script appended as -/// stored zip file. -/// -/// -#[allow(unused_variables)] -pub(crate) fn windows_script_launcher( - launcher_python_script: &str, - is_gui: bool, - python_executable: impl AsRef, -) -> Result, Error> { - // This method should only be called on Windows, but we avoid `#[cfg(windows)]` to retain - // compilation on all platforms. - if cfg!(not(windows)) { - return Err(Error::NotWindows); - } - - let launcher_bin: &[u8] = match env::consts::ARCH { - #[cfg(all(windows, target_arch = "x86"))] - "x86" => { - if is_gui { - LAUNCHER_I686_GUI - } else { - LAUNCHER_I686_CONSOLE - } - } - #[cfg(all(windows, target_arch = "x86_64"))] - "x86_64" => { - if is_gui { - LAUNCHER_X86_64_GUI - } else { - LAUNCHER_X86_64_CONSOLE - } - } - #[cfg(all(windows, target_arch = "aarch64"))] - "aarch64" => { - if is_gui { - LAUNCHER_AARCH64_GUI - } else { - LAUNCHER_AARCH64_CONSOLE - } - } - #[cfg(windows)] - arch => { - return Err(Error::UnsupportedWindowsArch(arch)); - } - #[cfg(not(windows))] - arch => &[], - }; - - let mut payload: Vec = Vec::new(); - { - // We're using the zip writer, but with stored compression - // https://github.com/njsmith/posy/blob/04927e657ca97a5e35bb2252d168125de9a3a025/src/trampolines/mod.rs#L75-L82 - // https://github.com/pypa/distlib/blob/8ed03aab48add854f377ce392efffb79bb4d6091/PC/launcher.c#L259-L271 - let stored = FileOptions::default().compression_method(zip::CompressionMethod::Stored); - let mut archive = ZipWriter::new(Cursor::new(&mut payload)); - let error_msg = "Writing to Vec should never fail"; - archive.start_file("__main__.py", stored).expect(error_msg); - archive - .write_all(launcher_python_script.as_bytes()) - .expect(error_msg); - archive.finish().expect(error_msg); - } - - let python = python_executable.as_ref(); - let python_path = python.simplified_display().to_string(); - - let mut launcher: Vec = Vec::with_capacity(launcher_bin.len() + payload.len()); - launcher.extend_from_slice(launcher_bin); - launcher.extend_from_slice(&payload); - launcher.extend_from_slice(python_path.as_bytes()); - launcher.extend_from_slice( - &u32::try_from(python_path.as_bytes().len()) - .expect("File Path to be smaller than 4GB") - .to_le_bytes(), - ); - launcher.extend_from_slice(&LAUNCHER_MAGIC_NUMBER); - - Ok(launcher) -} - /// Returns a [`PathBuf`] to `python[w].exe` for script execution. /// /// @@ -1075,54 +969,6 @@ mod test { Ok(()) } - #[test] - #[cfg(all(windows, target_arch = "x86"))] - fn test_launchers_are_small() { - // At time of writing, they are 45kb~ bytes. - assert!( - super::LAUNCHER_I686_GUI.len() < 45 * 1024, - "GUI launcher: {}", - super::LAUNCHER_I686_GUI.len() - ); - assert!( - super::LAUNCHER_I686_CONSOLE.len() < 45 * 1024, - "CLI launcher: {}", - super::LAUNCHER_I686_CONSOLE.len() - ); - } - - #[test] - #[cfg(all(windows, target_arch = "x86_64"))] - fn test_launchers_are_small() { - // At time of writing, they are 45kb~ bytes. - assert!( - super::LAUNCHER_X86_64_GUI.len() < 45 * 1024, - "GUI launcher: {}", - super::LAUNCHER_X86_64_GUI.len() - ); - assert!( - super::LAUNCHER_X86_64_CONSOLE.len() < 45 * 1024, - "CLI launcher: {}", - super::LAUNCHER_X86_64_CONSOLE.len() - ); - } - - #[test] - #[cfg(all(windows, target_arch = "aarch64"))] - fn test_launchers_are_small() { - // At time of writing, they are 45kb~ bytes. - assert!( - super::LAUNCHER_AARCH64_GUI.len() < 45 * 1024, - "GUI launcher: {}", - super::LAUNCHER_AARCH64_GUI.len() - ); - assert!( - super::LAUNCHER_AARCH64_CONSOLE.len() < 45 * 1024, - "CLI launcher: {}", - super::LAUNCHER_AARCH64_CONSOLE.len() - ); - } - #[test] fn test_script_executable() -> Result<()> { // Test with adjacent pythonw.exe diff --git a/crates/uv-normalize/src/group_name.rs b/crates/uv-normalize/src/group_name.rs index d41d3791cb45..72aa898ec729 100644 --- a/crates/uv-normalize/src/group_name.rs +++ b/crates/uv-normalize/src/group_name.rs @@ -3,7 +3,7 @@ use std::fmt::{Display, Formatter}; use std::str::FromStr; use std::sync::LazyLock; -use serde::{Deserialize, Deserializer}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; use crate::{validate_and_normalize_owned, validate_and_normalize_ref, InvalidNameError}; @@ -41,6 +41,15 @@ impl<'de> Deserialize<'de> for GroupName { } } +impl Serialize for GroupName { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + self.0.serialize(serializer) + } +} + impl Display for GroupName { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { self.0.fmt(f) diff --git a/crates/uv-pep440/Cargo.toml b/crates/uv-pep440/Cargo.toml index 001009c02ef7..cc8aab8e0b1f 100644 --- a/crates/uv-pep440/Cargo.toml +++ b/crates/uv-pep440/Cargo.toml @@ -20,11 +20,13 @@ doctest = false workspace = true [dependencies] -serde = { workspace = true, features = ["derive"] } rkyv = { workspace = true, optional = true } +serde = { workspace = true, features = ["derive"] } tracing = { workspace = true, optional = true } unicode-width = { workspace = true } unscanny = { workspace = true } +# Adds conversions from [`VersionSpecifiers`] to [`version_ranges::Ranges`] +version-ranges = { workspace = true, optional = true } [dev-dependencies] indoc = { version = "2.0.5" } diff --git a/crates/uv-pep440/src/lib.rs b/crates/uv-pep440/src/lib.rs index ca848d382e0a..982d6ede93c1 100644 --- a/crates/uv-pep440/src/lib.rs +++ b/crates/uv-pep440/src/lib.rs @@ -23,6 +23,8 @@ //! the version matching needs to catch all sorts of special cases #![warn(missing_docs)] +#[cfg(feature = "version-ranges")] +pub use version_ranges::{release_specifier_to_range, release_specifiers_to_ranges}; pub use { version::{ LocalSegment, Operator, OperatorParseError, Prerelease, PrereleaseKind, Version, @@ -39,3 +41,5 @@ mod version_specifier; #[cfg(test)] mod tests; +#[cfg(feature = "version-ranges")] +mod version_ranges; diff --git a/crates/uv-pep440/src/version.rs b/crates/uv-pep440/src/version.rs index 404791dd1534..26b3831ec1f1 100644 --- a/crates/uv-pep440/src/version.rs +++ b/crates/uv-pep440/src/version.rs @@ -32,6 +32,8 @@ pub enum Operator { /// `!= 1.2.*` NotEqualStar, /// `~=` + /// + /// Invariant: With `~=`, there are always at least 2 release segments. TildeEqual, /// `<` LessThan, @@ -1247,6 +1249,9 @@ struct VersionFull { /// > label”, separated from the public version identifier by a plus. /// > Local version labels have no specific semantics assigned, but /// > some syntactic restrictions are imposed. + /// + /// Local versions allow multiple segments separated by periods, such as `deadbeef.1.2.3`, see + /// [`LocalSegment`] for details on the semantics. local: Vec, /// An internal-only segment that does not exist in PEP 440, used to /// represent the smallest possible version of a release, preceding any diff --git a/crates/uv-pep440/src/version_ranges.rs b/crates/uv-pep440/src/version_ranges.rs new file mode 100644 index 000000000000..41c45fe291e1 --- /dev/null +++ b/crates/uv-pep440/src/version_ranges.rs @@ -0,0 +1,192 @@ +//! Convert [`VersionSpecifiers`] to [`version_ranges::Ranges`]. + +use version_ranges::Ranges; + +use crate::{Operator, Prerelease, Version, VersionSpecifier, VersionSpecifiers}; + +impl From for Ranges { + /// Convert [`VersionSpecifiers`] to a PubGrub-compatible version range, using PEP 440 + /// semantics. + fn from(specifiers: VersionSpecifiers) -> Self { + let mut range = Ranges::full(); + for specifier in specifiers { + range = range.intersection(&Self::from(specifier)); + } + range + } +} + +impl From for Ranges { + /// Convert the [`VersionSpecifier`] to a PubGrub-compatible version range, using PEP 440 + /// semantics. + fn from(specifier: VersionSpecifier) -> Self { + let VersionSpecifier { operator, version } = specifier; + match operator { + Operator::Equal => Ranges::singleton(version), + Operator::ExactEqual => Ranges::singleton(version), + Operator::NotEqual => Ranges::singleton(version).complement(), + Operator::TildeEqual => { + let [rest @ .., last, _] = version.release() else { + unreachable!("~= must have at least two segments"); + }; + let upper = Version::new(rest.iter().chain([&(last + 1)])) + .with_epoch(version.epoch()) + .with_dev(Some(0)); + + Ranges::from_range_bounds(version..upper) + } + Operator::LessThan => { + if version.any_prerelease() { + Ranges::strictly_lower_than(version) + } else { + // Per PEP 440: "The exclusive ordered comparison Ranges::lower_than(version), + Operator::GreaterThan => { + // Per PEP 440: "The exclusive ordered comparison >V MUST NOT allow a post-release of + // the given version unless V itself is a post release." + + if let Some(dev) = version.dev() { + Ranges::higher_than(version.with_dev(Some(dev + 1))) + } else if let Some(post) = version.post() { + Ranges::higher_than(version.with_post(Some(post + 1))) + } else { + Ranges::strictly_higher_than(version.with_max(Some(0))) + } + } + Operator::GreaterThanEqual => Ranges::higher_than(version), + Operator::EqualStar => { + let low = version.with_dev(Some(0)); + let mut high = low.clone(); + if let Some(post) = high.post() { + high = high.with_post(Some(post + 1)); + } else if let Some(pre) = high.pre() { + high = high.with_pre(Some(Prerelease { + kind: pre.kind, + number: pre.number + 1, + })); + } else { + let mut release = high.release().to_vec(); + *release.last_mut().unwrap() += 1; + high = high.with_release(release); + } + Ranges::from_range_bounds(low..high) + } + Operator::NotEqualStar => { + let low = version.with_dev(Some(0)); + let mut high = low.clone(); + if let Some(post) = high.post() { + high = high.with_post(Some(post + 1)); + } else if let Some(pre) = high.pre() { + high = high.with_pre(Some(Prerelease { + kind: pre.kind, + number: pre.number + 1, + })); + } else { + let mut release = high.release().to_vec(); + *release.last_mut().unwrap() += 1; + high = high.with_release(release); + } + Ranges::from_range_bounds(low..high).complement() + } + } + } +} + +/// Convert the [`VersionSpecifiers`] to a PubGrub-compatible version range, using release-only +/// semantics. +/// +/// Assumes that the range will only be tested against versions that consist solely of release +/// segments (e.g., `3.12.0`, but not `3.12.0b1`). +/// +/// These semantics are used for testing Python compatibility (e.g., `requires-python` against +/// the user's installed Python version). In that context, it's more intuitive that `3.13.0b0` +/// is allowed for projects that declare `requires-python = ">3.13"`. +/// +/// See: +pub fn release_specifiers_to_ranges(specifiers: VersionSpecifiers) -> Ranges { + let mut range = Ranges::full(); + for specifier in specifiers { + range = range.intersection(&release_specifier_to_range(specifier)); + } + range +} + +/// Convert the [`VersionSpecifier`] to a PubGrub-compatible version range, using release-only +/// semantics. +/// +/// Assumes that the range will only be tested against versions that consist solely of release +/// segments (e.g., `3.12.0`, but not `3.12.0b1`). +/// +/// These semantics are used for testing Python compatibility (e.g., `requires-python` against +/// the user's installed Python version). In that context, it's more intuitive that `3.13.0b0` +/// is allowed for projects that declare `requires-python = ">3.13"`. +/// +/// See: +pub fn release_specifier_to_range(specifier: VersionSpecifier) -> Ranges { + let VersionSpecifier { operator, version } = specifier; + match operator { + Operator::Equal => { + let version = version.only_release(); + Ranges::singleton(version) + } + Operator::ExactEqual => { + let version = version.only_release(); + Ranges::singleton(version) + } + Operator::NotEqual => { + let version = version.only_release(); + Ranges::singleton(version).complement() + } + Operator::TildeEqual => { + let [rest @ .., last, _] = version.release() else { + unreachable!("~= must have at least two segments"); + }; + let upper = Version::new(rest.iter().chain([&(last + 1)])); + let version = version.only_release(); + Ranges::from_range_bounds(version..upper) + } + Operator::LessThan => { + let version = version.only_release(); + Ranges::strictly_lower_than(version) + } + Operator::LessThanEqual => { + let version = version.only_release(); + Ranges::lower_than(version) + } + Operator::GreaterThan => { + let version = version.only_release(); + Ranges::strictly_higher_than(version) + } + Operator::GreaterThanEqual => { + let version = version.only_release(); + Ranges::higher_than(version) + } + Operator::EqualStar => { + let low = version.only_release(); + let high = { + let mut high = low.clone(); + let mut release = high.release().to_vec(); + *release.last_mut().unwrap() += 1; + high = high.with_release(release); + high + }; + Ranges::from_range_bounds(low..high) + } + Operator::NotEqualStar => { + let low = version.only_release(); + let high = { + let mut high = low.clone(); + let mut release = high.release().to_vec(); + *release.last_mut().unwrap() += 1; + high = high.with_release(release); + high + }; + Ranges::from_range_bounds(low..high).complement() + } + } +} diff --git a/crates/uv-pep440/src/version_specifier.rs b/crates/uv-pep440/src/version_specifier.rs index 56fa5f597e95..4a62541e2ef4 100644 --- a/crates/uv-pep440/src/version_specifier.rs +++ b/crates/uv-pep440/src/version_specifier.rs @@ -6,6 +6,7 @@ use crate::{ version, Operator, OperatorParseError, Version, VersionPattern, VersionPatternParseError, }; use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; +#[cfg(feature = "tracing")] use tracing::warn; /// Sorted version specifiers, such as `>=2.1,<3`. @@ -23,19 +24,12 @@ use tracing::warn; /// // VersionSpecifiers derefs into a list of specifiers /// assert_eq!(version_specifiers.iter().position(|specifier| *specifier.operator() == Operator::LessThan), Some(1)); /// ``` -#[derive( - Eq, - PartialEq, - Ord, - PartialOrd, - Debug, - Clone, - Hash, - rkyv::Archive, - rkyv::Deserialize, - rkyv::Serialize, +#[derive(Eq, PartialEq, Ord, PartialOrd, Debug, Clone, Hash)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Archive, rkyv::Deserialize, rkyv::Serialize) )] -#[rkyv(derive(Debug))] +#[cfg_attr(feature = "rkyv", rkyv(derive(Debug)))] pub struct VersionSpecifiers(Vec); impl std::ops::Deref for VersionSpecifiers { @@ -97,6 +91,7 @@ impl VersionSpecifiers { specifiers.push(VersionSpecifier::not_equals_star_version(prev.clone())); } _ => { + #[cfg(feature = "tracing")] warn!("Ignoring unsupported gap in `requires-python` version: {next:?} -> {lower:?}"); } } @@ -117,6 +112,15 @@ impl FromIterator for VersionSpecifiers { } } +impl IntoIterator for VersionSpecifiers { + type Item = VersionSpecifier; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + impl FromStr for VersionSpecifiers { type Err = VersionSpecifiersParseError; @@ -239,19 +243,12 @@ impl std::error::Error for VersionSpecifiersParseError {} /// let version_specifier = VersionSpecifier::from_str("== 1.*").unwrap(); /// assert!(version_specifier.contains(&version)); /// ``` -#[derive( - Eq, - Ord, - PartialEq, - PartialOrd, - Debug, - Clone, - Hash, - rkyv::Archive, - rkyv::Deserialize, - rkyv::Serialize, +#[derive(Eq, Ord, PartialEq, PartialOrd, Debug, Clone, Hash)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Archive, rkyv::Deserialize, rkyv::Serialize) )] -#[rkyv(derive(Debug))] +#[cfg_attr(feature = "rkyv", rkyv(derive(Debug)))] pub struct VersionSpecifier { /// ~=|==|!=|<=|>=|<|>|===, plus whether the version ended with a star pub(crate) operator: Operator, diff --git a/crates/uv-pep508/Cargo.toml b/crates/uv-pep508/Cargo.toml index a5d5dbc71b7c..15e0e0095b2c 100644 --- a/crates/uv-pep508/Cargo.toml +++ b/crates/uv-pep508/Cargo.toml @@ -24,28 +24,25 @@ workspace = true uv-fs = { workspace = true } uv-normalize = { workspace = true } uv-pep440 = { workspace = true } -uv-pubgrub = { workspace = true } boxcar = { workspace = true } indexmap = { workspace = true } itertools = { workspace = true } -pubgrub = { workspace = true } regex = { workspace = true } rustc-hash = { workspace = true } schemars = { workspace = true, optional = true } serde = { workspace = true, features = ["derive", "rc"] } -serde_json = { workspace = true, optional = true } smallvec = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true, optional = true } unicode-width = { workspace = true } url = { workspace = true, features = ["serde"] } +version-ranges = { workspace = true } [dev-dependencies] insta = { version = "1.40.0" } -log = { version = "0.4.22" } serde_json = { version = "1.0.128" } -testing_logger = { version = "0.1.1" } +tracing-test = { version = "0.2.5" } [features] tracing = ["dep:tracing", "uv-pep440/tracing"] diff --git a/crates/uv-pep508/src/lib.rs b/crates/uv-pep508/src/lib.rs index ab0bc0299c45..c1319b288653 100644 --- a/crates/uv-pep508/src/lib.rs +++ b/crates/uv-pep508/src/lib.rs @@ -26,18 +26,20 @@ use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; use thiserror::Error; use url::Url; -use crate::marker::MarkerValueExtra; use cursor::Cursor; pub use marker::{ ContainsMarkerTree, ExtraMarkerTree, ExtraOperator, InMarkerTree, MarkerEnvironment, MarkerEnvironmentBuilder, MarkerExpression, MarkerOperator, MarkerTree, MarkerTreeContents, - MarkerTreeKind, MarkerValue, MarkerValueString, MarkerValueVersion, MarkerWarningKind, - StringMarkerTree, StringVersion, VersionMarkerTree, + MarkerTreeKind, MarkerValue, MarkerValueExtra, MarkerValueString, MarkerValueVersion, + MarkerWarningKind, StringMarkerTree, StringVersion, VersionMarkerTree, }; pub use origin::RequirementOrigin; #[cfg(feature = "non-pep508-extensions")] pub use unnamed::{UnnamedRequirement, UnnamedRequirementUrl}; pub use uv_normalize::{ExtraName, InvalidNameError, PackageName}; +/// Version and version specifiers used in requirements (reexport). +// https://github.com/konstin/pep508_rs/issues/19 +pub use uv_pep440; use uv_pep440::{Version, VersionSpecifier, VersionSpecifiers}; pub use verbatim_url::{ expand_env_vars, split_scheme, strip_host, Scheme, VerbatimUrl, VerbatimUrlError, diff --git a/crates/uv-pep508/src/marker/algebra.rs b/crates/uv-pep508/src/marker/algebra.rs index f44ec54c94cb..a8a1b047b708 100644 --- a/crates/uv-pep508/src/marker/algebra.rs +++ b/crates/uv-pep508/src/marker/algebra.rs @@ -51,12 +51,10 @@ use std::sync::Mutex; use std::sync::MutexGuard; use itertools::Either; -use pubgrub::Range; use rustc_hash::FxHashMap; use std::sync::LazyLock; -use uv_pep440::Operator; -use uv_pep440::{Version, VersionSpecifier}; -use uv_pubgrub::PubGrubSpecifier; +use uv_pep440::{release_specifier_to_range, Operator, Version, VersionSpecifier}; +use version_ranges::Ranges; use crate::marker::MarkerValueExtra; use crate::ExtraOperator; @@ -403,7 +401,7 @@ impl InternerGuard<'_> { }); return self.create_node(node.var.clone(), children); }; - let py_range = Range::from_range_bounds((py_lower.cloned(), py_upper.cloned())); + let py_range = Ranges::from_range_bounds((py_lower.cloned(), py_upper.cloned())); if py_range.is_empty() { // Oops, the bounds imply there is nothing that can match, // so we always evaluate to false. @@ -428,12 +426,12 @@ impl InternerGuard<'_> { // are known to be satisfied. let &(ref first_range, first_node_id) = new.first().unwrap(); let first_upper = first_range.bounding_range().unwrap().1; - let clipped = Range::from_range_bounds((Bound::Unbounded, first_upper.cloned())); + let clipped = Ranges::from_range_bounds((Bound::Unbounded, first_upper.cloned())); *new.first_mut().unwrap() = (clipped, first_node_id); let &(ref last_range, last_node_id) = new.last().unwrap(); let last_lower = last_range.bounding_range().unwrap().0; - let clipped = Range::from_range_bounds((last_lower.cloned(), Bound::Unbounded)); + let clipped = Ranges::from_range_bounds((last_lower.cloned(), Bound::Unbounded)); *new.last_mut().unwrap() = (clipped, last_node_id); self.create_node(node.var.clone(), Edges::Version { edges: new }) @@ -459,7 +457,7 @@ impl InternerGuard<'_> { return i; } - let py_range = Range::from_range_bounds((py_lower.cloned(), py_upper.cloned())); + let py_range = Ranges::from_range_bounds((py_lower.cloned(), py_upper.cloned())); if py_range.is_empty() { // Oops, the bounds imply there is nothing that can match, // so we always evaluate to false. @@ -521,14 +519,14 @@ impl InternerGuard<'_> { // adjacent ranges map to the same node, which would not be // a canonical representation. if exclude_node_id == first_node_id { - let clipped = Range::from_range_bounds((Bound::Unbounded, first_upper.cloned())); + let clipped = Ranges::from_range_bounds((Bound::Unbounded, first_upper.cloned())); *new.first_mut().unwrap() = (clipped, first_node_id); } else { - let clipped = Range::from_range_bounds((py_lower.cloned(), first_upper.cloned())); + let clipped = Ranges::from_range_bounds((py_lower.cloned(), first_upper.cloned())); *new.first_mut().unwrap() = (clipped, first_node_id); let py_range_lower = - Range::from_range_bounds((py_lower.cloned(), Bound::Unbounded)); + Ranges::from_range_bounds((py_lower.cloned(), Bound::Unbounded)); new.insert(0, (py_range_lower.complement(), NodeId::FALSE.negate(i))); } } @@ -539,14 +537,14 @@ impl InternerGuard<'_> { // same reasoning applies here: to maintain a canonical // representation. if exclude_node_id == last_node_id { - let clipped = Range::from_range_bounds((last_lower.cloned(), Bound::Unbounded)); + let clipped = Ranges::from_range_bounds((last_lower.cloned(), Bound::Unbounded)); *new.last_mut().unwrap() = (clipped, last_node_id); } else { - let clipped = Range::from_range_bounds((last_lower.cloned(), py_upper.cloned())); + let clipped = Ranges::from_range_bounds((last_lower.cloned(), py_upper.cloned())); *new.last_mut().unwrap() = (clipped, last_node_id); let py_range_upper = - Range::from_range_bounds((Bound::Unbounded, py_upper.cloned())); + Ranges::from_range_bounds((Bound::Unbounded, py_upper.cloned())); new.push((py_range_upper.complement(), exclude_node_id)); } } @@ -688,7 +686,7 @@ pub(crate) enum Edges { // Invariant: All ranges are simple, meaning they can be represented by a bounded // interval without gaps. Additionally, there are at least two edges in the set. Version { - edges: SmallVec<(Range, NodeId)>, + edges: SmallVec<(Ranges, NodeId)>, }, // The edges of a string variable, representing a disjoint set of ranges that cover // the output space. @@ -696,7 +694,7 @@ pub(crate) enum Edges { // Invariant: All ranges are simple, meaning they can be represented by a bounded // interval without gaps. Additionally, there are at least two edges in the set. String { - edges: SmallVec<(Range, NodeId)>, + edges: SmallVec<(Ranges, NodeId)>, }, // The edges of a boolean variable, representing the values `true` (the `high` child) // and `false` (the `low` child). @@ -727,13 +725,13 @@ impl Edges { /// This function will panic for the `In` and `Contains` marker operators, which /// should be represented as separate boolean variables. fn from_string(operator: MarkerOperator, value: String) -> Edges { - let range: Range = match operator { - MarkerOperator::Equal => Range::singleton(value), - MarkerOperator::NotEqual => Range::singleton(value).complement(), - MarkerOperator::GreaterThan => Range::strictly_higher_than(value), - MarkerOperator::GreaterEqual => Range::higher_than(value), - MarkerOperator::LessThan => Range::strictly_lower_than(value), - MarkerOperator::LessEqual => Range::lower_than(value), + let range: Ranges = match operator { + MarkerOperator::Equal => Ranges::singleton(value), + MarkerOperator::NotEqual => Ranges::singleton(value).complement(), + MarkerOperator::GreaterThan => Ranges::strictly_higher_than(value), + MarkerOperator::GreaterEqual => Ranges::higher_than(value), + MarkerOperator::LessThan => Ranges::strictly_lower_than(value), + MarkerOperator::LessEqual => Ranges::lower_than(value), MarkerOperator::TildeEqual => unreachable!("string comparisons with ~= are ignored"), _ => unreachable!("`in` and `contains` are treated as boolean variables"), }; @@ -745,10 +743,9 @@ impl Edges { /// Returns the [`Edges`] for a version specifier. fn from_specifier(specifier: VersionSpecifier) -> Edges { - let specifier = - PubGrubSpecifier::from_release_specifier(&normalize_specifier(specifier)).unwrap(); + let specifier = release_specifier_to_range(normalize_specifier(specifier)); Edges::Version { - edges: Edges::from_range(&specifier.into()), + edges: Edges::from_range(&specifier), } } @@ -756,16 +753,15 @@ impl Edges { /// /// Only for use when the `key` is a `PythonVersion`. Normalizes to `PythonFullVersion`. fn from_python_versions(versions: Vec, negated: bool) -> Result { - let mut range = Range::empty(); + let mut range = Ranges::empty(); // TODO(zanieb): We need to make sure this is performant, repeated unions like this do not // seem efficient. for version in versions { let specifier = VersionSpecifier::equals_version(version.clone()); let specifier = python_version_to_full_version(specifier)?; - let pubgrub_specifier = - PubGrubSpecifier::from_release_specifier(&normalize_specifier(specifier)).unwrap(); - range = range.union(&pubgrub_specifier.into()); + let pubgrub_specifier = release_specifier_to_range(normalize_specifier(specifier)); + range = range.union(&pubgrub_specifier); } if negated { @@ -779,12 +775,12 @@ impl Edges { /// Returns an [`Edges`] where values in the given range are `true`. fn from_versions(versions: &Vec, negated: bool) -> Edges { - let mut range = Range::empty(); + let mut range = Ranges::empty(); // TODO(zanieb): We need to make sure this is performant, repeated unions like this do not // seem efficient. for version in versions { - range = range.union(&Range::singleton(version.clone())); + range = range.union(&Ranges::singleton(version.clone())); } if negated { @@ -797,7 +793,7 @@ impl Edges { } /// Returns an [`Edges`] where values in the given range are `true`. - fn from_range(range: &Range) -> SmallVec<(Range, NodeId)> + fn from_range(range: &Ranges) -> SmallVec<(Ranges, NodeId)> where T: Ord + Clone, { @@ -805,13 +801,13 @@ impl Edges { // Add the `true` edges. for (start, end) in range.iter() { - let range = Range::from_range_bounds((start.clone(), end.clone())); + let range = Ranges::from_range_bounds((start.clone(), end.clone())); edges.push((range, NodeId::TRUE)); } // Add the `false` edges. for (start, end) in range.complement().iter() { - let range = Range::from_range_bounds((start.clone(), end.clone())); + let range = Ranges::from_range_bounds((start.clone(), end.clone())); edges.push((range, NodeId::FALSE)); } @@ -891,12 +887,12 @@ impl Edges { /// In that case, we drop any ranges that do not exist in the domain of both edges. Note that /// this should not occur in practice because `requires-python` bounds are global. fn apply_ranges( - left_edges: &SmallVec<(Range, NodeId)>, + left_edges: &SmallVec<(Ranges, NodeId)>, left_parent: NodeId, - right_edges: &SmallVec<(Range, NodeId)>, + right_edges: &SmallVec<(Ranges, NodeId)>, right_parent: NodeId, mut apply: impl FnMut(NodeId, NodeId) -> NodeId, - ) -> SmallVec<(Range, NodeId)> + ) -> SmallVec<(Ranges, NodeId)> where T: Clone + Ord, { @@ -968,9 +964,9 @@ impl Edges { // Returns `true` if all intersecting ranges in two range maps are disjoint. fn is_disjoint_ranges( - left_edges: &SmallVec<(Range, NodeId)>, + left_edges: &SmallVec<(Ranges, NodeId)>, left_parent: NodeId, - right_edges: &SmallVec<(Range, NodeId)>, + right_edges: &SmallVec<(Ranges, NodeId)>, right_parent: NodeId, interner: &mut InternerGuard<'_>, ) -> bool @@ -1179,7 +1175,7 @@ fn python_version_to_full_version(specifier: VersionSpecifier) -> Result(range1: &Range, range2: &Range) -> Ordering +fn compare_disjoint_range_start(range1: &Ranges, range2: &Ranges) -> Ordering where T: Ord, { @@ -1199,7 +1195,7 @@ where } /// Returns `true` if two disjoint ranges can be conjoined seamlessly without introducing a gap. -fn can_conjoin(range1: &Range, range2: &Range) -> bool +fn can_conjoin(range1: &Ranges, range2: &Ranges) -> bool where T: Ord + Clone, { diff --git a/crates/uv-pep508/src/marker/simplify.rs b/crates/uv-pep508/src/marker/simplify.rs index 3c10d9a9e5c8..82230f373cb3 100644 --- a/crates/uv-pep508/src/marker/simplify.rs +++ b/crates/uv-pep508/src/marker/simplify.rs @@ -3,9 +3,9 @@ use std::ops::Bound; use indexmap::IndexMap; use itertools::Itertools; -use pubgrub::Range; use rustc_hash::FxBuildHasher; use uv_pep440::{Version, VersionSpecifier}; +use version_ranges::Ranges; use crate::{ExtraOperator, MarkerExpression, MarkerOperator, MarkerTree, MarkerTreeKind}; @@ -280,17 +280,17 @@ fn simplify(dnf: &mut Vec>) { /// Merge any edges that lead to identical subtrees into a single range. pub(crate) fn collect_edges<'a, T>( - map: impl ExactSizeIterator, MarkerTree)>, -) -> IndexMap, FxBuildHasher> + map: impl ExactSizeIterator, MarkerTree)>, +) -> IndexMap, FxBuildHasher> where T: Ord + Clone + 'a, { - let mut paths: IndexMap<_, Range<_>, FxBuildHasher> = IndexMap::default(); + let mut paths: IndexMap<_, Ranges<_>, FxBuildHasher> = IndexMap::default(); for (range, tree) in map { // OK because all ranges are guaranteed to be non-empty. let (start, end) = range.bounding_range().unwrap(); // Combine the ranges. - let range = Range::from_range_bounds((start.cloned(), end.cloned())); + let range = Ranges::from_range_bounds((start.cloned(), end.cloned())); paths .entry(tree) .and_modify(|union| *union = union.union(&range)) @@ -305,7 +305,7 @@ where /// /// For example, `os_name < 'Linux' or os_name > 'Linux'` can be simplified to /// `os_name != 'Linux'`. -fn range_inequality(range: &Range) -> Option> +fn range_inequality(range: &Ranges) -> Option> where T: Ord + Clone + fmt::Debug, { @@ -329,7 +329,7 @@ where /// /// For example, `python_full_version < '3.8' or python_full_version >= '3.9'` can be simplified to /// `python_full_version != '3.8.*'`. -fn star_range_inequality(range: &Range) -> Option { +fn star_range_inequality(range: &Ranges) -> Option { let (b1, b2) = range.iter().collect_tuple()?; match (b1, b2) { diff --git a/crates/uv-pep508/src/marker/tree.rs b/crates/uv-pep508/src/marker/tree.rs index 677ce64038d2..5959de032293 100644 --- a/crates/uv-pep508/src/marker/tree.rs +++ b/crates/uv-pep508/src/marker/tree.rs @@ -5,10 +5,10 @@ use std::ops::{Bound, Deref}; use std::str::FromStr; use itertools::Itertools; -use pubgrub::Range; use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; use uv_normalize::ExtraName; use uv_pep440::{Version, VersionParseError, VersionSpecifier}; +use version_ranges::Ranges; use crate::cursor::Cursor; use crate::marker::parse; @@ -1396,7 +1396,7 @@ pub enum MarkerTreeKind<'a> { pub struct VersionMarkerTree<'a> { id: NodeId, key: MarkerValueVersion, - map: &'a [(Range, NodeId)], + map: &'a [(Ranges, NodeId)], } impl VersionMarkerTree<'_> { @@ -1406,7 +1406,7 @@ impl VersionMarkerTree<'_> { } /// The edges of this node, corresponding to possible output ranges of the given variable. - pub fn edges(&self) -> impl ExactSizeIterator, MarkerTree)> + '_ { + pub fn edges(&self) -> impl ExactSizeIterator, MarkerTree)> + '_ { self.map .iter() .map(|(range, node)| (range, MarkerTree(node.negate(self.id)))) @@ -1432,7 +1432,7 @@ impl Ord for VersionMarkerTree<'_> { pub struct StringMarkerTree<'a> { id: NodeId, key: MarkerValueString, - map: &'a [(Range, NodeId)], + map: &'a [(Ranges, NodeId)], } impl StringMarkerTree<'_> { @@ -1442,7 +1442,7 @@ impl StringMarkerTree<'_> { } /// The edges of this node, corresponding to possible output ranges of the given variable. - pub fn children(&self) -> impl ExactSizeIterator, MarkerTree)> { + pub fn children(&self) -> impl ExactSizeIterator, MarkerTree)> { self.map .iter() .map(|(range, node)| (range, MarkerTree(node.negate(self.id)))) @@ -1937,59 +1937,66 @@ mod test { #[test] #[cfg(feature = "tracing")] - fn warnings() { + #[tracing_test::traced_test] + fn warnings1() { let env37 = env37(); - testing_logger::setup(); let compare_keys = MarkerTree::from_str("platform_version == sys_platform").unwrap(); compare_keys.evaluate(&env37, &[]); - testing_logger::validate(|captured_logs| { - assert_eq!( - captured_logs[0].body, - "Comparing two markers with each other doesn't make any sense, will evaluate to false" - ); - assert_eq!(captured_logs[0].level, log::Level::Warn); - assert_eq!(captured_logs.len(), 1); - }); + logs_contain( + "Comparing two markers with each other doesn't make any sense, will evaluate to false", + ); + } + + #[test] + #[cfg(feature = "tracing")] + #[tracing_test::traced_test] + fn warnings2() { + let env37 = env37(); let non_pep440 = MarkerTree::from_str("python_version >= '3.9.'").unwrap(); non_pep440.evaluate(&env37, &[]); - testing_logger::validate(|captured_logs| { - assert_eq!( - captured_logs[0].body, - "Expected PEP 440 version to compare with python_version, found `3.9.`, \ - will evaluate to false: after parsing `3.9`, found `.`, which is \ - not part of a valid version" - ); - assert_eq!(captured_logs[0].level, log::Level::Warn); - assert_eq!(captured_logs.len(), 1); - }); + logs_contain( + "Expected PEP 440 version to compare with python_version, found `3.9.`, \ + will evaluate to false: after parsing `3.9`, found `.`, which is \ + not part of a valid version", + ); + } + + #[test] + #[cfg(feature = "tracing")] + #[tracing_test::traced_test] + fn warnings3() { + let env37 = env37(); let string_string = MarkerTree::from_str("'b' >= 'a'").unwrap(); string_string.evaluate(&env37, &[]); - testing_logger::validate(|captured_logs| { - assert_eq!( - captured_logs[0].body, - "Comparing two quoted strings with each other doesn't make sense: 'b' >= 'a', will evaluate to false" - ); - assert_eq!(captured_logs[0].level, log::Level::Warn); - assert_eq!(captured_logs.len(), 1); - }); + logs_contain( + "Comparing two quoted strings with each other doesn't make sense: 'b' >= 'a', will evaluate to false" + ); + } + + #[test] + #[cfg(feature = "tracing")] + #[tracing_test::traced_test] + fn warnings4() { + let env37 = env37(); let string_string = MarkerTree::from_str(r"os.name == 'posix' and platform.machine == 'x86_64' and platform.python_implementation == 'CPython' and 'Ubuntu' in platform.version and sys.platform == 'linux'").unwrap(); string_string.evaluate(&env37, &[]); - testing_logger::validate(|captured_logs| { - let messages: Vec<_> = captured_logs + logs_assert(|lines: &[&str]| { + let lines: Vec<_> = lines .iter() - .map(|message| { - assert_eq!(message.level, log::Level::Warn); - &message.body - }) + .map(|s| s.split_once(" ").unwrap().1) .collect(); - let expected = [ - "os.name is deprecated in favor of os_name", - "platform.machine is deprecated in favor of platform_machine", - "platform.python_implementation is deprecated in favor of platform_python_implementation", - "platform.version is deprecated in favor of platform_version", - "sys.platform is deprecated in favor of sys_platform" + let expected = [ + "WARN warnings4: uv_pep508: os.name is deprecated in favor of os_name", + "WARN warnings4: uv_pep508: platform.machine is deprecated in favor of platform_machine", + "WARN warnings4: uv_pep508: platform.python_implementation is deprecated in favor of", + "WARN warnings4: uv_pep508: sys.platform is deprecated in favor of sys_platform", + "WARN warnings4: uv_pep508: Comparing linux and posix lexicographically" ]; - assert_eq!(messages, &expected); + if lines == expected { + Ok(()) + } else { + Err(format!("{lines:?}")) + } }); } diff --git a/crates/uv-pep508/src/tests.rs b/crates/uv-pep508/src/tests.rs index 970202ff4976..8ac718847d86 100644 --- a/crates/uv-pep508/src/tests.rs +++ b/crates/uv-pep508/src/tests.rs @@ -712,6 +712,7 @@ fn error_invalid_extra_unnamed_url() { /// Check that the relative path support feature toggle works. #[test] +#[cfg(feature = "non-pep508-extensions")] fn non_pep508_paths() { let requirements = &[ "foo @ file://./foo", @@ -748,6 +749,7 @@ fn no_space_after_operator() { } #[test] +#[cfg(feature = "non-pep508-extensions")] fn path_with_fragment() { let requirements = if cfg!(windows) { &[ diff --git a/crates/uv-pep508/src/verbatim_url.rs b/crates/uv-pep508/src/verbatim_url.rs index f75539d2ed2b..5d2f62597cbd 100644 --- a/crates/uv-pep508/src/verbatim_url.rs +++ b/crates/uv-pep508/src/verbatim_url.rs @@ -140,6 +140,7 @@ impl VerbatimUrl { } /// Return the underlying [`Path`], if the URL is a file URL. + #[cfg(feature = "non-pep508-extensions")] pub fn as_path(&self) -> Result { self.url .to_file_path() @@ -212,11 +213,8 @@ impl Pep508Url for VerbatimUrl { type Err = VerbatimUrlError; /// Create a `VerbatimUrl` to represent the requirement. - fn parse_url( - url: &str, - #[cfg_attr(not(feature = "non-pep508-extensions"), allow(unused_variables))] - working_dir: Option<&Path>, - ) -> Result { + #[cfg_attr(not(feature = "non-pep508-extensions"), allow(unused_variables))] + fn parse_url(url: &str, working_dir: Option<&Path>) -> Result { // Expand environment variables in the URL. let expanded = expand_env_vars(url); @@ -225,18 +223,24 @@ impl Pep508Url for VerbatimUrl { // Ex) `file:///home/ferris/project/scripts/...`, `file://localhost/home/ferris/project/scripts/...`, or `file:../ferris/` Some(Scheme::File) => { // Strip the leading slashes, along with the `localhost` host, if present. - let path = strip_host(path); // Transform, e.g., `/C:/Users/ferris/wheel-0.42.0.tar.gz` to `C:\Users\ferris\wheel-0.42.0.tar.gz`. - let path = normalize_url_path(path); - #[cfg(feature = "non-pep508-extensions")] - if let Some(working_dir) = working_dir { - return Ok(VerbatimUrl::from_path(path.as_ref(), working_dir)? - .with_given(url.to_string())); - } + { + let path = strip_host(path); + + let path = normalize_url_path(path); - Ok(VerbatimUrl::from_absolute_path(path.as_ref())?.with_given(url.to_string())) + if let Some(working_dir) = working_dir { + return Ok(VerbatimUrl::from_path(path.as_ref(), working_dir)? + .with_given(url.to_string())); + } + + Ok(VerbatimUrl::from_absolute_path(path.as_ref())? + .with_given(url.to_string())) + } + #[cfg(not(feature = "non-pep508-extensions"))] + Ok(VerbatimUrl::parse_url(expanded)?.with_given(url.to_string())) } // Ex) `https://download.pytorch.org/whl/torch_stable.html` @@ -248,24 +252,33 @@ impl Pep508Url for VerbatimUrl { // Ex) `C:\Users\ferris\wheel-0.42.0.tar.gz` _ => { #[cfg(feature = "non-pep508-extensions")] - if let Some(working_dir) = working_dir { - return Ok(VerbatimUrl::from_path(expanded.as_ref(), working_dir)? - .with_given(url.to_string())); + { + if let Some(working_dir) = working_dir { + return Ok(VerbatimUrl::from_path(expanded.as_ref(), working_dir)? + .with_given(url.to_string())); + } + + Ok(VerbatimUrl::from_absolute_path(expanded.as_ref())? + .with_given(url.to_string())) } - - Ok(VerbatimUrl::from_absolute_path(expanded.as_ref())? - .with_given(url.to_string())) + #[cfg(not(feature = "non-pep508-extensions"))] + Err(Self::Err::NotAUrl(expanded.to_string())) } } } else { // Ex) `../editable/` #[cfg(feature = "non-pep508-extensions")] - if let Some(working_dir) = working_dir { - return Ok(VerbatimUrl::from_path(expanded.as_ref(), working_dir)? - .with_given(url.to_string())); + { + if let Some(working_dir) = working_dir { + return Ok(VerbatimUrl::from_path(expanded.as_ref(), working_dir)? + .with_given(url.to_string())); + } + + Ok(VerbatimUrl::from_absolute_path(expanded.as_ref())?.with_given(url.to_string())) } - Ok(VerbatimUrl::from_absolute_path(expanded.as_ref())?.with_given(url.to_string())) + #[cfg(not(feature = "non-pep508-extensions"))] + Err(Self::Err::NotAUrl(expanded.to_string())) } } } @@ -288,6 +301,11 @@ pub enum VerbatimUrlError { /// Received a path that could not be normalized. #[error("path could not be normalized: {0}")] Normalization(PathBuf, #[source] std::io::Error), + + /// Received a path that could not be normalized. + #[cfg(not(feature = "non-pep508-extensions"))] + #[error("Not a URL (missing scheme): {0}")] + NotAUrl(String), } /// Expand all available environment variables. diff --git a/crates/uv-platform-tags/src/platform.rs b/crates/uv-platform-tags/src/platform.rs index 97e921a9e685..35b6ae843992 100644 --- a/crates/uv-platform-tags/src/platform.rs +++ b/crates/uv-platform-tags/src/platform.rs @@ -86,6 +86,7 @@ pub enum Arch { #[serde(alias = "amd64")] X86_64, S390X, + Riscv64, } impl fmt::Display for Arch { @@ -99,6 +100,7 @@ impl fmt::Display for Arch { Self::X86 => write!(f, "i686"), Self::X86_64 => write!(f, "x86_64"), Self::S390X => write!(f, "s390x"), + Self::Riscv64 => write!(f, "riscv64"), } } } @@ -115,7 +117,7 @@ impl Arch { // manylinux 1 Self::X86 | Self::X86_64 => Some(5), // unsupported - Self::Armv6L => None, + Self::Armv6L | Self::Riscv64 => None, } } } diff --git a/crates/uv-pubgrub/Cargo.toml b/crates/uv-pubgrub/Cargo.toml deleted file mode 100644 index 8c975311221f..000000000000 --- a/crates/uv-pubgrub/Cargo.toml +++ /dev/null @@ -1,18 +0,0 @@ -[package] -name = "uv-pubgrub" -version = "0.0.1" -edition = "2021" -description = "Common uv pubgrub types." - -[lib] -doctest = false - -[lints] -workspace = true - -[dependencies] -uv-pep440 = { workspace = true } - -itertools = { workspace = true } -thiserror = { workspace = true } -pubgrub = { workspace = true } diff --git a/crates/uv-pubgrub/src/lib.rs b/crates/uv-pubgrub/src/lib.rs deleted file mode 100644 index bf07a3639475..000000000000 --- a/crates/uv-pubgrub/src/lib.rs +++ /dev/null @@ -1,256 +0,0 @@ -use std::ops::Bound; - -use itertools::Itertools; -use pubgrub::Range; -use thiserror::Error; - -use uv_pep440::{Operator, Prerelease, Version, VersionSpecifier, VersionSpecifiers}; - -#[derive(Debug, Error)] -pub enum PubGrubSpecifierError { - #[error("~= operator requires at least two release segments: `{0}`")] - InvalidTildeEquals(VersionSpecifier), -} - -/// A range of versions that can be used to satisfy a requirement. -#[derive(Debug)] -pub struct PubGrubSpecifier(Range); - -impl PubGrubSpecifier { - /// Returns an iterator over the bounds of the [`PubGrubSpecifier`]. - pub fn iter(&self) -> impl Iterator, &Bound)> { - self.0.iter() - } - - /// Return the bounding [`Range`] of the [`PubGrubSpecifier`]. - pub fn bounding_range(&self) -> Option<(Bound<&Version>, Bound<&Version>)> { - self.0.bounding_range() - } -} - -impl From> for PubGrubSpecifier { - fn from(range: Range) -> Self { - PubGrubSpecifier(range) - } -} - -impl From for Range { - /// Convert a PubGrub specifier to a range of versions. - fn from(specifier: PubGrubSpecifier) -> Self { - specifier.0 - } -} - -impl PubGrubSpecifier { - /// Convert [`VersionSpecifiers`] to a PubGrub-compatible version range, using PEP 440 - /// semantics. - pub fn from_pep440_specifiers( - specifiers: &VersionSpecifiers, - ) -> Result { - let range = specifiers - .iter() - .map(Self::from_pep440_specifier) - .fold_ok(Range::full(), |range, specifier| { - range.intersection(&specifier.into()) - })?; - Ok(Self(range)) - } - - /// Convert the [`VersionSpecifier`] to a PubGrub-compatible version range, using PEP 440 - /// semantics. - pub fn from_pep440_specifier( - specifier: &VersionSpecifier, - ) -> Result { - let ranges = match specifier.operator() { - Operator::Equal => { - let version = specifier.version().clone(); - Range::singleton(version) - } - Operator::ExactEqual => { - let version = specifier.version().clone(); - Range::singleton(version) - } - Operator::NotEqual => { - let version = specifier.version().clone(); - Range::singleton(version).complement() - } - Operator::TildeEqual => { - let [rest @ .., last, _] = specifier.version().release() else { - return Err(PubGrubSpecifierError::InvalidTildeEquals(specifier.clone())); - }; - let upper = Version::new(rest.iter().chain([&(last + 1)])) - .with_epoch(specifier.version().epoch()) - .with_dev(Some(0)); - let version = specifier.version().clone(); - Range::from_range_bounds(version..upper) - } - Operator::LessThan => { - let version = specifier.version().clone(); - if version.any_prerelease() { - Range::strictly_lower_than(version) - } else { - // Per PEP 440: "The exclusive ordered comparison { - let version = specifier.version().clone(); - Range::lower_than(version) - } - Operator::GreaterThan => { - // Per PEP 440: "The exclusive ordered comparison >V MUST NOT allow a post-release of - // the given version unless V itself is a post release." - let version = specifier.version().clone(); - if let Some(dev) = version.dev() { - Range::higher_than(version.with_dev(Some(dev + 1))) - } else if let Some(post) = version.post() { - Range::higher_than(version.with_post(Some(post + 1))) - } else { - Range::strictly_higher_than(version.with_max(Some(0))) - } - } - Operator::GreaterThanEqual => { - let version = specifier.version().clone(); - Range::higher_than(version) - } - Operator::EqualStar => { - let low = specifier.version().clone().with_dev(Some(0)); - let mut high = low.clone(); - if let Some(post) = high.post() { - high = high.with_post(Some(post + 1)); - } else if let Some(pre) = high.pre() { - high = high.with_pre(Some(Prerelease { - kind: pre.kind, - number: pre.number + 1, - })); - } else { - let mut release = high.release().to_vec(); - *release.last_mut().unwrap() += 1; - high = high.with_release(release); - } - Range::from_range_bounds(low..high) - } - Operator::NotEqualStar => { - let low = specifier.version().clone().with_dev(Some(0)); - let mut high = low.clone(); - if let Some(post) = high.post() { - high = high.with_post(Some(post + 1)); - } else if let Some(pre) = high.pre() { - high = high.with_pre(Some(Prerelease { - kind: pre.kind, - number: pre.number + 1, - })); - } else { - let mut release = high.release().to_vec(); - *release.last_mut().unwrap() += 1; - high = high.with_release(release); - } - Range::from_range_bounds(low..high).complement() - } - }; - - Ok(Self(ranges)) - } - - /// Convert the [`VersionSpecifiers`] to a PubGrub-compatible version range, using release-only - /// semantics. - /// - /// Assumes that the range will only be tested against versions that consist solely of release - /// segments (e.g., `3.12.0`, but not `3.12.0b1`). - /// - /// These semantics are used for testing Python compatibility (e.g., `requires-python` against - /// the user's installed Python version). In that context, it's more intuitive that `3.13.0b0` - /// is allowed for projects that declare `requires-python = ">3.13"`. - /// - /// See: - pub fn from_release_specifiers( - specifiers: &VersionSpecifiers, - ) -> Result { - let range = specifiers - .iter() - .map(Self::from_release_specifier) - .fold_ok(Range::full(), |range, specifier| { - range.intersection(&specifier.into()) - })?; - Ok(Self(range)) - } - - /// Convert the [`VersionSpecifier`] to a PubGrub-compatible version range, using release-only - /// semantics. - /// - /// Assumes that the range will only be tested against versions that consist solely of release - /// segments (e.g., `3.12.0`, but not `3.12.0b1`). - /// - /// These semantics are used for testing Python compatibility (e.g., `requires-python` against - /// the user's installed Python version). In that context, it's more intuitive that `3.13.0b0` - /// is allowed for projects that declare `requires-python = ">3.13"`. - /// - /// See: - pub fn from_release_specifier( - specifier: &VersionSpecifier, - ) -> Result { - let ranges = match specifier.operator() { - Operator::Equal => { - let version = specifier.version().only_release(); - Range::singleton(version) - } - Operator::ExactEqual => { - let version = specifier.version().only_release(); - Range::singleton(version) - } - Operator::NotEqual => { - let version = specifier.version().only_release(); - Range::singleton(version).complement() - } - Operator::TildeEqual => { - let [rest @ .., last, _] = specifier.version().release() else { - return Err(PubGrubSpecifierError::InvalidTildeEquals(specifier.clone())); - }; - let upper = Version::new(rest.iter().chain([&(last + 1)])); - let version = specifier.version().only_release(); - Range::from_range_bounds(version..upper) - } - Operator::LessThan => { - let version = specifier.version().only_release(); - Range::strictly_lower_than(version) - } - Operator::LessThanEqual => { - let version = specifier.version().only_release(); - Range::lower_than(version) - } - Operator::GreaterThan => { - let version = specifier.version().only_release(); - Range::strictly_higher_than(version) - } - Operator::GreaterThanEqual => { - let version = specifier.version().only_release(); - Range::higher_than(version) - } - Operator::EqualStar => { - let low = specifier.version().only_release(); - let high = { - let mut high = low.clone(); - let mut release = high.release().to_vec(); - *release.last_mut().unwrap() += 1; - high = high.with_release(release); - high - }; - Range::from_range_bounds(low..high) - } - Operator::NotEqualStar => { - let low = specifier.version().only_release(); - let high = { - let mut high = low.clone(); - let mut release = high.release().to_vec(); - *release.last_mut().unwrap() += 1; - high = high.with_release(release); - high - }; - Range::from_range_bounds(low..high).complement() - } - }; - Ok(Self(ranges)) - } -} diff --git a/crates/uv-publish/Cargo.toml b/crates/uv-publish/Cargo.toml index 6c640f70a876..06f4fb9401eb 100644 --- a/crates/uv-publish/Cargo.toml +++ b/crates/uv-publish/Cargo.toml @@ -13,9 +13,12 @@ license.workspace = true doctest = false [dependencies] +uv-cache = { workspace = true } uv-client = { workspace = true } uv-configuration = { workspace = true } uv-distribution-filename = { workspace = true } +uv-distribution-types = { workspace = true } +uv-extract = { workspace = true } uv-fs = { workspace = true } uv-metadata = { workspace = true } uv-pypi-types = { workspace = true } @@ -35,7 +38,6 @@ reqwest-retry = { workspace = true } rustc-hash = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } -sha2 = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true } tokio-util = { workspace = true , features = ["io"] } diff --git a/crates/uv-publish/src/lib.rs b/crates/uv-publish/src/lib.rs index d3a68a1c6509..2c4cd76897f6 100644 --- a/crates/uv-publish/src/lib.rs +++ b/crates/uv-publish/src/lib.rs @@ -3,7 +3,7 @@ mod trusted_publishing; use crate::trusted_publishing::TrustedPublishingError; use base64::prelude::BASE64_STANDARD; use base64::Engine; -use fs_err::File; +use fs_err::tokio::File; use futures::TryStreamExt; use glob::{glob, GlobError, PatternError}; use itertools::Itertools; @@ -14,26 +14,27 @@ use reqwest_middleware::RequestBuilder; use reqwest_retry::{Retryable, RetryableStrategy}; use rustc_hash::FxHashSet; use serde::Deserialize; -use sha2::{Digest, Sha256}; -use std::io::BufReader; use std::path::{Path, PathBuf}; use std::sync::Arc; use std::{env, fmt, io}; use thiserror::Error; -use tokio::io::AsyncReadExt; +use tokio::io::{AsyncReadExt, BufReader}; use tokio_util::io::ReaderStream; use tracing::{debug, enabled, trace, Level}; use url::Url; -use uv_client::{BaseClient, UvRetryableStrategy}; +use uv_client::{BaseClient, OwnedArchive, RegistryClientBuilder, UvRetryableStrategy}; use uv_configuration::{KeyringProviderType, TrustedPublishing}; use uv_distribution_filename::{DistFilename, SourceDistExtension, SourceDistFilename}; use uv_fs::{ProgressReader, Simplified}; use uv_metadata::read_metadata_async_seek; -use uv_pypi_types::{Metadata23, MetadataError}; +use uv_pypi_types::{HashAlgorithm, HashDigest, Metadata23, MetadataError}; use uv_static::EnvVars; use uv_warnings::{warn_user, warn_user_once}; pub use trusted_publishing::TrustedPublishingToken; +use uv_cache::{Cache, Refresh}; +use uv_distribution_types::{IndexCapabilities, IndexUrl}; +use uv_extract::hash::{HashReader, Hasher}; #[derive(Error, Debug)] pub enum PublishError { @@ -54,6 +55,22 @@ pub enum PublishError { PublishSend(PathBuf, Url, #[source] PublishSendError), #[error("Failed to obtain token for trusted publishing")] TrustedPublishing(#[from] TrustedPublishingError), + #[error("{0} are not allowed when using trusted publishing")] + MixedCredentials(String), + #[error("Failed to query check URL")] + CheckUrlIndex(#[source] uv_client::Error), + #[error( + "Local file and index file do not match for {filename}. \ + Local: {hash_algorithm}={local}, Remote: {hash_algorithm}={remote}" + )] + HashMismatch { + filename: Box, + hash_algorithm: HashAlgorithm, + local: Box, + remote: Box, + }, + #[error("Hash is missing in index for {0}")] + MissingHash(Box), } /// Failure to get the metadata for a specific file. @@ -79,7 +96,7 @@ pub enum PublishPrepareError { #[derive(Error, Debug)] pub enum PublishSendError { #[error("Failed to send POST request")] - ReqwestMiddleware(#[from] reqwest_middleware::Error), + ReqwestMiddleware(#[source] reqwest_middleware::Error), #[error("Upload failed with status {0}")] StatusNoBody(StatusCode, #[source] reqwest::Error), #[error("Upload failed with status code {0}. Server says: {1}")] @@ -103,6 +120,15 @@ pub trait Reporter: Send + Sync + 'static { fn on_download_complete(&self, id: usize); } +/// Context for using a fresh registry client for check URL requests. +pub struct CheckUrlClient<'a> { + pub index_url: IndexUrl, + pub registry_client_builder: RegistryClientBuilder<'a>, + pub client: &'a BaseClient, + pub index_capabilities: IndexCapabilities, + pub cache: &'a Cache, +} + impl PublishSendError { /// Extract `code` from the PyPI json error response, if any. /// @@ -239,6 +265,15 @@ pub fn files_for_publishing( Ok(files) } +pub enum TrustedPublishResult { + /// We didn't check for trusted publishing. + Skipped, + /// We checked for trusted publishing and found a token. + Configured(TrustedPublishingToken), + /// We checked for optional trusted publishing, but it didn't succeed. + Ignored(TrustedPublishingError), +} + /// If applicable, attempt obtaining a token for trusted publishing. pub async fn check_trusted_publishing( username: Option<&str>, @@ -247,7 +282,7 @@ pub async fn check_trusted_publishing( trusted_publishing: TrustedPublishing, registry: &Url, client: &BaseClient, -) -> Result, PublishError> { +) -> Result { match trusted_publishing { TrustedPublishing::Automatic => { // If the user provided credentials, use those. @@ -255,28 +290,43 @@ pub async fn check_trusted_publishing( || password.is_some() || keyring_provider != KeyringProviderType::Disabled { - return Ok(None); + return Ok(TrustedPublishResult::Skipped); } // If we aren't in GitHub Actions, we can't use trusted publishing. if env::var(EnvVars::GITHUB_ACTIONS) != Ok("true".to_string()) { - return Ok(None); + return Ok(TrustedPublishResult::Skipped); } // We could check for credentials from the keyring or netrc the auth middleware first, but // given that we are in GitHub Actions we check for trusted publishing first. debug!("Running on GitHub Actions without explicit credentials, checking for trusted publishing"); match trusted_publishing::get_token(registry, client.for_host(registry)).await { - Ok(token) => Ok(Some(token)), + Ok(token) => Ok(TrustedPublishResult::Configured(token)), Err(err) => { // TODO(konsti): It would be useful if we could differentiate between actual errors // such as connection errors and warn for them while ignoring errors from trusted // publishing not being configured. debug!("Could not obtain trusted publishing credentials, skipping: {err}"); - Ok(None) + Ok(TrustedPublishResult::Ignored(err)) } } } TrustedPublishing::Always => { debug!("Using trusted publishing for GitHub Actions"); + + let mut conflicts = Vec::new(); + if username.is_some() { + conflicts.push("a username"); + } + if password.is_some() { + conflicts.push("a password"); + } + if keyring_provider != KeyringProviderType::Disabled { + conflicts.push("the keyring"); + } + if !conflicts.is_empty() { + return Err(PublishError::MixedCredentials(conflicts.join(" and "))); + } + if env::var(EnvVars::GITHUB_ACTIONS) != Ok("true".to_string()) { warn_user_once!( "Trusted publishing was requested, but you're not in GitHub Actions." @@ -284,9 +334,9 @@ pub async fn check_trusted_publishing( } let token = trusted_publishing::get_token(registry, client.for_host(registry)).await?; - Ok(Some(token)) + Ok(TrustedPublishResult::Configured(token)) } - TrustedPublishing::Never => Ok(None), + TrustedPublishing::Never => Ok(TrustedPublishResult::Skipped), } } @@ -304,6 +354,7 @@ pub async fn upload( retries: u32, username: Option<&str>, password: Option<&str>, + check_url_client: Option<&CheckUrlClient<'_>>, reporter: Arc, ) -> Result { let form_metadata = form_metadata(file, filename) @@ -336,29 +387,142 @@ pub async fn upload( } let response = result.map_err(|err| { - PublishError::PublishSend(file.to_path_buf(), registry.clone(), err.into()) + PublishError::PublishSend( + file.to_path_buf(), + registry.clone(), + PublishSendError::ReqwestMiddleware(err), + ) })?; - return handle_response(registry, response) + return match handle_response(registry, response).await { + Ok(()) => { + // Upload successful; for PyPI this can also mean a hash match in a raced upload + // (but it doesn't tell us), for other registries it should mean a fresh upload. + Ok(true) + } + Err(err) => { + if matches!( + err, + PublishSendError::Status(..) | PublishSendError::StatusNoBody(..) + ) { + if let Some(check_url_client) = &check_url_client { + if check_url(check_url_client, file, filename).await? { + // There was a raced upload of the same file, so even though our upload failed, + // the right file now exists in the registry. + return Ok(false); + } + } + } + Err(PublishError::PublishSend( + file.to_path_buf(), + registry.clone(), + err, + )) + } + }; + } +} + +/// Check whether we should skip the upload of a file because it already exists on the index. +pub async fn check_url( + check_url_client: &CheckUrlClient<'_>, + file: &Path, + filename: &DistFilename, +) -> Result { + let CheckUrlClient { + index_url, + registry_client_builder, + client, + index_capabilities, + cache, + } = check_url_client; + + // Avoid using the PyPI 10min default cache. + let cache_refresh = (*cache) + .clone() + .with_refresh(Refresh::from_args(None, vec![filename.name().clone()])); + let registry_client = registry_client_builder + .clone() + .cache(cache_refresh) + .wrap_existing(client); + + debug!("Checking for {filename} in the registry"); + let response = registry_client + .simple(filename.name(), Some(index_url), index_capabilities) + .await + .map_err(PublishError::CheckUrlIndex)?; + let [(_, simple_metadata)] = response.as_slice() else { + unreachable!("We queried a single index, we must get a single response"); + }; + let simple_metadata = OwnedArchive::deserialize(simple_metadata); + let Some(metadatum) = simple_metadata + .iter() + .find(|metadatum| &metadatum.version == filename.version()) + else { + return Ok(false); + }; + + let archived_file = match filename { + DistFilename::SourceDistFilename(source_dist) => metadatum + .files + .source_dists + .iter() + .find(|entry| &entry.name == source_dist) + .map(|entry| &entry.file), + DistFilename::WheelFilename(wheel) => metadatum + .files + .wheels + .iter() + .find(|entry| &entry.name == wheel) + .map(|entry| &entry.file), + }; + let Some(archived_file) = archived_file else { + return Ok(false); + }; + + // TODO(konsti): Do we have a preference for a hash here? + if let Some(remote_hash) = archived_file.hashes.first() { + // We accept the risk for TOCTOU errors here, since we already read the file once before the + // streaming upload to compute the hash for the form metadata. + let local_hash = hash_file(file, Hasher::from(remote_hash.algorithm)) .await - .map_err(|err| PublishError::PublishSend(file.to_path_buf(), registry.clone(), err)); + .map_err(|err| { + PublishError::PublishPrepare( + file.to_path_buf(), + Box::new(PublishPrepareError::Io(err)), + ) + })?; + if local_hash.digest == remote_hash.digest { + debug!( + "Found {filename} in the registry with matching hash {}", + remote_hash.digest + ); + Ok(true) + } else { + Err(PublishError::HashMismatch { + filename: Box::new(filename.clone()), + hash_algorithm: remote_hash.algorithm, + local: local_hash.digest, + remote: remote_hash.digest.clone(), + }) + } + } else { + Err(PublishError::MissingHash(Box::new(filename.clone()))) } } /// Calculate the SHA256 of a file. -fn hash_file(path: impl AsRef) -> Result { - // Ideally, this would be async, but in case we actually want to make parallel uploads we should - // use `spawn_blocking` since sha256 is cpu intensive. - let mut file = BufReader::new(File::open(path.as_ref())?); - let mut hasher = Sha256::new(); - io::copy(&mut file, &mut hasher)?; - Ok(format!("{:x}", hasher.finalize())) +async fn hash_file(path: impl AsRef, hasher: Hasher) -> Result { + debug!("Hashing {}", path.as_ref().display()); + let file = BufReader::new(File::open(path.as_ref()).await?); + let mut hashers = vec![hasher]; + HashReader::new(file, &mut hashers).finish().await?; + Ok(HashDigest::from(hashers.remove(0))) } // Not in `uv-metadata` because we only support tar files here. async fn source_dist_pkg_info(file: &Path) -> Result, PublishPrepareError> { - let file = fs_err::tokio::File::open(&file).await?; - let reader = tokio::io::BufReader::new(file); + let reader = BufReader::new(File::open(&file).await?); let decoded = async_compression::tokio::bufread::GzipDecoder::new(reader); let mut archive = tokio_tar::Archive::new(decoded); let mut pkg_infos: Vec<(PathBuf, Vec)> = archive @@ -411,8 +575,7 @@ async fn metadata(file: &Path, filename: &DistFilename) -> Result { - let file = fs_err::tokio::File::open(&file).await?; - let reader = tokio::io::BufReader::new(file); + let reader = BufReader::new(File::open(&file).await?); read_metadata_async_seek(wheel, reader).await? } }; @@ -426,13 +589,13 @@ async fn form_metadata( file: &Path, filename: &DistFilename, ) -> Result, PublishPrepareError> { - let hash_hex = hash_file(file)?; + let hash_hex = hash_file(file, Hasher::from(HashAlgorithm::Sha256)).await?; let metadata = metadata(file, filename).await?; let mut form_metadata = vec![ (":action", "file_upload".to_string()), - ("sha256_digest", hash_hex), + ("sha256_digest", hash_hex.digest.to_string()), ("protocol_version", "1".to_string()), ("metadata_version", metadata.metadata_version.clone()), // Twine transforms the name with `re.sub("[^A-Za-z0-9.]+", "-", name)` @@ -515,7 +678,7 @@ async fn build_request( form = form.text(*key, value.clone()); } - let file = fs_err::tokio::File::open(file).await?; + let file = File::open(file).await?; let idx = reporter.on_download_start(&filename.to_string(), Some(file.metadata().await?.len())); let reader = ProgressReader::new(file, move |read| { reporter.on_download_progress(idx, read as u64); @@ -561,8 +724,8 @@ async fn build_request( Ok((request, idx)) } -/// Returns `true` if the file was newly uploaded and `false` if it already existed. -async fn handle_response(registry: &Url, response: Response) -> Result { +/// Log response information and map response to an error variant if not successful. +async fn handle_response(registry: &Url, response: Response) -> Result<(), PublishSendError> { let status_code = response.status(); debug!("Response code for {registry}: {status_code}"); trace!("Response headers for {registry}: {response:?}"); @@ -589,7 +752,7 @@ async fn handle_response(registry: &Url, response: Response) -> Result Result Self { + match err { + VarError::NotPresent => Self::MissingEnvVar(env_var), + VarError::NotUnicode(os_string) => Self::InvalidEnvVar(env_var, os_string), + } + } +} + #[derive(Deserialize)] #[serde(transparent)] pub struct TrustedPublishingToken(String); -impl From for String { - fn from(token: TrustedPublishingToken) -> Self { - token.0 - } -} - impl Display for TrustedPublishingToken { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.0) @@ -75,7 +81,10 @@ pub(crate) async fn get_token( client: &ClientWithMiddleware, ) -> Result { // If this fails, we can skip the audience request. - let oidc_token_request_token = env::var(EnvVars::ACTIONS_ID_TOKEN_REQUEST_TOKEN)?; + let oidc_token_request_token = + env::var(EnvVars::ACTIONS_ID_TOKEN_REQUEST_TOKEN).map_err(|err| { + TrustedPublishingError::from_var_err(EnvVars::ACTIONS_ID_TOKEN_REQUEST_TOKEN, err) + })?; // Request 1: Get the audience let audience = get_audience(registry, client).await?; @@ -105,8 +114,17 @@ async fn get_audience( // (RFC 3986). let audience_url = Url::parse(&format!("https://{}/_/oidc/audience", registry.authority()))?; debug!("Querying the trusted publishing audience from {audience_url}"); - let response = client.get(audience_url).send().await?; - let audience = response.error_for_status()?.json::().await?; + let response = client + .get(audience_url.clone()) + .send() + .await + .map_err(|err| TrustedPublishingError::ReqwestMiddleware(audience_url.clone(), err))?; + let audience = response + .error_for_status() + .map_err(|err| TrustedPublishingError::Reqwest(audience_url.clone(), err))? + .json::() + .await + .map_err(|err| TrustedPublishingError::Reqwest(audience_url.clone(), err))?; trace!("The audience is `{}`", &audience.audience); Ok(audience.audience) } @@ -116,18 +134,27 @@ async fn get_oidc_token( oidc_token_request_token: &str, client: &ClientWithMiddleware, ) -> Result { - let mut oidc_token_url = Url::parse(&env::var(EnvVars::ACTIONS_ID_TOKEN_REQUEST_URL)?)?; + let oidc_token_url = env::var(EnvVars::ACTIONS_ID_TOKEN_REQUEST_URL).map_err(|err| { + TrustedPublishingError::from_var_err(EnvVars::ACTIONS_ID_TOKEN_REQUEST_URL, err) + })?; + let mut oidc_token_url = Url::parse(&oidc_token_url)?; oidc_token_url .query_pairs_mut() .append_pair("audience", audience); debug!("Querying the trusted publishing OIDC token from {oidc_token_url}"); let authorization = format!("bearer {oidc_token_request_token}"); let response = client - .get(oidc_token_url) + .get(oidc_token_url.clone()) .header(header::AUTHORIZATION, authorization) .send() - .await?; - let oidc_token: OidcToken = response.error_for_status()?.json().await?; + .await + .map_err(|err| TrustedPublishingError::ReqwestMiddleware(oidc_token_url.clone(), err))?; + let oidc_token: OidcToken = response + .error_for_status() + .map_err(|err| TrustedPublishingError::Reqwest(oidc_token_url.clone(), err))? + .json() + .await + .map_err(|err| TrustedPublishingError::Reqwest(oidc_token_url.clone(), err))?; Ok(oidc_token.value) } @@ -145,14 +172,18 @@ async fn get_publish_token( token: oidc_token.to_string(), }; let response = client - .post(mint_token_url) + .post(mint_token_url.clone()) .body(serde_json::to_vec(&mint_token_payload)?) .send() - .await?; + .await + .map_err(|err| TrustedPublishingError::ReqwestMiddleware(mint_token_url.clone(), err))?; // reqwest's implementation of `.json()` also goes through `.bytes()` let status = response.status(); - let body = response.bytes().await?; + let body = response + .bytes() + .await + .map_err(|err| TrustedPublishingError::Reqwest(mint_token_url.clone(), err))?; if status.is_success() { let publish_token: PublishToken = serde_json::from_slice(&body)?; diff --git a/crates/uv-pypi-types/src/metadata/requires_txt.rs b/crates/uv-pypi-types/src/metadata/requires_txt.rs index fbfa4fe6d112..f84d36c89cfe 100644 --- a/crates/uv-pypi-types/src/metadata/requires_txt.rs +++ b/crates/uv-pypi-types/src/metadata/requires_txt.rs @@ -3,8 +3,7 @@ use serde::Deserialize; use std::io::BufRead; use std::str::FromStr; use uv_normalize::ExtraName; -use uv_pep508::marker::MarkerValueExtra; -use uv_pep508::{ExtraOperator, MarkerExpression, MarkerTree, Requirement}; +use uv_pep508::{ExtraOperator, MarkerExpression, MarkerTree, MarkerValueExtra, Requirement}; /// `requires.txt` metadata as defined in . /// diff --git a/crates/uv-python/Cargo.toml b/crates/uv-python/Cargo.toml index 361d3fa7da68..a1cb0b509e09 100644 --- a/crates/uv-python/Cargo.toml +++ b/crates/uv-python/Cargo.toml @@ -20,6 +20,7 @@ uv-cache = { workspace = true } uv-cache-info = { workspace = true } uv-cache-key = { workspace = true } uv-client = { workspace = true } +uv-dirs = { workspace = true } uv-distribution-filename = { workspace = true } uv-extract = { workspace = true } uv-fs = { workspace = true } @@ -30,6 +31,7 @@ uv-platform-tags = { workspace = true } uv-pypi-types = { workspace = true } uv-state = { workspace = true } uv-static = { workspace = true } +uv-trampoline-builder = { workspace = true } uv-warnings = { workspace = true } anyhow = { workspace = true } @@ -57,6 +59,9 @@ tracing = { workspace = true } url = { workspace = true } which = { workspace = true } +[target.'cfg(target_os = "linux")'.dependencies] +procfs = { workspace = true } + [target.'cfg(target_os = "windows")'.dependencies] windows-sys = { workspace = true } windows-registry = { workspace = true } diff --git a/crates/uv-python/download-metadata.json b/crates/uv-python/download-metadata.json index 7798d5a1b055..e4e9d5ec98a7 100644 --- a/crates/uv-python/download-metadata.json +++ b/crates/uv-python/download-metadata.json @@ -177,8 +177,8 @@ "minor": 13, "patch": 0, "prerelease": "", - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.13.0%2B20241016-aarch64-unknown-linux-gnu-freethreaded%2Bdebug-full.tar.zst", - "sha256": "3b2f53f544d1cb81520bd45446b68f7d51b5bd65e6a2a1422450fd9472c18e6c", + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.13.0%2B20241016-aarch64-unknown-linux-gnu-freethreaded%2Blto-full.tar.zst", + "sha256": "59b50df9826475d24bb7eff781fa3949112b5e9c92adb29e96a09cdf1216d5bd", "variant": "freethreaded" }, "cpython-3.13.0+freethreaded-linux-armv7-gnueabi": { @@ -190,8 +190,8 @@ "minor": 13, "patch": 0, "prerelease": "", - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.13.0%2B20241016-armv7-unknown-linux-gnueabi-freethreaded%2Bdebug-full.tar.zst", - "sha256": "316cf6a946150ac6b42a1c43f18977966682b9d153c5891f717fba92f0cbe06f", + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.13.0%2B20241016-armv7-unknown-linux-gnueabi-freethreaded%2Blto-full.tar.zst", + "sha256": "cafc0f10503e6ec0a62da9273aabb7b1d5c3f3619e80a08f9076665eb7e24b00", "variant": "freethreaded" }, "cpython-3.13.0+freethreaded-linux-armv7-gnueabihf": { @@ -203,8 +203,8 @@ "minor": 13, "patch": 0, "prerelease": "", - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.13.0%2B20241016-armv7-unknown-linux-gnueabihf-freethreaded%2Bdebug-full.tar.zst", - "sha256": "0b92de0f95fb304991749e5a7beb6778f961fbf0ce5057cc6e34d6754b0a0137", + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.13.0%2B20241016-armv7-unknown-linux-gnueabihf-freethreaded%2Blto-full.tar.zst", + "sha256": "636fe5015ffefaa5588dbcb62c026bfd71e14e3fbfac92af0b969d9f88efc4a5", "variant": "freethreaded" }, "cpython-3.13.0+freethreaded-linux-powerpc64le-gnu": { @@ -216,8 +216,8 @@ "minor": 13, "patch": 0, "prerelease": "", - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.13.0%2B20241016-ppc64le-unknown-linux-gnu-freethreaded%2Bdebug-full.tar.zst", - "sha256": "2adcf07d6dea3a83747e24cd70721a15fd2b10a31e78314bd6782d3992b1670d", + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.13.0%2B20241016-ppc64le-unknown-linux-gnu-freethreaded%2Blto-full.tar.zst", + "sha256": "1217efa5f4ce67fcc9f7eb64165b1bd0912b2a21bc25c1a7e2cb174a21a5df7e", "variant": "freethreaded" }, "cpython-3.13.0+freethreaded-linux-s390x-gnu": { @@ -229,8 +229,8 @@ "minor": 13, "patch": 0, "prerelease": "", - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.13.0%2B20241016-s390x-unknown-linux-gnu-freethreaded%2Bdebug-full.tar.zst", - "sha256": "3b45d2be68ac66dde2d6cae55156806844f337063f475587c9fa023eea24460d", + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.13.0%2B20241016-s390x-unknown-linux-gnu-freethreaded%2Blto-full.tar.zst", + "sha256": "6c3e1e4f19d2b018b65a7e3ef4cd4225c5b9adfbc490218628466e636d5c4b8c", "variant": "freethreaded" }, "cpython-3.13.0+freethreaded-linux-x86_64-gnu": { diff --git a/crates/uv-python/fetch-download-metadata.py b/crates/uv-python/fetch-download-metadata.py index c3ad9b814b5e..cd4f966f7850 100755 --- a/crates/uv-python/fetch-download-metadata.py +++ b/crates/uv-python/fetch-download-metadata.py @@ -396,7 +396,7 @@ def _build_option_priority(self, build_options: list[str]) -> int: # Prefer optimized builds return -1 * sum( ( - "lgo" in build_options, + "lto" in build_options, "pgo" in build_options, ) ) @@ -612,6 +612,7 @@ def main() -> None: ) # Silence httpx logging logging.getLogger("httpx").setLevel(logging.WARNING) + logging.getLogger("httpcore").setLevel(logging.WARNING) asyncio.run(find()) diff --git a/crates/uv-python/src/cpuinfo.rs b/crates/uv-python/src/cpuinfo.rs new file mode 100644 index 000000000000..f0827886be74 --- /dev/null +++ b/crates/uv-python/src/cpuinfo.rs @@ -0,0 +1,33 @@ +//! Fetches CPU information. + +use anyhow::Error; + +#[cfg(target_os = "linux")] +use procfs::{CpuInfo, Current}; + +/// Detects whether the hardware supports floating-point operations using ARM's Vector Floating Point (VFP) hardware. +/// +/// This function is relevant specifically for ARM architectures, where the presence of the "vfp" flag in `/proc/cpuinfo` +/// indicates that the CPU supports hardware floating-point operations. +/// This helps determine whether the system is using the `gnueabihf` (hard-float) ABI or `gnueabi` (soft-float) ABI. +/// +/// More information on this can be found in the [Debian ARM Hard Float Port documentation](https://wiki.debian.org/ArmHardFloatPort#VFP). +#[cfg(target_os = "linux")] +pub(crate) fn detect_hardware_floating_point_support() -> Result { + let cpu_info = CpuInfo::current()?; + if let Some(features) = cpu_info.fields.get("Features") { + if features.contains("vfp") { + return Ok(true); // "vfp" found: hard-float (gnueabihf) detected + } + } + + Ok(false) // Default to soft-float (gnueabi) if no "vfp" flag is found +} + +/// For non-Linux systems or architectures, the function will return `false` as hardware floating-point detection +/// is not applicable outside of Linux ARM architectures. +#[cfg(not(target_os = "linux"))] +#[allow(clippy::unnecessary_wraps)] +pub(crate) fn detect_hardware_floating_point_support() -> Result { + Ok(false) // Non-Linux or non-ARM systems: hardware floating-point detection is not applicable +} diff --git a/crates/uv-python/src/discovery.rs b/crates/uv-python/src/discovery.rs index 549eb3808f86..4993f7868f47 100644 --- a/crates/uv-python/src/discovery.rs +++ b/crates/uv-python/src/discovery.rs @@ -133,7 +133,7 @@ pub enum EnvironmentPreference { Any, } -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum PythonVariant { #[default] Default, @@ -1236,6 +1236,16 @@ impl PythonVariant { PythonVariant::Freethreaded => interpreter.gil_disabled(), } } + + /// Return the lib or executable suffix for the variant, e.g., `t` for `python3.13t`. + /// + /// Returns an empty string for the default Python variant. + pub fn suffix(self) -> &'static str { + match self { + Self::Default => "", + Self::Freethreaded => "t", + } + } } impl PythonRequest { /// Create a request from a string. @@ -1635,12 +1645,7 @@ impl std::fmt::Display for ExecutableName { if let Some(prerelease) = &self.prerelease { write!(f, "{prerelease}")?; } - match self.variant { - PythonVariant::Default => {} - PythonVariant::Freethreaded => { - f.write_str("t")?; - } - }; + f.write_str(self.variant.suffix())?; f.write_str(std::env::consts::EXE_SUFFIX)?; Ok(()) } @@ -2074,11 +2079,30 @@ impl FromStr for VersionRequest { // Split the release component if it uses the wheel tag format (e.g., `38`) let version = split_wheel_tag_release_version(version); - // We dont allow post, dev, or local versions here - if version.post().is_some() || version.dev().is_some() || !version.local().is_empty() { + // We dont allow post or dev version here + if version.post().is_some() || version.dev().is_some() { return Err(Error::InvalidVersionRequest(s.to_string())); } + // Check if the local version includes a variant + let variant = if version.local().is_empty() { + variant + } else { + // If we already have a variant, do not allow another to be requested + if variant != PythonVariant::Default { + return Err(Error::InvalidVersionRequest(s.to_string())); + } + + let [uv_pep440::LocalSegment::String(local)] = version.local() else { + return Err(Error::InvalidVersionRequest(s.to_string())); + }; + + match local.as_str() { + "freethreaded" => PythonVariant::Freethreaded, + _ => return Err(Error::InvalidVersionRequest(s.to_string())), + } + }; + // Cast the release components into u8s since that's what we use in `VersionRequest` let Ok(release) = try_into_u8_slice(version.release()) else { return Err(Error::InvalidVersionRequest(s.to_string())); diff --git a/crates/uv-python/src/downloads.inc b/crates/uv-python/src/downloads.inc index e730f6742f55..45a2cc36073f 100644 --- a/crates/uv-python/src/downloads.inc +++ b/crates/uv-python/src/downloads.inc @@ -18,7 +18,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.13.0%2B20241016-aarch64-apple-darwin-install_only_stripped.tar.gz", sha256: Some("e94fafbac07da52c965cb6a7ffc51ce779bd253cd98af801347aac791b96499f") @@ -34,7 +33,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.13.0%2B20241016-x86_64-apple-darwin-install_only_stripped.tar.gz", sha256: Some("406664681bd44af35756ad08f5304f1ec57070bb76fae8ff357ff177f229b224") @@ -50,7 +48,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.13.0%2B20241016-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz", sha256: Some("06e633164cb0133685a2ce14af88df0dbcaea4b0b2c5d3348d6b81393307481a") @@ -66,7 +63,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnueabi), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.13.0%2B20241016-armv7-unknown-linux-gnueabi-install_only_stripped.tar.gz", sha256: Some("1b18f0eac4c3578ecca52ff388276546c701cea22410235716195c52ad7d0344") @@ -82,7 +78,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnueabihf), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.13.0%2B20241016-armv7-unknown-linux-gnueabihf-install_only_stripped.tar.gz", sha256: Some("be2bbcb985ecf12eb7a16c18043a2b0b8551d8e8799c49a0d766b541dd465f47") @@ -98,7 +93,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.13.0%2B20241016-ppc64le-unknown-linux-gnu-install_only_stripped.tar.gz", sha256: Some("afe014200fea7505a67658fd82e70ccb49982deee752809849e781b941b941ec") @@ -114,7 +108,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.13.0%2B20241016-s390x-unknown-linux-gnu-install_only_stripped.tar.gz", sha256: Some("b5782c027a8802b19656e961f73193cf060b124fd052dff19bb6d21b9e51ed14") @@ -130,7 +123,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.13.0%2B20241016-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz", sha256: Some("b5e74d1e16402b633c6f04519618231fc0dbae7d2f9e4b1ac17c294cc3d3d076") @@ -146,7 +138,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.13.0%2B20241016-x86_64-unknown-linux-musl-install_only_stripped.tar.gz", sha256: Some("10978500ab6589760716c644aeadffa0f2c0bf31ea10f0c6160fee933933a567") @@ -162,7 +153,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.13.0%2B20241016-i686-pc-windows-msvc-install_only_stripped.tar.gz", sha256: Some("d5538ed2a247220516d4c14e8452f2c49318b29f8b524c908a1ed42e405bd8cc") @@ -178,7 +168,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.13.0%2B20241016-x86_64-pc-windows-msvc-install_only_stripped.tar.gz", sha256: Some("c8134287496727922a5c47896b4f2b1623e3aab91cbb7c1ca64542db7593f3f1") @@ -194,7 +183,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Freethreaded - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.13.0%2B20241016-aarch64-apple-darwin-freethreaded%2Bpgo%2Blto-full.tar.zst", sha256: Some("efc2e71c0e05bc5bedb7a846e05f28dd26491b1744ded35ed82f8b49ccfa684b") @@ -210,7 +198,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Freethreaded - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.13.0%2B20241016-x86_64-apple-darwin-freethreaded%2Bpgo%2Blto-full.tar.zst", sha256: Some("2e07dfea62fe2215738551a179c87dbed1cc79d1b3654f4d7559889a6d5ce4eb") @@ -226,10 +213,9 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Freethreaded - }, - url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.13.0%2B20241016-aarch64-unknown-linux-gnu-freethreaded%2Bdebug-full.tar.zst", - sha256: Some("3b2f53f544d1cb81520bd45446b68f7d51b5bd65e6a2a1422450fd9472c18e6c") + url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.13.0%2B20241016-aarch64-unknown-linux-gnu-freethreaded%2Blto-full.tar.zst", + sha256: Some("59b50df9826475d24bb7eff781fa3949112b5e9c92adb29e96a09cdf1216d5bd") }, ManagedPythonDownload { key: PythonInstallationKey { @@ -242,10 +228,9 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnueabi), variant: PythonVariant::Freethreaded - }, - url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.13.0%2B20241016-armv7-unknown-linux-gnueabi-freethreaded%2Bdebug-full.tar.zst", - sha256: Some("316cf6a946150ac6b42a1c43f18977966682b9d153c5891f717fba92f0cbe06f") + url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.13.0%2B20241016-armv7-unknown-linux-gnueabi-freethreaded%2Blto-full.tar.zst", + sha256: Some("cafc0f10503e6ec0a62da9273aabb7b1d5c3f3619e80a08f9076665eb7e24b00") }, ManagedPythonDownload { key: PythonInstallationKey { @@ -258,10 +243,9 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnueabihf), variant: PythonVariant::Freethreaded - }, - url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.13.0%2B20241016-armv7-unknown-linux-gnueabihf-freethreaded%2Bdebug-full.tar.zst", - sha256: Some("0b92de0f95fb304991749e5a7beb6778f961fbf0ce5057cc6e34d6754b0a0137") + url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.13.0%2B20241016-armv7-unknown-linux-gnueabihf-freethreaded%2Blto-full.tar.zst", + sha256: Some("636fe5015ffefaa5588dbcb62c026bfd71e14e3fbfac92af0b969d9f88efc4a5") }, ManagedPythonDownload { key: PythonInstallationKey { @@ -274,10 +258,9 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Freethreaded - }, - url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.13.0%2B20241016-ppc64le-unknown-linux-gnu-freethreaded%2Bdebug-full.tar.zst", - sha256: Some("2adcf07d6dea3a83747e24cd70721a15fd2b10a31e78314bd6782d3992b1670d") + url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.13.0%2B20241016-ppc64le-unknown-linux-gnu-freethreaded%2Blto-full.tar.zst", + sha256: Some("1217efa5f4ce67fcc9f7eb64165b1bd0912b2a21bc25c1a7e2cb174a21a5df7e") }, ManagedPythonDownload { key: PythonInstallationKey { @@ -290,10 +273,9 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Freethreaded - }, - url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.13.0%2B20241016-s390x-unknown-linux-gnu-freethreaded%2Bdebug-full.tar.zst", - sha256: Some("3b45d2be68ac66dde2d6cae55156806844f337063f475587c9fa023eea24460d") + url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.13.0%2B20241016-s390x-unknown-linux-gnu-freethreaded%2Blto-full.tar.zst", + sha256: Some("6c3e1e4f19d2b018b65a7e3ef4cd4225c5b9adfbc490218628466e636d5c4b8c") }, ManagedPythonDownload { key: PythonInstallationKey { @@ -306,7 +288,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Freethreaded - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.13.0%2B20241016-x86_64-unknown-linux-gnu-freethreaded%2Bpgo%2Blto-full.tar.zst", sha256: Some("a73adeda301ad843cce05f31a2d3e76222b656984535a7b87696a24a098b216c") @@ -322,7 +303,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Freethreaded - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.13.0%2B20241016-i686-pc-windows-msvc-freethreaded%2Bpgo-full.tar.zst", sha256: Some("7794b0209af46b6347aab945f1ccc3b24add0a17b3f6fb7741447bc44d10bf4a") @@ -338,7 +318,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Freethreaded - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.13.0%2B20241016-x86_64-pc-windows-msvc-freethreaded%2Bpgo-full.tar.zst", sha256: Some("bfd89f9acf866463bc4baf01733da5e767d13f5d0112175a4f57ba91f1541310") @@ -354,7 +333,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241002/cpython-3.13.0rc3%2B20241002-aarch64-apple-darwin-install_only_stripped.tar.gz", sha256: Some("685ef71882f16eabab0bc838094727978370f0ad95c29f7f5c244ffa31316aeb") @@ -370,7 +348,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241002/cpython-3.13.0rc3%2B20241002-x86_64-apple-darwin-install_only_stripped.tar.gz", sha256: Some("0f5f9fcf82093c428b80c552165544439f4adcdbe5129ecf721d619e532e9b5e") @@ -386,7 +363,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241002/cpython-3.13.0rc3%2B20241002-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz", sha256: Some("1414c6b37f37e8fd9d14e48d81e313eb9c965cb0330747d5d2d689dd7e0c7043") @@ -402,7 +378,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnueabi), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241002/cpython-3.13.0rc3%2B20241002-armv7-unknown-linux-gnueabi-install_only_stripped.tar.gz", sha256: Some("11befeaf4768c2ebbb258f5b07f94b7700f16424f858d6d2c250b434e99ce07c") @@ -418,7 +393,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnueabihf), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241002/cpython-3.13.0rc3%2B20241002-armv7-unknown-linux-gnueabihf-install_only_stripped.tar.gz", sha256: Some("b7180d5ea5fda2f397d04e2e6e11a2a7e0d732542bf54c484afb81d087a7b927") @@ -434,7 +408,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241002/cpython-3.13.0rc3%2B20241002-ppc64le-unknown-linux-gnu-install_only_stripped.tar.gz", sha256: Some("59a2a81991d78bd658742d69b577a2b4c0734628ed42bff68615686eaf96f2ab") @@ -450,7 +423,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241002/cpython-3.13.0rc3%2B20241002-s390x-unknown-linux-gnu-install_only_stripped.tar.gz", sha256: Some("2769182e58b0dddec15222bfeecbd4b12fde61c38f23a90aa942514f3545fb9b") @@ -466,7 +438,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241002/cpython-3.13.0rc3%2B20241002-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz", sha256: Some("445156c61e1cc167f7b8777ad08cc36e5598e12cd27e07453f6e6dc0f62e421e") @@ -482,7 +453,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241002/cpython-3.13.0rc3%2B20241002-x86_64-unknown-linux-musl-install_only_stripped.tar.gz", sha256: Some("4df6b7665c735a728d72e6f49034f1a6b7d9a54b0fbc472dc2ca525eb3dd513f") @@ -498,7 +468,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241002/cpython-3.13.0rc3%2B20241002-i686-pc-windows-msvc-install_only_stripped.tar.gz", sha256: Some("873905b3e5e8cba700126e8d6ed28ad3aef0dd102f730f8ca196018477dd2da6") @@ -514,7 +483,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241002/cpython-3.13.0rc3%2B20241002-x86_64-pc-windows-msvc-install_only_stripped.tar.gz", sha256: Some("b59317828ef88f138ee122d420b60f2705bc72ae846ff69562e79e6c5cbc3177") @@ -530,7 +498,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240909/cpython-3.13.0rc2%2B20240909-aarch64-apple-darwin-install_only_stripped.tar.gz", sha256: Some("9e17f9fcc314a5dd489089a7502a525c4dd08af862f9cf33b52161a752f2a5b7") @@ -546,7 +513,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240909/cpython-3.13.0rc2%2B20240909-x86_64-apple-darwin-install_only_stripped.tar.gz", sha256: Some("971668ac7f3168efc4d2b589e9d36247ab8ca9f9525c56c8aa7bfd374060105b") @@ -562,7 +528,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240909/cpython-3.13.0rc2%2B20240909-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz", sha256: Some("d99a663d3b9f8792a659e366372e685550045cad12aef11645c06a9b6edcd071") @@ -578,7 +543,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnueabi), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240909/cpython-3.13.0rc2%2B20240909-armv7-unknown-linux-gnueabi-install_only_stripped.tar.gz", sha256: Some("4ca7f2aeaabf8dbb2193f0fa86f869525a5c209eb403a39a73f4cf7040cf3613") @@ -594,7 +558,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnueabihf), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240909/cpython-3.13.0rc2%2B20240909-armv7-unknown-linux-gnueabihf-install_only_stripped.tar.gz", sha256: Some("0db2d263bdbb3af1e8dc0677fa44a5cda992ba989551346ccbbfd50a86135c3d") @@ -610,7 +573,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240909/cpython-3.13.0rc2%2B20240909-ppc64le-unknown-linux-gnu-install_only_stripped.tar.gz", sha256: Some("70073333f7d3f0b900c7299659fec069bbefd5e04808b3729d2434b2232ac729") @@ -626,7 +588,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240909/cpython-3.13.0rc2%2B20240909-s390x-unknown-linux-gnu-install_only_stripped.tar.gz", sha256: Some("50a2080e30d1504e76e5471e46830f0b4974c66b538ed8ec7df416975133ff89") @@ -642,7 +603,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240909/cpython-3.13.0rc2%2B20240909-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz", sha256: Some("1893a218709d3664b7a2b80f5598b5f25c0c3fe2bcc8d0a1c75eec6bbb93d602") @@ -658,7 +618,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240909/cpython-3.13.0rc2%2B20240909-x86_64-unknown-linux-musl-install_only_stripped.tar.gz", sha256: Some("6f09aa5ba6aab8bf21955dbc3d6bab19125130ef0ebe29242b0e5ac1eebb3161") @@ -674,7 +633,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240909/cpython-3.13.0rc2%2B20240909-i686-pc-windows-msvc-install_only_stripped.tar.gz", sha256: Some("759f600b27a6a0ef2638cb02e8bbcc6de726dd1c896759f78da3e412f6c992e9") @@ -690,7 +648,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240909/cpython-3.13.0rc2%2B20240909-x86_64-pc-windows-msvc-install_only_stripped.tar.gz", sha256: Some("c883205751c714bd0519592673a88f160a55d34344cc1368353ad34a679eb94a") @@ -706,7 +663,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.12.7%2B20241016-aarch64-apple-darwin-install_only_stripped.tar.gz", sha256: Some("95dd397e3aef4cc1846867cf20be704bdd74edd16ea8032caf01e48f0c53d65d") @@ -722,7 +678,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.12.7%2B20241016-x86_64-apple-darwin-install_only_stripped.tar.gz", sha256: Some("848405b92bda20fad1f9bba99234c7d3f11e0b31e46f89835d1cb3d735e932aa") @@ -738,7 +693,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.12.7%2B20241016-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz", sha256: Some("c8f5ed70ee3c19da72d117f7b306adc6ca1eaf26afcbe1cc1be57d1e18df184c") @@ -754,7 +708,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnueabi), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.12.7%2B20241016-armv7-unknown-linux-gnueabi-install_only_stripped.tar.gz", sha256: Some("d73cb8428a105d01141dee0ceec445328ab70e039e31cd8c5c1d7d226fb67afc") @@ -770,7 +723,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnueabihf), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.12.7%2B20241016-armv7-unknown-linux-gnueabihf-install_only_stripped.tar.gz", sha256: Some("04b3087272d2bb8df98eec5fe81b666052907f292381cbecce17bec40fdd30c5") @@ -786,7 +738,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.12.7%2B20241016-ppc64le-unknown-linux-gnu-install_only_stripped.tar.gz", sha256: Some("922aa21fb9eacdd1c0a26ced4dca2725595453ae5b922d56b39ebdd2388175fd") @@ -802,7 +753,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.12.7%2B20241016-s390x-unknown-linux-gnu-install_only_stripped.tar.gz", sha256: Some("8e92d65b245b572fa6f520d428a9807a9da36428c7379a11d41ae428e69ed921") @@ -818,7 +768,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.12.7%2B20241016-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz", sha256: Some("3a4d53a7ba3916c0c1f35cbbe57068e2571b138389f29cf5c35367fec8f4c617") @@ -834,7 +783,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.12.7%2B20241016-x86_64-unknown-linux-musl-install_only_stripped.tar.gz", sha256: Some("9314cb4d5aa525f2dc9f8d6ac204bebcfdfa8eb0dd4d3788af68769184355484") @@ -850,7 +798,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.12.7%2B20241016-i686-pc-windows-msvc-install_only_stripped.tar.gz", sha256: Some("d7d7c897f11f12808d3fd9a0ce48e4de19369df4a9ee9390a4adae302902e333") @@ -866,7 +813,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.12.7%2B20241016-x86_64-pc-windows-msvc-install_only_stripped.tar.gz", sha256: Some("fa8ac308a7cd1774d599ad9a29f1e374fbdc11453b12a8c50cc4afdb5c4bfd1a") @@ -882,7 +828,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240909/cpython-3.12.6%2B20240909-aarch64-apple-darwin-install_only_stripped.tar.gz", sha256: Some("0419bafa4444a5aa0c554197bce0679e7cc0f28edc7ee8cfbe0ccea860bdb904") @@ -898,7 +843,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240909/cpython-3.12.6%2B20240909-x86_64-apple-darwin-install_only_stripped.tar.gz", sha256: Some("b10d19eb5548a3b3b0a5e6f9109834d7ecfc139bc15754f81a94d39eaa5bdd26") @@ -914,7 +858,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240909/cpython-3.12.6%2B20240909-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz", sha256: Some("22d119ac7df7f0bddfd4dfd075bcc4eb2532ed3df0bdba0579106835d49ef9cd") @@ -930,7 +873,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnueabi), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240909/cpython-3.12.6%2B20240909-armv7-unknown-linux-gnueabi-install_only_stripped.tar.gz", sha256: Some("190c23eb3b9c6b9638f69dc7fb829df8967ad64c82e82c93898a4d878d18ed2a") @@ -946,7 +888,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnueabihf), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240909/cpython-3.12.6%2B20240909-armv7-unknown-linux-gnueabihf-install_only_stripped.tar.gz", sha256: Some("31a043c40e1dbb528404ff6e1fcad25638d54dfab2d379c3989d47ec24e6938b") @@ -962,7 +903,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240909/cpython-3.12.6%2B20240909-ppc64le-unknown-linux-gnu-install_only_stripped.tar.gz", sha256: Some("fb49374b512b0e9f2cd2a720b3836f8a04228d73eb0786e64221eb55979edc6e") @@ -978,7 +918,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240909/cpython-3.12.6%2B20240909-s390x-unknown-linux-gnu-install_only_stripped.tar.gz", sha256: Some("89be19666ecb7cdbbfd596e462d690a78a380f1fe5c2967b25a1779b0cec9339") @@ -994,7 +933,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240909/cpython-3.12.6%2B20240909-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz", sha256: Some("b080463e4f0c452e592cdac1ca97936a6a19bb3d9a64da669a50ca843fce0108") @@ -1010,7 +948,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240909/cpython-3.12.6%2B20240909-x86_64-unknown-linux-musl-install_only_stripped.tar.gz", sha256: Some("661e2a4b03d6eccbb5b15f5bd2869fbdd39132513394d758287e46115e48d4ef") @@ -1026,7 +963,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240909/cpython-3.12.6%2B20240909-i686-pc-windows-msvc-install_only_stripped.tar.gz", sha256: Some("d87275e613632ab738528fe20a94a7193e824e91ba7f1e7845e7fcfc1f114900") @@ -1042,7 +978,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240909/cpython-3.12.6%2B20240909-x86_64-pc-windows-msvc-install_only_stripped.tar.gz", sha256: Some("fe9898060f52c2171c2aa074f470f91339bdcf9896dae6709021c914f58aa863") @@ -1058,7 +993,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240814/cpython-3.12.5%2B20240814-aarch64-apple-darwin-install_only_stripped.tar.gz", sha256: Some("90715cdab075e5a2680acf2695572d165b6269bdb5d1942ab577491478aea55f") @@ -1074,7 +1008,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240814/cpython-3.12.5%2B20240814-x86_64-apple-darwin-install_only_stripped.tar.gz", sha256: Some("49a9f7ad41d62e0ece9e664ca5ae95f022e7b68eef48e8a6f11620ec9247c686") @@ -1090,7 +1023,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240814/cpython-3.12.5%2B20240814-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz", sha256: Some("06e512178cb513658a01c054b3eafc649ca362ccbeb02a6ae8a55b02c1ba75ca") @@ -1106,7 +1038,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnueabi), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240814/cpython-3.12.5%2B20240814-armv7-unknown-linux-gnueabi-install_only_stripped.tar.gz", sha256: Some("7a584de9c2824f43d7a7b1c26eb61a18af770ebd603a74b45d57601ba62ba508") @@ -1122,7 +1053,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnueabihf), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240814/cpython-3.12.5%2B20240814-armv7-unknown-linux-gnueabihf-install_only_stripped.tar.gz", sha256: Some("a9992b30d7b3ecb558cd12fde919e3e2836f161f8f777afea31140d5fff6362e") @@ -1138,7 +1068,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240814/cpython-3.12.5%2B20240814-ppc64le-unknown-linux-gnu-install_only_stripped.tar.gz", sha256: Some("3bea081f4e6fa67e600a6a791bcfebb2891531ede2c21e23e1b7321b3369c737") @@ -1154,7 +1083,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240814/cpython-3.12.5%2B20240814-s390x-unknown-linux-gnu-install_only_stripped.tar.gz", sha256: Some("2b6ea3a5242de99574191ee42df864756eca6d7cb1dbd4cd7ab2850ba8b828f8") @@ -1170,7 +1098,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240814/cpython-3.12.5%2B20240814-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz", sha256: Some("10680b593b5e31833218fd83104dee74af970a3463403a22bae613b952a34e8d") @@ -1186,7 +1113,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240814/cpython-3.12.5%2B20240814-x86_64-unknown-linux-musl-install_only_stripped.tar.gz", sha256: Some("e61b1274e1195f227cb30ba5d89ea32d743796d992adcaffad4819e4b0405d24") @@ -1202,7 +1128,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240814/cpython-3.12.5%2B20240814-i686-pc-windows-msvc-install_only_stripped.tar.gz", sha256: Some("b1009d46b87330c099d02411ca5e9e333f13305c5abdbe20810a7c467cedb051") @@ -1218,7 +1143,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240814/cpython-3.12.5%2B20240814-x86_64-pc-windows-msvc-install_only_stripped.tar.gz", sha256: Some("6eb0398795e8875575934cf21cdc9c7c7acddb46f9a52f91fdad509723f2f0e9") @@ -1234,7 +1158,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240726/cpython-3.12.4%2B20240726-aarch64-apple-darwin-install_only_stripped.tar.gz", sha256: Some("ef6948e836f531bd7a58ffbe602803ff1c83c65f99d1da19be369ea61f136c93") @@ -1250,7 +1173,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240726/cpython-3.12.4%2B20240726-x86_64-apple-darwin-install_only_stripped.tar.gz", sha256: Some("9d68cbdd12d1d6f98d35cc76add232c12db75c6b7f49733bffc88e7b1c025a79") @@ -1266,7 +1188,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240726/cpython-3.12.4%2B20240726-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz", sha256: Some("6c9cf13644edc7250525ab1b2529ba1c0fff56c0c5a5c2242d84b6d4889d2bea") @@ -1282,7 +1203,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnueabi), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240726/cpython-3.12.4%2B20240726-armv7-unknown-linux-gnueabi-install_only_stripped.tar.gz", sha256: Some("5a23ed8eaf948fe48d7c05dbfb58ea8638dcd2c4880d8519e069281ab427cbcb") @@ -1298,7 +1218,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnueabihf), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240726/cpython-3.12.4%2B20240726-armv7-unknown-linux-gnueabihf-install_only_stripped.tar.gz", sha256: Some("4281764e69339a138e30211b9923d74036d07c7a56c6aacc6dbdb2802a575f51") @@ -1314,7 +1233,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240726/cpython-3.12.4%2B20240726-ppc64le-unknown-linux-gnu-install_only_stripped.tar.gz", sha256: Some("35a8359f1dc17a7a70007dae102a5e1562c0715a721377ede92137b2a0292406") @@ -1330,7 +1248,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240726/cpython-3.12.4%2B20240726-s390x-unknown-linux-gnu-install_only_stripped.tar.gz", sha256: Some("b2fd015ab3689e024de6fbb34a4942acdb54c2184d1963e22829aafa1d81ba2c") @@ -1346,7 +1263,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240726/cpython-3.12.4%2B20240726-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz", sha256: Some("ca076aee4329f53f988346eb0521ad2a2cf7f723b6296088d03b98d8f22f5420") @@ -1362,7 +1278,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240726/cpython-3.12.4%2B20240726-x86_64-unknown-linux-musl-install_only_stripped.tar.gz", sha256: Some("de4983ffa610ff2c3b9bcb62882366f017d94bf11b194c1fce17ad9e502acce6") @@ -1378,7 +1293,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240726/cpython-3.12.4%2B20240726-i686-pc-windows-msvc-install_only_stripped.tar.gz", sha256: Some("ff0fab24f38c22130e45b90b7ec10dc4ce9677b545d9fb9109a72d2ffbab7b02") @@ -1394,7 +1308,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240726/cpython-3.12.4%2B20240726-x86_64-pc-windows-msvc-install_only_stripped.tar.gz", sha256: Some("6dd7b4607f8a25f0f5f68e745f4c572b1a20c3bbfa86accfa45b52ab93b18ece") @@ -1410,7 +1323,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240415/cpython-3.12.3%2B20240415-aarch64-apple-darwin-install_only.tar.gz", sha256: Some("ccc40e5af329ef2af81350db2a88bbd6c17b56676e82d62048c15d548401519e") @@ -1426,7 +1338,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240415/cpython-3.12.3%2B20240415-x86_64-apple-darwin-install_only.tar.gz", sha256: Some("c37a22fca8f57d4471e3708de6d13097668c5f160067f264bb2b18f524c890c8") @@ -1442,7 +1353,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240415/cpython-3.12.3%2B20240415-aarch64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("ec8126de97945e629cca9aedc80a29c4ae2992c9d69f2655e27ae73906ba187d") @@ -1458,7 +1368,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnueabi), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240415/cpython-3.12.3%2B20240415-armv7-unknown-linux-gnueabi-install_only.tar.gz", sha256: Some("f693dd22b69361c17076157889eb8f1ce1a5ea670c031fae46782481ad892a64") @@ -1474,7 +1383,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnueabihf), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240415/cpython-3.12.3%2B20240415-armv7-unknown-linux-gnueabihf-install_only.tar.gz", sha256: Some("635080827bed4616dc271545677837203098e5b55e7195d803e1dca7da24fc0c") @@ -1490,7 +1398,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240415/cpython-3.12.3%2B20240415-ppc64le-unknown-linux-gnu-install_only.tar.gz", sha256: Some("c5dcf08b8077e617d949bda23027c49712f583120b3ed744f9b143da1d580572") @@ -1506,7 +1413,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240415/cpython-3.12.3%2B20240415-s390x-unknown-linux-gnu-install_only.tar.gz", sha256: Some("872fc321363b8cdd826fd2cb1adfd1ceb813bc1281f9d410c1c2c4e177e8df86") @@ -1522,7 +1428,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240415/cpython-3.12.3%2B20240415-x86_64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("a73ba777b5d55ca89edef709e6b8521e3f3d4289581f174c8699adfb608d09d6") @@ -1538,7 +1443,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240415/cpython-3.12.3%2B20240415-x86_64-unknown-linux-musl-install_only.tar.gz", sha256: Some("eb70814dc254f02714c77305de01b8ed2250c146320e22d0ed14b39021f89a8a") @@ -1554,7 +1458,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240415/cpython-3.12.3%2B20240415-i686-pc-windows-msvc-install_only.tar.gz", sha256: Some("bd723ad1aa05551627715a428660250f0e74db0f1421b03f399235772057ef55") @@ -1570,7 +1473,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240415/cpython-3.12.3%2B20240415-x86_64-pc-windows-msvc-install_only.tar.gz", sha256: Some("f7cfa4ad072feb4578c8afca5ba9a54ad591d665a441dd0d63aa366edbe19279") @@ -1586,7 +1488,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.12.2%2B20240224-aarch64-apple-darwin-install_only.tar.gz", sha256: Some("01c064c00013b0175c7858b159989819ead53f4746d40580b5b0b35b6e80fba6") @@ -1602,7 +1503,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.12.2%2B20240224-x86_64-apple-darwin-install_only.tar.gz", sha256: Some("a53a6670a202c96fec0b8c55ccc780ea3af5307eb89268d5b41a9775b109c094") @@ -1618,7 +1518,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.12.2%2B20240224-aarch64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("e52550379e7c4ac27a87de832d172658bc04150e4e27d4e858e6d8cbb96fd709") @@ -1634,7 +1533,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.12.2%2B20240224-ppc64le-unknown-linux-gnu-install_only.tar.gz", sha256: Some("74bc02c4bbbd26245c37b29b9e12d0a9c1b7ab93477fed8b651c988b6a9a6251") @@ -1650,7 +1548,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.12.2%2B20240224-s390x-unknown-linux-gnu-install_only.tar.gz", sha256: Some("ecd6b0285e5eef94deb784b588b4b425a15a43ae671bf206556659dc141a9825") @@ -1666,7 +1563,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.12.2%2B20240224-x86_64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("57a37b57f8243caa4cdac016176189573ad7620f0b6da5941c5e40660f9468ab") @@ -1682,7 +1578,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.12.2%2B20240224-x86_64-unknown-linux-musl-install_only.tar.gz", sha256: Some("b428b4151c70b85339ac2659e5f69f7e47142d34a506e05ecd095efe2e3dec81") @@ -1698,7 +1593,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.12.2%2B20240224-i686-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("1e919365f3e04eb111283f7a45d32eac2f327287ab7bf46720d5629e144cbff9") @@ -1714,7 +1608,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.12.2%2B20240224-x86_64-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("1e5655a6ccb1a64a78460e4e3ee21036c70246800f176a6c91043a3fe3654a3b") @@ -1730,7 +1623,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.12.1%2B20240107-aarch64-apple-darwin-install_only.tar.gz", sha256: Some("f93f8375ca6ac0a35d58ff007043cbd3a88d9609113f1cb59cf7c8d215f064af") @@ -1746,7 +1638,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.12.1%2B20240107-x86_64-apple-darwin-install_only.tar.gz", sha256: Some("eca96158c1568dedd9a0b3425375637a83764d1fa74446438293089a8bfac1f8") @@ -1762,7 +1653,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.12.1%2B20240107-aarch64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("236533ef20e665007a111c2f36efb59c87ae195ad7dca223b6dc03fb07064f0b") @@ -1778,7 +1668,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.12.1%2B20240107-ppc64le-unknown-linux-gnu-install_only.tar.gz", sha256: Some("78051f0d1411ee62bc2af5edfccf6e8400ac4ef82887a2affc19a7ace6a05267") @@ -1794,7 +1683,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.12.1%2B20240107-s390x-unknown-linux-gnu-install_only.tar.gz", sha256: Some("60631211c701f8d2c56e5dd7b154e68868128a019b9db1d53a264f56c0d4aee2") @@ -1810,7 +1698,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.12.1%2B20240107-x86_64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("74e330b8212ca22fd4d9a2003b9eec14892155566738febc8e5e572f267b9472") @@ -1826,7 +1713,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.12.1%2B20240107-x86_64-unknown-linux-musl-install_only.tar.gz", sha256: Some("876389f071d62ee9a4bdd7ce31e69c3cdd256fe498e4dd6bb2b80e674e7351fe") @@ -1842,7 +1728,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.12.1%2B20240107-i686-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("13c8a6f337a4e1ef043ffb8ea3c218ab2073afe0d3be36fcdf8ceb6f757210e8") @@ -1858,7 +1743,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.12.1%2B20240107-x86_64-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("fd5a9e0f41959d0341246d3643f2b8794f638adc0cec8dd5e1b6465198eae08a") @@ -1874,7 +1758,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.12.0%2B20231002-aarch64-apple-darwin-install_only.tar.gz", sha256: Some("4734a2be2becb813830112c780c9879ac3aff111a0b0cd590e65ec7465774d02") @@ -1890,7 +1773,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.12.0%2B20231002-x86_64-apple-darwin-install_only.tar.gz", sha256: Some("5a9e88c8aa52b609d556777b52ebde464ae4b4f77e4aac4eb693af57395c9abf") @@ -1906,7 +1788,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.12.0%2B20231002-aarch64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("bccfe67cf5465a3dfb0336f053966e2613a9bc85a6588c2fcf1366ef930c4f88") @@ -1922,7 +1803,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.12.0%2B20231002-ppc64le-unknown-linux-gnu-install_only.tar.gz", sha256: Some("b5dae075467ace32c594c7877fe6ebe0837681f814601d5d90ba4c0dfd87a1f2") @@ -1938,7 +1818,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.12.0%2B20231002-s390x-unknown-linux-gnu-install_only.tar.gz", sha256: Some("5681621349dd85d9726d1b67c84a9686ce78f72e73a6f9e4cc4119911655759e") @@ -1954,7 +1833,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.12.0%2B20231002-x86_64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("e51a5293f214053ddb4645b2c9f84542e2ef86870b8655704367bd4b29d39fe9") @@ -1970,7 +1848,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.12.0%2B20231002-x86_64-unknown-linux-musl-install_only.tar.gz", sha256: Some("922f9404f39dc4edb8558a93cef5c3330895a4c87acb1de2a2cf662ab942dbe5") @@ -1986,7 +1863,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.12.0%2B20231002-i686-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("6e4f30a998245cfaef00d1b87f8fd5f6c250bd222f933f8f38f124d4f03227f9") @@ -2002,7 +1878,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.12.0%2B20231002-x86_64-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("facfaa1fbc8653f95057f3c4a0f8aa833dab0e0b316e24ee8686bc761d4b4f8d") @@ -2018,7 +1893,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.11.10%2B20241016-aarch64-apple-darwin-install_only_stripped.tar.gz", sha256: Some("a5a224138a526acecfd17210953d76a28487968a767204902e2bde809bb0e759") @@ -2034,7 +1908,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.11.10%2B20241016-x86_64-apple-darwin-install_only_stripped.tar.gz", sha256: Some("575b49a7aa64e97b06de605b7e947033bf2310b5bc5f9aedb9859d4745033d91") @@ -2050,7 +1923,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.11.10%2B20241016-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz", sha256: Some("9d124604ffdea4fbaabb10b343c5a36b636a3e7b94dfc1cccd4531f33fceae5e") @@ -2066,7 +1938,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnueabi), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.11.10%2B20241016-armv7-unknown-linux-gnueabi-install_only_stripped.tar.gz", sha256: Some("deb089a5ac0fbd9ad2e3dc843d90019ead75b1ec895fd57a5abca190ba86cb77") @@ -2082,7 +1953,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnueabihf), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.11.10%2B20241016-armv7-unknown-linux-gnueabihf-install_only_stripped.tar.gz", sha256: Some("3655da6f1ccde823fc03f790bebfff106825e2b5ec4b733be225150275cd6321") @@ -2098,7 +1968,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.11.10%2B20241016-ppc64le-unknown-linux-gnu-install_only_stripped.tar.gz", sha256: Some("cc16cf0b1a1aa61f4e90d38ccaad0b65085cea69d2dcc2c6281ef9d4e6cccdd8") @@ -2114,7 +1983,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.11.10%2B20241016-s390x-unknown-linux-gnu-install_only_stripped.tar.gz", sha256: Some("e8017e3b916f8c7b8fbdf2bd5fc18c6eb7ce2397df240fbeea84b05d4c7a37a4") @@ -2130,7 +1998,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.11.10%2B20241016-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz", sha256: Some("03f15e19e2452641b6375b59ba094ff6cf2fc118315d24a6ca63ce60e4d4a6e0") @@ -2146,7 +2013,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.11.10%2B20241016-x86_64-unknown-linux-musl-install_only_stripped.tar.gz", sha256: Some("5b33f0ff29552f15daacf81c426ed585fae24987b47d614142a7906eae6f2b04") @@ -2162,7 +2028,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.11.10%2B20241016-i686-pc-windows-msvc-install_only_stripped.tar.gz", sha256: Some("0a5b423517722e9868ac4a63893f24f24db9bd67e8679e6e448343c5829d2e77") @@ -2178,7 +2043,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.11.10%2B20241016-x86_64-pc-windows-msvc-install_only_stripped.tar.gz", sha256: Some("ea770ebabc620ff46f1d0f905c774a9b8aa5834620e89617ad5e01f90d36b3ee") @@ -2194,7 +2058,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240814/cpython-3.11.9%2B20240814-aarch64-apple-darwin-install_only_stripped.tar.gz", sha256: Some("c4e2f7774421bcb381245945e132419b529399dfa4a56059acda1493751fa377") @@ -2210,7 +2073,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240814/cpython-3.11.9%2B20240814-x86_64-apple-darwin-install_only_stripped.tar.gz", sha256: Some("c8680f90137e36b54b3631271ccdfe5de363e7d563d8df87c53e11b956a00e04") @@ -2226,7 +2088,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240814/cpython-3.11.9%2B20240814-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz", sha256: Some("364cf099524fff92c31b8ff5ae3f7b32b0fa6cf1d380c6e37cf56140d08dfc87") @@ -2242,7 +2103,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnueabi), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240814/cpython-3.11.9%2B20240814-armv7-unknown-linux-gnueabi-install_only_stripped.tar.gz", sha256: Some("e64d3cf033c804e9c14aaf4ae746632c01894706098b20acbf00df4bd28d0b0e") @@ -2258,7 +2118,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnueabihf), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240814/cpython-3.11.9%2B20240814-armv7-unknown-linux-gnueabihf-install_only_stripped.tar.gz", sha256: Some("7630838c7602e6a6a56c41263d6a808a2a2004a7ea38770ffc4c7aaf34e169ae") @@ -2274,7 +2133,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240814/cpython-3.11.9%2B20240814-ppc64le-unknown-linux-gnu-install_only_stripped.tar.gz", sha256: Some("2387479d17127e5b087f582bac948f859c25c4b38c64f558e0a399af7a8a0225") @@ -2290,7 +2148,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240814/cpython-3.11.9%2B20240814-s390x-unknown-linux-gnu-install_only_stripped.tar.gz", sha256: Some("30c71053e9360471b7f350f1562ff4e42eb91ad2ca61b391295b5dea8b2b9efd") @@ -2306,7 +2163,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240814/cpython-3.11.9%2B20240814-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz", sha256: Some("daa487c7e73005c4426ac393273117cf0e2dc4ab9b2eeda366e04cd00eea00c9") @@ -2322,7 +2178,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240814/cpython-3.11.9%2B20240814-x86_64-unknown-linux-musl-install_only_stripped.tar.gz", sha256: Some("b3e94cbf19bd08bf02f6e6945f6c2211453f601c7c6f79721da63a06bf99b1f9") @@ -2338,7 +2193,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240814/cpython-3.11.9%2B20240814-i686-pc-windows-msvc-install_only_stripped.tar.gz", sha256: Some("091c99a210f4f401a305231f3f218ee3d5714658b8d3aac344d34efc716dff85") @@ -2354,7 +2208,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240814/cpython-3.11.9%2B20240814-x86_64-pc-windows-msvc-install_only_stripped.tar.gz", sha256: Some("8ac54a8d711ef0d49b62a2c3521c2d0403f1b221dc9d84c5f85fe48903e82523") @@ -2370,7 +2223,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.11.8%2B20240224-aarch64-apple-darwin-install_only.tar.gz", sha256: Some("389a51139f5abe071a0d70091ca5df3e7a3dfcfcbe3e0ba6ad85fb4c5638421e") @@ -2386,7 +2238,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.11.8%2B20240224-x86_64-apple-darwin-install_only.tar.gz", sha256: Some("097f467b0c36706bfec13f199a2eaf924e668f70c6e2bd1f1366806962f7e86e") @@ -2402,7 +2253,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.11.8%2B20240224-aarch64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("389b9005fb78dd5a6f68df5ea45ab7b30d9a4b3222af96999e94fd20d4ad0c6a") @@ -2418,7 +2268,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.11.8%2B20240224-ppc64le-unknown-linux-gnu-install_only.tar.gz", sha256: Some("eb2b31f8e50309aae493c6a359c32b723a676f07c641f5e8fe4b6aa4dbb50946") @@ -2434,7 +2283,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.11.8%2B20240224-s390x-unknown-linux-gnu-install_only.tar.gz", sha256: Some("844f64f4c16e24965778281da61d1e0e6cd1358a581df1662da814b1eed096b9") @@ -2450,7 +2298,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.11.8%2B20240224-x86_64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("94e13d0e5ad417035b80580f3e893a72e094b0900d5d64e7e34ab08e95439987") @@ -2466,7 +2313,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.11.8%2B20240224-x86_64-unknown-linux-musl-install_only.tar.gz", sha256: Some("08e1ebf51b5965e23f8e68664d17274c1cdabb5b2d7509a2003920e5d58172c7") @@ -2482,7 +2328,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.11.8%2B20240224-i686-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("75039951f8f94d7304bc17b674af1668b9e1ea6d6c9ba1da28e90c0ad8030e3c") @@ -2498,7 +2343,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.11.8%2B20240224-x86_64-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("b618f1f047349770ee1ef11d1b05899840abd53884b820fd25c7dfe2ec1664d4") @@ -2514,7 +2358,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.11.7%2B20240107-aarch64-apple-darwin-install_only.tar.gz", sha256: Some("b042c966920cf8465385ca3522986b12d745151a72c060991088977ca36d3883") @@ -2530,7 +2373,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.11.7%2B20240107-x86_64-apple-darwin-install_only.tar.gz", sha256: Some("a0e615eef1fafdc742da0008425a9030b7ea68a4ae4e73ac557ef27b112836d4") @@ -2546,7 +2388,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.11.7%2B20240107-aarch64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("b102eaf865eb715aa98a8a2ef19037b6cc3ae7dfd4a632802650f29de635aa13") @@ -2562,7 +2403,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.11.7%2B20240107-ppc64le-unknown-linux-gnu-install_only.tar.gz", sha256: Some("b44e1b74afe75c7b19143413632c4386708ae229117f8f950c2094e9681d34c7") @@ -2578,7 +2418,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.11.7%2B20240107-s390x-unknown-linux-gnu-install_only.tar.gz", sha256: Some("49520e3ff494708020f306e30b0964f079170be83e956be4504f850557378a22") @@ -2594,7 +2433,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.11.7%2B20240107-x86_64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("4a51ce60007a6facf64e5495f4cf322e311ba9f39a8cd3f3e4c026eae488e140") @@ -2610,7 +2448,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.11.7%2B20240107-x86_64-unknown-linux-musl-install_only.tar.gz", sha256: Some("1a919a35172eb9419eba841eeb0ec9879dbc2b006b284ee5c454c08197b50f74") @@ -2626,7 +2463,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.11.7%2B20240107-i686-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("f5a6ca1280749d8ceaf8851585ef6b0cd2f1f76e801a77c1d744019554eef2f0") @@ -2642,7 +2478,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.11.7%2B20240107-x86_64-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("67077e6fa918e4f4fd60ba169820b00be7c390c497bf9bc9cab2c255ea8e6f3e") @@ -2658,7 +2493,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.11.6%2B20231002-aarch64-apple-darwin-install_only.tar.gz", sha256: Some("916c35125b5d8323a21526d7a9154ca626453f63d0878e95b9f613a95006c990") @@ -2674,7 +2508,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.11.6%2B20231002-x86_64-apple-darwin-install_only.tar.gz", sha256: Some("178cb1716c2abc25cb56ae915096c1a083e60abeba57af001996e8bc6ce1a371") @@ -2690,7 +2523,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.11.6%2B20231002-aarch64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("3e26a672df17708c4dc928475a5974c3fb3a34a9b45c65fb4bd1e50504cc84ec") @@ -2706,7 +2538,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.11.6%2B20231002-ppc64le-unknown-linux-gnu-install_only.tar.gz", sha256: Some("7937035f690a624dba4d014ffd20c342e843dd46f89b0b0a1e5726b85deb8eaf") @@ -2722,7 +2553,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.11.6%2B20231002-s390x-unknown-linux-gnu-install_only.tar.gz", sha256: Some("f9f19823dba3209cedc4647b00f46ed0177242917db20fb7fb539970e384531c") @@ -2738,7 +2568,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.11.6%2B20231002-x86_64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("ee37a7eae6e80148c7e3abc56e48a397c1664f044920463ad0df0fc706eacea8") @@ -2754,7 +2583,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.11.6%2B20231002-x86_64-unknown-linux-musl-install_only.tar.gz", sha256: Some("c929e5fe676ad20afcf6807a797d21261ae0827e84ec18742031a9582aed0d46") @@ -2770,7 +2598,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.11.6%2B20231002-i686-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("dd48b2cfaae841b4cd9beed23e2ae68b13527a065ef3d271d228735769c4e64d") @@ -2786,7 +2613,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.11.6%2B20231002-x86_64-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("3933545e6d41462dd6a47e44133ea40995bc6efeed8c2e4cbdf1a699303e95ea") @@ -2802,7 +2628,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.11.5%2B20230826-aarch64-apple-darwin-install_only.tar.gz", sha256: Some("dab64b3580118ad2073babd7c29fd2053b616479df5c107d31fe2af1f45e948b") @@ -2818,7 +2643,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.11.5%2B20230826-x86_64-apple-darwin-install_only.tar.gz", sha256: Some("4a4efa7378c72f1dd8ebcce1afb99b24c01b07023aa6b8fea50eaedb50bf2bfc") @@ -2834,7 +2658,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.11.5%2B20230826-aarch64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("bb5c5d1ea0f199fe2d3f0996fff4b48ca6ddc415a3dbd98f50bff7fce48aac80") @@ -2850,7 +2673,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.11.5%2B20230826-i686-unknown-linux-gnu-install_only.tar.gz", sha256: Some("82de7e2551c015145c017742a5c0411d67a7544595df43c02b5efa4762d5123e") @@ -2866,7 +2688,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.11.5%2B20230826-ppc64le-unknown-linux-gnu-install_only.tar.gz", sha256: Some("14121b53e9c8c6d0741f911ae00102a35adbcf5c3cdf732687ef7617b7d7304d") @@ -2882,7 +2703,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.11.5%2B20230826-s390x-unknown-linux-gnu-install_only.tar.gz", sha256: Some("fe459da39874443579d6fe88c68777c6d3e331038e1fb92a0451879fb6beb16d") @@ -2898,7 +2718,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.11.5%2B20230826-x86_64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("fbed6f7694b2faae5d7c401a856219c945397f772eea5ca50c6eb825cbc9d1e1") @@ -2914,7 +2733,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.11.5%2B20230826-x86_64-unknown-linux-musl-install_only.tar.gz", sha256: Some("fe09ecd87f69a724acf26ca508d7ead91a951abb2da18dfb98fe22c284454121") @@ -2930,7 +2748,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.11.5%2B20230826-i686-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("936b624c2512a3a3370aae8adf603d6ae71ba8ebd39cc4714a13306891ea36f0") @@ -2946,7 +2763,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.11.5%2B20230826-x86_64-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("00f002263efc8aea896bcfaaf906b1f4dab3e5cd3db53e2b69ab9a10ba220b97") @@ -2962,7 +2778,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.11.4%2B20230726-aarch64-apple-darwin-install_only.tar.gz", sha256: Some("cb6d2948384a857321f2aa40fa67744cd9676a330f08b6dad7070bda0b6120a4") @@ -2978,7 +2793,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.11.4%2B20230726-x86_64-apple-darwin-install_only.tar.gz", sha256: Some("47e1557d93a42585972772e82661047ca5f608293158acb2778dccf120eabb00") @@ -2994,7 +2808,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.11.4%2B20230726-aarch64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("2e84fc53f4e90e11963281c5c871f593abcb24fc796a50337fa516be99af02fb") @@ -3010,7 +2823,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.11.4%2B20230726-i686-unknown-linux-gnu-install_only.tar.gz", sha256: Some("abdccc6ec7093f49da99680f5899a96bff0b96fde8f5d73f7aac121e0d05fdd8") @@ -3026,7 +2838,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.11.4%2B20230726-ppc64le-unknown-linux-gnu-install_only.tar.gz", sha256: Some("df7b92ed9cec96b3bb658fb586be947722ecd8e420fb23cee13d2e90abcfcf25") @@ -3042,7 +2853,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.11.4%2B20230726-s390x-unknown-linux-gnu-install_only.tar.gz", sha256: Some("e477f0749161f9aa7887964f089d9460a539f6b4a8fdab5166f898210e1a87a4") @@ -3058,7 +2868,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.11.4%2B20230726-x86_64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("e26247302bc8e9083a43ce9e8dd94905b40d464745b1603041f7bc9a93c65d05") @@ -3074,7 +2883,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.11.4%2B20230726-x86_64-unknown-linux-musl-install_only.tar.gz", sha256: Some("1218ca44595aeaf34271508db64a2abc581c3ee1eb307c1b0537ea746922b806") @@ -3090,7 +2898,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.11.4%2B20230726-i686-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("e2f4b41c3d89c5ec735e2563d752856cb3c19a0aa712ec7ef341712bafa7e905") @@ -3106,7 +2913,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.11.4%2B20230726-x86_64-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("878614c03ea38538ae2f758e36c85d2c0eb1eaaca86cd400ff8c76693ee0b3e1") @@ -3122,7 +2928,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.11.3%2B20230507-aarch64-apple-darwin-install_only.tar.gz", sha256: Some("09e412506a8d63edbb6901742b54da9aa7faf120b8dbdce56c57b303fc892c86") @@ -3138,7 +2943,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.11.3%2B20230507-x86_64-apple-darwin-install_only.tar.gz", sha256: Some("f710b8d60621308149c100d5175fec39274ed0b9c99645484fd93d1716ef4310") @@ -3154,7 +2958,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.11.3%2B20230507-aarch64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("8190accbbbbcf7620f1ff6d668e4dd090c639665d11188ce864b62554d40e5ab") @@ -3170,7 +2973,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.11.3%2B20230507-i686-unknown-linux-gnu-install_only.tar.gz", sha256: Some("36ff6c5ebca8bf07181b774874233eb37835a62b39493f975869acc5010d839d") @@ -3186,7 +2988,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.11.3%2B20230507-ppc64le-unknown-linux-gnu-install_only.tar.gz", sha256: Some("767d24f3570b35fedb945f5ac66224c8983f2d556ab83c5cfaa5f3666e9c212c") @@ -3202,7 +3003,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.11.3%2B20230507-x86_64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("da50b87d1ec42b3cb577dfd22a3655e43a53150f4f98a4bfb40757c9d7839ab5") @@ -3218,7 +3018,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.11.3%2B20230507-x86_64-unknown-linux-musl-install_only.tar.gz", sha256: Some("82eed5ae1ca9e60ed9b9cac97e910927ffe2e80e91161c74b2d70e44d5227de0") @@ -3234,7 +3033,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.11.3%2B20230507-i686-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("a6751e6fa5c7c4d4748ed534a7f00ad7f858f62ce73d63d44dd907036ba53985") @@ -3250,7 +3048,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.11.3%2B20230507-x86_64-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("24741066da6f35a7ff67bee65ce82eae870d84e1181843e64a7076d1571e95af") @@ -3266,7 +3063,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230116/cpython-3.11.1%2B20230116-aarch64-apple-darwin-install_only.tar.gz", sha256: Some("4918cdf1cab742a90f85318f88b8122aeaa2d04705803c7b6e78e81a3dd40f80") @@ -3282,7 +3078,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230116/cpython-3.11.1%2B20230116-x86_64-apple-darwin-install_only.tar.gz", sha256: Some("20a4203d069dc9b710f70b09e7da2ce6f473d6b1110f9535fb6f4c469ed54733") @@ -3298,7 +3093,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230116/cpython-3.11.1%2B20230116-aarch64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("debf15783bdcb5530504f533d33fda75a7b905cec5361ae8f33da5ba6599f8b4") @@ -3314,7 +3108,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230116/cpython-3.11.1%2B20230116-i686-unknown-linux-gnu-install_only.tar.gz", sha256: Some("8392230cf76c282cfeaf67dcbd2e0fac6da8cd3b3aead1250505c6ddd606caae") @@ -3330,7 +3123,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230116/cpython-3.11.1%2B20230116-x86_64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("02a551fefab3750effd0e156c25446547c238688a32fabde2995c941c03a6423") @@ -3346,7 +3138,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230116/cpython-3.11.1%2B20230116-x86_64-unknown-linux-musl-install_only.tar.gz", sha256: Some("7f0425d3e9b2283aba205493e9fe431bc2c2d67cc369bc922825b827a1b06b82") @@ -3362,7 +3153,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230116/cpython-3.11.1%2B20230116-i686-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("50b250dd261c3cca9ae8d96cb921e4ffbc64f778a198b6f8b8b0a338f77ae486") @@ -3378,7 +3168,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230116/cpython-3.11.1%2B20230116-x86_64-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("edc08979cb0666a597466176511529c049a6f0bba8adf70df441708f766de5bf") @@ -3394,7 +3183,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.10.15%2B20241016-aarch64-apple-darwin-install_only_stripped.tar.gz", sha256: Some("fa79bd909bfeb627ffe66a8b023153495ece659e5e3b2ff56268535024db851c") @@ -3410,7 +3198,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.10.15%2B20241016-x86_64-apple-darwin-install_only_stripped.tar.gz", sha256: Some("0d952fa2342794523ea7beee6a58e79e62045d0f018314ce282e9f2f1427ee2c") @@ -3426,7 +3213,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.10.15%2B20241016-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz", sha256: Some("6008b42df79a0c8a4efe3aa88c2aea1471116aa66881a8ed15f04d66438cb7f5") @@ -3442,7 +3228,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnueabi), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.10.15%2B20241016-armv7-unknown-linux-gnueabi-install_only_stripped.tar.gz", sha256: Some("38daa81e0cbdc199d69241c35855dd05709f8246484cfe66b84666e123abb7df") @@ -3458,7 +3243,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnueabihf), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.10.15%2B20241016-armv7-unknown-linux-gnueabihf-install_only_stripped.tar.gz", sha256: Some("af28aab17dd897d14ae04955b19be3080fbaa6778a251943d268bc597ac39427") @@ -3474,7 +3258,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.10.15%2B20241016-ppc64le-unknown-linux-gnu-install_only_stripped.tar.gz", sha256: Some("4b86196b928b51ef3a0d51aa1690236e3da4561e34254e2929c0fcd37b37a002") @@ -3490,7 +3273,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.10.15%2B20241016-s390x-unknown-linux-gnu-install_only_stripped.tar.gz", sha256: Some("fbac57f67ca8a684f0442ff73c511efc177850c48f508f23521a816eae34d75f") @@ -3506,7 +3288,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.10.15%2B20241016-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz", sha256: Some("25fb8e23cd3b82b748075a04fd18f3183cc7316c11d6f59eb4b0326843892600") @@ -3522,7 +3303,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.10.15%2B20241016-x86_64-unknown-linux-musl-install_only_stripped.tar.gz", sha256: Some("a169bdcd98f62421062fb9066763495913f4a86ee88c7d36e51df86d5d3cbe62") @@ -3538,7 +3318,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.10.15%2B20241016-i686-pc-windows-msvc-install_only_stripped.tar.gz", sha256: Some("976d1560a02f2b921668fafc76196c1ff1bb24ccaa76ed5567539fb6dab0aa5a") @@ -3554,7 +3333,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.10.15%2B20241016-x86_64-pc-windows-msvc-install_only_stripped.tar.gz", sha256: Some("45a95225c659f9b988f444d985df347140ecc71c0297c6857febf5ef440d689a") @@ -3570,7 +3348,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240814/cpython-3.10.14%2B20240814-aarch64-apple-darwin-install_only_stripped.tar.gz", sha256: Some("f7ca9bffbce433c8d445edd33a5424c405553d735efee65a2fc5d8bbb1c8e137") @@ -3586,7 +3363,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240814/cpython-3.10.14%2B20240814-x86_64-apple-darwin-install_only_stripped.tar.gz", sha256: Some("4404f44ec69c0708d4d88e98f39c2c1fe3bd462dc6a958b60aaf63028550c485") @@ -3602,7 +3378,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240814/cpython-3.10.14%2B20240814-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz", sha256: Some("0ffe64c77cacda7e3afcb0d8ba271c59ca0a30dfda218da39a573b412bb4afd7") @@ -3618,7 +3393,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnueabi), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240814/cpython-3.10.14%2B20240814-armv7-unknown-linux-gnueabi-install_only_stripped.tar.gz", sha256: Some("451449f18a49e6ceecf9c1f70f4aee0d1552eff103c3db291319125238182c9d") @@ -3634,7 +3408,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnueabihf), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240814/cpython-3.10.14%2B20240814-armv7-unknown-linux-gnueabihf-install_only_stripped.tar.gz", sha256: Some("7f215b85df78c568847329faeb2c5007c301741d9c4ccebbd935a3a2963197b5") @@ -3650,7 +3423,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240814/cpython-3.10.14%2B20240814-ppc64le-unknown-linux-gnu-install_only_stripped.tar.gz", sha256: Some("8b83fdd95cb864f8ebfa1a1dd7e700bb046b8283bfd0a3aa04f1ff259eaff99e") @@ -3666,7 +3438,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240814/cpython-3.10.14%2B20240814-s390x-unknown-linux-gnu-install_only_stripped.tar.gz", sha256: Some("ff1c4f010b1c6f563c71fa30f68293168536e0ed65f7d470a7e8c73252d08653") @@ -3682,7 +3453,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240814/cpython-3.10.14%2B20240814-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz", sha256: Some("159c456bb4a3802bafbce065ff54b99ddb16422500d75c1315573ee3b673af17") @@ -3698,7 +3468,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240814/cpython-3.10.14%2B20240814-x86_64-unknown-linux-musl-install_only_stripped.tar.gz", sha256: Some("8803a748f2197ec2360af6feebe9c936f4f6beabcae1db5557fdd98fc922982c") @@ -3714,7 +3483,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240814/cpython-3.10.14%2B20240814-i686-pc-windows-msvc-install_only_stripped.tar.gz", sha256: Some("a84742f13584fd39f4f4b0d9a5865621a3c88cad91b31f17f414186719063364") @@ -3730,7 +3498,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240814/cpython-3.10.14%2B20240814-x86_64-pc-windows-msvc-install_only_stripped.tar.gz", sha256: Some("61ad1abcaca639eecb5bd0b129ac0315d79f7b90cf0aca8e9fb85c9e7269c26b") @@ -3746,7 +3513,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.10.13%2B20240224-aarch64-apple-darwin-install_only.tar.gz", sha256: Some("5fdc0f6a5b5a90fd3c528e8b1da8e3aac931ea8690126c2fdb4254c84a3ff04a") @@ -3762,7 +3528,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.10.13%2B20240224-x86_64-apple-darwin-install_only.tar.gz", sha256: Some("6378dfd22f58bb553ddb02be28304d739cd730c1f95c15c74955c923a1bc3d6a") @@ -3778,7 +3543,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.10.13%2B20240224-aarch64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("a898a88705611b372297bb8fe4d23cc16b8603ce5f24494c3a8cfa65d83787f9") @@ -3794,7 +3558,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.10.13%2B20230826-i686-unknown-linux-gnu-install_only.tar.gz", sha256: Some("424d239b6df60e40849ad18505de394001233ab3d7470b5280fec6e643208bb9") @@ -3810,7 +3573,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.10.13%2B20240224-ppc64le-unknown-linux-gnu-install_only.tar.gz", sha256: Some("c23706e138a0351fc1e9def2974af7b8206bac7ecbbb98a78f5aa9e7535fee42") @@ -3826,7 +3588,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.10.13%2B20240224-s390x-unknown-linux-gnu-install_only.tar.gz", sha256: Some("09be8fb2cdfbb4a93d555f268f244dbe4d8ff1854b2658e8043aa4ec08aede3e") @@ -3842,7 +3603,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.10.13%2B20240224-x86_64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("d995d032ca702afd2fc3a689c1f84a6c64972ecd82bba76a61d525f08eb0e195") @@ -3858,7 +3618,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.10.13%2B20240224-x86_64-unknown-linux-musl-install_only.tar.gz", sha256: Some("48365ea10aa1b0768a153bfff50d1515a757d42409b02a4af4db354803f2d180") @@ -3874,7 +3633,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.10.13%2B20240224-i686-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("5365b90f9cba7186d12dd86516ece8b696db7311128e0b49c92234e01a74599f") @@ -3890,7 +3648,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.10.13%2B20240224-x86_64-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("086f7fe9156b897bb401273db8359017104168ac36f60f3af4e31ac7acd6634e") @@ -3906,7 +3663,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.10.12%2B20230726-aarch64-apple-darwin-install_only.tar.gz", sha256: Some("bc66c706ea8c5fc891635fda8f9da971a1a901d41342f6798c20ad0b2a25d1d6") @@ -3922,7 +3678,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.10.12%2B20230726-x86_64-apple-darwin-install_only.tar.gz", sha256: Some("8a6e3ed973a671de468d9c691ed9cb2c3a4858c5defffcf0b08969fba9c1dd04") @@ -3938,7 +3693,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.10.12%2B20230726-aarch64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("fee80e221663eca5174bd794cb5047e40d3910dbeadcdf1f09d405a4c1c15fe4") @@ -3954,7 +3708,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.10.12%2B20230726-i686-unknown-linux-gnu-install_only.tar.gz", sha256: Some("c7a5321a696ef6467791312368a04d36828907a8f5c557b96067fa534c716c18") @@ -3970,7 +3723,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.10.12%2B20230726-ppc64le-unknown-linux-gnu-install_only.tar.gz", sha256: Some("bb5e8cb0d2e44241725fa9b342238245503e7849917660006b0246a9c97b1d6c") @@ -3986,7 +3738,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.10.12%2B20230726-s390x-unknown-linux-gnu-install_only.tar.gz", sha256: Some("8d33d435ae6fb93ded7fc26798cc0a1a4f546a4e527012a1e2909cc314b332df") @@ -4002,7 +3753,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.10.12%2B20230726-x86_64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("a476dbca9184df9fc69fe6309cda5ebaf031d27ca9e529852437c94ec1bc43d3") @@ -4018,7 +3768,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.10.12%2B20230726-x86_64-unknown-linux-musl-install_only.tar.gz", sha256: Some("9080014bee2d4bd1f96bcbebf447d40c35ae9354382246add1160bd0d433ebf7") @@ -4034,7 +3783,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.10.12%2B20230726-i686-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("a5a5f9c9082b6503462a6b134111d3c303052cbc49ff31fff2ade38b39978e5d") @@ -4050,7 +3798,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.10.12%2B20230726-x86_64-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("c1a31c353ca44de7d1b1a3b6c55a823e9c1eed0423d4f9f66e617bdb1b608685") @@ -4066,7 +3813,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.10.11%2B20230507-aarch64-apple-darwin-install_only.tar.gz", sha256: Some("8348bc3c2311f94ec63751fb71bd0108174be1c4def002773cf519ee1506f96f") @@ -4082,7 +3828,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.10.11%2B20230507-x86_64-apple-darwin-install_only.tar.gz", sha256: Some("bd3fc6e4da6f4033ebf19d66704e73b0804c22641ddae10bbe347c48f82374ad") @@ -4098,7 +3843,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.10.11%2B20230507-aarch64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("c7573fdb00239f86b22ea0e8e926ca881d24fde5e5890851339911d76110bc35") @@ -4114,7 +3858,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.10.11%2B20230507-i686-unknown-linux-gnu-install_only.tar.gz", sha256: Some("c70518620e32b074b1b40579012f0c67191a967e43e84b8f46052b6b893f7eeb") @@ -4130,7 +3873,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.10.11%2B20230507-ppc64le-unknown-linux-gnu-install_only.tar.gz", sha256: Some("73a9d4c89ed51be39dd2de4e235078281087283e9fdedef65bec02f503e906ee") @@ -4146,7 +3888,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.10.11%2B20230507-x86_64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("c5bcaac91bc80bfc29cf510669ecad12d506035ecb3ad85ef213416d54aecd79") @@ -4162,7 +3903,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.10.11%2B20230507-x86_64-unknown-linux-musl-install_only.tar.gz", sha256: Some("c5dde3276541a8ad000ba631ec70012aa2261926c13f54d2b1de83dad61d59c1") @@ -4178,7 +3918,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.10.11%2B20230507-i686-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("e4ed3414cd0e687017f0a56fed88ff39b3f5dfb24a0d62e9c7ca55854178bcde") @@ -4194,7 +3933,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.10.11%2B20230507-x86_64-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("9c2d3604a06fcd422289df73015cd00e7271d90de28d2c910f0e2309a7f73a68") @@ -4210,7 +3948,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230116/cpython-3.10.9%2B20230116-aarch64-apple-darwin-install_only.tar.gz", sha256: Some("018d05a779b2de7a476f3b3ff2d10f503d69d14efcedd0774e6dab8c22ef84ff") @@ -4226,7 +3963,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230116/cpython-3.10.9%2B20230116-x86_64-apple-darwin-install_only.tar.gz", sha256: Some("0e685f98dce0e5bc8da93c7081f4e6c10219792e223e4b5886730fd73a7ba4c6") @@ -4242,7 +3978,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230116/cpython-3.10.9%2B20230116-aarch64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("2003750f40cd09d4bf7a850342613992f8d9454f03b3c067989911fb37e7a4d1") @@ -4258,7 +3993,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230116/cpython-3.10.9%2B20230116-i686-unknown-linux-gnu-install_only.tar.gz", sha256: Some("44566c08eb8054aa0784f76b85d2c6c70a62f4988d5e9abcce819b517b329fdd") @@ -4274,7 +4008,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230116/cpython-3.10.9%2B20230116-x86_64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("d196347aeb701a53fe2bb2b095abec38d27d0fa0443f8a1c2023a1bed6e18cdf") @@ -4290,7 +4023,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230116/cpython-3.10.9%2B20230116-x86_64-unknown-linux-musl-install_only.tar.gz", sha256: Some("cf17e6d042777170e423c6b80e096ad8273d9848708875db0d23dd45bdb3d516") @@ -4306,7 +4038,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230116/cpython-3.10.9%2B20230116-i686-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("c5c51d9a3e8d8cdac67d8f3ad7c4008de169ff1480e17021f154d5c99fcee9e3") @@ -4322,7 +4053,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230116/cpython-3.10.9%2B20230116-x86_64-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("59c6970cecb357dc1d8554bd0540eb81ee7f6d16a07acf3d14ed294ece02c035") @@ -4338,7 +4068,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.10.8%2B20221106-aarch64-apple-darwin-install_only.tar.gz", sha256: Some("d52b03817bd245d28e0a8b2f715716cd0fcd112820ccff745636932c76afa20a") @@ -4354,7 +4083,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.10.8%2B20221106-x86_64-apple-darwin-install_only.tar.gz", sha256: Some("525b79c7ce5de90ab66bd07b0ac1008bafa147ddc8a41bef15ffb7c9c1e9e7c5") @@ -4370,7 +4098,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.10.8%2B20221106-aarch64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("33170bef18c811906b738be530f934640491b065bf16c4d276c6515321918132") @@ -4386,7 +4113,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.10.8%2B20221106-i686-unknown-linux-gnu-install_only.tar.gz", sha256: Some("2deee7cbbd5dad339d713a75ec92239725d2035e833af5b9981b026dee0b9213") @@ -4402,7 +4128,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.10.8%2B20221106-x86_64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("6c8db44ae0e18e320320bbaaafd2d69cde8bfea171ae2d651b7993d1396260b7") @@ -4418,7 +4143,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.10.8%2B20221106-x86_64-unknown-linux-musl-install_only.tar.gz", sha256: Some("9f035bbe53f55fb406f95cb68459ba245b386084eeb5760f1660f416b730328d") @@ -4434,7 +4158,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.10.8%2B20221106-i686-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("94e76273166f72624128e52b5402db244cea041dab4a6bcdc70b304b66e27e95") @@ -4450,7 +4173,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.10.8%2B20221106-x86_64-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("f2b6d2f77118f06dd2ca04dae1175e44aaa5077a5ed8ddc63333c15347182bfe") @@ -4466,7 +4188,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.10.7%2B20221002-aarch64-apple-darwin-install_only.tar.gz", sha256: Some("70f6ca1da8e6fce832ad0b7f9fdaba0b84ba0ac0a4c626127acb6d49df4b8f91") @@ -4482,7 +4203,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.10.7%2B20221002-x86_64-apple-darwin-install_only.tar.gz", sha256: Some("6101f580434544d28d5590543029a7c6bdf07efa4bcdb5e4cbedb3cd83241922") @@ -4498,7 +4218,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.10.7%2B20221002-aarch64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("dfeec186a62a6068259d90e8d77e7d30eaf9c2b4ae7b205ff8caab7cb21f277c") @@ -4514,7 +4233,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.10.7%2B20221002-i686-unknown-linux-gnu-install_only.tar.gz", sha256: Some("4a611ce990dc1f32bc4b35d276f04521464127f77e1133ac5bb9c6ba23e94a82") @@ -4530,7 +4248,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.10.7%2B20221002-x86_64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("c12c9ad2b2c75464541d897c0528013adecd8be5b30acf4411f7759729841711") @@ -4546,7 +4263,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.10.7%2B20221002-x86_64-unknown-linux-musl-install_only.tar.gz", sha256: Some("3e0cab6e49ad5ef95851049463797ec713eee6e1f2fa1d99e30516d37797c3f0") @@ -4562,7 +4278,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.10.7%2B20221002-i686-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("384e711dd657c3439be4e50b2485478a7ed7a259a741d4480fc96d82cc09d318") @@ -4578,7 +4293,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.10.7%2B20221002-x86_64-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("b464352f8cbf06ab4c041b7559c9bda7e9f6001a94f67ab0a342cba078f3805f") @@ -4594,7 +4308,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.10.6%2B20220802-aarch64-apple-darwin-install_only.tar.gz", sha256: Some("efaf66acdb9a4eb33d57702607d2e667b1a319d58c167a43c96896b97419b8b7") @@ -4610,7 +4323,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.10.6%2B20220802-x86_64-apple-darwin-install_only.tar.gz", sha256: Some("7718411adf3ea1480f3f018a643eb0550282aefe39e5ecb3f363a4a566a9398c") @@ -4626,7 +4338,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.10.6%2B20220802-aarch64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("81625f5c97f61e2e3d7e9f62c484b1aa5311f21bd6545451714b949a29da5435") @@ -4642,7 +4353,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.10.6%2B20220802-i686-unknown-linux-gnu-install_only.tar.gz", sha256: Some("b152801a2609e6a38f3cc9e7e21d8b6cf5b6f31dacfcaca01e162c514e851ed6") @@ -4658,7 +4368,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.10.6%2B20220802-x86_64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("55aa2190d28dcfdf414d96dc5dcea9fe048fadcd583dc3981fec020869826111") @@ -4674,7 +4383,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.10.6%2B20220802-x86_64-unknown-linux-musl-install_only.tar.gz", sha256: Some("8cafe6409e9d192b288b84a21bc0c309f1d3f6b809a471b2858c7bf1bb09f3a7") @@ -4690,7 +4398,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.10.6%2B20220802-i686-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("27f22babf29ceebae18b2c2e38e2c48d22de686688c8a31c5f8d7d51541583c1") @@ -4706,7 +4413,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.10.6%2B20220802-x86_64-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("91889a7dbdceea585ff4d3b7856a6bb8f8a4eca83a0ff52a73542c2e67220eaa") @@ -4722,7 +4428,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220630/cpython-3.10.5%2B20220630-aarch64-apple-darwin-install_only.tar.gz", sha256: Some("19d1aa4a6d9ddb0094fc36961b129de9abe1673bce66c86cd97b582795c496a8") @@ -4738,7 +4443,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220630/cpython-3.10.5%2B20220630-x86_64-apple-darwin-install_only.tar.gz", sha256: Some("eca0584397d9a3ef6f7bb32b0476318b01c89b7b0a031ef97a0dcaa5ba5127a8") @@ -4754,7 +4458,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220630/cpython-3.10.5%2B20220630-aarch64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("012fa37c12d2647d76d004dc003302563864d2f1cd0731b71eeafad63d28b3f0") @@ -4770,7 +4473,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220630/cpython-3.10.5%2B20220630-i686-unknown-linux-gnu-install_only.tar.gz", sha256: Some("5abf5baf40f8573ce7d7e4ad323457f511833e1663e61ac5a11d5563a735159f") @@ -4786,7 +4488,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220630/cpython-3.10.5%2B20220630-x86_64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("460f87a389be28c953c24c6f942f172f9ce7f331367b4daf89cb450baedd51d7") @@ -4802,7 +4503,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220630/cpython-3.10.5%2B20220630-x86_64-unknown-linux-musl-install_only.tar.gz", sha256: Some("6aad42c7b03989173dd0e4d066e8c1e9f176f4b31d5bde26dbb5297f38f656d0") @@ -4818,7 +4518,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220630/cpython-3.10.5%2B20220630-i686-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("2846e9c7e8484034989ab218022009fdd9dcb12a7bfb4b0329a404552d37e9aa") @@ -4834,7 +4533,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220630/cpython-3.10.5%2B20220630-x86_64-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("c830ab2a3a488f9cf95e4e81c581d9ef73e483c2e6546136379443e9bb725119") @@ -4850,7 +4548,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220528/cpython-3.10.4%2B20220528-aarch64-apple-darwin-install_only.tar.gz", sha256: Some("6d2e4e6b1c403bce84cfb846400754017f525fe8017f186e8e7072fcaaf3aa71") @@ -4866,7 +4563,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220528/cpython-3.10.4%2B20220528-x86_64-apple-darwin-install_only.tar.gz", sha256: Some("c4a57a13b084d49ce8c2eb5b2662ee45b0c55b08ddd696f473233b0787f03988") @@ -4882,7 +4578,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220528/cpython-3.10.4%2B20220528-aarch64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("7a8989392dc9b41d85959a752448c60852cf0061de565e98445c27f6bbdf63be") @@ -4898,7 +4593,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220528/cpython-3.10.4%2B20220528-i686-unknown-linux-gnu-install_only.tar.gz", sha256: Some("f3bc0828a0e0a8974e3fe90b4e99549296a7578de2321d791be1bad28191921d") @@ -4914,7 +4608,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220528/cpython-3.10.4%2B20220528-x86_64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("1f8423808ad84c0e56c8e14c32685cbfbc1159e0d9f943ac946f29e84cf1b5ee") @@ -4930,7 +4623,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220528/cpython-3.10.4%2B20220528-x86_64-unknown-linux-musl-install_only.tar.gz", sha256: Some("74c8da0aa24233c76bdd984d3c9e44442eca316be8a2cb4972d9264fedb0d5e8") @@ -4946,7 +4638,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220528/cpython-3.10.4%2B20220528-i686-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("e1dfa5dde910f908cad8bd688b29d28df832f7b150555679c204580d1af0c4a6") @@ -4962,7 +4653,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220528/cpython-3.10.4%2B20220528-x86_64-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("7231ba2af9525cae620a5f4ae3bf89a939fdc053ba0cc64ee3dead8f13188005") @@ -4978,7 +4668,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220318/cpython-3.10.3%2B20220318-aarch64-apple-darwin-install_only.tar.gz", sha256: Some("db46dadfccc407aa1f66ed607eefbf12f781e343adcb1edee0a3883d081292ce") @@ -4994,7 +4683,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220318/cpython-3.10.3%2B20220318-x86_64-apple-darwin-install_only.tar.gz", sha256: Some("ec2e90b6a589db7ef9f74358b1436558167629f9e4d725c8150496f9cb08a9d4") @@ -5010,7 +4698,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220318/cpython-3.10.3%2B20220318-aarch64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("f52ee68c13c4f9356eb78a5305d3178af2cb90c38a8ce8ce9990a7cf6ff06144") @@ -5026,7 +4713,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220318/cpython-3.10.3%2B20220318-i686-unknown-linux-gnu-install_only.tar.gz", sha256: Some("2f125a927c3af52ef89af11857df988a042e26ce095129701b915e75b2ec6bff") @@ -5042,7 +4728,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220318/cpython-3.10.3%2B20220318-x86_64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("b9989411bed71ba4867538c991f20b55f549dd9131905733f0df9f3fde81ad1d") @@ -5058,7 +4743,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220318/cpython-3.10.3%2B20220318-x86_64-unknown-linux-musl-install_only.tar.gz", sha256: Some("a60b589176879bdd465659660b87e954f969bed072c03c578ec828d6134f4ae1") @@ -5074,7 +4758,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220318/cpython-3.10.3%2B20220318-i686-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("bb7f2a5143010fa482c5b442cced85516696cfc416ca92c903ef374532401a33") @@ -5090,7 +4773,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220318/cpython-3.10.3%2B20220318-x86_64-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("ba593370742ed8a7bc70ce563dd6a53e30ece1f6881e3888d334c1b485b0d9d0") @@ -5106,7 +4788,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.10.2%2B20220227-aarch64-apple-darwin-install_only.tar.gz", sha256: Some("1409acd9a506e2d1d3b65c1488db4e40d8f19d09a7df099667c87a506f71c0ef") @@ -5122,7 +4803,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.10.2%2B20220227-x86_64-apple-darwin-install_only.tar.gz", sha256: Some("8146ad4390710ec69b316a5649912df0247d35f4a42e2aa9615bffd87b3e235a") @@ -5138,7 +4818,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.10.2%2B20220227-aarch64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("8f351a8cc348bb45c0f95b8634c8345ec6e749e483384188ad865b7428342703") @@ -5154,7 +4833,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.10.2%2B20220227-i686-unknown-linux-gnu-install_only.tar.gz", sha256: Some("4fa49dab83bf82409816db431806525ce894280a509ca96c91e3efc9beed1fea") @@ -5170,7 +4848,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.10.2%2B20220227-x86_64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("9b64eca2a94f7aff9409ad70bdaa7fbbf8148692662e764401883957943620dd") @@ -5186,7 +4863,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.10.2%2B20220227-x86_64-unknown-linux-musl-install_only.tar.gz", sha256: Some("c4f398f6f7f9bbf0df98407ad66bc5760f3afc2cd8ba33a99cf4dcc8c90fd9ae") @@ -5202,7 +4878,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.10.2%2B20220227-i686-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("5321f8c2c71239b1e2002d284be8ec825d4a6f95cd921e58db71f259834b7aa1") @@ -5218,7 +4893,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.10.2%2B20220227-x86_64-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("a1d9a594cd3103baa24937ad9150c1a389544b4350e859200b3e5c036ac352bd") @@ -5234,7 +4908,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20211017/cpython-3.10.0-aarch64-apple-darwin-pgo%2Blto-20211017T1616.tar.zst", sha256: None @@ -5250,7 +4923,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20211017/cpython-3.10.0-x86_64-apple-darwin-pgo%2Blto-20211017T1616.tar.zst", sha256: None @@ -5266,7 +4938,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20211017/cpython-3.10.0-aarch64-unknown-linux-gnu-lto-20211017T1616.tar.zst", sha256: None @@ -5282,7 +4953,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20211017/cpython-3.10.0-i686-unknown-linux-gnu-pgo%2Blto-20211017T1616.tar.zst", sha256: None @@ -5298,7 +4968,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20211017/cpython-3.10.0-x86_64-unknown-linux-gnu-pgo%2Blto-20211017T1616.tar.zst", sha256: None @@ -5314,7 +4983,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20211017/cpython-3.10.0-x86_64-unknown-linux-musl-lto-20211017T1616.tar.zst", sha256: None @@ -5330,7 +4998,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20211017/cpython-3.10.0-i686-pc-windows-msvc-shared-pgo-20211017T1616.tar.zst", sha256: None @@ -5346,7 +5013,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20211017/cpython-3.10.0-x86_64-pc-windows-msvc-shared-pgo-20211017T1616.tar.zst", sha256: None @@ -5362,7 +5028,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.9.20%2B20241016-aarch64-apple-darwin-install_only_stripped.tar.gz", sha256: Some("41e9bb2d45e1a0467e534dafc6691b3d3c2b79fd9a564562f4c0c41eb343d30a") @@ -5378,7 +5043,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.9.20%2B20241016-x86_64-apple-darwin-install_only_stripped.tar.gz", sha256: Some("440f4ebc651e707ed24d5dc68d3b0b2197e7fb369bb77685b1b539dbf30ab1e5") @@ -5394,7 +5058,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.9.20%2B20241016-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz", sha256: Some("3742c9d6563527a003b12ac689c07e6965911ff89fd9cbbd3c17ac7bfb037d4a") @@ -5410,7 +5073,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnueabi), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.9.20%2B20241016-armv7-unknown-linux-gnueabi-install_only_stripped.tar.gz", sha256: Some("33f89a84b170bbed966f3028b84f5c39b3c6e30d615585107d69c9ed9fe49564") @@ -5426,7 +5088,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnueabihf), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.9.20%2B20241016-armv7-unknown-linux-gnueabihf-install_only_stripped.tar.gz", sha256: Some("73ecdd7318b44c88cc5877d039111067723510e922852fd207ac05b03a11863c") @@ -5442,7 +5103,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.9.20%2B20241016-ppc64le-unknown-linux-gnu-install_only_stripped.tar.gz", sha256: Some("556dd3e80ba644dfb8ca5a8a68681f243717d8ef4a517e486a49e1f6da46278c") @@ -5458,7 +5118,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.9.20%2B20241016-s390x-unknown-linux-gnu-install_only_stripped.tar.gz", sha256: Some("9eb2296c68484602bf9a27659ca91f6c073c8b1c97c2791bb1b0191aa8b9e45a") @@ -5474,7 +5133,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.9.20%2B20241016-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz", sha256: Some("44d9d016f9820f39e5bb542782557d46876b69d23d0a204eb2f367739da623e0") @@ -5490,7 +5148,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.9.20%2B20241016-x86_64-unknown-linux-musl-install_only_stripped.tar.gz", sha256: Some("332ce515daa15173f73d0ecebc988fadfac5583af8355d8895b3eb8086dac813") @@ -5506,7 +5163,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.9.20%2B20241016-i686-pc-windows-msvc-install_only_stripped.tar.gz", sha256: Some("4d331f59031e02c857f4afbcfc933de3c68c8fb47ce919103147d760a0d7165f") @@ -5522,7 +5178,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.9.20%2B20241016-x86_64-pc-windows-msvc-install_only_stripped.tar.gz", sha256: Some("ce3779065ab824333e8d6d0a3d055d4073cdcc9a6e60abe24929023369f91512") @@ -5538,7 +5193,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240814/cpython-3.9.19%2B20240814-aarch64-apple-darwin-install_only_stripped.tar.gz", sha256: Some("451582f8a6a8c15ef35a327afcdbf8d03b1ebba7192e90d850d092dac91f91c6") @@ -5554,7 +5208,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240814/cpython-3.9.19%2B20240814-x86_64-apple-darwin-install_only_stripped.tar.gz", sha256: Some("ebaf4336d0cbff4466c994d5bcaa92a38c91d06694d0cd675ec663259d5f37c7") @@ -5570,7 +5223,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240814/cpython-3.9.19%2B20240814-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz", sha256: Some("23d08f7f0bf151c2ea54b2c9c143bc710faf166ff74225b0f967fab1e2d7a151") @@ -5586,7 +5238,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnueabi), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240814/cpython-3.9.19%2B20240814-armv7-unknown-linux-gnueabi-install_only_stripped.tar.gz", sha256: Some("cebb879d47874f3f943a4334a8fcd8baa3cd7ef4be8cae6b4c8ae980d981a28d") @@ -5602,7 +5253,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnueabihf), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240814/cpython-3.9.19%2B20240814-armv7-unknown-linux-gnueabihf-install_only_stripped.tar.gz", sha256: Some("c32d3227c44919349172c27b35275bad379f1679f729fbd4f336625903171a1a") @@ -5618,7 +5268,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240814/cpython-3.9.19%2B20240814-ppc64le-unknown-linux-gnu-install_only_stripped.tar.gz", sha256: Some("3cc60442d5694db1abe2a0c6e73459ebb6e7ba7fc7b0a986f3699d463a8f9557") @@ -5634,7 +5283,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240814/cpython-3.9.19%2B20240814-s390x-unknown-linux-gnu-install_only_stripped.tar.gz", sha256: Some("b41f834311532ee9dcf76cad3cdeda285d0e283de2182ce9870c37c40970cdd3") @@ -5650,7 +5298,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240814/cpython-3.9.19%2B20240814-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz", sha256: Some("3b7d574b6bbf8303789a1d26b96a81dcca907381441ce15818c784e18d1db299") @@ -5666,7 +5313,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240814/cpython-3.9.19%2B20240814-x86_64-unknown-linux-musl-install_only_stripped.tar.gz", sha256: Some("5c6605b1cfa6a952420f2267d10bed9ae20a02858a769b7275d8805f6b9fe40b") @@ -5682,7 +5328,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240814/cpython-3.9.19%2B20240814-i686-pc-windows-msvc-install_only_stripped.tar.gz", sha256: Some("1c78c6dd763e6d583c3c3f917544bc4446d0e9fbe2b6e206042fa801ff9fb9ab") @@ -5698,7 +5343,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240814/cpython-3.9.19%2B20240814-x86_64-pc-windows-msvc-install_only_stripped.tar.gz", sha256: Some("426da4d31e665b77dacf15cd89494a995ed634a9b97324bbef9cf36fcda4c8a9") @@ -5714,7 +5358,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.9.18%2B20240224-aarch64-apple-darwin-install_only.tar.gz", sha256: Some("2548f911a6e316575c303ba42bb51540dc9b47a9f76a06a2a37460d93b177aa2") @@ -5730,7 +5373,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.9.18%2B20240224-x86_64-apple-darwin-install_only.tar.gz", sha256: Some("171d8b472fce0295be0e28bb702c43d5a2a39feccb3e72efe620ac3843c3e402") @@ -5746,7 +5388,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.9.18%2B20240224-aarch64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("e5bc5196baa603d635ee6b0cd141e359752ad3e8ea76127eb9141a3155c51200") @@ -5762,7 +5403,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.9.18%2B20230826-i686-unknown-linux-gnu-install_only.tar.gz", sha256: Some("10c422080317886057e968010495037ba65731ab7653bcaeabadf67a6fa5e99e") @@ -5778,7 +5418,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.9.18%2B20240224-ppc64le-unknown-linux-gnu-install_only.tar.gz", sha256: Some("d6b18df7a25fe034fd5ce4e64216df2cc78b2d4d908d2a1c94058ae700d73d22") @@ -5794,7 +5433,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.9.18%2B20240224-s390x-unknown-linux-gnu-install_only.tar.gz", sha256: Some("15d059507c7e900e9665f31e8d903e5a24a68ceed24f9a1c5ac06ab42a354f3f") @@ -5810,7 +5448,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.9.18%2B20240224-x86_64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("0e5663025121186bd17d331538a44f48b41baff247891d014f3f962cbe2716b4") @@ -5826,7 +5463,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.9.18%2B20240224-x86_64-unknown-linux-musl-install_only.tar.gz", sha256: Some("cb47455810ae63d98501b3bb4fcdfdb9924633fb2e86e62d77e523a3bdee44ba") @@ -5842,7 +5478,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.9.18%2B20240224-i686-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("904ff5d2f6402640e2b7e2b12075af0bd75b3e8685cc5248fd2a3cda3105d2a8") @@ -5858,7 +5493,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.9.18%2B20240224-x86_64-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("a9bdbd728ed4c353a4157ecf74386117fb2a2769a9353f491c528371cfe7f6cd") @@ -5874,7 +5508,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.9.17%2B20230726-aarch64-apple-darwin-install_only.tar.gz", sha256: Some("73dbe2d702210b566221da9265acc274ba15275c5d0d1fa327f44ad86cde9aa1") @@ -5890,7 +5523,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.9.17%2B20230726-x86_64-apple-darwin-install_only.tar.gz", sha256: Some("dfe1bea92c94b9cb779288b0b06e39157c5ff7e465cdd24032ac147c2af485c0") @@ -5906,7 +5538,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.9.17%2B20230726-aarch64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("b77012ddaf7e0673e4aa4b1c5085275a06eee2d66f33442b5c54a12b62b96cbe") @@ -5922,7 +5553,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.9.17%2B20230726-i686-unknown-linux-gnu-install_only.tar.gz", sha256: Some("aed29a64c835444c2f1aff83c55b14123114d74c54d96493a0eabfdd8c6d012c") @@ -5938,7 +5568,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.9.17%2B20230726-ppc64le-unknown-linux-gnu-install_only.tar.gz", sha256: Some("c591a28d943dce5cf9833e916125fdfbeb3120270c4866ee214493ccb5b83c3c") @@ -5954,7 +5583,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.9.17%2B20230726-s390x-unknown-linux-gnu-install_only.tar.gz", sha256: Some("01454d7cc7c9c2fccde42ba868c4f372eaaafa48049d49dd94c9cf2875f497e6") @@ -5970,7 +5598,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.9.17%2B20230726-x86_64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("26c4a712b4b8e11ed5c027db5654eb12927c02da4857b777afb98f7a930ce637") @@ -5986,7 +5613,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.9.17%2B20230726-x86_64-unknown-linux-musl-install_only.tar.gz", sha256: Some("194316e9cc7add1dd12be3e3eea2908fd4d623799edd7df69e360c6a446b750d") @@ -6002,7 +5628,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.9.17%2B20230726-i686-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("09f9d4bc66be5e0df2dfd1dc4742923e46c271f8f085178696c77073477aa0c1") @@ -6018,7 +5643,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.9.17%2B20230726-x86_64-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("9b9a1e21eff29dcf043cea38180cf8ca3604b90117d00062a7b31605d4157714") @@ -6034,7 +5658,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.9.16%2B20230507-aarch64-apple-darwin-install_only.tar.gz", sha256: Some("c1de1d854717a6245f45262ef1bb17b09e2c587590e7e3f406593c143ff875bd") @@ -6050,7 +5673,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.9.16%2B20230507-x86_64-apple-darwin-install_only.tar.gz", sha256: Some("3abc4d5fbbc80f5f848f280927ac5d13de8dc03aabb6ae65d8247cbb68e6f6bf") @@ -6066,7 +5688,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.9.16%2B20230507-aarch64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("f629b75ebfcafe9ceee2e796b7e4df5cf8dbd14f3c021afca078d159ab797acf") @@ -6082,7 +5703,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.9.16%2B20230507-i686-unknown-linux-gnu-install_only.tar.gz", sha256: Some("ab0a14b3ae72bf48b94820e096e86b3cf3e05729862f768e109aa8318016c4f2") @@ -6098,7 +5718,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.9.16%2B20230507-ppc64le-unknown-linux-gnu-install_only.tar.gz", sha256: Some("ff3ac35c58f67839aff9b5185a976abd3d1abbe61af02089f7105e876c1fe284") @@ -6114,7 +5733,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.9.16%2B20230507-x86_64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("2b6e146234a4ef2a8946081fc3fbfffe0765b80b690425a49ebe40b47c33445b") @@ -6130,7 +5748,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.9.16%2B20230507-x86_64-unknown-linux-musl-install_only.tar.gz", sha256: Some("5d9b13e8d5ee7a26fd0cf6e6d7e5a1ea90ddddd1f30ed2400bda60506f7dcea3") @@ -6146,7 +5763,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.9.16%2B20230507-i686-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("219532ffa49af88e3b90e9135cf3b6e1fa11cf165b03098fb9776a07af8ca6d0") @@ -6162,7 +5778,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.9.16%2B20230507-x86_64-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("cdabb47204e96ce7ea31fbd0b5ed586114dd7d8f8eddf60a509a7f70b48a1c5e") @@ -6178,7 +5793,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.9.15%2B20221106-aarch64-apple-darwin-install_only.tar.gz", sha256: Some("64dc7e1013481c9864152c3dd806c41144c79d5e9cd3140e185c6a5060bdc9ab") @@ -6194,7 +5808,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.9.15%2B20221106-x86_64-apple-darwin-install_only.tar.gz", sha256: Some("f2bcade6fc976c472f18f2b3204d67202d43ae55cf6f9e670f95e488f780da08") @@ -6210,7 +5823,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.9.15%2B20221106-aarch64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("52a8c0a67fb919f80962d992da1bddb511cdf92faf382701ce7673e10a8ff98f") @@ -6226,7 +5838,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.9.15%2B20221106-i686-unknown-linux-gnu-install_only.tar.gz", sha256: Some("bf32a86c220e4d1690bb92b67653f20b8325808accd81bff03b5c30ae74e6444") @@ -6242,7 +5853,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.9.15%2B20221106-x86_64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("cdc3a4cfddcd63b6cebdd75b14970e02d8ef0ac5be4d350e57ab5df56c19e85e") @@ -6258,7 +5868,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.9.15%2B20221106-x86_64-unknown-linux-musl-install_only.tar.gz", sha256: Some("81b1c76ac789521fcececdcdc643f6de6fc282083b1a36a9973d835fc8a39391") @@ -6274,7 +5883,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.9.15%2B20221106-i686-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("0b81089247f258f244e9792daaa03675da6f58597daa6913e82f2679862238dd") @@ -6290,7 +5898,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.9.15%2B20221106-x86_64-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("022daacab215679b87f0d200d08b9068a721605fa4721ebeda38220fc641ccf6") @@ -6306,7 +5913,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.9.14%2B20221002-aarch64-apple-darwin-install_only.tar.gz", sha256: Some("e38df7f230979ce6c53a5bafb3a81287838e5f3892c40cd1b98a0c961c444713") @@ -6322,7 +5928,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.9.14%2B20221002-x86_64-apple-darwin-install_only.tar.gz", sha256: Some("b7d3a1f4b57e9350571ccee49c82f503133de0d113a2dbaebc8ccf108fb3fe1b") @@ -6338,7 +5943,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.9.14%2B20221002-aarch64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("fe538201559ca37f44cd5f66c42a65fe7272cb4f1f63edd698b6f306771db1e9") @@ -6354,7 +5958,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.9.14%2B20221002-i686-unknown-linux-gnu-install_only.tar.gz", sha256: Some("3af1c255110c2f42ed0b7957502c92edf8b5c5e6fc5f699a2475bf8a560325c0") @@ -6370,7 +5973,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.9.14%2B20221002-x86_64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("e63d0c00a499e0202ba7a0f53ce69fca6d30237af39af9bc3c76bce6c7bf14d7") @@ -6386,7 +5988,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.9.14%2B20221002-x86_64-unknown-linux-musl-install_only.tar.gz", sha256: Some("ceb26ef5f5a9b7b47fa95225fffce9c8ef0c9c1fbeca69fbda236a0c10de7ad8") @@ -6402,7 +6003,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.9.14%2B20221002-i686-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("f3526e8416be86ff9091750ebc7388d6726acf32cc5ab0e6a60c67c6aacb2569") @@ -6418,7 +6018,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.9.14%2B20221002-x86_64-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("f111c3c129f4a5a171d25350ce58dad4c7e58fbe664e9b4f7c275345c9fe18a6") @@ -6434,7 +6033,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.9.13%2B20220802-aarch64-apple-darwin-install_only.tar.gz", sha256: Some("d9603edc296a2dcbc59d7ada780fd12527f05c3e0b99f7545112daf11636d6e5") @@ -6450,7 +6048,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.9.13%2B20220802-x86_64-apple-darwin-install_only.tar.gz", sha256: Some("9540a7efb7c8a54a48aff1cb9480e49588d9c0a3f934ad53f5b167338174afa3") @@ -6466,7 +6063,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.9.13%2B20220802-aarch64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("80415aac1b96255b9211f6a4c300f31e9940c7e07a23d0dec12b53aa52c0d25e") @@ -6482,7 +6078,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.9.13%2B20220802-i686-unknown-linux-gnu-install_only.tar.gz", sha256: Some("efcc8fef0d498afe576ab209fee001fda3b552de1a85f621f2602787aa6cf3d4") @@ -6498,7 +6093,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.9.13%2B20220802-x86_64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("ce1cfca2715e7e646dd618a8cb9baff93000e345ccc979b801fc6ccde7ce97df") @@ -6514,7 +6108,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.9.13%2B20220802-x86_64-unknown-linux-musl-install_only.tar.gz", sha256: Some("766ed7e805e8c7abc4e50f1c94814575e7978ed7bd1f9e9ccec82d66b47b567f") @@ -6530,7 +6123,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.9.13%2B20220802-i686-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("90e3879382f06fea3ba6d477f0c2a434a1e14cd83d174e1c7b87e2f22bc2e748") @@ -6546,7 +6138,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.9.13%2B20220802-x86_64-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("b538127025a467c64b3351babca2e4d2ea7bdfb7867d5febb3529c34456cdcd4") @@ -6562,7 +6153,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220502/cpython-3.9.12%2B20220502-aarch64-apple-darwin-install_only.tar.gz", sha256: Some("8dee06c07cc6429df34b6abe091a4684a86f7cec76f5d1ccc1c3ce2bd11168df") @@ -6578,7 +6168,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220502/cpython-3.9.12%2B20220502-x86_64-apple-darwin-install_only.tar.gz", sha256: Some("2453ba7f76b3df3310353b48c881d6cff622ba06e30d2b6ae91588b2bc9e481a") @@ -6594,7 +6183,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220502/cpython-3.9.12%2B20220502-aarch64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("2ee1426c181e65133e57dc55c6a685cb1fb5e63ef02d684b8a667d5c031c4203") @@ -6610,7 +6198,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220502/cpython-3.9.12%2B20220502-i686-unknown-linux-gnu-install_only.tar.gz", sha256: Some("233e1a9626d9fe13baac8de3689df48401d0ad5da1c2f134ad57d8e3e878a1a5") @@ -6626,7 +6213,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220502/cpython-3.9.12%2B20220502-x86_64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("ccca12f698b3b810d79c52f007078f520d588232a36bc12ede944ec3ea417816") @@ -6642,7 +6228,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220502/cpython-3.9.12%2B20220502-x86_64-unknown-linux-musl-install_only.tar.gz", sha256: Some("dd0eaf7ef64008d4a51a73243f368e0311b7936b0ac18f8d1305fffb0dfb76e6") @@ -6658,7 +6243,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220502/cpython-3.9.12%2B20220502-i686-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("8b7e440137bfa349a008641a75a2b1fd8ae22d290731778a144878a59a721c51") @@ -6674,7 +6258,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220502/cpython-3.9.12%2B20220502-x86_64-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("3024147fd987d9e1b064a3d94932178ff8e0fe98cfea955704213c0762fee8df") @@ -6690,7 +6273,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220318/cpython-3.9.11%2B20220318-aarch64-apple-darwin-install_only.tar.gz", sha256: Some("cf92a28f98c8d884df0937bf19d5f1a40caa25a6a211a237b7e9b592b2b71c2b") @@ -6706,7 +6288,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220318/cpython-3.9.11%2B20220318-x86_64-apple-darwin-install_only.tar.gz", sha256: Some("43889d1a424c84fb155e1619f062adb6984fbde80b6043611790f22bcbeec300") @@ -6722,7 +6303,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220318/cpython-3.9.11%2B20220318-aarch64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("0e50f099409c5e651b5fddd16124af1d830d11653e786a93c28e5b8f8aa470c4") @@ -6738,7 +6318,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220318/cpython-3.9.11%2B20220318-i686-unknown-linux-gnu-install_only.tar.gz", sha256: Some("75ac727631eab002bd120246197a8235145cb90687be181f7a52de6f41d44d34") @@ -6754,7 +6333,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220318/cpython-3.9.11%2B20220318-x86_64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("0429d5ceb095d5e24c292bf1a39208b88ae236a680ef8fa3e1830e3a1a7e8882") @@ -6770,7 +6348,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220318/cpython-3.9.11%2B20220318-x86_64-unknown-linux-musl-install_only.tar.gz", sha256: Some("ae8f55d90ae173f96e81f376daa5a9969a77531a6f7b8eacbe8ad90b41bbca1d") @@ -6786,7 +6363,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220318/cpython-3.9.11%2B20220318-i686-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("ceac8729b285a8c8e861176dd2dadd7f8e7e26d8f64cac6c6226a14d2252cd4c") @@ -6802,7 +6378,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220318/cpython-3.9.11%2B20220318-x86_64-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("0c529a511f7a03908fc126c4a8467b47e24a4d98812147e8e786cf59e86febf0") @@ -6818,7 +6393,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.9.10%2B20220227-aarch64-apple-darwin-install_only.tar.gz", sha256: Some("ad66c2a3e7263147e046a32694de7b897a46fb0124409d29d3a93ede631c8aee") @@ -6834,7 +6408,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.9.10%2B20220227-x86_64-apple-darwin-install_only.tar.gz", sha256: Some("fdaf594142446029e314a9beb91f1ac75af866320b50b8b968181e592550cd68") @@ -6850,7 +6423,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.9.10%2B20220227-aarch64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("12dd1f125762f47975990ec744532a1cf3db74ad60f4dfb476ca42deb7f78ca4") @@ -6866,7 +6438,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.9.10%2B20220227-i686-unknown-linux-gnu-install_only.tar.gz", sha256: Some("37ba43845c3df9ba012d69121ad29ea7f21ea2f5994a155007cf1560d74ce503") @@ -6882,7 +6453,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.9.10%2B20220227-x86_64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("455089cc576bd9a58db45e919d1fc867ecdbb0208067dffc845cc9bbf0701b70") @@ -6898,7 +6468,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.9.10%2B20220227-x86_64-unknown-linux-musl-install_only.tar.gz", sha256: Some("30add63ec16e07ad13e19f6d7061f7e4c7b971962354f48ab3e85656ce3b393d") @@ -6914,7 +6483,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.9.10%2B20220227-i686-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("56c0342a9af0412676e89cdf7b52ac76037031786b3f5c40942b8b82d366c96f") @@ -6930,7 +6498,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.9.10%2B20220227-x86_64-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("c145d9d8143ce163670af124b623d7a2405143a3708b033b4d33eed355e61b24") @@ -6946,7 +6513,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20211017/cpython-3.9.7-aarch64-apple-darwin-pgo%2Blto-20211017T1616.tar.zst", sha256: None @@ -6962,7 +6528,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20211017/cpython-3.9.7-x86_64-apple-darwin-pgo%2Blto-20211017T1616.tar.zst", sha256: None @@ -6978,7 +6543,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20211017/cpython-3.9.7-aarch64-unknown-linux-gnu-lto-20211017T1616.tar.zst", sha256: None @@ -6994,7 +6558,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20211017/cpython-3.9.7-i686-unknown-linux-gnu-pgo%2Blto-20211017T1616.tar.zst", sha256: None @@ -7010,7 +6573,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20211017/cpython-3.9.7-x86_64-unknown-linux-gnu-pgo%2Blto-20211017T1616.tar.zst", sha256: None @@ -7026,7 +6588,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20211017/cpython-3.9.7-x86_64-unknown-linux-musl-lto-20211017T1616.tar.zst", sha256: None @@ -7042,7 +6603,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20211017/cpython-3.9.7-i686-pc-windows-msvc-shared-pgo-20211017T1616.tar.zst", sha256: None @@ -7058,7 +6618,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20211017/cpython-3.9.7-x86_64-pc-windows-msvc-shared-pgo-20211017T1616.tar.zst", sha256: None @@ -7074,7 +6633,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210724/cpython-3.9.6-aarch64-apple-darwin-pgo%2Blto-20210724T1424.tar.zst", sha256: None @@ -7090,7 +6648,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210724/cpython-3.9.6-x86_64-apple-darwin-pgo%2Blto-20210724T1424.tar.zst", sha256: None @@ -7106,7 +6663,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210724/cpython-3.9.6-aarch64-unknown-linux-gnu-lto-20210724T1424.tar.zst", sha256: None @@ -7122,7 +6678,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210724/cpython-3.9.6-i686-unknown-linux-gnu-pgo%2Blto-20210724T1424.tar.zst", sha256: None @@ -7138,7 +6693,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210724/cpython-3.9.6-x86_64-unknown-linux-gnu-pgo%2Blto-20210724T1424.tar.zst", sha256: None @@ -7154,7 +6708,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210724/cpython-3.9.6-x86_64-unknown-linux-musl-lto-20210724T1424.tar.zst", sha256: None @@ -7170,7 +6723,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210724/cpython-3.9.6-i686-pc-windows-msvc-shared-pgo-20210724T1424.tar.zst", sha256: None @@ -7186,7 +6738,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210724/cpython-3.9.6-x86_64-pc-windows-msvc-shared-pgo-20210724T1424.tar.zst", sha256: None @@ -7202,7 +6753,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210506/cpython-3.9.5-aarch64-apple-darwin-pgo%2Blto-20210506T0943.tar.zst", sha256: None @@ -7218,7 +6768,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210506/cpython-3.9.5-x86_64-apple-darwin-pgo%2Blto-20210506T0943.tar.zst", sha256: None @@ -7234,7 +6783,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210506/cpython-3.9.5-i686-unknown-linux-gnu-pgo%2Blto-20210506T0943.tar.zst", sha256: None @@ -7250,7 +6798,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210506/cpython-3.9.5-x86_64-unknown-linux-gnu-pgo%2Blto-20210506T0943.tar.zst", sha256: None @@ -7266,7 +6813,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210506/cpython-3.9.5-x86_64-unknown-linux-musl-lto-20210506T0943.tar.zst", sha256: None @@ -7282,7 +6828,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210506/cpython-3.9.5-i686-pc-windows-msvc-shared-pgo-20210506T0943.tar.zst", sha256: None @@ -7298,7 +6843,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210506/cpython-3.9.5-x86_64-pc-windows-msvc-shared-pgo-20210506T0943.tar.zst", sha256: None @@ -7314,7 +6858,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210415/cpython-3.9.4-aarch64-apple-darwin-pgo%2Blto-20210414T1515.tar.zst", sha256: None @@ -7330,7 +6873,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210415/cpython-3.9.4-x86_64-apple-darwin-pgo%2Blto-20210414T1515.tar.zst", sha256: None @@ -7346,7 +6888,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210415/cpython-3.9.4-i686-unknown-linux-gnu-pgo%2Blto-20210414T1515.tar.zst", sha256: None @@ -7362,7 +6903,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210415/cpython-3.9.4-x86_64-unknown-linux-gnu-pgo%2Blto-20210414T1515.tar.zst", sha256: None @@ -7378,7 +6918,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210415/cpython-3.9.4-x86_64-unknown-linux-musl-lto-20210414T1515.tar.zst", sha256: None @@ -7394,7 +6933,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210415/cpython-3.9.4-i686-pc-windows-msvc-shared-pgo-20210414T1515.tar.zst", sha256: None @@ -7410,7 +6948,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210415/cpython-3.9.4-x86_64-pc-windows-msvc-shared-pgo-20210414T1515.tar.zst", sha256: None @@ -7426,7 +6963,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210414/cpython-3.9.3-aarch64-apple-darwin-pgo%2Blto-20210413T2055.tar.zst", sha256: None @@ -7442,7 +6978,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210414/cpython-3.9.3-x86_64-apple-darwin-pgo%2Blto-20210413T2055.tar.zst", sha256: None @@ -7458,7 +6993,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210414/cpython-3.9.3-x86_64-unknown-linux-gnu-pgo%2Blto-20210413T2055.tar.zst", sha256: None @@ -7474,7 +7008,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210414/cpython-3.9.3-x86_64-unknown-linux-musl-lto-20210413T2055.tar.zst", sha256: None @@ -7490,7 +7023,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210414/cpython-3.9.3-i686-pc-windows-msvc-shared-pgo-20210413T2055.tar.zst", sha256: None @@ -7506,7 +7038,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210414/cpython-3.9.3-x86_64-pc-windows-msvc-shared-pgo-20210413T2055.tar.zst", sha256: None @@ -7522,7 +7053,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210327/cpython-3.9.2-aarch64-apple-darwin-pgo%2Blto-20210327T1202.tar.zst", sha256: None @@ -7538,7 +7068,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210327/cpython-3.9.2-x86_64-apple-darwin-pgo%2Blto-20210327T1202.tar.zst", sha256: None @@ -7554,7 +7083,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210327/cpython-3.9.2-i686-unknown-linux-gnu-pgo%2Blto-20210327T1202.tar.zst", sha256: None @@ -7570,7 +7098,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210327/cpython-3.9.2-x86_64-unknown-linux-gnu-pgo%2Blto-20210327T1202.tar.zst", sha256: None @@ -7586,7 +7113,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210327/cpython-3.9.2-x86_64-unknown-linux-musl-lto-20210327T1202.tar.zst", sha256: None @@ -7602,7 +7128,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210327/cpython-3.9.2-i686-pc-windows-msvc-shared-pgo-20210327T1202.tar.zst", sha256: None @@ -7618,7 +7143,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210327/cpython-3.9.2-x86_64-pc-windows-msvc-shared-pgo-20210327T1202.tar.zst", sha256: None @@ -7634,7 +7158,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210103/cpython-3.9.1-x86_64-apple-darwin-pgo-20210103T1125.tar.zst", sha256: None @@ -7650,7 +7173,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210103/cpython-3.9.1-x86_64-unknown-linux-gnu-pgo-20210103T1125.tar.zst", sha256: None @@ -7666,7 +7188,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210103/cpython-3.9.1-x86_64-unknown-linux-musl-noopt-20210103T1125.tar.zst", sha256: None @@ -7682,7 +7203,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210103/cpython-3.9.1-i686-pc-windows-msvc-shared-pgo-20210103T1125.tar.zst", sha256: None @@ -7698,7 +7218,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210103/cpython-3.9.1-x86_64-pc-windows-msvc-shared-pgo-20210103T1125.tar.zst", sha256: None @@ -7714,7 +7233,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20201020/cpython-3.9.0-x86_64-apple-darwin-pgo-20201020T0626.tar.zst", sha256: None @@ -7730,7 +7248,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20201020/cpython-3.9.0-x86_64-unknown-linux-gnu-pgo-20201020T0627.tar.zst", sha256: None @@ -7746,7 +7263,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20201020/cpython-3.9.0-x86_64-unknown-linux-musl-noopt-20201020T0627.tar.zst", sha256: None @@ -7762,7 +7278,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20201020/cpython-3.9.0-i686-pc-windows-msvc-shared-pgo-20201021T0245.tar.zst", sha256: None @@ -7778,7 +7293,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20201020/cpython-3.9.0-x86_64-pc-windows-msvc-shared-pgo-20201021T0245.tar.zst", sha256: None @@ -7794,7 +7308,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241002/cpython-3.8.20%2B20241002-aarch64-apple-darwin-install_only_stripped.tar.gz", sha256: Some("30ba44af64e599bde7307908393374bdcd99e185bf9b3c9de3f697f3fbe6bf8f") @@ -7810,7 +7323,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241002/cpython-3.8.20%2B20241002-x86_64-apple-darwin-install_only_stripped.tar.gz", sha256: Some("375b6eead6c852cabbf3ccfd43dc4f6dd4c36381bf74c9a7910acb839fd5c57f") @@ -7826,7 +7338,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241002/cpython-3.8.20%2B20241002-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz", sha256: Some("75a187ebfab81096e3f3d91d70c1349e64defbdfb0e8a067cb5233d017655e31") @@ -7842,7 +7353,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241002/cpython-3.8.20%2B20241002-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz", sha256: Some("a3a75094545912d4e9413673441b3f0d2e58ce9b264477f910800148801ccf11") @@ -7858,7 +7368,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241002/cpython-3.8.20%2B20241002-x86_64-unknown-linux-musl-install_only_stripped.tar.gz", sha256: Some("fcddfd3f1090833e1f3106be021809630008b53026bc96dcaab2986625db27fa") @@ -7874,7 +7383,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241002/cpython-3.8.20%2B20241002-i686-pc-windows-msvc-install_only_stripped.tar.gz", sha256: Some("d829105aaf53a1cadf8738e040c6211bc9bef2c6e4757b972954f0f322d57e7d") @@ -7890,7 +7398,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20241002/cpython-3.8.20%2B20241002-x86_64-pc-windows-msvc-install_only_stripped.tar.gz", sha256: Some("ec2f723dcfbf09581578a716c05cc67823a43d77111e6dd9e0d1557ccc6dcbf3") @@ -7906,7 +7413,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240814/cpython-3.8.19%2B20240814-aarch64-apple-darwin-install_only_stripped.tar.gz", sha256: Some("6a15ee2b507aed4d5b15fd1b66fc570aa49183f15aa6c412eccd065446f17d8e") @@ -7922,7 +7428,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240814/cpython-3.8.19%2B20240814-x86_64-apple-darwin-install_only_stripped.tar.gz", sha256: Some("1a24263b039c1172bd42d74a5694492f3e3dbe4d3e52a1e7cc2856fee7dbee4a") @@ -7938,7 +7443,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240814/cpython-3.8.19%2B20240814-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz", sha256: Some("202211923850303f521146ee1831642aaf357ffeeadbe13a0a91884317227528") @@ -7954,7 +7458,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240814/cpython-3.8.19%2B20240814-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz", sha256: Some("0f1579dbb01c98af7a12fef4c9aa8a99d45b91393f64431f5de712f892bc5c0b") @@ -7970,7 +7473,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240814/cpython-3.8.19%2B20240814-x86_64-unknown-linux-musl-install_only_stripped.tar.gz", sha256: Some("6ee6c7469c9d2c7078beb95a9a3a261c42502e0b1603722a0689bdb2e789060c") @@ -7986,7 +7488,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240814/cpython-3.8.19%2B20240814-i686-pc-windows-msvc-install_only_stripped.tar.gz", sha256: Some("73bf0135330b96c48ca79ccd6d2f3287a7466573a5fc1b62d982bcdb1d5f0ab3") @@ -8002,7 +7503,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240814/cpython-3.8.19%2B20240814-x86_64-pc-windows-msvc-install_only_stripped.tar.gz", sha256: Some("89d238b125cd7546b7d0cbd7f484a438d2c2f239c15c9b38ec3c62b1f343a6ca") @@ -8018,7 +7518,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.8.18%2B20240224-aarch64-apple-darwin-install_only.tar.gz", sha256: Some("4d493a1792bf211f37f98404cc1468f09bd781adc2602dea0df82ad264c11abc") @@ -8034,7 +7533,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.8.18%2B20240224-x86_64-apple-darwin-install_only.tar.gz", sha256: Some("7d2cd8d289d5e3cdd0a8c06c028c7c621d3d00ce44b7e2f08c1724ae0471c626") @@ -8050,7 +7548,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.8.18%2B20240224-aarch64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("6588c9eed93833d9483d01fe40ac8935f691a1af8e583d404ec7666631b52487") @@ -8066,7 +7563,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.8.18%2B20240224-x86_64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("5ae36825492372554c02708bdd26b8dcd57e3dbf34b3d6d599ad91d93540b2b7") @@ -8082,7 +7578,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.8.18%2B20240224-x86_64-unknown-linux-musl-install_only.tar.gz", sha256: Some("e591d3925f88f78a5dffb765fd10b9dab6e497d35cf58169da83eab521c86a37") @@ -8098,7 +7593,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.8.18%2B20240224-i686-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("c24f9c9e8638cff0ce6aa808a57cc5f22009bc33e3bcf410a726b79d7c5545fe") @@ -8114,7 +7608,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.8.18%2B20240224-x86_64-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("dba923ee5df8f99db04f599e826be92880746c02247c8d8e4d955d4bc711af11") @@ -8130,7 +7623,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.8.17%2B20230826-aarch64-apple-darwin-install_only.tar.gz", sha256: Some("c6f7a130d0044a78e39648f4dae56dcff5a41eba91888a99f6e560507162e6a1") @@ -8146,7 +7638,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.8.17%2B20230826-x86_64-apple-darwin-install_only.tar.gz", sha256: Some("155b06821607bae1a58ecc60a7d036b358c766f19e493b8876190765c883a5c2") @@ -8162,7 +7653,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.8.17%2B20230826-aarch64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("9f6d585091fe26906ff1dbb80437a3fe37a1e3db34d6ecc0098f3d6a78356682") @@ -8178,7 +7668,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.8.17%2B20230826-i686-unknown-linux-gnu-install_only.tar.gz", sha256: Some("e580fdd923bbae612334559dc58bd5fd13cce53b769294d63bc88e7c6662f7d9") @@ -8194,7 +7683,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.8.17%2B20230826-x86_64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("8d3e1826c0bb7821ec63288038644808a2d45553245af106c685ef5892fabcd8") @@ -8210,7 +7698,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.8.17%2B20230826-x86_64-unknown-linux-musl-install_only.tar.gz", sha256: Some("322b7837cfd8282c62ae3d2f0e98f0843cbe287e4b8c4852b786123f2e13b307") @@ -8226,7 +7713,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.8.17%2B20230826-i686-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("cb6af626ba811044e9c5ee09140a6920565d2b1b237a11886b96354a9fcc242e") @@ -8242,7 +7728,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.8.17%2B20230826-x86_64-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("6428e1b4e0b4482d390828de7d4c82815257443416cb786abe10cb2466ca68cd") @@ -8258,7 +7743,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.8.16%2B20230726-aarch64-apple-darwin-install_only.tar.gz", sha256: Some("7e484eb6de40d6f6bdfd5099eaa9647f65e45fb6d846ccfc56b1cb1e38b5ab02") @@ -8274,7 +7758,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.8.16%2B20230726-x86_64-apple-darwin-install_only.tar.gz", sha256: Some("28506e509646c11cb2f57a7203bd1b08b6e8e5b159ae308bd5bb93b0d334bdaf") @@ -8290,7 +7773,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.8.16%2B20230726-aarch64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("9c6615931fd1045bf9f2148aa7dd9ce1ece8575ed68a5483a0b615322a43d54c") @@ -8306,7 +7788,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.8.16%2B20230726-i686-unknown-linux-gnu-install_only.tar.gz", sha256: Some("1260fd6af34104bbd57489175e6f7bfea76d4bd06a242a0f8e20e390e870b227") @@ -8322,7 +7803,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.8.16%2B20230726-x86_64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("b1f1502c3a13b899724dbd32bd77a973fa9733b932c5700d747fe33d5de9ac4f") @@ -8338,7 +7818,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.8.16%2B20230726-x86_64-unknown-linux-musl-install_only.tar.gz", sha256: Some("840aefa3b03b66b6561360735dc0ac4e0a36a3ebb4d1f85d92f5b5f6638953cc") @@ -8354,7 +7833,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.8.16%2B20230726-i686-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("77466f93ef5b030cf13d0446067089b0ce0d415cc6d1702655bdbb12a8c18c97") @@ -8370,7 +7848,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.8.16%2B20230726-x86_64-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("120b3312fa79bac2ace45641171c2bc590c4e4462d7ad124d64597e124a36ae7") @@ -8386,7 +7863,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.8.15%2B20221106-aarch64-apple-darwin-install_only.tar.gz", sha256: Some("1e0a92d1a4f5e6d4a99f86b1cbf9773d703fe7fd032590f3e9c285c7a5eeb00a") @@ -8402,7 +7878,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.8.15%2B20221106-x86_64-apple-darwin-install_only.tar.gz", sha256: Some("70b57f28c2b5e1e3dd89f0d30edd5bc414e8b20195766cf328e1b26bed7890e1") @@ -8418,7 +7893,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.8.15%2B20221106-aarch64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("886ab33ced13c84bf59ce8ff79eba6448365bfcafea1bf415bd1d75e21b690aa") @@ -8434,7 +7908,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.8.15%2B20221106-i686-unknown-linux-gnu-install_only.tar.gz", sha256: Some("3bc1f49147913d93cea9cbb753fbaae90b86f1ee979f975c4712a35f02cbd86b") @@ -8450,7 +7923,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.8.15%2B20221106-x86_64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("e47edfb2ceaf43fc699e20c179ec428b6f3e497cf8e2dcd8e9c936d4b96b1e56") @@ -8466,7 +7938,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.8.15%2B20221106-x86_64-unknown-linux-musl-install_only.tar.gz", sha256: Some("f767d0438eca5b18c1267c5121055a5808a1412ea7668ef17da3dc9bdd24a55f") @@ -8482,7 +7953,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.8.15%2B20221106-i686-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("318c059324b84b5d7685bcd0874698799d9e3689b51dbcf596e7a47a39a3d49a") @@ -8498,7 +7968,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.8.15%2B20221106-x86_64-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("2fdc3fa1c95f982179bbbaedae2b328197658638799b6dcb63f9f494b0de59e2") @@ -8514,7 +7983,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.8.14%2B20221002-aarch64-apple-darwin-install_only.tar.gz", sha256: Some("6c17f6dcda59de5d8eee922ef7eede403a540dae05423ef2c2a042d8d4f22467") @@ -8530,7 +7998,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.8.14%2B20221002-x86_64-apple-darwin-install_only.tar.gz", sha256: Some("3ed4db8d0308c584196d97c629058ea69bbd8b7f9a034cf8c2c701ebb286c091") @@ -8546,7 +8013,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.8.14%2B20221002-aarch64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("c45e42deee43e3ebc4ca5b019c37d8ae25fb5b5f1ba5f602098a81b99d2bc804") @@ -8562,7 +8028,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.8.14%2B20221002-i686-unknown-linux-gnu-install_only.tar.gz", sha256: Some("d01d813939ad549ca253c52e5b8361b4490cc5c8cbda00ab6e0c524565153e2b") @@ -8578,7 +8043,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.8.14%2B20221002-x86_64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("4eb53bce831bf52682067579c09ccaccb6524dd44bd4b8047454c69b4817f4f0") @@ -8594,7 +8058,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.8.14%2B20221002-x86_64-unknown-linux-musl-install_only.tar.gz", sha256: Some("72c08b1c1d8cc14cb8d22eab18b463bb514ea160472fdc7400bd69ae375cf9c4") @@ -8610,7 +8073,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.8.14%2B20221002-i686-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("a0730f3a9e60581f02bdb852953fbb52cf98e8431259fa39cb668a060bd002a0") @@ -8626,7 +8088,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.8.14%2B20221002-x86_64-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("1af39953b4c8324ed0608e316bc763006f27e76643155d92eae18e4db6fc162f") @@ -8642,7 +8103,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.8.13%2B20220802-aarch64-apple-darwin-install_only.tar.gz", sha256: Some("ae4131253d890b013171cb5f7b03cadc585ae263719506f7b7e063a7cf6fde76") @@ -8658,7 +8118,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.8.13%2B20220802-x86_64-apple-darwin-install_only.tar.gz", sha256: Some("cd6e7c0a27daf7df00f6882eaba01490dd963f698e99aeee9706877333e0df69") @@ -8674,7 +8133,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.8.13%2B20220802-aarch64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("8dc7814bf3425bbf78c6e6e5a6529ded6ae463fa6a4b79c025b343bae4fd955a") @@ -8690,7 +8148,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.8.13%2B20220802-i686-unknown-linux-gnu-install_only.tar.gz", sha256: Some("9485599ad9053dfba08c91854717272e95b7c81e0d099d9c51a46fc5a095ccb4") @@ -8706,7 +8163,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.8.13%2B20220802-x86_64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("fb566629ccb5f76ef56d275a3f8017d683f1c20c5beb5d5f38b155ed11e16187") @@ -8722,7 +8178,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.8.13%2B20220802-x86_64-unknown-linux-musl-install_only.tar.gz", sha256: Some("2c90a0d048caf146d4c33560d6eead1428a225219018d364b1af77f23c492984") @@ -8738,7 +8193,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.8.13%2B20220802-i686-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("a50668d4c5fbcb374d3ca93ee18db910bc3b462693db073669f31e6da993abf9") @@ -8754,7 +8208,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.8.13%2B20220802-x86_64-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("f20643f1b3e263a56287319aea5c3888530c09ad9de3a5629b1a5d207807e6b9") @@ -8770,7 +8223,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.8.12%2B20220227-aarch64-apple-darwin-install_only.tar.gz", sha256: Some("f9a3cbb81e0463d6615125964762d133387d561b226a30199f5b039b20f1d944") @@ -8786,7 +8238,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.8.12%2B20220227-x86_64-apple-darwin-install_only.tar.gz", sha256: Some("f323fbc558035c13a85ce2267d0fad9e89282268ecb810e364fff1d0a079d525") @@ -8802,7 +8253,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.8.12%2B20220227-i686-unknown-linux-gnu-install_only.tar.gz", sha256: Some("fcb2033f01a2b10a51be68c9a1b4c7d7759b582f58a503371fe67ab59987b418") @@ -8818,7 +8268,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.8.12%2B20220227-x86_64-unknown-linux-gnu-install_only.tar.gz", sha256: Some("5be9c6d61e238b90dfd94755051c0d3a2d8023ebffdb4b0fa4e8fedd09a6cab6") @@ -8834,7 +8283,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.8.12%2B20220227-x86_64-unknown-linux-musl-install_only.tar.gz", sha256: Some("27faf8aa62de2cd4e59b75a6edce4cab549eba81f0f9cc21df0e370a8a2f3a25") @@ -8850,7 +8298,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.8.12%2B20220227-i686-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("aaa75b9115af73dc3daf7db050ed4f60fd67d2a23ebab30670f18fb8cfa71f33") @@ -8866,7 +8313,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.8.12%2B20220227-x86_64-pc-windows-msvc-shared-install_only.tar.gz", sha256: Some("4658e08a00d60b1e01559b74d58ff4dd04da6df935d55f6268a15d6d0a679d74") @@ -8882,7 +8328,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210724/cpython-3.8.11-x86_64-apple-darwin-pgo%2Blto-20210724T1424.tar.zst", sha256: None @@ -8898,7 +8343,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210724/cpython-3.8.11-i686-unknown-linux-gnu-pgo%2Blto-20210724T1424.tar.zst", sha256: None @@ -8914,7 +8358,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210724/cpython-3.8.11-x86_64-unknown-linux-gnu-pgo%2Blto-20210724T1424.tar.zst", sha256: None @@ -8930,7 +8373,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210724/cpython-3.8.11-x86_64-unknown-linux-musl-lto-20210724T1424.tar.zst", sha256: None @@ -8946,7 +8388,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210724/cpython-3.8.11-i686-pc-windows-msvc-shared-pgo-20210724T1424.tar.zst", sha256: None @@ -8962,7 +8403,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210724/cpython-3.8.11-x86_64-pc-windows-msvc-shared-pgo-20210724T1424.tar.zst", sha256: None @@ -8978,7 +8418,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210506/cpython-3.8.10-x86_64-apple-darwin-pgo%2Blto-20210506T0943.tar.zst", sha256: None @@ -8994,7 +8433,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210506/cpython-3.8.10-i686-unknown-linux-gnu-pgo%2Blto-20210506T0943.tar.zst", sha256: None @@ -9010,7 +8448,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210506/cpython-3.8.10-x86_64-unknown-linux-gnu-pgo%2Blto-20210506T0943.tar.zst", sha256: None @@ -9026,7 +8463,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210506/cpython-3.8.10-x86_64-unknown-linux-musl-lto-20210506T0943.tar.zst", sha256: None @@ -9042,7 +8478,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210506/cpython-3.8.10-i686-pc-windows-msvc-shared-pgo-20210506T0943.tar.zst", sha256: None @@ -9058,7 +8493,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210506/cpython-3.8.10-x86_64-pc-windows-msvc-shared-pgo-20210506T0943.tar.zst", sha256: None @@ -9074,7 +8508,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210415/cpython-3.8.9-x86_64-apple-darwin-pgo%2Blto-20210414T1515.tar.zst", sha256: None @@ -9090,7 +8523,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210415/cpython-3.8.9-i686-unknown-linux-gnu-pgo%2Blto-20210414T1515.tar.zst", sha256: None @@ -9106,7 +8538,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210415/cpython-3.8.9-x86_64-unknown-linux-gnu-pgo%2Blto-20210414T1515.tar.zst", sha256: None @@ -9122,7 +8553,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210415/cpython-3.8.9-x86_64-unknown-linux-musl-lto-20210414T1515.tar.zst", sha256: None @@ -9138,7 +8568,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210415/cpython-3.8.9-i686-pc-windows-msvc-shared-pgo-20210414T1515.tar.zst", sha256: None @@ -9154,7 +8583,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210415/cpython-3.8.9-x86_64-pc-windows-msvc-shared-pgo-20210414T1515.tar.zst", sha256: None @@ -9170,7 +8598,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210327/cpython-3.8.8-x86_64-apple-darwin-pgo%2Blto-20210327T1202.tar.zst", sha256: None @@ -9186,7 +8613,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210327/cpython-3.8.8-i686-unknown-linux-gnu-pgo%2Blto-20210327T1202.tar.zst", sha256: None @@ -9202,7 +8628,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210327/cpython-3.8.8-x86_64-unknown-linux-gnu-pgo%2Blto-20210327T1202.tar.zst", sha256: None @@ -9218,7 +8643,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210327/cpython-3.8.8-x86_64-unknown-linux-musl-lto-20210327T1202.tar.zst", sha256: None @@ -9234,7 +8658,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210327/cpython-3.8.8-i686-pc-windows-msvc-shared-pgo-20210327T1202.tar.zst", sha256: None @@ -9250,7 +8673,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210327/cpython-3.8.8-x86_64-pc-windows-msvc-shared-pgo-20210327T1202.tar.zst", sha256: None @@ -9266,7 +8688,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210103/cpython-3.8.7-x86_64-apple-darwin-pgo-20210103T1125.tar.zst", sha256: None @@ -9282,7 +8703,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210103/cpython-3.8.7-x86_64-unknown-linux-gnu-pgo-20210103T1125.tar.zst", sha256: None @@ -9298,7 +8718,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210103/cpython-3.8.7-x86_64-unknown-linux-musl-noopt-20210103T1125.tar.zst", sha256: None @@ -9314,7 +8733,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210103/cpython-3.8.7-i686-pc-windows-msvc-shared-pgo-20210103T1125.tar.zst", sha256: None @@ -9330,7 +8748,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20210103/cpython-3.8.7-x86_64-pc-windows-msvc-shared-pgo-20210103T1125.tar.zst", sha256: None @@ -9346,7 +8763,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20201020/cpython-3.8.6-x86_64-apple-darwin-pgo-20201020T0626.tar.zst", sha256: None @@ -9362,7 +8778,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20201020/cpython-3.8.6-x86_64-unknown-linux-gnu-pgo-20201020T0627.tar.zst", sha256: None @@ -9378,7 +8793,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20201020/cpython-3.8.6-x86_64-unknown-linux-musl-noopt-20201020T0627.tar.zst", sha256: None @@ -9394,7 +8808,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20201020/cpython-3.8.6-i686-pc-windows-msvc-shared-pgo-20201021T0233.tar.zst", sha256: None @@ -9410,7 +8823,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20201020/cpython-3.8.6-x86_64-pc-windows-msvc-shared-pgo-20201021T0232.tar.zst", sha256: None @@ -9426,7 +8838,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20200823/cpython-3.8.5-x86_64-apple-darwin-pgo-20200823T2228.tar.zst", sha256: None @@ -9442,7 +8853,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20200822/cpython-3.8.5-x86_64-unknown-linux-gnu-pgo-20200823T0036.tar.zst", sha256: None @@ -9458,7 +8868,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20200822/cpython-3.8.5-x86_64-unknown-linux-musl-noopt-20200823T0036.tar.zst", sha256: None @@ -9474,7 +8883,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20200830/cpython-3.8.5-i686-pc-windows-msvc-shared-pgo-20200830T2311.tar.zst", sha256: None @@ -9490,7 +8898,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20200830/cpython-3.8.5-x86_64-pc-windows-msvc-shared-pgo-20200830T2254.tar.zst", sha256: None @@ -9506,7 +8913,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20200530/cpython-3.8.3-x86_64-apple-darwin-pgo-20200530T1845.tar.zst", sha256: None @@ -9522,7 +8928,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20200517/cpython-3.8.3-x86_64-unknown-linux-gnu-pgo-20200518T0040.tar.zst", sha256: None @@ -9538,7 +8943,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20200517/cpython-3.8.3-x86_64-unknown-linux-musl-noopt-20200518T0040.tar.zst", sha256: None @@ -9554,7 +8958,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20200517/cpython-3.8.3-i686-pc-windows-msvc-shared-pgo-20200518T0154.tar.zst", sha256: None @@ -9570,7 +8973,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20200517/cpython-3.8.3-x86_64-pc-windows-msvc-shared-pgo-20200517T2207.tar.zst", sha256: None @@ -9586,7 +8988,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20200418/cpython-3.8.2-x86_64-apple-darwin-pgo-20200418T2238.tar.zst", sha256: None @@ -9602,7 +9003,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20200418/cpython-3.8.2-x86_64-unknown-linux-gnu-pgo-20200418T2243.tar.zst", sha256: None @@ -9618,7 +9018,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20200418/cpython-3.8.2-x86_64-unknown-linux-musl-noopt-20200418T2309.tar.zst", sha256: None @@ -9634,7 +9033,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20200418/cpython-3.8.2-i686-pc-windows-msvc-shared-pgo-20200418T2315.tar.zst", sha256: None @@ -9650,7 +9048,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20200418/cpython-3.8.2-x86_64-pc-windows-msvc-shared-pgo-20200418T2315.tar.zst", sha256: None @@ -9666,7 +9063,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20200823/cpython-3.7.9-x86_64-apple-darwin-pgo-20200823T2228.tar.zst", sha256: None @@ -9682,7 +9078,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20200822/cpython-3.7.9-x86_64-unknown-linux-gnu-pgo-20200823T0036.tar.zst", sha256: None @@ -9698,7 +9093,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20200822/cpython-3.7.9-x86_64-unknown-linux-musl-noopt-20200823T0036.tar.zst", sha256: None @@ -9714,7 +9108,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20200822/cpython-3.7.9-i686-pc-windows-msvc-shared-pgo-20200823T0159.tar.zst", sha256: None @@ -9730,7 +9123,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20200822/cpython-3.7.9-x86_64-pc-windows-msvc-shared-pgo-20200823T0118.tar.zst", sha256: None @@ -9746,7 +9138,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20200530/cpython-3.7.7-x86_64-apple-darwin-pgo-20200530T1845.tar.zst", sha256: None @@ -9762,7 +9153,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20200517/cpython-3.7.7-x86_64-unknown-linux-gnu-pgo-20200518T0040.tar.zst", sha256: None @@ -9778,7 +9168,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Musl), variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20200517/cpython-3.7.7-x86_64-unknown-linux-musl-noopt-20200518T0040.tar.zst", sha256: None @@ -9794,7 +9183,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20200517/cpython-3.7.7-i686-pc-windows-msvc-shared-pgo-20200517T2153.tar.zst", sha256: None @@ -9810,7 +9198,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20200517/cpython-3.7.7-x86_64-pc-windows-msvc-shared-pgo-20200517T2128.tar.zst", sha256: None @@ -9826,7 +9213,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20200216/cpython-3.7.6-windows-x86-shared-pgo-20200217T0110.tar.zst", sha256: None @@ -9842,7 +9228,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://github.com/indygreg/python-build-standalone/releases/download/20200216/cpython-3.7.6-windows-amd64-shared-pgo-20200217T0022.tar.zst", sha256: None @@ -9858,7 +9243,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.10-v7.3.17-macos_arm64.tar.bz2", sha256: Some("a050e25e8d686853dd5afc363e55625165825dacfb55f8753d8225ebe417cfd2") @@ -9874,7 +9258,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.10-v7.3.17-macos_x86_64.tar.bz2", sha256: Some("6c2c5f2300d7564e711421b4968abd63243cb96f76e363975dd648ebf4a362ee") @@ -9890,7 +9273,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.10-v7.3.17-aarch64.tar.bz2", sha256: Some("53b6e5907df869c49e4eae7aca09fba16d150741097efb245892c1477d2395f2") @@ -9906,7 +9288,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.10-v7.3.17-linux32.tar.bz2", sha256: Some("e534110e1047da37c1d586c392f74de3424f871d906a2083de6d41f2a8cc9164") @@ -9922,7 +9303,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.10-v7.3.16-s390x.tar.bz2", sha256: Some("af97efe498a209ba18c7bc7d084164a9907fb3736588b6864955177e19d5216a") @@ -9938,7 +9318,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.10-v7.3.17-linux64.tar.bz2", sha256: Some("fdcdb9b24f1a7726003586503fdeb264fd68fc37fbfcea022dcfe825a7fee18b") @@ -9954,7 +9333,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.10-v7.3.17-win64.zip", sha256: Some("cab794a03ddda26238c72942ea6f225612e0dc17c76cac6652da83a95024e6e8") @@ -9970,7 +9348,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.10-v7.3.15-macos_arm64.tar.bz2", sha256: Some("d927c5105ea7880f7596fe459183e35cc17c853ef5105678b2ad62a8d000a548") @@ -9986,7 +9363,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.10-v7.3.15-macos_x86_64.tar.bz2", sha256: Some("559b61ba7e7c5a5c23cef5370f1fab47ccdb939ac5d2b42b4bef091abe3f6964") @@ -10002,7 +9378,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.10-v7.3.15-aarch64.tar.bz2", sha256: Some("52146fccaf64e87e71d178dda8de63c01577ec3923073dc69e1519622bcacb74") @@ -10018,7 +9393,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.10-v7.3.15-linux32.tar.bz2", sha256: Some("75dd58c9abd8b9d78220373148355bc3119febcf27a2c781d64ad85e7232c4aa") @@ -10034,7 +9408,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.10-v7.3.15-s390x.tar.bz2", sha256: Some("209e57596381e13c9914d1332f359dc4b78de06576739747eb797bdbf85062b8") @@ -10050,7 +9423,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.10-v7.3.15-linux64.tar.bz2", sha256: Some("33c584e9a70a71afd0cb7dd8ba9996720b911b3b8ed0156aea298d4487ad22c3") @@ -10066,7 +9438,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.10-v7.3.15-win64.zip", sha256: Some("b378b3ab1c3719aee0c3e5519e7bff93ff67b2d8aa987fe4f088b54382db676c") @@ -10082,7 +9453,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.10-v7.3.12-macos_arm64.tar.bz2", sha256: Some("45671b1e9437f95ccd790af10dbeb57733cca1ed9661463b727d3c4f5caa7ba0") @@ -10098,7 +9468,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.10-v7.3.12-macos_x86_64.tar.bz2", sha256: Some("dbc15d8570560d5f79366883c24bc42231a92855ac19a0f28cb0adeb11242666") @@ -10114,7 +9483,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.10-v7.3.12-aarch64.tar.bz2", sha256: Some("26208b5a134d9860a08f74cce60960005758e82dc5f0e3566a48ed863a1f16a1") @@ -10130,7 +9498,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.10-v7.3.12-linux32.tar.bz2", sha256: Some("811667825ae58ada4b7c3d8bc1b5055b9f9d6a377e51aedfbe0727966603f60e") @@ -10146,7 +9513,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.10-v7.3.12-s390x.tar.bz2", sha256: Some("043c13a585479428b463ab69575a088db74aadc16798d6e677d97f563585fee3") @@ -10162,7 +9528,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.10-v7.3.12-linux64.tar.bz2", sha256: Some("6c577993160b6f5ee8cab73cd1a807affcefafe2f7441c87bd926c10505e8731") @@ -10178,7 +9543,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.10-v7.3.12-win64.zip", sha256: Some("8c3b1d34fb99100e230e94560410a38d450dc844effbee9ea183518e4aff595c") @@ -10194,7 +9558,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.9-v7.3.16-macos_arm64.tar.bz2", sha256: Some("88f824e7a2d676440d09bc90fc959ae0fd3557d7e2f14bfbbe53d41d159a47fe") @@ -10210,7 +9573,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.9-v7.3.16-macos_x86_64.tar.bz2", sha256: Some("fda015431621e7e5aa16359d114f2c45a77ed936992c1efff86302e768a6b21c") @@ -10226,7 +9588,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.9-v7.3.16-aarch64.tar.bz2", sha256: Some("de3f2ed3581b30555ac0dd3e4df78a262ec736a36fb2e8f28259f8539b278ef4") @@ -10242,7 +9603,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.9-v7.3.16-linux32.tar.bz2", sha256: Some("583b6d6dd4e8c07cbc04da04a7ec2bdfa6674825289c2378c5e018d5abe779ea") @@ -10258,7 +9618,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.9-v7.3.16-s390x.tar.bz2", sha256: Some("7a56ebb27dba3110dc1ff52d8e0449cdb37fe5c2275f7faf11432e4e164833ba") @@ -10274,7 +9633,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.9-v7.3.16-linux64.tar.bz2", sha256: Some("16f9c5b808c848516e742986e826b833cdbeda09ad8764e8704595adbe791b23") @@ -10290,7 +9648,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.9-v7.3.16-win64.zip", sha256: Some("06ec12a5e964dc0ad33e6f380185a4d295178dce6d6df512f508e7aee00a1323") @@ -10306,7 +9663,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.9-v7.3.15-macos_arm64.tar.bz2", sha256: Some("300541c32125767a91b182b03d9cc4257f04971af32d747ecd4d62549d72acfd") @@ -10322,7 +9678,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.9-v7.3.15-macos_x86_64.tar.bz2", sha256: Some("18ad7c9cb91c5e8ef9d40442b2fd1f6392ae113794c5b6b7d3a45e04f19edec6") @@ -10338,7 +9693,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.9-v7.3.15-aarch64.tar.bz2", sha256: Some("03e35fcba290454bb0ccf7ee57fb42d1e63108d10d593776a382c0a2fe355de0") @@ -10354,7 +9708,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.9-v7.3.15-linux32.tar.bz2", sha256: Some("c6209380977066c9e8b96e8258821c70f996004ce1bc8659ae83d4fd5a89ff5c") @@ -10370,7 +9723,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.9-v7.3.15-s390x.tar.bz2", sha256: Some("deeb5e54c36a0fd9cfefd16e63a0d5bed4f4a43e6bbc01c23f0ed8f7f1c0aaf3") @@ -10386,7 +9738,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.9-v7.3.15-linux64.tar.bz2", sha256: Some("f062be307200bde434817e1620cebc13f563d6ab25309442c5f4d0f0d68f0912") @@ -10402,7 +9753,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.9-v7.3.15-win64.zip", sha256: Some("a156dad8b58570597eaaabe05663f00f80c60bc11df4a9c46d0953b6c5eb9209") @@ -10418,7 +9768,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.9-v7.3.12-macos_arm64.tar.bz2", sha256: Some("0e8a1a3468b9790c734ac698f5b00cc03fc16899ccc6ce876465fac0b83980e3") @@ -10434,7 +9783,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.9-v7.3.12-macos_x86_64.tar.bz2", sha256: Some("64f008ffa070c407e5ef46c8256b2e014de7196ea5d858385861254e7959f4eb") @@ -10450,7 +9798,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.9-v7.3.12-aarch64.tar.bz2", sha256: Some("e9327fb9edaf2ad91935d5b8563ec5ff24193bddb175c1acaaf772c025af1824") @@ -10466,7 +9813,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.9-v7.3.12-linux32.tar.bz2", sha256: Some("aa04370d38f451683ccc817d76c2b3e0f471dbb879e0bd618d9affbdc9cd37a4") @@ -10482,7 +9828,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.9-v7.3.12-s390x.tar.bz2", sha256: Some("20d84658a6899bdd2ca35b00ead33a2f56cff2c40dce1af630466d27952f6d4f") @@ -10498,7 +9843,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.9-v7.3.12-linux64.tar.bz2", sha256: Some("84c89b966fab2b58f451a482ee30ca7fec3350435bd0b9614615c61dc6da2390") @@ -10514,7 +9858,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.9-v7.3.12-win64.zip", sha256: Some("0996054207b401aeacace1aa11bad82cfcb463838a1603c5f263626c47bbe0e6") @@ -10530,7 +9873,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.9-v7.3.11-macos_arm64.tar.bz2", sha256: Some("91ad7500f1a39531dbefa0b345a3dcff927ff9971654e8d2e9ef7c5ae311f57e") @@ -10546,7 +9888,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.9-v7.3.11-macos_x86_64.tar.bz2", sha256: Some("d33f40b207099872585afd71873575ca6ea638a27d823bc621238c5ae82542ed") @@ -10562,7 +9903,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.9-v7.3.11-aarch64.tar.bz2", sha256: Some("09175dc652ed895d98e9ad63d216812bf3ee7e398d900a9bf9eb2906ba8302b9") @@ -10578,7 +9918,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.9-v7.3.11-linux32.tar.bz2", sha256: Some("0099d72c2897b229057bff7e2c343624aeabdc60d6fb43ca882bff082f1ffa48") @@ -10594,7 +9933,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.9-v7.3.11-s390x.tar.bz2", sha256: Some("e1f30f2ddbe3f446ddacd79677b958d56c07463b20171fb2abf8f9a3178b79fc") @@ -10610,7 +9948,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.9-v7.3.11-linux64.tar.bz2", sha256: Some("d506172ca11071274175d74e9c581c3166432d0179b036470e3b9e8d20eae581") @@ -10626,7 +9963,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.9-v7.3.11-win64.zip", sha256: Some("57faad132d42d3e7a6406fcffafffe0b4f390cf0e2966abb8090d073c6edf405") @@ -10642,7 +9978,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.9-v7.3.10-macos_arm64.tar.bz2", sha256: Some("e2a6bec7408e6497c7de8165aa4a1b15e2416aec4a72f2578f793fb06859ccba") @@ -10658,7 +9993,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.9-v7.3.10-macos_x86_64.tar.bz2", sha256: Some("f90c8619b41e68ec9ffd7d5e913fe02e60843da43d3735b1c1bc75bcfe638d97") @@ -10674,7 +10008,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.9-v7.3.10-aarch64.tar.bz2", sha256: Some("657a04fd9a5a992a2f116a9e7e9132ea0c578721f59139c9fb2083775f71e514") @@ -10690,7 +10023,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.9-v7.3.10-linux32.tar.bz2", sha256: Some("b6db59613b9a1c0c1ab87bc103f52ee95193423882dc8a848b68850b8ba59cc5") @@ -10706,7 +10038,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.9-v7.3.10-s390x.tar.bz2", sha256: Some("ca6525a540cf0c682d1592ae35d3fbc97559a97260e4b789255cc76dde7a14f0") @@ -10722,7 +10053,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.9-v7.3.10-linux64.tar.bz2", sha256: Some("95cf99406179460d63ddbfe1ec870f889d05f7767ce81cef14b88a3a9e127266") @@ -10738,7 +10068,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.9-v7.3.10-win64.zip", sha256: Some("07e18b7b24c74af9730dfaab16e24b22ef94ea9a4b64cbb2c0d80610a381192a") @@ -10754,7 +10083,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.9-v7.3.9-osx64.tar.bz2", sha256: Some("59c8852168b2b1ba1f0211ff043c678760380d2f9faf2f95042a8878554dbc25") @@ -10770,7 +10098,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.9-v7.3.9-aarch64.tar.bz2", sha256: Some("2e1ae193d98bc51439642a7618d521ea019f45b8fb226940f7e334c548d2b4b9") @@ -10786,7 +10113,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.9-v7.3.9-linux32.tar.bz2", sha256: Some("0de4b9501cf28524cdedcff5052deee9ea4630176a512bdc408edfa30914bae7") @@ -10802,7 +10128,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.9-v7.3.9-s390x.tar.bz2", sha256: Some("774dca83bcb4403fb99b3d155e7bd572ef8c52b9fe87a657109f64e75ad71732") @@ -10818,7 +10143,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.9-v7.3.9-linux64.tar.bz2", sha256: Some("46818cb3d74b96b34787548343d266e2562b531ddbaf330383ba930ff1930ed5") @@ -10834,7 +10158,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.9-v7.3.9-win64.zip", sha256: Some("be48ab42f95c402543a7042c999c9433b17e55477c847612c8733a583ca6dff5") @@ -10850,7 +10173,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.9-v7.3.8-osx64.tar.bz2", sha256: Some("95bd88ac8d6372cd5b7b5393de7b7d5c615a0c6e42fdb1eb67f2d2d510965aee") @@ -10866,7 +10188,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.9-v7.3.8-aarch64-portable.tar.bz2", sha256: Some("b7282bc4484bceae5bc4cc04e05ee4faf51cb624c8fc7a69d92e5fdf0d0c96aa") @@ -10882,7 +10203,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.9-v7.3.8-linux32.tar.bz2", sha256: Some("a0d18e4e73cc655eb02354759178b8fb161d3e53b64297d05e2fff91f7cf862d") @@ -10898,7 +10218,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.9-v7.3.8-s390x.tar.bz2", sha256: Some("37b596bfe76707ead38ffb565629697e9b6fa24e722acc3c632b41ec624f5d95") @@ -10914,7 +10233,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.9-v7.3.8-linux64.tar.bz2", sha256: Some("129a055032bba700cd1d0acacab3659cf6b7180e25b1b2f730e792f06d5b3010") @@ -10930,7 +10248,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.9-v7.3.8-win64.zip", sha256: Some("c1b2e4cde2dcd1208d41ef7b7df8e5c90564a521e7a5db431673da335a1ba697") @@ -10946,7 +10263,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.8-v7.3.11-macos_arm64.tar.bz2", sha256: Some("78cdc79ff964c4bfd13eb45a7d43a011cbe8d8b513323d204891f703fdc4fa1a") @@ -10962,7 +10278,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.8-v7.3.11-macos_x86_64.tar.bz2", sha256: Some("194ca0b4d91ae409a9cb1a59eb7572d7affa8a451ea3daf26539aa515443433a") @@ -10978,7 +10293,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.8-v7.3.11-aarch64.tar.bz2", sha256: Some("9a2fa0b8d92b7830aa31774a9a76129b0ff81afbd22cd5c41fbdd9119e859f55") @@ -10994,7 +10308,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.8-v7.3.11-linux32.tar.bz2", sha256: Some("a79b31fce8f5bc1f9940b6777134189a1d3d18bda4b1c830384cda90077c9176") @@ -11010,7 +10323,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.8-v7.3.11-s390x.tar.bz2", sha256: Some("eab7734d86d96549866f1cba67f4f9c73c989f6a802248beebc504080d4c3fcd") @@ -11026,7 +10338,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.8-v7.3.11-linux64.tar.bz2", sha256: Some("470330e58ac105c094041aa07bb05676b06292bc61409e26f5c5593ebb2292d9") @@ -11042,7 +10353,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.8-v7.3.11-win64.zip", sha256: Some("0f46fb6df32941ea016f77cfd7e9b426d5ac25a2af2453414df66103941c8435") @@ -11058,7 +10368,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.8-v7.3.10-macos_arm64.tar.bz2", sha256: Some("6cb1429371e4854b718148a509d80143f801e3abfc72fef58d88aeeee1e98f9e") @@ -11074,7 +10383,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.8-v7.3.10-macos_x86_64.tar.bz2", sha256: Some("399eb1ce4c65f62f6a096b7c273536601b7695e3c0dc0457393a659b95b7615b") @@ -11090,7 +10398,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.8-v7.3.10-aarch64.tar.bz2", sha256: Some("e4caa1a545f22cfee87d5b9aa6f8852347f223643ad7d2562e0b2a2f4663ad98") @@ -11106,7 +10413,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.8-v7.3.10-linux32.tar.bz2", sha256: Some("b70ed7fdc73a74ebdc04f07439f7bad1a849aaca95e26b4a74049d0e483f071c") @@ -11122,7 +10428,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.8-v7.3.10-s390x.tar.bz2", sha256: Some("c294f8e815158388628fe77ac5b8ad6cd93c8db1359091fa02d41cf6da4d61a1") @@ -11138,7 +10443,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.8-v7.3.10-linux64.tar.bz2", sha256: Some("ceef6496fd4ab1c99e3ec22ce657b8f10f8bb77a32427fadfb5e1dd943806011") @@ -11154,7 +10458,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.8-v7.3.10-win64.zip", sha256: Some("362dd624d95bd64743190ea2539b97452ecb3d53ea92ceb2fbe9f48dc60e6b8f") @@ -11170,7 +10473,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.8-v7.3.9-osx64.tar.bz2", sha256: Some("91a5c2c1facd5a4931a8682b7d792f7cf4f2ba25cd2e7e44e982139a6d5e4840") @@ -11186,7 +10488,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.8-v7.3.9-aarch64.tar.bz2", sha256: Some("5e124455e207425e80731dff317f0432fa0aba1f025845ffca813770e2447e32") @@ -11202,7 +10503,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.8-v7.3.9-linux32.tar.bz2", sha256: Some("4b261516c6c59078ab0c8bd7207327a1b97057b4ec1714ed5e79a026f9efd492") @@ -11218,7 +10518,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.8-v7.3.9-s390x.tar.bz2", sha256: Some("c6177a0016c9145c7b99fddb5d74cc2e518ccdb216a6deb51ef6a377510cc930") @@ -11234,7 +10533,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.8-v7.3.9-linux64.tar.bz2", sha256: Some("08be25ec82fc5d23b78563eda144923517daba481a90af0ace7a047c9c9a3c34") @@ -11250,7 +10548,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.8-v7.3.9-win64.zip", sha256: Some("05022baaa55db2b60880f2422312d9e4025e1267303ac57f33e8253559d0be88") @@ -11266,7 +10563,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.8-v7.3.8-osx64.tar.bz2", sha256: Some("de1b283ff112d76395c0162a1cf11528e192bdc230ee3f1b237f7694c7518dee") @@ -11282,7 +10578,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.8-v7.3.8-aarch64-portable.tar.bz2", sha256: Some("0210536e9f1841ba283c13b04783394050837bb3e6f4091c9f1bd9c7f2b94b55") @@ -11298,7 +10593,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.8-v7.3.8-linux32.tar.bz2", sha256: Some("bea4b275decd492af6462157d293dd6fcf08a949859f8aec0959537b40afd032") @@ -11314,7 +10608,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.8-v7.3.8-s390x.tar.bz2", sha256: Some("ad53d373d6e275a41ca64da7d88afb6a17e48e7bfb2a6fff92daafdc06da6b90") @@ -11330,7 +10623,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.8-v7.3.8-linux64.tar.bz2", sha256: Some("089f8e3e357d6130815964ddd3507c13bd53e4976ccf0a89b5c36a9a6775a188") @@ -11346,7 +10638,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.8-v7.3.8-win64.zip", sha256: Some("0894c468e7de758c509a602a28ef0ba4fbf197ccdf946c7853a7283d9bb2a345") @@ -11362,7 +10653,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.7-v7.3.9-osx64.tar.bz2", sha256: Some("12d92f578a200d50959e55074b20f29f93c538943e9a6e6522df1a1cc9cef542") @@ -11378,7 +10668,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.7-v7.3.9-aarch64.tar.bz2", sha256: Some("dfc62f2c453fb851d10a1879c6e75c31ffebbf2a44d181bb06fcac4750d023fc") @@ -11394,7 +10683,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.7-v7.3.9-linux32.tar.bz2", sha256: Some("3398cece0167b81baa219c9cd54a549443d8c0a6b553ec8ec13236281e0d86cd") @@ -11410,7 +10698,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.7-v7.3.9-s390x.tar.bz2", sha256: Some("fcab3b9e110379948217cf592229542f53c33bfe881006f95ce30ac815a6df48") @@ -11426,7 +10713,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.7-v7.3.9-linux64.tar.bz2", sha256: Some("c58195124d807ecc527499ee19bc511ed753f4f2e418203ca51bc7e3b124d5d1") @@ -11442,7 +10728,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.7-v7.3.9-win64.zip", sha256: Some("8acb184b48fb3c854de0662e4d23a66b90e73b1ab73a86695022c12c745d8b00") @@ -11458,7 +10743,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.7-v7.3.8-osx64.tar.bz2", sha256: Some("76b8eef5b059a7e478f525615482d2a6e9feb83375e3f63c16381d80521a693f") @@ -11474,7 +10758,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.7-v7.3.8-aarch64-portable.tar.bz2", sha256: Some("639c76f128a856747aee23a34276fa101a7a157ea81e76394fbaf80b97dcf2f2") @@ -11490,7 +10773,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.7-v7.3.8-linux32.tar.bz2", sha256: Some("38429ec6ea1aca391821ee4fbda7358ae86de4600146643f2af2fe2c085af839") @@ -11506,7 +10788,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.7-v7.3.8-s390x.tar.bz2", sha256: Some("5c2cd3f7cf04cb96f6bcc6b02e271f5d7275867763978e66651b8d1605ef3141") @@ -11522,7 +10803,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.7-v7.3.8-linux64.tar.bz2", sha256: Some("409085db79a6d90bfcf4f576dca1538498e65937acfbe03bd4909bdc262ff378") @@ -11538,7 +10818,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.7-v7.3.8-win64.zip", sha256: Some("96df67492bc8d62b2e71dddf5f6c58965a26cac9799c5f4081401af0494b3bcc") @@ -11554,7 +10833,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.7-v7.3.5-osx64.tar.bz2", sha256: Some("b3a7d3099ad83de7c267bb79ae609d5ce73b01800578ffd91ba7e221b13f80db") @@ -11570,7 +10848,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.7-v7.3.5-aarch64.tar.bz2", sha256: Some("85d83093b3ef5b863f641bc4073d057cc98bb821e16aa9361a5ff4898e70e8ee") @@ -11586,7 +10863,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.7-v7.3.5-linux32.tar.bz2", sha256: Some("3dd8b565203d372829e53945c599296fa961895130342ea13791b17c84ed06c4") @@ -11602,7 +10878,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.7-v7.3.5-s390x.tar.bz2", sha256: Some("dffdf5d73613be2c6809dc1a3cf3ee6ac2f3af015180910247ff24270b532ed5") @@ -11618,7 +10893,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.7-v7.3.5-linux64.tar.bz2", sha256: Some("9000db3e87b54638e55177e68cbeb30a30fe5d17b6be48a9eb43d65b3ebcfc26") @@ -11634,7 +10908,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.7-v7.3.5-win64.zip", sha256: Some("072bd22427178dc4e65d961f50281bd2f56e11c4e4d9f16311c703f69f46ae24") @@ -11650,7 +10923,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Darwin), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.7-v7.3.3-osx64.tar.bz2", sha256: Some("d72b27d5bb60813273f14f07378a08822186a66e216c5d1a768ad295b582438d") @@ -11666,7 +10938,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.7-v7.3.3-aarch64.tar.bz2", sha256: Some("ee4aa041558b58de6063dd6df93b3def221c4ca4c900d6a9db5b1b52135703a8") @@ -11682,7 +10953,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.7-v7.3.3-linux32.tar.bz2", sha256: Some("7d81b8e9fcd07c067cfe2f519ab770ec62928ee8787f952cadf2d2786246efc8") @@ -11698,7 +10968,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.7-v7.3.3-s390x.tar.bz2", sha256: Some("92000d90b9a37f2e9cb7885f2a872adfa9e48e74bf7f84a8b8185c8181f0502d") @@ -11714,7 +10983,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Linux), libc: Libc::Some(target_lexicon::Environment::Gnu), variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.7-v7.3.3-linux64.tar.bz2", sha256: Some("37e2804c4661c86c857d709d28c7de716b000d31e89766599fdf5a98928b7096") @@ -11730,7 +10998,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ os: Os(target_lexicon::OperatingSystem::Windows), libc: Libc::None, variant: PythonVariant::Default - }, url: "https://downloads.python.org/pypy/pypy3.7-v7.3.3-win32.zip", sha256: Some("a282ce40aa4f853e877a5dbb38f0a586a29e563ae9ba82fd50c7e5dc465fb649") diff --git a/crates/uv-python/src/downloads.inc.mustache b/crates/uv-python/src/downloads.inc.mustache index 2de417e78158..a556d02fe0b8 100644 --- a/crates/uv-python/src/downloads.inc.mustache +++ b/crates/uv-python/src/downloads.inc.mustache @@ -22,7 +22,6 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ libc: Libc::None, {{/value.libc}} variant: {{value.variant}} - }, url: "{{value.url}}", {{#value.sha256}} diff --git a/crates/uv-python/src/downloads.rs b/crates/uv-python/src/downloads.rs index 4e865375da52..b278e5c31f4f 100644 --- a/crates/uv-python/src/downloads.rs +++ b/crates/uv-python/src/downloads.rs @@ -39,10 +39,10 @@ pub enum Error { InvalidPythonVersion(String), #[error("Invalid request key (too many parts): {0}")] TooManyParts(String), - #[error(transparent)] - NetworkError(#[from] WrappedReqwestError), - #[error(transparent)] - NetworkMiddlewareError(#[from] anyhow::Error), + #[error("Failed to download {0}")] + NetworkError(Url, #[source] WrappedReqwestError), + #[error("Failed to download {0}")] + NetworkMiddlewareError(Url, #[source] anyhow::Error), #[error("Failed to extract archive: {0}")] ExtractError(String, #[source] uv_extract::Error), #[error("Failed to hash installation")] @@ -423,7 +423,7 @@ pub enum DownloadResult { } impl ManagedPythonDownload { - /// Return the first [`PythonDownload`] matching a request, if any. + /// Return the first [`ManagedPythonDownload`] matching a request, if any. pub fn from_request( request: &PythonDownloadRequest, ) -> Result<&'static ManagedPythonDownload, Error> { @@ -433,7 +433,7 @@ impl ManagedPythonDownload { .ok_or(Error::NoDownloadFound(request.clone())) } - /// Iterate over all [`PythonDownload`]'s. + /// Iterate over all [`ManagedPythonDownload`]s. pub fn iter_all() -> impl Iterator { PYTHON_DOWNLOADS .iter() @@ -465,13 +465,14 @@ impl ManagedPythonDownload { client: &uv_client::BaseClient, installation_dir: &Path, cache_dir: &Path, + reinstall: bool, reporter: Option<&dyn Reporter>, ) -> Result { let url = self.download_url()?; let path = installation_dir.join(self.key().to_string()); - // If it already exists, return it - if path.is_dir() { + // If it is not a reinstall and the dir already exists, return it. + if !reinstall && path.is_dir() { return Ok(DownloadResult::AlreadyAvailable(path)); } @@ -560,7 +561,13 @@ impl ManagedPythonDownload { } } - // Persist it to the target + // Remove the target if it already exists. + if path.is_dir() { + debug!("Removing existing directory: {}", path.user_display()); + fs_err::tokio::remove_dir_all(&path).await?; + } + + // Persist it to the target. debug!("Moving {} to {}", extracted.display(), path.user_display()); rename_with_retry(extracted, &path) .await @@ -612,18 +619,18 @@ impl ManagedPythonDownload { } } -impl From for Error { - fn from(error: reqwest::Error) -> Self { - Self::NetworkError(WrappedReqwestError::from(error)) +impl Error { + pub(crate) fn from_reqwest(url: Url, err: reqwest::Error) -> Self { + Self::NetworkError(url, WrappedReqwestError::from(err)) } -} -impl From for Error { - fn from(error: reqwest_middleware::Error) -> Self { - match error { - reqwest_middleware::Error::Middleware(error) => Self::NetworkMiddlewareError(error), + pub(crate) fn from_reqwest_middleware(url: Url, err: reqwest_middleware::Error) -> Self { + match err { + reqwest_middleware::Error::Middleware(error) => { + Self::NetworkMiddlewareError(url, error) + } reqwest_middleware::Error::Reqwest(error) => { - Self::NetworkError(WrappedReqwestError::from(error)) + Self::NetworkError(url, WrappedReqwestError::from(error)) } } } @@ -694,10 +701,17 @@ async fn read_url( Ok((Either::Left(reader), Some(size))) } else { - let response = client.for_host(url).get(url.clone()).send().await?; + let response = client + .for_host(url) + .get(url.clone()) + .send() + .await + .map_err(|err| Error::from_reqwest_middleware(url.clone(), err))?; // Ensure the request was successful. - response.error_for_status_ref()?; + response + .error_for_status_ref() + .map_err(|err| Error::from_reqwest(url.clone(), err))?; let size = response.content_length(); let stream = response diff --git a/crates/uv-python/src/installation.rs b/crates/uv-python/src/installation.rs index b0a326bad96c..9faef3438abb 100644 --- a/crates/uv-python/src/installation.rs +++ b/crates/uv-python/src/installation.rs @@ -132,7 +132,7 @@ impl PythonInstallation { info!("Fetching requested Python..."); let result = download - .fetch(&client, installations_dir, &cache_dir, reporter) + .fetch(&client, installations_dir, &cache_dir, false, reporter) .await?; let path = match result { @@ -305,6 +305,17 @@ impl PythonInstallationKey { pub fn libc(&self) -> &Libc { &self.libc } + + /// Return a canonical name for a versioned executable. + pub fn versioned_executable_name(&self) -> String { + format!( + "python{maj}.{min}{var}{exe}", + maj = self.major, + min = self.minor, + var = self.variant.suffix(), + exe = std::env::consts::EXE_SUFFIX + ) + } } impl fmt::Display for PythonInstallationKey { @@ -405,5 +416,6 @@ impl Ord for PythonInstallationKey { .then_with(|| self.os.to_string().cmp(&other.os.to_string())) .then_with(|| self.arch.to_string().cmp(&other.arch.to_string())) .then_with(|| self.libc.to_string().cmp(&other.libc.to_string())) + .then_with(|| self.variant.cmp(&other.variant)) } } diff --git a/crates/uv-python/src/lib.rs b/crates/uv-python/src/lib.rs index 3cad5989c13b..c12ce93ad487 100644 --- a/crates/uv-python/src/lib.rs +++ b/crates/uv-python/src/lib.rs @@ -21,6 +21,7 @@ pub use crate::version_files::{ }; pub use crate::virtualenv::{Error as VirtualEnvError, PyVenvConfiguration, VirtualEnvironment}; +mod cpuinfo; mod discovery; pub mod downloads; mod environment; diff --git a/crates/uv-python/src/managed.rs b/crates/uv-python/src/managed.rs index 98f8dccdf277..c1d215b6411c 100644 --- a/crates/uv-python/src/managed.rs +++ b/crates/uv-python/src/managed.rs @@ -1,15 +1,20 @@ use core::fmt; -use fs_err as fs; -use itertools::Itertools; use std::cmp::Reverse; use std::ffi::OsStr; use std::io::{self, Write}; use std::path::{Path, PathBuf}; use std::str::FromStr; + +use fs_err as fs; +use itertools::Itertools; +use same_file::is_same_file; use thiserror::Error; use tracing::{debug, warn}; +use uv_fs::{symlink_or_copy_file, LockedFile, Simplified}; use uv_state::{StateBucket, StateStore}; +use uv_static::EnvVars; +use uv_trampoline_builder::{windows_python_launcher, Launcher}; use crate::downloads::Error as DownloadError; use crate::implementation::{ @@ -21,9 +26,6 @@ use crate::platform::Error as PlatformError; use crate::platform::{Arch, Libc, Os}; use crate::python_version::PythonVersion; use crate::{PythonRequest, PythonVariant}; -use uv_fs::{LockedFile, Simplified}; -use uv_static::EnvVars; - #[derive(Error, Debug)] pub enum Error { #[error(transparent)] @@ -53,14 +55,33 @@ pub enum Error { #[source] err: io::Error, }, + #[error("Failed to create Python executable link at {} from {}", to.user_display(), from.user_display())] + LinkExecutable { + from: PathBuf, + to: PathBuf, + #[source] + err: io::Error, + }, + #[error("Failed to create directory for Python executable link at {}", to.user_display())] + ExecutableDirectory { + to: PathBuf, + #[source] + err: io::Error, + }, #[error("Failed to read Python installation directory: {0}", dir.user_display())] ReadError { dir: PathBuf, #[source] err: io::Error, }, + #[error("Failed to find a directory to install executables into")] + NoExecutableDirectory, + #[error(transparent)] + LauncherError(#[from] uv_trampoline_builder::Error), #[error("Failed to read managed Python directory name: {0}")] NameError(String), + #[error("Failed to construct absolute path to managed Python directory: {}", _0.user_display())] + AbsolutePath(PathBuf, #[source] std::io::Error), #[error(transparent)] NameParseError(#[from] installation::PythonInstallationKeyError), #[error(transparent)] @@ -267,18 +288,78 @@ impl ManagedPythonInstallation { .ok_or(Error::NameError("not a valid string".to_string()))?, )?; + let path = std::path::absolute(&path).map_err(|err| Error::AbsolutePath(path, err))?; + Ok(Self { path, key }) } - /// The path to this toolchain's Python executable. + /// The path to this managed installation's Python executable. + /// + /// If the installation has multiple execututables i.e., `python`, `python3`, etc., this will + /// return the _canonical_ executable name which the other names link to. On Unix, this is + /// `python{major}.{minor}{variant}` and on Windows, this is `python{exe}`. pub fn executable(&self) -> PathBuf { - if cfg!(windows) { - self.python_dir().join("python.exe") + let implementation = match self.implementation() { + ImplementationName::CPython => "python", + ImplementationName::PyPy => "pypy", + ImplementationName::GraalPy => { + unreachable!("Managed installations of GraalPy are not supported") + } + }; + + let version = match self.implementation() { + ImplementationName::CPython => { + if cfg!(unix) { + format!("{}.{}", self.key.major, self.key.minor) + } else { + String::new() + } + } + // PyPy uses a full version number, even on Windows. + ImplementationName::PyPy => format!("{}.{}", self.key.major, self.key.minor), + ImplementationName::GraalPy => { + unreachable!("Managed installations of GraalPy are not supported") + } + }; + + // On Windows, the executable is just `python.exe` even for alternative variants + let variant = if cfg!(unix) { + self.key.variant.suffix() + } else { + "" + }; + + let name = format!( + "{implementation}{version}{variant}{exe}", + exe = std::env::consts::EXE_SUFFIX + ); + + let executable = if cfg!(windows) { + self.python_dir().join(name) } else if cfg!(unix) { - self.python_dir().join("bin").join("python3") + self.python_dir().join("bin").join(name) } else { unimplemented!("Only Windows and Unix systems are supported.") + }; + + // Workaround for python-build-standalone v20241016 which is missing the standard + // `python.exe` executable in free-threaded distributions on Windows. + // + // See https://github.com/astral-sh/uv/issues/8298 + if cfg!(windows) + && matches!(self.key.variant, PythonVariant::Freethreaded) + && !executable.exists() + { + // This is the alternative executable name for the freethreaded variant + return self.python_dir().join(format!( + "python{}.{}t{}", + self.key.major, + self.key.minor, + std::env::consts::EXE_SUFFIX + )); } + + executable } fn python_dir(&self) -> PathBuf { @@ -336,39 +417,38 @@ impl ManagedPythonInstallation { pub fn ensure_canonical_executables(&self) -> Result<(), Error> { let python = self.executable(); - // Workaround for python-build-standalone v20241016 which is missing the standard - // `python.exe` executable in free-threaded distributions on Windows. - // - // See https://github.com/astral-sh/uv/issues/8298 - if !python.try_exists()? { - match self.key.variant { - PythonVariant::Default => return Err(Error::MissingExecutable(python.clone())), - PythonVariant::Freethreaded => { - // This is the alternative executable name for the freethreaded variant - let python_in_dist = self.python_dir().join(format!( - "python{}.{}t{}", - self.key.major, - self.key.minor, - std::env::consts::EXE_SUFFIX - )); + let canonical_names = &["python"]; + + for name in canonical_names { + let executable = + python.with_file_name(format!("{name}{exe}", exe = std::env::consts::EXE_SUFFIX)); + + // Do not attempt to perform same-file copies — this is fine on Unix but fails on + // Windows with a permission error instead of 'already exists' + if executable == python { + continue; + } + + match uv_fs::symlink_or_copy_file(&python, &executable) { + Ok(()) => { debug!( - "Creating link {} -> {}", + "Created link {} -> {}", + executable.user_display(), python.user_display(), - python_in_dist.user_display() ); - uv_fs::symlink_copy_fallback_file(&python_in_dist, &python).map_err(|err| { - if err.kind() == io::ErrorKind::NotFound { - Error::MissingExecutable(python_in_dist.clone()) - } else { - Error::CanonicalizeExecutable { - from: python_in_dist, - to: python, - err, - } - } - })?; } - } + Err(err) if err.kind() == io::ErrorKind::NotFound => { + return Err(Error::MissingExecutable(python.clone())) + } + Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {} + Err(err) => { + return Err(Error::CanonicalizeExecutable { + from: executable, + to: python, + err, + }) + } + }; } Ok(()) @@ -381,10 +461,7 @@ impl ManagedPythonInstallation { let stdlib = if matches!(self.key.os, Os(target_lexicon::OperatingSystem::Windows)) { self.python_dir().join("Lib") } else { - let lib_suffix = match self.key.variant { - PythonVariant::Default => "", - PythonVariant::Freethreaded => "t", - }; + let lib_suffix = self.key.variant.suffix(); let python = if matches!( self.key.implementation, LenientImplementationName::Known(ImplementationName::PyPy) @@ -401,6 +478,70 @@ impl ManagedPythonInstallation { Ok(()) } + + /// Create a link to the managed Python executable. + /// + /// If the file already exists at the target path, an error will be returned. + pub fn create_bin_link(&self, target: &Path) -> Result<(), Error> { + let python = self.executable(); + + let bin = target.parent().ok_or(Error::NoExecutableDirectory)?; + fs_err::create_dir_all(bin).map_err(|err| Error::ExecutableDirectory { + to: bin.to_path_buf(), + err, + })?; + + if cfg!(unix) { + // Note this will never copy on Unix — we use it here to allow compilation on Windows + match symlink_or_copy_file(&python, target) { + Ok(()) => Ok(()), + Err(err) if err.kind() == io::ErrorKind::NotFound => { + Err(Error::MissingExecutable(python.clone())) + } + Err(err) => Err(Error::LinkExecutable { + from: python, + to: target.to_path_buf(), + err, + }), + } + } else if cfg!(windows) { + // TODO(zanieb): Install GUI launchers as well + let launcher = windows_python_launcher(&python, false)?; + + // OK to use `std::fs` here, `fs_err` does not support `File::create_new` and we attach + // error context anyway + #[allow(clippy::disallowed_types)] + { + std::fs::File::create_new(target) + .and_then(|mut file| file.write_all(launcher.as_ref())) + .map_err(|err| Error::LinkExecutable { + from: python, + to: target.to_path_buf(), + err, + }) + } + } else { + unimplemented!("Only Windows and Unix systems are supported.") + } + } + + /// Returns `true` if the path is a link to this installation's binary, e.g., as created by + /// [`ManagedPythonInstallation::create_bin_link`]. + pub fn is_bin_link(&self, path: &Path) -> bool { + if cfg!(unix) { + is_same_file(path, self.executable()).unwrap_or_default() + } else if cfg!(windows) { + let Some(launcher) = Launcher::try_from_path(path).unwrap_or_default() else { + return false; + }; + if !matches!(launcher.kind, uv_trampoline_builder::LauncherKind::Python) { + return false; + } + launcher.python_path == self.executable() + } else { + unreachable!("Only Windows and Unix are supported") + } + } } /// Generate a platform portion of a key from the environment. @@ -423,3 +564,9 @@ impl fmt::Display for ManagedPythonInstallation { ) } } + +/// Find the directory to install Python executables into. +pub fn python_executable_dir() -> Result { + uv_dirs::user_executable_directory(Some(EnvVars::UV_PYTHON_BIN_DIR)) + .ok_or(Error::NoExecutableDirectory) +} diff --git a/crates/uv-python/src/platform.rs b/crates/uv-python/src/platform.rs index fd6eb08ce75f..6b54477c0467 100644 --- a/crates/uv-python/src/platform.rs +++ b/crates/uv-python/src/platform.rs @@ -1,3 +1,4 @@ +use crate::cpuinfo::detect_hardware_floating_point_support; use crate::libc::{detect_linux_libc, LibcDetectionError, LibcVersion}; use std::fmt::Display; use std::ops::Deref; @@ -30,7 +31,17 @@ impl Libc { pub(crate) fn from_env() -> Result { match std::env::consts::OS { "linux" => Ok(Self::Some(match detect_linux_libc()? { - LibcVersion::Manylinux { .. } => target_lexicon::Environment::Gnu, + LibcVersion::Manylinux { .. } => match std::env::consts::ARCH { + // Checks if the CPU supports hardware floating-point operations. + // Depending on the result, it selects either the `gnueabihf` (hard-float) or `gnueabi` (soft-float) environment. + // download-metadata.json only includes armv7. + "arm" | "armv7" => match detect_hardware_floating_point_support() { + Ok(true) => target_lexicon::Environment::Gnueabihf, + Ok(false) => target_lexicon::Environment::Gnueabi, + Err(_) => target_lexicon::Environment::Gnu, + }, + _ => target_lexicon::Environment::Gnu, + }, LibcVersion::Musllinux { .. } => target_lexicon::Environment::Musl, })), "windows" | "macos" => Ok(Self::None), @@ -165,6 +176,9 @@ impl From<&uv_platform_tags::Arch> for Arch { target_lexicon::X86_32Architecture::I686, )), uv_platform_tags::Arch::X86_64 => Self(target_lexicon::Architecture::X86_64), + uv_platform_tags::Arch::Riscv64 => Self(target_lexicon::Architecture::Riscv64( + target_lexicon::Riscv64Architecture::Riscv64, + )), } } } diff --git a/crates/uv-python/src/py_launcher.rs b/crates/uv-python/src/py_launcher.rs index a4aa0ed27054..af7482d19def 100644 --- a/crates/uv-python/src/py_launcher.rs +++ b/crates/uv-python/src/py_launcher.rs @@ -3,7 +3,7 @@ use std::cmp::Ordering; use std::path::PathBuf; use std::str::FromStr; use tracing::debug; -use windows_registry::{Key, Value, CURRENT_USER, LOCAL_MACHINE}; +use windows_registry::{Key, CURRENT_USER, LOCAL_MACHINE}; /// A Python interpreter found in the Windows registry through PEP 514 or from a known Microsoft /// Store path. @@ -16,15 +16,6 @@ pub(crate) struct WindowsPython { pub(crate) version: Option, } -/// Adding `windows_registry::Value::into_string()`. -fn value_to_string(value: Value) -> Option { - match value { - Value::String(string) => Some(string), - Value::Bytes(bytes) => String::from_utf8(bytes.clone()).ok(), - Value::U32(_) | Value::U64(_) | Value::MultiString(_) | Value::Unknown(_) => None, - } -} - /// Find all Pythons registered in the Windows registry following PEP 514. pub(crate) fn registry_pythons() -> Result, windows_result::Error> { let mut registry_pythons = Vec::new(); @@ -72,11 +63,10 @@ pub(crate) fn registry_pythons() -> Result, windows_result::E fn read_registry_entry(company: &str, tag: &str, tag_key: &Key) -> Option { // `ExecutablePath` is mandatory for executable Pythons. - let Some(executable_path) = tag_key + let Ok(executable_path) = tag_key .open("InstallPath") .and_then(|install_path| install_path.get_value("ExecutablePath")) - .ok() - .and_then(value_to_string) + .and_then(String::try_from) else { debug!( r"Python interpreter in the registry is not executable: `Software\Python\{}\{}", @@ -88,11 +78,8 @@ fn read_registry_entry(company: &str, tag: &str, tag_key: &Key) -> Option Some(s), - _ => None, - }) .and_then(|s| match PythonVersion::from_str(&s) { Ok(version) => Some(version), Err(err) => { diff --git a/crates/uv-requirements-txt/src/lib.rs b/crates/uv-requirements-txt/src/lib.rs index 3ebe38b40951..1983f2dadb62 100644 --- a/crates/uv-requirements-txt/src/lib.rs +++ b/crates/uv-requirements-txt/src/lib.rs @@ -800,14 +800,19 @@ async fn read_url_to_string( let url = Url::from_str(path_utf8) .map_err(|err| RequirementsTxtParserError::InvalidUrl(path_utf8.to_string(), err))?; - Ok(client + let response = client .for_host(&url) - .get(url) + .get(url.clone()) .send() - .await? - .error_for_status()? + .await + .map_err(|err| RequirementsTxtParserError::from_reqwest_middleware(url.clone(), err))?; + let text = response + .error_for_status() + .map_err(|err| RequirementsTxtParserError::from_reqwest(url.clone(), err))? .text() - .await?) + .await + .map_err(|err| RequirementsTxtParserError::from_reqwest(url.clone(), err))?; + Ok(text) } /// Error parsing requirements.txt, wrapper with filename @@ -891,7 +896,7 @@ pub enum RequirementsTxtParserError { url: PathBuf, }, #[cfg(feature = "http")] - Reqwest(reqwest_middleware::Error), + Reqwest(Url, reqwest_middleware::Error), #[cfg(feature = "http")] InvalidUrl(String, url::ParseError), } @@ -957,8 +962,8 @@ impl Display for RequirementsTxtParserError { ) } #[cfg(feature = "http")] - Self::Reqwest(err) => { - write!(f, "Error while accessing remote requirements file {err}") + Self::Reqwest(url, _err) => { + write!(f, "Error while accessing remote requirements file: `{url}`") } #[cfg(feature = "http")] Self::InvalidUrl(url, err) => { @@ -989,7 +994,7 @@ impl std::error::Error for RequirementsTxtParserError { Self::Parser { .. } => None, Self::NonUnicodeUrl { .. } => None, #[cfg(feature = "http")] - Self::Reqwest(err) => err.source(), + Self::Reqwest(_, err) => err.source(), #[cfg(feature = "http")] Self::InvalidUrl(_, err) => err.source(), } @@ -1117,12 +1122,8 @@ impl Display for RequirementsTxtFileError { ) } #[cfg(feature = "http")] - RequirementsTxtParserError::Reqwest(err) => { - write!( - f, - "Error while accessing remote requirements file {}: {err}", - self.file.user_display(), - ) + RequirementsTxtParserError::Reqwest(url, _err) => { + write!(f, "Error while accessing remote requirements file: `{url}`") } #[cfg(feature = "http")] RequirementsTxtParserError::InvalidUrl(url, err) => { @@ -1145,16 +1146,13 @@ impl From for RequirementsTxtParserError { } #[cfg(feature = "http")] -impl From for RequirementsTxtParserError { - fn from(err: reqwest::Error) -> Self { - Self::Reqwest(reqwest_middleware::Error::Reqwest(err)) +impl RequirementsTxtParserError { + fn from_reqwest(url: Url, err: reqwest::Error) -> Self { + Self::Reqwest(url, reqwest_middleware::Error::Reqwest(err)) } -} -#[cfg(feature = "http")] -impl From for RequirementsTxtParserError { - fn from(err: reqwest_middleware::Error) -> Self { - Self::Reqwest(err) + fn from_reqwest_middleware(url: Url, err: reqwest_middleware::Error) -> Self { + Self::Reqwest(url, err) } } diff --git a/crates/uv-requirements/src/lookahead.rs b/crates/uv-requirements/src/lookahead.rs index 80c4da3aad9e..f0b60fa0434f 100644 --- a/crates/uv-requirements/src/lookahead.rs +++ b/crates/uv-requirements/src/lookahead.rs @@ -195,7 +195,7 @@ impl<'a, Context: BuildContext> LookaheadResolver<'a, Context> { .into_iter() .chain( metadata - .dev_dependencies + .dependency_groups .into_iter() .filter_map(|(group, dependencies)| { if self.dev.contains(&group) { diff --git a/crates/uv-resolver/Cargo.toml b/crates/uv-resolver/Cargo.toml index 2c04a0327dfa..6a881fa9bc16 100644 --- a/crates/uv-resolver/Cargo.toml +++ b/crates/uv-resolver/Cargo.toml @@ -30,7 +30,6 @@ uv-once-map = { workspace = true } uv-pep440 = { workspace = true } uv-pep508 = { workspace = true } uv-platform-tags = { workspace = true } -uv-pubgrub = { workspace = true } uv-pypi-types = { workspace = true } uv-python = { workspace = true } uv-requirements-txt = { workspace = true } diff --git a/crates/uv-resolver/src/candidate_selector.rs b/crates/uv-resolver/src/candidate_selector.rs index 4402277526b4..891d035fedc0 100644 --- a/crates/uv-resolver/src/candidate_selector.rs +++ b/crates/uv-resolver/src/candidate_selector.rs @@ -178,11 +178,15 @@ impl CandidateSelector { let preferences_match = preferences.get(package_name).filter(|(marker, _version)| { // `.unwrap_or(true)` because the universal marker is considered matching. - marker.map(|marker| marker == fork_markers).unwrap_or(true) + marker + .map(|marker| !marker.is_disjoint(fork_markers)) + .unwrap_or(true) }); let preferences_mismatch = preferences.get(package_name).filter(|(marker, _version)| { - marker.map(|marker| marker != fork_markers).unwrap_or(false) + marker + .map(|marker| marker.is_disjoint(fork_markers)) + .unwrap_or(false) }); self.get_preferred_from_iter( preferences_match.chain(preferences_mismatch), diff --git a/crates/uv-resolver/src/error.rs b/crates/uv-resolver/src/error.rs index 73f8bc634abb..1547f9d92b2b 100644 --- a/crates/uv-resolver/src/error.rs +++ b/crates/uv-resolver/src/error.rs @@ -9,9 +9,7 @@ use rustc_hash::FxHashMap; use crate::candidate_selector::CandidateSelector; use crate::dependency_provider::UvDependencyProvider; use crate::fork_urls::ForkUrls; -use crate::pubgrub::{ - PubGrubPackage, PubGrubPackageInner, PubGrubReportFormatter, PubGrubSpecifierError, -}; +use crate::pubgrub::{PubGrubPackage, PubGrubPackageInner, PubGrubReportFormatter}; use crate::python_requirement::PythonRequirement; use crate::resolution::ConflictingDistributionError; use crate::resolver::{IncompletePackage, ResolverMarkers, UnavailablePackage, UnavailableReason}; @@ -39,9 +37,6 @@ pub enum ResolveError { #[error("Attempted to wait on an unregistered task: `{_0}`")] UnregisteredTask(String), - #[error(transparent)] - PubGrubSpecifier(#[from] PubGrubSpecifierError), - #[error("Overrides contain conflicting URLs for package `{0}`:\n- {1}\n- {2}")] ConflictingOverrideUrls(PackageName, String, String), diff --git a/crates/uv-resolver/src/lib.rs b/crates/uv-resolver/src/lib.rs index 4d60b9f95cac..38b4ec140983 100644 --- a/crates/uv-resolver/src/lib.rs +++ b/crates/uv-resolver/src/lib.rs @@ -4,15 +4,15 @@ pub use exclude_newer::ExcludeNewer; pub use exclusions::Exclusions; pub use flat_index::{FlatDistributions, FlatIndex}; pub use lock::{ - Lock, LockError, RequirementsTxtExport, ResolverManifest, SatisfiesResult, TreeDisplay, + Lock, LockError, LockVersion, RequirementsTxtExport, ResolverManifest, SatisfiesResult, + TreeDisplay, VERSION, }; pub use manifest::Manifest; pub use options::{Flexibility, Options, OptionsBuilder}; pub use preferences::{Preference, PreferenceError, Preferences}; pub use prerelease::PrereleaseMode; -pub use pubgrub::{PubGrubSpecifier, PubGrubSpecifierError}; pub use python_requirement::PythonRequirement; -pub use requires_python::{RequiresPython, RequiresPythonError, RequiresPythonRange}; +pub use requires_python::{RequiresPython, RequiresPythonRange}; pub use resolution::{ AnnotationStyle, ConflictingDistributionError, DisplayResolutionGraph, ResolutionGraph, }; diff --git a/crates/uv-resolver/src/lock/mod.rs b/crates/uv-resolver/src/lock/mod.rs index 3ab114d7aa38..937b17a78944 100644 --- a/crates/uv-resolver/src/lock/mod.rs +++ b/crates/uv-resolver/src/lock/mod.rs @@ -14,8 +14,16 @@ use std::sync::{Arc, LazyLock}; use toml_edit::{value, Array, ArrayOfTables, InlineTable, Item, Table, Value}; use url::Url; +pub use crate::lock::requirements_txt::RequirementsTxtExport; +pub use crate::lock::tree::TreeDisplay; +use crate::requires_python::SimplifiedMarkerTree; +use crate::resolution::{AnnotatedDist, ResolutionGraphNode}; +use crate::{ + ExcludeNewer, InMemoryIndex, MetadataResponse, PrereleaseMode, RequiresPython, ResolutionGraph, + ResolutionMode, +}; use uv_cache_key::RepositoryUrl; -use uv_configuration::{BuildOptions, DevSpecification, ExtrasSpecification, InstallOptions}; +use uv_configuration::{BuildOptions, DevGroupsManifest, ExtrasSpecification, InstallOptions}; use uv_distribution::DistributionDatabase; use uv_distribution_filename::{DistExtension, ExtensionError, SourceDistExtension, WheelFilename}; use uv_distribution_types::{ @@ -35,22 +43,14 @@ use uv_pypi_types::{ ResolverMarkerEnvironment, }; use uv_types::{BuildContext, HashStrategy}; +use uv_workspace::dependency_groups::DependencyGroupError; use uv_workspace::{InstallTarget, Workspace}; -pub use crate::lock::requirements_txt::RequirementsTxtExport; -pub use crate::lock::tree::TreeDisplay; -use crate::requires_python::SimplifiedMarkerTree; -use crate::resolution::{AnnotatedDist, ResolutionGraphNode}; -use crate::{ - ExcludeNewer, InMemoryIndex, MetadataResponse, PrereleaseMode, RequiresPython, ResolutionGraph, - ResolutionMode, -}; - mod requirements_txt; mod tree; /// The current version of the lockfile format. -const VERSION: u32 = 1; +pub const VERSION: u32 = 1; static LINUX_MARKERS: LazyLock = LazyLock::new(|| { MarkerTree::from_str( @@ -211,7 +211,7 @@ impl Lock { continue; }; let marker = edge.weight().clone(); - package.add_dev_dependency( + package.add_group_dependency( &requires_python, group.clone(), dependency_dist, @@ -344,7 +344,7 @@ impl Lock { } // Perform the same validation for dev dependencies. - for (group, dependencies) in &mut package.dev_dependencies { + for (group, dependencies) in &mut package.dependency_groups { dependencies.sort(); for windows in dependencies.windows(2) { let (dep1, dep2) = (&windows[0], &windows[1]); @@ -390,7 +390,7 @@ impl Lock { .dependencies .iter_mut() .chain(dist.optional_dependencies.values_mut().flatten()) - .chain(dist.dev_dependencies.values_mut().flatten()) + .chain(dist.dependency_groups.values_mut().flatten()) { dep.extra.retain(|extra| { extras_by_id @@ -428,7 +428,7 @@ impl Lock { } // Perform the same validation for dev dependencies. - for dependencies in dist.dev_dependencies.values() { + for dependencies in dist.dependency_groups.values() { for dep in dependencies { if !by_id.contains_key(&dep.package_id) { return Err(LockErrorKind::UnrecognizedDependency { @@ -494,6 +494,11 @@ impl Lock { self } + /// Returns the lockfile version. + pub fn version(&self) -> u32 { + self.version + } + /// Returns the number of packages in the lockfile. pub fn len(&self) -> usize { self.packages.len() @@ -575,7 +580,7 @@ impl Lock { marker_env: &ResolverMarkerEnvironment, tags: &Tags, extras: &ExtrasSpecification, - dev: DevSpecification<'_>, + dev: &DevGroupsManifest, build_options: &BuildOptions, install_options: &InstallOptions, ) -> Result { @@ -615,7 +620,7 @@ impl Lock { // Add any dev dependencies. for group in dev.iter() { - for dep in root.dev_dependencies.get(group).into_iter().flatten() { + for dep in root.dependency_groups.get(group).into_iter().flatten() { if dep.complexified_marker.evaluate(marker_env, &[]) { let dep_dist = self.find_by_id(&dep.package_id); if seen.insert((&dep.package_id, None)) { @@ -633,8 +638,11 @@ impl Lock { // Add any dependency groups that are exclusive to the workspace root (e.g., dev // dependencies in (legacy) non-project workspace roots). + let groups = project + .groups() + .map_err(|err| LockErrorKind::DependencyGroup { err })?; for group in dev.iter() { - for dependency in project.group(group) { + for dependency in groups.get(group).into_iter().flatten() { if dependency.marker.evaluate(marker_env, &[]) { let root_name = &dependency.name; let root = self @@ -1229,11 +1237,12 @@ impl Lock { } } - // Validate the `dev-dependencies` metadata. + // Validate the `dependency-groups` metadata. { let expected: BTreeMap> = metadata - .dev_dependencies + .dependency_groups .into_iter() + .filter(|(_, requirements)| !requirements.is_empty()) .map(|(group, requirements)| { Ok::<_, LockError>(( group, @@ -1246,8 +1255,9 @@ impl Lock { .collect::>()?; let actual: BTreeMap> = package .metadata - .requires_dev + .dependency_groups .iter() + .filter(|(_, requirements)| !requirements.is_empty()) .map(|(group, requirements)| { Ok::<_, LockError>(( group.clone(), @@ -1261,7 +1271,7 @@ impl Lock { .collect::>()?; if expected != actual { - return Ok(SatisfiesResult::MismatchedDevDependencies( + return Ok(SatisfiesResult::MismatchedDependencyGroups( &package.id.name, &package.id.version, expected, @@ -1289,7 +1299,7 @@ impl Lock { } } - for dependencies in package.dev_dependencies.values() { + for dependencies in package.dependency_groups.values() { for dep in dependencies { if seen.insert(&dep.package_id) { let dep_dist = self.find_by_id(&dep.package_id); @@ -1353,8 +1363,8 @@ pub enum SatisfiesResult<'lock> { BTreeSet, BTreeSet, ), - /// A package in the lockfile contains different `dev-dependencies` metadata than expected. - MismatchedDevDependencies( + /// A package in the lockfile contains different `dependency-group` metadata than expected. + MismatchedDependencyGroups( &'lock PackageName, &'lock Version, BTreeMap>, @@ -1509,6 +1519,21 @@ impl TryFrom for Lock { } } +/// Like [`Lock`], but limited to the version field. Used for error reporting: by limiting parsing +/// to the version field, we can verify compatibility for lockfiles that may otherwise be +/// unparsable. +#[derive(Clone, Debug, serde::Deserialize)] +pub struct LockVersion { + version: u32, +} + +impl LockVersion { + /// Returns the lockfile version. + pub fn version(&self) -> u32 { + self.version + } +} + #[derive(Clone, Debug, PartialEq, Eq)] pub struct Package { pub(crate) id: PackageId, @@ -1524,8 +1549,8 @@ pub struct Package { dependencies: Vec, /// The resolved optional dependencies of the package. optional_dependencies: BTreeMap>, - /// The resolved development dependencies of the package. - dev_dependencies: BTreeMap>, + /// The resolved PEP 735 dependency groups of the package. + dependency_groups: BTreeMap>, /// The exact requirements from the package metadata. metadata: PackageMetadata, } @@ -1553,14 +1578,14 @@ impl Package { .collect::>() .map_err(LockErrorKind::RequirementRelativePath)? }; - let requires_dev = if id.source.is_immutable() { + let dependency_groups = if id.source.is_immutable() { BTreeMap::default() } else { annotated_dist .metadata .as_ref() .expect("metadata is present") - .dev_dependencies + .dependency_groups .iter() .map(|(group, requirements)| { let requirements = requirements @@ -1580,10 +1605,10 @@ impl Package { fork_markers, dependencies: vec![], optional_dependencies: BTreeMap::default(), - dev_dependencies: BTreeMap::default(), + dependency_groups: BTreeMap::default(), metadata: PackageMetadata { requires_dist, - requires_dev, + dependency_groups, }, }) } @@ -1659,18 +1684,18 @@ impl Package { Ok(()) } - /// Add the [`AnnotatedDist`] as a development dependency of the [`Package`]. - fn add_dev_dependency( + /// Add the [`AnnotatedDist`] to a dependency group of the [`Package`]. + fn add_group_dependency( &mut self, requires_python: &RequiresPython, - dev: GroupName, + group: GroupName, annotated_dist: &AnnotatedDist, marker: MarkerTree, root: &Path, ) -> Result<(), LockError> { let dep = Dependency::from_annotated_dist(requires_python, annotated_dist, marker, root)?; - let dev_deps = self.dev_dependencies.entry(dev).or_default(); - for existing_dep in &mut *dev_deps { + let deps = self.dependency_groups.entry(group).or_default(); + for existing_dep in &mut *deps { if existing_dep.package_id == dep.package_id // See note in add_dependency for why we use // simplified markers here. @@ -1681,7 +1706,7 @@ impl Package { } } - dev_deps.push(dep); + deps.push(dep); Ok(()) } @@ -2031,19 +2056,19 @@ impl Package { } } - if !self.dev_dependencies.is_empty() { - let mut dev_dependencies = Table::new(); - for (extra, deps) in &self.dev_dependencies { + if !self.dependency_groups.is_empty() { + let mut dependency_groups = Table::new(); + for (extra, deps) in &self.dependency_groups { let deps = each_element_on_its_line_array(deps.iter().map(|dep| { dep.to_toml(requires_python, dist_count_by_name) .into_inline_table() })); if !deps.is_empty() { - dev_dependencies.insert(extra.as_ref(), value(deps)); + dependency_groups.insert(extra.as_ref(), value(deps)); } } - if !dev_dependencies.is_empty() { - table.insert("dev-dependencies", Item::Table(dev_dependencies)); + if !dependency_groups.is_empty() { + table.insert("dev-dependencies", Item::Table(dependency_groups)); } } @@ -2086,9 +2111,9 @@ impl Package { metadata_table.insert("requires-dist", value(requires_dist)); } - if !self.metadata.requires_dev.is_empty() { - let mut requires_dev = Table::new(); - for (extra, deps) in &self.metadata.requires_dev { + if !self.metadata.dependency_groups.is_empty() { + let mut dependency_groups = Table::new(); + for (extra, deps) in &self.metadata.dependency_groups { let deps = deps .iter() .map(|requirement| { @@ -2103,12 +2128,10 @@ impl Package { [requirement] => Array::from_iter([requirement]), deps => each_element_on_its_line_array(deps.iter()), }; - if !deps.is_empty() { - requires_dev.insert(extra.as_ref(), value(deps)); - } + dependency_groups.insert(extra.as_ref(), value(deps)); } - if !requires_dev.is_empty() { - metadata_table.insert("requires-dev", Item::Table(requires_dev)); + if !dependency_groups.is_empty() { + metadata_table.insert("requires-dev", Item::Table(dependency_groups)); } } @@ -2218,8 +2241,8 @@ struct PackageWire { dependencies: Vec, #[serde(default)] optional_dependencies: BTreeMap>, - #[serde(default)] - dev_dependencies: BTreeMap>, + #[serde(default, rename = "dev-dependencies", alias = "dependency-groups")] + dependency_groups: BTreeMap>, } #[derive(Clone, Default, Debug, Eq, PartialEq, serde::Deserialize)] @@ -2227,8 +2250,8 @@ struct PackageWire { struct PackageMetadata { #[serde(default)] requires_dist: BTreeSet, - #[serde(default)] - requires_dev: BTreeMap>, + #[serde(default, rename = "requires-dev", alias = "dependency-groups")] + dependency_groups: BTreeMap>, } impl PackageWire { @@ -2258,8 +2281,8 @@ impl PackageWire { .into_iter() .map(|(extra, deps)| Ok((extra, unwire_deps(deps)?))) .collect::>()?, - dev_dependencies: self - .dev_dependencies + dependency_groups: self + .dependency_groups .into_iter() .map(|(group, deps)| Ok((group, unwire_deps(deps)?))) .collect::>()?, @@ -3129,9 +3152,15 @@ fn locked_git_url(git_dist: &GitSourceDist) -> Url { url.set_query(None); // Put the subdirectory in the query. - if let Some(subdirectory) = git_dist.subdirectory.as_deref().and_then(Path::to_str) { + if let Some(subdirectory) = git_dist + .subdirectory + .as_deref() + .map(PortablePath::from) + .as_ref() + .map(PortablePath::to_string) + { url.query_pairs_mut() - .append_pair("subdirectory", subdirectory); + .append_pair("subdirectory", &subdirectory); } // Put the requested reference in the query. @@ -3864,7 +3893,7 @@ enum LockErrorKind { /// The ID of the package for which a duplicate dependency was /// found. id: PackageId, - /// The name of the optional dependency group. + /// The name of the extra. extra: ExtraName, /// The ID of the conflicting dependency. dependency: Dependency, @@ -4115,6 +4144,11 @@ enum LockErrorKind { #[source] err: uv_distribution::Error, }, + #[error("Failed to resolve `dependency-groups`")] + DependencyGroup { + #[source] + err: DependencyGroupError, + }, } /// An error that occurs when a source string could not be parsed. diff --git a/crates/uv-resolver/src/lock/requirements_txt.rs b/crates/uv-resolver/src/lock/requirements_txt.rs index 1393a459f5bd..b58c99649cda 100644 --- a/crates/uv-resolver/src/lock/requirements_txt.rs +++ b/crates/uv-resolver/src/lock/requirements_txt.rs @@ -10,7 +10,7 @@ use petgraph::{Directed, Graph}; use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet}; use url::Url; -use uv_configuration::{DevSpecification, EditableMode, ExtrasSpecification, InstallOptions}; +use uv_configuration::{DevGroupsManifest, EditableMode, ExtrasSpecification, InstallOptions}; use uv_distribution_filename::{DistExtension, SourceDistExtension}; use uv_fs::Simplified; use uv_git::GitReference; @@ -22,18 +22,12 @@ use crate::graph_ops::marker_reachability; use crate::lock::{Package, PackageId, Source}; use crate::{Lock, LockError}; -type LockGraph<'lock> = Graph<&'lock Package, Edge, Directed>; - -#[derive(Debug, Clone, PartialEq, Eq)] -struct Node<'lock> { - package: &'lock Package, - marker: MarkerTree, -} +type LockGraph<'lock> = Graph, Edge, Directed>; /// An export of a [`Lock`] that renders in `requirements.txt` format. #[derive(Debug)] pub struct RequirementsTxtExport<'lock> { - nodes: Vec>, + nodes: Vec>, hashes: bool, editable: EditableMode, } @@ -43,7 +37,7 @@ impl<'lock> RequirementsTxtExport<'lock> { lock: &'lock Lock, root_name: &PackageName, extras: &ExtrasSpecification, - dev: DevSpecification<'_>, + dev: &DevGroupsManifest, editable: EditableMode, hashes: bool, install_options: &'lock InstallOptions, @@ -55,45 +49,62 @@ impl<'lock> RequirementsTxtExport<'lock> { let mut queue: VecDeque<(&Package, Option<&ExtraName>)> = VecDeque::new(); let mut seen = FxHashSet::default(); + let root = petgraph.add_node(Node::Root); + // Add the workspace package to the queue. - let root = lock + let dist = lock .find_by_name(root_name) .expect("found too many packages matching root") .expect("could not find root"); if dev.prod() { - // Add the base package. - queue.push_back((root, None)); + // Add the workspace package to the graph. + if let Entry::Vacant(entry) = inverse.entry(&dist.id) { + entry.insert(petgraph.add_node(Node::Package(dist))); + } - // Add any extras. + // Add an edge from the root. + let index = inverse[&dist.id]; + petgraph.add_edge(root, index, MarkerTree::TRUE); + + // Push its dependencies on the queue. + queue.push_back((dist, None)); match extras { ExtrasSpecification::None => {} ExtrasSpecification::All => { - for extra in root.optional_dependencies.keys() { - queue.push_back((root, Some(extra))); + for extra in dist.optional_dependencies.keys() { + queue.push_back((dist, Some(extra))); } } ExtrasSpecification::Some(extras) => { for extra in extras { - queue.push_back((root, Some(extra))); + queue.push_back((dist, Some(extra))); } } } - - // Add the root package to the graph. - inverse.insert(&root.id, petgraph.add_node(root)); } - // Add any dev dependencies. + // Add any development dependencies. for group in dev.iter() { - for dep in root.dev_dependencies.get(group).into_iter().flatten() { + for dep in dist.dependency_groups.get(group).into_iter().flatten() { let dep_dist = lock.find_by_id(&dep.package_id); // Add the dependency to the graph. if let Entry::Vacant(entry) = inverse.entry(&dep.package_id) { - entry.insert(petgraph.add_node(dep_dist)); + entry.insert(petgraph.add_node(Node::Package(dep_dist))); } + // Add an edge from the root. Development dependencies may be installed without + // installing the workspace package itself (which can never have markers on it + // anyway), so they're directly connected to the root. + let dep_index = inverse[&dep.package_id]; + petgraph.add_edge( + root, + dep_index, + dep.simplified_marker.as_simplified_marker_tree().clone(), + ); + + // Push its dependencies on the queue. if seen.insert((&dep.package_id, None)) { queue.push_back((dep_dist, None)); } @@ -126,7 +137,7 @@ impl<'lock> RequirementsTxtExport<'lock> { // Add the dependency to the graph. if let Entry::Vacant(entry) = inverse.entry(&dep.package_id) { - entry.insert(petgraph.add_node(dep_dist)); + entry.insert(petgraph.add_node(Node::Package(dep_dist))); } // Add the edge. @@ -152,12 +163,16 @@ impl<'lock> RequirementsTxtExport<'lock> { let mut reachability = marker_reachability(&petgraph, &[]); // Collect all packages. - let mut nodes: Vec = petgraph + let mut nodes = petgraph .node_references() + .filter_map(|(index, node)| match node { + Node::Root => None, + Node::Package(package) => Some((index, package)), + }) .filter(|(_index, package)| { install_options.include_package(&package.id.name, Some(root_name), lock.members()) }) - .map(|(index, package)| Node { + .map(|(index, package)| Requirement { package, marker: reachability.remove(&index).unwrap_or_default(), }) @@ -165,7 +180,7 @@ impl<'lock> RequirementsTxtExport<'lock> { // Sort the nodes, such that unnamed URLs (editables) appear at the top. nodes.sort_unstable_by(|a, b| { - NodeComparator::from(a.package).cmp(&NodeComparator::from(b.package)) + RequirementComparator::from(a.package).cmp(&RequirementComparator::from(b.package)) }); Ok(Self { @@ -179,7 +194,7 @@ impl<'lock> RequirementsTxtExport<'lock> { impl std::fmt::Display for RequirementsTxtExport<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { // Write out each package. - for Node { package, marker } in &self.nodes { + for Requirement { package, marker } in &self.nodes { match &package.id.source { Source::Registry(_) => { write!(f, "{}=={}", package.id.name, package.id.version)?; @@ -261,17 +276,31 @@ impl std::fmt::Display for RequirementsTxtExport<'_> { } } +/// A node in the [`LockGraph`]. +#[derive(Debug, Clone, PartialEq, Eq)] +enum Node<'lock> { + Root, + Package(&'lock Package), +} + /// The edges of the [`LockGraph`]. type Edge = MarkerTree; +/// A flat requirement, with its associated marker. +#[derive(Debug, Clone, PartialEq, Eq)] +struct Requirement<'lock> { + package: &'lock Package, + marker: MarkerTree, +} + #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -enum NodeComparator<'lock> { +enum RequirementComparator<'lock> { Editable(&'lock Path), Path(&'lock Path), Package(&'lock PackageId), } -impl<'lock> From<&'lock Package> for NodeComparator<'lock> { +impl<'lock> From<&'lock Package> for RequirementComparator<'lock> { fn from(value: &'lock Package) -> Self { match &value.id.source { Source::Path(path) | Source::Directory(path) => Self::Path(path), diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_missing.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_missing.snap index 9eae35fa0a5e..a17e0270042e 100644 --- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_missing.snap +++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_missing.snap @@ -1,5 +1,5 @@ --- -source: crates/uv-resolver/src/lock/mod.rs +source: crates/uv-resolver/src/lock/tests.rs expression: result --- Ok( @@ -78,10 +78,10 @@ Ok( fork_markers: [], dependencies: [], optional_dependencies: {}, - dev_dependencies: {}, + dependency_groups: {}, metadata: PackageMetadata { requires_dist: {}, - requires_dev: {}, + dependency_groups: {}, }, }, ], diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_present.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_present.snap index b08ac136e1e8..65aaa5849210 100644 --- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_present.snap +++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_present.snap @@ -1,5 +1,5 @@ --- -source: crates/uv-resolver/src/lock/mod.rs +source: crates/uv-resolver/src/lock/tests.rs expression: result --- Ok( @@ -85,10 +85,10 @@ Ok( fork_markers: [], dependencies: [], optional_dependencies: {}, - dev_dependencies: {}, + dependency_groups: {}, metadata: PackageMetadata { requires_dist: {}, - requires_dev: {}, + dependency_groups: {}, }, }, ], diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_required_present.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_required_present.snap index 18a9968fa1ae..09dc7df1f3a8 100644 --- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_required_present.snap +++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_required_present.snap @@ -1,5 +1,5 @@ --- -source: crates/uv-resolver/src/lock/mod.rs +source: crates/uv-resolver/src/lock/tests.rs expression: result --- Ok( @@ -81,10 +81,10 @@ Ok( fork_markers: [], dependencies: [], optional_dependencies: {}, - dev_dependencies: {}, + dependency_groups: {}, metadata: PackageMetadata { requires_dist: {}, - requires_dev: {}, + dependency_groups: {}, }, }, ], diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_source_unambiguous.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_source_unambiguous.snap index df2435de84c5..e93ae3260342 100644 --- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_source_unambiguous.snap +++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_source_unambiguous.snap @@ -1,5 +1,5 @@ --- -source: crates/uv-resolver/src/lock/mod.rs +source: crates/uv-resolver/src/lock/tests.rs expression: result --- Ok( @@ -71,10 +71,10 @@ Ok( fork_markers: [], dependencies: [], optional_dependencies: {}, - dev_dependencies: {}, + dependency_groups: {}, metadata: PackageMetadata { requires_dist: {}, - requires_dev: {}, + dependency_groups: {}, }, }, Package { @@ -136,10 +136,10 @@ Ok( }, ], optional_dependencies: {}, - dev_dependencies: {}, + dependency_groups: {}, metadata: PackageMetadata { requires_dist: {}, - requires_dev: {}, + dependency_groups: {}, }, }, ], diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_source_version_unambiguous.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_source_version_unambiguous.snap index df2435de84c5..e93ae3260342 100644 --- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_source_version_unambiguous.snap +++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_source_version_unambiguous.snap @@ -1,5 +1,5 @@ --- -source: crates/uv-resolver/src/lock/mod.rs +source: crates/uv-resolver/src/lock/tests.rs expression: result --- Ok( @@ -71,10 +71,10 @@ Ok( fork_markers: [], dependencies: [], optional_dependencies: {}, - dev_dependencies: {}, + dependency_groups: {}, metadata: PackageMetadata { requires_dist: {}, - requires_dev: {}, + dependency_groups: {}, }, }, Package { @@ -136,10 +136,10 @@ Ok( }, ], optional_dependencies: {}, - dev_dependencies: {}, + dependency_groups: {}, metadata: PackageMetadata { requires_dist: {}, - requires_dev: {}, + dependency_groups: {}, }, }, ], diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_version_unambiguous.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_version_unambiguous.snap index df2435de84c5..e93ae3260342 100644 --- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_version_unambiguous.snap +++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_version_unambiguous.snap @@ -1,5 +1,5 @@ --- -source: crates/uv-resolver/src/lock/mod.rs +source: crates/uv-resolver/src/lock/tests.rs expression: result --- Ok( @@ -71,10 +71,10 @@ Ok( fork_markers: [], dependencies: [], optional_dependencies: {}, - dev_dependencies: {}, + dependency_groups: {}, metadata: PackageMetadata { requires_dist: {}, - requires_dev: {}, + dependency_groups: {}, }, }, Package { @@ -136,10 +136,10 @@ Ok( }, ], optional_dependencies: {}, - dev_dependencies: {}, + dependency_groups: {}, metadata: PackageMetadata { requires_dist: {}, - requires_dev: {}, + dependency_groups: {}, }, }, ], diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_direct_has_subdir.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_direct_has_subdir.snap index 3624b1c56f33..8c7ca8d2c0be 100644 --- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_direct_has_subdir.snap +++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_direct_has_subdir.snap @@ -1,5 +1,5 @@ --- -source: crates/uv-resolver/src/lock/mod.rs +source: crates/uv-resolver/src/lock/tests.rs expression: result --- Ok( @@ -55,10 +55,10 @@ Ok( fork_markers: [], dependencies: [], optional_dependencies: {}, - dev_dependencies: {}, + dependency_groups: {}, metadata: PackageMetadata { requires_dist: {}, - requires_dev: {}, + dependency_groups: {}, }, }, ], diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_direct_no_subdir.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_direct_no_subdir.snap index 1ee52e43d3be..04e3df0ba703 100644 --- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_direct_no_subdir.snap +++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_direct_no_subdir.snap @@ -1,5 +1,5 @@ --- -source: crates/uv-resolver/src/lock/mod.rs +source: crates/uv-resolver/src/lock/tests.rs expression: result --- Ok( @@ -53,10 +53,10 @@ Ok( fork_markers: [], dependencies: [], optional_dependencies: {}, - dev_dependencies: {}, + dependency_groups: {}, metadata: PackageMetadata { requires_dist: {}, - requires_dev: {}, + dependency_groups: {}, }, }, ], diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_directory.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_directory.snap index 38c5bf51b463..3b0a488c4507 100644 --- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_directory.snap +++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_directory.snap @@ -1,5 +1,5 @@ --- -source: crates/uv-resolver/src/lock/mod.rs +source: crates/uv-resolver/src/lock/tests.rs expression: result --- Ok( @@ -48,10 +48,10 @@ Ok( fork_markers: [], dependencies: [], optional_dependencies: {}, - dev_dependencies: {}, + dependency_groups: {}, metadata: PackageMetadata { requires_dist: {}, - requires_dev: {}, + dependency_groups: {}, }, }, ], diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_editable.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_editable.snap index c445e4a2932a..2c8a20fd3256 100644 --- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_editable.snap +++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_editable.snap @@ -1,5 +1,5 @@ --- -source: crates/uv-resolver/src/lock/mod.rs +source: crates/uv-resolver/src/lock/tests.rs expression: result --- Ok( @@ -48,10 +48,10 @@ Ok( fork_markers: [], dependencies: [], optional_dependencies: {}, - dev_dependencies: {}, + dependency_groups: {}, metadata: PackageMetadata { requires_dist: {}, - requires_dev: {}, + dependency_groups: {}, }, }, ], diff --git a/crates/uv-resolver/src/lock/tree.rs b/crates/uv-resolver/src/lock/tree.rs index 3ce6a2dbf311..89191b3d6471 100644 --- a/crates/uv-resolver/src/lock/tree.rs +++ b/crates/uv-resolver/src/lock/tree.rs @@ -1,36 +1,28 @@ use std::borrow::Cow; -use std::collections::BTreeSet; +use std::collections::VecDeque; +use std::path::Path; use itertools::Itertools; +use petgraph::graph::{EdgeIndex, NodeIndex}; +use petgraph::prelude::EdgeRef; +use petgraph::Direction; use rustc_hash::{FxHashMap, FxHashSet}; -use uv_configuration::DevMode; +use uv_configuration::DevGroupsManifest; use uv_normalize::{ExtraName, GroupName, PackageName}; use uv_pypi_types::ResolverMarkerEnvironment; -use crate::lock::{Dependency, PackageId}; +use crate::lock::{Dependency, PackageId, Source}; use crate::Lock; #[derive(Debug)] pub struct TreeDisplay<'env> { - /// The root nodes in the [`Lock`]. - roots: Vec<&'env PackageId>, - /// The edges in the [`Lock`]. - /// - /// While the dependencies exist on the [`Lock`] directly, if `--invert` is enabled, the - /// direction must be inverted when constructing the tree. - dependencies: FxHashMap<&'env PackageId, Vec>>, - optional_dependencies: - FxHashMap<&'env PackageId, FxHashMap>>>, - dev_dependencies: FxHashMap<&'env PackageId, FxHashMap>>>, + /// The constructed dependency graph. + graph: petgraph::graph::Graph<&'env PackageId, Edge<'env>, petgraph::Directed>, + /// The packages considered as roots of the dependency tree. + roots: Vec, /// Maximum display depth of the dependency tree. depth: usize, - /// Whether to include development dependencies in the display. - dev: DevMode, - /// Prune the given packages from the display of the dependency tree. - prune: Vec, - /// Display only the specified packages. - packages: Vec, /// Whether to de-duplicate the displayed dependencies. no_dedupe: bool, } @@ -41,152 +33,233 @@ impl<'env> TreeDisplay<'env> { lock: &'env Lock, markers: Option<&'env ResolverMarkerEnvironment>, depth: usize, - prune: Vec, - packages: Vec, - dev: DevMode, + prune: &[PackageName], + packages: &[PackageName], + dev: &DevGroupsManifest, no_dedupe: bool, invert: bool, ) -> Self { - let mut non_roots = FxHashSet::default(); + // Identify the workspace members. + // + // The members are encoded directly in the lockfile, unless the workspace contains a + // single member at the root, in which case, we identify it by its source. + let members: FxHashSet<&PackageId> = if lock.members().is_empty() { + lock.packages + .iter() + .filter_map(|package| { + let (Source::Editable(path) | Source::Virtual(path)) = &package.id.source + else { + return None; + }; + if path == Path::new("") { + Some(&package.id) + } else { + None + } + }) + .collect() + } else { + lock.packages + .iter() + .filter_map(|package| { + if lock.members().contains(&package.id.name) { + Some(&package.id) + } else { + None + } + }) + .collect() + }; - // Index all the dependencies. We could read these from the `Lock` directly, but we have to - // support `--invert`, so we might as well build them up in either case. - let mut dependencies: FxHashMap<_, Vec<_>> = FxHashMap::default(); - let mut optional_dependencies: FxHashMap<_, FxHashMap<_, Vec<_>>> = FxHashMap::default(); - let mut dev_dependencies: FxHashMap<_, FxHashMap<_, Vec<_>>> = FxHashMap::default(); + // Create a graph. + let mut graph = petgraph::graph::Graph::<&PackageId, Edge, petgraph::Directed>::new(); + // Create the complete graph. + let mut inverse = FxHashMap::default(); for package in &lock.packages { - for dependency in &package.dependencies { - // Skip dependencies that don't apply to the current environment. - if let Some(environment_markers) = markers { - if !dependency - .complexified_marker - .evaluate(environment_markers, &[]) - { - non_roots.insert(dependency.package_id.clone()); + if prune.contains(&package.id.name) { + continue; + } + + // Insert the package into the graph. + let package_node = if let Some(index) = inverse.get(&package.id) { + *index + } else { + let index = graph.add_node(&package.id); + inverse.insert(&package.id, index); + index + }; + + if dev.prod() { + for dependency in &package.dependencies { + if markers.is_some_and(|markers| { + !dependency.complexified_marker.evaluate(markers, &[]) + }) { continue; } - } - let parent = if invert { - &dependency.package_id - } else { - &package.id - }; - let child = if invert { - Cow::Owned(Dependency { - package_id: package.id.clone(), - extra: dependency.extra.clone(), - simplified_marker: dependency.simplified_marker.clone(), - complexified_marker: dependency.complexified_marker.clone(), - }) - } else { - Cow::Borrowed(dependency) - }; - - non_roots.insert(child.package_id.clone()); + // Insert the dependency into the graph. + let dependency_node = if let Some(index) = inverse.get(&dependency.package_id) { + *index + } else { + let index = graph.add_node(&dependency.package_id); + inverse.insert(&dependency.package_id, index); + index + }; - dependencies.entry(parent).or_default().push(child); + // Add an edge between the package and the dependency. + graph.add_edge( + package_node, + dependency_node, + Edge::Prod(Cow::Borrowed(dependency)), + ); + } } - for (extra, dependencies) in &package.optional_dependencies { - for dependency in dependencies { - // Skip dependencies that don't apply to the current environment. - if let Some(environment_markers) = markers { - if !dependency - .complexified_marker - .evaluate(environment_markers, &[]) - { - non_roots.insert(dependency.package_id.clone()); + if dev.prod() { + for (extra, dependencies) in &package.optional_dependencies { + for dependency in dependencies { + if markers.is_some_and(|markers| { + !dependency.complexified_marker.evaluate(markers, &[]) + }) { continue; } - } - - let parent = if invert { - &dependency.package_id - } else { - &package.id - }; - let child = if invert { - Cow::Owned(Dependency { - package_id: package.id.clone(), - extra: dependency.extra.clone(), - simplified_marker: dependency.simplified_marker.clone(), - complexified_marker: dependency.complexified_marker.clone(), - }) - } else { - Cow::Borrowed(dependency) - }; - non_roots.insert(child.package_id.clone()); - - optional_dependencies - .entry(parent) - .or_default() - .entry(extra.clone()) - .or_default() - .push(child); + // Insert the dependency into the graph. + let dependency_node = + if let Some(index) = inverse.get(&dependency.package_id) { + *index + } else { + let index = graph.add_node(&dependency.package_id); + inverse.insert(&dependency.package_id, index); + index + }; + + // Add an edge between the package and the dependency. + graph.add_edge( + package_node, + dependency_node, + Edge::Optional(extra, Cow::Borrowed(dependency)), + ); + } } } - for (group, dependencies) in &package.dev_dependencies { - for dependency in dependencies { - // Skip dependencies that don't apply to the current environment. - if let Some(environment_markers) = markers { - if !dependency - .complexified_marker - .evaluate(environment_markers, &[]) - { - non_roots.insert(dependency.package_id.clone()); + for (group, dependencies) in &package.dependency_groups { + if dev.iter().contains(group) { + for dependency in dependencies { + if markers.is_some_and(|markers| { + !dependency.complexified_marker.evaluate(markers, &[]) + }) { continue; } + + // Insert the dependency into the graph. + let dependency_node = + if let Some(index) = inverse.get(&dependency.package_id) { + *index + } else { + let index = graph.add_node(&dependency.package_id); + inverse.insert(&dependency.package_id, index); + index + }; + + // Add an edge between the package and the dependency. + graph.add_edge( + package_node, + dependency_node, + Edge::Dev(group, Cow::Borrowed(dependency)), + ); } + } + } + } - let parent = if invert { - &dependency.package_id - } else { - &package.id - }; - let child = if invert { - Cow::Owned(Dependency { - package_id: package.id.clone(), - extra: dependency.extra.clone(), - simplified_marker: dependency.simplified_marker.clone(), - complexified_marker: dependency.complexified_marker.clone(), - }) - } else { - Cow::Borrowed(dependency) - }; + // Filter the graph to remove any unreachable nodes. + { + let mut reachable = graph + .node_indices() + .filter(|index| members.contains(graph[*index])) + .collect::>(); + let mut stack = reachable.iter().copied().collect::>(); + while let Some(node) = stack.pop_front() { + for edge in graph.edges_directed(node, Direction::Outgoing) { + if reachable.insert(edge.target()) { + stack.push_back(edge.target()); + } + } + } - non_roots.insert(child.package_id.clone()); + // Remove the unreachable nodes from the graph. + graph.retain_nodes(|_, index| reachable.contains(&index)); + } + + // Reverse the graph. + if invert { + graph.reverse(); + } - dev_dependencies - .entry(parent) - .or_default() - .entry(group.clone()) - .or_default() - .push(child); + // Filter the graph to those nodes reachable from the target packages. + if !packages.is_empty() { + let mut reachable = graph + .node_indices() + .filter(|index| packages.contains(&graph[*index].name)) + .collect::>(); + let mut stack = reachable.iter().copied().collect::>(); + while let Some(node) = stack.pop_front() { + for edge in graph.edges_directed(node, Direction::Outgoing) { + if reachable.insert(edge.target()) { + stack.push_back(edge.target()); + } } } + + // Remove the unreachable nodes from the graph. + graph.retain_nodes(|_, index| reachable.contains(&index)); } - // Compute the root nodes. - let roots = lock - .packages - .iter() - .map(|dist| &dist.id) - .filter(|id| !non_roots.contains(*id)) - .collect::>(); + // Compute the list of roots. + let roots = { + let mut edges = vec![]; + + // Remove any cycles. + let feedback_set: Vec = petgraph::algo::greedy_feedback_arc_set(&graph) + .map(|e| e.id()) + .collect(); + for edge_id in feedback_set { + if let Some((source, target)) = graph.edge_endpoints(edge_id) { + if let Some(weight) = graph.remove_edge(edge_id) { + edges.push((source, target, weight)); + } + } + } + + // Find the root nodes. + let mut roots = graph + .node_indices() + .filter(|index| { + graph + .edges_directed(*index, Direction::Incoming) + .next() + .is_none() + }) + .collect::>(); + + // Sort the roots. + roots.sort_by_key(|index| &graph[*index]); + + // Re-add the removed edges. + for (source, target, weight) in edges { + graph.add_edge(source, target, weight); + } + + roots + }; Self { + graph, roots, - dependencies, - optional_dependencies, - dev_dependencies, depth, - dev, - prune, - packages, no_dedupe, } } @@ -194,7 +267,7 @@ impl<'env> TreeDisplay<'env> { /// Perform a depth-first traversal of the given package and its dependencies. fn visit( &'env self, - node: Node<'env>, + cursor: Cursor, visited: &mut FxHashMap<&'env PackageId, Vec<&'env PackageId>>, path: &mut Vec<&'env PackageId>, ) -> Vec { @@ -203,75 +276,81 @@ impl<'env> TreeDisplay<'env> { return Vec::new(); } - let line = { - let mut line = format!("{}", node.package_id().name); + let package_id = self.graph[cursor.node()]; + let edge = cursor.edge().map(|edge_id| &self.graph[edge_id]); - if let Some(extras) = node.extras().filter(|extras| !extras.is_empty()) { - line.push_str(&format!("[{}]", extras.iter().join(","))); + let line = { + let mut line = format!("{}", package_id.name); + + if let Some(edge) = edge { + let extras = &edge.dependency().extra; + if !extras.is_empty() { + line.push('['); + line.push_str(extras.iter().join(", ").as_str()); + line.push(']'); + } } - line.push_str(&format!(" v{}", node.package_id().version)); + line.push(' '); + line.push('v'); + line.push_str(&format!("{}", package_id.version)); - match node { - Node::Root(_) => line, - Node::Dependency(_) => line, - Node::OptionalDependency(extra, _) => format!("{line} (extra: {extra})"), - Node::DevDependency(group, _) => format!("{line} (group: {group})"), + if let Some(edge) = edge { + match edge { + Edge::Prod(_) => {} + Edge::Optional(extra, _) => { + line.push_str(&format!(" (extra: {extra})")); + } + Edge::Dev(group, _) => { + line.push_str(&format!(" (group: {group})")); + } + } } + + line }; // Skip the traversal if: // 1. The package is in the current traversal path (i.e., a dependency cycle). // 2. The package has been visited and de-duplication is enabled (default). - if let Some(requirements) = visited.get(node.package_id()) { - if !self.no_dedupe || path.contains(&node.package_id()) { + if let Some(requirements) = visited.get(package_id) { + if !self.no_dedupe || path.contains(&package_id) { return if requirements.is_empty() { vec![line] } else { - vec![format!("{} (*)", line)] + vec![format!("{line} (*)")] }; } } - let dependencies: Vec> = self - .dependencies - .get(node.package_id()) - .filter(|_| self.dev != DevMode::Only) - .into_iter() - .flatten() - .map(|dep| Node::Dependency(dep.as_ref())) - .chain( - self.optional_dependencies - .get(node.package_id()) - .filter(|_| self.dev != DevMode::Only) - .into_iter() - .flatten() - .flat_map(|(extra, deps)| { - deps.iter() - .map(move |dep| Node::OptionalDependency(extra, dep)) - }), - ) - .chain( - self.dev_dependencies - .get(node.package_id()) - .filter(|_| self.dev != DevMode::Exclude) - .into_iter() - .flatten() - .flat_map(|(group, deps)| { - deps.iter().map(move |dep| Node::DevDependency(group, dep)) - }), - ) - .filter(|dep| !self.prune.contains(&dep.package_id().name)) + let mut dependencies = self + .graph + .edges_directed(cursor.node(), Direction::Outgoing) + .map(|edge| { + let node = edge.target(); + Cursor::new(node, edge.id()) + }) .collect::>(); + dependencies.sort_by_key(|node| { + let package_id = self.graph[node.node()]; + let edge = node + .edge() + .map(|edge_id| &self.graph[edge_id]) + .map(Edge::kind); + (edge, package_id) + }); let mut lines = vec![line]; // Keep track of the dependency path to avoid cycles. visited.insert( - node.package_id(), - dependencies.iter().map(Node::package_id).collect(), + package_id, + dependencies + .iter() + .map(|node| self.graph[node.node()]) + .collect(), ); - path.push(node.package_id()); + path.push(package_id); for (index, dep) in dependencies.iter().enumerate() { // For sub-visited packages, add the prefix to make the tree display user-friendly. @@ -316,57 +395,78 @@ impl<'env> TreeDisplay<'env> { /// Depth-first traverse the nodes to render the tree. fn render(&self) -> Vec { - let mut visited = FxHashMap::default(); let mut path = Vec::new(); - let mut lines = Vec::new(); + let mut lines = Vec::with_capacity(self.graph.node_count()); + let mut visited = + FxHashMap::with_capacity_and_hasher(self.graph.node_count(), rustc_hash::FxBuildHasher); - if self.packages.is_empty() { - for id in &self.roots { - path.clear(); - lines.extend(self.visit(Node::Root(id), &mut visited, &mut path)); - } - } else { - let by_package: FxHashMap<_, _> = self.roots.iter().map(|id| (&id.name, id)).collect(); - for package in &self.packages { - if let Some(id) = by_package.get(package) { - path.clear(); - lines.extend(self.visit(Node::Root(id), &mut visited, &mut path)); - } - } + for node in &self.roots { + path.clear(); + lines.extend(self.visit(Cursor::root(*node), &mut visited, &mut path)); } lines } } -#[derive(Debug, Copy, Clone)] -enum Node<'env> { - Root(&'env PackageId), - Dependency(&'env Dependency), - OptionalDependency(&'env ExtraName, &'env Dependency), - DevDependency(&'env GroupName, &'env Dependency), +#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd)] +enum Edge<'env> { + Prod(Cow<'env, Dependency>), + Optional(&'env ExtraName, Cow<'env, Dependency>), + Dev(&'env GroupName, Cow<'env, Dependency>), } -impl<'env> Node<'env> { - fn package_id(&self) -> &'env PackageId { +impl<'env> Edge<'env> { + fn dependency(&self) -> &Dependency { match self { - Self::Root(id) => id, - Self::Dependency(dep) => &dep.package_id, - Self::OptionalDependency(_, dep) => &dep.package_id, - Self::DevDependency(_, dep) => &dep.package_id, + Self::Prod(dependency) => dependency, + Self::Optional(_, dependency) => dependency, + Self::Dev(_, dependency) => dependency, } } - fn extras(&self) -> Option<&BTreeSet> { + fn kind(&self) -> EdgeKind<'env> { match self { - Self::Root(_) => None, - Self::Dependency(dep) => Some(&dep.extra), - Self::OptionalDependency(_, dep) => Some(&dep.extra), - Self::DevDependency(_, dep) => Some(&dep.extra), + Self::Prod(_) => EdgeKind::Prod, + Self::Optional(extra, _) => EdgeKind::Optional(extra), + Self::Dev(group, _) => EdgeKind::Dev(group), } } } +#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd)] +enum EdgeKind<'env> { + Prod, + Optional(&'env ExtraName), + Dev(&'env GroupName), +} + +/// A node in the dependency graph along with the edge that led to it, or `None` for root nodes. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd)] +struct Cursor(NodeIndex, Option); + +impl Cursor { + /// Create a [`Cursor`] representing a node in the dependency tree. + fn new(node: NodeIndex, edge: EdgeIndex) -> Self { + Self(node, Some(edge)) + } + + /// Create a [`Cursor`] representing a root node in the dependency tree. + fn root(node: NodeIndex) -> Self { + Self(node, None) + } + + /// Return the [`NodeIndex`] of the node. + fn node(&self) -> NodeIndex { + self.0 + } + + /// Return the [`EdgeIndex`] of the edge that led to the node, if any. + fn edge(&self) -> Option { + self.1 + } +} + impl std::fmt::Display for TreeDisplay<'_> { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { use owo_colors::OwoColorize; diff --git a/crates/uv-resolver/src/pubgrub/dependencies.rs b/crates/uv-resolver/src/pubgrub/dependencies.rs index 5045e94a0ec2..ee9ac491ebc8 100644 --- a/crates/uv-resolver/src/pubgrub/dependencies.rs +++ b/crates/uv-resolver/src/pubgrub/dependencies.rs @@ -1,7 +1,6 @@ use std::iter; -use itertools::Itertools; -use pubgrub::Range; +use pubgrub::Ranges; use tracing::warn; use uv_normalize::{ExtraName, PackageName}; @@ -12,12 +11,11 @@ use uv_pypi_types::{ }; use crate::pubgrub::{PubGrubPackage, PubGrubPackageInner}; -use crate::{PubGrubSpecifier, ResolveError}; #[derive(Clone, Debug, Eq, PartialEq)] pub(crate) struct PubGrubDependency { pub(crate) package: PubGrubPackage, - pub(crate) version: Range, + pub(crate) version: Ranges, /// The original version specifiers from the requirement. pub(crate) specifier: Option, @@ -32,12 +30,12 @@ impl PubGrubDependency { pub(crate) fn from_requirement<'a>( requirement: &'a Requirement, source_name: Option<&'a PackageName>, - ) -> impl Iterator> + 'a { + ) -> impl Iterator + 'a { // Add the package, plus any extra variants. iter::once(None) .chain(requirement.extras.clone().into_iter().map(Some)) .map(|extra| PubGrubRequirement::from_requirement(requirement, extra)) - .filter_map_ok(move |requirement| { + .filter_map(move |requirement| { let PubGrubRequirement { package, version, @@ -87,7 +85,7 @@ impl PubGrubDependency { #[derive(Debug, Clone)] pub(crate) struct PubGrubRequirement { pub(crate) package: PubGrubPackage, - pub(crate) version: Range, + pub(crate) version: Ranges, pub(crate) specifier: Option, pub(crate) url: Option, } @@ -95,10 +93,7 @@ pub(crate) struct PubGrubRequirement { impl PubGrubRequirement { /// Convert a [`Requirement`] to a PubGrub-compatible package and range, while returning the URL /// on the [`Requirement`], if any. - pub(crate) fn from_requirement( - requirement: &Requirement, - extra: Option, - ) -> Result { + pub(crate) fn from_requirement(requirement: &Requirement, extra: Option) -> Self { let (verbatim_url, parsed_url) = match &requirement.source { RequirementSource::Registry { specifier, .. } => { return Self::from_registry_requirement(specifier, extra, requirement); @@ -159,29 +154,27 @@ impl PubGrubRequirement { } }; - Ok(Self { + Self { package: PubGrubPackage::from_package( requirement.name.clone(), extra, requirement.marker.clone(), ), - version: Range::full(), + version: Ranges::full(), specifier: None, url: Some(VerbatimParsedUrl { parsed_url, verbatim: verbatim_url.clone(), }), - }) + } } fn from_registry_requirement( specifier: &VersionSpecifiers, extra: Option, requirement: &Requirement, - ) -> Result { - let version = PubGrubSpecifier::from_pep440_specifiers(specifier)?.into(); - - let requirement = Self { + ) -> PubGrubRequirement { + Self { package: PubGrubPackage::from_package( requirement.name.clone(), extra, @@ -189,9 +182,7 @@ impl PubGrubRequirement { ), specifier: Some(specifier.clone()), url: None, - version, - }; - - Ok(requirement) + version: Ranges::from(specifier.clone()), + } } } diff --git a/crates/uv-resolver/src/pubgrub/mod.rs b/crates/uv-resolver/src/pubgrub/mod.rs index d6aa611f3d86..13824ad07a40 100644 --- a/crates/uv-resolver/src/pubgrub/mod.rs +++ b/crates/uv-resolver/src/pubgrub/mod.rs @@ -3,7 +3,6 @@ pub(crate) use crate::pubgrub::distribution::PubGrubDistribution; pub(crate) use crate::pubgrub::package::{PubGrubPackage, PubGrubPackageInner, PubGrubPython}; pub(crate) use crate::pubgrub::priority::{PubGrubPriorities, PubGrubPriority}; pub(crate) use crate::pubgrub::report::PubGrubReportFormatter; -pub use uv_pubgrub::{PubGrubSpecifier, PubGrubSpecifierError}; mod dependencies; mod distribution; diff --git a/crates/uv-resolver/src/python_requirement.rs b/crates/uv-resolver/src/python_requirement.rs index b124635da13d..e581bdc09123 100644 --- a/crates/uv-resolver/src/python_requirement.rs +++ b/crates/uv-resolver/src/python_requirement.rs @@ -71,6 +71,12 @@ impl PythonRequirement { }) } + /// Returns `true` if the minimum version of Python required by the target is greater than the + /// installed version. + pub fn raises(&self, target: &RequiresPythonRange) -> bool { + target.lower() > self.target.range().lower() + } + /// Return the exact version of Python. pub fn exact(&self) -> &Version { &self.exact diff --git a/crates/uv-resolver/src/redirect.rs b/crates/uv-resolver/src/redirect.rs index 60004c394c24..3010fcedf8f8 100644 --- a/crates/uv-resolver/src/redirect.rs +++ b/crates/uv-resolver/src/redirect.rs @@ -1,5 +1,4 @@ use url::Url; - use uv_git::{GitReference, GitResolver}; use uv_pep508::VerbatimUrl; use uv_pypi_types::{ParsedGitUrl, ParsedUrl, VerbatimParsedUrl}; @@ -17,9 +16,8 @@ pub(crate) fn url_to_precise(url: VerbatimParsedUrl, git: &GitResolver) -> Verba let Some(new_git_url) = git.precise(git_url.clone()) else { debug_assert!( matches!(git_url.reference(), GitReference::FullCommit(_)), - "Unseen Git URL: {}, {:?}", + "Unseen Git URL: {}, {git_url:?}", url.verbatim, - git_url ); return url; }; diff --git a/crates/uv-resolver/src/requires_python.rs b/crates/uv-resolver/src/requires_python.rs index 4d8592be004e..56e51cb532cb 100644 --- a/crates/uv-resolver/src/requires_python.rs +++ b/crates/uv-resolver/src/requires_python.rs @@ -1,38 +1,30 @@ -use itertools::Itertools; use pubgrub::Range; use std::cmp::Ordering; use std::collections::Bound; use std::ops::Deref; use uv_distribution_filename::WheelFilename; -use uv_pep440::{Version, VersionSpecifier, VersionSpecifiers}; +use uv_pep440::{release_specifiers_to_ranges, Version, VersionSpecifier, VersionSpecifiers}; use uv_pep508::{MarkerExpression, MarkerTree, MarkerValueVersion}; -use uv_pubgrub::PubGrubSpecifier; - -#[derive(thiserror::Error, Debug)] -pub enum RequiresPythonError { - #[error(transparent)] - PubGrub(#[from] crate::pubgrub::PubGrubSpecifierError), -} /// The `Requires-Python` requirement specifier. /// -/// We treat `Requires-Python` as a lower bound. For example, if the requirement expresses -/// `>=3.8, <4`, we treat it as `>=3.8`. `Requires-Python` itself was intended to enable -/// packages to drop support for older versions of Python without breaking installations on -/// those versions, and packages cannot know whether they are compatible with future, unreleased -/// versions of Python. -/// /// See: #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct RequiresPython { /// The supported Python versions as provides by the user, usually through the `requires-python` /// field in `pyproject.toml`. /// - /// For a workspace, it's the union of all `requires-python` values in the workspace. If no - /// bound was provided by the user, it's greater equal the current Python version. + /// For a workspace, it's the intersection of all `requires-python` values in the workspace. If + /// no bound was provided by the user, it's greater equal the current Python version. + /// + /// The specifiers remain static over the lifetime of the workspace, such that they + /// represent the initial Python version constraints. specifiers: VersionSpecifiers, - /// The lower and upper bounds of `specifiers`. + /// The lower and upper bounds of the given specifiers. + /// + /// The range may be narrowed over the course of dependency resolution as the resolver + /// investigates environments with stricter Python version constraints. range: RequiresPythonRange, } @@ -52,15 +44,15 @@ impl RequiresPython { } /// Returns a [`RequiresPython`] from a version specifier. - pub fn from_specifiers(specifiers: &VersionSpecifiers) -> Result { - let (lower_bound, upper_bound) = PubGrubSpecifier::from_release_specifiers(specifiers)? + pub fn from_specifiers(specifiers: &VersionSpecifiers) -> Self { + let (lower_bound, upper_bound) = release_specifiers_to_ranges(specifiers.clone()) .bounding_range() .map(|(lower_bound, upper_bound)| (lower_bound.cloned(), upper_bound.cloned())) .unwrap_or((Bound::Unbounded, Bound::Unbounded)); - Ok(Self { + Self { specifiers: specifiers.clone(), range: RequiresPythonRange(LowerBound(lower_bound), UpperBound(upper_bound)), - }) + } } /// Returns a [`RequiresPython`] to express the intersection of the given version specifiers. @@ -68,23 +60,19 @@ impl RequiresPython { /// For example, given `>=3.8` and `>=3.9`, this would return `>=3.9`. pub fn intersection<'a>( specifiers: impl Iterator, - ) -> Result, RequiresPythonError> { + ) -> Option { // Convert to PubGrub range and perform an intersection. let range = specifiers .into_iter() - .map(PubGrubSpecifier::from_release_specifiers) - .fold_ok(None, |range: Option>, requires_python| { + .map(|specifier| release_specifiers_to_ranges(specifier.clone())) + .fold(None, |range: Option>, requires_python| { if let Some(range) = range { - Some(range.intersection(&requires_python.into())) + Some(range.intersection(&requires_python)) } else { - Some(requires_python.into()) + Some(requires_python) } })?; - let Some(range) = range else { - return Ok(None); - }; - // Extract the bounds. let (lower_bound, upper_bound) = range .bounding_range() @@ -99,13 +87,13 @@ impl RequiresPython { // Convert back to PEP 440 specifiers. let specifiers = VersionSpecifiers::from_release_only_bounds(range.iter()); - Ok(Some(Self { + Some(Self { specifiers, range: RequiresPythonRange(lower_bound, upper_bound), - })) + }) } - /// Narrow the [`RequiresPython`] to the given version, if it's stricter than the current target. + /// Narrow the [`RequiresPython`] by computing the intersection with the given range. pub fn narrow(&self, range: &RequiresPythonRange) -> Option { let lower = if range.0 >= self.range.0 { Some(&range.0) @@ -117,6 +105,8 @@ impl RequiresPython { } else { None }; + // TODO(charlie): Consider re-computing the specifiers (or removing them entirely in favor + // of tracking the range). After narrowing, the specifiers and range may be out of sync. match (lower, upper) { (Some(lower), Some(upper)) => Some(Self { specifiers: self.specifiers.clone(), @@ -225,26 +215,40 @@ impl RequiresPython { } /// Returns `true` if the `Requires-Python` is compatible with the given version. + /// + /// N.B. This operation should primarily be used when evaluating compatibility of Python + /// versions against the user's own project. For example, if the user defines a + /// `requires-python` in a `pyproject.toml`, this operation could be used to determine whether + /// a given Python interpreter is compatible with the user's project. pub fn contains(&self, version: &Version) -> bool { let version = version.only_release(); self.specifiers.contains(&version) } - /// Returns `true` if the `Requires-Python` is compatible with the given version specifiers. + /// Returns `true` if the `Requires-Python` is contained by the given version specifiers. + /// + /// In this context, we treat `Requires-Python` as a lower bound. For example, if the + /// requirement expresses `>=3.8, <4`, we treat it as `>=3.8`. `Requires-Python` itself was + /// intended to enable packages to drop support for older versions of Python without breaking + /// installations on those versions, and packages cannot know whether they are compatible with + /// future, unreleased versions of Python. + /// + /// The specifiers are considered to "contain" the `Requires-Python` if the specifiers are + /// compatible with all versions in the `Requires-Python` range (i.e., have a _lower_ lower + /// bound). /// /// For example, if the `Requires-Python` is `>=3.8`, then `>=3.7` would be considered /// compatible, since all versions in the `Requires-Python` range are also covered by the /// provided range. However, `>=3.9` would not be considered compatible, as the /// `Requires-Python` includes Python 3.8, but `>=3.9` does not. + /// + /// N.B. This operation should primarily be used when evaluating the compatibility of a + /// project's `Requires-Python` specifier against a dependency's `Requires-Python` specifier. pub fn is_contained_by(&self, target: &VersionSpecifiers) -> bool { - let Ok(target) = PubGrubSpecifier::from_release_specifiers(target) else { - return false; - }; - let target = target - .iter() - .next() - .map(|(lower, _)| lower) - .unwrap_or(&Bound::Unbounded); + let target = release_specifiers_to_ranges(target.clone()) + .bounding_range() + .map(|bounding_range| bounding_range.0.cloned()) + .unwrap_or(Bound::Unbounded); // We want, e.g., `self.range.lower()` to be `>=3.8` and `target` to be `>=3.7`. // @@ -485,8 +489,7 @@ impl serde::Serialize for RequiresPython { impl<'de> serde::Deserialize<'de> for RequiresPython { fn deserialize>(deserializer: D) -> Result { let specifiers = VersionSpecifiers::deserialize(deserializer)?; - let (lower_bound, upper_bound) = PubGrubSpecifier::from_release_specifiers(&specifiers) - .map_err(serde::de::Error::custom)? + let (lower_bound, upper_bound) = release_specifiers_to_ranges(specifiers.clone()) .bounding_range() .map(|(lower_bound, upper_bound)| (lower_bound.cloned(), upper_bound.cloned())) .unwrap_or((Bound::Unbounded, Bound::Unbounded)); diff --git a/crates/uv-resolver/src/requires_python/tests.rs b/crates/uv-resolver/src/requires_python/tests.rs index 660abd71bacd..dc8e97564c61 100644 --- a/crates/uv-resolver/src/requires_python/tests.rs +++ b/crates/uv-resolver/src/requires_python/tests.rs @@ -11,7 +11,7 @@ use crate::RequiresPython; #[test] fn requires_python_included() { let version_specifiers = VersionSpecifiers::from_str("==3.10.*").unwrap(); - let requires_python = RequiresPython::from_specifiers(&version_specifiers).unwrap(); + let requires_python = RequiresPython::from_specifiers(&version_specifiers); let wheel_names = &[ "bcrypt-4.1.3-cp37-abi3-macosx_10_12_universal2.whl", "black-24.4.2-cp310-cp310-win_amd64.whl", @@ -30,7 +30,7 @@ fn requires_python_included() { } let version_specifiers = VersionSpecifiers::from_str(">=3.12.3").unwrap(); - let requires_python = RequiresPython::from_specifiers(&version_specifiers).unwrap(); + let requires_python = RequiresPython::from_specifiers(&version_specifiers); let wheel_names = &["dearpygui-1.11.1-cp312-cp312-win_amd64.whl"]; for wheel_name in wheel_names { assert!( @@ -40,7 +40,7 @@ fn requires_python_included() { } let version_specifiers = VersionSpecifiers::from_str("==3.12.6").unwrap(); - let requires_python = RequiresPython::from_specifiers(&version_specifiers).unwrap(); + let requires_python = RequiresPython::from_specifiers(&version_specifiers); let wheel_names = &["lxml-5.3.0-cp312-cp312-musllinux_1_2_aarch64.whl"]; for wheel_name in wheel_names { assert!( @@ -50,7 +50,7 @@ fn requires_python_included() { } let version_specifiers = VersionSpecifiers::from_str("==3.12").unwrap(); - let requires_python = RequiresPython::from_specifiers(&version_specifiers).unwrap(); + let requires_python = RequiresPython::from_specifiers(&version_specifiers); let wheel_names = &["lxml-5.3.0-cp312-cp312-musllinux_1_2_x86_64.whl"]; for wheel_name in wheel_names { assert!( @@ -63,7 +63,7 @@ fn requires_python_included() { #[test] fn requires_python_dropped() { let version_specifiers = VersionSpecifiers::from_str("==3.10.*").unwrap(); - let requires_python = RequiresPython::from_specifiers(&version_specifiers).unwrap(); + let requires_python = RequiresPython::from_specifiers(&version_specifiers); let wheel_names = &[ "PySocks-1.7.1-py27-none-any.whl", "black-24.4.2-cp39-cp39-win_amd64.whl", @@ -83,7 +83,7 @@ fn requires_python_dropped() { } let version_specifiers = VersionSpecifiers::from_str(">=3.12.3").unwrap(); - let requires_python = RequiresPython::from_specifiers(&version_specifiers).unwrap(); + let requires_python = RequiresPython::from_specifiers(&version_specifiers); let wheel_names = &["dearpygui-1.11.1-cp310-cp310-win_amd64.whl"]; for wheel_name in wheel_names { assert!( @@ -152,7 +152,7 @@ fn is_exact_without_patch() { ]; for (version, expected) in test_cases { let version_specifiers = VersionSpecifiers::from_str(version).unwrap(); - let requires_python = RequiresPython::from_specifiers(&version_specifiers).unwrap(); + let requires_python = RequiresPython::from_specifiers(&version_specifiers); assert_eq!(requires_python.is_exact_without_patch(), expected); } } diff --git a/crates/uv-resolver/src/resolution/graph.rs b/crates/uv-resolver/src/resolution/graph.rs index 9e379585000c..6730a34acb44 100644 --- a/crates/uv-resolver/src/resolution/graph.rs +++ b/crates/uv-resolver/src/resolution/graph.rs @@ -355,7 +355,7 @@ impl ResolutionGraph { // Validate the development dependency group. if let Some(dev) = dev { - if !metadata.dev_dependencies.contains_key(dev) { + if !metadata.dependency_groups.contains_key(dev) { diagnostics.push(ResolutionDiagnostic::MissingDev { dist: dist.clone(), dev: dev.clone(), @@ -872,7 +872,7 @@ fn has_lower_bound( for requirement in metadata .requires_dist .iter() - .chain(metadata.dev_dependencies.values().flatten()) + .chain(metadata.dependency_groups.values().flatten()) { if requirement.name != *package_name { continue; diff --git a/crates/uv-resolver/src/resolver/fork_map.rs b/crates/uv-resolver/src/resolver/fork_map.rs index fa9fd166696a..2cbcaf66e4c3 100644 --- a/crates/uv-resolver/src/resolver/fork_map.rs +++ b/crates/uv-resolver/src/resolver/fork_map.rs @@ -62,11 +62,11 @@ impl ForkMap { match markers { // If we are solving for a specific environment we already filtered // compatible requirements `from_manifest`. - ResolverMarkers::SpecificEnvironment(_) => values - .first() - .map(|entry| &entry.value) - .into_iter() - .collect(), + // + // Or, if we haven't forked yet, all values are potentially compatible. + ResolverMarkers::SpecificEnvironment(_) | ResolverMarkers::Universal { .. } => { + values.iter().map(|entry| &entry.value).collect() + } // Return all values that were requested with markers that are compatible // with the current fork, i.e. the markers are not disjoint. @@ -75,9 +75,6 @@ impl ForkMap { .filter(|entry| !fork.is_disjoint(&entry.marker)) .map(|entry| &entry.value) .collect(), - - // If we haven't forked yet, all values are potentially compatible. - ResolverMarkers::Universal { .. } => values.iter().map(|entry| &entry.value).collect(), } } } diff --git a/crates/uv-resolver/src/resolver/mod.rs b/crates/uv-resolver/src/resolver/mod.rs index 279864f4a04a..e8433ea8614b 100644 --- a/crates/uv-resolver/src/resolver/mod.rs +++ b/crates/uv-resolver/src/resolver/mod.rs @@ -13,7 +13,7 @@ use dashmap::DashMap; use either::Either; use futures::{FutureExt, StreamExt}; use itertools::Itertools; -use pubgrub::{Incompatibility, Range, State}; +use pubgrub::{Incompatibility, Range, Ranges, State}; use rustc_hash::{FxHashMap, FxHashSet}; use tokio::sync::mpsc::{self, Receiver, Sender}; use tokio::sync::oneshot; @@ -34,7 +34,7 @@ use uv_distribution_types::{ }; use uv_git::GitResolver; use uv_normalize::{ExtraName, GroupName, PackageName}; -use uv_pep440::{Version, MIN_VERSION}; +use uv_pep440::{release_specifiers_to_ranges, Version, MIN_VERSION}; use uv_pep508::MarkerTree; use uv_platform_tags::Tags; use uv_pypi_types::{Requirement, ResolutionMetadata, VerbatimParsedUrl}; @@ -51,7 +51,7 @@ use crate::pins::FilePins; use crate::preferences::Preferences; use crate::pubgrub::{ PubGrubDependency, PubGrubDistribution, PubGrubPackage, PubGrubPackageInner, PubGrubPriorities, - PubGrubPython, PubGrubSpecifier, + PubGrubPython, }; use crate::python_requirement::PythonRequirement; use crate::resolution::ResolutionGraph; @@ -465,7 +465,7 @@ impl ResolverState version, ResolverVersion::Unavailable(version, reason) => { - state.add_unavailable_version(version, reason)?; + state.add_unavailable_version(version, reason); continue; } }; @@ -1241,7 +1241,7 @@ impl ResolverState, _>>()? + .collect() } PubGrubPackageInner::Package { name, @@ -1341,7 +1341,7 @@ impl ResolverState ResolverState = requirements .iter() .flat_map(|requirement| { PubGrubDependency::from_requirement(requirement, Some(name)) }) - .collect::, _>>()?; + .collect(); // If a package has metadata for an enabled dependency group, // add a dependency from it to the same package with the group // enabled. if extra.is_none() && dev.is_none() { for group in self.groups.get(name).into_iter().flatten() { - if !metadata.dev_dependencies.contains_key(group) { + if !metadata.dependency_groups.contains_key(group) { continue; } dependencies.push(PubGrubDependency { @@ -1610,10 +1610,9 @@ impl ResolverState ResolverState Result<(), ResolveError> { + fn add_unavailable_version(&mut self, version: Version, reason: UnavailableVersion) { // Incompatible requires-python versions are special in that we track // them as incompatible dependencies instead of marking the package version // as unavailable directly. @@ -2299,9 +2292,6 @@ impl ForkState { | IncompatibleDist::Wheel(IncompatibleWheel::RequiresPython(requires_python, kind)), ) = reason { - let python_version: Range = - PubGrubSpecifier::from_release_specifiers(&requires_python)?.into(); - let package = &self.next; self.pubgrub .add_incompatibility(Incompatibility::from_dependency( @@ -2312,13 +2302,13 @@ impl ForkState { PythonRequirementKind::Installed => PubGrubPython::Installed, PythonRequirementKind::Target => PubGrubPython::Target, })), - python_version.clone(), + release_specifiers_to_ranges(requires_python), ), )); self.pubgrub .partial_solution .add_decision(self.next.clone(), version); - return Ok(()); + return; }; self.pubgrub .add_incompatibility(Incompatibility::custom_version( @@ -2326,7 +2316,6 @@ impl ForkState { version.clone(), UnavailableReason::Version(reason), )); - Ok(()) } /// Subset the current markers with the new markers and update the python requirements fields @@ -2817,15 +2806,29 @@ impl Forks { // two transitive non-sibling dependencies conflict. In // that case, we don't detect the fork ahead of time (at // present). - if deps.len() == 1 { - let dep = deps.pop().unwrap(); - let markers = dep.package.marker().cloned().unwrap_or(MarkerTree::TRUE); - for fork in &mut forks { - if !fork.markers.is_disjoint(&markers) { - fork.dependencies.push(dep.clone()); + if let [dep] = deps.as_slice() { + // There's one exception: if the requirement increases the minimum-supported Python + // version, we also fork in order to respect that minimum in the subsequent + // resolution. + // + // For example, given `requires-python = ">=3.7"` and `uv ; python_version >= "3.8"`, + // where uv itself only supports Python 3.8 and later, we need to fork to ensure + // that the resolution can find a solution. + if !dep + .package + .marker() + .and_then(marker::requires_python) + .is_some_and(|bound| python_requirement.raises(&bound)) + { + let dep = deps.pop().unwrap(); + let markers = dep.package.marker().cloned().unwrap_or(MarkerTree::TRUE); + for fork in &mut forks { + if !fork.markers.is_disjoint(&markers) { + fork.dependencies.push(dep.clone()); + } } + continue; } - continue; } for dep in deps { let mut markers = dep.package.marker().cloned().unwrap_or(MarkerTree::TRUE); diff --git a/crates/uv-settings/src/settings.rs b/crates/uv-settings/src/settings.rs index ec880f390686..6ccb7d5eb752 100644 --- a/crates/uv-settings/src/settings.rs +++ b/crates/uv-settings/src/settings.rs @@ -921,7 +921,7 @@ pub struct PipOptions { "# )] pub strict: Option, - /// Include optional dependencies from the extra group name; may be provided more than once. + /// Include optional dependencies from the specified extra; may be provided more than once. /// /// Only applies to `pyproject.toml`, `setup.py`, and `setup.cfg` sources. #[option( @@ -1563,11 +1563,13 @@ pub struct OptionsWire { #[allow(dead_code)] sources: Option, #[allow(dead_code)] - dev_dependencies: Option, - #[allow(dead_code)] managed: Option, #[allow(dead_code)] r#package: Option, + #[allow(dead_code)] + default_groups: Option, + #[allow(dead_code)] + dev_dependencies: Option, } impl From for Options { @@ -1618,9 +1620,10 @@ impl From for Options { trusted_publishing, workspace: _, sources: _, - dev_dependencies: _, managed: _, package: _, + default_groups: _, + dev_dependencies: _, } = value; Self { diff --git a/crates/uv-state/Cargo.toml b/crates/uv-state/Cargo.toml index f90413be4526..5e3d8bd5b401 100644 --- a/crates/uv-state/Cargo.toml +++ b/crates/uv-state/Cargo.toml @@ -16,7 +16,6 @@ doctest = false workspace = true [dependencies] -directories = { workspace = true } -etcetera = { workspace = true } +uv-dirs = { workspace = true } tempfile = { workspace = true } fs-err = { workspace = true } diff --git a/crates/uv-state/src/lib.rs b/crates/uv-state/src/lib.rs index 9d1f65485e70..90c930ccc59e 100644 --- a/crates/uv-state/src/lib.rs +++ b/crates/uv-state/src/lib.rs @@ -4,14 +4,13 @@ use std::{ sync::Arc, }; -use directories::ProjectDirs; -use etcetera::BaseStrategy; use fs_err as fs; use tempfile::{tempdir, TempDir}; /// The main state storage abstraction. /// -/// This is appropriate +/// This is appropriate for storing persistent data that is not user-facing, such as managed Python +/// installations or tool environments. #[derive(Debug, Clone)] pub struct StateStore { /// The state storage. @@ -85,18 +84,12 @@ impl StateStore { pub fn from_settings(state_dir: Option) -> Result { if let Some(state_dir) = state_dir { StateStore::from_path(state_dir) - } else if let Some(data_dir) = ProjectDirs::from("", "", "uv") - .map(|dirs| dirs.data_dir().to_path_buf()) - .filter(|dir| dir.exists()) - { + } else if let Some(data_dir) = uv_dirs::legacy_user_state_dir().filter(|dir| dir.exists()) { // If the user has an existing directory at (e.g.) `/Users/user/Library/Application Support/uv`, // respect it for backwards compatibility. Otherwise, prefer the XDG strategy, even on // macOS. StateStore::from_path(data_dir) - } else if let Some(data_dir) = etcetera::base_strategy::choose_base_strategy() - .ok() - .map(|dirs| dirs.data_dir().join("uv")) - { + } else if let Some(data_dir) = uv_dirs::user_state_dir() { StateStore::from_path(data_dir) } else { StateStore::from_path(".uv") diff --git a/crates/uv-static/src/env_vars.rs b/crates/uv-static/src/env_vars.rs index 52bc89edbf76..7f5850b469ee 100644 --- a/crates/uv-static/src/env_vars.rs +++ b/crates/uv-static/src/env_vars.rs @@ -146,6 +146,9 @@ impl EnvVars { /// set, uv will use this password for publishing. pub const UV_PUBLISH_PASSWORD: &'static str = "UV_PUBLISH_PASSWORD"; + /// Don't upload a file if it already exists on the index. The value is the URL of the index. + pub const UV_PUBLISH_CHECK_URL: &'static str = "UV_PUBLISH_CHECK_URL"; + /// Equivalent to the `--no-sync` command-line argument. If set, uv will skip updating /// the environment. pub const UV_NO_SYNC: &'static str = "UV_NO_SYNC"; @@ -193,6 +196,9 @@ impl EnvVars { /// for more details. pub const UV_PROJECT_ENVIRONMENT: &'static str = "UV_PROJECT_ENVIRONMENT"; + /// Specifies the directory to place links to installed, managed Python executables. + pub const UV_PYTHON_BIN_DIR: &'static str = "UV_PYTHON_BIN_DIR"; + /// Specifies the directory for storing managed Python installations. pub const UV_PYTHON_INSTALL_DIR: &'static str = "UV_PYTHON_INSTALL_DIR"; @@ -311,6 +317,9 @@ impl EnvVars { /// Timeout (in seconds) for HTTP requests. (default: 30 s) pub const UV_HTTP_TIMEOUT: &'static str = "UV_HTTP_TIMEOUT"; + /// Timeout (in seconds) for HTTP requests. Equivalent to `UV_HTTP_TIMEOUT`. + pub const UV_REQUEST_TIMEOUT: &'static str = "UV_REQUEST_TIMEOUT"; + /// Timeout (in seconds) for HTTP requests. Equivalent to `UV_HTTP_TIMEOUT`. pub const HTTP_TIMEOUT: &'static str = "HTTP_TIMEOUT"; @@ -319,9 +328,6 @@ impl EnvVars { /// See [`PycInvalidationMode`](https://docs.python.org/3/library/py_compile.html#py_compile.PycInvalidationMode). pub const PYC_INVALIDATION_MODE: &'static str = "PYC_INVALIDATION_MODE"; - /// Timeout (in seconds) for HTTP requests. - pub const UV_REQUEST_TIMEOUT: &'static str = "UV_REQUEST_TIMEOUT"; - /// Used to detect an activated virtual environment. pub const VIRTUAL_ENV: &'static str = "VIRTUAL_ENV"; @@ -364,6 +370,9 @@ impl EnvVars { /// See [no-color.org](https://no-color.org). pub const NO_COLOR: &'static str = "NO_COLOR"; + /// Disables all progress output. For example, spinners and progress bars. + pub const UV_NO_PROGRESS: &'static str = "UV_NO_PROGRESS"; + /// Forces colored output regardless of terminal support. /// /// See [force-color.org](https://force-color.org). @@ -402,6 +411,16 @@ impl EnvVars { /// Alternate locations for git objects. Ignored by `uv` when performing fetch. pub const GIT_ALTERNATE_OBJECT_DIRECTORIES: &'static str = "GIT_ALTERNATE_OBJECT_DIRECTORIES"; + /// Used in tests for better git isolation. + /// + /// For example, we run some tests in ~/.local/share/uv/tests. + /// And if the user's `$HOME` directory is a git repository, + /// this will change the behavior of some tests. Setting + /// `GIT_CEILING_DIRECTORIES=/home/andrew/.local/share/uv/tests` will + /// prevent git from crawling up the directory tree past that point to find + /// parent git repositories. + pub const GIT_CEILING_DIRECTORIES: &'static str = "GIT_CEILING_DIRECTORIES"; + /// Used for trusted publishing via `uv publish`. pub const GITHUB_ACTIONS: &'static str = "GITHUB_ACTIONS"; @@ -445,7 +464,11 @@ impl EnvVars { #[attr_hidden] pub const TARGET: &'static str = "TARGET"; - /// Custom log level for verbose output, compatible with `tracing_subscriber`. + /// If set, uv will use this value as the log level for its `--verbose` output. Accepts + /// any filter compatible with the `tracing_subscriber` crate. + /// For example, `RUST_LOG=trace` will enable trace-level logging. + /// See the [tracing documentation](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#example-syntax) + /// for more. pub const RUST_LOG: &'static str = "RUST_LOG"; /// The directory containing the `Cargo.toml` manifest for a package. diff --git a/crates/uv-tool/Cargo.toml b/crates/uv-tool/Cargo.toml index f74b29fc248b..b8279180022e 100644 --- a/crates/uv-tool/Cargo.toml +++ b/crates/uv-tool/Cargo.toml @@ -17,6 +17,7 @@ workspace = true [dependencies] uv-cache = { workspace = true } +uv-dirs = { workspace = true } uv-fs = { workspace = true } uv-install-wheel = { workspace = true } uv-installer = { workspace = true } @@ -28,8 +29,6 @@ uv-settings = { workspace = true } uv-state = { workspace = true } uv-static = { workspace = true } uv-virtualenv = { workspace = true } - -dirs-sys = { workspace = true } fs-err = { workspace = true } pathdiff = { workspace = true } serde = { workspace = true } diff --git a/crates/uv-tool/src/lib.rs b/crates/uv-tool/src/lib.rs index 20b3e559f195..085793e5610c 100644 --- a/crates/uv-tool/src/lib.rs +++ b/crates/uv-tool/src/lib.rs @@ -2,6 +2,7 @@ use core::fmt; use fs_err as fs; +use uv_dirs::user_executable_directory; use uv_pep440::Version; use uv_pep508::{InvalidNameError, PackageName}; @@ -41,7 +42,7 @@ pub enum Error { EntrypointRead(#[from] uv_install_wheel::Error), #[error("Failed to find dist-info directory `{0}` in environment at {1}")] DistInfoMissing(String, PathBuf), - #[error("Failed to find a directory for executables")] + #[error("Failed to find a directory to install executables into")] NoExecutableDirectory, #[error(transparent)] ToolName(#[from] InvalidNameError), @@ -354,36 +355,9 @@ impl fmt::Display for InstalledTool { } } -/// Find a directory to place executables in. -/// -/// This follows, in order: -/// -/// - `$UV_TOOL_BIN_DIR` -/// - `$XDG_BIN_HOME` -/// - `$XDG_DATA_HOME/../bin` -/// - `$HOME/.local/bin` -/// -/// On all platforms. -/// -/// Errors if a directory cannot be found. -pub fn find_executable_directory() -> Result { - std::env::var_os(EnvVars::UV_TOOL_BIN_DIR) - .and_then(dirs_sys::is_absolute_path) - .or_else(|| std::env::var_os(EnvVars::XDG_BIN_HOME).and_then(dirs_sys::is_absolute_path)) - .or_else(|| { - std::env::var_os(EnvVars::XDG_DATA_HOME) - .and_then(dirs_sys::is_absolute_path) - .map(|path| path.join("../bin")) - }) - .or_else(|| { - // See https://github.com/dirs-dev/dirs-rs/blob/50b50f31f3363b7656e5e63b3fa1060217cbc844/src/win.rs#L5C58-L5C78 - #[cfg(windows)] - let home_dir = dirs_sys::known_folder_profile(); - #[cfg(not(windows))] - let home_dir = dirs_sys::home_dir(); - home_dir.map(|path| path.join(".local").join("bin")) - }) - .ok_or(Error::NoExecutableDirectory) +/// Find the tool executable directory. +pub fn tool_executable_dir() -> Result { + user_executable_directory(Some(EnvVars::UV_TOOL_BIN_DIR)).ok_or(Error::NoExecutableDirectory) } /// Find the `.dist-info` directory for a package in an environment. diff --git a/crates/uv-trampoline-builder/Cargo.toml b/crates/uv-trampoline-builder/Cargo.toml new file mode 100644 index 000000000000..a093ce5e9f83 --- /dev/null +++ b/crates/uv-trampoline-builder/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "uv-trampoline-builder" +version = "0.0.1" +publish = false +description = "Builds launchers for `uv-trampoline`" + +edition = { workspace = true } +rust-version = { workspace = true } +homepage = { workspace = true } +documentation = { workspace = true } +repository = { workspace = true } +authors = { workspace = true } +license = { workspace = true } + +[features] +default = ["production"] + +# Expect tests to run against production builds of `uv-trampoline` binaries, rather than debug builds +production = [] + +[lints] +workspace = true + +[dependencies] +uv-fs = { workspace = true } + +fs-err = {workspace = true } +thiserror = { workspace = true } +zip = { workspace = true } + +[dev-dependencies] +assert_cmd = { version = "2.0.16" } +assert_fs = { version = "1.1.2" } +anyhow = { version = "1.0.89" } +fs-err = { workspace = true } +which = { workspace = true } diff --git a/crates/uv-trampoline-builder/src/lib.rs b/crates/uv-trampoline-builder/src/lib.rs new file mode 100644 index 000000000000..a5920fd2d054 --- /dev/null +++ b/crates/uv-trampoline-builder/src/lib.rs @@ -0,0 +1,551 @@ +use std::io::{self, Cursor, Read, Seek, Write}; +use std::path::{Path, PathBuf}; +use std::str::Utf8Error; + +use fs_err::File; +use thiserror::Error; +use uv_fs::Simplified; +use zip::write::FileOptions; +use zip::ZipWriter; + +#[cfg(all(windows, target_arch = "x86"))] +const LAUNCHER_I686_GUI: &[u8] = + include_bytes!("../../uv-trampoline/trampolines/uv-trampoline-i686-gui.exe"); + +#[cfg(all(windows, target_arch = "x86"))] +const LAUNCHER_I686_CONSOLE: &[u8] = + include_bytes!("../../uv-trampoline/trampolines/uv-trampoline-i686-console.exe"); + +#[cfg(all(windows, target_arch = "x86_64"))] +const LAUNCHER_X86_64_GUI: &[u8] = + include_bytes!("../../uv-trampoline/trampolines/uv-trampoline-x86_64-gui.exe"); + +#[cfg(all(windows, target_arch = "x86_64"))] +const LAUNCHER_X86_64_CONSOLE: &[u8] = + include_bytes!("../../uv-trampoline/trampolines/uv-trampoline-x86_64-console.exe"); + +#[cfg(all(windows, target_arch = "aarch64"))] +const LAUNCHER_AARCH64_GUI: &[u8] = + include_bytes!("../../uv-trampoline/trampolines/uv-trampoline-aarch64-gui.exe"); + +#[cfg(all(windows, target_arch = "aarch64"))] +const LAUNCHER_AARCH64_CONSOLE: &[u8] = + include_bytes!("../../uv-trampoline/trampolines/uv-trampoline-aarch64-console.exe"); + +// See `uv-trampoline::bounce`. These numbers must match. +const PATH_LENGTH_SIZE: usize = size_of::(); +const MAX_PATH_LENGTH: u32 = 32 * 1024; +const MAGIC_NUMBER_SIZE: usize = 4; + +#[derive(Debug)] +pub struct Launcher { + pub kind: LauncherKind, + pub python_path: PathBuf, +} + +impl Launcher { + /// Read [`Launcher`] metadata from a trampoline executable file. + /// + /// Returns `Ok(None)` if the file is not a trampoline executable. + /// Returns `Err` if the file looks like a trampoline executable but is formatted incorrectly. + /// + /// Expects the following metadata to be at the end of the file: + /// + /// ```text + /// - file path (no greater than 32KB) + /// - file path length (u32) + /// - magic number(4 bytes) + /// ``` + /// + /// This should only be used on Windows, but should just return `Ok(None)` on other platforms. + /// + /// This is an implementation of [`uv-trampoline::bounce::read_trampoline_metadata`] that + /// returns errors instead of panicking. Unlike the utility there, we don't assume that the + /// file we are reading is a trampoline. + #[allow(clippy::cast_possible_wrap)] + pub fn try_from_path(path: &Path) -> Result, Error> { + let mut file = File::open(path)?; + + // Read the magic number + let Some(kind) = LauncherKind::try_from_file(&mut file)? else { + return Ok(None); + }; + + // Seek to the start of the path length. + let path_length_offset = (MAGIC_NUMBER_SIZE + PATH_LENGTH_SIZE) as i64; + file.seek(io::SeekFrom::End(-path_length_offset)) + .map_err(|err| { + Error::InvalidLauncherSeek("path length".to_string(), path_length_offset, err) + })?; + + // Read the path length + let mut buffer = [0; PATH_LENGTH_SIZE]; + file.read_exact(&mut buffer) + .map_err(|err| Error::InvalidLauncherRead("path length".to_string(), err))?; + + let path_length = { + let raw_length = u32::from_le_bytes(buffer); + + if raw_length > MAX_PATH_LENGTH { + return Err(Error::InvalidPathLength(raw_length)); + } + + // SAFETY: Above we guarantee the length is less than 32KB + raw_length as usize + }; + + // Seek to the start of the path + let path_offset = (MAGIC_NUMBER_SIZE + PATH_LENGTH_SIZE + path_length) as i64; + file.seek(io::SeekFrom::End(-path_offset)).map_err(|err| { + Error::InvalidLauncherSeek("executable path".to_string(), path_offset, err) + })?; + + // Read the path + let mut buffer = vec![0u8; path_length]; + file.read_exact(&mut buffer) + .map_err(|err| Error::InvalidLauncherRead("executable path".to_string(), err))?; + + let path = PathBuf::from( + String::from_utf8(buffer).map_err(|err| Error::InvalidPath(err.utf8_error()))?, + ); + + Ok(Some(Self { + kind, + python_path: path, + })) + } +} + +/// The kind of trampoline launcher to create. +/// +/// See [`uv-trampoline::bounce::TrampolineKind`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum LauncherKind { + /// The trampoline should execute itself, it's a zipped Python script. + Script, + /// The trampoline should just execute Python, it's a proxy Python executable. + Python, +} + +impl LauncherKind { + /// Return the magic number for this [`LauncherKind`]. + const fn magic_number(self) -> &'static [u8; 4] { + match self { + Self::Script => b"UVSC", + Self::Python => b"UVPY", + } + } + + /// Read a [`LauncherKind`] from 4 byte buffer. + /// + /// If the buffer does not contain a matching magic number, `None` is returned. + fn try_from_bytes(bytes: [u8; MAGIC_NUMBER_SIZE]) -> Option { + if &bytes == Self::Script.magic_number() { + return Some(Self::Script); + } + if &bytes == Self::Python.magic_number() { + return Some(Self::Python); + } + None + } + + /// Read a [`LauncherKind`] from a file handle, based on the magic number. + /// + /// This will mutate the file handle, seeking to the end of the file. + /// + /// If the file cannot be read, an [`io::Error`] is returned. If the path is not a launcher, + /// `None` is returned. + #[allow(clippy::cast_possible_wrap)] + pub fn try_from_file(file: &mut File) -> Result, Error> { + // If the file is less than four bytes, it's not a launcher. + let Ok(_) = file.seek(io::SeekFrom::End(-(MAGIC_NUMBER_SIZE as i64))) else { + return Ok(None); + }; + + let mut buffer = [0; MAGIC_NUMBER_SIZE]; + file.read_exact(&mut buffer) + .map_err(|err| Error::InvalidLauncherRead("magic number".to_string(), err))?; + + Ok(Self::try_from_bytes(buffer)) + } +} + +/// Note: The caller is responsible for adding the path of the wheel we're installing. +#[derive(Error, Debug)] +pub enum Error { + #[error(transparent)] + Io(#[from] io::Error), + #[error("Only paths with a length up to 32KB are supported but found a length of {0} bytes")] + InvalidPathLength(u32), + #[error("Failed to parse executable path")] + InvalidPath(#[source] Utf8Error), + #[error("Failed to seek to {0} at offset {1}")] + InvalidLauncherSeek(String, i64, #[source] io::Error), + #[error("Failed to read launcher {0}")] + InvalidLauncherRead(String, #[source] io::Error), + #[error( + "Unable to create Windows launcher for: {0} (only x86_64, x86, and arm64 are supported)" + )] + UnsupportedWindowsArch(&'static str), + #[error("Unable to create Windows launcher on non-Windows platform")] + NotWindows, +} + +#[allow(clippy::unnecessary_wraps, unused_variables)] +fn get_launcher_bin(gui: bool) -> Result<&'static [u8], Error> { + Ok(match std::env::consts::ARCH { + #[cfg(all(windows, target_arch = "x86"))] + "x86" => { + if gui { + LAUNCHER_I686_GUI + } else { + LAUNCHER_I686_CONSOLE + } + } + #[cfg(all(windows, target_arch = "x86_64"))] + "x86_64" => { + if gui { + LAUNCHER_X86_64_GUI + } else { + LAUNCHER_X86_64_CONSOLE + } + } + #[cfg(all(windows, target_arch = "aarch64"))] + "aarch64" => { + if gui { + LAUNCHER_AARCH64_GUI + } else { + LAUNCHER_AARCH64_CONSOLE + } + } + #[cfg(windows)] + arch => { + return Err(Error::UnsupportedWindowsArch(arch)); + } + #[cfg(not(windows))] + _ => &[], + }) +} + +/// A Windows script is a minimal .exe launcher binary with the python entrypoint script appended as +/// stored zip file. +/// +/// +#[allow(unused_variables)] +pub fn windows_script_launcher( + launcher_python_script: &str, + is_gui: bool, + python_executable: impl AsRef, +) -> Result, Error> { + // This method should only be called on Windows, but we avoid `#[cfg(windows)]` to retain + // compilation on all platforms. + if cfg!(not(windows)) { + return Err(Error::NotWindows); + } + + let launcher_bin: &[u8] = get_launcher_bin(is_gui)?; + + let mut payload: Vec = Vec::new(); + { + // We're using the zip writer, but with stored compression + // https://github.com/njsmith/posy/blob/04927e657ca97a5e35bb2252d168125de9a3a025/src/trampolines/mod.rs#L75-L82 + // https://github.com/pypa/distlib/blob/8ed03aab48add854f377ce392efffb79bb4d6091/PC/launcher.c#L259-L271 + let stored = FileOptions::default().compression_method(zip::CompressionMethod::Stored); + let mut archive = ZipWriter::new(Cursor::new(&mut payload)); + let error_msg = "Writing to Vec should never fail"; + archive.start_file("__main__.py", stored).expect(error_msg); + archive + .write_all(launcher_python_script.as_bytes()) + .expect(error_msg); + archive.finish().expect(error_msg); + } + + let python = python_executable.as_ref(); + let python_path = python.simplified_display().to_string(); + + let mut launcher: Vec = Vec::with_capacity(launcher_bin.len() + payload.len()); + launcher.extend_from_slice(launcher_bin); + launcher.extend_from_slice(&payload); + launcher.extend_from_slice(python_path.as_bytes()); + launcher.extend_from_slice( + &u32::try_from(python_path.as_bytes().len()) + .expect("file path should be smaller than 4GB") + .to_le_bytes(), + ); + launcher.extend_from_slice(LauncherKind::Script.magic_number()); + + Ok(launcher) +} + +/// A minimal .exe launcher binary for Python. +/// +/// Sort of equivalent to a `python` symlink on Unix. +#[allow(unused_variables)] +pub fn windows_python_launcher( + python_executable: impl AsRef, + is_gui: bool, +) -> Result, Error> { + // This method should only be called on Windows, but we avoid `#[cfg(windows)]` to retain + // compilation on all platforms. + if cfg!(not(windows)) { + return Err(Error::NotWindows); + } + + let launcher_bin: &[u8] = get_launcher_bin(is_gui)?; + + let python = python_executable.as_ref(); + let python_path = python.simplified_display().to_string(); + + let mut launcher: Vec = Vec::with_capacity(launcher_bin.len()); + launcher.extend_from_slice(launcher_bin); + launcher.extend_from_slice(python_path.as_bytes()); + launcher.extend_from_slice( + &u32::try_from(python_path.as_bytes().len()) + .expect("file path should be smaller than 4GB") + .to_le_bytes(), + ); + launcher.extend_from_slice(LauncherKind::Python.magic_number()); + + Ok(launcher) +} + +#[cfg(all(test, windows))] +#[allow(clippy::print_stdout)] +mod test { + use std::io::Write; + use std::path::Path; + use std::process::Command; + + use anyhow::Result; + use assert_cmd::prelude::OutputAssertExt; + use assert_fs::prelude::PathChild; + use fs_err::File; + + use which::which; + + use super::{windows_python_launcher, windows_script_launcher, Launcher, LauncherKind}; + + #[test] + #[cfg(all(windows, target_arch = "x86", feature = "production"))] + fn test_launchers_are_small() { + // At time of writing, they are ~45kb. + assert!( + super::LAUNCHER_I686_GUI.len() < 45 * 1024, + "GUI launcher: {}", + super::LAUNCHER_I686_GUI.len() + ); + assert!( + super::LAUNCHER_I686_CONSOLE.len() < 45 * 1024, + "CLI launcher: {}", + super::LAUNCHER_I686_CONSOLE.len() + ); + } + + #[test] + #[cfg(all(windows, target_arch = "x86_64", feature = "production"))] + fn test_launchers_are_small() { + // At time of writing, they are ~45kb. + assert!( + super::LAUNCHER_X86_64_GUI.len() < 45 * 1024, + "GUI launcher: {}", + super::LAUNCHER_X86_64_GUI.len() + ); + assert!( + super::LAUNCHER_X86_64_CONSOLE.len() < 45 * 1024, + "CLI launcher: {}", + super::LAUNCHER_X86_64_CONSOLE.len() + ); + } + + #[test] + #[cfg(all(windows, target_arch = "aarch64", feature = "production"))] + fn test_launchers_are_small() { + // At time of writing, they are ~45kb. + assert!( + super::LAUNCHER_AARCH64_GUI.len() < 45 * 1024, + "GUI launcher: {}", + super::LAUNCHER_AARCH64_GUI.len() + ); + assert!( + super::LAUNCHER_AARCH64_CONSOLE.len() < 45 * 1024, + "CLI launcher: {}", + super::LAUNCHER_AARCH64_CONSOLE.len() + ); + } + + /// Utility script for the test. + fn get_script_launcher(shebang: &str, is_gui: bool) -> String { + if is_gui { + format!( + r##"{shebang} +# -*- coding: utf-8 -*- +import re +import sys + +def make_gui() -> None: + from tkinter import Tk, ttk + root = Tk() + root.title("uv Test App") + frm = ttk.Frame(root, padding=10) + frm.grid() + ttk.Label(frm, text="Hello from uv-trampoline-gui.exe").grid(column=0, row=0) + root.mainloop() + +if __name__ == "__main__": + sys.argv[0] = re.sub(r"(-script\.pyw|\.exe)?$", "", sys.argv[0]) + sys.exit(make_gui()) +"## + ) + } else { + format!( + r##"{shebang} +# -*- coding: utf-8 -*- +import re +import sys + +def main_console() -> None: + print("Hello from uv-trampoline-console.exe", file=sys.stdout) + print("Hello from uv-trampoline-console.exe", file=sys.stderr) + for arg in sys.argv[1:]: + print(arg, file=sys.stderr) + +if __name__ == "__main__": + sys.argv[0] = re.sub(r"(-script\.pyw|\.exe)?$", "", sys.argv[0]) + sys.exit(main_console()) +"## + ) + } + } + + /// See [`uv-install-wheel::wheel::format_shebang`]. + fn format_shebang(executable: impl AsRef) -> String { + // Convert the executable to a simplified path. + let executable = executable.as_ref().display().to_string(); + format!("#!{executable}") + } + + #[test] + fn console_script_launcher() -> Result<()> { + // Create Temp Dirs + let temp_dir = assert_fs::TempDir::new()?; + let console_bin_path = temp_dir.child("launcher.console.exe"); + + // Locate an arbitrary python installation from PATH + let python_executable_path = which("python")?; + + // Generate Launcher Script + let launcher_console_script = + get_script_launcher(&format_shebang(&python_executable_path), false); + + // Generate Launcher Payload + let console_launcher = + windows_script_launcher(&launcher_console_script, false, &python_executable_path)?; + + // Create Launcher + File::create(console_bin_path.path())?.write_all(console_launcher.as_ref())?; + + println!( + "Wrote Console Launcher in {}", + console_bin_path.path().display() + ); + + let stdout_predicate = "Hello from uv-trampoline-console.exe\r\n"; + let stderr_predicate = "Hello from uv-trampoline-console.exe\r\n"; + + // Test Console Launcher + #[cfg(windows)] + Command::new(console_bin_path.path()) + .assert() + .success() + .stdout(stdout_predicate) + .stderr(stderr_predicate); + + let args_to_test = vec!["foo", "bar", "foo bar", "foo \"bar\"", "foo 'bar'"]; + let stderr_predicate = format!("{}{}\r\n", stderr_predicate, args_to_test.join("\r\n")); + + // Test Console Launcher (with args) + Command::new(console_bin_path.path()) + .args(args_to_test) + .assert() + .success() + .stdout(stdout_predicate) + .stderr(stderr_predicate); + + let launcher = Launcher::try_from_path(console_bin_path.path()) + .expect("We should succeed at reading the launcher") + .expect("The launcher should be valid"); + + assert!(launcher.kind == LauncherKind::Script); + assert!(launcher.python_path == python_executable_path); + + Ok(()) + } + + #[test] + fn console_python_launcher() -> Result<()> { + // Create Temp Dirs + let temp_dir = assert_fs::TempDir::new()?; + let console_bin_path = temp_dir.child("launcher.console.exe"); + + // Locate an arbitrary python installation from PATH + let python_executable_path = which("python")?; + + // Generate Launcher Payload + let console_launcher = windows_python_launcher(&python_executable_path, false)?; + + // Create Launcher + File::create(console_bin_path.path())?.write_all(console_launcher.as_ref())?; + + println!( + "Wrote Python Launcher in {}", + console_bin_path.path().display() + ); + + // Test Console Launcher + Command::new(console_bin_path.path()) + .arg("-c") + .arg("print('Hello from Python Launcher')") + .assert() + .success() + .stdout("Hello from Python Launcher\r\n"); + + let launcher = Launcher::try_from_path(console_bin_path.path()) + .expect("We should succeed at reading the launcher") + .expect("The launcher should be valid"); + + assert!(launcher.kind == LauncherKind::Python); + assert!(launcher.python_path == python_executable_path); + + Ok(()) + } + + #[test] + #[ignore] + fn gui_launcher() -> Result<()> { + // Create Temp Dirs + let temp_dir = assert_fs::TempDir::new()?; + let gui_bin_path = temp_dir.child("launcher.gui.exe"); + + // Locate an arbitrary pythonw installation from PATH + let pythonw_executable_path = which("pythonw")?; + + // Generate Launcher Script + let launcher_gui_script = + get_script_launcher(&format_shebang(&pythonw_executable_path), true); + + // Generate Launcher Payload + let gui_launcher = + windows_script_launcher(&launcher_gui_script, true, &pythonw_executable_path)?; + + // Create Launcher + File::create(gui_bin_path.path())?.write_all(gui_launcher.as_ref())?; + + println!("Wrote GUI Launcher in {}", gui_bin_path.path().display()); + + // Test GUI Launcher + // NOTICE: This will spawn a GUI and will wait until you close the window. + Command::new(gui_bin_path.path()).assert().success(); + + Ok(()) + } +} diff --git a/crates/uv-trampoline/Cargo.lock b/crates/uv-trampoline/Cargo.lock index 69241f6bba05..5ae4861c14e5 100644 --- a/crates/uv-trampoline/Cargo.lock +++ b/crates/uv-trampoline/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "adler" @@ -25,9 +25,9 @@ checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" [[package]] name = "anyhow" -version = "1.0.90" +version = "1.0.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37bf3594c4c988a53154954629820791dde498571819ae4ca50ca811e060cc95" +checksum = "c042108f3ed77fd83760a5fd79b53be043192bb3b9dba91d8c574c0ada7850c8" [[package]] name = "assert_cmd" @@ -245,9 +245,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.155" +version = "0.2.161" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" [[package]] name = "linux-raw-sys" @@ -346,9 +346,9 @@ checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" [[package]] name = "rustix" -version = "0.38.34" +version = "0.38.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +checksum = "aa260229e6538e52293eeb577aabd09945a09d6d9cc0fc550ed7529056c2e32a" dependencies = [ "bitflags", "errno", @@ -429,18 +429,18 @@ checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" [[package]] name = "thiserror" -version = "1.0.64" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" +checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.64" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" +checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602" dependencies = [ "proc-macro2", "quote", diff --git a/crates/uv-trampoline/README.md b/crates/uv-trampoline/README.md index 8ff40aa82a5d..3a4de1933aef 100644 --- a/crates/uv-trampoline/README.md +++ b/crates/uv-trampoline/README.md @@ -12,17 +12,20 @@ LLD and add the `rustup` targets: ```shell sudo apt install llvm clang lld -rustup target add i686-pc-windows-msvc -rustup target add x86_64-pc-windows-msvc -rustup target add aarch64-pc-windows-msvc +cargo install cargo-xwin +rustup toolchain install nightly-2024-10-27 +rustup component add rust-src --toolchain nightly-2024-10-27-x86_64-unknown-linux-gnu +rustup target add --toolchain nightly-2024-10-27 i686-pc-windows-msvc +rustup target add --toolchain nightly-2024-10-27 x86_64-pc-windows-msvc +rustup target add --toolchain nightly-2024-10-27 aarch64-pc-windows-msvc ``` -Then, build the trampolines for both supported architectures: +Then, build the trampolines for all supported architectures: ```shell -cargo +nightly-2024-06-08 xwin build --xwin-arch x86 --release --target i686-pc-windows-msvc -cargo +nightly-2024-06-08 xwin build --release --target x86_64-pc-windows-msvc -cargo +nightly-2024-06-08 xwin build --release --target aarch64-pc-windows-msvc +cargo +nightly-2024-10-27 xwin build --xwin-arch x86 --release --target i686-pc-windows-msvc +cargo +nightly-2024-10-27 xwin build --release --target x86_64-pc-windows-msvc +cargo +nightly-2024-10-27 xwin build --release --target aarch64-pc-windows-msvc ``` ### Cross-compiling from macOS @@ -32,22 +35,25 @@ LLVM and add the `rustup` targets: ```shell brew install llvm -rustup target add i686-pc-windows-msvc -rustup target add x86_64-pc-windows-msvc -rustup target add aarch64-pc-windows-msvc +cargo install cargo-xwin +rustup toolchain install nightly-2024-10-27 +rustup component add rust-src --toolchain nightly-2024-10-27-aarch64-apple-darwin +rustup target add --toolchain nightly-2024-10-27 i686-pc-windows-msvc +rustup target add --toolchain nightly-2024-10-27 x86_64-pc-windows-msvc +rustup target add --toolchain nightly-2024-10-27 aarch64-pc-windows-msvc ``` -Then, build the trampolines for both supported architectures: +Then, build the trampolines for all supported architectures: ```shell -cargo +nightly-2024-06-08 xwin build --release --target i686-pc-windows-msvc -cargo +nightly-2024-06-08 xwin build --release --target x86_64-pc-windows-msvc -cargo +nightly-2024-06-08 xwin build --release --target aarch64-pc-windows-msvc +cargo +nightly-2024-10-27 xwin build --release --target i686-pc-windows-msvc +cargo +nightly-2024-10-27 xwin build --release --target x86_64-pc-windows-msvc +cargo +nightly-2024-10-27 xwin build --release --target aarch64-pc-windows-msvc ``` ### Updating the prebuilt executables -After building the trampolines for both supported architectures: +After building the trampolines for all supported architectures: ```shell cp target/aarch64-pc-windows-msvc/release/uv-trampoline-console.exe trampolines/uv-trampoline-aarch64-console.exe diff --git a/crates/uv-trampoline/rust-toolchain.toml b/crates/uv-trampoline/rust-toolchain.toml index a0c8420807eb..fce0096acafe 100644 --- a/crates/uv-trampoline/rust-toolchain.toml +++ b/crates/uv-trampoline/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "nightly-2024-06-08" +channel = "nightly-2024-10-27" diff --git a/crates/uv-trampoline/src/bounce.rs b/crates/uv-trampoline/src/bounce.rs index 22c59340a0bf..a1cdd176d077 100644 --- a/crates/uv-trampoline/src/bounce.rs +++ b/crates/uv-trampoline/src/bounce.rs @@ -33,27 +33,60 @@ use windows::Win32::{ use crate::{eprintln, format}; -const MAGIC_NUMBER: [u8; 4] = [b'U', b'V', b'U', b'V']; const PATH_LEN_SIZE: usize = size_of::(); const MAX_PATH_LEN: u32 = 32 * 1024; -/// Transform ` ` to `python `. +/// The kind of trampoline. +enum TrampolineKind { + /// The trampoline should execute itself, it's a zipped Python script. + Script, + /// The trampoline should just execute Python, it's a proxy Python executable. + Python, +} + +impl TrampolineKind { + const fn magic_number(&self) -> &'static [u8; 4] { + match self { + Self::Script => b"UVSC", + Self::Python => b"UVPY", + } + } + + fn from_buffer(buffer: &[u8]) -> Option { + if buffer.ends_with(Self::Script.magic_number()) { + Some(Self::Script) + } else if buffer.ends_with(Self::Python.magic_number()) { + Some(Self::Python) + } else { + None + } + } +} + +/// Transform ` ` to `python ` or `python ` +/// depending on the [`TrampolineKind`]. fn make_child_cmdline() -> CString { let executable_name = std::env::current_exe().unwrap_or_else(|_| { eprintln!("Failed to get executable name"); exit_with_status(1); }); - let python_exe = find_python_exe(executable_name.as_ref()); + let (kind, python_exe) = read_trampoline_metadata(executable_name.as_ref()); let mut child_cmdline = Vec::::new(); push_quoted_path(python_exe.as_ref(), &mut child_cmdline); child_cmdline.push(b' '); - // Use the full executable name because CMD only passes the name of the executable (but not the path) - // when e.g. invoking `black` instead of `/Scripts/black` and Python then fails - // to find the file. Unfortunately, this complicates things because we now need to split the executable - // from the arguments string... - push_quoted_path(executable_name.as_ref(), &mut child_cmdline); + // Only execute the trampoline again if it's a script, otherwise, just invoke Python. + match kind { + TrampolineKind::Python => {} + TrampolineKind::Script => { + // Use the full executable name because CMD only passes the name of the executable (but not the path) + // when e.g. invoking `black` instead of `/Scripts/black` and Python then fails + // to find the file. Unfortunately, this complicates things because we now need to split the executable + // from the arguments string... + push_quoted_path(executable_name.as_ref(), &mut child_cmdline); + } + } push_arguments(&mut child_cmdline); @@ -86,17 +119,21 @@ fn push_quoted_path(path: &Path, command: &mut Vec) { command.extend(br#"""#); } -/// Reads the executable binary from the back to find the path to the Python executable that is written -/// after the ZIP file content. +/// Reads the executable binary from the back to find: +/// +/// * The path to the Python executable +/// * The kind of trampoline we are executing /// /// The executable is expected to have the following format: -/// * The file must end with the magic number 'UVUV'. +/// +/// * The file must end with the magic number 'UVPY' or 'UVSC' (identifying the trampoline kind) /// * The last 4 bytes (little endian) are the length of the path to the Python executable. /// * The path encoded as UTF-8 comes right before the length /// /// # Panics +/// /// If there's any IO error, or the file does not conform to the specified format. -fn find_python_exe(executable_name: &Path) -> PathBuf { +fn read_trampoline_metadata(executable_name: &Path) -> (TrampolineKind, PathBuf) { let mut file_handle = File::open(executable_name).unwrap_or_else(|_| { print_last_error_and_exit(&format!( "Failed to open executable '{}'", @@ -117,6 +154,7 @@ fn find_python_exe(executable_name: &Path) -> PathBuf { let mut buffer: Vec = Vec::new(); let mut bytes_to_read = 1024.min(u32::try_from(file_size).unwrap_or(u32::MAX)); + let mut kind; let path: String = loop { // SAFETY: Casting to usize is safe because we only support 64bit systems where usize is guaranteed to be larger than u32. buffer.resize(bytes_to_read as usize, 0); @@ -135,13 +173,14 @@ fn find_python_exe(executable_name: &Path) -> PathBuf { // Truncate the buffer to the actual number of bytes read. buffer.truncate(read_bytes); - if !buffer.ends_with(&MAGIC_NUMBER) { - eprintln!("Magic number 'UVUV' not found at the end of the file. Did you append the magic number, the length and the path to the python executable at the end of the file?"); + let Some(inner_kind) = TrampolineKind::from_buffer(&buffer) else { + eprintln!("Magic number 'UVSC' or 'UVPY' not found at the end of the file. Did you append the magic number, the length and the path to the python executable at the end of the file?"); exit_with_status(1); - } + }; + kind = inner_kind; // Remove the magic number - buffer.truncate(buffer.len() - MAGIC_NUMBER.len()); + buffer.truncate(buffer.len() - kind.magic_number().len()); let path_len = match buffer.get(buffer.len() - PATH_LEN_SIZE..) { Some(path_len) => { @@ -177,7 +216,7 @@ fn find_python_exe(executable_name: &Path) -> PathBuf { } else { // SAFETY: Casting to u32 is safe because `path_len` is guaranteed to be less than 32KBs, // MAGIC_NUMBER is 4 bytes and PATH_LEN_SIZE is 4 bytes. - bytes_to_read = (path_len + MAGIC_NUMBER.len() + PATH_LEN_SIZE) as u32; + bytes_to_read = (path_len + kind.magic_number().len() + PATH_LEN_SIZE) as u32; if u64::from(bytes_to_read) > file_size { eprintln!("The length of the python executable path exceeds the file size. Verify that the path length is appended to the end of the launcher script as a u32 in little endian"); @@ -201,10 +240,12 @@ fn find_python_exe(executable_name: &Path) -> PathBuf { }; // NOTICE: dunce adds 5kb~ - dunce::canonicalize(path.as_path()).unwrap_or_else(|_| { + let path = dunce::canonicalize(path.as_path()).unwrap_or_else(|_| { eprintln!("Failed to canonicalize script path"); exit_with_status(1); - }) + }); + + (kind, path) } fn push_arguments(output: &mut Vec) { diff --git a/crates/uv-trampoline/tests/harness.rs b/crates/uv-trampoline/tests/harness.rs deleted file mode 100644 index 0af6bdc04515..000000000000 --- a/crates/uv-trampoline/tests/harness.rs +++ /dev/null @@ -1,269 +0,0 @@ -use std::io::{Cursor, Write}; -use std::path::Path; -use std::process::Command; -use std::{env, io}; - -use anyhow::Result; -use assert_cmd::prelude::OutputAssertExt; -use assert_fs::prelude::PathChild; -use fs_err::File; -use thiserror::Error; -use which::which; -use zip::write::FileOptions; -use zip::ZipWriter; - -const LAUNCHER_MAGIC_NUMBER: [u8; 4] = [b'U', b'V', b'U', b'V']; - -#[cfg(all(windows, target_arch = "x86"))] -const LAUNCHER_I686_GUI: &[u8] = include_bytes!("../trampolines/uv-trampoline-i686-gui.exe"); - -#[cfg(all(windows, target_arch = "x86"))] -const LAUNCHER_I686_CONSOLE: &[u8] = - include_bytes!("../trampolines/uv-trampoline-i686-console.exe"); - -#[cfg(all(windows, target_arch = "x86_64"))] -const LAUNCHER_X86_64_GUI: &[u8] = include_bytes!("../trampolines/uv-trampoline-x86_64-gui.exe"); - -#[cfg(all(windows, target_arch = "x86_64"))] -const LAUNCHER_X86_64_CONSOLE: &[u8] = - include_bytes!("../trampolines/uv-trampoline-x86_64-console.exe"); - -#[cfg(all(windows, target_arch = "aarch64"))] -const LAUNCHER_AARCH64_GUI: &[u8] = include_bytes!("../trampolines/uv-trampoline-aarch64-gui.exe"); - -#[cfg(all(windows, target_arch = "aarch64"))] -const LAUNCHER_AARCH64_CONSOLE: &[u8] = - include_bytes!("../trampolines/uv-trampoline-aarch64-console.exe"); - -/// Note: The caller is responsible for adding the path of the wheel we're installing. -#[derive(Error, Debug)] -pub enum Error { - #[error(transparent)] - Io(#[from] io::Error), - #[error( - "Unable to create Windows launcher for: {0} (only x86_64, x86, and arm64 are supported)" - )] - UnsupportedWindowsArch(&'static str), - #[error("Unable to create Windows launcher on non-Windows platform")] - NotWindows, -} - -/// Wrapper script template function -/// -/// -fn get_script_launcher(shebang: &str, is_gui: bool) -> String { - if is_gui { - format!( - r##"{shebang} -# -*- coding: utf-8 -*- -import re -import sys - -def make_gui() -> None: - from tkinter import Tk, ttk - root = Tk() - root.title("uv Test App") - frm = ttk.Frame(root, padding=10) - frm.grid() - ttk.Label(frm, text="Hello from uv-trampoline-gui.exe").grid(column=0, row=0) - root.mainloop() - -if __name__ == "__main__": - sys.argv[0] = re.sub(r"(-script\.pyw|\.exe)?$", "", sys.argv[0]) - sys.exit(make_gui()) -"## - ) - } else { - format!( - r##"{shebang} -# -*- coding: utf-8 -*- -import re -import sys - -def main_console() -> None: - print("Hello from uv-trampoline-console.exe", file=sys.stdout) - print("Hello from uv-trampoline-console.exe", file=sys.stderr) - for arg in sys.argv[1:]: - print(arg, file=sys.stderr) - -if __name__ == "__main__": - sys.argv[0] = re.sub(r"(-script\.pyw|\.exe)?$", "", sys.argv[0]) - sys.exit(main_console()) -"## - ) - } -} - -/// Format the shebang for a given Python executable. -/// -/// Like pip, if a shebang is non-simple (too long or contains spaces), we use `/bin/sh` as the -/// executable. -/// -/// See: -fn format_shebang(executable: impl AsRef) -> String { - // Convert the executable to a simplified path. - let executable = executable.as_ref().display().to_string(); - format!("#!{executable}") -} - -/// A Windows script is a minimal .exe launcher binary with the python entrypoint script appended as -/// stored zip file. -/// -/// -#[allow(unused_variables)] -fn windows_script_launcher( - launcher_python_script: &str, - is_gui: bool, - python_executable: impl AsRef, -) -> Result, Error> { - // This method should only be called on Windows, but we avoid `#[cfg(windows)]` to retain - // compilation on all platforms. - if cfg!(not(windows)) { - return Err(Error::NotWindows); - } - - let launcher_bin: &[u8] = match env::consts::ARCH { - #[cfg(all(windows, target_arch = "x86"))] - "x86" => { - if is_gui { - LAUNCHER_I686_GUI - } else { - LAUNCHER_I686_CONSOLE - } - } - #[cfg(all(windows, target_arch = "x86_64"))] - "x86_64" => { - if is_gui { - LAUNCHER_X86_64_GUI - } else { - LAUNCHER_X86_64_CONSOLE - } - } - #[cfg(all(windows, target_arch = "aarch64"))] - "aarch64" => { - if is_gui { - LAUNCHER_AARCH64_GUI - } else { - LAUNCHER_AARCH64_CONSOLE - } - } - #[cfg(windows)] - arch => { - return Err(Error::UnsupportedWindowsArch(arch)); - } - #[cfg(not(windows))] - arch => &[], - }; - - let mut payload: Vec = Vec::new(); - { - // We're using the zip writer, but with stored compression - // https://github.com/njsmith/posy/blob/04927e657ca97a5e35bb2252d168125de9a3a025/src/trampolines/mod.rs#L75-L82 - // https://github.com/pypa/distlib/blob/8ed03aab48add854f377ce392efffb79bb4d6091/PC/launcher.c#L259-L271 - let stored = FileOptions::default().compression_method(zip::CompressionMethod::Stored); - let mut archive = ZipWriter::new(Cursor::new(&mut payload)); - let error_msg = "Writing to Vec should never fail"; - archive.start_file("__main__.py", stored).expect(error_msg); - archive - .write_all(launcher_python_script.as_bytes()) - .expect(error_msg); - archive.finish().expect(error_msg); - } - - let python = python_executable.as_ref(); - let python_path = python.display().to_string(); - - let mut launcher: Vec = Vec::with_capacity(launcher_bin.len() + payload.len()); - launcher.extend_from_slice(launcher_bin); - launcher.extend_from_slice(&payload); - launcher.extend_from_slice(python_path.as_bytes()); - launcher.extend_from_slice( - &u32::try_from(python_path.as_bytes().len()) - .expect("File Path to be smaller than 4GB") - .to_le_bytes(), - ); - launcher.extend_from_slice(&LAUNCHER_MAGIC_NUMBER); - - Ok(launcher) -} - -#[test] -fn generate_console_launcher() -> Result<()> { - // Create Temp Dirs - let temp_dir = assert_fs::TempDir::new()?; - let console_bin_path = temp_dir.child("launcher.console.exe"); - - // Locate an arbitrary python installation from PATH - let python_executable_path = which("python")?; - - // Generate Launcher Script - let launcher_console_script = - get_script_launcher(&format_shebang(&python_executable_path), false); - - // Generate Launcher Payload - let console_launcher = - windows_script_launcher(&launcher_console_script, false, &python_executable_path)?; - - // Create Launcher - File::create(console_bin_path.path())?.write_all(console_launcher.as_ref())?; - - println!( - "Wrote Console Launcher in {}", - console_bin_path.path().display() - ); - - let stdout_predicate = "Hello from uv-trampoline-console.exe\r\n"; - let stderr_predicate = "Hello from uv-trampoline-console.exe\r\n"; - - // Test Console Launcher - #[cfg(windows)] - Command::new(console_bin_path.path()) - .assert() - .success() - .stdout(stdout_predicate) - .stderr(stderr_predicate); - - let args_to_test = vec!["foo", "bar", "foo bar", "foo \"bar\"", "foo 'bar'"]; - let stderr_predicate = format!("{}{}\r\n", stderr_predicate, args_to_test.join("\r\n")); - - // Test Console Launcher (with args) - #[cfg(windows)] - Command::new(console_bin_path.path()) - .args(args_to_test) - .assert() - .success() - .stdout(stdout_predicate) - .stderr(stderr_predicate); - - Ok(()) -} - -#[test] -#[ignore] -fn generate_gui_launcher() -> Result<()> { - // Create Temp Dirs - let temp_dir = assert_fs::TempDir::new()?; - let gui_bin_path = temp_dir.child("launcher.gui.exe"); - - // Locate an arbitrary pythonw installation from PATH - let pythonw_executable_path = which("pythonw")?; - - // Generate Launcher Script - let launcher_gui_script = get_script_launcher(&format_shebang(&pythonw_executable_path), true); - - // Generate Launcher Payload - let gui_launcher = - windows_script_launcher(&launcher_gui_script, true, &pythonw_executable_path)?; - - // Create Launcher - File::create(gui_bin_path.path())?.write_all(gui_launcher.as_ref())?; - - println!("Wrote GUI Launcher in {}", gui_bin_path.path().display()); - - // Test GUI Launcher - // NOTICE: This will spawn a GUI and will wait until you close the window. - #[cfg(windows)] - Command::new(gui_bin_path.path()).assert().success(); - - Ok(()) -} diff --git a/crates/uv-trampoline/trampolines/uv-trampoline-aarch64-console.exe b/crates/uv-trampoline/trampolines/uv-trampoline-aarch64-console.exe index 5b6ea9cab831..773c2f9ba5ac 100755 Binary files a/crates/uv-trampoline/trampolines/uv-trampoline-aarch64-console.exe and b/crates/uv-trampoline/trampolines/uv-trampoline-aarch64-console.exe differ diff --git a/crates/uv-trampoline/trampolines/uv-trampoline-aarch64-gui.exe b/crates/uv-trampoline/trampolines/uv-trampoline-aarch64-gui.exe index 1f6524111b1e..f0fe2a3bb16e 100755 Binary files a/crates/uv-trampoline/trampolines/uv-trampoline-aarch64-gui.exe and b/crates/uv-trampoline/trampolines/uv-trampoline-aarch64-gui.exe differ diff --git a/crates/uv-trampoline/trampolines/uv-trampoline-i686-console.exe b/crates/uv-trampoline/trampolines/uv-trampoline-i686-console.exe index 3f0bed6a18a4..ee1e70e71313 100755 Binary files a/crates/uv-trampoline/trampolines/uv-trampoline-i686-console.exe and b/crates/uv-trampoline/trampolines/uv-trampoline-i686-console.exe differ diff --git a/crates/uv-trampoline/trampolines/uv-trampoline-i686-gui.exe b/crates/uv-trampoline/trampolines/uv-trampoline-i686-gui.exe index 936aedb5e003..6edd2f511478 100755 Binary files a/crates/uv-trampoline/trampolines/uv-trampoline-i686-gui.exe and b/crates/uv-trampoline/trampolines/uv-trampoline-i686-gui.exe differ diff --git a/crates/uv-trampoline/trampolines/uv-trampoline-x86_64-console.exe b/crates/uv-trampoline/trampolines/uv-trampoline-x86_64-console.exe index 8ba2f2b5acd0..f437f03f0c57 100755 Binary files a/crates/uv-trampoline/trampolines/uv-trampoline-x86_64-console.exe and b/crates/uv-trampoline/trampolines/uv-trampoline-x86_64-console.exe differ diff --git a/crates/uv-trampoline/trampolines/uv-trampoline-x86_64-gui.exe b/crates/uv-trampoline/trampolines/uv-trampoline-x86_64-gui.exe index 9f503fe512c9..39069c7431ec 100755 Binary files a/crates/uv-trampoline/trampolines/uv-trampoline-x86_64-gui.exe and b/crates/uv-trampoline/trampolines/uv-trampoline-x86_64-gui.exe differ diff --git a/crates/uv-version/Cargo.toml b/crates/uv-version/Cargo.toml index c33715831871..26f079d11854 100644 --- a/crates/uv-version/Cargo.toml +++ b/crates/uv-version/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-version" -version = "0.4.25" +version = "0.4.29" edition = { workspace = true } rust-version = { workspace = true } homepage = { workspace = true } diff --git a/crates/uv-workspace/Cargo.toml b/crates/uv-workspace/Cargo.toml index 967a90ae3ff4..d77d08fcd21c 100644 --- a/crates/uv-workspace/Cargo.toml +++ b/crates/uv-workspace/Cargo.toml @@ -16,6 +16,7 @@ doctest = false workspace = true [dependencies] +uv-cache-key = { workspace = true } uv-distribution-types = { workspace = true } uv-fs = { workspace = true, features = ["tokio", "schemars"] } uv-git = { workspace = true } diff --git a/crates/uv-workspace/src/dependency_groups.rs b/crates/uv-workspace/src/dependency_groups.rs new file mode 100644 index 000000000000..e3e431fd06da --- /dev/null +++ b/crates/uv-workspace/src/dependency_groups.rs @@ -0,0 +1,150 @@ +use std::collections::BTreeMap; +use std::str::FromStr; + +use thiserror::Error; +use tracing::warn; + +use uv_normalize::GroupName; +use uv_pep508::Pep508Error; +use uv_pypi_types::VerbatimParsedUrl; + +use crate::pyproject::DependencyGroupSpecifier; + +/// PEP 735 dependency groups, with any `include-group` entries resolved. +#[derive(Debug, Clone)] +pub struct FlatDependencyGroups( + BTreeMap>>, +); + +impl FlatDependencyGroups { + /// Resolve the dependency groups (which may contain references to other groups) into concrete + /// lists of requirements. + pub fn from_dependency_groups( + groups: &BTreeMap<&GroupName, &Vec>, + ) -> Result { + fn resolve_group<'data>( + resolved: &mut BTreeMap>>, + groups: &'data BTreeMap<&GroupName, &Vec>, + name: &'data GroupName, + parents: &mut Vec<&'data GroupName>, + ) -> Result<(), DependencyGroupError> { + let Some(specifiers) = groups.get(name) else { + // Missing group + let parent_name = parents + .iter() + .last() + .copied() + .expect("parent when group is missing"); + return Err(DependencyGroupError::GroupNotFound( + name.clone(), + parent_name.clone(), + )); + }; + + // "Dependency Group Includes MUST NOT include cycles, and tools SHOULD report an error if they detect a cycle." + if parents.contains(&name) { + return Err(DependencyGroupError::DependencyGroupCycle(Cycle( + parents.iter().copied().cloned().collect(), + ))); + } + + // If we already resolved this group, short-circuit. + if resolved.contains_key(name) { + return Ok(()); + } + + parents.push(name); + let mut requirements = Vec::with_capacity(specifiers.len()); + for specifier in *specifiers { + match specifier { + DependencyGroupSpecifier::Requirement(requirement) => { + match uv_pep508::Requirement::::from_str(requirement) { + Ok(requirement) => requirements.push(requirement), + Err(err) => { + return Err(DependencyGroupError::GroupParseError( + name.clone(), + requirement.clone(), + Box::new(err), + )); + } + } + } + DependencyGroupSpecifier::IncludeGroup { include_group } => { + resolve_group(resolved, groups, include_group, parents)?; + requirements + .extend(resolved.get(include_group).into_iter().flatten().cloned()); + } + DependencyGroupSpecifier::Object(map) => { + warn!( + "Ignoring Dependency Object Specifier referenced by `{name}`: {map:?}" + ); + } + } + } + parents.pop(); + + resolved.insert(name.clone(), requirements); + Ok(()) + } + + let mut resolved = BTreeMap::new(); + for name in groups.keys() { + let mut parents = Vec::new(); + resolve_group(&mut resolved, groups, name, &mut parents)?; + } + Ok(Self(resolved)) + } + + /// Return the requirements for a given group, if any. + pub fn get( + &self, + group: &GroupName, + ) -> Option<&Vec>> { + self.0.get(group) + } +} + +impl IntoIterator for FlatDependencyGroups { + type Item = (GroupName, Vec>); + type IntoIter = std::collections::btree_map::IntoIter< + GroupName, + Vec>, + >; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +#[derive(Debug, Error)] +pub enum DependencyGroupError { + #[error("Failed to parse entry in group `{0}`: `{1}`")] + GroupParseError( + GroupName, + String, + #[source] Box>, + ), + #[error("Failed to find group `{0}` included by `{1}`")] + GroupNotFound(GroupName, GroupName), + #[error("Detected a cycle in `dependency-groups`: {0}")] + DependencyGroupCycle(Cycle), +} + +/// A cycle in the `dependency-groups` table. +#[derive(Debug)] +pub struct Cycle(Vec); + +/// Display a cycle, e.g., `a -> b -> c -> a`. +impl std::fmt::Display for Cycle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let [first, rest @ ..] = self.0.as_slice() else { + return Ok(()); + }; + write!(f, "`{first}`")?; + for group in rest { + write!(f, " -> `{group}`")?; + } + write!(f, " -> `{first}`")?; + Ok(()) + } +} diff --git a/crates/uv-workspace/src/lib.rs b/crates/uv-workspace/src/lib.rs index 83cfd17a2c0d..74bc631d9040 100644 --- a/crates/uv-workspace/src/lib.rs +++ b/crates/uv-workspace/src/lib.rs @@ -3,6 +3,7 @@ pub use workspace::{ VirtualProject, Workspace, WorkspaceError, WorkspaceMember, }; +pub mod dependency_groups; pub mod pyproject; pub mod pyproject_mut; mod workspace; diff --git a/crates/uv-workspace/src/pyproject.rs b/crates/uv-workspace/src/pyproject.rs index 605a6e6f7df4..10d077501c95 100644 --- a/crates/uv-workspace/src/pyproject.rs +++ b/crates/uv-workspace/src/pyproject.rs @@ -6,21 +6,22 @@ //! //! Then lowers them into a dependency specification. -use glob::Pattern; -use owo_colors::OwoColorize; -use serde::de::SeqAccess; -use serde::{de::IntoDeserializer, Deserialize, Deserializer, Serialize}; use std::ops::Deref; use std::path::{Path, PathBuf}; use std::str::FromStr; use std::{collections::BTreeMap, mem}; + +use glob::Pattern; +use owo_colors::OwoColorize; +use serde::{de::IntoDeserializer, de::SeqAccess, Deserialize, Deserializer, Serialize}; use thiserror::Error; use url::Url; -use uv_distribution_types::Index; + +use uv_distribution_types::{Index, IndexName}; use uv_fs::{relative_to, PortablePathBuf}; use uv_git::GitReference; use uv_macros::OptionsMetadata; -use uv_normalize::{ExtraName, PackageName}; +use uv_normalize::{ExtraName, GroupName, PackageName}; use uv_pep440::{Version, VersionSpecifiers}; use uv_pep508::MarkerTree; use uv_pypi_types::{RequirementSource, SupportedEnvironments, VerbatimParsedUrl}; @@ -44,6 +45,8 @@ pub struct PyProjectToml { pub project: Option, /// Tool-specific metadata. pub tool: Option, + /// Non-project dependency groups, as defined in PEP 735. + pub dependency_groups: Option, /// The raw unserialized document. #[serde(skip)] pub raw: String, @@ -112,6 +115,73 @@ impl AsRef<[u8]> for PyProjectToml { } } +/// A specifier item in a [PEP 735](https://peps.python.org/pep-0735/) Dependency Group. +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(Serialize))] +pub enum DependencyGroupSpecifier { + /// A PEP 508-compatible requirement string. + Requirement(String), + /// A reference to another dependency group. + IncludeGroup { + /// The name of the group to include. + include_group: GroupName, + }, + /// A Dependency Object Specifier. + Object(BTreeMap), +} + +impl<'de> Deserialize<'de> for DependencyGroupSpecifier { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct Visitor; + + impl<'de> serde::de::Visitor<'de> for Visitor { + type Value = DependencyGroupSpecifier; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a string or a map with the `include-group` key") + } + + fn visit_str(self, value: &str) -> Result + where + E: serde::de::Error, + { + Ok(DependencyGroupSpecifier::Requirement(value.to_owned())) + } + + fn visit_map(self, mut map: M) -> Result + where + M: serde::de::MapAccess<'de>, + { + let mut map_data = BTreeMap::new(); + while let Some((key, value)) = map.next_entry()? { + map_data.insert(key, value); + } + + if map_data.is_empty() { + return Err(serde::de::Error::custom("missing field `include-group`")); + } + + if let Some(include_group) = map_data + .get("include-group") + .map(String::as_str) + .map(GroupName::from_str) + .transpose() + .map_err(serde::de::Error::custom)? + { + Ok(DependencyGroupSpecifier::IncludeGroup { include_group }) + } else { + Ok(DependencyGroupSpecifier::Object(map_data)) + } + } + } + + deserializer.deserialize_any(Visitor) + } +} + /// PEP 621 project metadata (`project`). /// /// See . @@ -152,8 +222,23 @@ pub struct Tool { #[serde(rename_all = "kebab-case")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct ToolUv { - /// The sources to use (e.g., workspace members, Git repositories, local paths) when resolving - /// dependencies. + /// The sources to use when resolving dependencies. + /// + /// `tool.uv.sources` enriches the dependency metadata with additional sources, incorporated + /// during development. A dependency source can be a Git repository, a URL, a local path, or an + /// alternative registry. + /// + /// See [Dependencies](../concepts/dependencies.md) for more. + #[option( + default = "\"[]\"", + value_type = "dict", + example = r#" + [tool.uv.sources] + httpx = { git = "https://github.com/encode/httpx", tag = "0.27.0" } + pytest = { url = "https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl" } + pydantic = { path = "/path/to/pydantic", editable = true } + "# + )] pub sources: Option, /// The indexes to use when resolving dependencies. @@ -228,48 +313,40 @@ pub struct ToolUv { )] pub package: Option, - /// The project's development dependencies. Development dependencies will be installed by - /// default in `uv run` and `uv sync`, but will not appear in the project's published metadata. - #[cfg_attr( - feature = "schemars", - schemars( - with = "Option>", - description = "PEP 508-style requirements, e.g., `ruff==0.5.0`, or `ruff @ https://...`." - ) - )] + /// The list of `dependency-groups` to install by default. #[option( - default = r#"[]"#, + default = r#"["dev"]"#, value_type = "list[str]", example = r#" - dev-dependencies = ["ruff==0.5.0"] + default-groups = ["docs"] "# )] - pub dev_dependencies: Option>>, + pub default_groups: Option>, - /// A list of supported environments against which to resolve dependencies. + /// The project's development dependencies. /// - /// By default, uv will resolve for all possible environments during a `uv lock` operation. - /// However, you can restrict the set of supported environments to improve performance and avoid - /// unsatisfiable branches in the solution space. + /// Development dependencies will be installed by default in `uv run` and `uv sync`, but will + /// not appear in the project's published metadata. /// - /// These environments will also respected when `uv pip compile` is invoked with the - /// `--universal` flag. + /// Use of this field is not recommend anymore. Instead, use the `dependency-groups.dev` field + /// which is a standardized way to declare development dependencies. The contents of + /// `tool.uv.dev-dependencies` and `dependency-groups.dev` are combined to determine the the + /// final requirements of the `dev` dependency group. #[cfg_attr( feature = "schemars", schemars( with = "Option>", - description = "A list of environment markers, e.g., `python_version >= '3.6'`." + description = "PEP 508-style requirements, e.g., `ruff==0.5.0`, or `ruff @ https://...`." ) )] #[option( default = r#"[]"#, - value_type = "str | list[str]", + value_type = "list[str]", example = r#" - # Resolve for macOS, but not for Linux or Windows. - environments = ["sys_platform == 'darwin'"] + dev-dependencies = ["ruff==0.5.0"] "# )] - pub environments: Option, + pub dev_dependencies: Option>>, /// Overrides to apply when resolving the project's dependencies. /// @@ -337,6 +414,31 @@ pub struct ToolUv { "# )] pub constraint_dependencies: Option>>, + + /// A list of supported environments against which to resolve dependencies. + /// + /// By default, uv will resolve for all possible environments during a `uv lock` operation. + /// However, you can restrict the set of supported environments to improve performance and avoid + /// unsatisfiable branches in the solution space. + /// + /// These environments will also respected when `uv pip compile` is invoked with the + /// `--universal` flag. + #[cfg_attr( + feature = "schemars", + schemars( + with = "Option>", + description = "A list of environment markers, e.g., `python_version >= '3.6'`." + ) + )] + #[option( + default = r#"[]"#, + value_type = "str | list[str]", + example = r#" + # Resolve for macOS, but not for Linux or Windows. + environments = ["sys_platform == 'darwin'"] + "# + )] + pub environments: Option, } #[derive(Default, Debug, Clone, PartialEq, Eq)] @@ -455,6 +557,84 @@ impl Deref for SerdePattern { } } +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(test, derive(Serialize))] +pub struct DependencyGroups(BTreeMap>); + +impl DependencyGroups { + /// Returns the names of the dependency groups. + pub fn keys(&self) -> impl Iterator { + self.0.keys() + } + + /// Returns the dependency group with the given name. + pub fn get(&self, group: &GroupName) -> Option<&Vec> { + self.0.get(group) + } + + /// Returns `true` if the dependency group is in the list. + pub fn contains_key(&self, group: &GroupName) -> bool { + self.0.contains_key(group) + } + + /// Returns an iterator over the dependency groups. + pub fn iter(&self) -> impl Iterator)> { + self.0.iter() + } +} + +impl<'a> IntoIterator for &'a DependencyGroups { + type Item = (&'a GroupName, &'a Vec); + type IntoIter = std::collections::btree_map::Iter<'a, GroupName, Vec>; + + fn into_iter(self) -> Self::IntoIter { + self.0.iter() + } +} + +/// Ensure that all keys in the TOML table are unique. +impl<'de> serde::de::Deserialize<'de> for DependencyGroups { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct GroupVisitor; + + impl<'de> serde::de::Visitor<'de> for GroupVisitor { + type Value = DependencyGroups; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a table with unique dependency group names") + } + + fn visit_map(self, mut access: M) -> Result + where + M: serde::de::MapAccess<'de>, + { + let mut sources = BTreeMap::new(); + while let Some((key, value)) = + access.next_entry::>()? + { + match sources.entry(key) { + std::collections::btree_map::Entry::Occupied(entry) => { + return Err(serde::de::Error::custom(format!( + "duplicate dependency group: `{}`", + entry.key() + ))); + } + std::collections::btree_map::Entry::Vacant(entry) => { + entry.insert(value); + } + } + } + Ok(DependencyGroups(sources)) + } + } + + deserializer.deserialize_map(GroupVisitor) + } +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[serde(rename_all = "kebab-case", try_from = "SourcesWire")] @@ -644,7 +824,7 @@ pub enum Source { }, /// A dependency pinned to a specific index, e.g., `torch` after setting `torch` to `https://download.pytorch.org/whl/cu118`. Registry { - index: String, + index: IndexName, #[serde( skip_serializing_if = "uv_pep508::marker::ser::is_empty", serialize_with = "uv_pep508::marker::ser::serialize", @@ -684,7 +864,7 @@ impl<'de> Deserialize<'de> for Source { url: Option, path: Option, editable: Option, - index: Option, + index: Option, workspace: Option, #[serde( skip_serializing_if = "uv_pep508::marker::ser::is_empty", @@ -993,7 +1173,7 @@ impl Source { source: RequirementSource, workspace: bool, editable: Option, - index: Option, + index: Option, rev: Option, tag: Option, branch: Option, @@ -1126,6 +1306,8 @@ pub enum DependencyType { Dev, /// A dependency in `project.optional-dependencies.{0}`. Optional(ExtraName), + /// A dependency in `dependency-groups.{0}`. + Group(GroupName), } /// diff --git a/crates/uv-workspace/src/pyproject_mut.rs b/crates/uv-workspace/src/pyproject_mut.rs index f24c960cf15e..e01ecf3bae4e 100644 --- a/crates/uv-workspace/src/pyproject_mut.rs +++ b/crates/uv-workspace/src/pyproject_mut.rs @@ -1,11 +1,18 @@ -use itertools::Itertools; use std::path::Path; use std::str::FromStr; use std::{fmt, mem}; + +use itertools::Itertools; use thiserror::Error; -use toml_edit::{Array, ArrayOfTables, DocumentMut, Item, RawString, Table, TomlError, Value}; +use toml_edit::{ + Array, ArrayOfTables, DocumentMut, Formatted, Item, RawString, Table, TomlError, Value, +}; +use url::Url; + +use uv_cache_key::CanonicalUrl; use uv_distribution_types::Index; use uv_fs::PortablePath; +use uv_normalize::GroupName; use uv_pep440::{Version, VersionSpecifier, VersionSpecifiers}; use uv_pep508::{ExtraName, MarkerTree, PackageName, Requirement, VersionOrUrl}; @@ -117,9 +124,11 @@ impl PyProjectTomlMut { Ok(()) } - /// Retrieves a mutable reference to the root [`Table`] of the TOML document, creating the - /// `project` table if necessary. - fn doc(&mut self) -> Result<&mut Table, Error> { + /// Retrieves a mutable reference to the `project` [`Table`] of the TOML document, creating the + /// table if necessary. + /// + /// For a script, this returns the root table. + fn project(&mut self) -> Result<&mut Table, Error> { let doc = match self.target { DependencyTarget::Script => self.doc.as_table_mut(), DependencyTarget::PyProjectToml => self @@ -134,7 +143,9 @@ impl PyProjectTomlMut { /// Retrieves an optional mutable reference to the `project` [`Table`], returning `None` if it /// doesn't exist. - fn doc_mut(&mut self) -> Result, Error> { + /// + /// For a script, this returns the root table. + fn project_mut(&mut self) -> Result, Error> { let doc = match self.target { DependencyTarget::Script => Some(self.doc.as_table_mut()), DependencyTarget::PyProjectToml => self @@ -156,7 +167,7 @@ impl PyProjectTomlMut { ) -> Result { // Get or create `project.dependencies`. let dependencies = self - .doc()? + .project()? .entry("dependencies") .or_insert(Item::Value(Value::Array(Array::new()))) .as_array_mut() @@ -220,70 +231,158 @@ impl PyProjectTomlMut { .ok_or(Error::MalformedSources)? .entry("index") .or_insert(Item::ArrayOfTables(ArrayOfTables::new())) - .as_array_of_tables() + .as_array_of_tables_mut() .ok_or(Error::MalformedSources)?; - let mut table = Table::new(); - if let Some(name) = index.name.as_ref() { - table.insert("name", toml_edit::value(name.to_string())); - } else if let Some(name) = existing + // If there's already an index with the same name or URL, update it (and move it to the top). + let mut table = existing .iter() .find(|table| { - table + // If the index has the same name, reuse it. + if let Some(index) = index.name.as_deref() { + if table + .get("name") + .and_then(|name| name.as_str()) + .is_some_and(|name| name == index) + { + return true; + } + } + + // If the index is the default, and there's another default index, reuse it. + if index.default + && table + .get("default") + .is_some_and(|default| default.as_bool() == Some(true)) + { + return true; + } + + // If there's another index with the same URL, reuse it. + if table .get("url") - .is_some_and(|url| url.as_str() == Some(index.url.url().as_str())) + .and_then(|item| item.as_str()) + .and_then(|url| Url::parse(url).ok()) + .is_some_and(|url| { + CanonicalUrl::new(&url) == CanonicalUrl::new(index.url.url()) + }) + { + return true; + } + + false }) - .and_then(|existing| existing.get("name")) + .cloned() + .unwrap_or_default(); + + // If necessary, update the name. + if let Some(index) = index.name.as_deref() { + if !table + .get("name") + .and_then(|name| name.as_str()) + .is_some_and(|name| name == index) + { + let mut formatted = Formatted::new(index.to_string()); + if let Some(value) = table.get("name").and_then(Item::as_value) { + if let Some(prefix) = value.decor().prefix() { + formatted.decor_mut().set_prefix(prefix.clone()); + } + if let Some(suffix) = value.decor().suffix() { + formatted.decor_mut().set_suffix(suffix.clone()); + } + } + table.insert("name", Value::String(formatted).into()); + } + } + + // If necessary, update the URL. + if !table + .get("url") + .and_then(|item| item.as_str()) + .and_then(|url| Url::parse(url).ok()) + .is_some_and(|url| CanonicalUrl::new(&url) == CanonicalUrl::new(index.url.url())) { - // If there's an existing index with the same URL, and a name, preserve the name. - table.insert("name", name.clone()); + let mut formatted = Formatted::new(index.url.to_string()); + if let Some(value) = table.get("url").and_then(Item::as_value) { + if let Some(prefix) = value.decor().prefix() { + formatted.decor_mut().set_prefix(prefix.clone()); + } + if let Some(suffix) = value.decor().suffix() { + formatted.decor_mut().set_suffix(suffix.clone()); + } + } + table.insert("url", Value::String(formatted).into()); } - table.insert("url", toml_edit::value(index.url.to_string())); + + // If necessary, update the default. if index.default { - table.insert("default", toml_edit::value(true)); + if !table + .get("default") + .and_then(toml_edit::Item::as_bool) + .is_some_and(|default| default) + { + let mut formatted = Formatted::new(true); + if let Some(value) = table.get("default").and_then(Item::as_value) { + if let Some(prefix) = value.decor().prefix() { + formatted.decor_mut().set_prefix(prefix.clone()); + } + if let Some(suffix) = value.decor().suffix() { + formatted.decor_mut().set_suffix(suffix.clone()); + } + } + table.insert("default", Value::Boolean(formatted).into()); + } } - // Push the item to the table. - let mut updated = ArrayOfTables::new(); - updated.push(table); - for table in existing { - // If there's another index with the same name, replace it. - if table - .get("name") - .is_some_and(|name| name.as_str() == index.name.as_deref()) - { - continue; + // Remove any replaced tables. + existing.retain(|table| { + // If the index has the same name, skip it. + if let Some(index) = index.name.as_deref() { + if table + .get("name") + .and_then(|name| name.as_str()) + .is_some_and(|name| name == index) + { + return false; + } } - // If there's another default index, remove it. + // If there's another default index, skip it. if index.default && table .get("default") .is_some_and(|default| default.as_bool() == Some(true)) { - continue; + return false; } - // If there's another index with the same URL, replace it. + // If there's another index with the same URL, skip it. if table .get("url") - .is_some_and(|url| url.as_str() == Some(index.url.url().as_str())) + .and_then(|item| item.as_str()) + .and_then(|url| Url::parse(url).ok()) + .is_some_and(|url| CanonicalUrl::new(&url) == CanonicalUrl::new(index.url.url())) { - continue; + return false; } - updated.push(table.clone()); + true + }); + + // Set the position to the minimum, if it's not already the first element. + if let Some(min) = existing.iter().filter_map(toml_edit::Table::position).min() { + table.set_position(min); + + // Increment the position of all existing elements. + for table in existing.iter_mut() { + if let Some(position) = table.position() { + table.set_position(position + 1); + } + } } - self.doc - .entry("tool") - .or_insert(implicit()) - .as_table_mut() - .ok_or(Error::MalformedSources)? - .entry("uv") - .or_insert(implicit()) - .as_table_mut() - .ok_or(Error::MalformedSources)? - .insert("index", Item::ArrayOfTables(updated)); + + // Push the item to the table. + existing.push(table); Ok(()) } @@ -299,7 +398,7 @@ impl PyProjectTomlMut { ) -> Result { // Get or create `project.optional-dependencies`. let optional_dependencies = self - .doc()? + .project()? .entry("optional-dependencies") .or_insert(Item::Table(Table::new())) .as_table_like_mut() @@ -323,6 +422,41 @@ impl PyProjectTomlMut { Ok(added) } + /// Adds a dependency to `dependency-groups`. + /// + /// Returns `true` if the dependency was added, `false` if it was updated. + pub fn add_dependency_group_requirement( + &mut self, + group: &GroupName, + req: &Requirement, + source: Option<&Source>, + ) -> Result { + // Get or create `dependency-groups`. + let dependency_groups = self + .doc + .entry("dependency-groups") + .or_insert(Item::Table(Table::new())) + .as_table_like_mut() + .ok_or(Error::MalformedDependencies)?; + + let group = dependency_groups + .entry(group.as_ref()) + .or_insert(Item::Value(Value::Array(Array::new()))) + .as_array_mut() + .ok_or(Error::MalformedDependencies)?; + + let name = req.name.clone(); + let added = add_dependency(req, group, source.is_some())?; + + dependency_groups.fmt(); + + if let Some(source) = source { + self.add_source(&name, source)?; + } + + Ok(added) + } + /// Set the minimum version for an existing dependency in `project.dependencies`. pub fn set_dependency_minimum_version( &mut self, @@ -331,7 +465,7 @@ impl PyProjectTomlMut { ) -> Result<(), Error> { // Get or create `project.dependencies`. let dependencies = self - .doc()? + .project()? .entry("dependencies") .or_insert(Item::Value(Value::Array(Array::new()))) .as_array_mut() @@ -400,7 +534,7 @@ impl PyProjectTomlMut { ) -> Result<(), Error> { // Get or create `project.optional-dependencies`. let optional_dependencies = self - .doc()? + .project()? .entry("optional-dependencies") .or_insert(Item::Table(Table::new())) .as_table_like_mut() @@ -428,6 +562,43 @@ impl PyProjectTomlMut { Ok(()) } + /// Set the minimum version for an existing dependency in `dependency-groups`. + pub fn set_dependency_group_requirement_minimum_version( + &mut self, + group: &GroupName, + index: usize, + version: Version, + ) -> Result<(), Error> { + // Get or create `dependency-groups`. + let dependency_groups = self + .doc + .entry("dependency-groups") + .or_insert(Item::Table(Table::new())) + .as_table_like_mut() + .ok_or(Error::MalformedDependencies)?; + + let group = dependency_groups + .entry(group.as_ref()) + .or_insert(Item::Value(Value::Array(Array::new()))) + .as_array_mut() + .ok_or(Error::MalformedDependencies)?; + + let Some(req) = group.get(index) else { + return Err(Error::MissingDependency(index)); + }; + + let mut req = req + .as_str() + .and_then(try_parse_requirement) + .ok_or(Error::MalformedDependencies)?; + req.version_or_url = Some(VersionOrUrl::VersionSpecifier(VersionSpecifiers::from( + VersionSpecifier::greater_than_equal_version(version), + ))); + group.replace(index, req.to_string()); + + Ok(()) + } + /// Adds a source to `tool.uv.sources`. fn add_source(&mut self, name: &PackageName, source: &Source) -> Result<(), Error> { // Get or create `tool.uv.sources`. @@ -458,7 +629,7 @@ impl PyProjectTomlMut { pub fn remove_dependency(&mut self, name: &PackageName) -> Result, Error> { // Try to get `project.dependencies`. let Some(dependencies) = self - .doc_mut()? + .project_mut()? .and_then(|project| project.get_mut("dependencies")) .map(|dependencies| { dependencies @@ -512,7 +683,7 @@ impl PyProjectTomlMut { ) -> Result, Error> { // Try to get `project.optional-dependencies.`. let Some(optional_dependencies) = self - .doc_mut()? + .project_mut()? .and_then(|project| project.get_mut("optional-dependencies")) .map(|extras| { extras @@ -537,6 +708,39 @@ impl PyProjectTomlMut { Ok(requirements) } + /// Removes all occurrences of the dependency in the group with the given name. + pub fn remove_dependency_group_requirement( + &mut self, + name: &PackageName, + group: &GroupName, + ) -> Result, Error> { + // Try to get `project.optional-dependencies.`. + let Some(group_dependencies) = self + .doc + .get_mut("dependency-groups") + .map(|groups| { + groups + .as_table_like_mut() + .ok_or(Error::MalformedDependencies) + }) + .transpose()? + .and_then(|groups| groups.get_mut(group.as_ref())) + .map(|dependencies| { + dependencies + .as_array_mut() + .ok_or(Error::MalformedDependencies) + }) + .transpose()? + else { + return Ok(Vec::new()); + }; + + let requirements = remove_dependency(name, group_dependencies); + self.remove_source(name)?; + + Ok(requirements) + } + /// Remove a matching source from `tool.uv.sources`, if it exists. fn remove_source(&mut self, name: &PackageName) -> Result<(), Error> { // If the dependency is still in use, don't remove the source. @@ -578,6 +782,26 @@ impl PyProjectTomlMut { Ok(()) } + /// Returns `true` if the `tool.uv.dev-dependencies` table is present. + pub fn has_dev_dependencies(&self) -> bool { + self.doc + .get("tool") + .and_then(Item::as_table) + .and_then(|tool| tool.get("uv")) + .and_then(Item::as_table) + .and_then(|uv| uv.get("dev-dependencies")) + .is_some() + } + + /// Returns `true` if the `dependency-groups` table is present and contains the given group. + pub fn has_dependency_group(&self, group: &GroupName) -> bool { + self.doc + .get("dependency-groups") + .and_then(Item::as_table) + .and_then(|groups| groups.get(group.as_ref())) + .is_some() + } + /// Returns all the places in this `pyproject.toml` that contain a dependency with the given /// name. /// @@ -618,6 +842,22 @@ impl PyProjectTomlMut { } } + // Check `dependency-groups`. + if let Some(groups) = self.doc.get("dependency-groups").and_then(Item::as_table) { + for (group, dependencies) in groups { + let Some(dependencies) = dependencies.as_array() else { + continue; + }; + let Ok(group) = GroupName::new(group.to_string()) else { + continue; + }; + + if !find_dependencies(name, marker, dependencies).is_empty() { + types.push(DependencyType::Group(group)); + } + } + } + // Check `tool.uv.dev-dependencies`. if let Some(dev_dependencies) = self .doc @@ -625,7 +865,7 @@ impl PyProjectTomlMut { .and_then(Item::as_table) .and_then(|tool| tool.get("uv")) .and_then(Item::as_table) - .and_then(|tool| tool.get("dev-dependencies")) + .and_then(|uv| uv.get("dev-dependencies")) .and_then(Item::as_array) { if !find_dependencies(name, marker, dev_dependencies).is_empty() { @@ -709,11 +949,16 @@ pub fn add_dependency( let index = index.unwrap_or(deps.len()); let mut value = Value::from(req_string.as_str()); + let decor = value.decor_mut(); - decor.set_prefix(deps.trailing().clone()); - deps.set_trailing(""); + + if index == deps.len() { + decor.set_prefix(deps.trailing().clone()); + deps.set_trailing(""); + } deps.insert_formatted(index, value); + // `reformat_array_multiline` uses the indentation of the first dependency entry. // Therefore, we retrieve the indentation of the first dependency entry and apply it to // the new entry. Note that it is only necessary if the newly added dependency is going @@ -896,6 +1141,11 @@ fn reformat_array_multiline(deps: &mut Array) { .and_then(|s| s.lines().last()) .unwrap_or_default(); + let decor_prefix = decor_prefix + .split_once('#') + .map(|(s, _)| s) + .unwrap_or(decor_prefix); + // If there is no indentation, use four-space. indentation_prefix = Some(if decor_prefix.is_empty() { " ".to_string() @@ -927,7 +1177,16 @@ fn reformat_array_multiline(deps: &mut Array) { let mut rv = String::new(); if comments.peek().is_some() { for comment in comments { - rv.push_str("\n "); + match comment.comment_type { + CommentType::OwnLine => { + let indentation_prefix_str = + format!("\n{}", indentation_prefix.as_ref().unwrap()); + rv.push_str(&indentation_prefix_str); + } + CommentType::EndOfLine => { + rv.push(' '); + } + } rv.push_str(&comment.text); } } diff --git a/crates/uv-workspace/src/workspace.rs b/crates/uv-workspace/src/workspace.rs index ece22fafa533..69254d38c705 100644 --- a/crates/uv-workspace/src/workspace.rs +++ b/crates/uv-workspace/src/workspace.rs @@ -1,9 +1,14 @@ //! Resolve the current [`ProjectWorkspace`] or [`Workspace`]. +use crate::dependency_groups::{DependencyGroupError, FlatDependencyGroups}; +use crate::pyproject::{ + DependencyGroups, Project, PyProjectToml, PyprojectTomlError, Sources, ToolUvSources, + ToolUvWorkspace, +}; use either::Either; use glob::{glob, GlobError, PatternError}; use rustc_hash::FxHashSet; -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; use std::path::{Path, PathBuf}; use tracing::{debug, trace, warn}; use uv_distribution_types::Index; @@ -14,10 +19,6 @@ use uv_pypi_types::{Requirement, RequirementSource, SupportedEnvironments, Verba use uv_static::EnvVars; use uv_warnings::{warn_user, warn_user_once}; -use crate::pyproject::{ - Project, PyProjectToml, PyprojectTomlError, Sources, ToolUvSources, ToolUvWorkspace, -}; - #[derive(thiserror::Error, Debug)] pub enum WorkspaceError { // Workspace structure errors. @@ -305,7 +306,7 @@ impl Workspace { /// `pyproject.toml`. /// /// Otherwise, returns an empty list. - pub fn non_project_requirements(&self) -> impl Iterator + '_ { + pub fn non_project_requirements(&self) -> Result, DependencyGroupError> { if self .packages .values() @@ -313,25 +314,46 @@ impl Workspace { { // If the workspace has an explicit root, the root is a member, so we don't need to // include any root-only requirements. - Either::Left(std::iter::empty()) + Ok(Vec::new()) } else { - // Otherwise, return the dev dependencies in the non-project workspace root. - Either::Right( - self.pyproject_toml - .tool - .as_ref() - .and_then(|tool| tool.uv.as_ref()) - .and_then(|uv| uv.dev_dependencies.as_ref()) - .into_iter() - .flatten() - .map(|requirement| { - Requirement::from( - requirement - .clone() - .with_origin(RequirementOrigin::Workspace), - ) - }), - ) + // Otherwise, return the dependency groups in the non-project workspace root. + // First, collect `tool.uv.dev_dependencies` + let dev_dependencies = self + .pyproject_toml + .tool + .as_ref() + .and_then(|tool| tool.uv.as_ref()) + .and_then(|uv| uv.dev_dependencies.as_ref()); + + // Then, collect `dependency-groups` + let dependency_groups = self + .pyproject_toml + .dependency_groups + .iter() + .flatten() + .collect::>(); + + // Resolve any `include-group` entries in `dependency-groups`. + let dependency_groups = + FlatDependencyGroups::from_dependency_groups(&dependency_groups)?; + + // Concatenate the two sets of requirements. + let dev_dependencies = dependency_groups + .into_iter() + .flat_map(|(_, requirements)| requirements) + .map(|requirement| { + Requirement::from(requirement.with_origin(RequirementOrigin::Workspace)) + }) + .chain(dev_dependencies.into_iter().flatten().map(|requirement| { + Requirement::from( + requirement + .clone() + .with_origin(RequirementOrigin::Workspace), + ) + })) + .collect(); + + Ok(dev_dependencies) } } @@ -392,6 +414,44 @@ impl Workspace { .collect() } + /// Returns the set of all dependency group names defined in the workspace. + pub fn groups(&self) -> BTreeSet<&GroupName> { + self.pyproject_toml + .dependency_groups + .iter() + .flat_map(DependencyGroups::keys) + .chain( + self.packages + .values() + .filter_map(|member| member.pyproject_toml.dependency_groups.as_ref()) + .flat_map(DependencyGroups::keys), + ) + .chain( + if self + .pyproject_toml + .tool + .as_ref() + .and_then(|tool| tool.uv.as_ref()) + .and_then(|uv| uv.dev_dependencies.as_ref()) + .is_some() + || self.packages.values().any(|member| { + member + .pyproject_toml + .tool + .as_ref() + .and_then(|tool| tool.uv.as_ref()) + .and_then(|uv| uv.dev_dependencies.as_ref()) + .is_some() + }) + { + Some(&*DEV_DEPENDENCIES) + } else { + None + }, + ) + .collect() + } + /// The path to the workspace root, the directory containing the top level `pyproject.toml` with /// the `uv.tool.workspace`, or the `pyproject.toml` in an implicit single workspace project. pub fn install_path(&self) -> &PathBuf { @@ -1434,7 +1494,7 @@ impl VirtualProject { } /// A target that can be installed. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Copy, Clone)] pub enum InstallTarget<'env> { /// A project (which could be a workspace root or member). Project(&'env ProjectWorkspace), @@ -1468,38 +1528,62 @@ impl<'env> InstallTarget<'env> { } } - /// Return the [`InstallTarget`] dependencies for the given group name. + /// Return the [`InstallTarget`] dependency groups. /// /// Returns dependencies that apply to the workspace root, but not any of its members. As such, /// only returns a non-empty iterator for virtual workspaces, which can include dev dependencies /// on the virtual root. - pub fn group( + pub fn groups( &self, - name: &GroupName, - ) -> impl Iterator> { + ) -> Result< + BTreeMap>>, + DependencyGroupError, + > { match self { - Self::Project(_) | Self::FrozenMember(..) => { - // For projects, dev dependencies are attached to the members. - Either::Left(std::iter::empty()) - } + Self::Project(_) | Self::FrozenMember(..) => Ok(BTreeMap::new()), Self::NonProject(workspace) => { - // For non-projects, we might have dev dependencies that are attached to the - // workspace root (which isn't a member). - if name == &*DEV_DEPENDENCIES { - Either::Right( - workspace - .pyproject_toml - .tool - .as_ref() - .and_then(|tool| tool.uv.as_ref()) - .and_then(|uv| uv.dev_dependencies.as_ref()) - .map(|dev| dev.iter()) - .into_iter() - .flatten(), - ) - } else { - Either::Left(std::iter::empty()) + // For non-projects, we might have `dependency-groups` or `tool.uv.dev-dependencies` + // that are attached to the workspace root (which isn't a member). + + // First, collect `tool.uv.dev_dependencies` + let dev_dependencies = workspace + .pyproject_toml() + .tool + .as_ref() + .and_then(|tool| tool.uv.as_ref()) + .and_then(|uv| uv.dev_dependencies.as_ref()); + + // Then, collect `dependency-groups` + let dependency_groups = workspace + .pyproject_toml() + .dependency_groups + .iter() + .flatten() + .collect::>(); + + // Merge any overlapping groups. + let mut map = BTreeMap::new(); + for (name, dependencies) in + FlatDependencyGroups::from_dependency_groups(&dependency_groups)? + .into_iter() + .chain( + // Only add the `dev` group if `dev-dependencies` is defined. + dev_dependencies.into_iter().map(|requirements| { + (DEV_DEPENDENCIES.clone(), requirements.clone()) + }), + ) + { + match map.entry(name) { + std::collections::btree_map::Entry::Vacant(entry) => { + entry.insert(dependencies); + } + std::collections::btree_map::Entry::Occupied(mut entry) => { + entry.get_mut().extend(dependencies); + } + } } + + Ok(map) } } } diff --git a/crates/uv-workspace/src/workspace/tests.rs b/crates/uv-workspace/src/workspace/tests.rs index 82978abdea7b..888f9de01119 100644 --- a/crates/uv-workspace/src/workspace/tests.rs +++ b/crates/uv-workspace/src/workspace/tests.rs @@ -1,12 +1,15 @@ use std::env; - use std::path::Path; +use std::str::FromStr; use anyhow::Result; use assert_fs::fixture::ChildPath; use assert_fs::prelude::*; use insta::assert_json_snapshot; +use uv_normalize::GroupName; + +use crate::pyproject::{DependencyGroupSpecifier, PyProjectToml}; use crate::workspace::{DiscoveryOptions, ProjectWorkspace}; async fn workspace_test(folder: &str) -> (ProjectWorkspace, String) { @@ -76,7 +79,8 @@ async fn albatross_in_example() { ], "optional-dependencies": null }, - "tool": null + "tool": null, + "dependency-groups": null } } } @@ -128,7 +132,8 @@ async fn albatross_project_in_excluded() { ], "optional-dependencies": null }, - "tool": null + "tool": null, + "dependency-groups": null } } } @@ -232,12 +237,14 @@ async fn albatross_root_workspace() { }, "managed": null, "package": null, + "default-groups": null, "dev-dependencies": null, - "environments": null, "override-dependencies": null, - "constraint-dependencies": null + "constraint-dependencies": null, + "environments": null } - } + }, + "dependency-groups": null } } } @@ -321,12 +328,14 @@ async fn albatross_virtual_workspace() { }, "managed": null, "package": null, + "default-groups": null, "dev-dependencies": null, - "environments": null, "override-dependencies": null, - "constraint-dependencies": null + "constraint-dependencies": null, + "environments": null } - } + }, + "dependency-groups": null } } } @@ -377,7 +386,8 @@ async fn albatross_just_project() { ], "optional-dependencies": null }, - "tool": null + "tool": null, + "dependency-groups": null } } } @@ -523,12 +533,14 @@ async fn exclude_package() -> Result<()> { }, "managed": null, "package": null, + "default-groups": null, "dev-dependencies": null, - "environments": null, "override-dependencies": null, - "constraint-dependencies": null + "constraint-dependencies": null, + "environments": null } - } + }, + "dependency-groups": null } } } @@ -624,12 +636,14 @@ async fn exclude_package() -> Result<()> { }, "managed": null, "package": null, + "default-groups": null, "dev-dependencies": null, - "environments": null, "override-dependencies": null, - "constraint-dependencies": null + "constraint-dependencies": null, + "environments": null } - } + }, + "dependency-groups": null } } } @@ -738,12 +752,14 @@ async fn exclude_package() -> Result<()> { }, "managed": null, "package": null, + "default-groups": null, "dev-dependencies": null, - "environments": null, "override-dependencies": null, - "constraint-dependencies": null + "constraint-dependencies": null, + "environments": null } - } + }, + "dependency-groups": null } } } @@ -826,12 +842,14 @@ async fn exclude_package() -> Result<()> { }, "managed": null, "package": null, + "default-groups": null, "dev-dependencies": null, - "environments": null, "override-dependencies": null, - "constraint-dependencies": null + "constraint-dependencies": null, + "environments": null } - } + }, + "dependency-groups": null } } } @@ -840,3 +858,39 @@ async fn exclude_package() -> Result<()> { Ok(()) } + +#[test] +fn read_dependency_groups() { + let toml = r#" +[dependency-groups] +foo = ["a", {include-group = "bar"}] +bar = ["b"] +"#; + + let result = + PyProjectToml::from_string(toml.to_string()).expect("Deserialization should succeed"); + + let groups = result + .dependency_groups + .expect("`dependency-groups` should be present"); + let foo = groups + .get(&GroupName::from_str("foo").unwrap()) + .expect("Group `foo` should be present"); + assert_eq!( + foo, + &[ + DependencyGroupSpecifier::Requirement("a".to_string()), + DependencyGroupSpecifier::IncludeGroup { + include_group: GroupName::from_str("bar").unwrap(), + } + ] + ); + + let bar = groups + .get(&GroupName::from_str("bar").unwrap()) + .expect("Group `bar` should be present"); + assert_eq!( + bar, + &[DependencyGroupSpecifier::Requirement("b".to_string())] + ); +} diff --git a/crates/uv/Cargo.toml b/crates/uv/Cargo.toml index f2a286a46656..4572de35f6f3 100644 --- a/crates/uv/Cargo.toml +++ b/crates/uv/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv" -version = "0.4.25" +version = "0.4.29" edition = { workspace = true } rust-version = { workspace = true } homepage = { workspace = true } @@ -67,17 +67,18 @@ flate2 = { workspace = true, default-features = false } fs-err = { workspace = true, features = ["tokio"] } futures = { workspace = true } http = { workspace = true } -indexmap = { workspace = true } indicatif = { workspace = true } indoc = { workspace = true } itertools = { workspace = true } jiff = { workspace = true } miette = { workspace = true, features = ["fancy-no-backtrace"] } owo-colors = { workspace = true } +petgraph = { workspace = true } rayon = { workspace = true } regex = { workspace = true } reqwest = { workspace = true } rustc-hash = { workspace = true } +same-file = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } tempfile = { workspace = true } @@ -119,7 +120,7 @@ ignored = [ ] [features] -default = ["python", "pypi", "git", "performance", "crates-io"] +default = ["python", "python-managed", "pypi", "git", "performance", "crates-io"] # Use better memory allocators, etc. — also turns-on self-update. performance = [ "performance-memory-allocator", @@ -133,6 +134,8 @@ performance-flate2-backend = ["dep:uv-performance-flate2-backend"] python = [] # Introduces a dependency on a local Python installation with specific patch versions. python-patch = [] +# Introduces a dependency on managed Python installations. +python-managed = [] # Introduces a dependency on PyPI. pypi = [] # Introduces a dependency on Git. diff --git a/crates/uv/src/commands/build_frontend.rs b/crates/uv/src/commands/build_frontend.rs index 028443a94e71..51d11b1400b2 100644 --- a/crates/uv/src/commands/build_frontend.rs +++ b/crates/uv/src/commands/build_frontend.rs @@ -187,14 +187,19 @@ async fn build_impl( } }; - let project = workspace + let package = workspace .packages() .get(package) - .ok_or_else(|| anyhow::anyhow!("Package `{}` not found in workspace", package))? - .root(); + .ok_or_else(|| anyhow::anyhow!("Package `{package}` not found in workspace"))?; + + if !package.pyproject_toml().is_package() { + let name = &package.project().name; + let pyproject_toml = package.root().join("pyproject.toml"); + return Err(anyhow::anyhow!("Package `{}` is missing a `{}`. For example, to build with `{}`, add the following to `{}`:\n```toml\n[build-system]\nrequires = [\"setuptools\"]\nbuild-backend = \"setuptools.build_meta\"\n```", name.cyan(), "build-system".green(), "setuptools".cyan(), pyproject_toml.user_display().cyan())); + } vec![AnnotatedSource::from(Source::Directory(Cow::Borrowed( - project, + package.root(), )))] } else if all { if matches!(src, Source::File(_)) { @@ -211,6 +216,10 @@ async fn build_impl( } }; + if workspace.packages().is_empty() { + return Err(anyhow::anyhow!("No packages found in workspace")); + } + let packages: Vec<_> = workspace .packages() .values() @@ -222,7 +231,10 @@ async fn build_impl( .collect(); if packages.is_empty() { - return Err(anyhow::anyhow!("No packages found in workspace")); + let member = workspace.packages().values().next().unwrap(); + let name = &member.project().name; + let pyproject_toml = member.root().join("pyproject.toml"); + return Err(anyhow::anyhow!("Workspace does contain any buildable packages. For example, to build `{}` with `{}`, add a `{}` to `{}`:\n```toml\n[build-system]\nrequires = [\"setuptools\"]\nbuild-backend = \"setuptools.build_meta\"\n```", name.cyan(), "setuptools".cyan(), "build-system".green(), pyproject_toml.user_display().cyan())); } packages @@ -384,7 +396,7 @@ async fn build_package( // (3) `Requires-Python` in `pyproject.toml` if interpreter_request.is_none() { if let Ok(workspace) = workspace { - interpreter_request = find_requires_python(workspace)? + interpreter_request = find_requires_python(workspace) .as_ref() .map(RequiresPython::specifiers) .map(|specifiers| { diff --git a/crates/uv/src/commands/pip/operations.rs b/crates/uv/src/commands/pip/operations.rs index 7b30ece08842..ba257b4a60e9 100644 --- a/crates/uv/src/commands/pip/operations.rs +++ b/crates/uv/src/commands/pip/operations.rs @@ -771,7 +771,4 @@ pub(crate) enum Error { #[error(transparent)] Anyhow(#[from] anyhow::Error), - - #[error(transparent)] - PubGrubSpecifier(#[from] uv_resolver::PubGrubSpecifierError), } diff --git a/crates/uv/src/commands/pip/tree.rs b/crates/uv/src/commands/pip/tree.rs index a1d5e8146ce0..703ebef50d23 100644 --- a/crates/uv/src/commands/pip/tree.rs +++ b/crates/uv/src/commands/pip/tree.rs @@ -1,16 +1,19 @@ +use std::collections::VecDeque; use std::fmt::Write; use anyhow::Result; -use indexmap::IndexMap; use owo_colors::OwoColorize; +use petgraph::graph::{EdgeIndex, NodeIndex}; +use petgraph::prelude::EdgeRef; +use petgraph::Direction; use rustc_hash::{FxHashMap, FxHashSet}; use uv_cache::Cache; -use uv_distribution::Metadata; use uv_distribution_types::{Diagnostic, Name}; use uv_installer::SitePackages; use uv_normalize::PackageName; -use uv_pypi_types::{RequirementSource, ResolverMarkerEnvironment}; +use uv_pep508::{Requirement, VersionOrUrl}; +use uv_pypi_types::{ResolutionMetadata, ResolverMarkerEnvironment, VerbatimParsedUrl}; use uv_python::{EnvironmentPreference, PythonEnvironment, PythonRequest}; use crate::commands::pip::operations::report_target_environment; @@ -22,8 +25,8 @@ use crate::printer::Printer; pub(crate) fn pip_tree( show_version_specifiers: bool, depth: u8, - prune: Vec, - package: Vec, + prune: &[PackageName], + package: &[PackageName], no_dedupe: bool, invert: bool, strict: bool, @@ -43,14 +46,17 @@ pub(crate) fn pip_tree( // Read packages from the virtual environment. let site_packages = SitePackages::from_environment(&environment)?; - let mut packages: IndexMap<_, Vec<_>> = IndexMap::new(); - for package in site_packages.iter() { - let metadata = Metadata::from_metadata23(package.metadata()?); + + let packages = { + let mut packages: FxHashMap<_, Vec<_>> = FxHashMap::default(); + for package in site_packages.iter() { + packages + .entry(package.name()) + .or_default() + .push(package.metadata()?); + } packages - .entry(package.name().clone()) - .or_default() - .push(metadata); - } + }; // Determine the markers to use for the resolution. let markers = environment.interpreter().resolver_markers(); @@ -64,7 +70,7 @@ pub(crate) fn pip_tree( invert, show_version_specifiers, &markers, - packages, + &packages, ) .render() .join("\n"); @@ -97,90 +103,172 @@ pub(crate) fn pip_tree( } #[derive(Debug)] -pub(crate) struct DisplayDependencyGraph { - packages: IndexMap>, +pub(crate) struct DisplayDependencyGraph<'env> { + /// The constructed dependency graph. + graph: petgraph::graph::Graph< + &'env ResolutionMetadata, + &'env Requirement, + petgraph::Directed, + >, + /// The packages considered as roots of the dependency tree. + roots: Vec, /// Maximum display depth of the dependency tree depth: usize, - /// Prune the given packages from the display of the dependency tree. - prune: Vec, - /// Display only the specified packages. - package: Vec, /// Whether to de-duplicate the displayed dependencies. no_dedupe: bool, - /// Map from package name to its requirements. - /// - /// If `--invert` is given the map is inverted. - requirements: FxHashMap>, - /// Map from requirement package name-to-parent-to-dependency metadata. - dependencies: FxHashMap>, + /// Whether to invert the dependency tree. + invert: bool, + /// Whether to include the version specifiers in the tree. + show_version_specifiers: bool, } -impl DisplayDependencyGraph { +impl<'env> DisplayDependencyGraph<'env> { /// Create a new [`DisplayDependencyGraph`] for the set of installed distributions. pub(crate) fn new( depth: usize, - prune: Vec, - package: Vec, + prune: &[PackageName], + package: &[PackageName], no_dedupe: bool, invert: bool, show_version_specifiers: bool, markers: &ResolverMarkerEnvironment, - packages: IndexMap>, + packages: &'env FxHashMap<&PackageName, Vec>, ) -> Self { - let mut requirements: FxHashMap<_, Vec<_>> = FxHashMap::default(); - let mut dependencies: FxHashMap> = - FxHashMap::default(); - - // Add all transitive requirements. + // Create a graph. + let mut graph = petgraph::graph::Graph::< + &ResolutionMetadata, + &Requirement, + petgraph::Directed, + >::new(); + + // Step 1: Add each installed package. + let mut inverse: FxHashMap> = FxHashMap::default(); for metadata in packages.values().flatten() { - // Ignore any optional dependencies. - for required in metadata - .requires_dist - .iter() - .filter(|requirement| requirement.marker.evaluate(markers, &[])) - { - let dependency = if invert { - Dependency::Inverted( - required.name.clone(), - metadata.name.clone(), - required.source.clone(), - ) - } else { - Dependency::Normal( - metadata.name.clone(), - required.name.clone(), - required.source.clone(), - ) - }; + if prune.contains(&metadata.name) { + continue; + } + + let index = graph.add_node(metadata); + inverse + .entry(metadata.name.clone()) + .or_default() + .push(index); + } + + // Step 2: Add all dependencies. + for index in graph.node_indices() { + let metadata = &graph[index]; + + for requirement in &metadata.requires_dist { + if prune.contains(&requirement.name) { + continue; + } + if !requirement.marker.evaluate(markers, &[]) { + continue; + } + + for dep_index in inverse + .get(&requirement.name) + .into_iter() + .flatten() + .copied() + { + let dep = &graph[dep_index]; + + // Avoid adding an edge if the dependency is not required by the current package. + if let Some(VersionOrUrl::VersionSpecifier(specifier)) = + requirement.version_or_url.as_ref() + { + if !specifier.contains(&dep.version) { + continue; + } + } + + graph.add_edge(index, dep_index, requirement); + } + } + } - requirements - .entry(dependency.parent().clone()) - .or_default() - .push(dependency.child().clone()); + // Step 2: Reverse the graph. + if invert { + graph.reverse(); + } - if show_version_specifiers { - dependencies - .entry(dependency.parent().clone()) - .or_default() - .insert(dependency.child().clone(), dependency); + // Step 3: Filter the graph to those nodes reachable from the target packages. + if !package.is_empty() { + // Perform a DFS from the root nodes to find the reachable nodes. + let mut reachable = graph + .node_indices() + .filter(|index| package.contains(&graph[*index].name)) + .collect::>(); + let mut stack = reachable.iter().copied().collect::>(); + while let Some(node) = stack.pop_front() { + for edge in graph.edges_directed(node, Direction::Outgoing) { + if reachable.insert(edge.target()) { + stack.push_back(edge.target()); + } } } + + // Remove the unreachable nodes from the graph. + graph.retain_nodes(|_, index| reachable.contains(&index)); } + + // Compute the list of roots. + let roots = { + let mut edges = vec![]; + + // Remove any cycles. + let feedback_set: Vec = petgraph::algo::greedy_feedback_arc_set(&graph) + .map(|e| e.id()) + .collect(); + for edge_id in feedback_set { + if let Some((source, target)) = graph.edge_endpoints(edge_id) { + if let Some(weight) = graph.remove_edge(edge_id) { + edges.push((source, target, weight)); + } + } + } + + // Find the root nodes. + let mut roots = graph + .node_indices() + .filter(|index| { + graph + .edges_directed(*index, Direction::Incoming) + .next() + .is_none() + }) + .collect::>(); + + // Sort the roots. + roots.sort_by_key(|index| { + let metadata = &graph[*index]; + (&metadata.name, &metadata.version) + }); + + // Re-add the removed edges. + for (source, target, weight) in edges { + graph.add_edge(source, target, weight); + } + + roots + }; + Self { - packages, + graph, + roots, depth, - prune, - package, no_dedupe, - requirements, - dependencies, + invert, + show_version_specifiers, } } /// Perform a depth-first traversal of the given distribution and its dependencies. - fn visit<'env>( - &'env self, - metadata: &'env Metadata, + fn visit( + &self, + cursor: Cursor, visited: &mut FxHashMap<&'env PackageName, Vec>, path: &mut Vec<&'env PackageName>, ) -> Vec { @@ -189,18 +277,31 @@ impl DisplayDependencyGraph { return Vec::new(); } + let metadata = &self.graph[cursor.node()]; let package_name = &metadata.name; let mut line = format!("{} v{}", package_name, metadata.version); // If the current package is not top-level (i.e., it has a parent), include the specifiers. - if let Some(last) = path.last().copied() { - if let Some(dependency) = self - .dependencies - .get(last) - .and_then(|deps| deps.get(package_name)) - { + if self.show_version_specifiers { + if let Some(edge) = cursor.edge() { line.push(' '); - line.push_str(&format!("[{dependency}]")); + + let source = &self.graph[edge]; + if self.invert { + let parent = self.graph.edge_endpoints(edge).unwrap().0; + let parent = &self.graph[parent].name; + let version = match source.version_or_url.as_ref() { + None => "*".to_string(), + Some(version) => version.to_string(), + }; + line.push_str(&format!("[requires: {parent} {version}]")); + } else { + let version = match source.version_or_url.as_ref() { + None => "*".to_string(), + Some(version) => version.to_string(), + }; + line.push_str(&format!("[required: {version}]")); + } } } @@ -217,25 +318,35 @@ impl DisplayDependencyGraph { } } - let requirements = self - .requirements - .get(package_name) - .into_iter() - .flatten() - .filter(|&req| { - // Skip if the current package is not one of the installed distributions. - !self.prune.contains(req) && self.packages.contains_key(req) + let mut dependencies = self + .graph + .edges_directed(cursor.node(), Direction::Outgoing) + .map(|edge| { + let node = edge.target(); + Cursor::new(node, edge.id()) }) - .cloned() .collect::>(); + dependencies.sort_by_key(|node| { + let metadata = &self.graph[node.node()]; + (&metadata.name, &metadata.version) + }); let mut lines = vec![line]; // Keep track of the dependency path to avoid cycles. - visited.insert(package_name, requirements.clone()); + visited.insert( + package_name, + dependencies + .iter() + .map(|node| { + let metadata = &self.graph[node.node()]; + metadata.name.clone() + }) + .collect(), + ); path.push(package_name); - for (index, req) in requirements.iter().enumerate() { + for (index, dep) in dependencies.iter().enumerate() { // For sub-visited packages, add the prefix to make the tree display user-friendly. // The key observation here is you can group the tree as follows when you're at the // root of the tree: @@ -255,24 +366,21 @@ impl DisplayDependencyGraph { // those in Group 3 have `└── ` at the top and ` ` at the rest. // This observation is true recursively even when looking at the subtree rooted // at `level_1_0`. - let (prefix_top, prefix_rest) = if requirements.len() - 1 == index { + let (prefix_top, prefix_rest) = if dependencies.len() - 1 == index { ("└── ", " ") } else { ("├── ", "│ ") }; - for distribution in self.packages.get(req).into_iter().flatten() { - for (visited_index, visited_line) in - self.visit(distribution, visited, path).iter().enumerate() - { - let prefix = if visited_index == 0 { - prefix_top - } else { - prefix_rest - }; + for (visited_index, visited_line) in self.visit(*dep, visited, path).iter().enumerate() + { + let prefix = if visited_index == 0 { + prefix_top + } else { + prefix_rest + }; - lines.push(format!("{prefix}{visited_line}")); - } + lines.push(format!("{prefix}{visited_line}")); } } path.pop(); @@ -282,81 +390,42 @@ impl DisplayDependencyGraph { /// Depth-first traverse the nodes to render the tree. pub(crate) fn render(&self) -> Vec { - let mut visited: FxHashMap<&PackageName, Vec> = FxHashMap::default(); - let mut path: Vec<&PackageName> = Vec::new(); - let mut lines: Vec = Vec::new(); - - if self.package.is_empty() { - // The root nodes are those that are not required by any other package. - let children: FxHashSet<_> = self.requirements.values().flatten().collect(); - for package in self.packages.values().flatten() { - // If the current package is not required by any other package, start the traversal - // with the current package as the root. - if !children.contains(&package.name) { - path.clear(); - lines.extend(self.visit(package, &mut visited, &mut path)); - } - } - } else { - for (index, package) in self.package.iter().enumerate() { - if index != 0 { - lines.push(String::new()); - } - - for package in self.packages.get(package).into_iter().flatten() { - path.clear(); - lines.extend(self.visit(package, &mut visited, &mut path)); - } - } + let mut path = Vec::new(); + let mut lines = Vec::with_capacity(self.graph.node_count()); + let mut visited = + FxHashMap::with_capacity_and_hasher(self.graph.node_count(), rustc_hash::FxBuildHasher); + + for node in &self.roots { + path.clear(); + lines.extend(self.visit(Cursor::root(*node), &mut visited, &mut path)); } lines } } -#[derive(Debug)] -enum Dependency { - /// Show dependencies from parent to the child package that it requires. - Normal(PackageName, PackageName, RequirementSource), - /// Show dependencies from the child package to the parent that requires it. - Inverted(PackageName, PackageName, RequirementSource), -} +/// A node in the dependency graph along with the edge that led to it, or `None` for root nodes. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd)] +struct Cursor(NodeIndex, Option); -impl Dependency { - /// Return the parent in the tree. - fn parent(&self) -> &PackageName { - match self { - Self::Normal(parent, _, _) => parent, - Self::Inverted(parent, _, _) => parent, - } +impl Cursor { + /// Create a [`Cursor`] representing a node in the dependency tree. + fn new(node: NodeIndex, edge: EdgeIndex) -> Self { + Self(node, Some(edge)) } - /// Return the child in the tree. - fn child(&self) -> &PackageName { - match self { - Self::Normal(_, child, _) => child, - Self::Inverted(_, child, _) => child, - } + /// Create a [`Cursor`] representing a root node in the dependency tree. + fn root(node: NodeIndex) -> Self { + Self(node, None) } -} -impl std::fmt::Display for Dependency { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Normal(_, _, source) => { - let version = match source.version_or_url() { - None => "*".to_string(), - Some(version) => version.to_string(), - }; - write!(f, "required: {version}") - } - Self::Inverted(parent, _, source) => { - let version = match source.version_or_url() { - None => "*".to_string(), - Some(version) => version.to_string(), - }; - write!(f, "requires: {parent} {version}") - } - } + /// Return the [`NodeIndex`] of the node. + fn node(&self) -> NodeIndex { + self.0 + } + + /// Return the [`EdgeIndex`] of the edge that led to the node, if any. + fn edge(&self) -> Option { + self.1 } } diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index 56ee08f4bf30..acf94ee0ba52 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -13,15 +13,15 @@ use uv_cache::Cache; use uv_cache_key::RepositoryUrl; use uv_client::{BaseClientBuilder, Connectivity, FlatIndexClient, RegistryClientBuilder}; use uv_configuration::{ - Concurrency, Constraints, DevMode, EditableMode, ExtrasSpecification, InstallOptions, - LowerBound, SourceStrategy, + Concurrency, Constraints, DevGroupsManifest, DevGroupsSpecification, DevMode, EditableMode, + ExtrasSpecification, GroupsSpecification, InstallOptions, LowerBound, SourceStrategy, }; use uv_dispatch::BuildDispatch; use uv_distribution::DistributionDatabase; -use uv_distribution_types::{Index, UnresolvedRequirement, VersionId}; +use uv_distribution_types::{Index, IndexName, UnresolvedRequirement, VersionId}; use uv_fs::Simplified; use uv_git::{GitReference, GIT_STORE}; -use uv_normalize::PackageName; +use uv_normalize::{PackageName, DEV_DEPENDENCIES}; use uv_pep508::{ExtraName, Requirement, UnnamedRequirement, VersionOrUrl}; use uv_pypi_types::{redact_credentials, ParsedUrl, RequirementSource, VerbatimParsedUrl}; use uv_python::{ @@ -42,6 +42,7 @@ use crate::commands::pip::loggers::{ }; use crate::commands::pip::operations::Modifications; use crate::commands::pip::resolution_environment; +use crate::commands::project::lock::LockMode; use crate::commands::project::{script_python_requirement, ProjectError}; use crate::commands::reporters::{PythonDownloadReporter, ResolverReporter}; use crate::commands::{diagnostics, pip, project, ExitStatus, SharedState}; @@ -203,6 +204,7 @@ pub(crate) async fn add( DependencyType::Optional(_) => { bail!("Project is missing a `[project]` table; add a `[project]` table to use optional dependencies, or run `{}` instead", "uv add --dev".green()) } + DependencyType::Group(_) => {} DependencyType::Dev => (), } } @@ -462,19 +464,60 @@ pub(crate) async fn add( _ => source, }; + // Determine the dependency type. + let dependency_type = match &dependency_type { + DependencyType::Dev => { + let existing = toml.find_dependency(&requirement.name, None); + if existing.iter().any(|dependency_type| matches!(dependency_type, DependencyType::Group(group) if group == &*DEV_DEPENDENCIES)) { + // If the dependency already exists in `dependency-groups.dev`, use that. + DependencyType::Group(DEV_DEPENDENCIES.clone()) + } else if existing.iter().any(|dependency_type| matches!(dependency_type, DependencyType::Dev)) { + // If the dependency already exists in `dev-dependencies`, use that. + DependencyType::Dev + } else { + // Otherwise, use `dependency-groups.dev`, unless it would introduce a separate table. + match (toml.has_dev_dependencies(), toml.has_dependency_group(&DEV_DEPENDENCIES)) { + (true, false) => DependencyType::Dev, + (false, true) => DependencyType::Group(DEV_DEPENDENCIES.clone()), + (true, true) => DependencyType::Group(DEV_DEPENDENCIES.clone()), + (false, false) => DependencyType::Group(DEV_DEPENDENCIES.clone()), + } + } + } + DependencyType::Group(group) if group == &*DEV_DEPENDENCIES => { + let existing = toml.find_dependency(&requirement.name, None); + if existing.iter().any(|dependency_type| matches!(dependency_type, DependencyType::Group(group) if group == &*DEV_DEPENDENCIES)) { + // If the dependency already exists in `dependency-groups.dev`, use that. + DependencyType::Group(DEV_DEPENDENCIES.clone()) + } else if existing.iter().any(|dependency_type| matches!(dependency_type, DependencyType::Dev)) { + // If the dependency already exists in `dev-dependencies`, use that. + DependencyType::Dev + } else { + // Otherwise, use `dependency-groups.dev`. + DependencyType::Group(DEV_DEPENDENCIES.clone()) + } + } + DependencyType::Production => DependencyType::Production, + DependencyType::Optional(extra) => DependencyType::Optional(extra.clone()), + DependencyType::Group(group) => DependencyType::Group(group.clone()), + }; + // Update the `pyproject.toml`. - let edit = match dependency_type { + let edit = match &dependency_type { DependencyType::Production => toml.add_dependency(&requirement, source.as_ref())?, DependencyType::Dev => toml.add_dev_dependency(&requirement, source.as_ref())?, - DependencyType::Optional(ref group) => { - toml.add_optional_dependency(group, &requirement, source.as_ref())? + DependencyType::Optional(ref extra) => { + toml.add_optional_dependency(extra, &requirement, source.as_ref())? + } + DependencyType::Group(ref group) => { + toml.add_dependency_group_requirement(group, &requirement, source.as_ref())? } }; // If the edit was inserted before the end of the list, update the existing edits. if let ArrayEdit::Add(index) = &edit { for edit in &mut edits { - if *edit.dependency_type == dependency_type { + if edit.dependency_type == dependency_type { match &mut edit.edit { ArrayEdit::Add(existing) => { if *existing >= *index { @@ -492,7 +535,7 @@ pub(crate) async fn add( } edits.push(DependencyEdit { - dependency_type: &dependency_type, + dependency_type, requirement, source, edit, @@ -556,8 +599,8 @@ pub(crate) async fn add( // Update the `pypackage.toml` in-memory. let project = project - .with_pyproject_toml(toml::from_str(&content).map_err(ProjectError::TomlParse)?) - .ok_or(ProjectError::TomlUpdate)?; + .with_pyproject_toml(toml::from_str(&content).map_err(ProjectError::PyprojectTomlParse)?) + .ok_or(ProjectError::PyprojectTomlUpdate)?; // Set the Ctrl-C handler to revert changes on exit. let _ = ctrlc::set_handler({ @@ -585,7 +628,6 @@ pub(crate) async fn add( &venv, state, locked, - frozen, no_sync, &dependency_type, raw_sources, @@ -642,11 +684,10 @@ pub(crate) async fn add( async fn lock_and_sync( mut project: VirtualProject, toml: &mut PyProjectTomlMut, - edits: &[DependencyEdit<'_>], + edits: &[DependencyEdit], venv: &PythonEnvironment, state: SharedState, locked: bool, - frozen: bool, no_sync: bool, dependency_type: &DependencyType, raw_sources: bool, @@ -658,11 +699,15 @@ async fn lock_and_sync( cache: &Cache, printer: Printer, ) -> Result<(), ProjectError> { + let mode = if locked { + LockMode::Locked(venv.interpreter()) + } else { + LockMode::Write(venv.interpreter()) + }; + let mut lock = project::lock::do_safe_lock( - locked, - frozen, + mode, project.workspace(), - venv.interpreter(), settings.into(), bounds, &state, @@ -739,8 +784,11 @@ async fn lock_and_sync( DependencyType::Dev => { toml.set_dev_dependency_minimum_version(*index, minimum)?; } - DependencyType::Optional(ref group) => { - toml.set_optional_dependency_minimum_version(group, *index, minimum)?; + DependencyType::Optional(ref extra) => { + toml.set_optional_dependency_minimum_version(extra, *index, minimum)?; + } + DependencyType::Group(ref group) => { + toml.set_dependency_group_requirement_minimum_version(group, *index, minimum)?; } } @@ -758,8 +806,10 @@ async fn lock_and_sync( // Update the `pypackage.toml` in-memory. project = project - .with_pyproject_toml(toml::from_str(&content).map_err(ProjectError::TomlParse)?) - .ok_or(ProjectError::TomlUpdate)?; + .with_pyproject_toml( + toml::from_str(&content).map_err(ProjectError::PyprojectTomlParse)?, + ) + .ok_or(ProjectError::PyprojectTomlUpdate)?; // Invalidate the project metadata. if let VirtualProject::Project(ref project) = project { @@ -773,10 +823,8 @@ async fn lock_and_sync( // If the file was modified, we have to lock again, though the only expected change is // the addition of the minimum version specifiers. lock = project::lock::do_safe_lock( - locked, - frozen, + mode, project.workspace(), - venv.interpreter(), settings.into(), bounds, &state, @@ -800,17 +848,23 @@ async fn lock_and_sync( let (extras, dev) = match dependency_type { DependencyType::Production => { let extras = ExtrasSpecification::None; - let dev = DevMode::Exclude; + let dev = DevGroupsSpecification::from(DevMode::Exclude); (extras, dev) } DependencyType::Dev => { let extras = ExtrasSpecification::None; - let dev = DevMode::Include; + let dev = DevGroupsSpecification::from(DevMode::Include); (extras, dev) } - DependencyType::Optional(ref group_name) => { - let extras = ExtrasSpecification::Some(vec![group_name.clone()]); - let dev = DevMode::Exclude; + DependencyType::Optional(ref extra_name) => { + let extras = ExtrasSpecification::Some(vec![extra_name.clone()]); + let dev = DevGroupsSpecification::from(DevMode::Exclude); + (extras, dev) + } + DependencyType::Group(ref group_name) => { + let extras = ExtrasSpecification::None; + let dev = + DevGroupsSpecification::from(GroupsSpecification::from_group(group_name.clone())); (extras, dev) } }; @@ -820,7 +874,7 @@ async fn lock_and_sync( venv, &lock, &extras, - dev, + &DevGroupsManifest::from_spec(dev), EditableMode::Editable, InstallOptions::default(), Modifications::Sufficient, @@ -910,7 +964,7 @@ fn resolve_requirement( requirement: uv_pypi_types::Requirement, workspace: bool, editable: Option, - index: Option, + index: Option, rev: Option, tag: Option, branch: Option, @@ -966,8 +1020,8 @@ impl Target { } #[derive(Debug, Clone)] -struct DependencyEdit<'a> { - dependency_type: &'a DependencyType, +struct DependencyEdit { + dependency_type: DependencyType, requirement: Requirement, source: Option, edit: ArrayEdit, diff --git a/crates/uv/src/commands/project/export.rs b/crates/uv/src/commands/project/export.rs index 4cbccb67ed5d..20fca2bce6da 100644 --- a/crates/uv/src/commands/project/export.rs +++ b/crates/uv/src/commands/project/export.rs @@ -8,17 +8,19 @@ use std::path::{Path, PathBuf}; use uv_cache::Cache; use uv_client::Connectivity; use uv_configuration::{ - Concurrency, DevMode, DevSpecification, EditableMode, ExportFormat, ExtrasSpecification, + Concurrency, DevGroupsSpecification, EditableMode, ExportFormat, ExtrasSpecification, InstallOptions, LowerBound, }; -use uv_normalize::{PackageName, DEV_DEPENDENCIES}; +use uv_normalize::PackageName; use uv_python::{PythonDownloads, PythonPreference, PythonRequest}; use uv_resolver::RequirementsTxtExport; use uv_workspace::{DiscoveryOptions, MemberDiscovery, VirtualProject, Workspace}; use crate::commands::pip::loggers::DefaultResolveLogger; -use crate::commands::project::lock::do_safe_lock; -use crate::commands::project::{ProjectError, ProjectInterpreter}; +use crate::commands::project::lock::{do_safe_lock, LockMode}; +use crate::commands::project::{ + default_dependency_groups, validate_dependency_groups, ProjectError, ProjectInterpreter, +}; use crate::commands::{diagnostics, pip, ExitStatus, OutputWriter, SharedState}; use crate::printer::Printer; use crate::settings::ResolverSettings; @@ -33,7 +35,7 @@ pub(crate) async fn export( install_options: InstallOptions, output_file: Option, extras: ExtrasSpecification, - dev: DevMode, + dev: DevGroupsSpecification, editable: EditableMode, locked: bool, frozen: bool, @@ -70,33 +72,47 @@ pub(crate) async fn export( VirtualProject::discover(project_dir, &DiscoveryOptions::default()).await? }; + // Determine the default groups to include. + validate_dependency_groups(&project, &dev)?; + let defaults = default_dependency_groups(project.pyproject_toml())?; + let VirtualProject::Project(project) = project else { return Err(anyhow::anyhow!("Legacy non-project roots are not supported in `uv export`; add a `[project]` table to your `pyproject.toml` to enable exports")); }; - // Find an interpreter for the project - let interpreter = ProjectInterpreter::discover( - project.workspace(), - python.as_deref().map(PythonRequest::parse), - python_preference, - python_downloads, - connectivity, - native_tls, - cache, - printer, - ) - .await? - .into_interpreter(); + // Determine the lock mode. + let interpreter; + let mode = if frozen { + LockMode::Frozen + } else { + // Find an interpreter for the project + interpreter = ProjectInterpreter::discover( + project.workspace(), + python.as_deref().map(PythonRequest::parse), + python_preference, + python_downloads, + connectivity, + native_tls, + cache, + printer, + ) + .await? + .into_interpreter(); + + if locked { + LockMode::Locked(&interpreter) + } else { + LockMode::Write(&interpreter) + } + }; // Initialize any shared state. let state = SharedState::default(); // Lock the project. let lock = match do_safe_lock( - locked, - frozen, + mode, project.workspace(), - &interpreter, settings.as_ref(), LowerBound::Warn, &state, @@ -131,13 +147,6 @@ pub(crate) async fn export( Err(err) => return Err(err.into()), }; - // Include development dependencies, if requested. - let dev = match dev { - DevMode::Include => DevSpecification::Include(std::slice::from_ref(&DEV_DEPENDENCIES)), - DevMode::Exclude => DevSpecification::Exclude, - DevMode::Only => DevSpecification::Only(std::slice::from_ref(&DEV_DEPENDENCIES)), - }; - // Write the resolved dependencies to the output channel. let mut writer = OutputWriter::new(!quiet || output_file.is_none(), output_file.as_deref()); @@ -148,7 +157,7 @@ pub(crate) async fn export( &lock, project.project_name(), &extras, - dev, + &dev.with_defaults(defaults), editable, hashes, &install_options, diff --git a/crates/uv/src/commands/project/init.rs b/crates/uv/src/commands/project/init.rs index 4814609a9a9f..f0144b0706d1 100644 --- a/crates/uv/src/commands/project/init.rs +++ b/crates/uv/src/commands/project/init.rs @@ -362,7 +362,7 @@ async fn init_project( } ref python_request @ PythonRequest::Version(VersionRequest::Range(ref specifiers, _)) => { - let requires_python = RequiresPython::from_specifiers(specifiers)?; + let requires_python = RequiresPython::from_specifiers(specifiers); let python_request = if no_pin_python { None @@ -417,10 +417,7 @@ async fn init_project( (requires_python, python_request) } } - } else if let Some(requires_python) = workspace - .as_ref() - .and_then(|workspace| find_requires_python(workspace).ok().flatten()) - { + } else if let Some(requires_python) = workspace.as_ref().and_then(find_requires_python) { // (2) `Requires-Python` from the workspace let python_request = PythonRequest::Version(VersionRequest::Range( requires_python.specifiers().clone(), @@ -975,7 +972,7 @@ fn generate_package_scripts( #[pyfunction] fn hello_from_bin() -> String {{ - return "Hello from {package}!".to_string(); + "Hello from {package}!".to_string() }} /// A Python module implemented in Rust. The name of this function must match diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs index c80bd6726fec..e6eadcbd51b7 100644 --- a/crates/uv/src/commands/project/lock.rs +++ b/crates/uv/src/commands/project/lock.rs @@ -4,7 +4,6 @@ use std::collections::BTreeSet; use std::fmt::Write; use std::path::Path; -use anstream::eprint; use owo_colors::OwoColorize; use rustc_hash::{FxBuildHasher, FxHashMap}; use tracing::debug; @@ -21,15 +20,15 @@ use uv_distribution_types::{ UnresolvedRequirementSpecification, }; use uv_git::ResolvedRepositoryReference; -use uv_normalize::{PackageName, DEV_DEPENDENCIES}; +use uv_normalize::PackageName; use uv_pep440::Version; use uv_pypi_types::{Requirement, SupportedEnvironments}; use uv_python::{Interpreter, PythonDownloads, PythonEnvironment, PythonPreference, PythonRequest}; use uv_requirements::upgrade::{read_lock_requirements, LockedRequirements}; use uv_requirements::ExtrasResolver; use uv_resolver::{ - FlatIndex, InMemoryIndex, Lock, Options, OptionsBuilder, PythonRequirement, RequiresPython, - ResolverManifest, ResolverMarkers, SatisfiesResult, + FlatIndex, InMemoryIndex, Lock, LockVersion, Options, OptionsBuilder, PythonRequirement, + RequiresPython, ResolverManifest, ResolverMarkers, SatisfiesResult, VERSION, }; use uv_types::{BuildContext, BuildIsolation, EmptyInstalledPackages, HashStrategy}; use uv_warnings::{warn_user, warn_user_once}; @@ -70,10 +69,12 @@ impl LockResult { } /// Resolve the project requirements into a lockfile. +#[allow(clippy::fn_params_excessive_bools)] pub(crate) async fn lock( project_dir: &Path, locked: bool, frozen: bool, + dry_run: bool, python: Option, settings: ResolverSettings, python_preference: PythonPreference, @@ -87,29 +88,41 @@ pub(crate) async fn lock( // Find the project requirements. let workspace = Workspace::discover(project_dir, &DiscoveryOptions::default()).await?; - // Find an interpreter for the project - let interpreter = ProjectInterpreter::discover( - &workspace, - python.as_deref().map(PythonRequest::parse), - python_preference, - python_downloads, - connectivity, - native_tls, - cache, - printer, - ) - .await? - .into_interpreter(); + // Determine the lock mode. + let interpreter; + let mode = if frozen { + LockMode::Frozen + } else { + // Find an interpreter for the project + interpreter = ProjectInterpreter::discover( + &workspace, + python.as_deref().map(PythonRequest::parse), + python_preference, + python_downloads, + connectivity, + native_tls, + cache, + printer, + ) + .await? + .into_interpreter(); + + if locked { + LockMode::Locked(&interpreter) + } else if dry_run { + LockMode::DryRun(&interpreter) + } else { + LockMode::Write(&interpreter) + } + }; // Initialize any shared state. let state = SharedState::default(); // Perform the lock operation. match do_safe_lock( - locked, - frozen, + mode, &workspace, - &interpreter, settings.as_ref(), LowerBound::Warn, &state, @@ -123,9 +136,25 @@ pub(crate) async fn lock( .await { Ok(lock) => { - if let LockResult::Changed(Some(previous), lock) = &lock { - report_upgrades(previous, lock, printer)?; + if dry_run { + let changed = if let LockResult::Changed(previous, lock) = &lock { + report_upgrades(previous.as_ref(), lock, printer, dry_run)? + } else { + false + }; + if !changed { + writeln!( + printer.stderr(), + "{}", + "No lockfile changes detected".bold() + )?; + } + } else { + if let LockResult::Changed(Some(previous), lock) = &lock { + report_upgrades(Some(previous), lock, printer, dry_run)?; + } } + Ok(ExitStatus::Success) } Err(ProjectError::Operation(pip::operations::Error::Resolve( @@ -151,12 +180,23 @@ pub(crate) async fn lock( } } +#[derive(Debug, Clone, Copy)] +pub(super) enum LockMode<'env> { + /// Write the lockfile to disk. + Write(&'env Interpreter), + /// Perform a resolution, but don't write the lockfile to disk. + DryRun(&'env Interpreter), + /// Error if the lockfile is not up-to-date with the project requirements. + Locked(&'env Interpreter), + /// Use the existing lockfile without performing a resolution. + Frozen, +} + /// Perform a lock operation, respecting the `--locked` and `--frozen` parameters. +#[allow(clippy::fn_params_excessive_bools)] pub(super) async fn do_safe_lock( - locked: bool, - frozen: bool, + mode: LockMode<'_>, workspace: &Workspace, - interpreter: &Interpreter, settings: ResolverSettingsRef<'_>, bounds: LowerBound, state: &SharedState, @@ -167,68 +207,84 @@ pub(super) async fn do_safe_lock( cache: &Cache, printer: Printer, ) -> Result { - if frozen { - // Read the existing lockfile, but don't attempt to lock the project. - let existing = read(workspace) - .await? - .ok_or_else(|| ProjectError::MissingLockfile)?; - Ok(LockResult::Unchanged(existing)) - } else if locked { - // Read the existing lockfile. - let existing = read(workspace) - .await? - .ok_or_else(|| ProjectError::MissingLockfile)?; + match mode { + LockMode::Frozen => { + // Read the existing lockfile, but don't attempt to lock the project. + let existing = read(workspace) + .await? + .ok_or_else(|| ProjectError::MissingLockfile)?; + Ok(LockResult::Unchanged(existing)) + } + LockMode::Locked(interpreter) => { + // Read the existing lockfile. + let existing = read(workspace) + .await? + .ok_or_else(|| ProjectError::MissingLockfile)?; + + // Perform the lock operation, but don't write the lockfile to disk. + let result = do_lock( + workspace, + interpreter, + Some(existing), + settings, + bounds, + state, + logger, + connectivity, + concurrency, + native_tls, + cache, + printer, + ) + .await?; - // Perform the lock operation, but don't write the lockfile to disk. - let result = do_lock( - workspace, - interpreter, - Some(existing), - settings, - bounds, - state, - logger, - connectivity, - concurrency, - native_tls, - cache, - printer, - ) - .await?; + // If the lockfile changed, return an error. + if matches!(result, LockResult::Changed(_, _)) { + return Err(ProjectError::LockMismatch); + } - // If the lockfile changed, return an error. - if matches!(result, LockResult::Changed(_, _)) { - return Err(ProjectError::LockMismatch); + Ok(result) } + LockMode::Write(interpreter) | LockMode::DryRun(interpreter) => { + // Read the existing lockfile. + let existing = match read(workspace).await { + Ok(Some(existing)) => Some(existing), + Ok(None) => None, + Err(ProjectError::Lock(err)) => { + warn_user!( + "Failed to read existing lockfile; ignoring locked requirements: {err}" + ); + None + } + Err(err) => return Err(err), + }; - Ok(result) - } else { - // Read the existing lockfile. - let existing = read(workspace).await?; + // Perform the lock operation. + let result = do_lock( + workspace, + interpreter, + existing, + settings, + bounds, + state, + logger, + connectivity, + concurrency, + native_tls, + cache, + printer, + ) + .await?; - // Perform the lock operation. - let result = do_lock( - workspace, - interpreter, - existing, - settings, - bounds, - state, - logger, - connectivity, - concurrency, - native_tls, - cache, - printer, - ) - .await?; + // If the lockfile changed, write it to disk. + if !matches!(mode, LockMode::DryRun(_)) { + if let LockResult::Changed(_, lock) = &result { + commit(lock, workspace).await?; + } + } - // If the lockfile changed, write it to disk. - if let LockResult::Changed(_, lock) = &result { - commit(lock, workspace).await?; + Ok(result) } - - Ok(result) } } @@ -269,10 +325,10 @@ async fn do_lock( } = settings; // Collect the requirements, etc. - let requirements = workspace.non_project_requirements().collect::>(); + let requirements = workspace.non_project_requirements()?; let overrides = workspace.overrides().into_iter().collect::>(); let constraints = workspace.constraints(); - let dev = vec![DEV_DEPENDENCIES.clone()]; + let dev = workspace.groups().into_iter().cloned().collect::>(); let source_trees = vec![]; // Collect the list of members. @@ -328,7 +384,7 @@ async fn do_lock( // Determine the supported Python range. If no range is defined, and warn and default to the // current minor version. - let requires_python = find_requires_python(workspace)?; + let requires_python = find_requires_python(workspace); let requires_python = if let Some(requires_python) = requires_python { if requires_python.is_unbounded() { @@ -869,7 +925,7 @@ impl ValidatedLock { ); Ok(Self::Preferable(lock)) } - SatisfiesResult::MismatchedDevDependencies(name, version, expected, actual) => { + SatisfiesResult::MismatchedDependencyGroups(name, version, expected, actual) => { debug!( "Ignoring existing lockfile due to mismatched dev dependencies for: `{name}=={version}`\n Expected: {:?}\n Actual: {:?}", expected, actual @@ -903,30 +959,62 @@ async fn commit(lock: &Lock, workspace: &Workspace) -> Result<(), ProjectError> /// Returns `Ok(None)` if the lockfile does not exist. pub(crate) async fn read(workspace: &Workspace) -> Result, ProjectError> { match fs_err::tokio::read_to_string(&workspace.install_path().join("uv.lock")).await { - Ok(encoded) => match toml::from_str(&encoded) { - Ok(lock) => Ok(Some(lock)), - Err(err) => { - eprint!("Failed to parse lockfile; ignoring locked requirements: {err}"); - Ok(None) + Ok(encoded) => { + match toml::from_str::(&encoded) { + Ok(lock) => { + // If the lockfile uses an unsupported version, raise an error. + if lock.version() != VERSION { + return Err(ProjectError::UnsupportedLockVersion( + VERSION, + lock.version(), + )); + } + Ok(Some(lock)) + } + Err(err) => { + // If we failed to parse the lockfile, determine whether it's a supported + // version. + if let Ok(lock) = toml::from_str::(&encoded) { + if lock.version() != VERSION { + return Err(ProjectError::UnparsableLockVersion( + VERSION, + lock.version(), + err, + )); + } + } + Err(ProjectError::UvLockParse(err)) + } } - }, + } Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None), Err(err) => Err(err.into()), } } /// Reports on the versions that were upgraded in the new lockfile. -fn report_upgrades(existing_lock: &Lock, new_lock: &Lock, printer: Printer) -> anyhow::Result<()> { +/// +/// Returns `true` if any upgrades were reported. +fn report_upgrades( + existing_lock: Option<&Lock>, + new_lock: &Lock, + printer: Printer, + dry_run: bool, +) -> anyhow::Result { let existing_packages: FxHashMap<&PackageName, BTreeSet<&Version>> = - existing_lock.packages().iter().fold( - FxHashMap::with_capacity_and_hasher(existing_lock.packages().len(), FxBuildHasher), - |mut acc, package| { - acc.entry(package.name()) - .or_default() - .insert(package.version()); - acc - }, - ); + if let Some(existing_lock) = existing_lock { + existing_lock.packages().iter().fold( + FxHashMap::with_capacity_and_hasher(existing_lock.packages().len(), FxBuildHasher), + |mut acc, package| { + acc.entry(package.name()) + .or_default() + .insert(package.version()); + acc + }, + ) + } else { + FxHashMap::default() + }; let new_distributions: FxHashMap<&PackageName, BTreeSet<&Version>> = new_lock.packages().iter().fold( @@ -939,11 +1027,13 @@ fn report_upgrades(existing_lock: &Lock, new_lock: &Lock, printer: Printer) -> a }, ); + let mut updated = false; for name in existing_packages .keys() .chain(new_distributions.keys()) .collect::>() { + updated = true; match (existing_packages.get(name), new_distributions.get(name)) { (Some(existing_versions), Some(new_versions)) => { if existing_versions != new_versions { @@ -960,7 +1050,7 @@ fn report_upgrades(existing_lock: &Lock, new_lock: &Lock, printer: Printer) -> a writeln!( printer.stderr(), "{} {name} {existing_versions} -> {new_versions}", - "Updated".green().bold() + if dry_run { "Update" } else { "Updated" }.green().bold() )?; } } @@ -973,7 +1063,7 @@ fn report_upgrades(existing_lock: &Lock, new_lock: &Lock, printer: Printer) -> a writeln!( printer.stderr(), "{} {name} {existing_versions}", - "Removed".red().bold() + if dry_run { "Remove" } else { "Removed" }.red().bold() )?; } (None, Some(new_versions)) => { @@ -985,7 +1075,7 @@ fn report_upgrades(existing_lock: &Lock, new_lock: &Lock, printer: Printer) -> a writeln!( printer.stderr(), "{} {name} {new_versions}", - "Added".green().bold() + if dry_run { "Add" } else { "Added" }.green().bold() )?; } (None, None) => { @@ -994,5 +1084,5 @@ fn report_upgrades(existing_lock: &Lock, new_lock: &Lock, printer: Printer) -> a } } - Ok(()) + Ok(updated) } diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index ed6fd4a01ab5..d11320c1ac0b 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -8,7 +8,8 @@ use tracing::debug; use uv_cache::Cache; use uv_client::{BaseClientBuilder, Connectivity, FlatIndexClient, RegistryClientBuilder}; use uv_configuration::{ - Concurrency, Constraints, ExtrasSpecification, LowerBound, Reinstall, Upgrade, + Concurrency, Constraints, DevGroupsSpecification, ExtrasSpecification, GroupsSpecification, + LowerBound, Reinstall, Upgrade, }; use uv_dispatch::BuildDispatch; use uv_distribution::DistributionDatabase; @@ -18,7 +19,7 @@ use uv_distribution_types::{ use uv_fs::Simplified; use uv_git::ResolvedRepositoryReference; use uv_installer::{SatisfiesResult, SitePackages}; -use uv_normalize::PackageName; +use uv_normalize::{GroupName, PackageName, DEV_DEPENDENCIES}; use uv_pep440::{Version, VersionSpecifiers}; use uv_pep508::MarkerTreeContents; use uv_pypi_types::Requirement; @@ -35,7 +36,9 @@ use uv_resolver::{ }; use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy}; use uv_warnings::{warn_user, warn_user_once}; -use uv_workspace::Workspace; +use uv_workspace::dependency_groups::DependencyGroupError; +use uv_workspace::pyproject::PyProjectToml; +use uv_workspace::{VirtualProject, Workspace}; use crate::commands::pip::loggers::{InstallLogger, ResolveLogger}; use crate::commands::pip::operations::{Changelog, Modifications}; @@ -64,6 +67,12 @@ pub(crate) enum ProjectError { )] MissingLockfile, + #[error("The lockfile at `uv.lock` uses an unsupported schema version (v{1}, but only v{0} is supported). Downgrade to a compatible uv version, or remove the `uv.lock` prior to running `uv lock` or `uv sync`.")] + UnsupportedLockVersion(u32, u32), + + #[error("Failed to parse `uv.lock`, which uses an unsupported schema version (v{1}, but only v{0} is supported). Downgrade to a compatible uv version, or remove the `uv.lock` prior to running `uv lock` or `uv sync`.")] + UnparsableLockVersion(u32, u32, #[source] toml::de::Error), + #[error("The current Python version ({0}) is not compatible with the locked Python requirement: `{1}`")] LockedPythonIncompatibility(Version, RequiresPython), @@ -116,6 +125,15 @@ pub(crate) enum ProjectError { PathBuf, ), + #[error("Group `{0}` is not defined in the project's `dependency-group` table")] + MissingGroupProject(GroupName), + + #[error("Group `{0}` is not defined in any project's `dependency-group` table")] + MissingGroupWorkspace(GroupName), + + #[error("Default group `{0}` (from `tool.uv.default-groups`) is not defined in the project's `dependency-group` table")] + MissingDefaultGroup(GroupName), + #[error("Supported environments must be disjoint, but the following markers overlap: `{0}` and `{1}`.\n\n{hint}{colon} replace `{1}` with `{2}`.", hint = "hint".bold().cyan(), colon = ":".bold())] OverlappingMarkers(String, String, String), @@ -128,11 +146,17 @@ pub(crate) enum ProjectError { #[error("Project virtual environment directory `{0}` cannot be used because {1}")] InvalidProjectEnvironmentDir(PathBuf, String), + #[error("Failed to parse `uv.lock`")] + UvLockParse(#[source] toml::de::Error), + #[error("Failed to parse `pyproject.toml`")] - TomlParse(#[source] toml::de::Error), + PyprojectTomlParse(#[source] toml::de::Error), #[error("Failed to update `pyproject.toml`")] - TomlUpdate, + PyprojectTomlUpdate, + + #[error(transparent)] + DependencyGroup(#[from] DependencyGroupError), #[error(transparent)] Python(#[from] uv_python::Error), @@ -155,9 +179,6 @@ pub(crate) enum ProjectError { #[error(transparent)] Operation(#[from] pip::operations::Error), - #[error(transparent)] - RequiresPython(#[from] uv_resolver::RequiresPythonError), - #[error(transparent)] Interpreter(#[from] uv_python::InterpreterError), @@ -187,9 +208,7 @@ pub(crate) enum ProjectError { /// /// For a [`Workspace`] with multiple packages, the `Requires-Python` bound is the union of the /// `Requires-Python` bounds of all the packages. -pub(crate) fn find_requires_python( - workspace: &Workspace, -) -> Result, uv_resolver::RequiresPythonError> { +pub(crate) fn find_requires_python(workspace: &Workspace) -> Option { RequiresPython::intersection(workspace.packages().values().filter_map(|member| { member .pyproject_toml() @@ -320,7 +339,7 @@ impl WorkspacePython { python_request: Option, workspace: &Workspace, ) -> Result { - let requires_python = find_requires_python(workspace)?; + let requires_python = find_requires_python(workspace); let (source, python_request) = if let Some(request) = python_request { // (1) Explicit request from user @@ -418,7 +437,7 @@ impl ProjectInterpreter { if fs_err::read_dir(&venv).is_ok_and(|mut dir| dir.next().is_some()) { return Err(ProjectError::InvalidProjectEnvironmentDir( venv, - "because it is not a valid Python environment (no Python executable was found)" + "it is not a valid Python environment (no Python executable was found)" .to_string(), )); } @@ -1347,6 +1366,79 @@ pub(crate) async fn script_python_requirement( )) } +/// Validate the dependency groups requested by the [`DevGroupsSpecification`]. +#[allow(clippy::result_large_err)] +pub(crate) fn validate_dependency_groups( + project: &VirtualProject, + dev: &DevGroupsSpecification, +) -> Result<(), ProjectError> { + for group in dev + .groups() + .into_iter() + .flat_map(GroupsSpecification::names) + { + match project { + VirtualProject::Project(project) => { + // The group must be defined in the target project. + if !project + .current_project() + .pyproject_toml() + .dependency_groups + .as_ref() + .is_some_and(|groups| groups.contains_key(group)) + { + return Err(ProjectError::MissingGroupProject(group.clone())); + } + } + VirtualProject::NonProject(workspace) => { + // The group must be defined in at least one workspace package. + if !workspace + .pyproject_toml() + .dependency_groups + .as_ref() + .is_some_and(|groups| groups.contains_key(group)) + { + if workspace.packages().values().all(|package| { + !package + .pyproject_toml() + .dependency_groups + .as_ref() + .is_some_and(|groups| groups.contains_key(group)) + }) { + return Err(ProjectError::MissingGroupWorkspace(group.clone())); + } + } + } + } + } + Ok(()) +} + +/// Returns the default dependency groups from the [`PyProjectToml`]. +#[allow(clippy::result_large_err)] +pub(crate) fn default_dependency_groups( + pyproject_toml: &PyProjectToml, +) -> Result, ProjectError> { + if let Some(defaults) = pyproject_toml + .tool + .as_ref() + .and_then(|tool| tool.uv.as_ref().and_then(|uv| uv.default_groups.as_ref())) + { + for group in defaults { + if !pyproject_toml + .dependency_groups + .as_ref() + .is_some_and(|groups| groups.contains_key(group)) + { + return Err(ProjectError::MissingDefaultGroup(group.clone())); + } + } + Ok(defaults.clone()) + } else { + Ok(vec![DEV_DEPENDENCIES.clone()]) + } +} + /// Warn if the user provides (e.g.) an `--index-url` in a requirements file. fn warn_on_requirements_txt_setting( spec: &RequirementsSpecification, diff --git a/crates/uv/src/commands/project/remove.rs b/crates/uv/src/commands/project/remove.rs index d5671388e57d..2c098b4a2ec2 100644 --- a/crates/uv/src/commands/project/remove.rs +++ b/crates/uv/src/commands/project/remove.rs @@ -6,9 +6,10 @@ use owo_colors::OwoColorize; use uv_cache::Cache; use uv_client::Connectivity; use uv_configuration::{ - Concurrency, DevMode, EditableMode, ExtrasSpecification, InstallOptions, LowerBound, + Concurrency, DevGroupsManifest, EditableMode, ExtrasSpecification, InstallOptions, LowerBound, }; use uv_fs::Simplified; +use uv_normalize::DEV_DEPENDENCIES; use uv_pep508::PackageName; use uv_python::{PythonDownloads, PythonPreference, PythonRequest}; use uv_scripts::Pep723Script; @@ -19,6 +20,8 @@ use uv_workspace::{DiscoveryOptions, InstallTarget, VirtualProject, Workspace}; use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger}; use crate::commands::pip::operations::Modifications; +use crate::commands::project::default_dependency_groups; +use crate::commands::project::lock::LockMode; use crate::commands::{project, ExitStatus, SharedState}; use crate::printer::Printer; use crate::settings::ResolverInstallerSettings; @@ -105,16 +108,18 @@ pub(crate) async fn remove( } } DependencyType::Dev => { - let deps = toml.remove_dev_dependency(&package)?; - if deps.is_empty() { + let dev_deps = toml.remove_dev_dependency(&package)?; + let group_deps = + toml.remove_dependency_group_requirement(&package, &DEV_DEPENDENCIES)?; + if dev_deps.is_empty() && group_deps.is_empty() { warn_if_present(&package, &toml); anyhow::bail!( - "The dependency `{package}` could not be found in `dev-dependencies`" + "The dependency `{package}` could not be found in `dev-dependencies` or `dependency-groups.dev`" ); } } - DependencyType::Optional(ref group) => { - let deps = toml.remove_optional_dependency(&package, group)?; + DependencyType::Optional(ref extra) => { + let deps = toml.remove_optional_dependency(&package, extra)?; if deps.is_empty() { warn_if_present(&package, &toml); anyhow::bail!( @@ -122,6 +127,27 @@ pub(crate) async fn remove( ); } } + DependencyType::Group(ref group) => { + if group == &*DEV_DEPENDENCIES { + let dev_deps = toml.remove_dev_dependency(&package)?; + let group_deps = + toml.remove_dependency_group_requirement(&package, &DEV_DEPENDENCIES)?; + if dev_deps.is_empty() && group_deps.is_empty() { + warn_if_present(&package, &toml); + anyhow::bail!( + "The dependency `{package}` could not be found in `dev-dependencies` or `dependency-groups.dev`" + ); + } + } else { + let deps = toml.remove_dependency_group_requirement(&package, group)?; + if deps.is_empty() { + warn_if_present(&package, &toml); + anyhow::bail!( + "The dependency `{package}` could not be found in `dependency-groups`" + ); + } + } + } } } @@ -168,15 +194,22 @@ pub(crate) async fn remove( ) .await?; + // Determine the lock mode. + let mode = if frozen { + LockMode::Frozen + } else if locked { + LockMode::Locked(venv.interpreter()) + } else { + LockMode::Write(venv.interpreter()) + }; + // Initialize any shared state. let state = SharedState::default(); // Lock and sync the environment, if necessary. let lock = project::lock::do_safe_lock( - locked, - frozen, + mode, project.workspace(), - venv.interpreter(), settings.as_ref().into(), LowerBound::Allow, &state, @@ -196,16 +229,18 @@ pub(crate) async fn remove( // Perform a full sync, because we don't know what exactly is affected by the removal. // TODO(ibraheem): Should we accept CLI overrides for this? Should we even sync here? - let dev = DevMode::Include; let extras = ExtrasSpecification::All; let install_options = InstallOptions::default(); + // Determine the default groups to include. + let defaults = default_dependency_groups(project.pyproject_toml())?; + project::sync::do_sync( InstallTarget::from(&project), &venv, &lock, &extras, - dev, + &DevGroupsManifest::from_defaults(defaults), EditableMode::Editable, install_options, Modifications::Exact, @@ -249,6 +284,11 @@ fn warn_if_present(name: &PackageName, pyproject: &PyProjectTomlMut) { "`{name}` is an optional dependency; try calling `uv remove --optional {group}`", ); } + DependencyType::Group(group) => { + warn_user!( + "`{name}` is in the `{group}` group; try calling `uv remove --group {group}`", + ); + } } } } diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index ed94cc20a8d7..6f20a4df1621 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -17,8 +17,8 @@ use uv_cache::Cache; use uv_cli::ExternalCommand; use uv_client::{BaseClientBuilder, Connectivity}; use uv_configuration::{ - Concurrency, DevMode, EditableMode, ExtrasSpecification, InstallOptions, LowerBound, - SourceStrategy, + Concurrency, DevGroupsSpecification, EditableMode, ExtrasSpecification, GroupsSpecification, + InstallOptions, LowerBound, SourceStrategy, }; use uv_distribution::LoweredRequirement; use uv_fs::which::is_executable; @@ -43,9 +43,10 @@ use crate::commands::pip::loggers::{ use crate::commands::pip::operations; use crate::commands::pip::operations::Modifications; use crate::commands::project::environment::CachedEnvironment; +use crate::commands::project::lock::LockMode; use crate::commands::project::{ - validate_requires_python, EnvironmentSpecification, ProjectError, PythonRequestSource, - WorkspacePython, + default_dependency_groups, validate_dependency_groups, validate_requires_python, + EnvironmentSpecification, ProjectError, PythonRequestSource, WorkspacePython, }; use crate::commands::reporters::PythonDownloadReporter; use crate::commands::{diagnostics, project, ExitStatus, SharedState}; @@ -68,7 +69,7 @@ pub(crate) async fn run( no_project: bool, no_config: bool, extras: ExtrasSpecification, - dev: DevMode, + dev: DevGroupsSpecification, editable: EditableMode, python: Option, settings: ResolverInstallerSettings, @@ -336,11 +337,14 @@ pub(crate) async fn run( if !extras.is_empty() { warn_user!("Extras are not supported for Python scripts with inline metadata"); } - if matches!(dev, DevMode::Exclude) { - warn_user!("`--no-dev` is not supported for Python scripts with inline metadata"); + if let Some(dev_mode) = dev.dev_mode() { + warn_user!( + "`{}` is not supported for Python scripts with inline metadata", + dev_mode.as_flag() + ); } - if matches!(dev, DevMode::Only) { - warn_user!("`--only-dev` is not supported for Python scripts with inline metadata"); + if let Some(flag) = dev.groups().and_then(GroupsSpecification::as_flag) { + warn_user!("`{flag}` is not supported for Python scripts with inline metadata"); } if package.is_some() { warn_user!( @@ -413,11 +417,14 @@ pub(crate) async fn run( if !extras.is_empty() { warn_user!("Extras have no effect when used alongside `--no-project`"); } - if matches!(dev, DevMode::Exclude) { - warn_user!("`--no-dev` has no effect when used alongside `--no-project`"); + if let Some(dev_mode) = dev.dev_mode() { + warn_user!( + "`{}` has no effect when used alongside `--no-project`", + dev_mode.as_flag() + ); } - if matches!(dev, DevMode::Only) { - warn_user!("`--only-dev` has no effect when used alongside `--no-project`"); + if let Some(flag) = dev.groups().and_then(GroupsSpecification::as_flag) { + warn_user!("`{flag}` has no effect when used alongside `--no-project`"); } if locked { warn_user!("`--locked` has no effect when used alongside `--no-project`"); @@ -433,11 +440,14 @@ pub(crate) async fn run( if !extras.is_empty() { warn_user!("Extras have no effect when used outside of a project"); } - if matches!(dev, DevMode::Exclude) { - warn_user!("`--no-dev` has no effect when used outside of a project"); + if let Some(dev_mode) = dev.dev_mode() { + warn_user!( + "`{}` has no effect when used outside of a project", + dev_mode.as_flag() + ); } - if matches!(dev, DevMode::Only) { - warn_user!("`--only-dev` has no effect when used outside of a project"); + if let Some(flag) = dev.groups().and_then(GroupsSpecification::as_flag) { + warn_user!("`{flag}` has no effect when used outside of a project"); } if locked { warn_user!("`--locked` has no effect when used outside of a project"); @@ -540,11 +550,22 @@ pub(crate) async fn run( .flatten(); } } else { + // Determine the default groups to include. + validate_dependency_groups(&project, &dev)?; + let defaults = default_dependency_groups(project.pyproject_toml())?; + + // Determine the lock mode. + let mode = if frozen { + LockMode::Frozen + } else if locked { + LockMode::Locked(venv.interpreter()) + } else { + LockMode::Write(venv.interpreter()) + }; + let result = match project::lock::do_safe_lock( - locked, - frozen, + mode, project.workspace(), - venv.interpreter(), settings.as_ref().into(), LowerBound::Allow, &state, @@ -590,7 +611,7 @@ pub(crate) async fn run( &venv, result.lock(), &extras, - dev, + &dev.with_defaults(defaults), editable, install_options, Modifications::Sufficient, diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index 52bee272bcbd..7070db979936 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -8,13 +8,13 @@ use uv_auth::store_credentials; use uv_cache::Cache; use uv_client::{Connectivity, FlatIndexClient, RegistryClientBuilder}; use uv_configuration::{ - Concurrency, Constraints, DevMode, DevSpecification, EditableMode, ExtrasSpecification, - HashCheckingMode, InstallOptions, LowerBound, + Concurrency, Constraints, DevGroupsManifest, DevGroupsSpecification, EditableMode, + ExtrasSpecification, HashCheckingMode, InstallOptions, LowerBound, }; use uv_dispatch::BuildDispatch; use uv_distribution_types::{DirectorySourceDist, Dist, Index, ResolvedDist, SourceDist}; use uv_installer::SitePackages; -use uv_normalize::{PackageName, DEV_DEPENDENCIES}; +use uv_normalize::PackageName; use uv_pep508::{MarkerTree, Requirement, VersionOrUrl}; use uv_pypi_types::{ LenientRequirement, ParsedArchiveUrl, ParsedGitUrl, ParsedUrl, VerbatimParsedUrl, @@ -23,14 +23,16 @@ use uv_python::{PythonDownloads, PythonEnvironment, PythonPreference, PythonRequ use uv_resolver::{FlatIndex, Lock}; use uv_types::{BuildIsolation, HashStrategy}; use uv_warnings::warn_user; -use uv_workspace::pyproject::{Source, Sources, ToolUvSources}; +use uv_workspace::pyproject::{DependencyGroupSpecifier, Source, Sources, ToolUvSources}; use uv_workspace::{DiscoveryOptions, InstallTarget, MemberDiscovery, VirtualProject, Workspace}; use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger, InstallLogger}; use crate::commands::pip::operations; use crate::commands::pip::operations::Modifications; -use crate::commands::project::lock::do_safe_lock; -use crate::commands::project::{ProjectError, SharedState}; +use crate::commands::project::lock::{do_safe_lock, LockMode}; +use crate::commands::project::{ + default_dependency_groups, validate_dependency_groups, ProjectError, SharedState, +}; use crate::commands::{diagnostics, pip, project, ExitStatus}; use crate::printer::Printer; use crate::settings::{InstallerSettingsRef, ResolverInstallerSettings}; @@ -43,7 +45,7 @@ pub(crate) async fn sync( frozen: bool, package: Option, extras: ExtrasSpecification, - dev: DevMode, + dev: DevGroupsSpecification, editable: EditableMode, install_options: InstallOptions, modifications: Modifications, @@ -93,6 +95,10 @@ pub(crate) async fn sync( warn_user!("Skipping installation of entry points (`project.scripts`) because this project is not packaged; to install entry points, set `tool.uv.package = true` or define a `build-system`"); } + // Determine the default groups to include. + validate_dependency_groups(&project, &dev)?; + let defaults = default_dependency_groups(project.pyproject_toml())?; + // Discover or create the virtual environment. let venv = project::get_or_init_environment( target.workspace(), @@ -109,11 +115,18 @@ pub(crate) async fn sync( // Initialize any shared state. let state = SharedState::default(); + // Determine the lock mode. + let mode = if frozen { + LockMode::Frozen + } else if locked { + LockMode::Locked(venv.interpreter()) + } else { + LockMode::Write(venv.interpreter()) + }; + let lock = match do_safe_lock( - locked, - frozen, + mode, target.workspace(), - venv.interpreter(), settings.as_ref().into(), LowerBound::Warn, &state, @@ -154,7 +167,7 @@ pub(crate) async fn sync( &venv, &lock, &extras, - dev, + &dev.with_defaults(defaults), editable, install_options, modifications, @@ -178,7 +191,7 @@ pub(super) async fn do_sync( venv: &PythonEnvironment, lock: &Lock, extras: &ExtrasSpecification, - dev: DevMode, + dev: &DevGroupsManifest, editable: EditableMode, install_options: InstallOptions, modifications: Modifications, @@ -252,13 +265,6 @@ pub(super) async fn do_sync( } } - // Include development dependencies, if requested. - let dev = match dev { - DevMode::Include => DevSpecification::Include(std::slice::from_ref(&DEV_DEPENDENCIES)), - DevMode::Exclude => DevSpecification::Exclude, - DevMode::Only => DevSpecification::Only(std::slice::from_ref(&DEV_DEPENDENCIES)), - }; - // Determine the tags to use for resolution. let tags = venv.interpreter().tags()?; @@ -486,6 +492,21 @@ fn store_credentials_from_workspace(workspace: &Workspace) { .into_iter() .flat_map(|optional| optional.values()) .flatten(); + let dependency_groups = member + .pyproject_toml() + .dependency_groups + .as_ref() + .into_iter() + .flatten() + .flat_map(|(_, dependencies)| { + dependencies.iter().filter_map(|specifier| { + if let DependencyGroupSpecifier::Requirement(requirement) = specifier { + Some(requirement) + } else { + None + } + }) + }); let dev_dependencies = member .pyproject_toml() .tool @@ -497,6 +518,7 @@ fn store_credentials_from_workspace(workspace: &Workspace) { for requirement in dependencies .chain(optional_dependencies) + .chain(dependency_groups) .filter_map(|requires_dist| { LenientRequirement::::from_str(requires_dist) .map(Requirement::from) diff --git a/crates/uv/src/commands/project/tree.rs b/crates/uv/src/commands/project/tree.rs index c767aca51fc7..c40392402309 100644 --- a/crates/uv/src/commands/project/tree.rs +++ b/crates/uv/src/commands/project/tree.rs @@ -5,15 +5,18 @@ use anyhow::Result; use uv_cache::Cache; use uv_client::Connectivity; -use uv_configuration::{Concurrency, DevMode, LowerBound, TargetTriple}; +use uv_configuration::{Concurrency, DevGroupsSpecification, LowerBound, TargetTriple}; use uv_pep508::PackageName; use uv_python::{PythonDownloads, PythonPreference, PythonRequest, PythonVersion}; use uv_resolver::TreeDisplay; -use uv_workspace::{DiscoveryOptions, Workspace}; +use uv_workspace::{DiscoveryOptions, VirtualProject, Workspace}; use crate::commands::pip::loggers::DefaultResolveLogger; use crate::commands::pip::resolution_markers; -use crate::commands::project::ProjectInterpreter; +use crate::commands::project::lock::LockMode; +use crate::commands::project::{ + default_dependency_groups, validate_dependency_groups, ProjectInterpreter, +}; use crate::commands::{project, ExitStatus, SharedState}; use crate::printer::Printer; use crate::settings::ResolverSettings; @@ -22,7 +25,7 @@ use crate::settings::ResolverSettings; #[allow(clippy::fn_params_excessive_bools)] pub(crate) async fn tree( project_dir: &Path, - dev: DevMode, + dev: DevGroupsSpecification, locked: bool, frozen: bool, universal: bool, @@ -46,29 +49,46 @@ pub(crate) async fn tree( // Find the project requirements. let workspace = Workspace::discover(project_dir, &DiscoveryOptions::default()).await?; - // Find an interpreter for the project - let interpreter = ProjectInterpreter::discover( - &workspace, - python.as_deref().map(PythonRequest::parse), - python_preference, - python_downloads, - connectivity, - native_tls, - cache, - printer, - ) - .await? - .into_interpreter(); + // Determine the default groups to include. + validate_dependency_groups(&VirtualProject::NonProject(workspace.clone()), &dev)?; + let defaults = default_dependency_groups(workspace.pyproject_toml())?; + + // Find an interpreter for the project, unless `--frozen` and `--universal` are both set. + let interpreter = if frozen && universal { + None + } else { + Some( + ProjectInterpreter::discover( + &workspace, + python.as_deref().map(PythonRequest::parse), + python_preference, + python_downloads, + connectivity, + native_tls, + cache, + printer, + ) + .await? + .into_interpreter(), + ) + }; + + // Determine the lock mode. + let mode = if frozen { + LockMode::Frozen + } else if locked { + LockMode::Locked(interpreter.as_ref().unwrap()) + } else { + LockMode::Write(interpreter.as_ref().unwrap()) + }; // Initialize any shared state. let state = SharedState::default(); // Update the lockfile, if necessary. let lock = project::lock::do_safe_lock( - locked, - frozen, + mode, &workspace, - &interpreter, settings.as_ref(), LowerBound::Allow, &state, @@ -83,20 +103,22 @@ pub(crate) async fn tree( .into_lock(); // Determine the markers to use for resolution. - let markers = resolution_markers( - python_version.as_ref(), - python_platform.as_ref(), - &interpreter, - ); + let markers = (!universal).then(|| { + resolution_markers( + python_version.as_ref(), + python_platform.as_ref(), + interpreter.as_ref().unwrap(), + ) + }); // Render the tree. let tree = TreeDisplay::new( &lock, - (!universal).then_some(&markers), + markers.as_ref(), depth.into(), - prune, - package, - dev, + &prune, + &package, + &dev.with_defaults(defaults), no_dedupe, invert, ); diff --git a/crates/uv/src/commands/publish.rs b/crates/uv/src/commands/publish.rs index 315add475644..c4c268cf884b 100644 --- a/crates/uv/src/commands/publish.rs +++ b/crates/uv/src/commands/publish.rs @@ -5,13 +5,20 @@ use anyhow::{bail, Context, Result}; use console::Term; use owo_colors::OwoColorize; use std::fmt::Write; +use std::iter; use std::sync::Arc; use std::time::Duration; use tracing::info; use url::Url; -use uv_client::{AuthIntegration, BaseClientBuilder, Connectivity, DEFAULT_RETRIES}; +use uv_cache::Cache; +use uv_client::{ + AuthIntegration, BaseClientBuilder, Connectivity, RegistryClientBuilder, DEFAULT_RETRIES, +}; use uv_configuration::{KeyringProviderType, TrustedHost, TrustedPublishing}; -use uv_publish::{check_trusted_publishing, files_for_publishing, upload}; +use uv_distribution_types::{Index, IndexCapabilities, IndexLocations, IndexUrl}; +use uv_publish::{ + check_trusted_publishing, files_for_publishing, upload, CheckUrlClient, TrustedPublishResult, +}; pub(crate) async fn publish( paths: Vec, @@ -21,6 +28,8 @@ pub(crate) async fn publish( allow_insecure_host: Vec, username: Option, password: Option, + check_url: Option, + cache: &Cache, connectivity: Connectivity, native_tls: bool, printer: Printer, @@ -49,7 +58,7 @@ pub(crate) async fn publish( .retries(0) .keyring(keyring_provider) .native_tls(native_tls) - .allow_insecure_host(allow_insecure_host) + .allow_insecure_host(allow_insecure_host.clone()) // Don't try cloning the request to make an unauthenticated request first. .auth_integration(AuthIntegration::OnlyAuthenticated) // Set a very high timeout for uploads, connections are often 10x slower on upload than @@ -60,6 +69,31 @@ pub(crate) async fn publish( .auth_integration(AuthIntegration::NoAuthMiddleware) .wrap_existing(&upload_client); + // Initialize the registry client. + let check_url_client = if let Some(index_url) = check_url { + let index_urls = IndexLocations::new( + vec![Index::from_index_url(index_url.clone())], + Vec::new(), + false, + ) + .index_urls(); + let registry_client_builder = RegistryClientBuilder::new(cache.clone()) + .native_tls(native_tls) + .connectivity(connectivity) + .index_urls(index_urls) + .keyring(keyring_provider) + .allow_insecure_host(allow_insecure_host.clone()); + Some(CheckUrlClient { + index_url, + registry_client_builder, + client: &upload_client, + index_capabilities: IndexCapabilities::default(), + cache, + }) + } else { + None + }; + // If applicable, attempt obtaining a token for trusted publishing. let trusted_publishing_token = check_trusted_publishing( username.as_deref(), @@ -71,15 +105,16 @@ pub(crate) async fn publish( ) .await?; - let (username, password) = if let Some(password) = trusted_publishing_token { - (Some("__token__".to_string()), Some(password.into())) - } else { - if username.is_none() && password.is_none() { - prompt_username_and_password()? + let (username, password) = + if let TrustedPublishResult::Configured(password) = &trusted_publishing_token { + (Some("__token__".to_string()), Some(password.to_string())) } else { - (username, password) - } - }; + if username.is_none() && password.is_none() { + prompt_username_and_password()? + } else { + (username, password) + } + }; if password.is_some() && username.is_none() { bail!( @@ -89,7 +124,43 @@ pub(crate) async fn publish( ); } + if username.is_none() && password.is_none() && keyring_provider == KeyringProviderType::Disabled + { + if let TrustedPublishResult::Ignored(err) = trusted_publishing_token { + // The user has configured something incorrectly: + // * The user forgot to configure credentials. + // * The user forgot to forward the secrets as env vars (or used the wrong ones). + // * The trusted publishing configuration is wrong. + writeln!( + printer.stderr(), + "Note: Neither credentials nor keyring are configured, and there was an error \ + fetching the trusted publishing token. If you don't want to use trusted \ + publishing, you can ignore this error, but you need to provide credentials." + )?; + writeln!( + printer.stderr(), + "{}: {err}", + "Trusted publishing error".red().bold() + )?; + for source in iter::successors(std::error::Error::source(&err), |&err| err.source()) { + writeln!( + printer.stderr(), + " {}: {}", + "Caused by".red().bold(), + source.to_string().trim() + )?; + } + } + } + for (file, raw_filename, filename) in files { + if let Some(check_url_client) = &check_url_client { + if uv_publish::check_url(check_url_client, &file, &filename).await? { + writeln!(printer.stderr(), "File {filename} already exists, skipping")?; + continue; + } + } + let size = fs_err::metadata(&file)?.len(); let (bytes, unit) = human_readable_bytes(size); writeln!( @@ -108,6 +179,7 @@ pub(crate) async fn publish( DEFAULT_RETRIES, username.as_deref(), password.as_deref(), + check_url_client.as_ref(), // Needs to be an `Arc` because the reqwest `Body` static lifetime requirement Arc::new(reporter), ) diff --git a/crates/uv/src/commands/python/dir.rs b/crates/uv/src/commands/python/dir.rs index d83668fe1a03..7c6a1b325726 100644 --- a/crates/uv/src/commands/python/dir.rs +++ b/crates/uv/src/commands/python/dir.rs @@ -3,15 +3,21 @@ use anyhow::Context; use owo_colors::OwoColorize; use uv_fs::Simplified; -use uv_python::managed::ManagedPythonInstallations; +use uv_python::managed::{python_executable_dir, ManagedPythonInstallations}; + +/// Show the Python installation directory. +pub(crate) fn dir(bin: bool) -> anyhow::Result<()> { + if bin { + let bin = python_executable_dir()?; + println!("{}", bin.simplified_display().cyan()); + } else { + let installed_toolchains = ManagedPythonInstallations::from_settings() + .context("Failed to initialize toolchain settings")?; + println!( + "{}", + installed_toolchains.root().simplified_display().cyan() + ); + } -/// Show the toolchain directory. -pub(crate) fn dir() -> anyhow::Result<()> { - let installed_toolchains = ManagedPythonInstallations::from_settings() - .context("Failed to initialize toolchain settings")?; - println!( - "{}", - installed_toolchains.root().simplified_display().cyan() - ); Ok(()) } diff --git a/crates/uv/src/commands/python/find.rs b/crates/uv/src/commands/python/find.rs index 2b3da29efaa0..06d43298ee1f 100644 --- a/crates/uv/src/commands/python/find.rs +++ b/crates/uv/src/commands/python/find.rs @@ -55,7 +55,7 @@ pub(crate) async fn find( }; if let Some(project) = project { - request = find_requires_python(project.workspace())? + request = find_requires_python(project.workspace()) .as_ref() .map(RequiresPython::specifiers) .map(|specifiers| { diff --git a/crates/uv/src/commands/python/install.rs b/crates/uv/src/commands/python/install.rs index 601694359783..6e7f8902d7e7 100644 --- a/crates/uv/src/commands/python/install.rs +++ b/crates/uv/src/commands/python/install.rs @@ -1,24 +1,108 @@ +use std::fmt::Write; +use std::io::ErrorKind; +use std::path::{Path, PathBuf}; + use anyhow::Result; -use fs_err as fs; use futures::stream::FuturesUnordered; use futures::StreamExt; -use itertools::Itertools; +use itertools::{Either, Itertools}; use owo_colors::OwoColorize; -use std::collections::BTreeSet; -use std::fmt::Write; -use std::path::Path; -use tracing::debug; +use rustc_hash::{FxHashMap, FxHashSet}; +use same_file::is_same_file; +use tracing::{debug, trace}; use uv_client::Connectivity; +use uv_configuration::PreviewMode; +use uv_fs::Simplified; use uv_python::downloads::{DownloadResult, ManagedPythonDownload, PythonDownloadRequest}; -use uv_python::managed::{ManagedPythonInstallation, ManagedPythonInstallations}; -use uv_python::{PythonDownloads, PythonRequest, PythonVersionFile}; +use uv_python::managed::{ + python_executable_dir, ManagedPythonInstallation, ManagedPythonInstallations, +}; +use uv_python::{PythonDownloads, PythonInstallationKey, PythonRequest, PythonVersionFile}; +use uv_shell::Shell; +use uv_warnings::warn_user; use crate::commands::python::{ChangeEvent, ChangeEventKind}; use crate::commands::reporters::PythonDownloadReporter; use crate::commands::{elapsed, ExitStatus}; use crate::printer::Printer; +#[derive(Debug, Clone)] +struct InstallRequest { + /// The original request from the user + request: PythonRequest, + /// A download request corresponding to the `request` with platform information filled + download_request: PythonDownloadRequest, + /// A download that satisfies the request + download: &'static ManagedPythonDownload, +} + +impl InstallRequest { + fn new(request: PythonRequest) -> Result { + // Make sure the request is a valid download request and fill platform information + let download_request = PythonDownloadRequest::from_request(&request) + .ok_or_else(|| { + anyhow::anyhow!("Cannot download managed Python for request: {request}") + })? + .fill()?; + + // Find a matching download + let download = ManagedPythonDownload::from_request(&download_request)?; + + Ok(Self { + request, + download_request, + download, + }) + } + + fn matches_installation(&self, installation: &ManagedPythonInstallation) -> bool { + self.download_request.satisfied_by_key(installation.key()) + } +} + +impl std::fmt::Display for InstallRequest { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.request) + } +} + +#[derive(Debug, Default)] +struct Changelog { + existing: FxHashSet, + installed: FxHashSet, + uninstalled: FxHashSet, + installed_executables: FxHashMap>, + uninstalled_executables: FxHashSet, +} + +impl Changelog { + fn events(&self) -> impl Iterator { + let reinstalled = self + .uninstalled + .intersection(&self.installed) + .cloned() + .collect::>(); + let uninstalled = self.uninstalled.difference(&reinstalled).cloned(); + let installed = self.installed.difference(&reinstalled).cloned(); + + uninstalled + .map(|key| ChangeEvent { + key: key.clone(), + kind: ChangeEventKind::Removed, + }) + .chain(installed.map(|key| ChangeEvent { + key: key.clone(), + kind: ChangeEventKind::Added, + })) + .chain(reinstalled.iter().map(|key| ChangeEvent { + key: key.clone(), + kind: ChangeEventKind::Reinstalled, + })) + .sorted_unstable_by(|a, b| a.key.cmp(&b.key).then_with(|| a.kind.cmp(&b.kind))) + } +} + /// Download and install Python versions. pub(crate) async fn install( project_dir: &Path, @@ -28,90 +112,77 @@ pub(crate) async fn install( native_tls: bool, connectivity: Connectivity, no_config: bool, + preview: PreviewMode, printer: Printer, ) -> Result { let start = std::time::Instant::now(); - let installations = ManagedPythonInstallations::from_settings()?.init()?; - let installations_dir = installations.root(); - let cache_dir = installations.cache(); - let _lock = installations.lock().await?; - - let targets = targets.into_iter().collect::>(); + // Resolve the requests + let mut is_default_install = false; let requests: Vec<_> = if targets.is_empty() { PythonVersionFile::discover(project_dir, no_config, true) .await? .map(PythonVersionFile::into_versions) - .unwrap_or_else(|| vec![PythonRequest::Default]) + .unwrap_or_else(|| { + // If no version file is found and no requests were made + is_default_install = true; + vec![PythonRequest::Default] + }) + .into_iter() + .map(InstallRequest::new) + .collect::>>()? } else { targets .iter() .map(|target| PythonRequest::parse(target.as_str())) - .collect() + .map(InstallRequest::new) + .collect::>>()? }; - let download_requests = requests - .iter() - .map(|request| { - PythonDownloadRequest::from_request(request).ok_or_else(|| { - anyhow::anyhow!("Cannot download managed Python for request: {request}") - }) - }) - .collect::>>()?; - - let installed_installations: Vec<_> = installations + // Read the existing installations, lock the directory for the duration + let installations = ManagedPythonInstallations::from_settings()?.init()?; + let installations_dir = installations.root(); + let cache_dir = installations.cache(); + let _lock = installations.lock().await?; + let existing_installations: Vec<_> = installations .find_all()? - .inspect(|installation| debug!("Found existing installation {}", installation.key())) + .inspect(|installation| trace!("Found existing installation {}", installation.key())) .collect(); - let mut unfilled_requests = Vec::new(); - let mut uninstalled = Vec::new(); - for (request, download_request) in requests.iter().zip(download_requests) { - if matches!(requests.as_slice(), [PythonRequest::Default]) { - writeln!(printer.stderr(), "Searching for Python installations")?; - } else { - writeln!( - printer.stderr(), - "Searching for Python versions matching: {}", - request.cyan() - )?; - } - if let Some(installation) = installed_installations + + // Find requests that are already satisfied + let mut changelog = Changelog::default(); + let (satisfied, unsatisfied): (Vec<_>, Vec<_>) = requests.iter().partition_map(|request| { + if let Some(installation) = existing_installations .iter() - .find(|installation| download_request.satisfied_by_key(installation.key())) + .find(|installation| request.matches_installation(installation)) { - if matches!(request, PythonRequest::Default) { - writeln!(printer.stderr(), "Found: {}", installation.key().green())?; + changelog.existing.insert(installation.key().clone()); + if reinstall { + debug!( + "Ignoring match `{}` for request `{}` due to `--reinstall` flag", + installation.key().green(), + request.cyan() + ); + + Either::Right(request) } else { - writeln!( - printer.stderr(), - "Found existing installation for {}: {}", - request.cyan(), + debug!( + "Found `{}` for request `{}`", installation.key().green(), - )?; - } - if reinstall { - fs::remove_dir_all(installation.path())?; - uninstalled.push(installation.key().clone()); - unfilled_requests.push(download_request); + request.cyan(), + ); + + Either::Left(installation) } } else { - unfilled_requests.push(download_request); - } - } + debug!("No installation found for request `{}`", request.cyan(),); - if unfilled_requests.is_empty() { - if matches!(requests.as_slice(), [PythonRequest::Default]) { - writeln!( - printer.stderr(), - "Python is already available. Use `uv python install ` to install a specific version.", - )?; - } else if requests.len() > 1 { - writeln!(printer.stderr(), "All requested versions already installed")?; + Either::Right(request) } - return Ok(ExitStatus::Success); - } + }); - if matches!(python_downloads, PythonDownloads::Never) { + // Check if Python downloads are banned + if matches!(python_downloads, PythonDownloads::Never) && !unsatisfied.is_empty() { writeln!( printer.stderr(), "Python downloads are not allowed (`python-downloads = \"never\"`). Change to `python-downloads = \"manual\"` to allow explicit installs.", @@ -119,40 +190,47 @@ pub(crate) async fn install( return Ok(ExitStatus::Failure); } - let downloads = unfilled_requests - .into_iter() - // Populate the download requests with defaults - .map(|request| ManagedPythonDownload::from_request(&PythonDownloadRequest::fill(request)?)) - .collect::, uv_python::downloads::Error>>()?; - - // Ensure we only download each version once - let downloads = downloads - .into_iter() + // Find downloads for the requests + let downloads = unsatisfied + .iter() + .inspect(|request| { + debug!( + "Found download `{}` for request `{}`", + request.download, + request.cyan(), + ); + }) + .map(|request| request.download) + // Ensure we only download each version once .unique_by(|download| download.key()) .collect::>(); - // Construct a client + // Download and unpack the Python versions concurrently let client = uv_client::BaseClientBuilder::new() .connectivity(connectivity) .native_tls(native_tls) .build(); - let reporter = PythonDownloadReporter::new(printer, downloads.len() as u64); - let mut tasks = FuturesUnordered::new(); for download in &downloads { tasks.push(async { ( download.key(), download - .fetch(&client, installations_dir, &cache_dir, Some(&reporter)) + .fetch( + &client, + installations_dir, + &cache_dir, + reinstall, + Some(&reporter), + ) .await, ) }); } - let mut installed = vec![]; let mut errors = vec![]; + let mut downloaded = Vec::with_capacity(downloads.len()); while let Some((key, result)) = tasks.next().await { match result { Ok(download) => { @@ -162,21 +240,107 @@ pub(crate) async fn install( DownloadResult::Fetched(path) => path, }; - installed.push(key.clone()); - - // Ensure the installations have externally managed markers - let managed = ManagedPythonInstallation::new(path.clone())?; - managed.ensure_externally_managed()?; - managed.ensure_canonical_executables()?; + let installation = ManagedPythonInstallation::new(path)?; + changelog.installed.insert(installation.key().clone()); + if changelog.existing.contains(installation.key()) { + changelog.uninstalled.insert(installation.key().clone()); + } + downloaded.push(installation); } Err(err) => { - errors.push((key, err)); + errors.push((key, anyhow::Error::new(err))); + } + } + } + + let bin = if preview.is_enabled() { + Some(python_executable_dir()?) + } else { + None + }; + + // Ensure that the installations are _complete_ for both downloaded installations and existing + // installations that match the request + for installation in downloaded.iter().chain(satisfied.iter().copied()) { + installation.ensure_externally_managed()?; + installation.ensure_canonical_executables()?; + + if preview.is_disabled() { + debug!("Skipping installation of Python executables, use `--preview` to enable."); + continue; + } + + let bin = bin + .as_ref() + .expect("We should have a bin directory with preview enabled") + .as_path(); + + let target = bin.join(installation.key().versioned_executable_name()); + + match installation.create_bin_link(&target) { + Ok(()) => { + debug!( + "Installed executable at {} for {}", + target.user_display(), + installation.key(), + ); + changelog.installed.insert(installation.key().clone()); + changelog + .installed_executables + .entry(installation.key().clone()) + .or_default() + .push(target.clone()); + } + Err(uv_python::managed::Error::LinkExecutable { from, to, err }) + if err.kind() == ErrorKind::AlreadyExists => + { + // TODO(zanieb): Add `--force` + if reinstall { + fs_err::remove_file(&to)?; + installation.create_bin_link(&target)?; + debug!( + "Updated executable at {} to {}", + target.user_display(), + installation.key(), + ); + changelog.installed.insert(installation.key().clone()); + changelog + .installed_executables + .entry(installation.key().clone()) + .or_default() + .push(target.clone()); + changelog.uninstalled_executables.insert(target); + } else { + if !is_same_file(&to, &from).unwrap_or_default() { + errors.push(( + installation.key(), + anyhow::anyhow!( + "Executable already exists at `{}`. Use `--reinstall` to force replacement.", + to.user_display() + ), + )); + } + } } + Err(err) => return Err(err.into()), + } + } + + if changelog.installed.is_empty() { + if is_default_install { + writeln!( + printer.stderr(), + "Python is already installed. Use `uv python install ` to install another version.", + )?; + } else if requests.len() > 1 { + writeln!(printer.stderr(), "All requested versions already installed")?; } + return Ok(ExitStatus::Success); } - if !installed.is_empty() { - if let [installed] = installed.as_slice() { + if !changelog.installed.is_empty() { + if changelog.installed.len() == 1 { + let installed = changelog.installed.iter().next().unwrap(); // Ex) "Installed Python 3.9.7 in 1.68s" writeln!( printer.stderr(), @@ -190,51 +354,71 @@ pub(crate) async fn install( )?; } else { // Ex) "Installed 2 versions in 1.68s" - let s = if installed.len() == 1 { "" } else { "s" }; writeln!( printer.stderr(), "{}", format!( "Installed {} {}", - format!("{} version{s}", installed.len()).bold(), + format!("{} versions", changelog.installed.len()).bold(), format!("in {}", elapsed(start.elapsed())).dimmed() ) .dimmed() )?; } - for event in uninstalled - .into_iter() - .map(|key| ChangeEvent { - key, - kind: ChangeEventKind::Removed, - }) - .chain(installed.into_iter().map(|key| ChangeEvent { - key, - kind: ChangeEventKind::Added, - })) - .sorted_unstable_by(|a, b| a.key.cmp(&b.key).then_with(|| a.kind.cmp(&b.kind))) - { + for event in changelog.events() { match event.kind { ChangeEventKind::Added => { - writeln!(printer.stderr(), " {} {}", "+".green(), event.key.bold())?; + writeln!( + printer.stderr(), + " {} {}{}", + "+".green(), + event.key.bold(), + format_installed_executables(&event.key, &changelog.installed_executables) + )?; } ChangeEventKind::Removed => { - writeln!(printer.stderr(), " {} {}", "-".red(), event.key.bold())?; + writeln!( + printer.stderr(), + " {} {}{}", + "-".red(), + event.key.bold(), + format_installed_executables(&event.key, &changelog.installed_executables) + )?; + } + ChangeEventKind::Reinstalled => { + writeln!( + printer.stderr(), + " {} {}{}", + "~".yellow(), + event.key.bold(), + format_installed_executables(&event.key, &changelog.installed_executables) + )?; } } } + + if preview.is_enabled() { + let bin = bin + .as_ref() + .expect("We should have a bin directory with preview enabled") + .as_path(); + warn_if_not_on_path(bin); + } } if !errors.is_empty() { - for (key, err) in errors { + for (key, err) in errors + .into_iter() + .sorted_unstable_by(|(key_a, _), (key_b, _)| key_a.cmp(key_b)) + { writeln!( printer.stderr(), "{}: Failed to install {}", "error".red().bold(), key.green() )?; - for err in anyhow::Error::new(err).chain() { + for err in err.chain() { writeln!( printer.stderr(), " {}: {}", @@ -248,3 +432,54 @@ pub(crate) async fn install( Ok(ExitStatus::Success) } + +// TODO(zanieb): Change the formatting of this to something nicer, probably integrate with +// `Changelog` and `ChangeEventKind`. +fn format_installed_executables( + key: &PythonInstallationKey, + installed_executables: &FxHashMap>, +) -> String { + if let Some(executables) = installed_executables.get(key) { + let executables = executables + .iter() + .filter_map(|path| path.file_name()) + .map(|name| name.to_string_lossy()) + .join(", "); + format!(" ({executables})") + } else { + String::new() + } +} + +fn warn_if_not_on_path(bin: &Path) { + if !Shell::contains_path(bin) { + if let Some(shell) = Shell::from_env() { + if let Some(command) = shell.prepend_path(bin) { + if shell.configuration_files().is_empty() { + warn_user!( + "`{}` is not on your PATH. To use the installed Python executable, run `{}`.", + bin.simplified_display().cyan(), + command.green() + ); + } else { + // TODO(zanieb): Update when we add `uv python update-shell` to match `uv tool` + warn_user!( + "`{}` is not on your PATH. To use the installed Python executable, run `{}`.", + bin.simplified_display().cyan(), + command.green(), + ); + } + } else { + warn_user!( + "`{}` is not on your PATH. To use the installed Python executable, add the directory to your PATH.", + bin.simplified_display().cyan(), + ); + } + } else { + warn_user!( + "`{}` is not on your PATH. To use the installed Python executable, add the directory to your PATH.", + bin.simplified_display().cyan(), + ); + } + } +} diff --git a/crates/uv/src/commands/python/mod.rs b/crates/uv/src/commands/python/mod.rs index 80a39fae14e1..afc700d2335b 100644 --- a/crates/uv/src/commands/python/mod.rs +++ b/crates/uv/src/commands/python/mod.rs @@ -11,6 +11,8 @@ pub(super) enum ChangeEventKind { Removed, /// The Python version was installed. Added, + /// The Python version was reinstalled. + Reinstalled, } #[derive(Debug)] diff --git a/crates/uv/src/commands/python/pin.rs b/crates/uv/src/commands/python/pin.rs index 653142c7516a..f588e624a3e1 100644 --- a/crates/uv/src/commands/python/pin.rs +++ b/crates/uv/src/commands/python/pin.rs @@ -253,7 +253,7 @@ fn assert_pin_compatible_with_project(pin: &Pin, virtual_project: &VirtualProjec project_workspace.project_name(), project_workspace.workspace().install_path().display() ); - let requires_python = find_requires_python(project_workspace.workspace())?; + let requires_python = find_requires_python(project_workspace.workspace()); (requires_python, "project") } VirtualProject::NonProject(workspace) => { @@ -261,7 +261,7 @@ fn assert_pin_compatible_with_project(pin: &Pin, virtual_project: &VirtualProjec "Discovered virtual workspace at: {}", workspace.install_path().display() ); - let requires_python = find_requires_python(workspace)?; + let requires_python = find_requires_python(workspace); (requires_python, "workspace") } }; diff --git a/crates/uv/src/commands/python/uninstall.rs b/crates/uv/src/commands/python/uninstall.rs index 531cda41586e..d0dc7528444f 100644 --- a/crates/uv/src/commands/python/uninstall.rs +++ b/crates/uv/src/commands/python/uninstall.rs @@ -7,8 +7,10 @@ use futures::StreamExt; use itertools::Itertools; use owo_colors::OwoColorize; +use tracing::{debug, warn}; +use uv_fs::Simplified; use uv_python::downloads::PythonDownloadRequest; -use uv_python::managed::ManagedPythonInstallations; +use uv_python::managed::{python_executable_dir, ManagedPythonInstallations}; use uv_python::PythonRequest; use crate::commands::python::{ChangeEvent, ChangeEventKind}; @@ -121,6 +123,42 @@ async fn do_uninstall( return Ok(ExitStatus::Failure); } + // Collect files in a directory + let executables = python_executable_dir()? + .read_dir() + .into_iter() + .flatten() + .filter_map(|entry| match entry { + Ok(entry) => Some(entry), + Err(err) => { + warn!("Failed to read executable: {}", err); + None + } + }) + .filter(|entry| entry.file_type().is_ok_and(|file_type| !file_type.is_dir())) + .map(|entry| entry.path()) + // Only include files that match the expected Python executable names + // TODO(zanieb): This is a minor optimization to avoid opening more files, but we could + // leave broken links behind, i.e., if the user created them. + .filter(|path| { + matching_installations.iter().any(|installation| { + path.file_name().and_then(|name| name.to_str()) + == Some(&installation.key().versioned_executable_name()) + }) + }) + // Only include Python executables that match the installations + .filter(|path| { + matching_installations + .iter() + .any(|installation| installation.is_bin_link(path.as_path())) + }) + .collect::>(); + + for executable in &executables { + fs_err::remove_file(executable)?; + debug!("Removed {}", executable.user_display()); + } + let mut tasks = FuturesUnordered::new(); for installation in &matching_installations { tasks.push(async { @@ -179,12 +217,17 @@ async fn do_uninstall( .sorted_unstable_by(|a, b| a.key.cmp(&b.key).then_with(|| a.kind.cmp(&b.kind))) { match event.kind { - ChangeEventKind::Added => { - writeln!(printer.stderr(), " {} {}", "+".green(), event.key.bold())?; - } + // TODO(zanieb): Track removed executables and report them all here ChangeEventKind::Removed => { - writeln!(printer.stderr(), " {} {}", "-".red(), event.key.bold())?; + writeln!( + printer.stderr(), + " {} {} ({})", + "-".red(), + event.key.bold(), + event.key.versioned_executable_name() + )?; } + _ => unreachable!(), } } } diff --git a/crates/uv/src/commands/tool/common.rs b/crates/uv/src/commands/tool/common.rs index 82ce665a43f4..08fae7fa607f 100644 --- a/crates/uv/src/commands/tool/common.rs +++ b/crates/uv/src/commands/tool/common.rs @@ -16,7 +16,7 @@ use uv_pypi_types::Requirement; use uv_python::PythonEnvironment; use uv_settings::ToolOptions; use uv_shell::Shell; -use uv_tool::{entrypoint_paths, find_executable_directory, InstalledTools, Tool, ToolEntrypoint}; +use uv_tool::{entrypoint_paths, tool_executable_dir, InstalledTools, Tool, ToolEntrypoint}; use uv_warnings::warn_user; use crate::commands::ExitStatus; @@ -79,7 +79,7 @@ pub(crate) fn install_executables( }; // Find a suitable path to install into - let executable_directory = find_executable_directory()?; + let executable_directory = tool_executable_dir()?; fs_err::create_dir_all(&executable_directory) .context("Failed to create executable directory")?; diff --git a/crates/uv/src/commands/tool/dir.rs b/crates/uv/src/commands/tool/dir.rs index 880a8eee3349..e8b1a6a40283 100644 --- a/crates/uv/src/commands/tool/dir.rs +++ b/crates/uv/src/commands/tool/dir.rs @@ -4,12 +4,12 @@ use owo_colors::OwoColorize; use uv_configuration::PreviewMode; use uv_fs::Simplified; -use uv_tool::{find_executable_directory, InstalledTools}; +use uv_tool::{tool_executable_dir, InstalledTools}; /// Show the tool directory. pub(crate) fn dir(bin: bool, _preview: PreviewMode) -> anyhow::Result<()> { if bin { - let executable_directory = find_executable_directory()?; + let executable_directory = tool_executable_dir()?; println!("{}", executable_directory.simplified_display().cyan()); } else { let installed_tools = diff --git a/crates/uv/src/commands/tool/run.rs b/crates/uv/src/commands/tool/run.rs index d43f2f85210b..33d31697724f 100644 --- a/crates/uv/src/commands/tool/run.rs +++ b/crates/uv/src/commands/tool/run.rs @@ -207,6 +207,15 @@ pub(crate) async fn run( for (name, _) in entrypoints { writeln!(printer.stdout(), "- {}", name.cyan())?; } + let suggested_command = format!( + "{} --from {} ", + invocation_source, from.name + ); + writeln!( + printer.stdout(), + "Consider using `{}` instead.", + suggested_command.green() + )?; } return Ok(ExitStatus::Failure); } diff --git a/crates/uv/src/commands/tool/update_shell.rs b/crates/uv/src/commands/tool/update_shell.rs index 58637c42297b..9a61917618dc 100644 --- a/crates/uv/src/commands/tool/update_shell.rs +++ b/crates/uv/src/commands/tool/update_shell.rs @@ -9,14 +9,14 @@ use tracing::debug; use uv_fs::Simplified; use uv_shell::Shell; -use uv_tool::find_executable_directory; +use uv_tool::tool_executable_dir; use crate::commands::ExitStatus; use crate::printer::Printer; /// Ensure that the executable directory is in PATH. pub(crate) async fn update_shell(printer: Printer) -> Result { - let executable_directory = find_executable_directory()?; + let executable_directory = tool_executable_dir()?; debug!( "Ensuring that the executable directory is in PATH: {}", executable_directory.simplified_display() diff --git a/crates/uv/src/commands/venv.rs b/crates/uv/src/commands/venv.rs index 0900eacbaf36..b1b85ee503e2 100644 --- a/crates/uv/src/commands/venv.rs +++ b/crates/uv/src/commands/venv.rs @@ -198,7 +198,6 @@ async fn venv_impl( if interpreter_request.is_none() { if let Some(project) = project { interpreter_request = find_requires_python(project.workspace()) - .into_diagnostic()? .as_ref() .map(RequiresPython::specifiers) .map(|specifiers| { diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index e9380b82872e..addb2e679255 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -643,8 +643,8 @@ async fn run(mut cli: Cli) -> Result { commands::pip_tree( args.show_version_specifiers, args.depth, - args.prune, - args.package, + &args.prune, + &args.package, args.no_dedupe, args.invert, args.shared.strict, @@ -922,6 +922,11 @@ async fn run(mut cli: Cli) -> Result { .with .into_iter() .map(RequirementsSource::from_with_package) + .chain( + args.with_editable + .into_iter() + .map(RequirementsSource::Editable), + ) .chain( args.with_requirements .into_iter() @@ -1052,6 +1057,7 @@ async fn run(mut cli: Cli) -> Result { globals.native_tls, globals.connectivity, cli.top_level.no_config, + globals.preview, printer, ) .await @@ -1106,9 +1112,13 @@ async fn run(mut cli: Cli) -> Result { .await } Commands::Python(PythonNamespace { - command: PythonCommand::Dir, + command: PythonCommand::Dir(args), }) => { - commands::python_dir()?; + // Resolve the settings from the command-line arguments and workspace configuration. + let args = settings::PythonDirSettings::resolve(args, filesystem); + show_settings!(args); + + commands::python_dir(args.bin)?; Ok(ExitStatus::Success) } Commands::Publish(args) => { @@ -1127,6 +1137,7 @@ async fn run(mut cli: Cli) -> Result { trusted_publishing, keyring_provider, allow_insecure_host, + check_url, } = PublishSettings::resolve(args, filesystem); commands::publish( @@ -1137,6 +1148,8 @@ async fn run(mut cli: Cli) -> Result { allow_insecure_host, username, password, + check_url, + &cache, globals.connectivity, globals.native_tls, printer, @@ -1347,6 +1360,7 @@ async fn run_project( project_dir, args.locked, args.frozen, + args.dry_run, args.python, args.settings, globals.python_preference, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 1b7d57088482..d1a4fbc68fc0 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -8,7 +8,7 @@ use url::Url; use uv_cache::{CacheArgs, Refresh}; use uv_cli::{ options::{flag, resolver_installer_options, resolver_options}, - AuthorFrom, BuildArgs, ExportArgs, PublishArgs, ToolUpgradeArgs, + AuthorFrom, BuildArgs, ExportArgs, PublishArgs, PythonDirArgs, ToolUpgradeArgs, }; use uv_cli::{ AddArgs, ColorChoice, ExternalCommand, GlobalArgs, InitArgs, ListFormat, LockArgs, Maybe, @@ -19,12 +19,12 @@ use uv_cli::{ }; use uv_client::Connectivity; use uv_configuration::{ - BuildOptions, Concurrency, ConfigSettings, DevMode, EditableMode, ExportFormat, + BuildOptions, Concurrency, ConfigSettings, DevGroupsSpecification, EditableMode, ExportFormat, ExtrasSpecification, HashCheckingMode, IndexStrategy, InstallOptions, KeyringProviderType, NoBinary, NoBuild, PreviewMode, ProjectBuildBackend, Reinstall, SourceStrategy, TargetTriple, TrustedHost, TrustedPublishing, Upgrade, VersionControlSystem, }; -use uv_distribution_types::{DependencyMetadata, Index, IndexLocations}; +use uv_distribution_types::{DependencyMetadata, Index, IndexLocations, IndexUrl}; use uv_install_wheel::linker::LinkMode; use uv_normalize::PackageName; use uv_pep508::{ExtraName, RequirementOrigin}; @@ -202,7 +202,8 @@ impl InitSettings { (_, _, _) => unreachable!("`app`, `lib`, and `script` are mutually exclusive"), }; - let package = flag(package || r#virtual, no_package).unwrap_or(kind.packaged_by_default()); + let package = flag(package || build_backend.is_some(), no_package || r#virtual) + .unwrap_or(kind.packaged_by_default()); Self { path, @@ -227,7 +228,7 @@ pub(crate) struct RunSettings { pub(crate) locked: bool, pub(crate) frozen: bool, pub(crate) extras: ExtrasSpecification, - pub(crate) dev: DevMode, + pub(crate) dev: DevGroupsSpecification, pub(crate) editable: EditableMode, pub(crate) with: Vec, pub(crate) with_editable: Vec, @@ -252,6 +253,9 @@ impl RunSettings { no_all_extras, dev, no_dev, + group, + no_group, + only_group, module: _, only_dev, no_editable, @@ -280,7 +284,9 @@ impl RunSettings { flag(all_extras, no_all_extras).unwrap_or_default(), extra.unwrap_or_default(), ), - dev: DevMode::from_args(dev, no_dev, only_dev), + dev: DevGroupsSpecification::from_args( + dev, no_dev, only_dev, group, no_group, only_group, + ), editable: EditableMode::from_args(no_editable), with, with_editable, @@ -389,6 +395,7 @@ pub(crate) struct ToolInstallSettings { pub(crate) from: Option, pub(crate) with: Vec, pub(crate) with_requirements: Vec, + pub(crate) with_editable: Vec, pub(crate) python: Option, pub(crate) refresh: Refresh, pub(crate) options: ResolverInstallerOptions, @@ -406,6 +413,7 @@ impl ToolInstallSettings { editable, from, with, + with_editable, with_requirements, installer, force, @@ -427,6 +435,7 @@ impl ToolInstallSettings { package, from, with, + with_editable, with_requirements: with_requirements .into_iter() .filter_map(Maybe::into_option) @@ -588,6 +597,23 @@ impl PythonListSettings { } } +/// The resolved settings to use for a `python dir` invocation. +#[allow(clippy::struct_excessive_bools)] +#[derive(Debug, Clone)] +pub(crate) struct PythonDirSettings { + pub(crate) bin: bool, +} + +impl PythonDirSettings { + /// Resolve the [`PythonDirSettings`] from the CLI and filesystem configuration. + #[allow(clippy::needless_pass_by_value)] + pub(crate) fn resolve(args: PythonDirArgs, _filesystem: Option) -> Self { + let PythonDirArgs { bin } = args; + + Self { bin } + } +} + /// The resolved settings to use for a `python install` invocation. #[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone)] @@ -690,7 +716,7 @@ pub(crate) struct SyncSettings { pub(crate) locked: bool, pub(crate) frozen: bool, pub(crate) extras: ExtrasSpecification, - pub(crate) dev: DevMode, + pub(crate) dev: DevGroupsSpecification, pub(crate) editable: EditableMode, pub(crate) install_options: InstallOptions, pub(crate) modifications: Modifications, @@ -711,6 +737,9 @@ impl SyncSettings { dev, no_dev, only_dev, + group, + only_group, + no_group, no_editable, inexact, exact, @@ -738,7 +767,9 @@ impl SyncSettings { flag(all_extras, no_all_extras).unwrap_or_default(), extra.unwrap_or_default(), ), - dev: DevMode::from_args(dev, no_dev, only_dev), + dev: DevGroupsSpecification::from_args( + dev, no_dev, only_dev, group, no_group, only_group, + ), editable: EditableMode::from_args(no_editable), install_options: InstallOptions::new( no_install_project, @@ -764,6 +795,7 @@ impl SyncSettings { pub(crate) struct LockSettings { pub(crate) locked: bool, pub(crate) frozen: bool, + pub(crate) dry_run: bool, pub(crate) python: Option, pub(crate) refresh: Refresh, pub(crate) settings: ResolverSettings, @@ -776,6 +808,7 @@ impl LockSettings { let LockArgs { locked, frozen, + dry_run, resolver, build, refresh, @@ -785,6 +818,7 @@ impl LockSettings { Self { locked, frozen, + dry_run, python: python.and_then(Maybe::into_option), refresh: Refresh::from(refresh), settings: ResolverSettings::combine(resolver_options(resolver, build), filesystem), @@ -825,6 +859,7 @@ impl AddSettings { requirements, dev, optional, + group, editable, no_editable, extra, @@ -843,8 +878,10 @@ impl AddSettings { python, } = args; - let dependency_type = if let Some(group) = optional { - DependencyType::Optional(group) + let dependency_type = if let Some(extra) = optional { + DependencyType::Optional(extra) + } else if let Some(group) = group { + DependencyType::Group(group) } else if dev { DependencyType::Dev } else { @@ -946,6 +983,7 @@ impl RemoveSettings { dev, optional, packages, + group, no_sync, locked, frozen, @@ -957,8 +995,10 @@ impl RemoveSettings { python, } = args; - let dependency_type = if let Some(group) = optional { - DependencyType::Optional(group) + let dependency_type = if let Some(extra) = optional { + DependencyType::Optional(extra) + } else if let Some(group) = group { + DependencyType::Group(group) } else if dev { DependencyType::Dev } else { @@ -987,7 +1027,7 @@ impl RemoveSettings { #[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone)] pub(crate) struct TreeSettings { - pub(crate) dev: DevMode, + pub(crate) dev: DevGroupsSpecification, pub(crate) locked: bool, pub(crate) frozen: bool, pub(crate) universal: bool, @@ -1009,7 +1049,11 @@ impl TreeSettings { tree, universal, dev, + only_dev, no_dev, + group, + no_group, + only_group, locked, frozen, build, @@ -1020,7 +1064,9 @@ impl TreeSettings { } = args; Self { - dev: DevMode::from_args(dev, no_dev, false), + dev: DevGroupsSpecification::from_args( + dev, no_dev, only_dev, group, no_group, only_group, + ), locked, frozen, universal, @@ -1044,7 +1090,7 @@ pub(crate) struct ExportSettings { pub(crate) format: ExportFormat, pub(crate) package: Option, pub(crate) extras: ExtrasSpecification, - pub(crate) dev: DevMode, + pub(crate) dev: DevGroupsSpecification, pub(crate) editable: EditableMode, pub(crate) hashes: bool, pub(crate) install_options: InstallOptions, @@ -1070,6 +1116,9 @@ impl ExportSettings { dev, no_dev, only_dev, + group, + no_group, + only_group, header, no_header, no_editable, @@ -1094,7 +1143,9 @@ impl ExportSettings { flag(all_extras, no_all_extras).unwrap_or_default(), extra.unwrap_or_default(), ), - dev: DevMode::from_args(dev, no_dev, only_dev), + dev: DevGroupsSpecification::from_args( + dev, no_dev, only_dev, group, no_group, only_group, + ), editable: EditableMode::from_args(no_editable), hashes: flag(hashes, no_hashes).unwrap_or(true), install_options: InstallOptions::new( @@ -2563,6 +2614,7 @@ pub(crate) struct PublishSettings { pub(crate) trusted_publishing: TrustedPublishing, pub(crate) keyring_provider: KeyringProviderType, pub(crate) allow_insecure_host: Vec, + pub(crate) check_url: Option, } impl PublishSettings { @@ -2616,6 +2668,7 @@ impl PublishSettings { }) .combine(allow_insecure_host) .unwrap_or_default(), + check_url: args.check_url, } } } diff --git a/crates/uv/tests/it/build.rs b/crates/uv/tests/it/build.rs index 2c1a027d9e55..9c84e6446c84 100644 --- a/crates/uv/tests/it/build.rs +++ b/crates/uv/tests/it/build.rs @@ -1985,3 +1985,104 @@ fn git_boundary_in_dist_build() -> Result<()> { Ok(()) } + +#[test] +fn build_non_package() -> Result<()> { + let context = TestContext::new("3.12"); + let filters = context + .filters() + .into_iter() + .chain([ + (r"exit code: 1", "exit status: 1"), + (r"bdist\.[^/\\\s]+-[^/\\\s]+", "bdist.linux-x86_64"), + (r"\\\.", ""), + (r"\[project\]", "[PKG]"), + (r"\[member\]", "[PKG]"), + ]) + .collect::>(); + + let project = context.temp_dir.child("project"); + + let pyproject_toml = project.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["anyio==3.7.0"] + + [tool.uv.workspace] + members = ["packages/*"] + "#, + )?; + + project.child("src").child("__init__.py").touch()?; + project.child("README").touch()?; + + let member = project.child("packages").child("member"); + fs_err::create_dir_all(member.path())?; + + member.child("pyproject.toml").write_str( + r#" + [project] + name = "member" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["iniconfig"] + "#, + )?; + + member.child("src").child("__init__.py").touch()?; + member.child("README").touch()?; + + // Build the member. + uv_snapshot!(&filters, context.build().arg("--package").arg("member").current_dir(&project), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Package `member` is missing a `build-system`. For example, to build with `setuptools`, add the following to `packages/member/pyproject.toml`: + ```toml + [build-system] + requires = ["setuptools"] + build-backend = "setuptools.build_meta" + ``` + "###); + + project + .child("dist") + .child("member-0.1.0.tar.gz") + .assert(predicate::path::missing()); + project + .child("dist") + .child("member-0.1.0-py3-none-any.whl") + .assert(predicate::path::missing()); + + // Build all packages. + uv_snapshot!(&filters, context.build().arg("--all").arg("--no-build-logs").current_dir(&project), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Workspace does contain any buildable packages. For example, to build `member` with `setuptools`, add a `build-system` to `packages/member/pyproject.toml`: + ```toml + [build-system] + requires = ["setuptools"] + build-backend = "setuptools.build_meta" + ``` + "###); + + project + .child("dist") + .child("member-0.1.0.tar.gz") + .assert(predicate::path::missing()); + project + .child("dist") + .child("member-0.1.0-py3-none-any.whl") + .assert(predicate::path::missing()); + + Ok(()) +} diff --git a/crates/uv/tests/it/common/mod.rs b/crates/uv/tests/it/common/mod.rs index b744c1a15179..48ef6c4bc9e5 100644 --- a/crates/uv/tests/it/common/mod.rs +++ b/crates/uv/tests/it/common/mod.rs @@ -76,6 +76,7 @@ pub const INSTA_FILTERS: &[(&str, &str)] = &[ /// * Set a cutoff for versions used in the resolution so the snapshots don't change after a new release. /// * Set the venv to a fresh `.venv` in `temp_dir` pub struct TestContext { + pub root: ChildPath, pub temp_dir: ChildPath, pub cache_dir: ChildPath, pub python_dir: ChildPath, @@ -212,6 +213,16 @@ impl TestContext { self } + /// Adds a filter that ignores platform information in a Python installation key. + pub fn with_filtered_python_keys(mut self) -> Self { + // Filter platform keys + self.filters.push(( + r"((?:cpython|pypy)-\d+\.\d+(?:\.(?:\[X\]|\d+))?[a-z]?(?:\+[a-z]+)?)-.*".to_string(), + "$1-[PLATFORM]".to_string(), + )); + self + } + /// Discover the path to the XDG state directory. We use this, rather than the OS-specific /// temporary directory, because on macOS (and Windows on GitHub Actions), they involve /// symlinks. (On macOS, the temporary directory is, like `/var/...`, which resolves to @@ -416,6 +427,7 @@ impl TestContext { )); Self { + root: ChildPath::new(root.path()), temp_dir, cache_dir, python_dir, @@ -431,7 +443,7 @@ impl TestContext { /// Create a uv command for testing. pub fn command(&self) -> Command { - let mut command = Command::new(get_bin()); + let mut command = self.new_command(); self.add_shared_args(&mut command, true); command } @@ -464,6 +476,11 @@ impl TestContext { command.env(EnvVars::VIRTUAL_ENV, self.venv.as_os_str()); } + if cfg!(unix) { + // Avoid locale issues in tests + command.env("LC_ALL", "C"); + } + if cfg!(all(windows, debug_assertions)) { // TODO(konstin): Reduce stack usage in debug mode enough that the tests pass with the // default windows stack of 1MB @@ -473,7 +490,7 @@ impl TestContext { /// Create a `pip compile` command for testing. pub fn pip_compile(&self) -> Command { - let mut command = Command::new(get_bin()); + let mut command = self.new_command(); command.arg("pip").arg("compile"); self.add_shared_args(&mut command, true); command @@ -481,14 +498,14 @@ impl TestContext { /// Create a `pip compile` command for testing. pub fn pip_sync(&self) -> Command { - let mut command = Command::new(get_bin()); + let mut command = self.new_command(); command.arg("pip").arg("sync"); self.add_shared_args(&mut command, true); command } pub fn pip_show(&self) -> Command { - let mut command = Command::new(get_bin()); + let mut command = self.new_command(); command.arg("pip").arg("show"); self.add_shared_args(&mut command, true); command @@ -496,7 +513,7 @@ impl TestContext { /// Create a `pip freeze` command with options shared across scenarios. pub fn pip_freeze(&self) -> Command { - let mut command = Command::new(get_bin()); + let mut command = self.new_command(); command.arg("pip").arg("freeze"); self.add_shared_args(&mut command, true); command @@ -504,14 +521,14 @@ impl TestContext { /// Create a `pip check` command with options shared across scenarios. pub fn pip_check(&self) -> Command { - let mut command = Command::new(get_bin()); + let mut command = self.new_command(); command.arg("pip").arg("check"); self.add_shared_args(&mut command, true); command } pub fn pip_list(&self) -> Command { - let mut command = Command::new(get_bin()); + let mut command = self.new_command(); command.arg("pip").arg("list"); self.add_shared_args(&mut command, true); command @@ -519,7 +536,7 @@ impl TestContext { /// Create a `uv venv` command pub fn venv(&self) -> Command { - let mut command = Command::new(get_bin()); + let mut command = self.new_command(); command.arg("venv"); self.add_shared_args(&mut command, false); command @@ -527,7 +544,7 @@ impl TestContext { /// Create a `pip install` command with options shared across scenarios. pub fn pip_install(&self) -> Command { - let mut command = Command::new(get_bin()); + let mut command = self.new_command(); command.arg("pip").arg("install"); self.add_shared_args(&mut command, true); command @@ -535,7 +552,7 @@ impl TestContext { /// Create a `pip uninstall` command with options shared across scenarios. pub fn pip_uninstall(&self) -> Command { - let mut command = Command::new(get_bin()); + let mut command = self.new_command(); command.arg("pip").arg("uninstall"); self.add_shared_args(&mut command, true); command @@ -543,7 +560,7 @@ impl TestContext { /// Create a `pip tree` command for testing. pub fn pip_tree(&self) -> Command { - let mut command = Command::new(get_bin()); + let mut command = self.new_command(); command.arg("pip").arg("tree"); self.add_shared_args(&mut command, true); command @@ -552,7 +569,7 @@ impl TestContext { /// Create a `uv help` command with options shared across scenarios. #[allow(clippy::unused_self)] pub fn help(&self) -> Command { - let mut command = Command::new(get_bin()); + let mut command = self.new_command(); command.arg("help"); command.env_remove(EnvVars::UV_CACHE_DIR); @@ -565,9 +582,10 @@ impl TestContext { command } - /// Create a `uv init` command with options shared across scenarios. + /// Create a `uv init` command with options shared across scenarios and + /// isolated from any git repository that may exist in a parent directory. pub fn init(&self) -> Command { - let mut command = Command::new(get_bin()); + let mut command = self.new_command(); command.arg("init"); self.add_shared_args(&mut command, false); command @@ -575,7 +593,7 @@ impl TestContext { /// Create a `uv sync` command with options shared across scenarios. pub fn sync(&self) -> Command { - let mut command = Command::new(get_bin()); + let mut command = self.new_command(); command.arg("sync"); self.add_shared_args(&mut command, false); command @@ -583,7 +601,7 @@ impl TestContext { /// Create a `uv lock` command with options shared across scenarios. pub fn lock(&self) -> Command { - let mut command = Command::new(get_bin()); + let mut command = self.new_command(); command.arg("lock"); self.add_shared_args(&mut command, false); command @@ -591,7 +609,7 @@ impl TestContext { /// Create a `uv export` command with options shared across scenarios. pub fn export(&self) -> Command { - let mut command = Command::new(get_bin()); + let mut command = self.new_command(); command.arg("export"); self.add_shared_args(&mut command, false); command @@ -599,16 +617,15 @@ impl TestContext { /// Create a `uv build` command with options shared across scenarios. pub fn build(&self) -> Command { - let mut command = Command::new(get_bin()); + let mut command = self.new_command(); command.arg("build"); self.add_shared_args(&mut command, false); command } /// Create a `uv publish` command with options shared across scenarios. - #[expect(clippy::unused_self)] // For consistency pub fn publish(&self) -> Command { - let mut command = Command::new(get_bin()); + let mut command = self.new_command(); command.arg("publish"); if cfg!(all(windows, debug_assertions)) { @@ -622,7 +639,7 @@ impl TestContext { /// Create a `uv python find` command with options shared across scenarios. pub fn python_find(&self) -> Command { - let mut command = Command::new(get_bin()); + let mut command = self.new_command(); command .arg("python") .arg("find") @@ -633,22 +650,55 @@ impl TestContext { command } - /// Create a `uv python pin` command with options shared across scenarios. - pub fn python_pin(&self) -> Command { - let mut command = Command::new(get_bin()); + /// Create a `uv python install` command with options shared across scenarios. + pub fn python_install(&self) -> Command { + let mut command = self.new_command(); + let managed = self.temp_dir.join("managed"); + let bin = self.temp_dir.join("bin"); + self.add_shared_args(&mut command, true); command .arg("python") - .arg("pin") - .env(EnvVars::UV_PREVIEW, "1") - .env(EnvVars::UV_PYTHON_INSTALL_DIR, "") + .arg("install") + .env(EnvVars::UV_PYTHON_INSTALL_DIR, managed) + .env(EnvVars::UV_PYTHON_BIN_DIR, bin.as_os_str()) + .env( + EnvVars::PATH, + std::env::join_paths( + std::iter::once(bin) + .chain(std::env::split_paths(&env::var("PATH").unwrap_or_default())), + ) + .unwrap(), + ) .current_dir(&self.temp_dir); + command + } + + /// Create a `uv python uninstall` command with options shared across scenarios. + pub fn python_uninstall(&self) -> Command { + let mut command = self.new_command(); + let managed = self.temp_dir.join("managed"); + let bin = self.temp_dir.join("bin"); + self.add_shared_args(&mut command, true); + command + .arg("python") + .arg("uninstall") + .env(EnvVars::UV_PYTHON_INSTALL_DIR, managed) + .env(EnvVars::UV_PYTHON_BIN_DIR, bin) + .current_dir(&self.temp_dir); + command + } + + /// Create a `uv python pin` command with options shared across scenarios. + pub fn python_pin(&self) -> Command { + let mut command = self.new_command(); + command.arg("python").arg("pin"); self.add_shared_args(&mut command, true); command } /// Create a `uv python dir` command with options shared across scenarios. pub fn python_dir(&self) -> Command { - let mut command = Command::new(get_bin()); + let mut command = self.new_command(); command.arg("python").arg("dir"); self.add_shared_args(&mut command, true); command @@ -656,7 +706,7 @@ impl TestContext { /// Create a `uv run` command with options shared across scenarios. pub fn run(&self) -> Command { - let mut command = Command::new(get_bin()); + let mut command = self.new_command(); command.arg("run").env(EnvVars::UV_SHOW_RESOLUTION, "1"); self.add_shared_args(&mut command, true); command @@ -664,7 +714,7 @@ impl TestContext { /// Create a `uv tool run` command with options shared across scenarios. pub fn tool_run(&self) -> Command { - let mut command = Command::new(get_bin()); + let mut command = self.new_command(); command .arg("tool") .arg("run") @@ -675,7 +725,7 @@ impl TestContext { /// Create a `uv upgrade run` command with options shared across scenarios. pub fn tool_upgrade(&self) -> Command { - let mut command = Command::new(get_bin()); + let mut command = self.new_command(); command.arg("tool").arg("upgrade"); self.add_shared_args(&mut command, false); command @@ -683,7 +733,7 @@ impl TestContext { /// Create a `uv tool install` command with options shared across scenarios. pub fn tool_install(&self) -> Command { - let mut command = Command::new(get_bin()); + let mut command = self.new_command(); command.arg("tool").arg("install"); self.add_shared_args(&mut command, false); command.env(EnvVars::UV_EXCLUDE_NEWER, EXCLUDE_NEWER); @@ -692,7 +742,7 @@ impl TestContext { /// Create a `uv tool list` command with options shared across scenarios. pub fn tool_list(&self) -> Command { - let mut command = Command::new(get_bin()); + let mut command = self.new_command(); command.arg("tool").arg("list"); self.add_shared_args(&mut command, false); command @@ -700,7 +750,7 @@ impl TestContext { /// Create a `uv tool dir` command with options shared across scenarios. pub fn tool_dir(&self) -> Command { - let mut command = Command::new(get_bin()); + let mut command = self.new_command(); command.arg("tool").arg("dir"); self.add_shared_args(&mut command, false); command @@ -708,7 +758,7 @@ impl TestContext { /// Create a `uv tool uninstall` command with options shared across scenarios. pub fn tool_uninstall(&self) -> Command { - let mut command = Command::new(get_bin()); + let mut command = self.new_command(); command.arg("tool").arg("uninstall"); self.add_shared_args(&mut command, false); command @@ -716,7 +766,7 @@ impl TestContext { /// Create a `uv add` command for the given requirements. pub fn add(&self) -> Command { - let mut command = Command::new(get_bin()); + let mut command = self.new_command(); command.arg("add"); self.add_shared_args(&mut command, false); command @@ -724,7 +774,7 @@ impl TestContext { /// Create a `uv remove` command for the given requirements. pub fn remove(&self) -> Command { - let mut command = Command::new(get_bin()); + let mut command = self.new_command(); command.arg("remove"); self.add_shared_args(&mut command, false); command @@ -732,7 +782,7 @@ impl TestContext { /// Create a `uv tree` command with options shared across scenarios. pub fn tree(&self) -> Command { - let mut command = Command::new(get_bin()); + let mut command = self.new_command(); command.arg("tree"); self.add_shared_args(&mut command, false); command @@ -740,7 +790,7 @@ impl TestContext { /// Create a `uv cache clean` command. pub fn clean(&self) -> Command { - let mut command = Command::new(get_bin()); + let mut command = self.new_command(); command.arg("cache").arg("clean"); self.add_shared_args(&mut command, false); command @@ -748,7 +798,7 @@ impl TestContext { /// Create a `uv cache prune` command. pub fn prune(&self) -> Command { - let mut command = Command::new(get_bin()); + let mut command = self.new_command(); command.arg("cache").arg("prune"); self.add_shared_args(&mut command, false); command @@ -758,7 +808,7 @@ impl TestContext { /// /// Note that this command is hidden and only invoking it through a build frontend is supported. pub fn build_backend(&self) -> Command { - let mut command = Command::new(get_bin()); + let mut command = self.new_command(); command.arg("build-backend"); self.add_shared_args(&mut command, false); command @@ -770,7 +820,7 @@ impl TestContext { /// Run the given python code and check whether it succeeds. pub fn assert_command(&self, command: &str) -> Assert { - Command::new(venv_to_interpreter(&self.venv)) + self.new_command_with(&venv_to_interpreter(&self.venv)) // Our tests change files in <1s, so we must disable CPython bytecode caching or we'll get stale files // https://github.com/python/cpython/issues/75953 .arg("-B") @@ -782,7 +832,7 @@ impl TestContext { /// Run the given python file and check whether it succeeds. pub fn assert_file(&self, file: impl AsRef) -> Assert { - Command::new(venv_to_interpreter(&self.venv)) + self.new_command_with(&venv_to_interpreter(&self.venv)) // Our tests change files in <1s, so we must disable CPython bytecode caching or we'll get stale files // https://github.com/python/cpython/issues/75953 .arg("-B") @@ -942,6 +992,33 @@ impl TestContext { fs_err::read_to_string(self.temp_dir.join(&file)) .unwrap_or_else(|_| panic!("Missing file: `{}`", file.user_display())) } + + /// Creates a new `Command` that is intended to be suitable for use in + /// all tests. + fn new_command(&self) -> Command { + self.new_command_with(&get_bin()) + } + + /// Creates a new `Command` that is intended to be suitable for use in + /// all tests, but with the given binary. + fn new_command_with(&self, bin: &Path) -> Command { + let mut command = Command::new(bin); + // I believe the intent of all tests is that they are run outside the + // context of an existing git repository. And when they aren't, state + // from the parent git repository can bleed into the behavior of `uv + // init` in a way that makes it difficult to test consistently. By + // setting GIT_CEILING_DIRECTORIES, we specifically prevent git from + // climbing up past the root of our test directory to look for any + // other git repos. + // + // If one wants to write a test specifically targeting uv within a + // pre-existing git repository, then the test should make the parent + // git repo explicitly. The GIT_CEILING_DIRECTORIES here shouldn't + // impact it, since it only prevents git from discovering repositories + // at or above the root. + command.env(EnvVars::GIT_CEILING_DIRECTORIES, self.root.path()); + command + } } /// Creates a "unified" diff between the two line-oriented strings suitable diff --git a/crates/uv/tests/it/edit.rs b/crates/uv/tests/it/edit.rs index 9c5727715e8f..397dc53b3767 100644 --- a/crates/uv/tests/it/edit.rs +++ b/crates/uv/tests/it/edit.rs @@ -1025,8 +1025,8 @@ fn add_remove_dev() -> Result<()> { requires = ["setuptools>=42"] build-backend = "setuptools.build_meta" - [tool.uv] - dev-dependencies = [ + [dependency-groups] + dev = [ "anyio==3.7.0", ] "### @@ -1113,7 +1113,7 @@ fn add_remove_dev() -> Result<()> { ----- stdout ----- ----- stderr ----- - warning: `anyio` is a development dependency; try calling `uv remove --dev` + warning: `anyio` is in the `dev` group; try calling `uv remove --group dev` error: The dependency `anyio` could not be found in `dependencies` "###); @@ -1151,8 +1151,8 @@ fn add_remove_dev() -> Result<()> { requires = ["setuptools>=42"] build-backend = "setuptools.build_meta" - [tool.uv] - dev-dependencies = [] + [dependency-groups] + dev = [] "### ); }); @@ -1174,6 +1174,11 @@ fn add_remove_dev() -> Result<()> { name = "project" version = "0.1.0" source = { editable = "." } + + [package.metadata] + + [package.metadata.requires-dev] + dev = [] "### ); }); @@ -1737,67 +1742,115 @@ fn add_remove_workspace() -> Result<()> { Ok(()) } -/// Add a workspace dependency as an editable. +/// `uv add --dev` should update `dev-dependencies` (rather than `dependency-group.dev`) if a +/// dependency already exists in `dev-dependencies`. #[test] -fn add_workspace_editable() -> Result<()> { +fn update_existing_dev() -> Result<()> { let context = TestContext::new("3.12"); - let workspace = context.temp_dir.child("pyproject.toml"); - workspace.write_str(indoc! {r#" + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! {r#" [project] - name = "parent" + name = "project" version = "0.1.0" requires-python = ">=3.12" dependencies = [] - [tool.uv.workspace] - members = ["child1", "child2"] + [tool.uv] + dev-dependencies = ["anyio"] + + [dependency-groups] + dev = [] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" "#})?; - let pyproject_toml = context.temp_dir.child("child1/pyproject.toml"); - pyproject_toml.write_str(indoc! {r#" + uv_snapshot!(context.filters(), context.add().arg("anyio==3.7.0").arg("--dev"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 4 packages in [TIME] + Prepared 4 packages in [TIME] + Installed 4 packages in [TIME] + + anyio==3.7.0 + + idna==3.6 + + project==0.1.0 (from file://[TEMP_DIR]/) + + sniffio==1.3.1 + "###); + + let pyproject_toml = context.read("pyproject.toml"); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + pyproject_toml, @r###" [project] - name = "child1" + name = "project" version = "0.1.0" requires-python = ">=3.12" dependencies = [] + [tool.uv] + dev-dependencies = [ + "anyio==3.7.0", + ] + + [dependency-groups] + dev = [] + [build-system] requires = ["setuptools>=42"] build-backend = "setuptools.build_meta" - "#})?; + "### + ); + }); - let pyproject_toml = context.temp_dir.child("child2/pyproject.toml"); + Ok(()) +} + +/// `uv add --dev` should add to `dev-dependencies` (rather than `dependency-group.dev`) if it +/// exists. +#[test] +fn add_existing_dev() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); pyproject_toml.write_str(indoc! {r#" [project] - name = "child2" + name = "project" version = "0.1.0" requires-python = ">=3.12" dependencies = [] + [tool.uv] + dev-dependencies = [] + [build-system] requires = ["setuptools>=42"] build-backend = "setuptools.build_meta" "#})?; - let child1 = context.temp_dir.join("child1"); - let mut add_cmd = context.add(); - add_cmd.arg("child2").arg("--editable").current_dir(&child1); - - uv_snapshot!(context.filters(), add_cmd, @r###" + uv_snapshot!(context.filters(), context.add().arg("anyio==3.7.0").arg("--dev"), @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- - Resolved 3 packages in [TIME] - Prepared 2 packages in [TIME] - Installed 2 packages in [TIME] - + child1==0.1.0 (from file://[TEMP_DIR]/child1) - + child2==0.1.0 (from file://[TEMP_DIR]/child2) + Resolved 4 packages in [TIME] + Prepared 4 packages in [TIME] + Installed 4 packages in [TIME] + + anyio==3.7.0 + + idna==3.6 + + project==0.1.0 (from file://[TEMP_DIR]/) + + sniffio==1.3.1 "###); - let pyproject_toml = fs_err::read_to_string(child1.join("pyproject.toml"))?; + let pyproject_toml = context.read("pyproject.toml"); insta::with_settings!({ filters => context.filters(), @@ -1805,121 +1858,125 @@ fn add_workspace_editable() -> Result<()> { assert_snapshot!( pyproject_toml, @r###" [project] - name = "child1" + name = "project" version = "0.1.0" requires-python = ">=3.12" - dependencies = [ - "child2", + dependencies = [] + + [tool.uv] + dev-dependencies = [ + "anyio==3.7.0", ] [build-system] requires = ["setuptools>=42"] build-backend = "setuptools.build_meta" - - [tool.uv.sources] - child2 = { workspace = true } "### ); }); - // `uv add` implies a full lock and sync, including development dependencies. - let lock = context.read("uv.lock"); - - insta::with_settings!({ - filters => context.filters(), - }, { - assert_snapshot!( - lock, @r###" - version = 1 - requires-python = ">=3.12" - - [options] - exclude-newer = "2024-03-25T00:00:00Z" + Ok(()) +} - [manifest] - members = [ - "child1", - "child2", - "parent", - ] +/// `uv add --group dev` should update `dev-dependencies` (rather than `dependency-group.dev`) if a +/// dependency already exists. +#[test] +fn update_existing_dev_group() -> Result<()> { + let context = TestContext::new("3.12"); - [[package]] - name = "child1" + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! {r#" + [project] + name = "project" version = "0.1.0" - source = { editable = "child1" } - dependencies = [ - { name = "child2" }, - ] - - [package.metadata] - requires-dist = [{ name = "child2", editable = "child2" }] + requires-python = ">=3.12" + dependencies = [] - [[package]] - name = "child2" - version = "0.1.0" - source = { editable = "child2" } + [tool.uv] + dev-dependencies = ["anyio"] - [[package]] - name = "parent" - version = "0.1.0" - source = { virtual = "." } - "### - ); - }); + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#})?; - // Install from the lockfile. - uv_snapshot!(context.filters(), context.sync().arg("--frozen").current_dir(&child1), @r###" + uv_snapshot!(context.filters(), context.add().arg("anyio==3.7.0").arg("--group").arg("dev"), @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- - Audited 2 packages in [TIME] + Resolved 4 packages in [TIME] + Prepared 4 packages in [TIME] + Installed 4 packages in [TIME] + + anyio==3.7.0 + + idna==3.6 + + project==0.1.0 (from file://[TEMP_DIR]/) + + sniffio==1.3.1 "###); - Ok(()) -} - -/// Add a workspace dependency via its path. -#[test] -fn add_workspace_path() -> Result<()> { - let context = TestContext::new("3.12"); + let pyproject_toml = context.read("pyproject.toml"); - let workspace = context.temp_dir.child("pyproject.toml"); - workspace.write_str(indoc! {r#" + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + pyproject_toml, @r###" [project] - name = "parent" + name = "project" version = "0.1.0" requires-python = ">=3.12" dependencies = [] - [tool.uv.workspace] - members = ["child"] - "#})?; + [tool.uv] + dev-dependencies = [ + "anyio==3.7.0", + ] - let pyproject_toml = context.temp_dir.child("child/pyproject.toml"); + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "### + ); + }); + + Ok(()) +} + +/// `uv add --group dev` should add to `dependency-group` even if `dev-dependencies` exists. +#[test] +fn add_existing_dev_group() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); pyproject_toml.write_str(indoc! {r#" [project] - name = "child" + name = "project" version = "0.1.0" requires-python = ">=3.12" dependencies = [] + [tool.uv] + dev-dependencies = [] + [build-system] requires = ["setuptools>=42"] build-backend = "setuptools.build_meta" "#})?; - uv_snapshot!(context.filters(), context.add().arg("./child"), @r###" + uv_snapshot!(context.filters(), context.add().arg("anyio==3.7.0").arg("--group").arg("dev"), @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- - Resolved 2 packages in [TIME] - Prepared 1 package in [TIME] - Installed 1 package in [TIME] - + child==0.1.0 (from file://[TEMP_DIR]/child) + Resolved 4 packages in [TIME] + Prepared 4 packages in [TIME] + Installed 4 packages in [TIME] + + anyio==3.7.0 + + idna==3.6 + + project==0.1.0 (from file://[TEMP_DIR]/) + + sniffio==1.3.1 "###); let pyproject_toml = context.read("pyproject.toml"); @@ -1930,30 +1987,376 @@ fn add_workspace_path() -> Result<()> { assert_snapshot!( pyproject_toml, @r###" [project] - name = "parent" + name = "project" version = "0.1.0" requires-python = ">=3.12" - dependencies = [ - "child", - ] + dependencies = [] - [tool.uv.workspace] - members = ["child"] + [tool.uv] + dev-dependencies = [] - [tool.uv.sources] - child = { workspace = true } + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + + [dependency-groups] + dev = [ + "anyio==3.7.0", + ] "### ); }); - // `uv add` implies a full lock and sync, including development dependencies. - let lock = context.read("uv.lock"); + Ok(()) +} - insta::with_settings!({ - filters => context.filters(), - }, { - assert_snapshot!( - lock, @r###" +/// `uv remove --dev` should remove from both `dev-dependencies` and `dependency-group.dev`. +#[test] +fn remove_both_dev() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! {r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [] + + [tool.uv] + dev-dependencies = ["anyio"] + + [dependency-groups] + dev = ["anyio>=3.7.0"] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#})?; + + uv_snapshot!(context.filters(), context.remove().arg("anyio").arg("--dev"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + project==0.1.0 (from file://[TEMP_DIR]/) + "###); + + let pyproject_toml = context.read("pyproject.toml"); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + pyproject_toml, @r###" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [] + + [tool.uv] + dev-dependencies = [] + + [dependency-groups] + dev = [] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "### + ); + }); + + Ok(()) +} + +/// `uv remove --group dev` should remove from both `dev-dependencies` and `dependency-group.dev`. +#[test] +fn remove_both_dev_group() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! {r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [] + + [tool.uv] + dev-dependencies = ["anyio"] + + [dependency-groups] + dev = ["anyio>=3.7.0"] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#})?; + + uv_snapshot!(context.filters(), context.remove().arg("anyio").arg("--group").arg("dev"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + project==0.1.0 (from file://[TEMP_DIR]/) + "###); + + let pyproject_toml = context.read("pyproject.toml"); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + pyproject_toml, @r###" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [] + + [tool.uv] + dev-dependencies = [] + + [dependency-groups] + dev = [] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "### + ); + }); + + Ok(()) +} + +/// Add a workspace dependency as an editable. +#[test] +fn add_workspace_editable() -> Result<()> { + let context = TestContext::new("3.12"); + + let workspace = context.temp_dir.child("pyproject.toml"); + workspace.write_str(indoc! {r#" + [project] + name = "parent" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [] + + [tool.uv.workspace] + members = ["child1", "child2"] + "#})?; + + let pyproject_toml = context.temp_dir.child("child1/pyproject.toml"); + pyproject_toml.write_str(indoc! {r#" + [project] + name = "child1" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#})?; + + let pyproject_toml = context.temp_dir.child("child2/pyproject.toml"); + pyproject_toml.write_str(indoc! {r#" + [project] + name = "child2" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#})?; + + let child1 = context.temp_dir.join("child1"); + let mut add_cmd = context.add(); + add_cmd.arg("child2").arg("--editable").current_dir(&child1); + + uv_snapshot!(context.filters(), add_cmd, @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + Prepared 2 packages in [TIME] + Installed 2 packages in [TIME] + + child1==0.1.0 (from file://[TEMP_DIR]/child1) + + child2==0.1.0 (from file://[TEMP_DIR]/child2) + "###); + + let pyproject_toml = fs_err::read_to_string(child1.join("pyproject.toml"))?; + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + pyproject_toml, @r###" + [project] + name = "child1" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [ + "child2", + ] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + + [tool.uv.sources] + child2 = { workspace = true } + "### + ); + }); + + // `uv add` implies a full lock and sync, including development dependencies. + let lock = context.read("uv.lock"); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r###" + version = 1 + requires-python = ">=3.12" + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [manifest] + members = [ + "child1", + "child2", + "parent", + ] + + [[package]] + name = "child1" + version = "0.1.0" + source = { editable = "child1" } + dependencies = [ + { name = "child2" }, + ] + + [package.metadata] + requires-dist = [{ name = "child2", editable = "child2" }] + + [[package]] + name = "child2" + version = "0.1.0" + source = { editable = "child2" } + + [[package]] + name = "parent" + version = "0.1.0" + source = { virtual = "." } + "### + ); + }); + + // Install from the lockfile. + uv_snapshot!(context.filters(), context.sync().arg("--frozen").current_dir(&child1), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Audited 2 packages in [TIME] + "###); + + Ok(()) +} + +/// Add a workspace dependency via its path. +#[test] +fn add_workspace_path() -> Result<()> { + let context = TestContext::new("3.12"); + + let workspace = context.temp_dir.child("pyproject.toml"); + workspace.write_str(indoc! {r#" + [project] + name = "parent" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [] + + [tool.uv.workspace] + members = ["child"] + "#})?; + + let pyproject_toml = context.temp_dir.child("child/pyproject.toml"); + pyproject_toml.write_str(indoc! {r#" + [project] + name = "child" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#})?; + + uv_snapshot!(context.filters(), context.add().arg("./child"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + child==0.1.0 (from file://[TEMP_DIR]/child) + "###); + + let pyproject_toml = context.read("pyproject.toml"); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + pyproject_toml, @r###" + [project] + name = "parent" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [ + "child", + ] + + [tool.uv.workspace] + members = ["child"] + + [tool.uv.sources] + child = { workspace = true } + "### + ); + }); + + // `uv add` implies a full lock and sync, including development dependencies. + let lock = context.read("uv.lock"); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r###" version = 1 requires-python = ">=3.12" @@ -2533,15 +2936,11 @@ fn add_update_marker() -> Result<()> { ----- stdout ----- ----- stderr ----- - Resolved 8 packages in [TIME] - Prepared 3 packages in [TIME] - Uninstalled 3 packages in [TIME] - Installed 3 packages in [TIME] - - idna==3.6 - + idna==2.7 + Resolved 10 packages in [TIME] + Prepared 1 package in [TIME] + Uninstalled 1 package in [TIME] + Installed 1 package in [TIME] ~ project==0.1.0 (from file://[TEMP_DIR]/) - - urllib3==2.2.1 - + urllib3==1.23 "###); let pyproject_toml = context.read("pyproject.toml"); @@ -2576,7 +2975,7 @@ fn add_update_marker() -> Result<()> { ----- stdout ----- ----- stderr ----- - Resolved 8 packages in [TIME] + Resolved 10 packages in [TIME] Prepared 1 package in [TIME] Uninstalled 1 package in [TIME] Installed 1 package in [TIME] @@ -2623,10 +3022,10 @@ fn add_update_marker() -> Result<()> { Installed 1 package in [TIME] - certifi==2024.2.2 - charset-normalizer==3.3.2 - - idna==2.7 + - idna==3.6 ~ project==0.1.0 (from file://[TEMP_DIR]/) - requests==2.31.0 - - urllib3==1.23 + - urllib3==2.2.1 "###); let pyproject_toml = context.read("pyproject.toml"); @@ -3718,8 +4117,8 @@ fn add_lower_bound_dev() -> Result<()> { requires = ["setuptools>=42"] build-backend = "setuptools.build_meta" - [tool.uv] - dev-dependencies = [ + [dependency-groups] + dev = [ "anyio>=4.3.0", ] "### @@ -3998,12 +4397,13 @@ fn add_non_project() -> Result<()> { }, { assert_snapshot!( pyproject_toml, @r###" - [tool.uv] - dev-dependencies = [ - "iniconfig>=2.0.0", - ] [tool.uv.workspace] members = [] + + [dependency-groups] + dev = [ + "iniconfig>=2.0.0", + ] "### ); }); @@ -4193,35 +4593,268 @@ fn add_requirements_file() -> Result<()> { requires = ["setuptools>=42"] build-backend = "setuptools.build_meta" - [tool.uv.sources] - anyio = { git = "https://github.com/agronholm/anyio.git", rev = "4.4.0" } + [tool.uv.sources] + anyio = { git = "https://github.com/agronholm/anyio.git", rev = "4.4.0" } + "### + ); + }); + + // Passing a `setup.py` should fail. + uv_snapshot!(context.filters(), context.add().arg("-r").arg("setup.py"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Adding requirements from a `setup.py` is not supported in `uv add` + "###); + + // Passing nothing should fail. + uv_snapshot!(context.filters(), context.add(), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: the following required arguments were not provided: + > + + Usage: uv add --cache-dir [CACHE_DIR] --exclude-newer > + + For more information, try '--help'. + "###); + + Ok(()) +} + +/// Add a requirement to a dependency group. +#[test] +fn add_group() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! {r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [] + "#})?; + + uv_snapshot!(context.filters(), context.add().arg("anyio==3.7.0").arg("--group").arg("test"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 4 packages in [TIME] + Prepared 3 packages in [TIME] + Installed 3 packages in [TIME] + + anyio==3.7.0 + + idna==3.6 + + sniffio==1.3.1 + "###); + + let pyproject_toml = context.read("pyproject.toml"); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + pyproject_toml, @r###" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [] + + [dependency-groups] + test = [ + "anyio==3.7.0", + ] + "### + ); + }); + + uv_snapshot!(context.filters(), context.add().arg("requests").arg("--group").arg("test"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 8 packages in [TIME] + Prepared 4 packages in [TIME] + Installed 4 packages in [TIME] + + certifi==2024.2.2 + + charset-normalizer==3.3.2 + + requests==2.31.0 + + urllib3==2.2.1 + "###); + + let pyproject_toml = context.read("pyproject.toml"); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + pyproject_toml, @r###" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [] + + [dependency-groups] + test = [ + "anyio==3.7.0", + "requests>=2.31.0", + ] + "### + ); + }); + + uv_snapshot!(context.filters(), context.add().arg("anyio==3.7.0").arg("--group").arg("second"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 8 packages in [TIME] + Audited 3 packages in [TIME] + "###); + + let pyproject_toml = context.read("pyproject.toml"); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + pyproject_toml, @r###" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [] + + [dependency-groups] + test = [ + "anyio==3.7.0", + "requests>=2.31.0", + ] + second = [ + "anyio==3.7.0", + ] + "### + ); + }); + + assert!(context.temp_dir.join("uv.lock").exists()); + + Ok(()) +} + +/// Remomve a requirement from a dependency group. +#[test] +fn remove_group() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! {r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [] + + [dependency-groups] + test = [ + "anyio==3.7.0", + ] + "#})?; + + uv_snapshot!(context.filters(), context.remove().arg("anyio").arg("--group").arg("test"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Audited in [TIME] + "###); + + let pyproject_toml = context.read("pyproject.toml"); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + pyproject_toml, @r###" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [] + + [dependency-groups] + test = [] + "### + ); + }); + + uv_snapshot!(context.filters(), context.remove().arg("anyio").arg("--group").arg("test"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: The dependency `anyio` could not be found in `dependency-groups` + "###); + + let pyproject_toml = context.read("pyproject.toml"); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + pyproject_toml, @r###" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [] + + [dependency-groups] + test = [] "### ); }); - // Passing a `setup.py` should fail. - uv_snapshot!(context.filters(), context.add().arg("-r").arg("setup.py"), @r###" + uv_snapshot!(context.filters(), context.remove().arg("anyio").arg("--group").arg("test"), @r###" success: false exit_code: 2 ----- stdout ----- ----- stderr ----- - error: Adding requirements from a `setup.py` is not supported in `uv add` + error: The dependency `anyio` could not be found in `dependency-groups` "###); - // Passing nothing should fail. - uv_snapshot!(context.filters(), context.add(), @r###" + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! {r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["anyio"] + "#})?; + + uv_snapshot!(context.filters(), context.remove().arg("anyio").arg("--group").arg("test"), @r###" success: false exit_code: 2 ----- stdout ----- ----- stderr ----- - error: the following required arguments were not provided: - > - - Usage: uv add --cache-dir [CACHE_DIR] --exclude-newer > - - For more information, try '--help'. + warning: `anyio` is a production dependency + error: The dependency `anyio` could not be found in `dependency-groups` "###); Ok(()) @@ -5439,6 +6072,9 @@ fn add_index() -> Result<()> { version = "0.1.0" requires-python = ">=3.12" dependencies = [] + + [tool.uv] + constraint-dependencies = ["markupsafe<3"] "#})?; uv_snapshot!(context.filters(), context.add().arg("iniconfig==2.0.0").arg("--index").arg("https://pypi.org/simple").env_remove(EnvVars::UV_EXCLUDE_NEWER), @r###" @@ -5468,6 +6104,9 @@ fn add_index() -> Result<()> { "iniconfig==2.0.0", ] + [tool.uv] + constraint-dependencies = ["markupsafe<3"] + [[tool.uv.index]] url = "https://pypi.org/simple" "### @@ -5484,6 +6123,9 @@ fn add_index() -> Result<()> { version = 1 requires-python = ">=3.12" + [manifest] + constraints = [{ name = "markupsafe", specifier = "<3" }] + [[package]] name = "iniconfig" version = "2.0.0" @@ -5507,7 +6149,7 @@ fn add_index() -> Result<()> { ); }); - // Adding a subsequent index should put it _below_ the existing index. + // Adding a subsequent index should put it _above_ the existing index. uv_snapshot!(context.filters(), context.add().arg("jinja2").arg("--index").arg("pytorch=https://download.pytorch.org/whl/cu121").env_remove(EnvVars::UV_EXCLUDE_NEWER), @r###" success: true exit_code: 0 @@ -5537,15 +6179,18 @@ fn add_index() -> Result<()> { "jinja2>=3.1.3", ] + [tool.uv] + constraint-dependencies = ["markupsafe<3"] + [[tool.uv.index]] name = "pytorch" url = "https://download.pytorch.org/whl/cu121" - [[tool.uv.index]] - url = "https://pypi.org/simple" - [tool.uv.sources] jinja2 = { index = "pytorch" } + + [[tool.uv.index]] + url = "https://pypi.org/simple" "### ); }); @@ -5560,6 +6205,9 @@ fn add_index() -> Result<()> { version = 1 requires-python = ">=3.12" + [manifest] + constraints = [{ name = "markupsafe", specifier = "<3" }] + [[package]] name = "iniconfig" version = "2.0.0" @@ -5638,15 +6286,18 @@ fn add_index() -> Result<()> { "jinja2>=3.1.3", ] + [tool.uv] + constraint-dependencies = ["markupsafe<3"] + + [tool.uv.sources] + jinja2 = { index = "pytorch" } + [[tool.uv.index]] name = "pytorch" url = "https://test.pypi.org/simple" [[tool.uv.index]] url = "https://pypi.org/simple" - - [tool.uv.sources] - jinja2 = { index = "pytorch" } "### ); }); @@ -5661,6 +6312,9 @@ fn add_index() -> Result<()> { version = 1 requires-python = ">=3.12" + [manifest] + constraints = [{ name = "markupsafe", specifier = "<3" }] + [[package]] name = "iniconfig" version = "2.0.0" @@ -5748,15 +6402,18 @@ fn add_index() -> Result<()> { "typing-extensions>=4.12.2", ] + [tool.uv] + constraint-dependencies = ["markupsafe<3"] + + [tool.uv.sources] + jinja2 = { index = "pytorch" } + [[tool.uv.index]] url = "https://pypi.org/simple" [[tool.uv.index]] name = "pytorch" url = "https://test.pypi.org/simple" - - [tool.uv.sources] - jinja2 = { index = "pytorch" } "### ); }); @@ -5771,6 +6428,9 @@ fn add_index() -> Result<()> { version = 1 requires-python = ">=3.12" + [manifest] + constraints = [{ name = "markupsafe", specifier = "<3" }] + [[package]] name = "iniconfig" version = "2.0.0" @@ -5867,15 +6527,18 @@ fn add_index() -> Result<()> { "typing-extensions>=4.12.2", ] + [tool.uv] + constraint-dependencies = ["markupsafe<3"] + + [tool.uv.sources] + jinja2 = { index = "pytorch" } + [[tool.uv.index]] name = "pytorch" url = "https://test.pypi.org/simple" [[tool.uv.index]] url = "https://pypi.org/simple" - - [tool.uv.sources] - jinja2 = { index = "pytorch" } "### ); }); @@ -5890,6 +6553,9 @@ fn add_index() -> Result<()> { version = 1 requires-python = ">=3.12" + [manifest] + constraints = [{ name = "markupsafe", specifier = "<3" }] + [[package]] name = "iniconfig" version = "2.0.0" @@ -6031,31 +6697,232 @@ fn add_default_index_url() -> Result<()> { { url = "https://test-files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, ] - [[package]] + [[package]] + name = "project" + version = "0.1.0" + source = { virtual = "." } + dependencies = [ + { name = "iniconfig" }, + ] + + [package.metadata] + requires-dist = [{ name = "iniconfig", specifier = ">=2.0.0" }] + "### + ); + }); + + // Adding another `--default-index` replaces the current default. + uv_snapshot!(context.filters(), context.add().arg("typing-extensions").arg("--default-index").arg("https://pypi.org/simple"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + typing-extensions==4.10.0 + "###); + + let pyproject_toml = fs_err::read_to_string(context.temp_dir.join("pyproject.toml"))?; + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + pyproject_toml, @r###" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [ + "iniconfig>=2.0.0", + "typing-extensions>=4.10.0", + ] + + [[tool.uv.index]] + url = "https://pypi.org/simple" + default = true + "### + ); + }); + + let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock"))?; + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r###" + version = 1 + requires-python = ">=3.12" + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [[package]] + name = "iniconfig" + version = "2.0.0" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, + ] + + [[package]] + name = "project" + version = "0.1.0" + source = { virtual = "." } + dependencies = [ + { name = "iniconfig" }, + { name = "typing-extensions" }, + ] + + [package.metadata] + requires-dist = [ + { name = "iniconfig", specifier = ">=2.0.0" }, + { name = "typing-extensions", specifier = ">=4.10.0" }, + ] + + [[package]] + name = "typing-extensions" + version = "4.10.0" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/16/3a/0d26ce356c7465a19c9ea8814b960f8a36c3b0d07c323176620b7b483e44/typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb", size = 77558 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/de/dc04a3ea60b22624b51c703a84bbe0184abcd1d0b9bc8074b5d6b7ab90bb/typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475", size = 33926 }, + ] + "### + ); + }); + + Ok(()) +} + +#[test] +fn add_index_credentials() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! {r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [] + + # Set an internal index as the default, without credentials. + [[tool.uv.index]] + name = "internal" + url = "https://pypi-proxy.fly.dev/basic-auth/simple" + default = true + "#})?; + + // Provide credentials for the index via the environment variable. + uv_snapshot!(context.filters(), context.add().arg("iniconfig==2.0.0").env("UV_DEFAULT_INDEX", "https://public:heron@pypi-proxy.fly.dev/basic-auth/simple"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + iniconfig==2.0.0 + "###); + + let pyproject_toml = fs_err::read_to_string(context.temp_dir.join("pyproject.toml"))?; + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + pyproject_toml, @r###" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [ + "iniconfig==2.0.0", + ] + + # Set an internal index as the default, without credentials. + [[tool.uv.index]] + name = "internal" + url = "https://pypi-proxy.fly.dev/basic-auth/simple" + default = true + "### + ); + }); + + let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock"))?; + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r###" + version = 1 + requires-python = ">=3.12" + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [[package]] + name = "iniconfig" + version = "2.0.0" + source = { registry = "https://pypi-proxy.fly.dev/basic-auth/simple" } + sdist = { url = "https://pypi-proxy.fly.dev/basic-auth/files/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } + wheels = [ + { url = "https://pypi-proxy.fly.dev/basic-auth/files/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, + ] + + [[package]] + name = "project" + version = "0.1.0" + source = { virtual = "." } + dependencies = [ + { name = "iniconfig" }, + ] + + [package.metadata] + requires-dist = [{ name = "iniconfig", specifier = "==2.0.0" }] + "### + ); + }); + + Ok(()) +} + +#[test] +fn add_index_comments() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! {r#" + [project] name = "project" version = "0.1.0" - source = { virtual = "." } - dependencies = [ - { name = "iniconfig" }, - ] + requires-python = ">=3.12" + dependencies = [] - [package.metadata] - requires-dist = [{ name = "iniconfig", specifier = ">=2.0.0" }] - "### - ); - }); + [[tool.uv.index]] + name = "internal" + url = "https://test.pypi.org/simple" # This is a test index. + default = true + "#})?; - // Adding another `--default-index` replaces the current default. - uv_snapshot!(context.filters(), context.add().arg("typing-extensions").arg("--default-index").arg("https://pypi.org/simple"), @r###" + // Preserve the comment on the index URL, despite replacing it. + uv_snapshot!(context.filters(), context.add().arg("iniconfig==2.0.0").env("UV_DEFAULT_INDEX", "https://pypi.org/simple"), @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- - Resolved 3 packages in [TIME] + Resolved 2 packages in [TIME] Prepared 1 package in [TIME] Installed 1 package in [TIME] - + typing-extensions==4.10.0 + + iniconfig==2.0.0 "###); let pyproject_toml = fs_err::read_to_string(context.temp_dir.join("pyproject.toml"))?; @@ -6070,12 +6937,12 @@ fn add_default_index_url() -> Result<()> { version = "0.1.0" requires-python = ">=3.12" dependencies = [ - "iniconfig>=2.0.0", - "typing-extensions>=4.10.0", + "iniconfig==2.0.0", ] [[tool.uv.index]] - url = "https://pypi.org/simple" + name = "internal" + url = "https://pypi.org/simple" # This is a test index. default = true "### ); @@ -6109,23 +6976,10 @@ fn add_default_index_url() -> Result<()> { source = { virtual = "." } dependencies = [ { name = "iniconfig" }, - { name = "typing-extensions" }, ] [package.metadata] - requires-dist = [ - { name = "iniconfig", specifier = ">=2.0.0" }, - { name = "typing-extensions", specifier = ">=4.10.0" }, - ] - - [[package]] - name = "typing-extensions" - version = "4.10.0" - source = { registry = "https://pypi.org/simple" } - sdist = { url = "https://files.pythonhosted.org/packages/16/3a/0d26ce356c7465a19c9ea8814b960f8a36c3b0d07c323176620b7b483e44/typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb", size = 77558 } - wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/de/dc04a3ea60b22624b51c703a84bbe0184abcd1d0b9bc8074b5d6b7ab90bb/typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475", size = 33926 }, - ] + requires-dist = [{ name = "iniconfig", specifier = "==2.0.0" }] "### ); }); @@ -6256,13 +7110,13 @@ fn add_self() -> Result<()> { requires = ["setuptools>=42"] build-backend = "setuptools.build_meta" - [tool.uv] - dev-dependencies = [ - "anyio[types]", - ] - [tool.uv.sources] anyio = { workspace = true } + + [dependency-groups] + dev = [ + "anyio[types]", + ] "### ); }); @@ -6403,3 +7257,240 @@ fn add_preserves_open_bracket_comment() -> Result<()> { }); Ok(()) } + +#[test] +fn add_preserves_empty_comment() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! {r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [ + # First line. + # Second line. + ] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#})?; + + uv_snapshot!(context.filters(), context.add().arg("anyio==3.7.0"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 4 packages in [TIME] + Prepared 4 packages in [TIME] + Installed 4 packages in [TIME] + + anyio==3.7.0 + + idna==3.6 + + project==0.1.0 (from file://[TEMP_DIR]/) + + sniffio==1.3.1 + "###); + + let pyproject_toml = context.read("pyproject.toml"); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + pyproject_toml, @r###" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [ + # First line. + # Second line. + "anyio==3.7.0", + ] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "### + ); + }); + + Ok(()) +} + +#[test] +fn add_preserves_trailing_comment() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! {r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [ + "idna", + "iniconfig", # Use iniconfig. + # First line. + # Second line. + ] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#})?; + + uv_snapshot!(context.filters(), context.add().arg("anyio==3.7.0"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 5 packages in [TIME] + Prepared 5 packages in [TIME] + Installed 5 packages in [TIME] + + anyio==3.7.0 + + idna==3.6 + + iniconfig==2.0.0 + + project==0.1.0 (from file://[TEMP_DIR]/) + + sniffio==1.3.1 + "###); + + let pyproject_toml = context.read("pyproject.toml"); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + pyproject_toml, @r###" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [ + "anyio==3.7.0", + "idna", + "iniconfig", # Use iniconfig. + # First line. + # Second line. + ] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "### + ); + }); + + uv_snapshot!(context.filters(), context.add().arg("typing-extensions"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 6 packages in [TIME] + Prepared 2 packages in [TIME] + Uninstalled 1 package in [TIME] + Installed 2 packages in [TIME] + ~ project==0.1.0 (from file://[TEMP_DIR]/) + + typing-extensions==4.10.0 + "###); + + let pyproject_toml = context.read("pyproject.toml"); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + pyproject_toml, @r###" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [ + "anyio==3.7.0", + "idna", + "iniconfig", # Use iniconfig. + # First line. + # Second line. + "typing-extensions>=4.10.0", + ] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "### + ); + }); + + Ok(()) +} + +#[test] +fn add_preserves_trailing_depth() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! {r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [ + "idna", + "iniconfig",# Use iniconfig. + # First line. + # Second line. + ] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#})?; + + uv_snapshot!(context.filters(), context.add().arg("anyio==3.7.0"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 5 packages in [TIME] + Prepared 5 packages in [TIME] + Installed 5 packages in [TIME] + + anyio==3.7.0 + + idna==3.6 + + iniconfig==2.0.0 + + project==0.1.0 (from file://[TEMP_DIR]/) + + sniffio==1.3.1 + "###); + + let pyproject_toml = context.read("pyproject.toml"); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + pyproject_toml, @r###" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [ + "anyio==3.7.0", + "idna", + "iniconfig", # Use iniconfig. + # First line. + # Second line. + ] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "### + ); + }); + + Ok(()) +} diff --git a/crates/uv/tests/it/export.rs b/crates/uv/tests/it/export.rs index e56c300e8339..3b6f0666fbfe 100644 --- a/crates/uv/tests/it/export.rs +++ b/crates/uv/tests/it/export.rs @@ -1,7 +1,7 @@ #![allow(clippy::disallowed_types)] use crate::common::{apply_filters, uv_snapshot, TestContext}; -use anyhow::Result; +use anyhow::{Ok, Result}; use assert_cmd::assert::OutputAssertExt; use assert_fs::prelude::*; use std::process::Stdio; @@ -1013,3 +1013,107 @@ fn no_editable() -> Result<()> { Ok(()) } + +#[test] +fn export_group() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["typing-extensions"] + [dependency-groups] + foo = ["anyio ; sys_platform == 'darwin'"] + bar = ["iniconfig"] + dev = ["sniffio"] + "#, + )?; + + context.lock().assert().success(); + + uv_snapshot!(context.filters(), context.export(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv export --cache-dir [CACHE_DIR] + sniffio==1.3.1 \ + --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc \ + --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 + typing-extensions==4.10.0 \ + --hash=sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb \ + --hash=sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475 + + ----- stderr ----- + Resolved 6 packages in [TIME] + "###); + + uv_snapshot!(context.filters(), context.export().arg("--only-group").arg("bar"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv export --cache-dir [CACHE_DIR] --only-group bar + iniconfig==2.0.0 \ + --hash=sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3 \ + --hash=sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374 + + ----- stderr ----- + Resolved 6 packages in [TIME] + "###); + + uv_snapshot!(context.filters(), context.export().arg("--group").arg("foo"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv export --cache-dir [CACHE_DIR] --group foo + anyio==4.3.0 ; sys_platform == 'darwin' \ + --hash=sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6 \ + --hash=sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8 + idna==3.6 ; sys_platform == 'darwin' \ + --hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \ + --hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f + sniffio==1.3.1 \ + --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc \ + --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 + typing-extensions==4.10.0 \ + --hash=sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb \ + --hash=sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475 + + ----- stderr ----- + Resolved 6 packages in [TIME] + "###); + + uv_snapshot!(context.filters(), context.export().arg("--group").arg("foo").arg("--group").arg("bar"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv export --cache-dir [CACHE_DIR] --group foo --group bar + anyio==4.3.0 ; sys_platform == 'darwin' \ + --hash=sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6 \ + --hash=sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8 + idna==3.6 ; sys_platform == 'darwin' \ + --hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \ + --hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f + iniconfig==2.0.0 \ + --hash=sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3 \ + --hash=sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374 + sniffio==1.3.1 \ + --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc \ + --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 + typing-extensions==4.10.0 \ + --hash=sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb \ + --hash=sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475 + + ----- stderr ----- + Resolved 6 packages in [TIME] + "###); + + Ok(()) +} diff --git a/crates/uv/tests/it/help.rs b/crates/uv/tests/it/help.rs index ee9bf082660c..faf837a7a358 100644 --- a/crates/uv/tests/it/help.rs +++ b/crates/uv/tests/it/help.rs @@ -54,7 +54,7 @@ fn help() { --native-tls Whether to load TLS certificates from the platform's native certificate store [env: UV_NATIVE_TLS=] --offline Disable network access - --no-progress Hide all progress outputs + --no-progress Hide all progress outputs [env: UV_NO_PROGRESS=] --directory Change to the given directory prior to running the command --project Run the command within the given project directory --config-file The path to a `uv.toml` file to use for configuration [env: @@ -123,7 +123,7 @@ fn help_flag() { --native-tls Whether to load TLS certificates from the platform's native certificate store [env: UV_NATIVE_TLS=] --offline Disable network access - --no-progress Hide all progress outputs + --no-progress Hide all progress outputs [env: UV_NO_PROGRESS=] --directory Change to the given directory prior to running the command --project Run the command within the given project directory --config-file The path to a `uv.toml` file to use for configuration [env: @@ -191,7 +191,7 @@ fn help_short_flag() { --native-tls Whether to load TLS certificates from the platform's native certificate store [env: UV_NATIVE_TLS=] --offline Disable network access - --no-progress Hide all progress outputs + --no-progress Hide all progress outputs [env: UV_NO_PROGRESS=] --directory Change to the given directory prior to running the command --project Run the command within the given project directory --config-file The path to a `uv.toml` file to use for configuration [env: @@ -211,7 +211,7 @@ fn help_short_flag() { fn help_subcommand() { let context = TestContext::new_with_versions(&[]); - uv_snapshot!(context.filters(), context.help().arg("python"), @r##" + uv_snapshot!(context.filters(), context.help().arg("python"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -275,8 +275,10 @@ fn help_subcommand() { --cache-dir [CACHE_DIR] Path to the cache directory. - Defaults to `$HOME/Library/Caches/uv` on macOS, `$XDG_CACHE_HOME/uv` or `$HOME/.cache/uv` - on Linux, and `%LOCALAPPDATA%/uv/cache` on Windows. + Defaults to `$XDG_CACHE_HOME/uv` or `$HOME/.cache/uv` on macOS and Linux, and + `%LOCALAPPDATA%/uv/cache` on Windows. + + To view the location of the cache directory, run `uv cache dir`. [env: UV_CACHE_DIR=] @@ -344,6 +346,8 @@ fn help_subcommand() { Hide all progress outputs. For example, spinners or progress bars. + + [env: UV_NO_PROGRESS=] --directory Change to the given directory prior to running the command. @@ -392,14 +396,14 @@ fn help_subcommand() { ----- stderr ----- - "##); + "###); } #[test] fn help_subsubcommand() { let context = TestContext::new_with_versions(&[]); - uv_snapshot!(context.filters(), context.help().arg("python").arg("install"), @r##" + uv_snapshot!(context.filters(), context.help().arg("python").arg("install"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -407,13 +411,15 @@ fn help_subsubcommand() { Multiple Python versions may be requested. - Supports CPython and PyPy. - - CPython distributions are downloaded from the `python-build-standalone` project. + Supports CPython and PyPy. CPython distributions are downloaded from the `python-build-standalone` + project. PyPy distributions are downloaded from `python.org`. Python versions are installed into the uv Python directory, which can be retrieved with `uv python - dir`. A `python` executable is not made globally available, managed Python versions are only used in - uv commands or in active virtual environments. + dir`. + + A `python` executable is not made globally available, managed Python versions are only used in uv + commands or in active virtual environments. There is experimental support for adding Python + executables to the `PATH` — use the `--preview` flag to enable this behavior. See `uv help python` to view supported request formats. @@ -445,8 +451,10 @@ fn help_subsubcommand() { --cache-dir [CACHE_DIR] Path to the cache directory. - Defaults to `$HOME/Library/Caches/uv` on macOS, `$XDG_CACHE_HOME/uv` or `$HOME/.cache/uv` - on Linux, and `%LOCALAPPDATA%/uv/cache` on Windows. + Defaults to `$XDG_CACHE_HOME/uv` or `$HOME/.cache/uv` on macOS and Linux, and + `%LOCALAPPDATA%/uv/cache` on Windows. + + To view the location of the cache directory, run `uv cache dir`. [env: UV_CACHE_DIR=] @@ -514,6 +522,8 @@ fn help_subsubcommand() { Hide all progress outputs. For example, spinners or progress bars. + + [env: UV_NO_PROGRESS=] --directory Change to the given directory prior to running the command. @@ -560,7 +570,7 @@ fn help_subsubcommand() { ----- stderr ----- - "##); + "###); } #[test] @@ -603,7 +613,7 @@ fn help_flag_subcommand() { --native-tls Whether to load TLS certificates from the platform's native certificate store [env: UV_NATIVE_TLS=] --offline Disable network access - --no-progress Hide all progress outputs + --no-progress Hide all progress outputs [env: UV_NO_PROGRESS=] --directory Change to the given directory prior to running the command --project Run the command within the given project directory --config-file The path to a `uv.toml` file to use for configuration [env: @@ -657,7 +667,7 @@ fn help_flag_subsubcommand() { --native-tls Whether to load TLS certificates from the platform's native certificate store [env: UV_NATIVE_TLS=] --offline Disable network access - --no-progress Hide all progress outputs + --no-progress Hide all progress outputs [env: UV_NO_PROGRESS=] --directory Change to the given directory prior to running the command --project Run the command within the given project directory --config-file The path to a `uv.toml` file to use for configuration [env: @@ -803,7 +813,7 @@ fn help_with_global_option() { --native-tls Whether to load TLS certificates from the platform's native certificate store [env: UV_NATIVE_TLS=] --offline Disable network access - --no-progress Hide all progress outputs + --no-progress Hide all progress outputs [env: UV_NO_PROGRESS=] --directory Change to the given directory prior to running the command --project Run the command within the given project directory --config-file The path to a `uv.toml` file to use for configuration [env: @@ -908,7 +918,7 @@ fn help_with_no_pager() { --native-tls Whether to load TLS certificates from the platform's native certificate store [env: UV_NATIVE_TLS=] --offline Disable network access - --no-progress Hide all progress outputs + --no-progress Hide all progress outputs [env: UV_NO_PROGRESS=] --directory Change to the given directory prior to running the command --project Run the command within the given project directory --config-file The path to a `uv.toml` file to use for configuration [env: diff --git a/crates/uv/tests/it/init.rs b/crates/uv/tests/it/init.rs index 808eb4796b1b..5a972fb49161 100644 --- a/crates/uv/tests/it/init.rs +++ b/crates/uv/tests/it/init.rs @@ -1601,7 +1601,7 @@ fn init_virtual_project() -> Result<()> { filters => context.filters(), }, { assert_snapshot!( - pyproject, @r###" + pyproject, @r#" [project] name = "foo" version = "0.1.0" @@ -1609,14 +1609,7 @@ fn init_virtual_project() -> Result<()> { readme = "README.md" requires-python = ">=3.12" dependencies = [] - - [project.scripts] - foo = "foo:main" - - [build-system] - requires = ["hatchling"] - build-backend = "hatchling.build" - "### + "# ); }); @@ -1635,7 +1628,7 @@ fn init_virtual_project() -> Result<()> { filters => context.filters(), }, { assert_snapshot!( - pyproject, @r###" + pyproject, @r#" [project] name = "foo" version = "0.1.0" @@ -1644,16 +1637,9 @@ fn init_virtual_project() -> Result<()> { requires-python = ">=3.12" dependencies = [] - [project.scripts] - foo = "foo:main" - - [build-system] - requires = ["hatchling"] - build-backend = "hatchling.build" - [tool.uv.workspace] members = ["bar"] - "### + "# ); }); @@ -1730,7 +1716,7 @@ fn init_nested_virtual_workspace() -> Result<()> { filters => context.filters(), }, { assert_snapshot!( - pyproject, @r###" + pyproject, @r#" [project] name = "foo" version = "0.1.0" @@ -1738,14 +1724,7 @@ fn init_nested_virtual_workspace() -> Result<()> { readme = "README.md" requires-python = ">=3.12" dependencies = [] - - [project.scripts] - foo = "foo:main" - - [build-system] - requires = ["hatchling"] - build-backend = "hatchling.build" - "### + "# ); }); @@ -2536,6 +2515,45 @@ fn init_library_flit() -> Result<()> { Ok(()) } +/// Run `uv init --build-backend flit` should be equivalent to `uv init --package --build-backend flit`. +#[test] +fn init_backend_implies_package() { + let context = TestContext::new("3.12"); + + uv_snapshot!(context.filters(), context.init().arg("project").arg("--build-backend").arg("flit"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Initialized project `project` at `[TEMP_DIR]/project` + "#); + + let pyproject = context.read("project/pyproject.toml"); + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + pyproject, @r#" + [project] + name = "project" + version = "0.1.0" + description = "Add your description here" + readme = "README.md" + requires-python = ">=3.12" + dependencies = [] + + [project.scripts] + project = "project:main" + + [build-system] + requires = ["flit_core>=3.2,<4"] + build-backend = "flit_core.buildapi" + "# + ); + }); +} + /// Run `uv init --app --package --build-backend maturin` to create a packaged application project #[test] #[cfg(feature = "crates-io")] @@ -2626,7 +2644,7 @@ fn init_app_build_backend_maturin() -> Result<()> { #[pyfunction] fn hello_from_bin() -> String { - return "Hello from foo!".to_string(); + "Hello from foo!".to_string() } /// A Python module implemented in Rust. The name of this function must match @@ -2879,7 +2897,7 @@ fn init_lib_build_backend_maturin() -> Result<()> { #[pyfunction] fn hello_from_bin() -> String { - return "Hello from foo!".to_string(); + "Hello from foo!".to_string() } /// A Python module implemented in Rust. The name of this function must match diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs index 4426d4ac7f54..140206bd9323 100644 --- a/crates/uv/tests/it/lock.rs +++ b/crates/uv/tests/it/lock.rs @@ -3754,6 +3754,102 @@ fn lock_requires_python_exact() -> Result<()> { Ok(()) } +/// Fork, even with a single dependency, if the minimum Python version is increased. +#[test] +fn lock_requires_python_fork() -> Result<()> { + let context = TestContext::new("3.11"); + + let lockfile = context.temp_dir.join("uv.lock"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "warehouse" + version = "1.0.0" + requires-python = ">=3.9" + dependencies = ["uv ; python_version>='3.8'"] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#, + )?; + + uv_snapshot!(context.filters(), context.lock().env(EnvVars::UV_EXCLUDE_NEWER, "2024-08-29T00:00:00Z"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + "###); + + let lock = fs_err::read_to_string(&lockfile).unwrap(); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r###" + version = 1 + requires-python = ">=3.9" + + [options] + exclude-newer = "2024-08-29T00:00:00Z" + + [[package]] + name = "uv" + version = "0.4.0" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/0f/dc/94b6609d89693be22119f8ff7f586f6125de6d6ff096daa06b5250760563/uv-0.4.0.tar.gz", hash = "sha256:1658a17b7c4c0ad750fc44a7ef1196e058fb0c18873f54420c17f3ce807bfc24", size = 1807995 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/1f/eeddd9565b2627495ee9588c2852e3c267f893247726aa4ee967df70d388/uv-0.4.0-py3-none-linux_armv6l.whl", hash = "sha256:3870d045d878e3da6505f4ebae7ecf01761ec481ae5de5e30e57e8e58557d755", size = 10760426 }, + { url = "https://files.pythonhosted.org/packages/76/be/0e5f3d36a5811315c5e97ac860ab80885865c79f64fcf8cf72a8e978f08f/uv-0.4.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:de7171d3e3ea994c754e750b79c788735eea0e50a60f878225f23645f047dd5b", size = 11152293 }, + { url = "https://files.pythonhosted.org/packages/2e/86/9844e8ab08e25cbf2094e2fa1a7ad66563036bfed77b46986b6a2489c10c/uv-0.4.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:02e0295566454289348de502677e2240ad86f2cb5fa058504e1b2ca2a2ebf7e1", size = 10302231 }, + { url = "https://files.pythonhosted.org/packages/9d/75/28c3386d0649a5becc95b6fe323d7891548932aa93c8ed29c32b6ad3f52d/uv-0.4.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:6e0d1b257d87d46c1047f62fc32cddb46df510e7382dec232b4ebc2475cf0957", size = 10611491 }, + { url = "https://files.pythonhosted.org/packages/c9/50/8300c36a88cc1a62c261a84ebbdff0c69794825f26930d7a0ecee5d277b7/uv-0.4.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:787764145fb16f73eba04cce0855d18aeb0de3fc86e43aadd2ebe4992aa32c7f", size = 10579879 }, + { url = "https://files.pythonhosted.org/packages/a3/0f/eeb272ff4e86a39644e152a28fafa43798c1498b0ac39b2e2ae2c410d7be/uv-0.4.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c83f59c3326f7169f927603cc4b766e321fa941f1047f70ce388292e08d0966b", size = 11168249 }, + { url = "https://files.pythonhosted.org/packages/b9/94/6655626f14585124fda2eb039781ff74a996da69437f197f74b7ac75f7a7/uv-0.4.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:0d6a1420b9ae8f391e733626ef583d1b328070e952ce83b65e4afb514ae03086", size = 11957706 }, + { url = "https://files.pythonhosted.org/packages/c1/b5/821a343e33233b4efcd3ce725d648f89cce2ffbaaa232cda1ab094ef0d82/uv-0.4.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ef78d636b45e4b919d0bd3c17c2dc42600cdab2ad6e0dad971fb5a733398987", size = 11767024 }, + { url = "https://files.pythonhosted.org/packages/ce/4e/286122669389f87fb9fa57c5ccbc977b843ee69f54bdfc781520e6fe8a38/uv-0.4.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0671259b9a1ba67535382264ec8c000501d3bd9e3f9d28dbe6a57145a168fa31", size = 14705106 }, + { url = "https://files.pythonhosted.org/packages/0c/71/f7a8b9a0f49f2fa5c979bf7d10e83791043e3eec3e43d80099acee4368fd/uv-0.4.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85a94f21bebe4b3f452afe5fbc36ed326583c8b6cc7fe3091f5f571a727ed799", size = 11512897 }, + { url = "https://files.pythonhosted.org/packages/bf/91/b0b4cb51b5ec31ec45fa52eae2aaa91ad20196a66e94a3d593379195ca80/uv-0.4.0-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:d9eb82b0198f10cb5f0354d5c4483f6b304ac6f0b74131b88fbf092a2030268f", size = 10704876 }, + { url = "https://files.pythonhosted.org/packages/12/18/1055d2b5de7cc12fb83b2f3ba397869b8277d0a3c8524aa34fb96cc8f548/uv-0.4.0-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:90497e0413d76378000d8d62891d49e786065321ae9f68b933b0914f92cf3ea2", size = 10576751 }, + { url = "https://files.pythonhosted.org/packages/3d/05/637f37dc173635688e14f4f358d75b58d136a8d78c2fe60365fd56f7d5ae/uv-0.4.0-py3-none-musllinux_1_1_i686.whl", hash = "sha256:1e10d262f55857b4e85dc3b550cd4fa09714fa639d53886065f19aa1f09720f7", size = 11000442 }, + { url = "https://files.pythonhosted.org/packages/0a/21/bc467390c62493d6778c5c2c805375340b3d220073330011cd5e4562a161/uv-0.4.0-py3-none-musllinux_1_1_ppc64le.whl", hash = "sha256:80329b24feb52cf46187a50d6e163aad3afc52bc601218d876970d855bb71f9b", size = 12714226 }, + { url = "https://files.pythonhosted.org/packages/eb/12/9801ebf36cdd72baf695adef704999d830e824e78db7bf66491fc7712ca7/uv-0.4.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:e911e8e0c59144c54468fae5e4bcbd76f8bafc5c6ca33d14f88e2ce8a633f7ab", size = 11651071 }, + { url = "https://files.pythonhosted.org/packages/81/12/bd8bc40b3b88be4c75726b610302628edec6b2cbd4f58717403fb7cfc7c5/uv-0.4.0-py3-none-win32.whl", hash = "sha256:93861f0d0bf5c44ded97ee2b188f0c30e0521e7a51f3f63daf6e64766913f036", size = 10933271 }, + { url = "https://files.pythonhosted.org/packages/d6/c2/7219ee34993c50b3fedd81a7d77f27232df0bcf5451920dd16717fcc9b1f/uv-0.4.0-py3-none-win_amd64.whl", hash = "sha256:6b9b8db49928b71f926cf48150a34c69458447e554e7b65c1337d3e907bd7fb5", size = 12145169 }, + ] + + [[package]] + name = "warehouse" + version = "1.0.0" + source = { editable = "." } + dependencies = [ + { name = "uv" }, + ] + + [package.metadata] + requires-dist = [{ name = "uv", marker = "python_full_version >= '3.8'" }] + "### + ); + }); + + // Re-run with `--locked`. + uv_snapshot!(context.filters(), context.lock().arg("--locked").env(EnvVars::UV_EXCLUDE_NEWER, "2024-08-29T00:00:00Z"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + "###); + + Ok(()) +} + /// Lock a requirement from PyPI, respecting the `Requires-Python` metadata #[test] fn lock_requires_python_wheels() -> Result<()> { @@ -6304,6 +6400,7 @@ fn lock_redact_https() -> Result<()> { ----- stderr ----- error: Failed to prepare distributions Caused by: Failed to download `iniconfig==2.0.0` + Caused by: Failed to fetch: `https://pypi-proxy.fly.dev/basic-auth/files/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl` Caused by: HTTP status client error (401 Unauthorized) for url (https://pypi-proxy.fly.dev/basic-auth/files/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl) "###); @@ -6316,6 +6413,7 @@ fn lock_redact_https() -> Result<()> { ----- stderr ----- error: Failed to prepare distributions Caused by: Failed to download `iniconfig==2.0.0` + Caused by: Failed to fetch: `https://pypi-proxy.fly.dev/basic-auth/files/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl` Caused by: HTTP status client error (401 Unauthorized) for url (https://pypi-proxy.fly.dev/basic-auth/files/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl) "###); @@ -6355,6 +6453,7 @@ fn lock_redact_https() -> Result<()> { ----- stderr ----- error: Failed to prepare distributions Caused by: Failed to download `iniconfig==2.0.0` + Caused by: Failed to fetch: `https://pypi-proxy.fly.dev/basic-auth/files/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl` Caused by: HTTP status client error (401 Unauthorized) for url (https://pypi-proxy.fly.dev/basic-auth/files/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl) "###); @@ -6817,7 +6916,7 @@ fn lock_env_credentials() -> Result<()> { build-backend = "setuptools.build_meta" [[tool.uv.index]] - name = "proxy" + name = "internal-proxy" url = "https://pypi-proxy.fly.dev/basic-auth/simple" default = true "#, @@ -6838,8 +6937,8 @@ fn lock_env_credentials() -> Result<()> { // Provide credentials via environment variables. uv_snapshot!(context.filters(), context.lock() - .env(EnvVars::index_username("PROXY"), "public") - .env(EnvVars::index_password("PROXY"), "heron"), @r###" + .env(EnvVars::index_username("INTERNAL_PROXY"), "public") + .env(EnvVars::index_password("INTERNAL_PROXY"), "heron"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -9341,7 +9440,7 @@ fn lock_mismatched_sources() -> Result<()> { ----- stderr ----- error: Failed to build: `project @ file://[TEMP_DIR]/` - Caused by: Failed to parse entry for: `uv-public-pypackage` + Caused by: Failed to parse entry: `uv-public-pypackage` Caused by: Can't combine URLs from both `project.dependencies` and `tool.uv.sources` "###); @@ -10900,6 +10999,151 @@ fn lock_missing_metadata() -> Result<()> { Ok(()) } +/// Test backwards compatibility for `package.dependency-groups`. `package.dev-dependencies` was +/// accidentally renamed to `package.dependency-groups` in v0.4.27, which is technically out of +/// compliance with our lockfile versioning policy. In v0.4.28, we renamed it back to +/// `package.dev-dependencies`, with backwards compatibility for `package.dependency-groups`. +#[test] +fn lock_dev_dependencies_alias() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [] + + [tool.uv] + dev-dependencies = ["iniconfig"] + "#, + )?; + + let lock = context.temp_dir.child("uv.lock"); + + // Write a lockfile with `[package.dev-dependencies]`. + lock.write_str(r#" + version = 1 + requires-python = ">=3.12" + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [[package]] + name = "project" + version = "0.1.0" + source = { virtual = "." } + + [package.dependency-groups] + dev = [{ name = "iniconfig" }] + + [package.metadata] + + [package.metadata.dependency-groups] + dev = [{ name = "iniconfig" }] + + [[package]] + name = "iniconfig" + version = "2.0.0" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, + ] + "#)?; + + // Re-locking should be a no-op. + uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + "###); + + // If we add a package, re-locking should use `dependency-groups`. + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["typing-extensions"] + + [tool.uv] + dev-dependencies = ["iniconfig"] + "#, + )?; + + uv_snapshot!(context.filters(), context.lock(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + Added typing-extensions v4.10.0 + "###); + + let lock = context.read("uv.lock"); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r###" + version = 1 + requires-python = ">=3.12" + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [[package]] + name = "iniconfig" + version = "2.0.0" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, + ] + + [[package]] + name = "project" + version = "0.1.0" + source = { virtual = "." } + dependencies = [ + { name = "typing-extensions" }, + ] + + [package.dev-dependencies] + dev = [ + { name = "iniconfig" }, + ] + + [package.metadata] + requires-dist = [{ name = "typing-extensions" }] + + [package.metadata.requires-dev] + dev = [{ name = "iniconfig" }] + + [[package]] + name = "typing-extensions" + version = "4.10.0" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/16/3a/0d26ce356c7465a19c9ea8814b960f8a36c3b0d07c323176620b7b483e44/typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb", size = 77558 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/de/dc04a3ea60b22624b51c703a84bbe0184abcd1d0b9bc8074b5d6b7ab90bb/typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475", size = 33926 }, + ] + "### + ); + }); + + Ok(()) +} + /// Lock a `pyproject.toml`, reorder the dependencies, and ensure that the lockfile is _not_ updated /// on the next run. #[test] @@ -12202,25 +12446,23 @@ fn lock_non_project_conditional() -> Result<()> { Ok(()) } -/// `coverage` defines a `toml` extra, but it doesn't enable any dependencies after Python 3.11. +/// Lock a (legacy) non-project workspace root with `dependency-group`. #[test] -fn lock_dropped_dev_extra() -> Result<()> { - let context = TestContext::new("3.12"); +fn lock_non_project_group() -> Result<()> { + let context = TestContext::new("3.10"); let pyproject_toml = context.temp_dir.child("pyproject.toml"); pyproject_toml.write_str( r#" - [project] - name = "project" - version = "0.1.0" - requires-python = ">=3.12" - - [build-system] - requires = ["setuptools>=42"] - build-backend = "setuptools.build_meta" + [tool.uv.workspace] + members = [] [tool.uv] - dev-dependencies = ["coverage[toml]"] + dev-dependencies = ["anyio"] + + [dependency-groups] + lint = ["iniconfig"] + dev = ["typing-extensions"] "#, )?; @@ -12230,7 +12472,8 @@ fn lock_dropped_dev_extra() -> Result<()> { ----- stdout ----- ----- stderr ----- - Resolved 2 packages in [TIME] + warning: No `requires-python` value found in the workspace. Defaulting to `>=3.10`. + Resolved 6 packages in [TIME] "###); let lock = context.read("uv.lock"); @@ -12241,43 +12484,77 @@ fn lock_dropped_dev_extra() -> Result<()> { assert_snapshot!( lock, @r###" version = 1 - requires-python = ">=3.12" + requires-python = ">=3.10" [options] exclude-newer = "2024-03-25T00:00:00Z" + [manifest] + requirements = [ + { name = "anyio" }, + { name = "iniconfig" }, + { name = "typing-extensions" }, + ] + [[package]] - name = "coverage" - version = "7.4.4" + name = "anyio" + version = "4.3.0" source = { registry = "https://pypi.org/simple" } - sdist = { url = "https://files.pythonhosted.org/packages/bf/d5/f809d8b630cf4c11fe490e20037a343d12a74ec2783c6cdb5aee725e7137/coverage-7.4.4.tar.gz", hash = "sha256:c901df83d097649e257e803be22592aedfd5182f07b3cc87d640bbb9afd50f49", size = 783727 } + dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "sniffio" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/db/4d/3970183622f0330d3c23d9b8a5f52e365e50381fd484d08e3285104333d3/anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6", size = 159642 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/de/a54b245e781bfd6f3fd7ce5566a695686b5c25ee7c743f514e7634428972/coverage-7.4.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:201bef2eea65e0e9c56343115ba3814e896afe6d36ffd37bab783261db430f76", size = 206409 }, - { url = "https://files.pythonhosted.org/packages/88/92/07f9c593cd27e3c595b8cb83b95adad8c9ba3d611debceed097a5fd6be4b/coverage-7.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41c9c5f3de16b903b610d09650e5e27adbfa7f500302718c9ffd1c12cf9d6818", size = 206568 }, - { url = "https://files.pythonhosted.org/packages/41/6d/e142c823e5d4b24481f990da4cf9d2d577a6f4e1fb6faf39d9a4e42b1d43/coverage-7.4.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d898fe162d26929b5960e4e138651f7427048e72c853607f2b200909794ed978", size = 238920 }, - { url = "https://files.pythonhosted.org/packages/30/1a/105f0139df6a2adbcaa0c110711a46dbd9f59e93a09ca15a97d59c2564f2/coverage-7.4.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ea79bb50e805cd6ac058dfa3b5c8f6c040cb87fe83de10845857f5535d1db70", size = 236288 }, - { url = "https://files.pythonhosted.org/packages/98/79/185cb42910b6a2b2851980407c8445ac0da0750dff65e420e86f973c8396/coverage-7.4.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce4b94265ca988c3f8e479e741693d143026632672e3ff924f25fab50518dd51", size = 238223 }, - { url = "https://files.pythonhosted.org/packages/92/12/2303d1c543a11ea060dbc7144ed3174fc09107b5dd333649415c95ede58b/coverage-7.4.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:00838a35b882694afda09f85e469c96367daa3f3f2b097d846a7216993d37f4c", size = 245161 }, - { url = "https://files.pythonhosted.org/packages/96/5a/7d0e945c4759fe9d19aad1679dd3096aeb4cb9fcf0062fe24554dc4787b8/coverage-7.4.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:fdfafb32984684eb03c2d83e1e51f64f0906b11e64482df3c5db936ce3839d48", size = 243066 }, - { url = "https://files.pythonhosted.org/packages/f4/1b/79cdb7b11bbbd6540a536ac79412904b5c1f8903d5c1330084212afa8ceb/coverage-7.4.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:69eb372f7e2ece89f14751fbcbe470295d73ed41ecd37ca36ed2eb47512a6ab9", size = 244805 }, - { url = "https://files.pythonhosted.org/packages/af/7f/54dc676e7e63549838a3a7b95a8e11df80441bf7d64c6ce8f1cdbc0d1ff0/coverage-7.4.4-cp312-cp312-win32.whl", hash = "sha256:137eb07173141545e07403cca94ab625cc1cc6bc4c1e97b6e3846270e7e1fea0", size = 208590 }, - { url = "https://files.pythonhosted.org/packages/46/c4/1dfe76d96034a347d717a2392b004d42d45934cb94efa362ad41ca871f6e/coverage-7.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:d71eec7d83298f1af3326ce0ff1d0ea83c7cb98f72b577097f9083b20bdaf05e", size = 209415 }, + { url = "https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8", size = 85584 }, ] [[package]] - name = "project" - version = "0.1.0" - source = { editable = "." } + name = "exceptiongroup" + version = "1.2.0" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/8e/1c/beef724eaf5b01bb44b6338c8c3494eff7cab376fab4904cfbbc3585dc79/exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68", size = 26264 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/9a/5028fd52db10e600f1c4674441b968cf2ea4959085bfb5b99fb1250e5f68/exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14", size = 16210 }, + ] - [package.dev-dependencies] - dev = [ - { name = "coverage" }, + [[package]] + name = "idna" + version = "3.6" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567 }, ] - [package.metadata] + [[package]] + name = "iniconfig" + version = "2.0.0" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, + ] - [package.metadata.requires-dev] - dev = [{ name = "coverage", extras = ["toml"] }] + [[package]] + name = "sniffio" + version = "1.3.1" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, + ] + + [[package]] + name = "typing-extensions" + version = "4.10.0" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/16/3a/0d26ce356c7465a19c9ea8814b960f8a36c3b0d07c323176620b7b483e44/typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb", size = 77558 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/de/dc04a3ea60b22624b51c703a84bbe0184abcd1d0b9bc8074b5d6b7ab90bb/typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475", size = 33926 }, + ] "### ); }); @@ -12289,7 +12566,8 @@ fn lock_dropped_dev_extra() -> Result<()> { ----- stdout ----- ----- stderr ----- - Resolved 2 packages in [TIME] + warning: No `requires-python` value found in the workspace. Defaulting to `>=3.10`. + Resolved 6 packages in [TIME] "###); // Re-run with `--offline`. We shouldn't need a network connection to validate an @@ -12300,28 +12578,16 @@ fn lock_dropped_dev_extra() -> Result<()> { ----- stdout ----- ----- stderr ----- - Resolved 2 packages in [TIME] - "###); - - // Install from the lockfile. - uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Prepared 2 packages in [TIME] - Installed 2 packages in [TIME] - + coverage==7.4.4 - + project==0.1.0 (from file://[TEMP_DIR]/) + warning: No `requires-python` value found in the workspace. Defaulting to `>=3.10`. + Resolved 6 packages in [TIME] "###); Ok(()) } -/// Use a trailing slash on the declared index. +/// `coverage` defines a `toml` extra, but it doesn't enable any dependencies after Python 3.11. #[test] -fn lock_trailing_slash() -> Result<()> { +fn lock_dropped_dev_extra() -> Result<()> { let context = TestContext::new("3.12"); let pyproject_toml = context.temp_dir.child("pyproject.toml"); @@ -12331,14 +12597,13 @@ fn lock_trailing_slash() -> Result<()> { name = "project" version = "0.1.0" requires-python = ">=3.12" - dependencies = ["anyio==3.7.0"] [build-system] requires = ["setuptools>=42"] build-backend = "setuptools.build_meta" [tool.uv] - index-url = "https://pypi.org/simple/" + dev-dependencies = ["coverage[toml]"] "#, )?; @@ -12348,7 +12613,7 @@ fn lock_trailing_slash() -> Result<()> { ----- stdout ----- ----- stderr ----- - Resolved 4 packages in [TIME] + Resolved 2 packages in [TIME] "###); let lock = context.read("uv.lock"); @@ -12365,46 +12630,37 @@ fn lock_trailing_slash() -> Result<()> { exclude-newer = "2024-03-25T00:00:00Z" [[package]] - name = "anyio" - version = "3.7.0" - source = { registry = "https://pypi.org/simple/" } - dependencies = [ - { name = "idna" }, - { name = "sniffio" }, - ] - sdist = { url = "https://files.pythonhosted.org/packages/c6/b3/fefbf7e78ab3b805dec67d698dc18dd505af7a18a8dd08868c9b4fa736b5/anyio-3.7.0.tar.gz", hash = "sha256:275d9973793619a5374e1c89a4f4ad3f4b0a5510a2b5b939444bee8f4c4d37ce", size = 142737 } - wheels = [ - { url = "https://files.pythonhosted.org/packages/68/fe/7ce1926952c8a403b35029e194555558514b365ad77d75125f521a2bec62/anyio-3.7.0-py3-none-any.whl", hash = "sha256:eddca883c4175f14df8aedce21054bfca3adb70ffe76a9f607aef9d7fa2ea7f0", size = 80873 }, - ] - - [[package]] - name = "idna" - version = "3.6" - source = { registry = "https://pypi.org/simple/" } - sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426 } + name = "coverage" + version = "7.4.4" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/bf/d5/f809d8b630cf4c11fe490e20037a343d12a74ec2783c6cdb5aee725e7137/coverage-7.4.4.tar.gz", hash = "sha256:c901df83d097649e257e803be22592aedfd5182f07b3cc87d640bbb9afd50f49", size = 783727 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567 }, + { url = "https://files.pythonhosted.org/packages/a0/de/a54b245e781bfd6f3fd7ce5566a695686b5c25ee7c743f514e7634428972/coverage-7.4.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:201bef2eea65e0e9c56343115ba3814e896afe6d36ffd37bab783261db430f76", size = 206409 }, + { url = "https://files.pythonhosted.org/packages/88/92/07f9c593cd27e3c595b8cb83b95adad8c9ba3d611debceed097a5fd6be4b/coverage-7.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41c9c5f3de16b903b610d09650e5e27adbfa7f500302718c9ffd1c12cf9d6818", size = 206568 }, + { url = "https://files.pythonhosted.org/packages/41/6d/e142c823e5d4b24481f990da4cf9d2d577a6f4e1fb6faf39d9a4e42b1d43/coverage-7.4.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d898fe162d26929b5960e4e138651f7427048e72c853607f2b200909794ed978", size = 238920 }, + { url = "https://files.pythonhosted.org/packages/30/1a/105f0139df6a2adbcaa0c110711a46dbd9f59e93a09ca15a97d59c2564f2/coverage-7.4.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ea79bb50e805cd6ac058dfa3b5c8f6c040cb87fe83de10845857f5535d1db70", size = 236288 }, + { url = "https://files.pythonhosted.org/packages/98/79/185cb42910b6a2b2851980407c8445ac0da0750dff65e420e86f973c8396/coverage-7.4.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce4b94265ca988c3f8e479e741693d143026632672e3ff924f25fab50518dd51", size = 238223 }, + { url = "https://files.pythonhosted.org/packages/92/12/2303d1c543a11ea060dbc7144ed3174fc09107b5dd333649415c95ede58b/coverage-7.4.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:00838a35b882694afda09f85e469c96367daa3f3f2b097d846a7216993d37f4c", size = 245161 }, + { url = "https://files.pythonhosted.org/packages/96/5a/7d0e945c4759fe9d19aad1679dd3096aeb4cb9fcf0062fe24554dc4787b8/coverage-7.4.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:fdfafb32984684eb03c2d83e1e51f64f0906b11e64482df3c5db936ce3839d48", size = 243066 }, + { url = "https://files.pythonhosted.org/packages/f4/1b/79cdb7b11bbbd6540a536ac79412904b5c1f8903d5c1330084212afa8ceb/coverage-7.4.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:69eb372f7e2ece89f14751fbcbe470295d73ed41ecd37ca36ed2eb47512a6ab9", size = 244805 }, + { url = "https://files.pythonhosted.org/packages/af/7f/54dc676e7e63549838a3a7b95a8e11df80441bf7d64c6ce8f1cdbc0d1ff0/coverage-7.4.4-cp312-cp312-win32.whl", hash = "sha256:137eb07173141545e07403cca94ab625cc1cc6bc4c1e97b6e3846270e7e1fea0", size = 208590 }, + { url = "https://files.pythonhosted.org/packages/46/c4/1dfe76d96034a347d717a2392b004d42d45934cb94efa362ad41ca871f6e/coverage-7.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:d71eec7d83298f1af3326ce0ff1d0ea83c7cb98f72b577097f9083b20bdaf05e", size = 209415 }, ] [[package]] name = "project" version = "0.1.0" source = { editable = "." } - dependencies = [ - { name = "anyio" }, + + [package.dev-dependencies] + dev = [ + { name = "coverage" }, ] [package.metadata] - requires-dist = [{ name = "anyio", specifier = "==3.7.0" }] - [[package]] - name = "sniffio" - version = "1.3.1" - source = { registry = "https://pypi.org/simple/" } - sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } - wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, - ] + [package.metadata.requires-dev] + dev = [{ name = "coverage", extras = ["toml"] }] "### ); }); @@ -12416,7 +12672,7 @@ fn lock_trailing_slash() -> Result<()> { ----- stdout ----- ----- stderr ----- - Resolved 4 packages in [TIME] + Resolved 2 packages in [TIME] "###); // Re-run with `--offline`. We shouldn't need a network connection to validate an @@ -12427,7 +12683,7 @@ fn lock_trailing_slash() -> Result<()> { ----- stdout ----- ----- stderr ----- - Resolved 4 packages in [TIME] + Resolved 2 packages in [TIME] "###); // Install from the lockfile. @@ -12437,19 +12693,18 @@ fn lock_trailing_slash() -> Result<()> { ----- stdout ----- ----- stderr ----- - Prepared 4 packages in [TIME] - Installed 4 packages in [TIME] - + anyio==3.7.0 - + idna==3.6 + Prepared 2 packages in [TIME] + Installed 2 packages in [TIME] + + coverage==7.4.4 + project==0.1.0 (from file://[TEMP_DIR]/) - + sniffio==1.3.1 "###); Ok(()) } +/// Lock with an empty (but existent) `tool.uv.dev-dependencies` group. #[test] -fn lock_explicit_index() -> Result<()> { +fn lock_empty_dev_dependencies() -> Result<()> { let context = TestContext::new("3.12"); let pyproject_toml = context.temp_dir.child("pyproject.toml"); @@ -12459,19 +12714,14 @@ fn lock_explicit_index() -> Result<()> { name = "project" version = "0.1.0" requires-python = ">=3.12" - dependencies = ["anyio==3.7.0", "iniconfig==2.0.0"] + dependencies = ["iniconfig"] [build-system] requires = ["setuptools>=42"] build-backend = "setuptools.build_meta" - [tool.uv.sources] - iniconfig = { index = "test" } - - [[tool.uv.index]] - name = "test" - url = "https://test.pypi.org/simple" - explicit = true + [tool.uv] + dev-dependencies = [] "#, )?; @@ -12481,10 +12731,10 @@ fn lock_explicit_index() -> Result<()> { ----- stdout ----- ----- stderr ----- - Resolved 5 packages in [TIME] + Resolved 2 packages in [TIME] "###); - let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap(); + let lock = context.read("uv.lock"); insta::with_settings!({ filters => context.filters(), @@ -12497,35 +12747,13 @@ fn lock_explicit_index() -> Result<()> { [options] exclude-newer = "2024-03-25T00:00:00Z" - [[package]] - name = "anyio" - version = "3.7.0" - source = { registry = "https://pypi.org/simple" } - dependencies = [ - { name = "idna" }, - { name = "sniffio" }, - ] - sdist = { url = "https://files.pythonhosted.org/packages/c6/b3/fefbf7e78ab3b805dec67d698dc18dd505af7a18a8dd08868c9b4fa736b5/anyio-3.7.0.tar.gz", hash = "sha256:275d9973793619a5374e1c89a4f4ad3f4b0a5510a2b5b939444bee8f4c4d37ce", size = 142737 } - wheels = [ - { url = "https://files.pythonhosted.org/packages/68/fe/7ce1926952c8a403b35029e194555558514b365ad77d75125f521a2bec62/anyio-3.7.0-py3-none-any.whl", hash = "sha256:eddca883c4175f14df8aedce21054bfca3adb70ffe76a9f607aef9d7fa2ea7f0", size = 80873 }, - ] - - [[package]] - name = "idna" - version = "3.6" - source = { registry = "https://pypi.org/simple" } - sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426 } - wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567 }, - ] - [[package]] name = "iniconfig" version = "2.0.0" - source = { registry = "https://test.pypi.org/simple" } - sdist = { url = "https://test-files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } wheels = [ - { url = "https://test-files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, ] [[package]] @@ -12533,33 +12761,58 @@ fn lock_explicit_index() -> Result<()> { version = "0.1.0" source = { editable = "." } dependencies = [ - { name = "anyio" }, { name = "iniconfig" }, ] [package.metadata] - requires-dist = [ - { name = "anyio", specifier = "==3.7.0" }, - { name = "iniconfig", specifier = "==2.0.0", index = "https://test.pypi.org/simple" }, - ] + requires-dist = [{ name = "iniconfig" }] - [[package]] - name = "sniffio" - version = "1.3.1" - source = { registry = "https://pypi.org/simple" } - sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } - wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, - ] + [package.metadata.requires-dev] + dev = [] "### ); }); + // Re-run with `--locked`. + uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + "###); + + // Re-run with `--offline`. We shouldn't need a network connection to validate an + // already-correct lockfile with immutable metadata. + uv_snapshot!(context.filters(), context.lock().arg("--locked").arg("--offline").arg("--no-cache"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + "###); + + // Install from the lockfile. + uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Prepared 2 packages in [TIME] + Installed 2 packages in [TIME] + + iniconfig==2.0.0 + + project==0.1.0 (from file://[TEMP_DIR]/) + "###); + Ok(()) } +/// Lock with an empty (but existent) `dependency-groups` group. #[test] -fn lock_named_index() -> Result<()> { +fn lock_empty_dependency_group() -> Result<()> { let context = TestContext::new("3.12"); let pyproject_toml = context.temp_dir.child("pyproject.toml"); @@ -12569,20 +12822,14 @@ fn lock_named_index() -> Result<()> { name = "project" version = "0.1.0" requires-python = ">=3.12" - dependencies = ["typing-extensions"] - - [[tool.uv.index]] - name = "pytorch" - url = "https://download.pytorch.org/whl/cu121" - explicit = true + dependencies = ["iniconfig"] - [[tool.uv.index]] - name = "heron" - url = "https://pypi-proxy.fly.dev/simple" + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" - [[tool.uv.index]] - name = "test" - url = "https://test.pypi.org/simple" + [dependency-groups] + empty = [] "#, )?; @@ -12595,7 +12842,7 @@ fn lock_named_index() -> Result<()> { Resolved 2 packages in [TIME] "###); - let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap(); + let lock = context.read("uv.lock"); insta::with_settings!({ filters => context.filters(), @@ -12608,37 +12855,74 @@ fn lock_named_index() -> Result<()> { [options] exclude-newer = "2024-03-25T00:00:00Z" + [[package]] + name = "iniconfig" + version = "2.0.0" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, + ] + [[package]] name = "project" version = "0.1.0" - source = { virtual = "." } + source = { editable = "." } dependencies = [ - { name = "typing-extensions" }, + { name = "iniconfig" }, ] [package.metadata] - requires-dist = [{ name = "typing-extensions" }] + requires-dist = [{ name = "iniconfig" }] - [[package]] - name = "typing-extensions" - version = "4.10.0" - source = { registry = "https://pypi-proxy.fly.dev/simple" } - sdist = { url = "https://files.pythonhosted.org/packages/16/3a/0d26ce356c7465a19c9ea8814b960f8a36c3b0d07c323176620b7b483e44/typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb", size = 77558 } - wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/de/dc04a3ea60b22624b51c703a84bbe0184abcd1d0b9bc8074b5d6b7ab90bb/typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475", size = 33926 }, - ] + [package.metadata.requires-dev] + empty = [] "### ); }); + // Re-run with `--locked`. + uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + "###); + + // Re-run with `--offline`. We shouldn't need a network connection to validate an + // already-correct lockfile with immutable metadata. + uv_snapshot!(context.filters(), context.lock().arg("--locked").arg("--offline").arg("--no-cache"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + "###); + + // Install from the lockfile. + uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Prepared 2 packages in [TIME] + Installed 2 packages in [TIME] + + iniconfig==2.0.0 + + project==0.1.0 (from file://[TEMP_DIR]/) + "###); + Ok(()) } +/// Use a trailing slash on the declared index. #[test] -fn lock_default_index() -> Result<()> { +fn lock_trailing_slash() -> Result<()> { let context = TestContext::new("3.12"); - // If an index is included, PyPI will still be used as the default index. let pyproject_toml = context.temp_dir.child("pyproject.toml"); pyproject_toml.write_str( r#" @@ -12646,11 +12930,14 @@ fn lock_default_index() -> Result<()> { name = "project" version = "0.1.0" requires-python = ">=3.12" - dependencies = ["iniconfig"] + dependencies = ["anyio==3.7.0"] - [[tool.uv.index]] - name = "pytorch" - url = "https://download.pytorch.org/whl/cu121" + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + + [tool.uv] + index-url = "https://pypi.org/simple/" "#, )?; @@ -12660,10 +12947,10 @@ fn lock_default_index() -> Result<()> { ----- stdout ----- ----- stderr ----- - Resolved 2 packages in [TIME] + Resolved 4 packages in [TIME] "###); - let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap(); + let lock = context.read("uv.lock"); insta::with_settings!({ filters => context.filters(), @@ -12677,97 +12964,91 @@ fn lock_default_index() -> Result<()> { exclude-newer = "2024-03-25T00:00:00Z" [[package]] - name = "iniconfig" - version = "2.0.0" - source = { registry = "https://pypi.org/simple" } - sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } + name = "anyio" + version = "3.7.0" + source = { registry = "https://pypi.org/simple/" } + dependencies = [ + { name = "idna" }, + { name = "sniffio" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/c6/b3/fefbf7e78ab3b805dec67d698dc18dd505af7a18a8dd08868c9b4fa736b5/anyio-3.7.0.tar.gz", hash = "sha256:275d9973793619a5374e1c89a4f4ad3f4b0a5510a2b5b939444bee8f4c4d37ce", size = 142737 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, + { url = "https://files.pythonhosted.org/packages/68/fe/7ce1926952c8a403b35029e194555558514b365ad77d75125f521a2bec62/anyio-3.7.0-py3-none-any.whl", hash = "sha256:eddca883c4175f14df8aedce21054bfca3adb70ffe76a9f607aef9d7fa2ea7f0", size = 80873 }, + ] + + [[package]] + name = "idna" + version = "3.6" + source = { registry = "https://pypi.org/simple/" } + sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567 }, ] [[package]] name = "project" version = "0.1.0" - source = { virtual = "." } + source = { editable = "." } dependencies = [ - { name = "iniconfig" }, + { name = "anyio" }, ] [package.metadata] - requires-dist = [{ name = "iniconfig" }] + requires-dist = [{ name = "anyio", specifier = "==3.7.0" }] + + [[package]] + name = "sniffio" + version = "1.3.1" + source = { registry = "https://pypi.org/simple/" } + sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, + ] "### ); }); - // Unless that index is marked as the default. - pyproject_toml.write_str( - r#" - [project] - name = "project" - version = "0.1.0" - requires-python = ">=3.12" - dependencies = ["iniconfig"] - - [[tool.uv.index]] - name = "pytorch" - url = "https://download.pytorch.org/whl/cu121" - default = true - "#, - )?; - - uv_snapshot!(context.filters(), context.lock(), @r###" - success: false - exit_code: 1 + // Re-run with `--locked`. + uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###" + success: true + exit_code: 0 ----- stdout ----- ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because iniconfig was not found in the package registry and your project depends on iniconfig, we can conclude that your project's requirements are unsatisfiable. - - hint: An index URL (https://download.pytorch.org/whl/cu121) could not be queried due to a lack of valid authentication credentials (403 Forbidden). + Resolved 4 packages in [TIME] "###); - let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap(); - - insta::with_settings!({ - filters => context.filters(), - }, { - assert_snapshot!( - lock, @r###" - version = 1 - requires-python = ">=3.12" - - [options] - exclude-newer = "2024-03-25T00:00:00Z" + // Re-run with `--offline`. We shouldn't need a network connection to validate an + // already-correct lockfile with immutable metadata. + uv_snapshot!(context.filters(), context.lock().arg("--locked").arg("--offline").arg("--no-cache"), @r###" + success: true + exit_code: 0 + ----- stdout ----- - [[package]] - name = "iniconfig" - version = "2.0.0" - source = { registry = "https://pypi.org/simple" } - sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } - wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, - ] + ----- stderr ----- + Resolved 4 packages in [TIME] + "###); - [[package]] - name = "project" - version = "0.1.0" - source = { virtual = "." } - dependencies = [ - { name = "iniconfig" }, - ] + // Install from the lockfile. + uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" + success: true + exit_code: 0 + ----- stdout ----- - [package.metadata] - requires-dist = [{ name = "iniconfig" }] - "### - ); - }); + ----- stderr ----- + Prepared 4 packages in [TIME] + Installed 4 packages in [TIME] + + anyio==3.7.0 + + idna==3.6 + + project==0.1.0 (from file://[TEMP_DIR]/) + + sniffio==1.3.1 + "###); Ok(()) } #[test] -fn lock_named_index_cli() -> Result<()> { +fn lock_invalid_index() -> Result<()> { let context = TestContext::new("3.12"); let pyproject_toml = context.temp_dir.child("pyproject.toml"); @@ -12777,45 +13058,48 @@ fn lock_named_index_cli() -> Result<()> { name = "project" version = "0.1.0" requires-python = ">=3.12" - dependencies = ["jinja2==3.1.2"] + dependencies = ["anyio==3.7.0", "iniconfig==2.0.0"] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" [tool.uv.sources] - jinja2 = { index = "pytorch" } + iniconfig = { index = "internal proxy" } + + [[tool.uv.index]] + name = "internal proxy" + url = "https://test.pypi.org/simple" + explicit = true "#, )?; - // The package references a non-existent index. - uv_snapshot!(context.filters(), context.lock().env_remove("UV_EXCLUDE_NEWER"), @r###" + uv_snapshot!(context.filters(), context.lock(), @r###" success: false exit_code: 2 ----- stdout ----- ----- stderr ----- - error: Failed to build: `project @ file://[TEMP_DIR]/` - Caused by: Failed to parse entry for: `jinja2` - Caused by: Package `jinja2` references an undeclared index: `pytorch` - "###); - - // But it's fine if it comes from the CLI. - uv_snapshot!(context.filters(), context.lock().arg("--index").arg("pytorch=https://download.pytorch.org/whl/cu121").env_remove("UV_EXCLUDE_NEWER"), @r###" - success: true - exit_code: 0 - ----- stdout ----- + warning: Failed to parse `pyproject.toml` during settings discovery: + TOML parse error at line 16, column 16 + | + 16 | name = "internal proxy" + | ^^^^^^^^^^^^^^^^ + Index names may only contain letters, digits, hyphens, underscores, and periods, but found unsupported character (` `) in: `internal proxy` - ----- stderr ----- - Resolved 3 packages in [TIME] + error: Failed to parse: `pyproject.toml` + Caused by: TOML parse error at line 13, column 31 + | + 13 | iniconfig = { index = "internal proxy" } + | ^^^^^^^^^^^^^^^^ + Index names may only contain letters, digits, hyphens, underscores, and periods, but found unsupported character (` `) in: `internal proxy` "###); Ok(()) } -/// If a name is reused, the higher-priority index should "overwrite" the lower-priority index. -/// In other words, the lower-priority index should be ignored entirely during implicit resolution. -/// -/// In this test, we should use PyPI (the default index) and ignore `https://example.com` entirely. -/// (Querying `https://example.com` would fail with a 500.) #[test] -fn lock_repeat_named_index() -> Result<()> { +fn lock_explicit_index() -> Result<()> { let context = TestContext::new("3.12"); let pyproject_toml = context.temp_dir.child("pyproject.toml"); @@ -12825,26 +13109,29 @@ fn lock_repeat_named_index() -> Result<()> { name = "project" version = "0.1.0" requires-python = ">=3.12" - dependencies = ["iniconfig"] + dependencies = ["anyio==3.7.0", "iniconfig==2.0.0"] - [[tool.uv.index]] - name = "pytorch" - url = "https://download.pytorch.org/whl/cu121" + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + + [tool.uv.sources] + iniconfig = { index = "test" } [[tool.uv.index]] - name = "pytorch" - url = "https://example.com" + name = "test" + url = "https://test.pypi.org/simple" + explicit = true "#, )?; - // Fall back to PyPI, since `iniconfig` doesn't exist on the PyTorch index. uv_snapshot!(context.filters(), context.lock(), @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- - Resolved 2 packages in [TIME] + Resolved 5 packages in [TIME] "###); let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap(); @@ -12861,24 +13148,59 @@ fn lock_repeat_named_index() -> Result<()> { exclude-newer = "2024-03-25T00:00:00Z" [[package]] - name = "iniconfig" - version = "2.0.0" + name = "anyio" + version = "3.7.0" source = { registry = "https://pypi.org/simple" } - sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } + dependencies = [ + { name = "idna" }, + { name = "sniffio" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/c6/b3/fefbf7e78ab3b805dec67d698dc18dd505af7a18a8dd08868c9b4fa736b5/anyio-3.7.0.tar.gz", hash = "sha256:275d9973793619a5374e1c89a4f4ad3f4b0a5510a2b5b939444bee8f4c4d37ce", size = 142737 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, + { url = "https://files.pythonhosted.org/packages/68/fe/7ce1926952c8a403b35029e194555558514b365ad77d75125f521a2bec62/anyio-3.7.0-py3-none-any.whl", hash = "sha256:eddca883c4175f14df8aedce21054bfca3adb70ffe76a9f607aef9d7fa2ea7f0", size = 80873 }, ] [[package]] - name = "project" - version = "0.1.0" - source = { virtual = "." } - dependencies = [ + name = "idna" + version = "3.6" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567 }, + ] + + [[package]] + name = "iniconfig" + version = "2.0.0" + source = { registry = "https://test.pypi.org/simple" } + sdist = { url = "https://test-files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } + wheels = [ + { url = "https://test-files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, + ] + + [[package]] + name = "project" + version = "0.1.0" + source = { editable = "." } + dependencies = [ + { name = "anyio" }, { name = "iniconfig" }, ] [package.metadata] - requires-dist = [{ name = "iniconfig" }] + requires-dist = [ + { name = "anyio", specifier = "==3.7.0" }, + { name = "iniconfig", specifier = "==2.0.0", index = "https://test.pypi.org/simple" }, + ] + + [[package]] + name = "sniffio" + version = "1.3.1" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, + ] "### ); }); @@ -12886,10 +13208,8 @@ fn lock_repeat_named_index() -> Result<()> { Ok(()) } -/// If a name is reused, the higher-priority index should "overwrite" the lower-priority index. -/// This includes names passed in via the CLI. #[test] -fn lock_repeat_named_index_cli() -> Result<()> { +fn lock_named_index() -> Result<()> { let context = TestContext::new("3.12"); let pyproject_toml = context.temp_dir.child("pyproject.toml"); @@ -12899,22 +13219,30 @@ fn lock_repeat_named_index_cli() -> Result<()> { name = "project" version = "0.1.0" requires-python = ">=3.12" - dependencies = ["jinja2==3.1.2"] + dependencies = ["typing-extensions"] [[tool.uv.index]] name = "pytorch" url = "https://download.pytorch.org/whl/cu121" + explicit = true + + [[tool.uv.index]] + name = "heron" + url = "https://pypi-proxy.fly.dev/simple" + + [[tool.uv.index]] + name = "test" + url = "https://test.pypi.org/simple" "#, )?; - // Resolve to the PyTorch index. - uv_snapshot!(context.filters(), context.lock().env_remove(EnvVars::UV_EXCLUDE_NEWER), @r###" + uv_snapshot!(context.filters(), context.lock(), @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- - Resolved 3 packages in [TIME] + Resolved 2 packages in [TIME] "###); let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap(); @@ -12927,53 +13255,62 @@ fn lock_repeat_named_index_cli() -> Result<()> { version = 1 requires-python = ">=3.12" - [[package]] - name = "jinja2" - version = "3.1.2" - source = { registry = "https://download.pytorch.org/whl/cu121" } - dependencies = [ - { name = "markupsafe" }, - ] - wheels = [ - { url = "https://download.pytorch.org/whl/Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61" }, - ] - - [[package]] - name = "markupsafe" - version = "2.1.5" - source = { registry = "https://download.pytorch.org/whl/cu121" } - wheels = [ - { url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1" }, - { url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4" }, - { url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee" }, - { url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5" }, - { url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b" }, - { url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb" }, - ] + [options] + exclude-newer = "2024-03-25T00:00:00Z" [[package]] name = "project" version = "0.1.0" source = { virtual = "." } dependencies = [ - { name = "jinja2" }, + { name = "typing-extensions" }, ] [package.metadata] - requires-dist = [{ name = "jinja2", specifier = "==3.1.2" }] + requires-dist = [{ name = "typing-extensions" }] + + [[package]] + name = "typing-extensions" + version = "4.10.0" + source = { registry = "https://pypi-proxy.fly.dev/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/16/3a/0d26ce356c7465a19c9ea8814b960f8a36c3b0d07c323176620b7b483e44/typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb", size = 77558 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/de/dc04a3ea60b22624b51c703a84bbe0184abcd1d0b9bc8074b5d6b7ab90bb/typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475", size = 33926 }, + ] "### ); }); - // Resolve to PyPI, since the PyTorch index is replaced by the Packse index, which doesn't - // include `jinja2`. - uv_snapshot!(context.filters(), context.lock().arg("--index").arg(format!("pytorch={}", packse_index_url())).env_remove(EnvVars::UV_EXCLUDE_NEWER), @r###" + Ok(()) +} + +#[test] +fn lock_default_index() -> Result<()> { + let context = TestContext::new("3.12"); + + // If an index is included, PyPI will still be used as the default index. + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["iniconfig"] + + [[tool.uv.index]] + name = "pytorch" + url = "https://download.pytorch.org/whl/cu121" + "#, + )?; + + uv_snapshot!(context.filters(), context.lock(), @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- - Resolved 3 packages in [TIME] + Resolved 2 packages in [TIME] "###); let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap(); @@ -12986,34 +13323,16 @@ fn lock_repeat_named_index_cli() -> Result<()> { version = 1 requires-python = ">=3.12" - [[package]] - name = "jinja2" - version = "3.1.2" - source = { registry = "https://pypi.org/simple" } - dependencies = [ - { name = "markupsafe" }, - ] - sdist = { url = "https://files.pythonhosted.org/packages/7a/ff/75c28576a1d900e87eb6335b063fab47a8ef3c8b4d88524c4bf78f670cce/Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852", size = 268239 } - wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/c3/f068337a370801f372f2f8f6bad74a5c140f6fda3d9de154052708dd3c65/Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61", size = 133101 }, - ] + [options] + exclude-newer = "2024-03-25T00:00:00Z" [[package]] - name = "markupsafe" - version = "2.1.5" + name = "iniconfig" + version = "2.0.0" source = { registry = "https://pypi.org/simple" } - sdist = { url = "https://files.pythonhosted.org/packages/87/5b/aae44c6655f3801e81aa3eef09dbbf012431987ba564d7231722f68df02d/MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", size = 19384 } + sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } wheels = [ - { url = "https://files.pythonhosted.org/packages/53/bd/583bf3e4c8d6a321938c13f49d44024dbe5ed63e0a7ba127e454a66da974/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", size = 18215 }, - { url = "https://files.pythonhosted.org/packages/48/d6/e7cd795fc710292c3af3a06d80868ce4b02bfbbf370b7cee11d282815a2a/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", size = 14069 }, - { url = "https://files.pythonhosted.org/packages/51/b5/5d8ec796e2a08fc814a2c7d2584b55f889a55cf17dd1a90f2beb70744e5c/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", size = 29452 }, - { url = "https://files.pythonhosted.org/packages/0a/0d/2454f072fae3b5a137c119abf15465d1771319dfe9e4acbb31722a0fff91/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", size = 28462 }, - { url = "https://files.pythonhosted.org/packages/2d/75/fd6cb2e68780f72d47e6671840ca517bda5ef663d30ada7616b0462ad1e3/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", size = 27869 }, - { url = "https://files.pythonhosted.org/packages/b0/81/147c477391c2750e8fc7705829f7351cf1cd3be64406edcf900dc633feb2/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", size = 33906 }, - { url = "https://files.pythonhosted.org/packages/8b/ff/9a52b71839d7a256b563e85d11050e307121000dcebc97df120176b3ad93/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", size = 32296 }, - { url = "https://files.pythonhosted.org/packages/88/07/2dc76aa51b481eb96a4c3198894f38b480490e834479611a4053fbf08623/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", size = 33038 }, - { url = "https://files.pythonhosted.org/packages/96/0c/620c1fb3661858c0e37eb3cbffd8c6f732a67cd97296f725789679801b31/MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", size = 16572 }, - { url = "https://files.pythonhosted.org/packages/3f/14/c3554d512d5f9100a95e737502f4a2323a1959f6d0d01e0d0997b35f7b10/MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", size = 17127 }, + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, ] [[package]] @@ -13021,54 +13340,44 @@ fn lock_repeat_named_index_cli() -> Result<()> { version = "0.1.0" source = { virtual = "." } dependencies = [ - { name = "jinja2" }, + { name = "iniconfig" }, ] [package.metadata] - requires-dist = [{ name = "jinja2", specifier = "==3.1.2" }] + requires-dist = [{ name = "iniconfig" }] "### ); }); - Ok(()) -} - -/// Lock a project with `package = false`, making it a virtual project. -#[test] -fn lock_explicit_virtual_project() -> Result<()> { - let context = TestContext::new("3.12"); - - let pyproject_toml = context.temp_dir.child("pyproject.toml"); + // Unless that index is marked as the default. pyproject_toml.write_str( r#" [project] name = "project" version = "0.1.0" requires-python = ">=3.12" - dependencies = ["black"] - - [build-system] - requires = ["setuptools>=42"] - build-backend = "setuptools.build_meta" + dependencies = ["iniconfig"] - [tool.uv] - package = false - dev-dependencies = [ - "anyio" - ] + [[tool.uv.index]] + name = "pytorch" + url = "https://download.pytorch.org/whl/cu121" + default = true "#, )?; uv_snapshot!(context.filters(), context.lock(), @r###" - success: true - exit_code: 0 + success: false + exit_code: 1 ----- stdout ----- ----- stderr ----- - Resolved 11 packages in [TIME] + × No solution found when resolving dependencies: + ╰─▶ Because iniconfig was not found in the package registry and your project depends on iniconfig, we can conclude that your project's requirements are unsatisfiable. + + hint: An index URL (https://download.pytorch.org/whl/cu121) could not be queried due to a lack of valid authentication credentials (403 Forbidden). "###); - let lock = context.read("uv.lock"); + let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap(); insta::with_settings!({ filters => context.filters(), @@ -13082,182 +13391,81 @@ fn lock_explicit_virtual_project() -> Result<()> { exclude-newer = "2024-03-25T00:00:00Z" [[package]] - name = "anyio" - version = "4.3.0" + name = "iniconfig" + version = "2.0.0" source = { registry = "https://pypi.org/simple" } - dependencies = [ - { name = "idna" }, - { name = "sniffio" }, - ] - sdist = { url = "https://files.pythonhosted.org/packages/db/4d/3970183622f0330d3c23d9b8a5f52e365e50381fd484d08e3285104333d3/anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6", size = 159642 } + sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } wheels = [ - { url = "https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8", size = 85584 }, + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, ] [[package]] - name = "black" - version = "24.3.0" - source = { registry = "https://pypi.org/simple" } + name = "project" + version = "0.1.0" + source = { virtual = "." } dependencies = [ - { name = "click" }, - { name = "mypy-extensions" }, - { name = "packaging" }, - { name = "pathspec" }, - { name = "platformdirs" }, - ] - sdist = { url = "https://files.pythonhosted.org/packages/8f/5f/bac24a952668c7482cfdb4ebf91ba57a796c9da8829363a772040c1a3312/black-24.3.0.tar.gz", hash = "sha256:a0c9c4a0771afc6919578cec71ce82a3e31e054904e7197deacbc9382671c41f", size = 634292 } - wheels = [ - { url = "https://files.pythonhosted.org/packages/b6/c6/1d174efa9ff02b22d0124c73fc5f4d4fb006d0d9a081aadc354d05754a13/black-24.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:2818cf72dfd5d289e48f37ccfa08b460bf469e67fb7c4abb07edc2e9f16fb63f", size = 1600822 }, - { url = "https://files.pythonhosted.org/packages/d9/ed/704731afffe460b8ff0672623b40fce9fe569f2ee617c15857e4d4440a3a/black-24.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4acf672def7eb1725f41f38bf6bf425c8237248bb0804faa3965c036f7672d11", size = 1429987 }, - { url = "https://files.pythonhosted.org/packages/a8/05/8dd038e30caadab7120176d4bc109b7ca2f4457f12eef746b0560a583458/black-24.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7ed6668cbbfcd231fa0dc1b137d3e40c04c7f786e626b405c62bcd5db5857e4", size = 1755319 }, - { url = "https://files.pythonhosted.org/packages/71/9d/e5fa1ff4ef1940be15a64883c0bb8d2fcf626efec996eab4ae5a8c691d2c/black-24.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:56f52cfbd3dabe2798d76dbdd299faa046a901041faf2cf33288bc4e6dae57b5", size = 1385180 }, - { url = "https://files.pythonhosted.org/packages/4d/ea/31770a7e49f3eedfd8cd7b35e78b3a3aaad860400f8673994bc988318135/black-24.3.0-py3-none-any.whl", hash = "sha256:41622020d7120e01d377f74249e677039d20e6344ff5851de8a10f11f513bf93", size = 201493 }, + { name = "iniconfig" }, ] - [[package]] - name = "click" - version = "8.1.7" - source = { registry = "https://pypi.org/simple" } - dependencies = [ - { name = "colorama", marker = "platform_system == 'Windows'" }, - ] - sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 } - wheels = [ - { url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941 }, - ] + [package.metadata] + requires-dist = [{ name = "iniconfig" }] + "### + ); + }); - [[package]] - name = "colorama" - version = "0.4.6" - source = { registry = "https://pypi.org/simple" } - sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } - wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, - ] + Ok(()) +} - [[package]] - name = "idna" - version = "3.6" - source = { registry = "https://pypi.org/simple" } - sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426 } - wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567 }, - ] - - [[package]] - name = "mypy-extensions" - version = "1.0.0" - source = { registry = "https://pypi.org/simple" } - sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 } - wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, - ] - - [[package]] - name = "packaging" - version = "24.0" - source = { registry = "https://pypi.org/simple" } - sdist = { url = "https://files.pythonhosted.org/packages/ee/b5/b43a27ac7472e1818c4bafd44430e69605baefe1f34440593e0332ec8b4d/packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9", size = 147882 } - wheels = [ - { url = "https://files.pythonhosted.org/packages/49/df/1fceb2f8900f8639e278b056416d49134fb8d84c5942ffaa01ad34782422/packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", size = 53488 }, - ] - - [[package]] - name = "pathspec" - version = "0.12.1" - source = { registry = "https://pypi.org/simple" } - sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 } - wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 }, - ] - - [[package]] - name = "platformdirs" - version = "4.2.0" - source = { registry = "https://pypi.org/simple" } - sdist = { url = "https://files.pythonhosted.org/packages/96/dc/c1d911bf5bb0fdc58cc05010e9f3efe3b67970cef779ba7fbc3183b987a8/platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768", size = 20055 } - wheels = [ - { url = "https://files.pythonhosted.org/packages/55/72/4898c44ee9ea6f43396fbc23d9bfaf3d06e01b83698bdf2e4c919deceb7c/platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068", size = 17717 }, - ] +#[test] +fn lock_named_index_cli() -> Result<()> { + let context = TestContext::new("3.12"); - [[package]] + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] name = "project" version = "0.1.0" - source = { virtual = "." } - dependencies = [ - { name = "black" }, - ] - - [package.dev-dependencies] - dev = [ - { name = "anyio" }, - ] - - [package.metadata] - requires-dist = [{ name = "black" }] - - [package.metadata.requires-dev] - dev = [{ name = "anyio" }] - - [[package]] - name = "sniffio" - version = "1.3.1" - source = { registry = "https://pypi.org/simple" } - sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } - wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, - ] - "### - ); - }); - - // Re-run with `--locked`. - uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###" - success: true - exit_code: 0 - ----- stdout ----- + requires-python = ">=3.12" + dependencies = ["jinja2==3.1.2"] - ----- stderr ----- - Resolved 11 packages in [TIME] - "###); + [tool.uv.sources] + jinja2 = { index = "pytorch" } + "#, + )?; - // Re-run with `--offline`. We shouldn't need a network connection to validate an - // already-correct lockfile with immutable metadata. - uv_snapshot!(context.filters(), context.lock().arg("--locked").arg("--offline").arg("--no-cache"), @r###" - success: true - exit_code: 0 + // The package references a non-existent index. + uv_snapshot!(context.filters(), context.lock().env_remove("UV_EXCLUDE_NEWER"), @r###" + success: false + exit_code: 2 ----- stdout ----- ----- stderr ----- - Resolved 11 packages in [TIME] + error: Failed to build: `project @ file://[TEMP_DIR]/` + Caused by: Failed to parse entry: `jinja2` + Caused by: Package `jinja2` references an undeclared index: `pytorch` "###); - // Install from the lockfile. The virtual project should _not_ be installed. - uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" + // But it's fine if it comes from the CLI. + uv_snapshot!(context.filters(), context.lock().arg("--index").arg("pytorch=https://download.pytorch.org/whl/cu121").env_remove("UV_EXCLUDE_NEWER"), @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- - Prepared 9 packages in [TIME] - Installed 9 packages in [TIME] - + anyio==4.3.0 - + black==24.3.0 - + click==8.1.7 - + idna==3.6 - + mypy-extensions==1.0.0 - + packaging==24.0 - + pathspec==0.12.1 - + platformdirs==4.2.0 - + sniffio==1.3.1 + Resolved 3 packages in [TIME] "###); Ok(()) } -/// Lock a project that is implicitly virtual (by way of omitting `build-system`). +/// If a name is reused, the higher-priority index should "overwrite" the lower-priority index. +/// In other words, the lower-priority index should be ignored entirely during implicit resolution. +/// +/// In this test, we should use PyPI (the default index) and ignore `https://example.com` entirely. +/// (Querying `https://example.com` would fail with a 500.) #[test] -fn lock_implicit_virtual_project() -> Result<()> { +fn lock_repeat_named_index() -> Result<()> { let context = TestContext::new("3.12"); let pyproject_toml = context.temp_dir.child("pyproject.toml"); @@ -13267,25 +13475,29 @@ fn lock_implicit_virtual_project() -> Result<()> { name = "project" version = "0.1.0" requires-python = ">=3.12" - dependencies = ["black"] + dependencies = ["iniconfig"] - [tool.uv] - dev-dependencies = [ - "anyio" - ] + [[tool.uv.index]] + name = "pytorch" + url = "https://download.pytorch.org/whl/cu121" + + [[tool.uv.index]] + name = "pytorch" + url = "https://example.com" "#, )?; + // Fall back to PyPI, since `iniconfig` doesn't exist on the PyTorch index. uv_snapshot!(context.filters(), context.lock(), @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- - Resolved 11 packages in [TIME] + Resolved 2 packages in [TIME] "###); - let lock = context.read("uv.lock"); + let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap(); insta::with_settings!({ filters => context.filters(), @@ -13299,61 +13511,291 @@ fn lock_implicit_virtual_project() -> Result<()> { exclude-newer = "2024-03-25T00:00:00Z" [[package]] - name = "anyio" - version = "4.3.0" + name = "iniconfig" + version = "2.0.0" source = { registry = "https://pypi.org/simple" } - dependencies = [ - { name = "idna" }, - { name = "sniffio" }, - ] - sdist = { url = "https://files.pythonhosted.org/packages/db/4d/3970183622f0330d3c23d9b8a5f52e365e50381fd484d08e3285104333d3/anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6", size = 159642 } + sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } wheels = [ - { url = "https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8", size = 85584 }, + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, ] [[package]] - name = "black" - version = "24.3.0" - source = { registry = "https://pypi.org/simple" } + name = "project" + version = "0.1.0" + source = { virtual = "." } dependencies = [ - { name = "click" }, - { name = "mypy-extensions" }, - { name = "packaging" }, - { name = "pathspec" }, - { name = "platformdirs" }, - ] - sdist = { url = "https://files.pythonhosted.org/packages/8f/5f/bac24a952668c7482cfdb4ebf91ba57a796c9da8829363a772040c1a3312/black-24.3.0.tar.gz", hash = "sha256:a0c9c4a0771afc6919578cec71ce82a3e31e054904e7197deacbc9382671c41f", size = 634292 } - wheels = [ - { url = "https://files.pythonhosted.org/packages/b6/c6/1d174efa9ff02b22d0124c73fc5f4d4fb006d0d9a081aadc354d05754a13/black-24.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:2818cf72dfd5d289e48f37ccfa08b460bf469e67fb7c4abb07edc2e9f16fb63f", size = 1600822 }, - { url = "https://files.pythonhosted.org/packages/d9/ed/704731afffe460b8ff0672623b40fce9fe569f2ee617c15857e4d4440a3a/black-24.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4acf672def7eb1725f41f38bf6bf425c8237248bb0804faa3965c036f7672d11", size = 1429987 }, - { url = "https://files.pythonhosted.org/packages/a8/05/8dd038e30caadab7120176d4bc109b7ca2f4457f12eef746b0560a583458/black-24.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7ed6668cbbfcd231fa0dc1b137d3e40c04c7f786e626b405c62bcd5db5857e4", size = 1755319 }, - { url = "https://files.pythonhosted.org/packages/71/9d/e5fa1ff4ef1940be15a64883c0bb8d2fcf626efec996eab4ae5a8c691d2c/black-24.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:56f52cfbd3dabe2798d76dbdd299faa046a901041faf2cf33288bc4e6dae57b5", size = 1385180 }, - { url = "https://files.pythonhosted.org/packages/4d/ea/31770a7e49f3eedfd8cd7b35e78b3a3aaad860400f8673994bc988318135/black-24.3.0-py3-none-any.whl", hash = "sha256:41622020d7120e01d377f74249e677039d20e6344ff5851de8a10f11f513bf93", size = 201493 }, + { name = "iniconfig" }, ] + [package.metadata] + requires-dist = [{ name = "iniconfig" }] + "### + ); + }); + + Ok(()) +} + +/// If a name is reused, the higher-priority index should "overwrite" the lower-priority index. +/// This includes names passed in via the CLI. +#[test] +fn lock_repeat_named_index_cli() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["jinja2==3.1.2"] + + [tool.uv] + constraint-dependencies = ["markupsafe<3"] + + [[tool.uv.index]] + name = "pytorch" + url = "https://download.pytorch.org/whl/cu121" + "#, + )?; + + // Resolve to the PyTorch index. + uv_snapshot!(context.filters(), context.lock().env_remove(EnvVars::UV_EXCLUDE_NEWER), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + "###); + + let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap(); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r###" + version = 1 + requires-python = ">=3.12" + + [manifest] + constraints = [{ name = "markupsafe", specifier = "<3" }] + [[package]] - name = "click" - version = "8.1.7" - source = { registry = "https://pypi.org/simple" } + name = "jinja2" + version = "3.1.2" + source = { registry = "https://download.pytorch.org/whl/cu121" } dependencies = [ - { name = "colorama", marker = "platform_system == 'Windows'" }, + { name = "markupsafe" }, ] - sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941 }, + { url = "https://download.pytorch.org/whl/Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61" }, ] [[package]] - name = "colorama" - version = "0.4.6" - source = { registry = "https://pypi.org/simple" } - sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } + name = "markupsafe" + version = "2.1.5" + source = { registry = "https://download.pytorch.org/whl/cu121" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, + { url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1" }, + { url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4" }, + { url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee" }, + { url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5" }, + { url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b" }, + { url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb" }, ] [[package]] - name = "idna" + name = "project" + version = "0.1.0" + source = { virtual = "." } + dependencies = [ + { name = "jinja2" }, + ] + + [package.metadata] + requires-dist = [{ name = "jinja2", specifier = "==3.1.2" }] + "### + ); + }); + + // Resolve to PyPI, since the PyTorch index is replaced by the Packse index, which doesn't + // include `jinja2`. + uv_snapshot!(context.filters(), context.lock().arg("--index").arg(format!("pytorch={}", packse_index_url())).env_remove(EnvVars::UV_EXCLUDE_NEWER), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + "###); + + let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap(); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r###" + version = 1 + requires-python = ">=3.12" + + [manifest] + constraints = [{ name = "markupsafe", specifier = "<3" }] + + [[package]] + name = "jinja2" + version = "3.1.2" + source = { registry = "https://pypi.org/simple" } + dependencies = [ + { name = "markupsafe" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/7a/ff/75c28576a1d900e87eb6335b063fab47a8ef3c8b4d88524c4bf78f670cce/Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852", size = 268239 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/c3/f068337a370801f372f2f8f6bad74a5c140f6fda3d9de154052708dd3c65/Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61", size = 133101 }, + ] + + [[package]] + name = "markupsafe" + version = "2.1.5" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/87/5b/aae44c6655f3801e81aa3eef09dbbf012431987ba564d7231722f68df02d/MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", size = 19384 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/53/bd/583bf3e4c8d6a321938c13f49d44024dbe5ed63e0a7ba127e454a66da974/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", size = 18215 }, + { url = "https://files.pythonhosted.org/packages/48/d6/e7cd795fc710292c3af3a06d80868ce4b02bfbbf370b7cee11d282815a2a/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", size = 14069 }, + { url = "https://files.pythonhosted.org/packages/51/b5/5d8ec796e2a08fc814a2c7d2584b55f889a55cf17dd1a90f2beb70744e5c/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", size = 29452 }, + { url = "https://files.pythonhosted.org/packages/0a/0d/2454f072fae3b5a137c119abf15465d1771319dfe9e4acbb31722a0fff91/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", size = 28462 }, + { url = "https://files.pythonhosted.org/packages/2d/75/fd6cb2e68780f72d47e6671840ca517bda5ef663d30ada7616b0462ad1e3/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", size = 27869 }, + { url = "https://files.pythonhosted.org/packages/b0/81/147c477391c2750e8fc7705829f7351cf1cd3be64406edcf900dc633feb2/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", size = 33906 }, + { url = "https://files.pythonhosted.org/packages/8b/ff/9a52b71839d7a256b563e85d11050e307121000dcebc97df120176b3ad93/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", size = 32296 }, + { url = "https://files.pythonhosted.org/packages/88/07/2dc76aa51b481eb96a4c3198894f38b480490e834479611a4053fbf08623/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", size = 33038 }, + { url = "https://files.pythonhosted.org/packages/96/0c/620c1fb3661858c0e37eb3cbffd8c6f732a67cd97296f725789679801b31/MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", size = 16572 }, + { url = "https://files.pythonhosted.org/packages/3f/14/c3554d512d5f9100a95e737502f4a2323a1959f6d0d01e0d0997b35f7b10/MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", size = 17127 }, + ] + + [[package]] + name = "project" + version = "0.1.0" + source = { virtual = "." } + dependencies = [ + { name = "jinja2" }, + ] + + [package.metadata] + requires-dist = [{ name = "jinja2", specifier = "==3.1.2" }] + "### + ); + }); + + Ok(()) +} + +/// Lock a project with `package = false`, making it a virtual project. +#[test] +fn lock_explicit_virtual_project() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["black"] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + + [tool.uv] + package = false + dev-dependencies = [ + "anyio" + ] + "#, + )?; + + uv_snapshot!(context.filters(), context.lock(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 11 packages in [TIME] + "###); + + let lock = context.read("uv.lock"); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r###" + version = 1 + requires-python = ">=3.12" + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [[package]] + name = "anyio" + version = "4.3.0" + source = { registry = "https://pypi.org/simple" } + dependencies = [ + { name = "idna" }, + { name = "sniffio" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/db/4d/3970183622f0330d3c23d9b8a5f52e365e50381fd484d08e3285104333d3/anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6", size = 159642 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8", size = 85584 }, + ] + + [[package]] + name = "black" + version = "24.3.0" + source = { registry = "https://pypi.org/simple" } + dependencies = [ + { name = "click" }, + { name = "mypy-extensions" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "platformdirs" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/8f/5f/bac24a952668c7482cfdb4ebf91ba57a796c9da8829363a772040c1a3312/black-24.3.0.tar.gz", hash = "sha256:a0c9c4a0771afc6919578cec71ce82a3e31e054904e7197deacbc9382671c41f", size = 634292 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/c6/1d174efa9ff02b22d0124c73fc5f4d4fb006d0d9a081aadc354d05754a13/black-24.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:2818cf72dfd5d289e48f37ccfa08b460bf469e67fb7c4abb07edc2e9f16fb63f", size = 1600822 }, + { url = "https://files.pythonhosted.org/packages/d9/ed/704731afffe460b8ff0672623b40fce9fe569f2ee617c15857e4d4440a3a/black-24.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4acf672def7eb1725f41f38bf6bf425c8237248bb0804faa3965c036f7672d11", size = 1429987 }, + { url = "https://files.pythonhosted.org/packages/a8/05/8dd038e30caadab7120176d4bc109b7ca2f4457f12eef746b0560a583458/black-24.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7ed6668cbbfcd231fa0dc1b137d3e40c04c7f786e626b405c62bcd5db5857e4", size = 1755319 }, + { url = "https://files.pythonhosted.org/packages/71/9d/e5fa1ff4ef1940be15a64883c0bb8d2fcf626efec996eab4ae5a8c691d2c/black-24.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:56f52cfbd3dabe2798d76dbdd299faa046a901041faf2cf33288bc4e6dae57b5", size = 1385180 }, + { url = "https://files.pythonhosted.org/packages/4d/ea/31770a7e49f3eedfd8cd7b35e78b3a3aaad860400f8673994bc988318135/black-24.3.0-py3-none-any.whl", hash = "sha256:41622020d7120e01d377f74249e677039d20e6344ff5851de8a10f11f513bf93", size = 201493 }, + ] + + [[package]] + name = "click" + version = "8.1.7" + source = { registry = "https://pypi.org/simple" } + dependencies = [ + { name = "colorama", marker = "platform_system == 'Windows'" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941 }, + ] + + [[package]] + name = "colorama" + version = "0.4.6" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, + ] + + [[package]] + name = "idna" version = "3.6" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426 } @@ -13472,10 +13914,9 @@ fn lock_implicit_virtual_project() -> Result<()> { Ok(()) } -/// Lock a project that has a path dependency that is implicitly virtual (by way of omitting -/// `build-system`). +/// Lock a project that is implicitly virtual (by way of omitting `build-system`). #[test] -fn lock_implicit_virtual_path() -> Result<()> { +fn lock_implicit_virtual_project() -> Result<()> { let context = TestContext::new("3.12"); let pyproject_toml = context.temp_dir.child("pyproject.toml"); @@ -13485,21 +13926,12 @@ fn lock_implicit_virtual_path() -> Result<()> { name = "project" version = "0.1.0" requires-python = ">=3.12" - dependencies = ["anyio >3", "child"] - - [tool.uv.sources] - child = { path = "./child" } - "#, - )?; + dependencies = ["black"] - let child = context.temp_dir.child("child"); - child.child("pyproject.toml").write_str( - r#" - [project] - name = "child" - version = "0.1.0" - requires-python = ">=3.12" - dependencies = ["iniconfig >1"] + [tool.uv] + dev-dependencies = [ + "anyio" + ] "#, )?; @@ -13509,7 +13941,7 @@ fn lock_implicit_virtual_path() -> Result<()> { ----- stdout ----- ----- stderr ----- - Resolved 6 packages in [TIME] + Resolved 11 packages in [TIME] "###); let lock = context.read("uv.lock"); @@ -13539,36 +13971,263 @@ fn lock_implicit_virtual_path() -> Result<()> { ] [[package]] - name = "child" - version = "0.1.0" - source = { virtual = "child" } + name = "black" + version = "24.3.0" + source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "iniconfig" }, + { name = "click" }, + { name = "mypy-extensions" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "platformdirs" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/8f/5f/bac24a952668c7482cfdb4ebf91ba57a796c9da8829363a772040c1a3312/black-24.3.0.tar.gz", hash = "sha256:a0c9c4a0771afc6919578cec71ce82a3e31e054904e7197deacbc9382671c41f", size = 634292 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/c6/1d174efa9ff02b22d0124c73fc5f4d4fb006d0d9a081aadc354d05754a13/black-24.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:2818cf72dfd5d289e48f37ccfa08b460bf469e67fb7c4abb07edc2e9f16fb63f", size = 1600822 }, + { url = "https://files.pythonhosted.org/packages/d9/ed/704731afffe460b8ff0672623b40fce9fe569f2ee617c15857e4d4440a3a/black-24.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4acf672def7eb1725f41f38bf6bf425c8237248bb0804faa3965c036f7672d11", size = 1429987 }, + { url = "https://files.pythonhosted.org/packages/a8/05/8dd038e30caadab7120176d4bc109b7ca2f4457f12eef746b0560a583458/black-24.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7ed6668cbbfcd231fa0dc1b137d3e40c04c7f786e626b405c62bcd5db5857e4", size = 1755319 }, + { url = "https://files.pythonhosted.org/packages/71/9d/e5fa1ff4ef1940be15a64883c0bb8d2fcf626efec996eab4ae5a8c691d2c/black-24.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:56f52cfbd3dabe2798d76dbdd299faa046a901041faf2cf33288bc4e6dae57b5", size = 1385180 }, + { url = "https://files.pythonhosted.org/packages/4d/ea/31770a7e49f3eedfd8cd7b35e78b3a3aaad860400f8673994bc988318135/black-24.3.0-py3-none-any.whl", hash = "sha256:41622020d7120e01d377f74249e677039d20e6344ff5851de8a10f11f513bf93", size = 201493 }, ] - - [package.metadata] - requires-dist = [{ name = "iniconfig", specifier = ">1" }] [[package]] - name = "idna" - version = "3.6" + name = "click" + version = "8.1.7" source = { registry = "https://pypi.org/simple" } - sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426 } + dependencies = [ + { name = "colorama", marker = "platform_system == 'Windows'" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567 }, + { url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941 }, ] [[package]] - name = "iniconfig" - version = "2.0.0" + name = "colorama" + version = "0.4.6" source = { registry = "https://pypi.org/simple" } - sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } + sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, ] [[package]] - name = "project" + name = "idna" + version = "3.6" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567 }, + ] + + [[package]] + name = "mypy-extensions" + version = "1.0.0" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, + ] + + [[package]] + name = "packaging" + version = "24.0" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/ee/b5/b43a27ac7472e1818c4bafd44430e69605baefe1f34440593e0332ec8b4d/packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9", size = 147882 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/49/df/1fceb2f8900f8639e278b056416d49134fb8d84c5942ffaa01ad34782422/packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", size = 53488 }, + ] + + [[package]] + name = "pathspec" + version = "0.12.1" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 }, + ] + + [[package]] + name = "platformdirs" + version = "4.2.0" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/96/dc/c1d911bf5bb0fdc58cc05010e9f3efe3b67970cef779ba7fbc3183b987a8/platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768", size = 20055 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/55/72/4898c44ee9ea6f43396fbc23d9bfaf3d06e01b83698bdf2e4c919deceb7c/platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068", size = 17717 }, + ] + + [[package]] + name = "project" + version = "0.1.0" + source = { virtual = "." } + dependencies = [ + { name = "black" }, + ] + + [package.dev-dependencies] + dev = [ + { name = "anyio" }, + ] + + [package.metadata] + requires-dist = [{ name = "black" }] + + [package.metadata.requires-dev] + dev = [{ name = "anyio" }] + + [[package]] + name = "sniffio" + version = "1.3.1" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, + ] + "### + ); + }); + + // Re-run with `--locked`. + uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 11 packages in [TIME] + "###); + + // Re-run with `--offline`. We shouldn't need a network connection to validate an + // already-correct lockfile with immutable metadata. + uv_snapshot!(context.filters(), context.lock().arg("--locked").arg("--offline").arg("--no-cache"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 11 packages in [TIME] + "###); + + // Install from the lockfile. The virtual project should _not_ be installed. + uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Prepared 9 packages in [TIME] + Installed 9 packages in [TIME] + + anyio==4.3.0 + + black==24.3.0 + + click==8.1.7 + + idna==3.6 + + mypy-extensions==1.0.0 + + packaging==24.0 + + pathspec==0.12.1 + + platformdirs==4.2.0 + + sniffio==1.3.1 + "###); + + Ok(()) +} + +/// Lock a project that has a path dependency that is implicitly virtual (by way of omitting +/// `build-system`). +#[test] +fn lock_implicit_virtual_path() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["anyio >3", "child"] + + [tool.uv.sources] + child = { path = "./child" } + "#, + )?; + + let child = context.temp_dir.child("child"); + child.child("pyproject.toml").write_str( + r#" + [project] + name = "child" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["iniconfig >1"] + "#, + )?; + + uv_snapshot!(context.filters(), context.lock(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 6 packages in [TIME] + "###); + + let lock = context.read("uv.lock"); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r###" + version = 1 + requires-python = ">=3.12" + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [[package]] + name = "anyio" + version = "4.3.0" + source = { registry = "https://pypi.org/simple" } + dependencies = [ + { name = "idna" }, + { name = "sniffio" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/db/4d/3970183622f0330d3c23d9b8a5f52e365e50381fd484d08e3285104333d3/anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6", size = 159642 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8", size = 85584 }, + ] + + [[package]] + name = "child" + version = "0.1.0" + source = { virtual = "child" } + dependencies = [ + { name = "iniconfig" }, + ] + + [package.metadata] + requires-dist = [{ name = "iniconfig", specifier = ">1" }] + + [[package]] + name = "idna" + version = "3.6" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567 }, + ] + + [[package]] + name = "iniconfig" + version = "2.0.0" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, + ] + + [[package]] + name = "project" version = "0.1.0" source = { virtual = "." } dependencies = [ @@ -14153,119 +14812,1135 @@ fn lock_python_upper_bound() -> Result<()> { Ok(()) } -/// See: -#[test] -fn lock_simplified_environments() -> Result<()> { - let context = TestContext::new("3.11"); +/// See: +#[test] +fn lock_simplified_environments() -> Result<()> { + let context = TestContext::new("3.11"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.11,<3.12" + dependencies = ["iniconfig"] + + [tool.uv] + environments = [ + "sys_platform == 'darwin' and python_version >= '3.11' and python_version < '3.12'", + "sys_platform != 'darwin' and python_version >= '3.11' and python_version < '3.12'", + ] + "#, + )?; + + uv_snapshot!(context.filters(), context.lock(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + "###); + + let lock = context.read("uv.lock"); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r###" + version = 1 + requires-python = "==3.11.*" + resolution-markers = [ + "sys_platform == 'darwin'", + "sys_platform != 'darwin'", + ] + supported-markers = [ + "sys_platform == 'darwin'", + "sys_platform != 'darwin'", + ] + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [[package]] + name = "iniconfig" + version = "2.0.0" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, + ] + + [[package]] + name = "project" + version = "0.1.0" + source = { virtual = "." } + dependencies = [ + { name = "iniconfig" }, + ] + + [package.metadata] + requires-dist = [{ name = "iniconfig" }] + "### + ); + }); + + // Re-run with `--locked`. + uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + "###); + + // Re-run with `--offline`. We shouldn't need a network connection to validate an + // already-correct lockfile with immutable metadata. + uv_snapshot!(context.filters(), context.lock().arg("--locked").arg("--offline").arg("--no-cache"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + "###); + + // Install from the lockfile. + uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + iniconfig==2.0.0 + "###); + + Ok(()) +} + +#[test] +fn lock_dependency_metadata() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["anyio==3.7.0"] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + + [[tool.uv.dependency-metadata]] + name = "anyio" + version = "3.7.0" + requires-dist = ["iniconfig"] + "#, + )?; + + uv_snapshot!(context.filters(), context.lock(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + "###); + + let lock = context.read("uv.lock"); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r###" + version = 1 + requires-python = ">=3.12" + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [manifest] + + [[manifest.dependency-metadata]] + name = "anyio" + version = "3.7.0" + requires-dist = ["iniconfig"] + + [[package]] + name = "anyio" + version = "3.7.0" + source = { registry = "https://pypi.org/simple" } + dependencies = [ + { name = "iniconfig" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/c6/b3/fefbf7e78ab3b805dec67d698dc18dd505af7a18a8dd08868c9b4fa736b5/anyio-3.7.0.tar.gz", hash = "sha256:275d9973793619a5374e1c89a4f4ad3f4b0a5510a2b5b939444bee8f4c4d37ce", size = 142737 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/68/fe/7ce1926952c8a403b35029e194555558514b365ad77d75125f521a2bec62/anyio-3.7.0-py3-none-any.whl", hash = "sha256:eddca883c4175f14df8aedce21054bfca3adb70ffe76a9f607aef9d7fa2ea7f0", size = 80873 }, + ] + + [[package]] + name = "iniconfig" + version = "2.0.0" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, + ] + + [[package]] + name = "project" + version = "0.1.0" + source = { editable = "." } + dependencies = [ + { name = "anyio" }, + ] + + [package.metadata] + requires-dist = [{ name = "anyio", specifier = "==3.7.0" }] + "### + ); + }); + + // Re-run with `--locked`. + uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + "###); + + // Re-run with `--offline`. We shouldn't need a network connection to validate an + // already-correct lockfile with immutable metadata. + uv_snapshot!(context.filters(), context.lock().arg("--locked").arg("--offline").arg("--no-cache"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + "###); + + // Install from the lockfile. + uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Prepared 3 packages in [TIME] + Installed 3 packages in [TIME] + + anyio==3.7.0 + + iniconfig==2.0.0 + + project==0.1.0 (from file://[TEMP_DIR]/) + "###); + + // Update the static metadata. + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["anyio==3.7.0"] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + + [[tool.uv.dependency-metadata]] + name = "anyio" + version = "3.7.0" + requires-dist = ["typing-extensions"] + "#, + )?; + + // The lockfile should update. + uv_snapshot!(context.filters(), context.lock(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + Removed iniconfig v2.0.0 + Added typing-extensions v4.10.0 + "###); + + // Remove the static metadata. + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["anyio==3.7.0"] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#, + )?; + + // The lockfile should update. + uv_snapshot!(context.filters(), context.lock(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 4 packages in [TIME] + Added idna v3.6 + Added sniffio v1.3.1 + Removed typing-extensions v4.10.0 + "###); + + // Use a blanket match. + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["anyio==3.7.0"] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + + [[tool.uv.dependency-metadata]] + name = "anyio" + requires-dist = ["iniconfig"] + "#, + )?; + + // The lockfile should update. + uv_snapshot!(context.filters(), context.lock(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + Removed idna v3.6 + Added iniconfig v2.0.0 + Removed sniffio v1.3.1 + "###); + + Ok(()) +} + +#[test] +fn lock_dependency_metadata_git() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["anyio"] + + [tool.uv.sources] + anyio = { git = "https://github.com/agronholm/anyio", tag = "4.6.2" } + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + + [[tool.uv.dependency-metadata]] + name = "anyio" + version = "4.6.0.post2" + requires-dist = ["iniconfig"] + "#, + )?; + + uv_snapshot!(context.filters(), context.lock(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + "###); + + let lock = context.read("uv.lock"); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r###" + version = 1 + requires-python = ">=3.12" + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [manifest] + + [[manifest.dependency-metadata]] + name = "anyio" + version = "4.6.0.post2" + requires-dist = ["iniconfig"] + + [[package]] + name = "anyio" + version = "4.6.0.post2" + source = { git = "https://github.com/agronholm/anyio?tag=4.6.2#c4844254e6db0cb804c240ba07405db73d810e0b" } + dependencies = [ + { name = "iniconfig" }, + ] + + [[package]] + name = "iniconfig" + version = "2.0.0" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, + ] + + [[package]] + name = "project" + version = "0.1.0" + source = { editable = "." } + dependencies = [ + { name = "anyio" }, + ] + + [package.metadata] + requires-dist = [{ name = "anyio", git = "https://github.com/agronholm/anyio?tag=4.6.2" }] + "### + ); + }); + + // Re-run with `--locked`. + uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + "###); + + // Re-run with `--offline`. We shouldn't need a network connection to validate an + // already-correct lockfile with immutable metadata. + uv_snapshot!(context.filters(), context.lock().arg("--locked").arg("--offline").arg("--no-cache"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + "###); + + // Install from the lockfile. + uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Prepared 3 packages in [TIME] + Installed 3 packages in [TIME] + + anyio==4.6.2 (from git+https://github.com/agronholm/anyio@c4844254e6db0cb804c240ba07405db73d810e0b) + + iniconfig==2.0.0 + + project==0.1.0 (from file://[TEMP_DIR]/) + "###); + + Ok(()) +} + +#[test] +fn lock_strip_fragment() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["iniconfig @ https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl#sha256=b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#, + )?; + + uv_snapshot!(context.filters(), context.lock(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + "###); + + let lock = context.read("uv.lock"); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r###" + version = 1 + requires-python = ">=3.12" + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [[package]] + name = "iniconfig" + version = "2.0.0" + source = { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" }, + ] + + [[package]] + name = "project" + version = "0.1.0" + source = { editable = "." } + dependencies = [ + { name = "iniconfig" }, + ] + + [package.metadata] + requires-dist = [{ name = "iniconfig", url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl#sha256=b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" }] + "### + ); + }); + + // Re-run with `--locked`. + uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + "###); + + // Install from the lockfile. + uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Prepared 1 package in [TIME] + Installed 2 packages in [TIME] + + iniconfig==2.0.0 (from https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl) + + project==0.1.0 (from file://[TEMP_DIR]/) + "###); + + Ok(()) +} + +#[test] +fn lock_request_requires_python() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.8, <=3.10" + dependencies = ["iniconfig"] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#, + )?; + + // Request a version that conflicts with `--requires-python`. + uv_snapshot!(context.filters(), context.lock().arg("--python").arg("3.12"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] + error: The requested interpreter resolved to Python 3.12.[X], which is incompatible with the project's Python requirement: `>=3.8, <=3.10` + "###); + + // Add a `.python-version` file that conflicts. + let python_version = context.temp_dir.child(".python-version"); + python_version.write_str("3.12")?; + + uv_snapshot!(context.filters(), context.lock(), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] + error: The Python request from `.python-version` resolved to Python 3.12.[X], which is incompatible with the project's Python requirement: `>=3.8, <=3.10` + "###); + + Ok(()) +} + +#[test] +fn lock_duplicate_sources() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "projeect" + version = "0.1.0" + dependencies = ["python-multipart"] + + [tool.uv.sources] + python-multipart = { url = "https://files.pythonhosted.org/packages/3d/47/444768600d9e0ebc82f8e347775d24aef8f6348cf00e9fa0e81910814e6d/python_multipart-0.0.9-py3-none-any.whl" } + python-multipart = { url = "https://files.pythonhosted.org/packages/c0/3e/9fbfd74e7f5b54f653f7ca99d44ceb56e718846920162165061c4c22b71a/python_multipart-0.0.8-py3-none-any.whl" } + "#, + )?; + + uv_snapshot!(context.filters(), context.lock(), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + warning: Failed to parse `pyproject.toml` during settings discovery: + TOML parse error at line 9, column 9 + | + 9 | python-multipart = { url = "https://files.pythonhosted.org/packages/c0/3e/9fbfd74e7f5b54f653f7ca99d44ceb56e718846920162165061c4c22b71a/python_multipart-0.0.8-py3-none-any.whl" } + | ^ + duplicate key `python-multipart` in table `tool.uv.sources` + + error: Failed to parse: `pyproject.toml` + Caused by: TOML parse error at line 9, column 9 + | + 9 | python-multipart = { url = "https://files.pythonhosted.org/packages/c0/3e/9fbfd74e7f5b54f653f7ca99d44ceb56e718846920162165061c4c22b71a/python_multipart-0.0.8-py3-none-any.whl" } + | ^ + duplicate key `python-multipart` in table `tool.uv.sources` + + "###); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + dependencies = ["python-multipart"] + + [tool.uv.sources] + python-multipart = { url = "https://files.pythonhosted.org/packages/3d/47/444768600d9e0ebc82f8e347775d24aef8f6348cf00e9fa0e81910814e6d/python_multipart-0.0.9-py3-none-any.whl" } + python_multipart = { url = "https://files.pythonhosted.org/packages/c0/3e/9fbfd74e7f5b54f653f7ca99d44ceb56e718846920162165061c4c22b71a/python_multipart-0.0.8-py3-none-any.whl" } + "#, + )?; + + uv_snapshot!(context.filters(), context.lock(), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Failed to parse: `pyproject.toml` + Caused by: TOML parse error at line 7, column 9 + | + 7 | [tool.uv.sources] + | ^^^^^^^^^^^^^^^^^ + duplicate sources for package `python-multipart` + + "###); + + Ok(()) +} + +#[test] +fn lock_invalid_project_table() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("a/pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "a" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["b"] + + [tool.uv.sources] + b = { path = "../b" } + "#, + )?; + + let pyproject_toml = context.temp_dir.child("b/pyproject.toml"); + pyproject_toml.write_str( + r" + [project.urls] + repository = 'https://github.com/octocat/octocat-python' + ", + )?; + + uv_snapshot!(context.filters(), context.lock().current_dir(context.temp_dir.join("a")), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] + error: Failed to build: `b @ file://[TEMP_DIR]/b` + Caused by: Failed to extract static metadata from `pyproject.toml` + Caused by: `pyproject.toml` is using the `[project]` table, but the required `project.name` field is not set. + Caused by: TOML parse error at line 2, column 10 + | + 2 | [project.urls] + | ^^^^^^^ + missing field `name` + + "###); + + Ok(()) +} + +#[test] +fn lock_unsupported_version() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["iniconfig==2.0.0"] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#, + )?; + + // Validate schema, invalid version. + context.temp_dir.child("uv.lock").write_str( + r#" + version = 2 + requires-python = ">=3.12" + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [[package]] + name = "iniconfig" + version = "2.0.0" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, + ] + + [[package]] + name = "project" + version = "0.1.0" + source = { editable = "." } + dependencies = [ + { name = "iniconfig" }, + ] + + [package.metadata] + requires-dist = [{ name = "iniconfig", specifier = "==2.0.0" }] + "#, + )?; + + uv_snapshot!(context.filters(), context.lock().arg("--frozen"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: The lockfile at `uv.lock` uses an unsupported schema version (v2, but only v1 is supported). Downgrade to a compatible uv version, or remove the `uv.lock` prior to running `uv lock` or `uv sync`. + "###); + + // Invalid schema (`iniconfig` is referenced, but missing), invalid version. + context.temp_dir.child("uv.lock").write_str( + r#" + version = 2 + requires-python = ">=3.12" + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [[package]] + name = "project" + version = "0.1.0" + source = { editable = "." } + dependencies = [ + { name = "iniconfig" }, + ] + + [package.metadata] + requires-dist = [{ name = "iniconfig", specifier = "==2.0.0" }] + "#, + )?; + + uv_snapshot!(context.filters(), context.lock().arg("--frozen"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Failed to parse `uv.lock`, which uses an unsupported schema version (v2, but only v1 is supported). Downgrade to a compatible uv version, or remove the `uv.lock` prior to running `uv lock` or `uv sync`. + Caused by: Dependency `iniconfig` has missing `version` field but has more than one matching package + "###); + + Ok(()) +} + +/// See: +#[test] +fn lock_change_requires_python() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [ + "anyio <3 ; python_version == '3.12'", + "anyio >3, <4 ; python_version > '3.12'", + ] + "#, + )?; + + uv_snapshot!(context.filters(), context.lock(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 5 packages in [TIME] + "###); + + let lock = context.read("uv.lock"); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r###" + version = 1 + requires-python = ">=3.12" + resolution-markers = [ + "python_full_version < '3.13'", + "python_full_version >= '3.13'", + ] + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [[package]] + name = "anyio" + version = "2.2.0" + source = { registry = "https://pypi.org/simple" } + resolution-markers = [ + "python_full_version < '3.13'", + ] + dependencies = [ + { name = "idna", marker = "python_full_version < '3.13'" }, + { name = "sniffio", marker = "python_full_version < '3.13'" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/d3/e6/901a94731af20e7109415525666cb3753a2bd1edd19616c2730448dffd0d/anyio-2.2.0.tar.gz", hash = "sha256:4a41c5b3a65ed92e469d51b6fba3779301850ea2e352afcf9e36c46f21ee14a9", size = 97217 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/49/c3/b83a3c02c7d6f66932e9a72621d7f207cbfd2bd72b4c8931567ee386fb55/anyio-2.2.0-py3-none-any.whl", hash = "sha256:aa3da546ed17f097ca876c78024dea380a3b7fa80759abfdda59f12176a3dac8", size = 65320 }, + ] + + [[package]] + name = "anyio" + version = "3.7.1" + source = { registry = "https://pypi.org/simple" } + resolution-markers = [ + "python_full_version >= '3.13'", + ] + dependencies = [ + { name = "idna", marker = "python_full_version >= '3.13'" }, + { name = "sniffio", marker = "python_full_version >= '3.13'" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/28/99/2dfd53fd55ce9838e6ff2d4dac20ce58263798bd1a0dbe18b3a9af3fcfce/anyio-3.7.1.tar.gz", hash = "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780", size = 142927 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/19/24/44299477fe7dcc9cb58d0a57d5a7588d6af2ff403fdd2d47a246c91a3246/anyio-3.7.1-py3-none-any.whl", hash = "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5", size = 80896 }, + ] + + [[package]] + name = "idna" + version = "3.6" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567 }, + ] + + [[package]] + name = "project" + version = "0.1.0" + source = { virtual = "." } + dependencies = [ + { name = "anyio", version = "2.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "anyio", version = "3.7.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, + ] + + [package.metadata] + requires-dist = [ + { name = "anyio", marker = "python_full_version == '3.12.*'", specifier = "<3" }, + { name = "anyio", marker = "python_full_version >= '3.13'", specifier = ">3,<4" }, + ] + + [[package]] + name = "sniffio" + version = "1.3.1" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, + ] + "### + ); + }); + + // Lower the `requires-python`, expanding the set of supported versions. Loosen the upper-bound + // on `anyio` to ensure that we respect the already-locked version. + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.10" + dependencies = [ + "anyio <3 ; python_version == '3.12'", + "anyio >3 ; python_version > '3.12'", + ] + "#, + )?; + + uv_snapshot!(context.filters(), context.lock(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 5 packages in [TIME] + "###); + + let lock = context.read("uv.lock"); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r###" + version = 1 + requires-python = ">=3.10" + resolution-markers = [ + "python_full_version < '3.12'", + "python_full_version == '3.12.*'", + "python_full_version >= '3.13'", + ] + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [[package]] + name = "anyio" + version = "2.2.0" + source = { registry = "https://pypi.org/simple" } + resolution-markers = [ + "python_full_version == '3.12.*'", + ] + dependencies = [ + { name = "idna", marker = "python_full_version == '3.12.*'" }, + { name = "sniffio", marker = "python_full_version == '3.12.*'" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/d3/e6/901a94731af20e7109415525666cb3753a2bd1edd19616c2730448dffd0d/anyio-2.2.0.tar.gz", hash = "sha256:4a41c5b3a65ed92e469d51b6fba3779301850ea2e352afcf9e36c46f21ee14a9", size = 97217 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/49/c3/b83a3c02c7d6f66932e9a72621d7f207cbfd2bd72b4c8931567ee386fb55/anyio-2.2.0-py3-none-any.whl", hash = "sha256:aa3da546ed17f097ca876c78024dea380a3b7fa80759abfdda59f12176a3dac8", size = 65320 }, + ] + + [[package]] + name = "anyio" + version = "3.7.1" + source = { registry = "https://pypi.org/simple" } + resolution-markers = [ + "python_full_version >= '3.13'", + ] + dependencies = [ + { name = "idna", marker = "python_full_version >= '3.13'" }, + { name = "sniffio", marker = "python_full_version >= '3.13'" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/28/99/2dfd53fd55ce9838e6ff2d4dac20ce58263798bd1a0dbe18b3a9af3fcfce/anyio-3.7.1.tar.gz", hash = "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780", size = 142927 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/19/24/44299477fe7dcc9cb58d0a57d5a7588d6af2ff403fdd2d47a246c91a3246/anyio-3.7.1-py3-none-any.whl", hash = "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5", size = 80896 }, + ] + + [[package]] + name = "idna" + version = "3.6" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567 }, + ] + + [[package]] + name = "project" + version = "0.1.0" + source = { virtual = "." } + dependencies = [ + { name = "anyio", version = "2.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.12.*'" }, + { name = "anyio", version = "3.7.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, + ] + + [package.metadata] + requires-dist = [ + { name = "anyio", marker = "python_full_version == '3.12.*'", specifier = "<3" }, + { name = "anyio", marker = "python_full_version >= '3.13'", specifier = ">3" }, + ] + + [[package]] + name = "sniffio" + version = "1.3.1" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, + ] + "### + ); + }); + + Ok(()) +} + +/// Pass credentials for a named index via environment variables. +#[test] +fn lock_keyring_credentials() -> Result<()> { + let keyring_context = TestContext::new("3.12"); + + // Install our keyring plugin + keyring_context + .pip_install() + .arg( + keyring_context + .workspace_root + .join("scripts") + .join("packages") + .join("keyring_test_plugin"), + ) + .assert() + .success(); + + let context = TestContext::new("3.12"); let pyproject_toml = context.temp_dir.child("pyproject.toml"); pyproject_toml.write_str( r#" [project] - name = "project" + name = "foo" version = "0.1.0" - requires-python = ">=3.11,<3.12" + requires-python = ">=3.12" dependencies = ["iniconfig"] + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + [tool.uv] - environments = [ - "sys_platform == 'darwin' and python_version >= '3.11' and python_version < '3.12'", - "sys_platform != 'darwin' and python_version >= '3.11' and python_version < '3.12'", - ] + keyring-provider = "subprocess" + + [[tool.uv.index]] + name = "proxy" + url = "https://pypi-proxy.fly.dev/basic-auth/simple" + default = true "#, )?; - uv_snapshot!(context.filters(), context.lock(), @r###" + // Provide credentials via environment variables. + uv_snapshot!(context.filters(), context.lock() + .env(EnvVars::index_username("PROXY"), "public") + .env(EnvVars::KEYRING_TEST_CREDENTIALS, r#"{"pypi-proxy.fly.dev": {"public": "heron"}}"#) + .env(EnvVars::PATH, venv_bin_path(&keyring_context.venv)), @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- + Request for public@https://pypi-proxy.fly.dev/basic-auth/simple/iniconfig/ + Request for public@pypi-proxy.fly.dev Resolved 2 packages in [TIME] "###); - let lock = context.read("uv.lock"); + let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap(); + // The lockfile shout omit the credentials. insta::with_settings!({ filters => context.filters(), }, { assert_snapshot!( lock, @r###" version = 1 - requires-python = "==3.11.*" - resolution-markers = [ - "sys_platform == 'darwin'", - "sys_platform != 'darwin'", - ] - supported-markers = [ - "sys_platform == 'darwin'", - "sys_platform != 'darwin'", - ] + requires-python = ">=3.12" [options] exclude-newer = "2024-03-25T00:00:00Z" [[package]] - name = "iniconfig" - version = "2.0.0" - source = { registry = "https://pypi.org/simple" } - sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } - wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, - ] - - [[package]] - name = "project" + name = "foo" version = "0.1.0" - source = { virtual = "." } + source = { editable = "." } dependencies = [ { name = "iniconfig" }, ] [package.metadata] requires-dist = [{ name = "iniconfig" }] + + [[package]] + name = "iniconfig" + version = "2.0.0" + source = { registry = "https://pypi-proxy.fly.dev/basic-auth/simple" } + sdist = { url = "https://pypi-proxy.fly.dev/basic-auth/files/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } + wheels = [ + { url = "https://pypi-proxy.fly.dev/basic-auth/files/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, + ] "### ); }); - // Re-run with `--locked`. - uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 2 packages in [TIME] - "###); - - // Re-run with `--offline`. We shouldn't need a network connection to validate an - // already-correct lockfile with immutable metadata. - uv_snapshot!(context.filters(), context.lock().arg("--locked").arg("--offline").arg("--no-cache"), @r###" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 2 packages in [TIME] - "###); - - // Install from the lockfile. - uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Prepared 1 package in [TIME] - Installed 1 package in [TIME] - + iniconfig==2.0.0 - "###); - Ok(()) } #[test] -fn lock_dependency_metadata() -> Result<()> { +fn lock_multiple_sources() -> Result<()> { let context = TestContext::new("3.12"); let pyproject_toml = context.temp_dir.child("pyproject.toml"); @@ -14275,16 +15950,13 @@ fn lock_dependency_metadata() -> Result<()> { name = "project" version = "0.1.0" requires-python = ">=3.12" - dependencies = ["anyio==3.7.0"] - - [build-system] - requires = ["setuptools>=42"] - build-backend = "setuptools.build_meta" + dependencies = ["iniconfig"] - [[tool.uv.dependency-metadata]] - name = "anyio" - version = "3.7.0" - requires-dist = ["iniconfig"] + [tool.uv.sources] + iniconfig = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", marker = "sys_platform != 'win32'" }, + { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", marker = "sys_platform == 'win32'" }, + ] "#, )?; @@ -14306,48 +15978,48 @@ fn lock_dependency_metadata() -> Result<()> { lock, @r###" version = 1 requires-python = ">=3.12" + resolution-markers = [ + "sys_platform != 'win32'", + "sys_platform == 'win32'", + ] [options] exclude-newer = "2024-03-25T00:00:00Z" - [manifest] - - [[manifest.dependency-metadata]] - name = "anyio" - version = "3.7.0" - requires-dist = ["iniconfig"] - [[package]] - name = "anyio" - version = "3.7.0" - source = { registry = "https://pypi.org/simple" } - dependencies = [ - { name = "iniconfig" }, - ] - sdist = { url = "https://files.pythonhosted.org/packages/c6/b3/fefbf7e78ab3b805dec67d698dc18dd505af7a18a8dd08868c9b4fa736b5/anyio-3.7.0.tar.gz", hash = "sha256:275d9973793619a5374e1c89a4f4ad3f4b0a5510a2b5b939444bee8f4c4d37ce", size = 142737 } - wheels = [ - { url = "https://files.pythonhosted.org/packages/68/fe/7ce1926952c8a403b35029e194555558514b365ad77d75125f521a2bec62/anyio-3.7.0-py3-none-any.whl", hash = "sha256:eddca883c4175f14df8aedce21054bfca3adb70ffe76a9f607aef9d7fa2ea7f0", size = 80873 }, + name = "iniconfig" + version = "2.0.0" + source = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz" } + resolution-markers = [ + "sys_platform == 'win32'", ] + sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3" } [[package]] name = "iniconfig" version = "2.0.0" - source = { registry = "https://pypi.org/simple" } - sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } + source = { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl" } + resolution-markers = [ + "sys_platform != 'win32'", + ] wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" }, ] [[package]] name = "project" version = "0.1.0" - source = { editable = "." } + source = { virtual = "." } dependencies = [ - { name = "anyio" }, + { name = "iniconfig", version = "2.0.0", source = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz" }, marker = "sys_platform == 'win32'" }, + { name = "iniconfig", version = "2.0.0", source = { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl" }, marker = "sys_platform != 'win32'" }, ] [package.metadata] - requires-dist = [{ name = "anyio", specifier = "==3.7.0" }] + requires-dist = [ + { name = "iniconfig", marker = "sys_platform != 'win32'", url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl" }, + { name = "iniconfig", marker = "sys_platform == 'win32'", url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz" }, + ] "### ); }); @@ -14362,128 +16034,52 @@ fn lock_dependency_metadata() -> Result<()> { Resolved 3 packages in [TIME] "###); - // Re-run with `--offline`. We shouldn't need a network connection to validate an - // already-correct lockfile with immutable metadata. - uv_snapshot!(context.filters(), context.lock().arg("--locked").arg("--offline").arg("--no-cache"), @r###" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 3 packages in [TIME] - "###); - - // Install from the lockfile. - uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Prepared 3 packages in [TIME] - Installed 3 packages in [TIME] - + anyio==3.7.0 - + iniconfig==2.0.0 - + project==0.1.0 (from file://[TEMP_DIR]/) - "###); - - // Update the static metadata. - pyproject_toml.write_str( - r#" - [project] - name = "project" - version = "0.1.0" - requires-python = ">=3.12" - dependencies = ["anyio==3.7.0"] - - [build-system] - requires = ["setuptools>=42"] - build-backend = "setuptools.build_meta" - - [[tool.uv.dependency-metadata]] - name = "anyio" - version = "3.7.0" - requires-dist = ["typing-extensions"] - "#, - )?; - - // The lockfile should update. - uv_snapshot!(context.filters(), context.lock(), @r###" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 3 packages in [TIME] - Removed iniconfig v2.0.0 - Added typing-extensions v4.10.0 - "###); - - // Remove the static metadata. - pyproject_toml.write_str( - r#" - [project] - name = "project" - version = "0.1.0" - requires-python = ">=3.12" - dependencies = ["anyio==3.7.0"] - - [build-system] - requires = ["setuptools>=42"] - build-backend = "setuptools.build_meta" - "#, - )?; - - // The lockfile should update. - uv_snapshot!(context.filters(), context.lock(), @r###" - success: true - exit_code: 0 - ----- stdout ----- + Ok(()) +} - ----- stderr ----- - Resolved 4 packages in [TIME] - Added idna v3.6 - Added sniffio v1.3.1 - Removed typing-extensions v4.10.0 - "###); +#[test] +fn lock_multiple_sources_conflict() -> Result<()> { + let context = TestContext::new("3.12"); - // Use a blanket match. + let pyproject_toml = context.temp_dir.child("pyproject.toml"); pyproject_toml.write_str( r#" [project] name = "project" version = "0.1.0" requires-python = ">=3.12" - dependencies = ["anyio==3.7.0"] - - [build-system] - requires = ["setuptools>=42"] - build-backend = "setuptools.build_meta" + dependencies = ["iniconfig"] - [[tool.uv.dependency-metadata]] - name = "anyio" - requires-dist = ["iniconfig"] + [tool.uv.sources] + iniconfig = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", marker = "sys_platform == 'win32' and python_version == '3.12'" }, + { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", marker = "sys_platform == 'win32'" }, + ] "#, )?; - // The lockfile should update. uv_snapshot!(context.filters(), context.lock(), @r###" - success: true - exit_code: 0 + success: false + exit_code: 2 ----- stdout ----- ----- stderr ----- - Resolved 3 packages in [TIME] - Removed idna v3.6 - Added iniconfig v2.0.0 - Removed sniffio v1.3.1 + error: Failed to parse: `pyproject.toml` + Caused by: TOML parse error at line 9, column 21 + | + 9 | iniconfig = [ + | ^ + Source markers must be disjoint, but the following markers overlap: `python_full_version == '3.12.*' and sys_platform == 'win32'` and `sys_platform == 'win32'`. + + hint: replace `sys_platform == 'win32'` with `python_full_version != '3.12.*' and sys_platform == 'win32'`. + "###); Ok(()) } #[test] -fn lock_strip_fragment() -> Result<()> { +fn lock_multiple_sources_index() -> Result<()> { let context = TestContext::new("3.12"); let pyproject_toml = context.temp_dir.child("pyproject.toml"); @@ -14493,24 +16089,37 @@ fn lock_strip_fragment() -> Result<()> { name = "project" version = "0.1.0" requires-python = ">=3.12" - dependencies = ["iniconfig @ https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl#sha256=b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"] + dependencies = ["jinja2>=3"] - [build-system] - requires = ["setuptools>=42"] - build-backend = "setuptools.build_meta" + [tool.uv] + constraint-dependencies = ["markupsafe<3"] + + [tool.uv.sources] + jinja2 = [ + { index = "torch-cu118", marker = "sys_platform == 'win32'"}, + { index = "torch-cu124", marker = "sys_platform != 'win32'"}, + ] + + [[tool.uv.index]] + name = "torch-cu118" + url = "https://download.pytorch.org/whl/cu118" + + [[tool.uv.index]] + name = "torch-cu124" + url = "https://download.pytorch.org/whl/cu124" "#, )?; - uv_snapshot!(context.filters(), context.lock(), @r###" + uv_snapshot!(context.filters(), context.lock().env_remove(EnvVars::UV_EXCLUDE_NEWER), @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- - Resolved 2 packages in [TIME] + Resolved 4 packages in [TIME] "###); - let lock = context.read("uv.lock"); + let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap(); insta::with_settings!({ filters => context.filters(), @@ -14519,60 +16128,88 @@ fn lock_strip_fragment() -> Result<()> { lock, @r###" version = 1 requires-python = ">=3.12" + resolution-markers = [ + "sys_platform == 'win32'", + "sys_platform != 'win32'", + ] - [options] - exclude-newer = "2024-03-25T00:00:00Z" + [manifest] + constraints = [{ name = "markupsafe", specifier = "<3" }] [[package]] - name = "iniconfig" - version = "2.0.0" - source = { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl" } + name = "jinja2" + version = "3.1.3" + source = { registry = "https://download.pytorch.org/whl/cu118" } + resolution-markers = [ + "sys_platform == 'win32'", + ] + dependencies = [ + { name = "markupsafe", marker = "sys_platform == 'win32'" }, + ] wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" }, + { url = "https://download.pytorch.org/whl/Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa" }, + ] + + [[package]] + name = "jinja2" + version = "3.1.3" + source = { registry = "https://download.pytorch.org/whl/cu124" } + resolution-markers = [ + "sys_platform != 'win32'", + ] + dependencies = [ + { name = "markupsafe", marker = "sys_platform != 'win32'" }, + ] + wheels = [ + { url = "https://download.pytorch.org/whl/Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa" }, + ] + + [[package]] + name = "markupsafe" + version = "2.1.5" + source = { registry = "https://download.pytorch.org/whl/cu118" } + wheels = [ + { url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1" }, + { url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4" }, + { url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee" }, + { url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5" }, + { url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b" }, + { url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb" }, ] [[package]] name = "project" version = "0.1.0" - source = { editable = "." } + source = { virtual = "." } dependencies = [ - { name = "iniconfig" }, + { name = "jinja2", version = "3.1.3", source = { registry = "https://download.pytorch.org/whl/cu118" }, marker = "sys_platform == 'win32'" }, + { name = "jinja2", version = "3.1.3", source = { registry = "https://download.pytorch.org/whl/cu124" }, marker = "sys_platform != 'win32'" }, ] [package.metadata] - requires-dist = [{ name = "iniconfig", url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl#sha256=b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" }] + requires-dist = [ + { name = "jinja2", marker = "sys_platform != 'win32'", specifier = ">=3", index = "https://download.pytorch.org/whl/cu124" }, + { name = "jinja2", marker = "sys_platform == 'win32'", specifier = ">=3", index = "https://download.pytorch.org/whl/cu118" }, + ] "### ); }); // Re-run with `--locked`. - uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 2 packages in [TIME] - "###); - - // Install from the lockfile. - uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" + uv_snapshot!(context.filters(), context.lock().arg("--locked").env_remove(EnvVars::UV_EXCLUDE_NEWER), @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- - Prepared 1 package in [TIME] - Installed 2 packages in [TIME] - + iniconfig==2.0.0 (from https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl) - + project==0.1.0 (from file://[TEMP_DIR]/) + Resolved 4 packages in [TIME] "###); Ok(()) } #[test] -fn lock_request_requires_python() -> Result<()> { +fn lock_multiple_sources_index_mixed() -> Result<()> { let context = TestContext::new("3.12"); let pyproject_toml = context.temp_dir.child("pyproject.toml"); @@ -14581,165 +16218,167 @@ fn lock_request_requires_python() -> Result<()> { [project] name = "project" version = "0.1.0" - requires-python = ">=3.8, <=3.10" - dependencies = ["iniconfig"] - - [build-system] - requires = ["setuptools>=42"] - build-backend = "setuptools.build_meta" - "#, - )?; + requires-python = ">=3.12" + dependencies = ["jinja2>=3"] - // Request a version that conflicts with `--requires-python`. - uv_snapshot!(context.filters(), context.lock().arg("--python").arg("3.12"), @r###" - success: false - exit_code: 2 - ----- stdout ----- + [tool.uv] + constraint-dependencies = ["markupsafe<3"] - ----- stderr ----- - Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] - error: The requested interpreter resolved to Python 3.12.[X], which is incompatible with the project's Python requirement: `>=3.8, <=3.10` - "###); + [tool.uv.sources] + jinja2 = [ + { index = "torch-cu118", marker = "sys_platform == 'win32'"}, + { url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl", marker = "sys_platform != 'win32'"}, + ] - // Add a `.python-version` file that conflicts. - let python_version = context.temp_dir.child(".python-version"); - python_version.write_str("3.12")?; + [[tool.uv.index]] + name = "torch-cu118" + url = "https://download.pytorch.org/whl/cu118" + "#, + )?; - uv_snapshot!(context.filters(), context.lock(), @r###" - success: false - exit_code: 2 + uv_snapshot!(context.filters(), context.lock().env_remove(EnvVars::UV_EXCLUDE_NEWER), @r###" + success: true + exit_code: 0 ----- stdout ----- ----- stderr ----- - Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] - error: The Python request from `.python-version` resolved to Python 3.12.[X], which is incompatible with the project's Python requirement: `>=3.8, <=3.10` + Resolved 4 packages in [TIME] "###); - Ok(()) -} - -#[test] -fn lock_duplicate_sources() -> Result<()> { - let context = TestContext::new("3.12"); + let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap(); - let pyproject_toml = context.temp_dir.child("pyproject.toml"); - pyproject_toml.write_str( - r#" - [project] - name = "projeect" - version = "0.1.0" - dependencies = ["python-multipart"] + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r###" + version = 1 + requires-python = ">=3.12" + resolution-markers = [ + "sys_platform == 'win32'", + "sys_platform != 'win32'", + ] - [tool.uv.sources] - python-multipart = { url = "https://files.pythonhosted.org/packages/3d/47/444768600d9e0ebc82f8e347775d24aef8f6348cf00e9fa0e81910814e6d/python_multipart-0.0.9-py3-none-any.whl" } - python-multipart = { url = "https://files.pythonhosted.org/packages/c0/3e/9fbfd74e7f5b54f653f7ca99d44ceb56e718846920162165061c4c22b71a/python_multipart-0.0.8-py3-none-any.whl" } - "#, - )?; + [manifest] + constraints = [{ name = "markupsafe", specifier = "<3" }] - uv_snapshot!(context.filters(), context.lock(), @r###" - success: false - exit_code: 2 - ----- stdout ----- + [[package]] + name = "jinja2" + version = "3.1.3" + source = { registry = "https://download.pytorch.org/whl/cu118" } + resolution-markers = [ + "sys_platform == 'win32'", + ] + dependencies = [ + { name = "markupsafe", marker = "sys_platform == 'win32'" }, + ] + wheels = [ + { url = "https://download.pytorch.org/whl/Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa" }, + ] - ----- stderr ----- - warning: Failed to parse `pyproject.toml` during settings discovery: - TOML parse error at line 9, column 9 - | - 9 | python-multipart = { url = "https://files.pythonhosted.org/packages/c0/3e/9fbfd74e7f5b54f653f7ca99d44ceb56e718846920162165061c4c22b71a/python_multipart-0.0.8-py3-none-any.whl" } - | ^ - duplicate key `python-multipart` in table `tool.uv.sources` + [[package]] + name = "jinja2" + version = "3.1.4" + source = { url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl" } + resolution-markers = [ + "sys_platform != 'win32'", + ] + dependencies = [ + { name = "markupsafe", marker = "sys_platform != 'win32'" }, + ] + wheels = [ + { url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d" }, + ] - error: Failed to parse: `pyproject.toml` - Caused by: TOML parse error at line 9, column 9 - | - 9 | python-multipart = { url = "https://files.pythonhosted.org/packages/c0/3e/9fbfd74e7f5b54f653f7ca99d44ceb56e718846920162165061c4c22b71a/python_multipart-0.0.8-py3-none-any.whl" } - | ^ - duplicate key `python-multipart` in table `tool.uv.sources` + [package.metadata] + requires-dist = [ + { name = "babel", marker = "extra == 'i18n'", specifier = ">=2.7" }, + { name = "markupsafe", specifier = ">=2.0" }, + ] - "###); + [[package]] + name = "markupsafe" + version = "2.1.5" + source = { registry = "https://download.pytorch.org/whl/cu118" } + wheels = [ + { url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1" }, + { url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4" }, + { url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee" }, + { url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5" }, + { url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b" }, + { url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb" }, + ] - let pyproject_toml = context.temp_dir.child("pyproject.toml"); - pyproject_toml.write_str( - r#" - [project] + [[package]] name = "project" version = "0.1.0" - dependencies = ["python-multipart"] + source = { virtual = "." } + dependencies = [ + { name = "jinja2", version = "3.1.3", source = { registry = "https://download.pytorch.org/whl/cu118" }, marker = "sys_platform == 'win32'" }, + { name = "jinja2", version = "3.1.4", source = { url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl" }, marker = "sys_platform != 'win32'" }, + ] - [tool.uv.sources] - python-multipart = { url = "https://files.pythonhosted.org/packages/3d/47/444768600d9e0ebc82f8e347775d24aef8f6348cf00e9fa0e81910814e6d/python_multipart-0.0.9-py3-none-any.whl" } - python_multipart = { url = "https://files.pythonhosted.org/packages/c0/3e/9fbfd74e7f5b54f653f7ca99d44ceb56e718846920162165061c4c22b71a/python_multipart-0.0.8-py3-none-any.whl" } - "#, - )?; + [package.metadata] + requires-dist = [ + { name = "jinja2", marker = "sys_platform != 'win32'", url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl" }, + { name = "jinja2", marker = "sys_platform == 'win32'", specifier = ">=3", index = "https://download.pytorch.org/whl/cu118" }, + ] + "### + ); + }); - uv_snapshot!(context.filters(), context.lock(), @r###" - success: false - exit_code: 2 + // Re-run with `--locked`. + uv_snapshot!(context.filters(), context.lock().arg("--locked").env_remove(EnvVars::UV_EXCLUDE_NEWER), @r###" + success: true + exit_code: 0 ----- stdout ----- ----- stderr ----- - error: Failed to parse: `pyproject.toml` - Caused by: TOML parse error at line 7, column 9 - | - 7 | [tool.uv.sources] - | ^^^^^^^^^^^^^^^^^ - duplicate sources for package `python-multipart` - + Resolved 4 packages in [TIME] "###); Ok(()) } #[test] -fn lock_invalid_project_table() -> Result<()> { +fn lock_multiple_sources_index_non_total() -> Result<()> { let context = TestContext::new("3.12"); - let pyproject_toml = context.temp_dir.child("a/pyproject.toml"); + let pyproject_toml = context.temp_dir.child("pyproject.toml"); pyproject_toml.write_str( r#" [project] - name = "a" + name = "project" version = "0.1.0" requires-python = ">=3.12" - dependencies = ["b"] + dependencies = ["jinja2>=3"] [tool.uv.sources] - b = { path = "../b" } - "#, - )?; + jinja2 = [ + { index = "torch-cu118", marker = "sys_platform == 'win32'"}, + ] - let pyproject_toml = context.temp_dir.child("b/pyproject.toml"); - pyproject_toml.write_str( - r" - [project.urls] - repository = 'https://github.com/octocat/octocat-python' - ", + [[tool.uv.index]] + name = "torch-cu118" + url = "https://download.pytorch.org/whl/cu118" + "#, )?; - uv_snapshot!(context.filters(), context.lock().current_dir(context.temp_dir.join("a")), @r###" + uv_snapshot!(context.filters(), context.lock().env_remove(EnvVars::UV_EXCLUDE_NEWER), @r###" success: false exit_code: 2 ----- stdout ----- ----- stderr ----- - Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] - error: Failed to build: `b @ file://[TEMP_DIR]/b` - Caused by: Failed to extract static metadata from `pyproject.toml` - Caused by: `pyproject.toml` is using the `[project]` table, but the required `project.name` field is not set. - Caused by: TOML parse error at line 2, column 10 - | - 2 | [project.urls] - | ^^^^^^^ - missing field `name` - + Resolved 4 packages in [TIME] + error: Found duplicate package `jinja2==3.1.3 @ registry+https://download.pytorch.org/whl/cu118` "###); Ok(()) } -/// See: #[test] -fn lock_change_requires_python() -> Result<()> { +fn lock_multiple_sources_index_explicit() -> Result<()> { let context = TestContext::new("3.12"); let pyproject_toml = context.temp_dir.child("pyproject.toml"); @@ -14749,10 +16388,17 @@ fn lock_change_requires_python() -> Result<()> { name = "project" version = "0.1.0" requires-python = ">=3.12" - dependencies = [ - "anyio <3 ; python_version == '3.12'", - "anyio >3, <4 ; python_version > '3.12'", + dependencies = ["jinja2>=3"] + + [tool.uv.sources] + jinja2 = [ + { index = "torch-cu118", marker = "sys_platform == 'win32'"}, ] + + [[tool.uv.index]] + name = "torch-cu118" + url = "https://test.pypi.org/simple" + explicit = true "#, )?; @@ -14762,10 +16408,10 @@ fn lock_change_requires_python() -> Result<()> { ----- stdout ----- ----- stderr ----- - Resolved 5 packages in [TIME] + Resolved 4 packages in [TIME] "###); - let lock = context.read("uv.lock"); + let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap(); insta::with_settings!({ filters => context.filters(), @@ -14775,52 +16421,59 @@ fn lock_change_requires_python() -> Result<()> { version = 1 requires-python = ">=3.12" resolution-markers = [ - "python_full_version < '3.13'", - "python_full_version >= '3.13'", + "sys_platform == 'win32'", + "sys_platform != 'win32'", ] [options] exclude-newer = "2024-03-25T00:00:00Z" [[package]] - name = "anyio" - version = "2.2.0" + name = "jinja2" + version = "3.1.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.13'", + "sys_platform != 'win32'", ] dependencies = [ - { name = "idna", marker = "python_full_version < '3.13'" }, - { name = "sniffio", marker = "python_full_version < '3.13'" }, + { name = "markupsafe", marker = "sys_platform != 'win32'" }, ] - sdist = { url = "https://files.pythonhosted.org/packages/d3/e6/901a94731af20e7109415525666cb3753a2bd1edd19616c2730448dffd0d/anyio-2.2.0.tar.gz", hash = "sha256:4a41c5b3a65ed92e469d51b6fba3779301850ea2e352afcf9e36c46f21ee14a9", size = 97217 } + sdist = { url = "https://files.pythonhosted.org/packages/b2/5e/3a21abf3cd467d7876045335e681d276ac32492febe6d98ad89562d1a7e1/Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90", size = 268261 } wheels = [ - { url = "https://files.pythonhosted.org/packages/49/c3/b83a3c02c7d6f66932e9a72621d7f207cbfd2bd72b4c8931567ee386fb55/anyio-2.2.0-py3-none-any.whl", hash = "sha256:aa3da546ed17f097ca876c78024dea380a3b7fa80759abfdda59f12176a3dac8", size = 65320 }, + { url = "https://files.pythonhosted.org/packages/30/6d/6de6be2d02603ab56e72997708809e8a5b0fbfee080735109b40a3564843/Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa", size = 133236 }, ] [[package]] - name = "anyio" - version = "3.7.1" - source = { registry = "https://pypi.org/simple" } + name = "jinja2" + version = "3.1.3" + source = { registry = "https://test.pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.13'", + "sys_platform == 'win32'", ] dependencies = [ - { name = "idna", marker = "python_full_version >= '3.13'" }, - { name = "sniffio", marker = "python_full_version >= '3.13'" }, + { name = "markupsafe", marker = "sys_platform == 'win32'" }, ] - sdist = { url = "https://files.pythonhosted.org/packages/28/99/2dfd53fd55ce9838e6ff2d4dac20ce58263798bd1a0dbe18b3a9af3fcfce/anyio-3.7.1.tar.gz", hash = "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780", size = 142927 } + sdist = { url = "https://test-files.pythonhosted.org/packages/3e/f0/69ae37cced6b277dc0419dbb1c6e4fb259e5e319a1a971061a2776316bec/Jinja2-3.1.3.tar.gz", hash = "sha256:27fb536952e578492fa66d8681d8967d8bdf1eb36368b1f842b53251c9f0bfe1", size = 268254 } wheels = [ - { url = "https://files.pythonhosted.org/packages/19/24/44299477fe7dcc9cb58d0a57d5a7588d6af2ff403fdd2d47a246c91a3246/anyio-3.7.1-py3-none-any.whl", hash = "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5", size = 80896 }, + { url = "https://test-files.pythonhosted.org/packages/47/dc/9d1c0f1ddbedb1e67f7d00e91819b5a9157056ad83bfa64c12ecef8a4f4e/Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:ddd11470e8a1dc4c30e3146400f0130fed7d85886c5f8082f309355b4b0c1128", size = 133236 }, ] [[package]] - name = "idna" - version = "3.6" + name = "markupsafe" + version = "2.1.5" source = { registry = "https://pypi.org/simple" } - sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426 } + sdist = { url = "https://files.pythonhosted.org/packages/87/5b/aae44c6655f3801e81aa3eef09dbbf012431987ba564d7231722f68df02d/MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", size = 19384 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567 }, + { url = "https://files.pythonhosted.org/packages/53/bd/583bf3e4c8d6a321938c13f49d44024dbe5ed63e0a7ba127e454a66da974/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", size = 18215 }, + { url = "https://files.pythonhosted.org/packages/48/d6/e7cd795fc710292c3af3a06d80868ce4b02bfbbf370b7cee11d282815a2a/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", size = 14069 }, + { url = "https://files.pythonhosted.org/packages/51/b5/5d8ec796e2a08fc814a2c7d2584b55f889a55cf17dd1a90f2beb70744e5c/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", size = 29452 }, + { url = "https://files.pythonhosted.org/packages/0a/0d/2454f072fae3b5a137c119abf15465d1771319dfe9e4acbb31722a0fff91/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", size = 28462 }, + { url = "https://files.pythonhosted.org/packages/2d/75/fd6cb2e68780f72d47e6671840ca517bda5ef663d30ada7616b0462ad1e3/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", size = 27869 }, + { url = "https://files.pythonhosted.org/packages/b0/81/147c477391c2750e8fc7705829f7351cf1cd3be64406edcf900dc633feb2/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", size = 33906 }, + { url = "https://files.pythonhosted.org/packages/8b/ff/9a52b71839d7a256b563e85d11050e307121000dcebc97df120176b3ad93/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", size = 32296 }, + { url = "https://files.pythonhosted.org/packages/88/07/2dc76aa51b481eb96a4c3198894f38b480490e834479611a4053fbf08623/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", size = 33038 }, + { url = "https://files.pythonhosted.org/packages/96/0c/620c1fb3661858c0e37eb3cbffd8c6f732a67cd97296f725789679801b31/MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", size = 16572 }, + { url = "https://files.pythonhosted.org/packages/3f/14/c3554d512d5f9100a95e737502f4a2323a1959f6d0d01e0d0997b35f7b10/MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", size = 17127 }, ] [[package]] @@ -14828,39 +16481,48 @@ fn lock_change_requires_python() -> Result<()> { version = "0.1.0" source = { virtual = "." } dependencies = [ - { name = "anyio", version = "2.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, - { name = "anyio", version = "3.7.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, + { name = "jinja2", version = "3.1.3", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'win32'" }, + { name = "jinja2", version = "3.1.3", source = { registry = "https://test.pypi.org/simple" }, marker = "sys_platform == 'win32'" }, ] [package.metadata] requires-dist = [ - { name = "anyio", marker = "python_full_version == '3.12.*'", specifier = "<3" }, - { name = "anyio", marker = "python_full_version >= '3.13'", specifier = ">3,<4" }, - ] - - [[package]] - name = "sniffio" - version = "1.3.1" - source = { registry = "https://pypi.org/simple" } - sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } - wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, + { name = "jinja2", marker = "sys_platform != 'win32'", specifier = ">=3" }, + { name = "jinja2", marker = "sys_platform == 'win32'", specifier = ">=3", index = "https://test.pypi.org/simple" }, ] "### ); }); - // Lower the `requires-python`, expanding the set of supported versions. Loosen the upper-bound - // on `anyio` to ensure that we respect the already-locked version. + // Re-run with `--locked`. + uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 4 packages in [TIME] + "###); + + Ok(()) +} + +#[test] +fn lock_multiple_sources_non_total() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); pyproject_toml.write_str( r#" [project] name = "project" version = "0.1.0" - requires-python = ">=3.10" - dependencies = [ - "anyio <3 ; python_version == '3.12'", - "anyio >3 ; python_version > '3.12'", + requires-python = ">=3.12" + dependencies = ["iniconfig"] + + [tool.uv.sources] + iniconfig = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", marker = "sys_platform == 'darwin'" }, ] "#, )?; @@ -14871,7 +16533,7 @@ fn lock_change_requires_python() -> Result<()> { ----- stdout ----- ----- stderr ----- - Resolved 5 packages in [TIME] + Resolved 3 packages in [TIME] "###); let lock = context.read("uv.lock"); @@ -14882,55 +16544,36 @@ fn lock_change_requires_python() -> Result<()> { assert_snapshot!( lock, @r###" version = 1 - requires-python = ">=3.10" + requires-python = ">=3.12" resolution-markers = [ - "python_full_version < '3.12'", - "python_full_version == '3.12.*'", - "python_full_version >= '3.13'", + "sys_platform == 'darwin'", + "sys_platform != 'darwin'", ] [options] exclude-newer = "2024-03-25T00:00:00Z" [[package]] - name = "anyio" - version = "2.2.0" + name = "iniconfig" + version = "2.0.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version == '3.12.*'", - ] - dependencies = [ - { name = "idna", marker = "python_full_version == '3.12.*'" }, - { name = "sniffio", marker = "python_full_version == '3.12.*'" }, + "sys_platform != 'darwin'", ] - sdist = { url = "https://files.pythonhosted.org/packages/d3/e6/901a94731af20e7109415525666cb3753a2bd1edd19616c2730448dffd0d/anyio-2.2.0.tar.gz", hash = "sha256:4a41c5b3a65ed92e469d51b6fba3779301850ea2e352afcf9e36c46f21ee14a9", size = 97217 } + sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } wheels = [ - { url = "https://files.pythonhosted.org/packages/49/c3/b83a3c02c7d6f66932e9a72621d7f207cbfd2bd72b4c8931567ee386fb55/anyio-2.2.0-py3-none-any.whl", hash = "sha256:aa3da546ed17f097ca876c78024dea380a3b7fa80759abfdda59f12176a3dac8", size = 65320 }, + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, ] [[package]] - name = "anyio" - version = "3.7.1" - source = { registry = "https://pypi.org/simple" } + name = "iniconfig" + version = "2.0.0" + source = { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl" } resolution-markers = [ - "python_full_version >= '3.13'", - ] - dependencies = [ - { name = "idna", marker = "python_full_version >= '3.13'" }, - { name = "sniffio", marker = "python_full_version >= '3.13'" }, - ] - sdist = { url = "https://files.pythonhosted.org/packages/28/99/2dfd53fd55ce9838e6ff2d4dac20ce58263798bd1a0dbe18b3a9af3fcfce/anyio-3.7.1.tar.gz", hash = "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780", size = 142927 } - wheels = [ - { url = "https://files.pythonhosted.org/packages/19/24/44299477fe7dcc9cb58d0a57d5a7588d6af2ff403fdd2d47a246c91a3246/anyio-3.7.1-py3-none-any.whl", hash = "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5", size = 80896 }, + "sys_platform == 'darwin'", ] - - [[package]] - name = "idna" - version = "3.6" - source = { registry = "https://pypi.org/simple" } - sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567 }, + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" }, ] [[package]] @@ -14938,92 +16581,63 @@ fn lock_change_requires_python() -> Result<()> { version = "0.1.0" source = { virtual = "." } dependencies = [ - { name = "anyio", version = "2.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.12.*'" }, - { name = "anyio", version = "3.7.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, + { name = "iniconfig", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'darwin'" }, + { name = "iniconfig", version = "2.0.0", source = { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl" }, marker = "sys_platform == 'darwin'" }, ] [package.metadata] requires-dist = [ - { name = "anyio", marker = "python_full_version == '3.12.*'", specifier = "<3" }, - { name = "anyio", marker = "python_full_version >= '3.13'", specifier = ">3" }, - ] - - [[package]] - name = "sniffio" - version = "1.3.1" - source = { registry = "https://pypi.org/simple" } - sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } - wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, + { name = "iniconfig", marker = "sys_platform != 'darwin'" }, + { name = "iniconfig", marker = "sys_platform == 'darwin'", url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl" }, ] "### ); }); + // Re-run with `--locked`. + uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + "###); + Ok(()) } -/// Pass credentials for a named index via environment variables. #[test] -fn lock_keyring_credentials() -> Result<()> { - let keyring_context = TestContext::new("3.12"); - - // Install our keyring plugin - keyring_context - .pip_install() - .arg( - keyring_context - .workspace_root - .join("scripts") - .join("packages") - .join("keyring_test_plugin"), - ) - .assert() - .success(); - +fn lock_multiple_sources_respect_marker() -> Result<()> { let context = TestContext::new("3.12"); let pyproject_toml = context.temp_dir.child("pyproject.toml"); pyproject_toml.write_str( r#" [project] - name = "foo" + name = "project" version = "0.1.0" requires-python = ">=3.12" - dependencies = ["iniconfig"] - - [build-system] - requires = ["setuptools>=42"] - build-backend = "setuptools.build_meta" - - [tool.uv] - keyring-provider = "subprocess" + dependencies = ["iniconfig ; platform_system == 'Windows'"] - [[tool.uv.index]] - name = "proxy" - url = "https://pypi-proxy.fly.dev/basic-auth/simple" - default = true + [tool.uv.sources] + iniconfig = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", marker = "sys_platform == 'darwin'" }, + ] "#, )?; - // Provide credentials via environment variables. - uv_snapshot!(context.filters(), context.lock() - .env(EnvVars::index_username("PROXY"), "public") - .env(EnvVars::KEYRING_TEST_CREDENTIALS, r#"{"pypi-proxy.fly.dev": {"public": "heron"}}"#) - .env(EnvVars::PATH, venv_bin_path(&keyring_context.venv)), @r###" + uv_snapshot!(context.filters(), context.lock(), @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- - Request for public@https://pypi-proxy.fly.dev/basic-auth/simple/iniconfig/ - Request for public@pypi-proxy.fly.dev - Resolved 2 packages in [TIME] + Resolved 3 packages in [TIME] "###); - let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap(); + let lock = context.read("uv.lock"); - // The lockfile shout omit the credentials. insta::with_settings!({ filters => context.filters(), }, { @@ -15031,38 +16645,71 @@ fn lock_keyring_credentials() -> Result<()> { lock, @r###" version = 1 requires-python = ">=3.12" + resolution-markers = [ + "platform_system == 'Windows' and sys_platform == 'darwin'", + "platform_system == 'Windows' and sys_platform != 'darwin'", + "platform_system != 'Windows'", + ] [options] exclude-newer = "2024-03-25T00:00:00Z" [[package]] - name = "foo" - version = "0.1.0" - source = { editable = "." } - dependencies = [ - { name = "iniconfig" }, + name = "iniconfig" + version = "2.0.0" + source = { registry = "https://pypi.org/simple" } + resolution-markers = [ + "platform_system == 'Windows' and sys_platform != 'darwin'", + ] + sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, ] - - [package.metadata] - requires-dist = [{ name = "iniconfig" }] [[package]] name = "iniconfig" version = "2.0.0" - source = { registry = "https://pypi-proxy.fly.dev/basic-auth/simple" } - sdist = { url = "https://pypi-proxy.fly.dev/basic-auth/files/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } + source = { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl" } + resolution-markers = [ + "platform_system == 'Windows' and sys_platform == 'darwin'", + ] wheels = [ - { url = "https://pypi-proxy.fly.dev/basic-auth/files/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" }, + ] + + [[package]] + name = "project" + version = "0.1.0" + source = { virtual = "." } + dependencies = [ + { name = "iniconfig", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "platform_system == 'Windows' and sys_platform != 'darwin'" }, + { name = "iniconfig", version = "2.0.0", source = { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl" }, marker = "platform_system == 'Windows' and sys_platform == 'darwin'" }, + ] + + [package.metadata] + requires-dist = [ + { name = "iniconfig", marker = "platform_system == 'Windows' and sys_platform != 'darwin'" }, + { name = "iniconfig", marker = "platform_system == 'Windows' and sys_platform == 'darwin'", url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl" }, ] "### ); }); + // Re-run with `--locked`. + uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + "###); + Ok(()) } #[test] -fn lock_multiple_sources() -> Result<()> { +fn lock_multiple_sources_extra() -> Result<()> { let context = TestContext::new("3.12"); let pyproject_toml = context.temp_dir.child("pyproject.toml"); @@ -15074,10 +16721,12 @@ fn lock_multiple_sources() -> Result<()> { requires-python = ">=3.12" dependencies = ["iniconfig"] + [project.optional-dependencies] + cpu = [] + [tool.uv.sources] iniconfig = [ - { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", marker = "sys_platform != 'win32'" }, - { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", marker = "sys_platform == 'win32'" }, + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", marker = "extra == 'cpu'" }, ] "#, )?; @@ -15088,7 +16737,7 @@ fn lock_multiple_sources() -> Result<()> { ----- stdout ----- ----- stderr ----- - Resolved 3 packages in [TIME] + Resolved 2 packages in [TIME] "###); let lock = context.read("uv.lock"); @@ -15100,30 +16749,14 @@ fn lock_multiple_sources() -> Result<()> { lock, @r###" version = 1 requires-python = ">=3.12" - resolution-markers = [ - "sys_platform != 'win32'", - "sys_platform == 'win32'", - ] [options] exclude-newer = "2024-03-25T00:00:00Z" - [[package]] - name = "iniconfig" - version = "2.0.0" - source = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz" } - resolution-markers = [ - "sys_platform == 'win32'", - ] - sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3" } - [[package]] name = "iniconfig" version = "2.0.0" source = { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl" } - resolution-markers = [ - "sys_platform != 'win32'", - ] wheels = [ { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" }, ] @@ -15132,15 +16765,16 @@ fn lock_multiple_sources() -> Result<()> { name = "project" version = "0.1.0" source = { virtual = "." } - dependencies = [ - { name = "iniconfig", version = "2.0.0", source = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz" }, marker = "sys_platform == 'win32'" }, - { name = "iniconfig", version = "2.0.0", source = { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl" }, marker = "sys_platform != 'win32'" }, + + [package.optional-dependencies] + cpu = [ + { name = "iniconfig" }, ] [package.metadata] requires-dist = [ - { name = "iniconfig", marker = "sys_platform != 'win32'", url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl" }, - { name = "iniconfig", marker = "sys_platform == 'win32'", url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz" }, + { name = "iniconfig", marker = "extra == 'cpu'", url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl" }, + { name = "iniconfig", marker = "extra != 'cpu'" }, ] "### ); @@ -15153,14 +16787,14 @@ fn lock_multiple_sources() -> Result<()> { ----- stdout ----- ----- stderr ----- - Resolved 3 packages in [TIME] + Resolved 2 packages in [TIME] "###); Ok(()) } #[test] -fn lock_multiple_sources_conflict() -> Result<()> { +fn lock_dry_run() -> Result<()> { let context = TestContext::new("3.12"); let pyproject_toml = context.temp_dir.child("pyproject.toml"); @@ -15170,38 +16804,126 @@ fn lock_multiple_sources_conflict() -> Result<()> { name = "project" version = "0.1.0" requires-python = ">=3.12" - dependencies = ["iniconfig"] + dependencies = [ + "anyio <3 ; python_version == '3.12'", + "anyio >3, <4 ; python_version > '3.12'", + "matplotlib==3.1.0" + ] + "#, + )?; - [tool.uv.sources] - iniconfig = [ - { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", marker = "sys_platform == 'win32' and python_version == '3.12'" }, - { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", marker = "sys_platform == 'win32'" }, + uv_snapshot!(context.filters(), context.lock(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 12 packages in [TIME] + "###); + + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [ + "requests==2.25.1", + "matplotlib==3.5.0" + ] + "#, + )?; + + uv_snapshot!(context.filters(), context.lock().arg("--dry-run"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 19 packages in [TIME] + Remove anyio v2.2.0, v3.7.1 + Add certifi v2024.2.2 + Add chardet v4.0.0 + Add fonttools v4.50.0 + Update idna v3.6 -> v2.10 + Update matplotlib v3.1.0 -> v3.5.0 + Add packaging v24.0 + Add pillow v10.2.0 + Add requests v2.25.1 + Add setuptools v69.2.0 + Add setuptools-scm v8.0.4 + Remove sniffio v1.3.1 + Add typing-extensions v4.10.0 + Add urllib3 v1.26.18 + "###); + + Ok(()) +} + +#[test] +fn lock_dry_run_noop() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [ + "anyio <3 ; python_version == '3.12'", + "anyio >3, <4 ; python_version > '3.12'", ] "#, )?; + uv_snapshot!(context.filters(), context.lock().arg("--dry-run"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 5 packages in [TIME] + Add anyio v2.2.0, v3.7.1 + Add idna v3.6 + Add project v0.1.0 + Add sniffio v1.3.1 + "###); + uv_snapshot!(context.filters(), context.lock(), @r###" - success: false - exit_code: 2 + success: true + exit_code: 0 ----- stdout ----- ----- stderr ----- - error: Failed to parse: `pyproject.toml` - Caused by: TOML parse error at line 9, column 21 - | - 9 | iniconfig = [ - | ^ - Source markers must be disjoint, but the following markers overlap: `python_full_version == '3.12.*' and sys_platform == 'win32'` and `sys_platform == 'win32'`. + Resolved 5 packages in [TIME] + "###); - hint: replace `sys_platform == 'win32'` with `python_full_version != '3.12.*' and sys_platform == 'win32'`. + uv_snapshot!(context.filters(), context.lock().arg("--dry-run"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 5 packages in [TIME] + No lockfile changes detected + "###); + + uv_snapshot!(context.filters(), context.lock().arg("--dry-run").arg("-U"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + ----- stderr ----- + Resolved 5 packages in [TIME] "###); Ok(()) } #[test] -fn lock_multiple_sources_index() -> Result<()> { +fn lock_group_include() -> Result<()> { let context = TestContext::new("3.12"); let pyproject_toml = context.temp_dir.child("pyproject.toml"); @@ -15211,34 +16933,24 @@ fn lock_multiple_sources_index() -> Result<()> { name = "project" version = "0.1.0" requires-python = ">=3.12" - dependencies = ["jinja2>=3"] - - [tool.uv.sources] - jinja2 = [ - { index = "torch-cu118", marker = "sys_platform == 'win32'"}, - { index = "torch-cu124", marker = "sys_platform != 'win32'"}, - ] - - [[tool.uv.index]] - name = "torch-cu118" - url = "https://download.pytorch.org/whl/cu118" + dependencies = ["typing-extensions"] - [[tool.uv.index]] - name = "torch-cu124" - url = "https://download.pytorch.org/whl/cu124" + [dependency-groups] + foo = ["anyio", {include-group = "bar"}] + bar = ["trio"] "#, )?; - uv_snapshot!(context.filters(), context.lock().env_remove(EnvVars::UV_EXCLUDE_NEWER), @r###" + uv_snapshot!(context.filters(), context.lock(), @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- - Resolved 4 packages in [TIME] + Resolved 11 packages in [TIME] "###); - let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap(); + let lock = context.read("uv.lock"); insta::with_settings!({ filters => context.filters(), @@ -15247,50 +16959,64 @@ fn lock_multiple_sources_index() -> Result<()> { lock, @r###" version = 1 requires-python = ">=3.12" - resolution-markers = [ - "sys_platform == 'win32'", - "sys_platform != 'win32'", - ] + + [options] + exclude-newer = "2024-03-25T00:00:00Z" [[package]] - name = "jinja2" - version = "3.1.3" - source = { registry = "https://download.pytorch.org/whl/cu118" } - resolution-markers = [ - "sys_platform == 'win32'", - ] + name = "anyio" + version = "4.3.0" + source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "markupsafe", marker = "sys_platform == 'win32'" }, + { name = "idna" }, + { name = "sniffio" }, ] + sdist = { url = "https://files.pythonhosted.org/packages/db/4d/3970183622f0330d3c23d9b8a5f52e365e50381fd484d08e3285104333d3/anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6", size = 159642 } wheels = [ - { url = "https://download.pytorch.org/whl/Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa" }, + { url = "https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8", size = 85584 }, ] [[package]] - name = "jinja2" - version = "3.1.3" - source = { registry = "https://download.pytorch.org/whl/cu124" } - resolution-markers = [ - "sys_platform != 'win32'", + name = "attrs" + version = "23.2.0" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/e3/fc/f800d51204003fa8ae392c4e8278f256206e7a919b708eef054f5f4b650d/attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30", size = 780820 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/44/827b2a91a5816512fcaf3cc4ebc465ccd5d598c45cefa6703fcf4a79018f/attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1", size = 60752 }, ] + + [[package]] + name = "cffi" + version = "1.16.0" + source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "markupsafe", marker = "sys_platform != 'win32'" }, + { name = "pycparser" }, ] + sdist = { url = "https://files.pythonhosted.org/packages/68/ce/95b0bae7968c65473e1298efb042e10cafc7bafc14d9e4f154008241c91d/cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0", size = 512873 } wheels = [ - { url = "https://download.pytorch.org/whl/Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa" }, + { url = "https://files.pythonhosted.org/packages/c9/6e/751437067affe7ac0944b1ad4856ec11650da77f0dd8f305fae1117ef7bb/cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b", size = 173564 }, + { url = "https://files.pythonhosted.org/packages/e9/63/e285470a4880a4f36edabe4810057bd4b562c6ddcc165eacf9c3c7210b40/cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235", size = 181956 }, ] [[package]] - name = "markupsafe" - version = "2.1.5" - source = { registry = "https://download.pytorch.org/whl/cu118" } + name = "idna" + version = "3.6" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426 } wheels = [ - { url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1" }, - { url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4" }, - { url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee" }, - { url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5" }, - { url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b" }, - { url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb" }, + { url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567 }, + ] + + [[package]] + name = "outcome" + version = "1.3.0.post0" + source = { registry = "https://pypi.org/simple" } + dependencies = [ + { name = "attrs" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/98/df/77698abfac98571e65ffeb0c1fba8ffd692ab8458d617a0eed7d9a8d38f2/outcome-1.3.0.post0.tar.gz", hash = "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8", size = 21060 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/55/8b/5ab7257531a5d830fc8000c476e63c935488d74609b50f9384a643ec0a62/outcome-1.3.0.post0-py2.py3-none-any.whl", hash = "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b", size = 10692 }, ] [[package]] @@ -15298,34 +17024,89 @@ fn lock_multiple_sources_index() -> Result<()> { version = "0.1.0" source = { virtual = "." } dependencies = [ - { name = "jinja2", version = "3.1.3", source = { registry = "https://download.pytorch.org/whl/cu118" }, marker = "sys_platform == 'win32'" }, - { name = "jinja2", version = "3.1.3", source = { registry = "https://download.pytorch.org/whl/cu124" }, marker = "sys_platform != 'win32'" }, + { name = "typing-extensions" }, + ] + + [package.dev-dependencies] + bar = [ + { name = "trio" }, + ] + foo = [ + { name = "anyio" }, + { name = "trio" }, ] [package.metadata] - requires-dist = [ - { name = "jinja2", marker = "sys_platform != 'win32'", specifier = ">=3", index = "https://download.pytorch.org/whl/cu124" }, - { name = "jinja2", marker = "sys_platform == 'win32'", specifier = ">=3", index = "https://download.pytorch.org/whl/cu118" }, + requires-dist = [{ name = "typing-extensions" }] + + [package.metadata.requires-dev] + bar = [{ name = "trio" }] + foo = [ + { name = "anyio" }, + { name = "trio" }, + ] + + [[package]] + name = "pycparser" + version = "2.21" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/5e/0b/95d387f5f4433cb0f53ff7ad859bd2c6051051cebbb564f139a999ab46de/pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206", size = 170877 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/62/d5/5f610ebe421e85889f2e55e33b7f9a6795bd982198517d912eb1c76e1a53/pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9", size = 118697 }, + ] + + [[package]] + name = "sniffio" + version = "1.3.1" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, + ] + + [[package]] + name = "sortedcontainers" + version = "2.4.0" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575 }, + ] + + [[package]] + name = "trio" + version = "0.25.0" + source = { registry = "https://pypi.org/simple" } + dependencies = [ + { name = "attrs" }, + { name = "cffi", marker = "implementation_name != 'pypy' and os_name == 'nt'" }, + { name = "idna" }, + { name = "outcome" }, + { name = "sniffio" }, + { name = "sortedcontainers" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/b4/51/4f5ae37ec58768b9c30e5bc5b89431a7baf3fa9d0dda98983af6ef55eb47/trio-0.25.0.tar.gz", hash = "sha256:9b41f5993ad2c0e5f62d0acca320ec657fdb6b2a2c22b8c7aed6caf154475c4e", size = 551863 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/17/c9/f86f89f14d52f9f2f652ce24cb2f60141a51d087db1563f3fba94ba07346/trio-0.25.0-py3-none-any.whl", hash = "sha256:e6458efe29cc543e557a91e614e2b51710eba2961669329ce9c862d50c6e8e81", size = 467161 }, + ] + + [[package]] + name = "typing-extensions" + version = "4.10.0" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/16/3a/0d26ce356c7465a19c9ea8814b960f8a36c3b0d07c323176620b7b483e44/typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb", size = 77558 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/de/dc04a3ea60b22624b51c703a84bbe0184abcd1d0b9bc8074b5d6b7ab90bb/typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475", size = 33926 }, ] "### ); }); - // Re-run with `--locked`. - uv_snapshot!(context.filters(), context.lock().arg("--locked").env_remove(EnvVars::UV_EXCLUDE_NEWER), @r###" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 4 packages in [TIME] - "###); - Ok(()) } #[test] -fn lock_multiple_sources_index_mixed() -> Result<()> { +fn lock_group_include_cycle() -> Result<()> { let context = TestContext::new("3.12"); let pyproject_toml = context.temp_dir.child("pyproject.toml"); @@ -15335,123 +17116,61 @@ fn lock_multiple_sources_index_mixed() -> Result<()> { name = "project" version = "0.1.0" requires-python = ">=3.12" - dependencies = ["jinja2>=3"] - - [tool.uv.sources] - jinja2 = [ - { index = "torch-cu118", marker = "sys_platform == 'win32'"}, - { url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl", marker = "sys_platform != 'win32'"}, - ] + dependencies = ["typing-extensions"] - [[tool.uv.index]] - name = "torch-cu118" - url = "https://download.pytorch.org/whl/cu118" + [dependency-groups] + foo = ["anyio", {include-group = "bar"}] + bar = [{include-group = "foobar"}] + foobar = [{include-group = "foo"}] "#, )?; - uv_snapshot!(context.filters(), context.lock().env_remove(EnvVars::UV_EXCLUDE_NEWER), @r###" - success: true - exit_code: 0 + uv_snapshot!(context.filters(), context.lock(), @r###" + success: false + exit_code: 2 ----- stdout ----- ----- stderr ----- - Resolved 4 packages in [TIME] + error: Failed to build: `project @ file://[TEMP_DIR]/` + Caused by: Detected a cycle in `dependency-groups`: `bar` -> `foobar` -> `foo` -> `bar` "###); - let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap(); - - insta::with_settings!({ - filters => context.filters(), - }, { - assert_snapshot!( - lock, @r###" - version = 1 - requires-python = ">=3.12" - resolution-markers = [ - "sys_platform == 'win32'", - "sys_platform != 'win32'", - ] - - [[package]] - name = "jinja2" - version = "3.1.3" - source = { registry = "https://download.pytorch.org/whl/cu118" } - resolution-markers = [ - "sys_platform == 'win32'", - ] - dependencies = [ - { name = "markupsafe", marker = "sys_platform == 'win32'" }, - ] - wheels = [ - { url = "https://download.pytorch.org/whl/Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa" }, - ] - - [[package]] - name = "jinja2" - version = "3.1.4" - source = { url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl" } - resolution-markers = [ - "sys_platform != 'win32'", - ] - dependencies = [ - { name = "markupsafe", marker = "sys_platform != 'win32'" }, - ] - wheels = [ - { url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d" }, - ] - - [package.metadata] - requires-dist = [ - { name = "babel", marker = "extra == 'i18n'", specifier = ">=2.7" }, - { name = "markupsafe", specifier = ">=2.0" }, - ] + Ok(()) +} - [[package]] - name = "markupsafe" - version = "2.1.5" - source = { registry = "https://download.pytorch.org/whl/cu118" } - wheels = [ - { url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1" }, - { url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4" }, - { url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee" }, - { url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5" }, - { url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b" }, - { url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb" }, - ] +#[test] +fn lock_group_include_missing() -> Result<()> { + let context = TestContext::new("3.12"); - [[package]] + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] name = "project" version = "0.1.0" - source = { virtual = "." } - dependencies = [ - { name = "jinja2", version = "3.1.3", source = { registry = "https://download.pytorch.org/whl/cu118" }, marker = "sys_platform == 'win32'" }, - { name = "jinja2", version = "3.1.4", source = { url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl" }, marker = "sys_platform != 'win32'" }, - ] + requires-python = ">=3.12" + dependencies = ["typing-extensions"] - [package.metadata] - requires-dist = [ - { name = "jinja2", marker = "sys_platform != 'win32'", url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl" }, - { name = "jinja2", marker = "sys_platform == 'win32'", specifier = ">=3", index = "https://download.pytorch.org/whl/cu118" }, - ] - "### - ); - }); + [dependency-groups] + foo = ["anyio", {include-group = "bar"}] + "#, + )?; - // Re-run with `--locked`. - uv_snapshot!(context.filters(), context.lock().arg("--locked").env_remove(EnvVars::UV_EXCLUDE_NEWER), @r###" - success: true - exit_code: 0 + uv_snapshot!(context.filters(), context.lock(), @r###" + success: false + exit_code: 2 ----- stdout ----- ----- stderr ----- - Resolved 4 packages in [TIME] + error: Failed to build: `project @ file://[TEMP_DIR]/` + Caused by: Failed to find group `bar` included by `foo` "###); Ok(()) } #[test] -fn lock_multiple_sources_index_non_total() -> Result<()> { +fn lock_group_invalid_entry_package() -> Result<()> { let context = TestContext::new("3.12"); let pyproject_toml = context.temp_dir.child("pyproject.toml"); @@ -15461,34 +17180,44 @@ fn lock_multiple_sources_index_non_total() -> Result<()> { name = "project" version = "0.1.0" requires-python = ">=3.12" - dependencies = ["jinja2>=3"] - - [tool.uv.sources] - jinja2 = [ - { index = "torch-cu118", marker = "sys_platform == 'win32'"}, - ] + dependencies = ["typing-extensions"] - [[tool.uv.index]] - name = "torch-cu118" - url = "https://download.pytorch.org/whl/cu118" + [dependency-groups] + foo = ["invalid!"] "#, )?; - uv_snapshot!(context.filters(), context.lock().env_remove(EnvVars::UV_EXCLUDE_NEWER), @r###" + uv_snapshot!(context.filters(), context.lock(), @r###" success: false exit_code: 2 ----- stdout ----- ----- stderr ----- - Resolved 4 packages in [TIME] - error: Found duplicate package `jinja2==3.1.3 @ registry+https://download.pytorch.org/whl/cu118` + error: Failed to build: `project @ file://[TEMP_DIR]/` + Caused by: Failed to parse entry in group `foo`: `invalid!` + Caused by: no such comparison operator "!", must be one of ~= == != <= >= < > === + invalid! + ^ + "###); + + uv_snapshot!(context.filters(), context.sync().arg("--group").arg("foo"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Failed to build: `project @ file://[TEMP_DIR]/` + Caused by: Failed to parse entry in group `foo`: `invalid!` + Caused by: no such comparison operator "!", must be one of ~= == != <= >= < > === + invalid! + ^ "###); Ok(()) } #[test] -fn lock_multiple_sources_index_explicit() -> Result<()> { +fn lock_group_invalid_entry_group_name() -> Result<()> { let context = TestContext::new("3.12"); let pyproject_toml = context.temp_dir.child("pyproject.toml"); @@ -15498,127 +17227,99 @@ fn lock_multiple_sources_index_explicit() -> Result<()> { name = "project" version = "0.1.0" requires-python = ">=3.12" - dependencies = ["jinja2>=3"] - - [tool.uv.sources] - jinja2 = [ - { index = "torch-cu118", marker = "sys_platform == 'win32'"}, - ] + dependencies = ["typing-extensions"] - [[tool.uv.index]] - name = "torch-cu118" - url = "https://test.pypi.org/simple" - explicit = true + [dependency-groups] + foo = [{include-group = "invalid!"}] "#, )?; uv_snapshot!(context.filters(), context.lock(), @r###" - success: true - exit_code: 0 + success: false + exit_code: 2 ----- stdout ----- ----- stderr ----- - Resolved 4 packages in [TIME] + error: Failed to parse: `pyproject.toml` + Caused by: TOML parse error at line 9, column 16 + | + 9 | foo = [{include-group = "invalid!"}] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + Not a valid package or extra name: "invalid!". Names must start and end with a letter or digit and may only contain -, _, ., and alphanumeric characters. + "###); - let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap(); + Ok(()) +} - insta::with_settings!({ - filters => context.filters(), - }, { - assert_snapshot!( - lock, @r###" - version = 1 +#[test] +fn lock_group_invalid_duplicate_group_name() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" requires-python = ">=3.12" - resolution-markers = [ - "sys_platform == 'win32'", - "sys_platform != 'win32'", - ] + dependencies = ["typing-extensions"] - [options] - exclude-newer = "2024-03-25T00:00:00Z" + [dependency-groups] + foo-bar = [] + foo_bar = [] + "#, + )?; - [[package]] - name = "jinja2" - version = "3.1.3" - source = { registry = "https://pypi.org/simple" } - resolution-markers = [ - "sys_platform != 'win32'", - ] - dependencies = [ - { name = "markupsafe", marker = "sys_platform != 'win32'" }, - ] - sdist = { url = "https://files.pythonhosted.org/packages/b2/5e/3a21abf3cd467d7876045335e681d276ac32492febe6d98ad89562d1a7e1/Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90", size = 268261 } - wheels = [ - { url = "https://files.pythonhosted.org/packages/30/6d/6de6be2d02603ab56e72997708809e8a5b0fbfee080735109b40a3564843/Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa", size = 133236 }, - ] + uv_snapshot!(context.filters(), context.lock(), @r###" + success: false + exit_code: 2 + ----- stdout ----- - [[package]] - name = "jinja2" - version = "3.1.3" - source = { registry = "https://test.pypi.org/simple" } - resolution-markers = [ - "sys_platform == 'win32'", - ] - dependencies = [ - { name = "markupsafe", marker = "sys_platform == 'win32'" }, - ] - sdist = { url = "https://test-files.pythonhosted.org/packages/3e/f0/69ae37cced6b277dc0419dbb1c6e4fb259e5e319a1a971061a2776316bec/Jinja2-3.1.3.tar.gz", hash = "sha256:27fb536952e578492fa66d8681d8967d8bdf1eb36368b1f842b53251c9f0bfe1", size = 268254 } - wheels = [ - { url = "https://test-files.pythonhosted.org/packages/47/dc/9d1c0f1ddbedb1e67f7d00e91819b5a9157056ad83bfa64c12ecef8a4f4e/Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:ddd11470e8a1dc4c30e3146400f0130fed7d85886c5f8082f309355b4b0c1128", size = 133236 }, - ] + ----- stderr ----- + error: Failed to parse: `pyproject.toml` + Caused by: TOML parse error at line 8, column 9 + | + 8 | [dependency-groups] + | ^^^^^^^^^^^^^^^^^^^ + duplicate dependency group: `foo-bar` + "###); - [[package]] - name = "markupsafe" - version = "2.1.5" - source = { registry = "https://pypi.org/simple" } - sdist = { url = "https://files.pythonhosted.org/packages/87/5b/aae44c6655f3801e81aa3eef09dbbf012431987ba564d7231722f68df02d/MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", size = 19384 } - wheels = [ - { url = "https://files.pythonhosted.org/packages/53/bd/583bf3e4c8d6a321938c13f49d44024dbe5ed63e0a7ba127e454a66da974/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", size = 18215 }, - { url = "https://files.pythonhosted.org/packages/48/d6/e7cd795fc710292c3af3a06d80868ce4b02bfbbf370b7cee11d282815a2a/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", size = 14069 }, - { url = "https://files.pythonhosted.org/packages/51/b5/5d8ec796e2a08fc814a2c7d2584b55f889a55cf17dd1a90f2beb70744e5c/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", size = 29452 }, - { url = "https://files.pythonhosted.org/packages/0a/0d/2454f072fae3b5a137c119abf15465d1771319dfe9e4acbb31722a0fff91/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", size = 28462 }, - { url = "https://files.pythonhosted.org/packages/2d/75/fd6cb2e68780f72d47e6671840ca517bda5ef663d30ada7616b0462ad1e3/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", size = 27869 }, - { url = "https://files.pythonhosted.org/packages/b0/81/147c477391c2750e8fc7705829f7351cf1cd3be64406edcf900dc633feb2/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", size = 33906 }, - { url = "https://files.pythonhosted.org/packages/8b/ff/9a52b71839d7a256b563e85d11050e307121000dcebc97df120176b3ad93/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", size = 32296 }, - { url = "https://files.pythonhosted.org/packages/88/07/2dc76aa51b481eb96a4c3198894f38b480490e834479611a4053fbf08623/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", size = 33038 }, - { url = "https://files.pythonhosted.org/packages/96/0c/620c1fb3661858c0e37eb3cbffd8c6f732a67cd97296f725789679801b31/MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", size = 16572 }, - { url = "https://files.pythonhosted.org/packages/3f/14/c3554d512d5f9100a95e737502f4a2323a1959f6d0d01e0d0997b35f7b10/MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", size = 17127 }, - ] + Ok(()) +} - [[package]] +#[test] +fn lock_group_invalid_entry_table() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] name = "project" version = "0.1.0" - source = { virtual = "." } - dependencies = [ - { name = "jinja2", version = "3.1.3", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'win32'" }, - { name = "jinja2", version = "3.1.3", source = { registry = "https://test.pypi.org/simple" }, marker = "sys_platform == 'win32'" }, - ] + requires-python = ">=3.12" + dependencies = ["typing-extensions"] - [package.metadata] - requires-dist = [ - { name = "jinja2", marker = "sys_platform != 'win32'", specifier = ">=3" }, - { name = "jinja2", marker = "sys_platform == 'win32'", specifier = ">=3", index = "https://test.pypi.org/simple" }, - ] - "### - ); - }); + [dependency-groups] + foo = [{bar = "unknown"}] + "#, + )?; - // Re-run with `--locked`. - uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###" + uv_snapshot!(context.filters(), context.lock(), @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- - Resolved 4 packages in [TIME] + Resolved 2 packages in [TIME] "###); Ok(()) } #[test] -fn lock_multiple_sources_non_total() -> Result<()> { +fn lock_group_invalid_entry_type() -> Result<()> { let context = TestContext::new("3.12"); let pyproject_toml = context.temp_dir.child("pyproject.toml"); @@ -15628,97 +17329,69 @@ fn lock_multiple_sources_non_total() -> Result<()> { name = "project" version = "0.1.0" requires-python = ">=3.12" - dependencies = ["iniconfig"] + dependencies = ["typing-extensions"] - [tool.uv.sources] - iniconfig = [ - { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", marker = "sys_platform == 'darwin'" }, - ] + [dependency-groups] + foo = [{include-group = true}] "#, )?; uv_snapshot!(context.filters(), context.lock(), @r###" - success: true - exit_code: 0 + success: false + exit_code: 2 ----- stdout ----- ----- stderr ----- - Resolved 3 packages in [TIME] - "###); - - let lock = context.read("uv.lock"); - - insta::with_settings!({ - filters => context.filters(), - }, { - assert_snapshot!( - lock, @r###" - version = 1 - requires-python = ">=3.12" - resolution-markers = [ - "sys_platform == 'darwin'", - "sys_platform != 'darwin'", - ] + error: Failed to parse: `pyproject.toml` + Caused by: TOML parse error at line 9, column 33 + | + 9 | foo = [{include-group = true}] + | ^^^^ + invalid type: boolean `true`, expected a string - [options] - exclude-newer = "2024-03-25T00:00:00Z" + "###); - [[package]] - name = "iniconfig" - version = "2.0.0" - source = { registry = "https://pypi.org/simple" } - resolution-markers = [ - "sys_platform != 'darwin'", - ] - sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } - wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, - ] + Ok(()) +} - [[package]] - name = "iniconfig" - version = "2.0.0" - source = { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl" } - resolution-markers = [ - "sys_platform == 'darwin'", - ] - wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" }, - ] +#[test] +fn lock_group_empty_entry_table() -> Result<()> { + let context = TestContext::new("3.12"); - [[package]] + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] name = "project" version = "0.1.0" - source = { virtual = "." } - dependencies = [ - { name = "iniconfig", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'darwin'" }, - { name = "iniconfig", version = "2.0.0", source = { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl" }, marker = "sys_platform == 'darwin'" }, - ] + requires-python = ">=3.12" + dependencies = ["typing-extensions"] - [package.metadata] - requires-dist = [ - { name = "iniconfig", marker = "sys_platform != 'darwin'" }, - { name = "iniconfig", marker = "sys_platform == 'darwin'", url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl" }, - ] - "### - ); - }); + [dependency-groups] + foo = [{}] + "#, + )?; - // Re-run with `--locked`. - uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###" - success: true - exit_code: 0 + uv_snapshot!(context.filters(), context.lock(), @r###" + success: false + exit_code: 2 ----- stdout ----- ----- stderr ----- - Resolved 3 packages in [TIME] + error: Failed to parse: `pyproject.toml` + Caused by: TOML parse error at line 9, column 16 + | + 9 | foo = [{}] + | ^^ + missing field `include-group` + "###); Ok(()) } #[test] -fn lock_multiple_sources_respect_marker() -> Result<()> { +fn lock_group_workspace() -> Result<()> { let context = TestContext::new("3.12"); let pyproject_toml = context.temp_dir.child("pyproject.toml"); @@ -15728,12 +17401,41 @@ fn lock_multiple_sources_respect_marker() -> Result<()> { name = "project" version = "0.1.0" requires-python = ">=3.12" - dependencies = ["iniconfig ; platform_system == 'Windows'"] + dependencies = ["child"] + + [dependency-groups] + types = ["sniffio>1"] + async = ["anyio>3"] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + + [tool.uv.workspace] + members = ["child"] [tool.uv.sources] - iniconfig = [ - { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", marker = "sys_platform == 'darwin'" }, - ] + child = { workspace = true } + "#, + )?; + + // Add a workspace member. + let child = context.temp_dir.child("child"); + child.child("pyproject.toml").write_str( + r#" + [project] + name = "child" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["iniconfig>1"] + + [dependency-groups] + types = ["typing-extensions>4"] + testing = ["pytest>8"] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" "#, )?; @@ -15743,7 +17445,7 @@ fn lock_multiple_sources_respect_marker() -> Result<()> { ----- stdout ----- ----- stderr ----- - Resolved 3 packages in [TIME] + Resolved 11 packages in [TIME] "###); let lock = context.read("uv.lock"); @@ -15755,89 +17457,174 @@ fn lock_multiple_sources_respect_marker() -> Result<()> { lock, @r###" version = 1 requires-python = ">=3.12" - resolution-markers = [ - "platform_system == 'Windows' and sys_platform == 'darwin'", - "platform_system == 'Windows' and sys_platform != 'darwin'", - "platform_system != 'Windows'", - ] [options] exclude-newer = "2024-03-25T00:00:00Z" + [manifest] + members = [ + "child", + "project", + ] + + [[package]] + name = "anyio" + version = "4.3.0" + source = { registry = "https://pypi.org/simple" } + dependencies = [ + { name = "idna" }, + { name = "sniffio" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/db/4d/3970183622f0330d3c23d9b8a5f52e365e50381fd484d08e3285104333d3/anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6", size = 159642 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8", size = 85584 }, + ] + + [[package]] + name = "child" + version = "0.1.0" + source = { editable = "child" } + dependencies = [ + { name = "iniconfig" }, + ] + + [package.dev-dependencies] + testing = [ + { name = "pytest" }, + ] + types = [ + { name = "typing-extensions" }, + ] + + [package.metadata] + requires-dist = [{ name = "iniconfig", specifier = ">1" }] + + [package.metadata.requires-dev] + testing = [{ name = "pytest", specifier = ">8" }] + types = [{ name = "typing-extensions", specifier = ">4" }] + + [[package]] + name = "colorama" + version = "0.4.6" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, + ] + + [[package]] + name = "idna" + version = "3.6" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567 }, + ] + [[package]] name = "iniconfig" version = "2.0.0" source = { registry = "https://pypi.org/simple" } - resolution-markers = [ - "platform_system == 'Windows' and sys_platform != 'darwin'", - ] sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } wheels = [ { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, ] [[package]] - name = "iniconfig" - version = "2.0.0" - source = { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl" } - resolution-markers = [ - "platform_system == 'Windows' and sys_platform == 'darwin'", + name = "packaging" + version = "24.0" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/ee/b5/b43a27ac7472e1818c4bafd44430e69605baefe1f34440593e0332ec8b4d/packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9", size = 147882 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/49/df/1fceb2f8900f8639e278b056416d49134fb8d84c5942ffaa01ad34782422/packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", size = 53488 }, ] + + [[package]] + name = "pluggy" + version = "1.4.0" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/54/c6/43f9d44d92aed815e781ca25ba8c174257e27253a94630d21be8725a2b59/pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be", size = 65812 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" }, + { url = "https://files.pythonhosted.org/packages/a5/5b/0cc789b59e8cc1bf288b38111d002d8c5917123194d45b29dcdac64723cc/pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981", size = 20120 }, ] [[package]] name = "project" version = "0.1.0" - source = { virtual = "." } + source = { editable = "." } dependencies = [ - { name = "iniconfig", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "platform_system == 'Windows' and sys_platform != 'darwin'" }, - { name = "iniconfig", version = "2.0.0", source = { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl" }, marker = "platform_system == 'Windows' and sys_platform == 'darwin'" }, + { name = "child" }, + ] + + [package.dev-dependencies] + async = [ + { name = "anyio" }, + ] + types = [ + { name = "sniffio" }, ] [package.metadata] - requires-dist = [ - { name = "iniconfig", marker = "platform_system == 'Windows' and sys_platform != 'darwin'" }, - { name = "iniconfig", marker = "platform_system == 'Windows' and sys_platform == 'darwin'", url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl" }, + requires-dist = [{ name = "child", editable = "child" }] + + [package.metadata.requires-dev] + async = [{ name = "anyio", specifier = ">3" }] + types = [{ name = "sniffio", specifier = ">1" }] + + [[package]] + name = "pytest" + version = "8.1.1" + source = { registry = "https://pypi.org/simple" } + dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/30/b7/7d44bbc04c531dcc753056920e0988032e5871ac674b5a84cb979de6e7af/pytest-8.1.1.tar.gz", hash = "sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044", size = 1409703 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/7e/c79cecfdb6aa85c6c2e3cf63afc56d0f165f24f5c66c03c695c4d9b84756/pytest-8.1.1-py3-none-any.whl", hash = "sha256:2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7", size = 337359 }, + ] + + [[package]] + name = "sniffio" + version = "1.3.1" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, + ] + + [[package]] + name = "typing-extensions" + version = "4.10.0" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/16/3a/0d26ce356c7465a19c9ea8814b960f8a36c3b0d07c323176620b7b483e44/typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb", size = 77558 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/de/dc04a3ea60b22624b51c703a84bbe0184abcd1d0b9bc8074b5d6b7ab90bb/typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475", size = 33926 }, ] "### ); }); - // Re-run with `--locked`. - uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 3 packages in [TIME] - "###); - Ok(()) } #[test] -fn lock_multiple_sources_extra() -> Result<()> { +fn lock_transitive_git() -> Result<()> { let context = TestContext::new("3.12"); let pyproject_toml = context.temp_dir.child("pyproject.toml"); pyproject_toml.write_str( r#" [project] - name = "project" + name = "a" version = "0.1.0" requires-python = ">=3.12" - dependencies = ["iniconfig"] - - [project.optional-dependencies] - cpu = [] + dependencies = ["c"] [tool.uv.sources] - iniconfig = [ - { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", marker = "extra == 'cpu'" }, - ] + c = { git = "https://github.com/astral-sh/workspace-virtual-root-test", subdirectory = "packages/c", rev = "fac39c8d4c5d0ef32744e2bb309bbe34a759fd46" } "#, )?; @@ -15847,7 +17634,7 @@ fn lock_multiple_sources_extra() -> Result<()> { ----- stdout ----- ----- stderr ----- - Resolved 2 packages in [TIME] + Resolved 6 packages in [TIME] "###); let lock = context.read("uv.lock"); @@ -15864,27 +17651,61 @@ fn lock_multiple_sources_extra() -> Result<()> { exclude-newer = "2024-03-25T00:00:00Z" [[package]] - name = "iniconfig" - version = "2.0.0" - source = { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl" } + name = "a" + version = "0.1.0" + source = { virtual = "." } + dependencies = [ + { name = "c" }, + ] + + [package.metadata] + requires-dist = [{ name = "c", git = "https://github.com/astral-sh/workspace-virtual-root-test?subdirectory=packages%2Fc&rev=fac39c8d4c5d0ef32744e2bb309bbe34a759fd46" }] + + [[package]] + name = "anyio" + version = "4.3.0" + source = { registry = "https://pypi.org/simple" } + dependencies = [ + { name = "idna" }, + { name = "sniffio" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/db/4d/3970183622f0330d3c23d9b8a5f52e365e50381fd484d08e3285104333d3/anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6", size = 159642 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" }, + { url = "https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8", size = 85584 }, ] [[package]] - name = "project" - version = "0.1.0" - source = { virtual = "." } + name = "c" + version = "1.0.0" + source = { git = "https://github.com/astral-sh/workspace-virtual-root-test?subdirectory=packages%2Fc&rev=fac39c8d4c5d0ef32744e2bb309bbe34a759fd46#fac39c8d4c5d0ef32744e2bb309bbe34a759fd46" } + dependencies = [ + { name = "d" }, + ] - [package.optional-dependencies] - cpu = [ - { name = "iniconfig" }, + [[package]] + name = "d" + version = "1.0.0" + source = { git = "https://github.com/astral-sh/workspace-virtual-root-test?subdirectory=packages%2Fd&rev=fac39c8d4c5d0ef32744e2bb309bbe34a759fd46#fac39c8d4c5d0ef32744e2bb309bbe34a759fd46" } + dependencies = [ + { name = "anyio" }, ] - [package.metadata] - requires-dist = [ - { name = "iniconfig", marker = "extra == 'cpu'", url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl" }, - { name = "iniconfig", marker = "extra != 'cpu'" }, + [[package]] + name = "idna" + version = "3.6" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567 }, + ] + + [[package]] + name = "sniffio" + version = "1.3.1" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, ] "### ); @@ -15897,7 +17718,34 @@ fn lock_multiple_sources_extra() -> Result<()> { ----- stdout ----- ----- stderr ----- - Resolved 2 packages in [TIME] + Resolved 6 packages in [TIME] + "###); + + // Re-run with `--offline`. We shouldn't need a network connection to validate an + // already-correct lockfile with immutable metadata. + uv_snapshot!(context.filters(), context.lock().arg("--locked").arg("--offline").arg("--no-cache"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 6 packages in [TIME] + "###); + + // Install from the lockfile. + uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Prepared 5 packages in [TIME] + Installed 5 packages in [TIME] + + anyio==4.3.0 + + c==1.0.0 (from git+https://github.com/astral-sh/workspace-virtual-root-test@fac39c8d4c5d0ef32744e2bb309bbe34a759fd46#subdirectory=packages/c) + + d==1.0.0 (from git+https://github.com/astral-sh/workspace-virtual-root-test@fac39c8d4c5d0ef32744e2bb309bbe34a759fd46#subdirectory=packages/d) + + idna==3.6 + + sniffio==1.3.1 "###); Ok(()) diff --git a/crates/uv/tests/it/main.rs b/crates/uv/tests/it/main.rs index 0c95a03751ab..74393449a10b 100644 --- a/crates/uv/tests/it/main.rs +++ b/crates/uv/tests/it/main.rs @@ -66,10 +66,13 @@ mod publish; mod python_dir; -#[cfg(all(feature = "python", feature = "pypi"))] +#[cfg(feature = "python")] mod python_find; -#[cfg(all(feature = "python", feature = "pypi"))] +#[cfg(feature = "python-managed")] +mod python_install; + +#[cfg(feature = "python")] mod python_pin; #[cfg(all(feature = "python", feature = "pypi"))] diff --git a/crates/uv/tests/it/pip_compile.rs b/crates/uv/tests/it/pip_compile.rs index 964542dfe2da..23b3857957c9 100644 --- a/crates/uv/tests/it/pip_compile.rs +++ b/crates/uv/tests/it/pip_compile.rs @@ -7989,10 +7989,6 @@ fn universal_requires_python() -> Result<()> { } /// Perform a universal resolution that requires narrowing the supported Python range in a non-fork. -/// -/// This should resolve successfully, but currently fails [1]. -/// -/// [1]: https://github.com/astral-sh/uv/issues/4668 #[test] fn universal_requires_python_incomplete() -> Result<()> { let context = TestContext::new("3.12"); @@ -8006,15 +8002,17 @@ fn universal_requires_python_incomplete() -> Result<()> { .arg("-p") .arg("3.7") .arg("--universal"), @r###" - success: false - exit_code: 1 + success: true + exit_code: 0 ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile --cache-dir [CACHE_DIR] requirements.in -p 3.7 --universal + uv==0.1.24 ; python_full_version >= '3.8' + # via -r requirements.in ----- stderr ----- warning: The requested Python version 3.7 is not available; 3.12.[X] will be used to build dependencies instead. - × No solution found when resolving dependencies: - ╰─▶ Because only uv{python_full_version >= '3.8'}<=0.1.24 is available and the requested Python version (>=3.7) does not satisfy Python>=3.8, we can conclude that all versions of uv{python_full_version >= '3.8'} are incompatible. - And because you require uv{python_full_version >= '3.8'}, we can conclude that your requirements are unsatisfiable. + Resolved 1 package in [TIME] "### ); @@ -9365,6 +9363,7 @@ fn not_found_direct_url() -> Result<()> { ----- stderr ----- error: Failed to download: `iniconfig @ https://files.pythonhosted.org/packages/ef/a6/fake/iniconfig-2.0.0-py3-none-any.whl` + Caused by: Failed to fetch: `https://files.pythonhosted.org/packages/ef/a6/fake/iniconfig-2.0.0-py3-none-any.whl` Caused by: HTTP status client error (404 Not Found) for url (https://files.pythonhosted.org/packages/ef/a6/fake/iniconfig-2.0.0-py3-none-any.whl) "### ); @@ -11647,7 +11646,7 @@ fn invalid_tool_uv_sources() -> Result<()> { ----- stdout ----- ----- stderr ----- - error: Failed to parse entry for: `urllib3` + error: Failed to parse entry: `urllib3` Caused by: Expected direct URL (`https://files.pythonhosted.org/packages/a2/73/a68704750a7679d0b6d3ad7aa8d4da8e14e151ae82e6fee774e6e0d05ec8/urllib3-2.2.1-py3-none-any.tar.baz`) to end in a supported file extension: `.whl`, `.tar.gz`, `.zip`, `.tar.bz2`, `.tar.lz`, `.tar.lzma`, `.tar.xz`, `.tar.zst`, `.tar`, `.tbz`, `.tgz`, `.tlz`, or `.txz` "### ); @@ -12409,7 +12408,7 @@ fn prune_unreachable() -> Result<()> { ----- stdout ----- # This file was autogenerated by uv via the following command: # uv pip compile --cache-dir [CACHE_DIR] requirements.in --universal -p 3.7 - argcomplete==3.1.2 ; python_full_version >= '3.8' + argcomplete==3.2.3 ; python_full_version >= '3.8' # via -r requirements.in ----- stderr ----- diff --git a/crates/uv/tests/it/pip_install.rs b/crates/uv/tests/it/pip_install.rs index dcd47878e2b5..1862e80e7759 100644 --- a/crates/uv/tests/it/pip_install.rs +++ b/crates/uv/tests/it/pip_install.rs @@ -191,7 +191,7 @@ fn invalid_pyproject_toml_option_unknown_field() -> Result<()> { | 2 | unknown = "field" | ^^^^^^^ - unknown field `unknown`, expected one of `native-tls`, `offline`, `no-cache`, `cache-dir`, `preview`, `python-preference`, `python-downloads`, `concurrent-downloads`, `concurrent-builds`, `concurrent-installs`, `index`, `index-url`, `extra-index-url`, `no-index`, `find-links`, `index-strategy`, `keyring-provider`, `allow-insecure-host`, `resolution`, `prerelease`, `dependency-metadata`, `config-settings`, `no-build-isolation`, `no-build-isolation-package`, `exclude-newer`, `link-mode`, `compile-bytecode`, `no-sources`, `upgrade`, `upgrade-package`, `reinstall`, `reinstall-package`, `no-build`, `no-build-package`, `no-binary`, `no-binary-package`, `publish-url`, `trusted-publishing`, `pip`, `cache-keys`, `override-dependencies`, `constraint-dependencies`, `environments`, `workspace`, `sources`, `dev-dependencies`, `managed`, `package` + unknown field `unknown`, expected one of `native-tls`, `offline`, `no-cache`, `cache-dir`, `preview`, `python-preference`, `python-downloads`, `concurrent-downloads`, `concurrent-builds`, `concurrent-installs`, `index`, `index-url`, `extra-index-url`, `no-index`, `find-links`, `index-strategy`, `keyring-provider`, `allow-insecure-host`, `resolution`, `prerelease`, `dependency-metadata`, `config-settings`, `no-build-isolation`, `no-build-isolation-package`, `exclude-newer`, `link-mode`, `compile-bytecode`, `no-sources`, `upgrade`, `upgrade-package`, `reinstall`, `reinstall-package`, `no-build`, `no-build-package`, `no-binary`, `no-binary-package`, `publish-url`, `trusted-publishing`, `pip`, `cache-keys`, `override-dependencies`, `constraint-dependencies`, `environments`, `workspace`, `sources`, `managed`, `package`, `default-groups`, `dev-dependencies` Resolved in [TIME] Audited in [TIME] diff --git a/crates/uv/tests/it/pip_sync.rs b/crates/uv/tests/it/pip_sync.rs index 3b0a78510031..920f2125121b 100644 --- a/crates/uv/tests/it/pip_sync.rs +++ b/crates/uv/tests/it/pip_sync.rs @@ -5607,3 +5607,34 @@ fn sync_seed() -> Result<()> { Ok(()) } + +/// Sanitize zip files during extraction. +#[test] +fn sanitize() -> Result<()> { + let context = TestContext::new("3.12"); + + // Install a zip file that includes a path that extends outside the parent. + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt.write_str("payload-package @ https://github.com/astral-sh/sanitize-wheel-test/raw/bc59283d5b4b136a191792e32baa51b477fdf65e/payload_package-0.1.0-py3-none-any.whl")?; + + uv_snapshot!(context.pip_sync() + .arg("requirements.txt"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + payload-package==0.1.0 (from https://github.com/astral-sh/sanitize-wheel-test/raw/bc59283d5b4b136a191792e32baa51b477fdf65e/payload_package-0.1.0-py3-none-any.whl) + "### + ); + + // There should be no `payload` file in the root. + if let Some(parent) = context.temp_dir.parent() { + assert!(!parent.join("payload").exists()); + } + + Ok(()) +} diff --git a/crates/uv/tests/it/pip_tree.rs b/crates/uv/tests/it/pip_tree.rs index 038fa22e86be..15f84b47b309 100644 --- a/crates/uv/tests/it/pip_tree.rs +++ b/crates/uv/tests/it/pip_tree.rs @@ -1,3 +1,5 @@ +#![cfg(not(windows))] + use std::process::Command; use assert_fs::fixture::FileWriteStr; @@ -95,62 +97,16 @@ fn single_package() { ); context.assert_command("import requests").success(); + uv_snapshot!(context.filters(), context.pip_tree(), @r###" success: true exit_code: 0 ----- stdout ----- requests v2.31.0 + ├── certifi v2024.2.2 ├── charset-normalizer v3.3.2 ├── idna v3.6 - ├── urllib3 v2.2.1 - └── certifi v2024.2.2 - - ----- stderr ----- - "### - ); -} -// `pandas` requires `numpy` with markers on Python version. -#[test] -#[cfg(not(windows))] -fn python_version_marker() { - let context = TestContext::new("3.12"); - - let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.write_str("pandas==2.2.1").unwrap(); - - uv_snapshot!(context - .pip_install() - .arg("-r") - .arg("requirements.txt") - .arg("--strict"), @r###" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 6 packages in [TIME] - Prepared 6 packages in [TIME] - Installed 6 packages in [TIME] - + numpy==1.26.4 - + pandas==2.2.1 - + python-dateutil==2.9.0.post0 - + pytz==2024.1 - + six==1.16.0 - + tzdata==2024.1 - - "### - ); - - uv_snapshot!(context.filters(), context.pip_tree(), @r###" - success: true - exit_code: 0 - ----- stdout ----- - pandas v2.2.1 - ├── numpy v1.26.4 - ├── python-dateutil v2.9.0.post0 - │ └── six v1.16.0 - ├── pytz v2024.1 - └── tzdata v2024.1 + └── urllib3 v2.2.1 ----- stderr ----- "### @@ -162,9 +118,7 @@ fn nested_dependencies() { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt - .write_str("scikit-learn==1.4.1.post1") - .unwrap(); + requirements_txt.write_str("flask").unwrap(); uv_snapshot!(context .pip_install() @@ -176,14 +130,16 @@ fn nested_dependencies() { ----- stdout ----- ----- stderr ----- - Resolved 5 packages in [TIME] - Prepared 5 packages in [TIME] - Installed 5 packages in [TIME] - + joblib==1.3.2 - + numpy==1.26.4 - + scikit-learn==1.4.1.post1 - + scipy==1.12.0 - + threadpoolctl==3.4.0 + Resolved 7 packages in [TIME] + Prepared 7 packages in [TIME] + Installed 7 packages in [TIME] + + blinker==1.7.0 + + click==8.1.7 + + flask==3.0.2 + + itsdangerous==2.1.2 + + jinja2==3.1.3 + + markupsafe==2.1.5 + + werkzeug==3.0.1 "### ); @@ -191,27 +147,27 @@ fn nested_dependencies() { success: true exit_code: 0 ----- stdout ----- - scikit-learn v1.4.1.post1 - ├── numpy v1.26.4 - ├── scipy v1.12.0 - │ └── numpy v1.26.4 - ├── joblib v1.3.2 - └── threadpoolctl v3.4.0 + flask v3.0.2 + ├── blinker v1.7.0 + ├── click v8.1.7 + ├── itsdangerous v2.1.2 + ├── jinja2 v3.1.3 + │ └── markupsafe v2.1.5 + └── werkzeug v3.0.1 + └── markupsafe v2.1.5 ----- stderr ----- "### ); } -// Identical test as `invert` since `--reverse` is simply an alias for `--invert`. +/// Identical test as `invert` since `--reverse` is simply an alias for `--invert`. #[test] fn reverse() { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt - .write_str("scikit-learn==1.4.1.post1") - .unwrap(); + requirements_txt.write_str("flask").unwrap(); uv_snapshot!(context .pip_install() @@ -223,14 +179,16 @@ fn reverse() { ----- stdout ----- ----- stderr ----- - Resolved 5 packages in [TIME] - Prepared 5 packages in [TIME] - Installed 5 packages in [TIME] - + joblib==1.3.2 - + numpy==1.26.4 - + scikit-learn==1.4.1.post1 - + scipy==1.12.0 - + threadpoolctl==3.4.0 + Resolved 7 packages in [TIME] + Prepared 7 packages in [TIME] + Installed 7 packages in [TIME] + + blinker==1.7.0 + + click==8.1.7 + + flask==3.0.2 + + itsdangerous==2.1.2 + + jinja2==3.1.3 + + markupsafe==2.1.5 + + werkzeug==3.0.1 "### ); @@ -238,14 +196,17 @@ fn reverse() { success: true exit_code: 0 ----- stdout ----- - joblib v1.3.2 - └── scikit-learn v1.4.1.post1 - numpy v1.26.4 - ├── scikit-learn v1.4.1.post1 - └── scipy v1.12.0 - └── scikit-learn v1.4.1.post1 - threadpoolctl v3.4.0 - └── scikit-learn v1.4.1.post1 + blinker v1.7.0 + └── flask v3.0.2 + click v8.1.7 + └── flask v3.0.2 + itsdangerous v2.1.2 + └── flask v3.0.2 + markupsafe v2.1.5 + ├── jinja2 v3.1.3 + │ └── flask v3.0.2 + └── werkzeug v3.0.1 + └── flask v3.0.2 ----- stderr ----- "### @@ -257,9 +218,7 @@ fn invert() { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt - .write_str("scikit-learn==1.4.1.post1") - .unwrap(); + requirements_txt.write_str("flask").unwrap(); uv_snapshot!(context .pip_install() @@ -271,14 +230,16 @@ fn invert() { ----- stdout ----- ----- stderr ----- - Resolved 5 packages in [TIME] - Prepared 5 packages in [TIME] - Installed 5 packages in [TIME] - + joblib==1.3.2 - + numpy==1.26.4 - + scikit-learn==1.4.1.post1 - + scipy==1.12.0 - + threadpoolctl==3.4.0 + Resolved 7 packages in [TIME] + Prepared 7 packages in [TIME] + Installed 7 packages in [TIME] + + blinker==1.7.0 + + click==8.1.7 + + flask==3.0.2 + + itsdangerous==2.1.2 + + jinja2==3.1.3 + + markupsafe==2.1.5 + + werkzeug==3.0.1 "### ); @@ -286,14 +247,17 @@ fn invert() { success: true exit_code: 0 ----- stdout ----- - joblib v1.3.2 - └── scikit-learn v1.4.1.post1 - numpy v1.26.4 - ├── scikit-learn v1.4.1.post1 - └── scipy v1.12.0 - └── scikit-learn v1.4.1.post1 - threadpoolctl v3.4.0 - └── scikit-learn v1.4.1.post1 + blinker v1.7.0 + └── flask v3.0.2 + click v8.1.7 + └── flask v3.0.2 + itsdangerous v2.1.2 + └── flask v3.0.2 + markupsafe v2.1.5 + ├── jinja2 v3.1.3 + │ └── flask v3.0.2 + └── werkzeug v3.0.1 + └── flask v3.0.2 ----- stderr ----- "### @@ -305,9 +269,7 @@ fn depth() { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt - .write_str("scikit-learn==1.4.1.post1") - .unwrap(); + requirements_txt.write_str("flask").unwrap(); uv_snapshot!(context.pip_install() .arg("-r") @@ -318,14 +280,16 @@ fn depth() { ----- stdout ----- ----- stderr ----- - Resolved 5 packages in [TIME] - Prepared 5 packages in [TIME] - Installed 5 packages in [TIME] - + joblib==1.3.2 - + numpy==1.26.4 - + scikit-learn==1.4.1.post1 - + scipy==1.12.0 - + threadpoolctl==3.4.0 + Resolved 7 packages in [TIME] + Prepared 7 packages in [TIME] + Installed 7 packages in [TIME] + + blinker==1.7.0 + + click==8.1.7 + + flask==3.0.2 + + itsdangerous==2.1.2 + + jinja2==3.1.3 + + markupsafe==2.1.5 + + werkzeug==3.0.1 "### ); @@ -342,10 +306,9 @@ fn depth() { success: true exit_code: 0 ----- stdout ----- - scikit-learn v1.4.1.post1 + flask v3.0.2 ----- stderr ----- - "### ); @@ -362,14 +325,14 @@ fn depth() { success: true exit_code: 0 ----- stdout ----- - scikit-learn v1.4.1.post1 - ├── numpy v1.26.4 - ├── scipy v1.12.0 - ├── joblib v1.3.2 - └── threadpoolctl v3.4.0 + flask v3.0.2 + ├── blinker v1.7.0 + ├── click v8.1.7 + ├── itsdangerous v2.1.2 + ├── jinja2 v3.1.3 + └── werkzeug v3.0.1 ----- stderr ----- - "### ); @@ -386,12 +349,14 @@ fn depth() { success: true exit_code: 0 ----- stdout ----- - scikit-learn v1.4.1.post1 - ├── numpy v1.26.4 - ├── scipy v1.12.0 - │ └── numpy v1.26.4 - ├── joblib v1.3.2 - └── threadpoolctl v3.4.0 + flask v3.0.2 + ├── blinker v1.7.0 + ├── click v8.1.7 + ├── itsdangerous v2.1.2 + ├── jinja2 v3.1.3 + │ └── markupsafe v2.1.5 + └── werkzeug v3.0.1 + └── markupsafe v2.1.5 ----- stderr ----- "### @@ -403,9 +368,7 @@ fn prune() { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt - .write_str("scikit-learn==1.4.1.post1") - .unwrap(); + requirements_txt.write_str("flask").unwrap(); uv_snapshot!(context.pip_install() .arg("-r") @@ -416,14 +379,16 @@ fn prune() { ----- stdout ----- ----- stderr ----- - Resolved 5 packages in [TIME] - Prepared 5 packages in [TIME] - Installed 5 packages in [TIME] - + joblib==1.3.2 - + numpy==1.26.4 - + scikit-learn==1.4.1.post1 - + scipy==1.12.0 - + threadpoolctl==3.4.0 + Resolved 7 packages in [TIME] + Prepared 7 packages in [TIME] + Installed 7 packages in [TIME] + + blinker==1.7.0 + + click==8.1.7 + + flask==3.0.2 + + itsdangerous==2.1.2 + + jinja2==3.1.3 + + markupsafe==2.1.5 + + werkzeug==3.0.1 "### ); @@ -433,17 +398,19 @@ fn prune() { .arg("--cache-dir") .arg(context.cache_dir.path()) .arg("--prune") - .arg("numpy") + .arg("werkzeug") .env(EnvVars::VIRTUAL_ENV, context.venv.as_os_str()) .env(EnvVars::UV_NO_WRAP, "1") .current_dir(&context.temp_dir), @r###" success: true exit_code: 0 ----- stdout ----- - scikit-learn v1.4.1.post1 - ├── scipy v1.12.0 - ├── joblib v1.3.2 - └── threadpoolctl v3.4.0 + flask v3.0.2 + ├── blinker v1.7.0 + ├── click v8.1.7 + ├── itsdangerous v2.1.2 + └── jinja2 v3.1.3 + └── markupsafe v2.1.5 ----- stderr ----- "### @@ -455,323 +422,22 @@ fn prune() { .arg("--cache-dir") .arg(context.cache_dir.path()) .arg("--prune") - .arg("numpy") - .arg("--prune") - .arg("joblib") - .env(EnvVars::VIRTUAL_ENV, context.venv.as_os_str()) - .env(EnvVars::UV_NO_WRAP, "1") - .current_dir(&context.temp_dir), @r###" - success: true - exit_code: 0 - ----- stdout ----- - scikit-learn v1.4.1.post1 - ├── scipy v1.12.0 - └── threadpoolctl v3.4.0 - - ----- stderr ----- - "### - ); - - uv_snapshot!(context.filters(), Command::new(get_bin()) - .arg("pip") - .arg("tree") - .arg("--cache-dir") - .arg(context.cache_dir.path()) + .arg("werkzeug") .arg("--prune") - .arg("scipy") + .arg("jinja2") .env(EnvVars::VIRTUAL_ENV, context.venv.as_os_str()) .env(EnvVars::UV_NO_WRAP, "1") .current_dir(&context.temp_dir), @r###" success: true exit_code: 0 ----- stdout ----- - scikit-learn v1.4.1.post1 - ├── numpy v1.26.4 - ├── joblib v1.3.2 - └── threadpoolctl v3.4.0 - - ----- stderr ----- - "### - ); -} - -#[test] -#[cfg(target_os = "macos")] -fn complex_nested_dependencies_inverted() { - let context = TestContext::new("3.12"); - - let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.write_str("packse").unwrap(); - - uv_snapshot!(context - .pip_install() - .arg("-r") - .arg("requirements.txt") - .arg("--strict"), @r###" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 32 packages in [TIME] - Prepared 32 packages in [TIME] - Installed 32 packages in [TIME] - + certifi==2024.2.2 - + charset-normalizer==3.3.2 - + chevron-blue==0.2.1 - + docutils==0.20.1 - + hatchling==1.22.4 - + idna==3.6 - + importlib-metadata==7.1.0 - + jaraco-classes==3.3.1 - + jaraco-context==4.3.0 - + jaraco-functools==4.0.0 - + keyring==25.0.0 - + markdown-it-py==3.0.0 - + mdurl==0.1.2 - + more-itertools==10.2.0 - + msgspec==0.18.6 - + nh3==0.2.15 - + packaging==24.0 - + packse==0.3.12 - + pathspec==0.12.1 - + pkginfo==1.10.0 - + pluggy==1.4.0 - + pygments==2.17.2 - + readme-renderer==43.0 - + requests==2.31.0 - + requests-toolbelt==1.0.0 - + rfc3986==2.0.0 - + rich==13.7.1 - + setuptools==69.2.0 - + trove-classifiers==2024.3.3 - + twine==4.0.2 - + urllib3==2.2.1 - + zipp==3.18.1 - "### - ); - - uv_snapshot!(context.filters(), context.pip_tree().arg("--invert"), @r###" - success: true - exit_code: 0 - ----- stdout ----- - certifi v2024.2.2 - └── requests v2.31.0 - ├── requests-toolbelt v1.0.0 - │ └── twine v4.0.2 - │ └── packse v0.3.12 - └── twine v4.0.2 (*) - charset-normalizer v3.3.2 - └── requests v2.31.0 (*) - chevron-blue v0.2.1 - └── packse v0.3.12 - docutils v0.20.1 - └── readme-renderer v43.0 - └── twine v4.0.2 (*) - idna v3.6 - └── requests v2.31.0 (*) - jaraco-context v4.3.0 - └── keyring v25.0.0 - └── twine v4.0.2 (*) - mdurl v0.1.2 - └── markdown-it-py v3.0.0 - └── rich v13.7.1 - └── twine v4.0.2 (*) - more-itertools v10.2.0 - ├── jaraco-classes v3.3.1 - │ └── keyring v25.0.0 (*) - └── jaraco-functools v4.0.0 - └── keyring v25.0.0 (*) - msgspec v0.18.6 - └── packse v0.3.12 - nh3 v0.2.15 - └── readme-renderer v43.0 (*) - packaging v24.0 - └── hatchling v1.22.4 - └── packse v0.3.12 - pathspec v0.12.1 - └── hatchling v1.22.4 (*) - pkginfo v1.10.0 - └── twine v4.0.2 (*) - pluggy v1.4.0 - └── hatchling v1.22.4 (*) - pygments v2.17.2 - ├── readme-renderer v43.0 (*) - └── rich v13.7.1 (*) - rfc3986 v2.0.0 - └── twine v4.0.2 (*) - setuptools v69.2.0 - └── packse v0.3.12 - trove-classifiers v2024.3.3 - └── hatchling v1.22.4 (*) - urllib3 v2.2.1 - ├── requests v2.31.0 (*) - └── twine v4.0.2 (*) - zipp v3.18.1 - └── importlib-metadata v7.1.0 - └── twine v4.0.2 (*) - (*) Package tree already displayed - - ----- stderr ----- - "### - ); -} - -#[test] -#[cfg(target_os = "macos")] -fn complex_nested_dependencies() { - let context = TestContext::new("3.12"); - - let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.write_str("packse").unwrap(); - - uv_snapshot!(context - .pip_install() - .arg("-r") - .arg("requirements.txt") - .arg("--strict"), @r###" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 32 packages in [TIME] - Prepared 32 packages in [TIME] - Installed 32 packages in [TIME] - + certifi==2024.2.2 - + charset-normalizer==3.3.2 - + chevron-blue==0.2.1 - + docutils==0.20.1 - + hatchling==1.22.4 - + idna==3.6 - + importlib-metadata==7.1.0 - + jaraco-classes==3.3.1 - + jaraco-context==4.3.0 - + jaraco-functools==4.0.0 - + keyring==25.0.0 - + markdown-it-py==3.0.0 - + mdurl==0.1.2 - + more-itertools==10.2.0 - + msgspec==0.18.6 - + nh3==0.2.15 - + packaging==24.0 - + packse==0.3.12 - + pathspec==0.12.1 - + pkginfo==1.10.0 - + pluggy==1.4.0 - + pygments==2.17.2 - + readme-renderer==43.0 - + requests==2.31.0 - + requests-toolbelt==1.0.0 - + rfc3986==2.0.0 - + rich==13.7.1 - + setuptools==69.2.0 - + trove-classifiers==2024.3.3 - + twine==4.0.2 - + urllib3==2.2.1 - + zipp==3.18.1 - "### - ); - - uv_snapshot!(context.filters(), context.pip_tree(), @r###" - success: true - exit_code: 0 - ----- stdout ----- - packse v0.3.12 - ├── chevron-blue v0.2.1 - ├── hatchling v1.22.4 - │ ├── packaging v24.0 - │ ├── pathspec v0.12.1 - │ ├── pluggy v1.4.0 - │ └── trove-classifiers v2024.3.3 - ├── msgspec v0.18.6 - ├── setuptools v69.2.0 - └── twine v4.0.2 - ├── pkginfo v1.10.0 - ├── readme-renderer v43.0 - │ ├── nh3 v0.2.15 - │ ├── docutils v0.20.1 - │ └── pygments v2.17.2 - ├── requests v2.31.0 - │ ├── charset-normalizer v3.3.2 - │ ├── idna v3.6 - │ ├── urllib3 v2.2.1 - │ └── certifi v2024.2.2 - ├── requests-toolbelt v1.0.0 - │ └── requests v2.31.0 (*) - ├── urllib3 v2.2.1 - ├── importlib-metadata v7.1.0 - │ └── zipp v3.18.1 - ├── keyring v25.0.0 - │ ├── jaraco-classes v3.3.1 - │ │ └── more-itertools v10.2.0 - │ ├── jaraco-functools v4.0.0 - │ │ └── more-itertools v10.2.0 - │ └── jaraco-context v4.3.0 - ├── rfc3986 v2.0.0 - └── rich v13.7.1 - ├── markdown-it-py v3.0.0 - │ └── mdurl v0.1.2 - └── pygments v2.17.2 - (*) Package tree already displayed - - ----- stderr ----- - "### - ); -} - -#[test] -#[cfg(target_os = "macos")] -fn prune_large_tree() { - let context = TestContext::new("3.12"); - - let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.write_str("packse").unwrap(); - - uv_snapshot!(context.pip_install() - .arg("-r") - .arg("requirements.txt") - .arg("--strict"), @r###" - success: true - exit_code: 0 - ----- stdout ----- + flask v3.0.2 + ├── blinker v1.7.0 + ├── click v8.1.7 + └── itsdangerous v2.1.2 + markupsafe v2.1.5 ----- stderr ----- - Resolved 32 packages in [TIME] - Prepared 32 packages in [TIME] - Installed 32 packages in [TIME] - + certifi==2024.2.2 - + charset-normalizer==3.3.2 - + chevron-blue==0.2.1 - + docutils==0.20.1 - + hatchling==1.22.4 - + idna==3.6 - + importlib-metadata==7.1.0 - + jaraco-classes==3.3.1 - + jaraco-context==4.3.0 - + jaraco-functools==4.0.0 - + keyring==25.0.0 - + markdown-it-py==3.0.0 - + mdurl==0.1.2 - + more-itertools==10.2.0 - + msgspec==0.18.6 - + nh3==0.2.15 - + packaging==24.0 - + packse==0.3.12 - + pathspec==0.12.1 - + pkginfo==1.10.0 - + pluggy==1.4.0 - + pygments==2.17.2 - + readme-renderer==43.0 - + requests==2.31.0 - + requests-toolbelt==1.0.0 - + rfc3986==2.0.0 - + rich==13.7.1 - + setuptools==69.2.0 - + trove-classifiers==2024.3.3 - + twine==4.0.2 - + urllib3==2.2.1 - + zipp==3.18.1 "### ); @@ -781,104 +447,26 @@ fn prune_large_tree() { .arg("--cache-dir") .arg(context.cache_dir.path()) .arg("--prune") - .arg("hatchling") + .arg("werkzeug") .env(EnvVars::VIRTUAL_ENV, context.venv.as_os_str()) .env(EnvVars::UV_NO_WRAP, "1") .current_dir(&context.temp_dir), @r###" success: true exit_code: 0 ----- stdout ----- - packse v0.3.12 - ├── chevron-blue v0.2.1 - ├── msgspec v0.18.6 - ├── setuptools v69.2.0 - └── twine v4.0.2 - ├── pkginfo v1.10.0 - ├── readme-renderer v43.0 - │ ├── nh3 v0.2.15 - │ ├── docutils v0.20.1 - │ └── pygments v2.17.2 - ├── requests v2.31.0 - │ ├── charset-normalizer v3.3.2 - │ ├── idna v3.6 - │ ├── urllib3 v2.2.1 - │ └── certifi v2024.2.2 - ├── requests-toolbelt v1.0.0 - │ └── requests v2.31.0 (*) - ├── urllib3 v2.2.1 - ├── importlib-metadata v7.1.0 - │ └── zipp v3.18.1 - ├── keyring v25.0.0 - │ ├── jaraco-classes v3.3.1 - │ │ └── more-itertools v10.2.0 - │ ├── jaraco-functools v4.0.0 - │ │ └── more-itertools v10.2.0 - │ └── jaraco-context v4.3.0 - ├── rfc3986 v2.0.0 - └── rich v13.7.1 - ├── markdown-it-py v3.0.0 - │ └── mdurl v0.1.2 - └── pygments v2.17.2 - (*) Package tree already displayed + flask v3.0.2 + ├── blinker v1.7.0 + ├── click v8.1.7 + ├── itsdangerous v2.1.2 + └── jinja2 v3.1.3 + └── markupsafe v2.1.5 ----- stderr ----- "### ); } -// Ensure `pip tree` behaves correctly with a package that has a cyclic dependency. -// package `uv-cyclic-dependencies-a` and `uv-cyclic-dependencies-b` depend on each other, -// which creates a dependency cycle. -// Additionally, package `uv-cyclic-dependencies-c` is included (depends on `uv-cyclic-dependencies-a`) -// to make this test case more realistic and meaningful. -#[test] -fn cyclic_dependency() { - let context = TestContext::new("3.12"); - - let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt - .write_str("uv-cyclic-dependencies-c") - .unwrap(); - - let mut command = context.pip_install(); - command.env_remove(EnvVars::UV_EXCLUDE_NEWER); - command - .arg("-r") - .arg("requirements.txt") - .arg("--index-url") - .arg("https://test.pypi.org/simple/"); - - uv_snapshot!(context.filters(), command, @r###" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 3 packages in [TIME] - Prepared 3 packages in [TIME] - Installed 3 packages in [TIME] - + uv-cyclic-dependencies-a==0.1.0 - + uv-cyclic-dependencies-b==0.1.0 - + uv-cyclic-dependencies-c==0.1.0 - "### - ); - - uv_snapshot!(context.filters(), context.pip_tree(), @r###" - success: true - exit_code: 0 - ----- stdout ----- - uv-cyclic-dependencies-c v0.1.0 - └── uv-cyclic-dependencies-a v0.1.0 - └── uv-cyclic-dependencies-b v0.1.0 - └── uv-cyclic-dependencies-a v0.1.0 (*) - (*) Package tree already displayed - - ----- stderr ----- - "### - ); -} - -// Ensure `pip tree` behaves correctly after a package has been removed. +/// Ensure `pip tree` behaves correctly after a package has been removed. #[test] fn removed_dependency() { let context = TestContext::new("3.12"); @@ -970,39 +558,34 @@ fn multiple_packages() { "### ); - let mut filters = context.filters(); - if cfg!(windows) { - filters.push(("└── colorama v0.4.6\n", "")); - } context.assert_command("import requests").success(); - uv_snapshot!(filters, context.pip_tree(), @r###" + uv_snapshot!(context.filters(), context.pip_tree(), @r###" success: true exit_code: 0 ----- stdout ----- click v8.1.7 requests v2.31.0 + ├── certifi v2024.2.2 ├── charset-normalizer v3.3.2 ├── idna v3.6 - ├── urllib3 v2.2.1 - └── certifi v2024.2.2 + └── urllib3 v2.2.1 ----- stderr ----- "### ); } -// Both `pendulum` and `boto3` depend on `python-dateutil`. +/// Show the installed tree in the presence of a cycle. #[test] -#[cfg(not(windows))] -fn multiple_packages_shared_descendant() { +fn cycle() { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); requirements_txt .write_str( r" - pendulum==3.0.0 - boto3==1.34.69 + testtools==2.3.0 + fixtures==3.0.0 ", ) .unwrap(); @@ -1020,17 +603,16 @@ fn multiple_packages_shared_descendant() { Resolved 10 packages in [TIME] Prepared 10 packages in [TIME] Installed 10 packages in [TIME] - + boto3==1.34.69 - + botocore==1.34.69 - + jmespath==1.0.1 - + pendulum==3.0.0 - + python-dateutil==2.9.0.post0 - + s3transfer==0.10.1 + + argparse==1.4.0 + + extras==1.0.0 + + fixtures==3.0.0 + + linecache2==1.0.0 + + pbr==6.0.0 + + python-mimeparse==1.6.0 + six==1.16.0 - + time-machine==2.14.1 - + tzdata==2024.1 - + urllib3==2.2.1 - + + testtools==2.3.0 + + traceback2==1.4.0 + + unittest2==1.1.0 "### ); @@ -1038,20 +620,21 @@ fn multiple_packages_shared_descendant() { success: true exit_code: 0 ----- stdout ----- - boto3 v1.34.69 - ├── botocore v1.34.69 - │ ├── jmespath v1.0.1 - │ ├── python-dateutil v2.9.0.post0 - │ │ └── six v1.16.0 - │ └── urllib3 v2.2.1 - ├── jmespath v1.0.1 - └── s3transfer v0.10.1 - └── botocore v1.34.69 (*) - pendulum v3.0.0 - ├── python-dateutil v2.9.0.post0 (*) - ├── tzdata v2024.1 - └── time-machine v2.14.1 - └── python-dateutil v2.9.0.post0 (*) + testtools v2.3.0 + ├── extras v1.0.0 + ├── fixtures v3.0.0 + │ ├── pbr v6.0.0 + │ ├── six v1.16.0 + │ └── testtools v2.3.0 (*) + ├── pbr v6.0.0 + ├── python-mimeparse v1.6.0 + ├── six v1.16.0 + ├── traceback2 v1.4.0 + │ └── linecache2 v1.0.0 + └── unittest2 v1.1.0 + ├── argparse v1.4.0 + ├── six v1.16.0 + └── traceback2 v1.4.0 (*) (*) Package tree already displayed ----- stderr ----- @@ -1059,18 +642,17 @@ fn multiple_packages_shared_descendant() { ); } -// Test the interaction between `--no-dedupe` and `--invert`. +/// Both `pendulum` and `boto3` depend on `python-dateutil`. #[test] -#[cfg(not(windows))] -fn no_dedupe_and_invert() { +fn multiple_packages_shared_descendant() { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); requirements_txt .write_str( r" - pendulum==3.0.0 - boto3==1.34.69 + pendulum + time-machine ", ) .unwrap(); @@ -1085,68 +667,45 @@ fn no_dedupe_and_invert() { ----- stdout ----- ----- stderr ----- - Resolved 10 packages in [TIME] - Prepared 10 packages in [TIME] - Installed 10 packages in [TIME] - + boto3==1.34.69 - + botocore==1.34.69 - + jmespath==1.0.1 + Resolved 5 packages in [TIME] + Prepared 5 packages in [TIME] + Installed 5 packages in [TIME] + pendulum==3.0.0 + python-dateutil==2.9.0.post0 - + s3transfer==0.10.1 + six==1.16.0 + time-machine==2.14.1 + tzdata==2024.1 - + urllib3==2.2.1 - "### ); - uv_snapshot!(context.filters(), context.pip_tree().arg("--no-dedupe").arg("--invert"), @r###" + uv_snapshot!(context.filters(), context.pip_tree(), @r###" success: true exit_code: 0 ----- stdout ----- - jmespath v1.0.1 - ├── boto3 v1.34.69 - └── botocore v1.34.69 - ├── boto3 v1.34.69 - └── s3transfer v0.10.1 - └── boto3 v1.34.69 - six v1.16.0 - └── python-dateutil v2.9.0.post0 - ├── botocore v1.34.69 - │ ├── boto3 v1.34.69 - │ └── s3transfer v0.10.1 - │ └── boto3 v1.34.69 - ├── pendulum v3.0.0 - └── time-machine v2.14.1 - └── pendulum v3.0.0 - tzdata v2024.1 - └── pendulum v3.0.0 - urllib3 v2.2.1 - └── botocore v1.34.69 - ├── boto3 v1.34.69 - └── s3transfer v0.10.1 - └── boto3 v1.34.69 + pendulum v3.0.0 + ├── python-dateutil v2.9.0.post0 + │ └── six v1.16.0 + ├── time-machine v2.14.1 + │ └── python-dateutil v2.9.0.post0 (*) + └── tzdata v2024.1 + (*) Package tree already displayed ----- stderr ----- "### ); } -// Ensure that --no-dedupe behaves as expected -// in the presence of dependency cycles. +/// Test the interaction between `--no-dedupe` and `--invert`. #[test] -#[cfg(not(windows))] -fn no_dedupe_and_cycle() { +fn no_dedupe_and_invert() { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); requirements_txt .write_str( r" - pendulum==3.0.0 - boto3==1.34.69 + pendulum + time-machine ", ) .unwrap(); @@ -1161,75 +720,28 @@ fn no_dedupe_and_cycle() { ----- stdout ----- ----- stderr ----- - Resolved 10 packages in [TIME] - Prepared 10 packages in [TIME] - Installed 10 packages in [TIME] - + boto3==1.34.69 - + botocore==1.34.69 - + jmespath==1.0.1 + Resolved 5 packages in [TIME] + Prepared 5 packages in [TIME] + Installed 5 packages in [TIME] + pendulum==3.0.0 + python-dateutil==2.9.0.post0 - + s3transfer==0.10.1 + six==1.16.0 + time-machine==2.14.1 + tzdata==2024.1 - + urllib3==2.2.1 - - "### - ); - - let mut command = context.pip_install(); - command.env_remove(EnvVars::UV_EXCLUDE_NEWER); - command - .arg("uv-cyclic-dependencies-c==0.1.0") - .arg("--index-url") - .arg("https://test.pypi.org/simple/"); - - uv_snapshot!(context.filters(), command, @r###" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 3 packages in [TIME] - Prepared 3 packages in [TIME] - Installed 3 packages in [TIME] - + uv-cyclic-dependencies-a==0.1.0 - + uv-cyclic-dependencies-b==0.1.0 - + uv-cyclic-dependencies-c==0.1.0 "### ); - uv_snapshot!(context.filters(), context.pip_tree() - .arg("--no-dedupe"), @r###" + uv_snapshot!(context.filters(), context.pip_tree().arg("--no-dedupe").arg("--invert"), @r###" success: true exit_code: 0 ----- stdout ----- - boto3 v1.34.69 - ├── botocore v1.34.69 - │ ├── jmespath v1.0.1 - │ ├── python-dateutil v2.9.0.post0 - │ │ └── six v1.16.0 - │ └── urllib3 v2.2.1 - ├── jmespath v1.0.1 - └── s3transfer v0.10.1 - └── botocore v1.34.69 - ├── jmespath v1.0.1 - ├── python-dateutil v2.9.0.post0 - │ └── six v1.16.0 - └── urllib3 v2.2.1 - pendulum v3.0.0 - ├── python-dateutil v2.9.0.post0 - │ └── six v1.16.0 - ├── tzdata v2024.1 - └── time-machine v2.14.1 - └── python-dateutil v2.9.0.post0 - └── six v1.16.0 - uv-cyclic-dependencies-c v0.1.0 - └── uv-cyclic-dependencies-a v0.1.0 - └── uv-cyclic-dependencies-b v0.1.0 - └── uv-cyclic-dependencies-a v0.1.0 (*) - (*) Package tree is a cycle and cannot be shown + six v1.16.0 + └── python-dateutil v2.9.0.post0 + ├── pendulum v3.0.0 + └── time-machine v2.14.1 + └── pendulum v3.0.0 + tzdata v2024.1 + └── pendulum v3.0.0 ----- stderr ----- "### @@ -1237,7 +749,6 @@ fn no_dedupe_and_cycle() { } #[test] -#[cfg(not(windows))] fn no_dedupe() { let context = TestContext::new("3.12"); @@ -1245,8 +756,8 @@ fn no_dedupe() { requirements_txt .write_str( r" - pendulum==3.0.0 - boto3==1.34.69 + pendulum + time-machine ", ) .unwrap(); @@ -1261,20 +772,14 @@ fn no_dedupe() { ----- stdout ----- ----- stderr ----- - Resolved 10 packages in [TIME] - Prepared 10 packages in [TIME] - Installed 10 packages in [TIME] - + boto3==1.34.69 - + botocore==1.34.69 - + jmespath==1.0.1 + Resolved 5 packages in [TIME] + Prepared 5 packages in [TIME] + Installed 5 packages in [TIME] + pendulum==3.0.0 + python-dateutil==2.9.0.post0 - + s3transfer==0.10.1 + six==1.16.0 + time-machine==2.14.1 + tzdata==2024.1 - + urllib3==2.2.1 - "### ); @@ -1283,26 +788,13 @@ fn no_dedupe() { success: true exit_code: 0 ----- stdout ----- - boto3 v1.34.69 - ├── botocore v1.34.69 - │ ├── jmespath v1.0.1 - │ ├── python-dateutil v2.9.0.post0 - │ │ └── six v1.16.0 - │ └── urllib3 v2.2.1 - ├── jmespath v1.0.1 - └── s3transfer v0.10.1 - └── botocore v1.34.69 - ├── jmespath v1.0.1 - ├── python-dateutil v2.9.0.post0 - │ └── six v1.16.0 - └── urllib3 v2.2.1 pendulum v3.0.0 ├── python-dateutil v2.9.0.post0 │ └── six v1.16.0 - ├── tzdata v2024.1 - └── time-machine v2.14.1 - └── python-dateutil v2.9.0.post0 - └── six v1.16.0 + ├── time-machine v2.14.1 + │ └── python-dateutil v2.9.0.post0 + │ └── six v1.16.0 + └── tzdata v2024.1 ----- stderr ----- "### @@ -1350,98 +842,12 @@ fn with_editable() { ); } -#[test] -#[cfg(target_os = "macos")] -fn package_flag_complex() { - let context = TestContext::new("3.12"); - - let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.write_str("packse").unwrap(); - - uv_snapshot!(context - .pip_install() - .arg("-r") - .arg("requirements.txt") - .arg("--strict"), @r###" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 32 packages in [TIME] - Prepared 32 packages in [TIME] - Installed 32 packages in [TIME] - + certifi==2024.2.2 - + charset-normalizer==3.3.2 - + chevron-blue==0.2.1 - + docutils==0.20.1 - + hatchling==1.22.4 - + idna==3.6 - + importlib-metadata==7.1.0 - + jaraco-classes==3.3.1 - + jaraco-context==4.3.0 - + jaraco-functools==4.0.0 - + keyring==25.0.0 - + markdown-it-py==3.0.0 - + mdurl==0.1.2 - + more-itertools==10.2.0 - + msgspec==0.18.6 - + nh3==0.2.15 - + packaging==24.0 - + packse==0.3.12 - + pathspec==0.12.1 - + pkginfo==1.10.0 - + pluggy==1.4.0 - + pygments==2.17.2 - + readme-renderer==43.0 - + requests==2.31.0 - + requests-toolbelt==1.0.0 - + rfc3986==2.0.0 - + rich==13.7.1 - + setuptools==69.2.0 - + trove-classifiers==2024.3.3 - + twine==4.0.2 - + urllib3==2.2.1 - + zipp==3.18.1 - "### - ); - - uv_snapshot!( - context.filters(), - context.pip_tree() - .arg("--package") - .arg("hatchling") - .arg("--package") - .arg("keyring"), @r###" - success: true - exit_code: 0 - ----- stdout ----- - hatchling v1.22.4 - ├── packaging v24.0 - ├── pathspec v0.12.1 - ├── pluggy v1.4.0 - └── trove-classifiers v2024.3.3 - - keyring v25.0.0 - ├── jaraco-classes v3.3.1 - │ └── more-itertools v10.2.0 - ├── jaraco-functools v4.0.0 - │ └── more-itertools v10.2.0 - └── jaraco-context v4.3.0 - - ----- stderr ----- - "### - ); -} - #[test] fn package_flag() { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt - .write_str("scikit-learn==1.4.1.post1") - .unwrap(); + requirements_txt.write_str("flask").unwrap(); uv_snapshot!(context .pip_install() @@ -1453,14 +859,16 @@ fn package_flag() { ----- stdout ----- ----- stderr ----- - Resolved 5 packages in [TIME] - Prepared 5 packages in [TIME] - Installed 5 packages in [TIME] - + joblib==1.3.2 - + numpy==1.26.4 - + scikit-learn==1.4.1.post1 - + scipy==1.12.0 - + threadpoolctl==3.4.0 + Resolved 7 packages in [TIME] + Prepared 7 packages in [TIME] + Installed 7 packages in [TIME] + + blinker==1.7.0 + + click==8.1.7 + + flask==3.0.2 + + itsdangerous==2.1.2 + + jinja2==3.1.3 + + markupsafe==2.1.5 + + werkzeug==3.0.1 "### ); @@ -1468,12 +876,13 @@ fn package_flag() { context.filters(), context.pip_tree() .arg("--package") - .arg("numpy"), + .arg("werkzeug"), @r###" success: true exit_code: 0 ----- stdout ----- - numpy v1.26.4 + werkzeug v3.0.1 + └── markupsafe v2.1.5 ----- stderr ----- "### @@ -1483,17 +892,17 @@ fn package_flag() { context.filters(), context.pip_tree() .arg("--package") - .arg("scipy") + .arg("werkzeug") .arg("--package") - .arg("joblib"), + .arg("jinja2"), @r###" success: true exit_code: 0 ----- stdout ----- - scipy v1.12.0 - └── numpy v1.26.4 - - joblib v1.3.2 + jinja2 v3.1.3 + └── markupsafe v2.1.5 + werkzeug v3.0.1 + └── markupsafe v2.1.5 ----- stderr ----- "### @@ -1533,113 +942,10 @@ fn show_version_specifiers_simple() { exit_code: 0 ----- stdout ----- requests v2.31.0 + ├── certifi v2024.2.2 [required: >=2017.4.17] ├── charset-normalizer v3.3.2 [required: >=2, <4] ├── idna v3.6 [required: >=2.5, <4] - ├── urllib3 v2.2.1 [required: >=1.21.1, <3] - └── certifi v2024.2.2 [required: >=2017.4.17] - - ----- stderr ----- - "### - ); -} - -#[test] -#[cfg(target_os = "macos")] -fn show_version_specifiers_complex() { - let context = TestContext::new("3.12"); - - let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.write_str("packse").unwrap(); - - uv_snapshot!(context - .pip_install() - .arg("-r") - .arg("requirements.txt") - .arg("--strict"), @r###" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 32 packages in [TIME] - Prepared 32 packages in [TIME] - Installed 32 packages in [TIME] - + certifi==2024.2.2 - + charset-normalizer==3.3.2 - + chevron-blue==0.2.1 - + docutils==0.20.1 - + hatchling==1.22.4 - + idna==3.6 - + importlib-metadata==7.1.0 - + jaraco-classes==3.3.1 - + jaraco-context==4.3.0 - + jaraco-functools==4.0.0 - + keyring==25.0.0 - + markdown-it-py==3.0.0 - + mdurl==0.1.2 - + more-itertools==10.2.0 - + msgspec==0.18.6 - + nh3==0.2.15 - + packaging==24.0 - + packse==0.3.12 - + pathspec==0.12.1 - + pkginfo==1.10.0 - + pluggy==1.4.0 - + pygments==2.17.2 - + readme-renderer==43.0 - + requests==2.31.0 - + requests-toolbelt==1.0.0 - + rfc3986==2.0.0 - + rich==13.7.1 - + setuptools==69.2.0 - + trove-classifiers==2024.3.3 - + twine==4.0.2 - + urllib3==2.2.1 - + zipp==3.18.1 - "### - ); - - uv_snapshot!(context.filters(), context.pip_tree().arg("--show-version-specifiers"), @r###" - success: true - exit_code: 0 - ----- stdout ----- - packse v0.3.12 - ├── chevron-blue v0.2.1 [required: >=0.2.1, <0.3.0] - ├── hatchling v1.22.4 [required: >=1.20.0, <2.0.0] - │ ├── packaging v24.0 [required: >=21.3] - │ ├── pathspec v0.12.1 [required: >=0.10.1] - │ ├── pluggy v1.4.0 [required: >=1.0.0] - │ └── trove-classifiers v2024.3.3 [required: *] - ├── msgspec v0.18.6 [required: >=0.18.4, <0.19.0] - ├── setuptools v69.2.0 [required: >=69.1.1, <70.0.0] - └── twine v4.0.2 [required: >=4.0.2, <5.0.0] - ├── pkginfo v1.10.0 [required: >=1.8.1] - ├── readme-renderer v43.0 [required: >=35.0] - │ ├── nh3 v0.2.15 [required: >=0.2.14] - │ ├── docutils v0.20.1 [required: >=0.13.1] - │ └── pygments v2.17.2 [required: >=2.5.1] - ├── requests v2.31.0 [required: >=2.20] - │ ├── charset-normalizer v3.3.2 [required: >=2, <4] - │ ├── idna v3.6 [required: >=2.5, <4] - │ ├── urllib3 v2.2.1 [required: >=1.21.1, <3] - │ └── certifi v2024.2.2 [required: >=2017.4.17] - ├── requests-toolbelt v1.0.0 [required: >=0.8.0, !=0.9.0] - │ └── requests v2.31.0 [required: >=2.0.1, <3.0.0] (*) - ├── urllib3 v2.2.1 [required: >=1.26.0] - ├── importlib-metadata v7.1.0 [required: >=3.6] - │ └── zipp v3.18.1 [required: >=0.5] - ├── keyring v25.0.0 [required: >=15.1] - │ ├── jaraco-classes v3.3.1 [required: *] - │ │ └── more-itertools v10.2.0 [required: *] - │ ├── jaraco-functools v4.0.0 [required: *] - │ │ └── more-itertools v10.2.0 [required: *] - │ └── jaraco-context v4.3.0 [required: *] - ├── rfc3986 v2.0.0 [required: >=1.4.0] - └── rich v13.7.1 [required: >=12.0.0] - ├── markdown-it-py v3.0.0 [required: >=2.2.0] - │ └── mdurl v0.1.2 [required: ~=0.1] - └── pygments v2.17.2 [required: >=2.13.0, <3.0.0] - (*) Package tree already displayed + └── urllib3 v2.2.1 [required: >=1.21.1, <3] ----- stderr ----- "### @@ -1651,9 +957,7 @@ fn show_version_specifiers_with_invert() { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt - .write_str("scikit-learn==1.4.1.post1") - .unwrap(); + requirements_txt.write_str("flask").unwrap(); uv_snapshot!(context .pip_install() @@ -1665,14 +969,16 @@ fn show_version_specifiers_with_invert() { ----- stdout ----- ----- stderr ----- - Resolved 5 packages in [TIME] - Prepared 5 packages in [TIME] - Installed 5 packages in [TIME] - + joblib==1.3.2 - + numpy==1.26.4 - + scikit-learn==1.4.1.post1 - + scipy==1.12.0 - + threadpoolctl==3.4.0 + Resolved 7 packages in [TIME] + Prepared 7 packages in [TIME] + Installed 7 packages in [TIME] + + blinker==1.7.0 + + click==8.1.7 + + flask==3.0.2 + + itsdangerous==2.1.2 + + jinja2==3.1.3 + + markupsafe==2.1.5 + + werkzeug==3.0.1 "### ); @@ -1684,14 +990,17 @@ fn show_version_specifiers_with_invert() { success: true exit_code: 0 ----- stdout ----- - joblib v1.3.2 - └── scikit-learn v1.4.1.post1 [requires: joblib >=1.2.0] - numpy v1.26.4 - ├── scikit-learn v1.4.1.post1 [requires: numpy >=1.19.5, <2.0] - └── scipy v1.12.0 [requires: numpy >=1.22.4, <1.29.0] - └── scikit-learn v1.4.1.post1 [requires: scipy >=1.6.0] - threadpoolctl v3.4.0 - └── scikit-learn v1.4.1.post1 [requires: threadpoolctl >=2.0.0] + blinker v1.7.0 + └── flask v3.0.2 [requires: blinker >=1.6.2] + click v8.1.7 + └── flask v3.0.2 [requires: click >=8.1.3] + itsdangerous v2.1.2 + └── flask v3.0.2 [requires: itsdangerous >=2.1.2] + markupsafe v2.1.5 + ├── jinja2 v3.1.3 [requires: markupsafe >=2.0] + │ └── flask v3.0.2 [requires: jinja2 >=3.1.2] + └── werkzeug v3.0.1 [requires: markupsafe >=2.1.1] + └── flask v3.0.2 [requires: werkzeug >=3.0.0] ----- stderr ----- "### @@ -1703,9 +1012,7 @@ fn show_version_specifiers_with_package() { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt - .write_str("scikit-learn==1.4.1.post1") - .unwrap(); + requirements_txt.write_str("flask").unwrap(); uv_snapshot!(context .pip_install() @@ -1717,14 +1024,16 @@ fn show_version_specifiers_with_package() { ----- stdout ----- ----- stderr ----- - Resolved 5 packages in [TIME] - Prepared 5 packages in [TIME] - Installed 5 packages in [TIME] - + joblib==1.3.2 - + numpy==1.26.4 - + scikit-learn==1.4.1.post1 - + scipy==1.12.0 - + threadpoolctl==3.4.0 + Resolved 7 packages in [TIME] + Prepared 7 packages in [TIME] + Installed 7 packages in [TIME] + + blinker==1.7.0 + + click==8.1.7 + + flask==3.0.2 + + itsdangerous==2.1.2 + + jinja2==3.1.3 + + markupsafe==2.1.5 + + werkzeug==3.0.1 "### ); @@ -1733,12 +1042,12 @@ fn show_version_specifiers_with_package() { context.pip_tree() .arg("--show-version-specifiers") .arg("--package") - .arg("scipy"), @r###" + .arg("werkzeug"), @r###" success: true exit_code: 0 ----- stdout ----- - scipy v1.12.0 - └── numpy v1.26.4 [required: >=1.22.4, <1.29.0] + werkzeug v3.0.1 + └── markupsafe v2.1.5 [required: >=2.1.1] ----- stderr ----- "### diff --git a/crates/uv/tests/it/publish.rs b/crates/uv/tests/it/publish.rs index 94039ba75188..752d7c427a84 100644 --- a/crates/uv/tests/it/publish.rs +++ b/crates/uv/tests/it/publish.rs @@ -1,4 +1,5 @@ use crate::common::{uv_snapshot, TestContext}; +use uv_static::EnvVars; #[test] fn username_password_no_longer_supported() { @@ -21,7 +22,7 @@ fn username_password_no_longer_supported() { Publishing 1 file to https://test.pypi.org/legacy/ Uploading ok-1.0.0-py3-none-any.whl ([SIZE]) error: Failed to publish `../../scripts/links/ok-1.0.0-py3-none-any.whl` to https://test.pypi.org/legacy/ - Caused by: Permission denied (status code 403 Forbidden): 403 Username/Password authentication is no longer supported. Migrate to API Tokens or Trusted Publishers instead. See https://test.pypi.org/help/#apitoken and https://test.pypi.org/help/#trusted-publishers + Caused by: Upload failed with status code 403 Forbidden. Server says: 403 Username/Password authentication is no longer supported. Migrate to API Tokens or Trusted Publishers instead. See https://test.pypi.org/help/#apitoken and https://test.pypi.org/help/#trusted-publishers "### ); } @@ -47,7 +48,97 @@ fn invalid_token() { Publishing 1 file to https://test.pypi.org/legacy/ Uploading ok-1.0.0-py3-none-any.whl ([SIZE]) error: Failed to publish `../../scripts/links/ok-1.0.0-py3-none-any.whl` to https://test.pypi.org/legacy/ - Caused by: Permission denied (status code 403 Forbidden): 403 Invalid or non-existent authentication information. See https://test.pypi.org/help/#invalid-auth for more information. + Caused by: Upload failed with status code 403 Forbidden. Server says: 403 Invalid or non-existent authentication information. See https://test.pypi.org/help/#invalid-auth for more information. + "### + ); +} + +/// Emulate a missing `permission` `id-token: write` situation. +#[test] +fn mixed_credentials() { + let context = TestContext::new("3.12"); + + uv_snapshot!(context.filters(), context.publish() + .arg("--username") + .arg("ferris") + .arg("--password") + .arg("ZmVycmlz") + .arg("--publish-url") + .arg("https://test.pypi.org/legacy/") + .arg("--trusted-publishing") + .arg("always") + .arg("../../scripts/links/ok-1.0.0-py3-none-any.whl") + // Emulate CI + .env(EnvVars::GITHUB_ACTIONS, "true") + // Just to make sure + .env_remove(EnvVars::ACTIONS_ID_TOKEN_REQUEST_TOKEN), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + warning: `uv publish` is experimental and may change without warning + Publishing 1 file to https://test.pypi.org/legacy/ + error: a username and a password are not allowed when using trusted publishing + "### + ); +} + +/// Emulate a missing `permission` `id-token: write` situation. +#[test] +fn missing_trusted_publishing_permission() { + let context = TestContext::new("3.12"); + + uv_snapshot!(context.filters(), context.publish() + .arg("--publish-url") + .arg("https://test.pypi.org/legacy/") + .arg("--trusted-publishing") + .arg("always") + .arg("../../scripts/links/ok-1.0.0-py3-none-any.whl") + // Emulate CI + .env(EnvVars::GITHUB_ACTIONS, "true") + // Just to make sure + .env_remove(EnvVars::ACTIONS_ID_TOKEN_REQUEST_TOKEN), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + warning: `uv publish` is experimental and may change without warning + Publishing 1 file to https://test.pypi.org/legacy/ + error: Failed to obtain token for trusted publishing + Caused by: Environment variable ACTIONS_ID_TOKEN_REQUEST_TOKEN not set, is the `id-token: write` permission missing? + "### + ); +} + +/// Check the error when there are no credentials provided on GitHub Actions. Is it an incorrect +/// trusted publishing configuration? +#[test] +fn no_credentials() { + let context = TestContext::new("3.12"); + + uv_snapshot!(context.filters(), context.publish() + .arg("--publish-url") + .arg("https://test.pypi.org/legacy/") + .arg("../../scripts/links/ok-1.0.0-py3-none-any.whl") + // Emulate CI + .env(EnvVars::GITHUB_ACTIONS, "true") + // Just to make sure + .env_remove(EnvVars::ACTIONS_ID_TOKEN_REQUEST_TOKEN), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + warning: `uv publish` is experimental and may change without warning + Publishing 1 file to https://test.pypi.org/legacy/ + Note: Neither credentials nor keyring are configured, and there was an error fetching the trusted publishing token. If you don't want to use trusted publishing, you can ignore this error, but you need to provide credentials. + Trusted publishing error: Environment variable ACTIONS_ID_TOKEN_REQUEST_TOKEN not set, is the `id-token: write` permission missing? + Uploading ok-1.0.0-py3-none-any.whl ([SIZE]) + error: Failed to publish `../../scripts/links/ok-1.0.0-py3-none-any.whl` to https://test.pypi.org/legacy/ + Caused by: Failed to send POST request + Caused by: Missing credentials for https://test.pypi.org/legacy/ "### ); } diff --git a/crates/uv/tests/it/python_install.rs b/crates/uv/tests/it/python_install.rs new file mode 100644 index 000000000000..ce647abc1202 --- /dev/null +++ b/crates/uv/tests/it/python_install.rs @@ -0,0 +1,285 @@ +use std::process::Command; + +use assert_fs::{assert::PathAssert, prelude::PathChild}; +use predicates::prelude::predicate; + +use crate::common::{uv_snapshot, TestContext}; + +#[test] +fn python_install() { + let context: TestContext = TestContext::new_with_versions(&[]).with_filtered_python_keys(); + + // Install the latest version + uv_snapshot!(context.filters(), context.python_install(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Installed Python 3.13.0 in [TIME] + + cpython-3.13.0-[PLATFORM] + "###); + + let bin_python = context + .temp_dir + .child("bin") + .child(format!("python3.13{}", std::env::consts::EXE_SUFFIX)); + + // The executable should not be installed in the bin directory (requires preview) + bin_python.assert(predicate::path::missing()); + + // Should be a no-op when already installed + uv_snapshot!(context.filters(), context.python_install(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Python is already installed. Use `uv python install ` to install another version. + "###); + + // Similarly, when a requested version is already installed + uv_snapshot!(context.filters(), context.python_install().arg("3.13"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + "###); + + // You can opt-in to a reinstall + uv_snapshot!(context.filters(), context.python_install().arg("--reinstall"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Installed Python 3.13.0 in [TIME] + ~ cpython-3.13.0-[PLATFORM] + "###); + + // Uninstallation requires an argument + uv_snapshot!(context.filters(), context.python_uninstall(), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: the following required arguments were not provided: + ... + + Usage: uv python uninstall ... + + For more information, try '--help'. + "###); + + uv_snapshot!(context.filters(), context.python_uninstall().arg("3.13"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Searching for Python versions matching: Python 3.13 + Uninstalled Python 3.13.0 in [TIME] + - cpython-3.13.0-[PLATFORM] + "###); +} + +#[test] +fn python_install_preview() { + let context: TestContext = TestContext::new_with_versions(&[]).with_filtered_python_keys(); + + // Install the latest version + uv_snapshot!(context.filters(), context.python_install().arg("--preview"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Installed Python 3.13.0 in [TIME] + + cpython-3.13.0-[PLATFORM] + "###); + + let bin_python = context + .temp_dir + .child("bin") + .child(format!("python3.13{}", std::env::consts::EXE_SUFFIX)); + + // The executable should be installed in the bin directory + bin_python.assert(predicate::path::exists()); + + // On Unix, it should be a link + #[cfg(unix)] + bin_python.assert(predicate::path::is_symlink()); + + // The executable should "work" + uv_snapshot!(context.filters(), Command::new(bin_python.as_os_str()) + .arg("-c").arg("import subprocess; print('hello world')"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + hello world + + ----- stderr ----- + "###); + + // Should be a no-op when already installed + uv_snapshot!(context.filters(), context.python_install().arg("--preview"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Python is already installed. Use `uv python install ` to install another version. + "###); + + // You can opt-in to a reinstall + uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("--reinstall"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Installed Python 3.13.0 in [TIME] + ~ cpython-3.13.0-[PLATFORM] + "###); + + // The executable should still be present in the bin directory + bin_python.assert(predicate::path::exists()); + + // Uninstallation requires an argument + uv_snapshot!(context.filters(), context.python_uninstall(), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: the following required arguments were not provided: + ... + + Usage: uv python uninstall ... + + For more information, try '--help'. + "###); + + uv_snapshot!(context.filters(), context.python_uninstall().arg("3.13"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Searching for Python versions matching: Python 3.13 + Uninstalled Python 3.13.0 in [TIME] + - cpython-3.13.0-[PLATFORM] + "###); + + // The executable should be removed + bin_python.assert(predicate::path::missing()); +} + +#[test] +fn python_install_freethreaded() { + let context: TestContext = TestContext::new_with_versions(&[]).with_filtered_python_keys(); + + // Install the latest version + uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.13t"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Installed Python 3.13.0 in [TIME] + + cpython-3.13.0+freethreaded-[PLATFORM] + "###); + + let bin_python = context + .temp_dir + .child("bin") + .child(format!("python3.13t{}", std::env::consts::EXE_SUFFIX)); + + // The executable should be installed in the bin directory + bin_python.assert(predicate::path::exists()); + + // On Unix, it should be a link + #[cfg(unix)] + bin_python.assert(predicate::path::is_symlink()); + + // The executable should "work" + uv_snapshot!(context.filters(), Command::new(bin_python.as_os_str()) + .arg("-c").arg("import subprocess; print('hello world')"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + hello world + + ----- stderr ----- + "###); + + // Should be distinct from 3.13 + uv_snapshot!(context.filters(), context.python_install().arg("3.13"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Installed Python 3.13.0 in [TIME] + + cpython-3.13.0-[PLATFORM] + "###); + + // Should not work with older Python versions + uv_snapshot!(context.filters(), context.python_install().arg("3.12t"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: No download found for request: cpython-3.12t-[PLATFORM] + "###); + + uv_snapshot!(context.filters(), context.python_uninstall().arg("--all"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Searching for Python installations + Uninstalled 2 versions in [TIME] + - cpython-3.13.0-[PLATFORM] + - cpython-3.13.0+freethreaded-[PLATFORM] + "###); +} + +#[test] +fn python_install_invalid_request() { + let context: TestContext = TestContext::new_with_versions(&[]).with_filtered_python_keys(); + + // Request something that is not a Python version + uv_snapshot!(context.filters(), context.python_install().arg("foobar"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Cannot download managed Python for request: executable name `foobar` + "###); + + // Request a version we don't have a download for + uv_snapshot!(context.filters(), context.python_install().arg("3.8.0"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: No download found for request: cpython-3.8.0-[PLATFORM] + "###); + + // Request a version we don't have a download for mixed with one we do + uv_snapshot!(context.filters(), context.python_install().arg("3.8.0").arg("3.12"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: No download found for request: cpython-3.8.0-[PLATFORM] + "###); +} diff --git a/crates/uv/tests/it/run.rs b/crates/uv/tests/it/run.rs index f59837fb1705..826982f1b810 100644 --- a/crates/uv/tests/it/run.rs +++ b/crates/uv/tests/it/run.rs @@ -4,6 +4,7 @@ use anyhow::Result; use assert_cmd::assert::OutputAssertExt; use assert_fs::{fixture::ChildPath, prelude::*}; use indoc::indoc; +use insta::assert_snapshot; use predicates::str::contains; use std::path::Path; @@ -415,6 +416,30 @@ fn run_pep723_script() -> Result<()> { "# })?; + // Running a script with `--group` should warn. + uv_snapshot!(context.filters(), context.run().arg("--group").arg("foo").arg("main.py"), @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + Reading inline script metadata from `main.py` + × No solution found when resolving script dependencies: + ╰─▶ Because there are no versions of add and you require add, we can conclude that your requirements are unsatisfiable. + "###); + + // If the script can't be resolved, we should reference the script. + let test_script = context.temp_dir.child("main.py"); + test_script.write_str(indoc! { r#" + # /// script + # requires-python = ">=3.11" + # dependencies = [ + # "add", + # ] + # /// + "# + })?; + uv_snapshot!(context.filters(), context.run().arg("--no-project").arg("main.py"), @r###" success: false exit_code: 1 @@ -924,6 +949,161 @@ fn run_with_editable() -> Result<()> { Ok(()) } +#[test] +fn run_group() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["typing-extensions"] + + [dependency-groups] + foo = ["anyio"] + bar = ["iniconfig"] + dev = ["sniffio"] + "#, + )?; + + let test_script = context.temp_dir.child("main.py"); + test_script.write_str(indoc! { r#" + try: + import anyio + print("imported `anyio`") + except ImportError: + print("failed to import `anyio`") + + try: + import iniconfig + print("imported `iniconfig`") + except ImportError: + print("failed to import `iniconfig`") + + try: + import typing_extensions + print("imported `typing_extensions`") + except ImportError: + print("failed to import `typing_extensions`") + "# + })?; + + context.lock().assert().success(); + + uv_snapshot!(context.filters(), context.run().arg("main.py"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + failed to import `anyio` + failed to import `iniconfig` + imported `typing_extensions` + + ----- stderr ----- + Resolved 6 packages in [TIME] + Prepared 2 packages in [TIME] + Installed 2 packages in [TIME] + + sniffio==1.3.1 + + typing-extensions==4.10.0 + "###); + + uv_snapshot!(context.filters(), context.run().arg("--only-group").arg("bar").arg("main.py"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + failed to import `anyio` + imported `iniconfig` + imported `typing_extensions` + + ----- stderr ----- + Resolved 6 packages in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + iniconfig==2.0.0 + "###); + + uv_snapshot!(context.filters(), context.run().arg("--group").arg("foo").arg("main.py"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + imported `anyio` + imported `iniconfig` + imported `typing_extensions` + + ----- stderr ----- + Resolved 6 packages in [TIME] + Prepared 2 packages in [TIME] + Installed 2 packages in [TIME] + + anyio==4.3.0 + + idna==3.6 + "###); + + uv_snapshot!(context.filters(), context.run().arg("--group").arg("foo").arg("--group").arg("bar").arg("main.py"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + imported `anyio` + imported `iniconfig` + imported `typing_extensions` + + ----- stderr ----- + Resolved 6 packages in [TIME] + Audited 5 packages in [TIME] + "###); + + uv_snapshot!(context.filters(), context.run().arg("--group").arg("foo").arg("--no-project").arg("main.py"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + imported `anyio` + imported `iniconfig` + imported `typing_extensions` + + ----- stderr ----- + warning: `--group foo` has no effect when used alongside `--no-project` + "###); + + uv_snapshot!(context.filters(), context.run().arg("--group").arg("foo").arg("--group").arg("bar").arg("--no-project").arg("main.py"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + imported `anyio` + imported `iniconfig` + imported `typing_extensions` + + ----- stderr ----- + warning: `--group` has no effect when used alongside `--no-project` + "###); + + uv_snapshot!(context.filters(), context.run().arg("--group").arg("dev").arg("--no-project").arg("main.py"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + imported `anyio` + imported `iniconfig` + imported `typing_extensions` + + ----- stderr ----- + warning: `--group dev` has no effect when used alongside `--no-project` + "###); + + uv_snapshot!(context.filters(), context.run().arg("--dev").arg("--no-project").arg("main.py"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + imported `anyio` + imported `iniconfig` + imported `typing_extensions` + + ----- stderr ----- + warning: `--dev` has no effect when used alongside `--no-project` + "###); + + Ok(()) +} + #[test] fn run_locked() -> Result<()> { let context = TestContext::new("3.12"); @@ -958,6 +1138,62 @@ fn run_locked() -> Result<()> { let existing = context.read("uv.lock"); + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + existing, @r###" + version = 1 + requires-python = ">=3.12" + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [[package]] + name = "anyio" + version = "3.7.0" + source = { registry = "https://pypi.org/simple" } + dependencies = [ + { name = "idna" }, + { name = "sniffio" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/c6/b3/fefbf7e78ab3b805dec67d698dc18dd505af7a18a8dd08868c9b4fa736b5/anyio-3.7.0.tar.gz", hash = "sha256:275d9973793619a5374e1c89a4f4ad3f4b0a5510a2b5b939444bee8f4c4d37ce", size = 142737 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/68/fe/7ce1926952c8a403b35029e194555558514b365ad77d75125f521a2bec62/anyio-3.7.0-py3-none-any.whl", hash = "sha256:eddca883c4175f14df8aedce21054bfca3adb70ffe76a9f607aef9d7fa2ea7f0", size = 80873 }, + ] + + [[package]] + name = "idna" + version = "3.6" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567 }, + ] + + [[package]] + name = "project" + version = "0.1.0" + source = { editable = "." } + dependencies = [ + { name = "anyio" }, + ] + + [package.metadata] + requires-dist = [{ name = "anyio", specifier = "==3.7.0" }] + + [[package]] + name = "sniffio" + version = "1.3.1" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, + ] + "###); + } + ); + // Update the requirements. pyproject_toml.write_str( r#" @@ -990,7 +1226,28 @@ fn run_locked() -> Result<()> { assert_eq!(existing, updated); // Lock the updated requirements. - context.lock().assert().success(); + uv_snapshot!(context.lock(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + Removed anyio v3.7.0 + Removed idna v3.6 + Added iniconfig v2.0.0 + Removed sniffio v1.3.1 + "###); + + // Lock the updated requirements. + uv_snapshot!(context.lock(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + "###); // Running with `--locked` should succeed. uv_snapshot!(context.filters(), context.run().arg("--locked").arg("--").arg("python").arg("--version"), @r###" @@ -1417,7 +1674,7 @@ fn run_editable() -> Result<()> { #[test] fn run_from_directory() -> Result<()> { - // default 3.11 so that the .python-version is meaningful + // Default to 3.11 so that the `.python-version` is meaningful. let context = TestContext::new_with_versions(&["3.10", "3.11", "3.12"]); let project_dir = context.temp_dir.child("project"); @@ -1488,6 +1745,7 @@ fn run_from_directory() -> Result<()> { + foo==1.0.0 (from file://[TEMP_DIR]/project) "###); + fs_err::remove_dir_all(context.temp_dir.join("project").join(".venv"))?; uv_snapshot!(filters.clone(), context.run().arg("--project").arg("project").arg("./project/main.py"), @r###" success: true exit_code: 0 @@ -1495,11 +1753,15 @@ fn run_from_directory() -> Result<()> { ----- stderr ----- warning: `VIRTUAL_ENV=.venv` does not match the project environment path `[PROJECT_VENV]/` and will be ignored + Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] + Creating virtual environment at: [PROJECT_VENV]/ Resolved 1 package in [TIME] - Audited 1 package in [TIME] + Installed 1 package in [TIME] + + foo==1.0.0 (from file://[TEMP_DIR]/project) "###); // Use `--directory`, which switches to the provided directory entirely. + fs_err::remove_dir_all(context.temp_dir.join("project").join(".venv"))?; uv_snapshot!(filters.clone(), context.run().arg("--directory").arg("project").arg("main"), @r###" success: true exit_code: 0 @@ -1508,10 +1770,14 @@ fn run_from_directory() -> Result<()> { ----- stderr ----- warning: `VIRTUAL_ENV=[VENV]/` does not match the project environment path `.venv` and will be ignored + Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] + Creating virtual environment at: .venv Resolved 1 package in [TIME] - Audited 1 package in [TIME] + Installed 1 package in [TIME] + + foo==1.0.0 (from file://[TEMP_DIR]/project) "###); + fs_err::remove_dir_all(context.temp_dir.join("project").join(".venv"))?; uv_snapshot!(filters.clone(), context.run().arg("--directory").arg("project").arg("./main.py"), @r###" success: true exit_code: 0 @@ -1519,10 +1785,14 @@ fn run_from_directory() -> Result<()> { ----- stderr ----- warning: `VIRTUAL_ENV=[VENV]/` does not match the project environment path `.venv` and will be ignored + Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] + Creating virtual environment at: .venv Resolved 1 package in [TIME] - Audited 1 package in [TIME] + Installed 1 package in [TIME] + + foo==1.0.0 (from file://[TEMP_DIR]/project) "###); + fs_err::remove_dir_all(context.temp_dir.join("project").join(".venv"))?; uv_snapshot!(filters.clone(), context.run().arg("--directory").arg("project").arg("./project/main.py"), @r###" success: false exit_code: 2 @@ -1530,8 +1800,11 @@ fn run_from_directory() -> Result<()> { ----- stderr ----- warning: `VIRTUAL_ENV=[VENV]/` does not match the project environment path `.venv` and will be ignored + Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] + Creating virtual environment at: .venv Resolved 1 package in [TIME] - Audited 1 package in [TIME] + Installed 1 package in [TIME] + + foo==1.0.0 (from file://[TEMP_DIR]/project) error: Failed to spawn: `./project/main.py` Caused by: No such file or directory (os error 2) "###); @@ -1547,6 +1820,7 @@ fn run_from_directory() -> Result<()> { .child(PYTHON_VERSION_FILENAME) .write_str("3.10")?; + fs_err::remove_dir_all(context.temp_dir.join("project").join(".venv"))?; uv_snapshot!(filters.clone(), context.run().arg("--project").arg("project").arg("main"), @r###" success: true exit_code: 0 @@ -1556,13 +1830,13 @@ fn run_from_directory() -> Result<()> { ----- stderr ----- warning: `VIRTUAL_ENV=.venv` does not match the project environment path `[PROJECT_VENV]/` and will be ignored Using CPython 3.10.[X] interpreter at: [PYTHON-3.10] - Removed virtual environment at: [PROJECT_VENV]/ Creating virtual environment at: [PROJECT_VENV]/ Resolved 1 package in [TIME] Installed 1 package in [TIME] + foo==1.0.0 (from file://[TEMP_DIR]/project) "###); + fs_err::remove_dir_all(context.temp_dir.join("project").join(".venv"))?; uv_snapshot!(filters.clone(), context.run().arg("--directory").arg("project").arg("main"), @r###" success: true exit_code: 0 @@ -1571,8 +1845,11 @@ fn run_from_directory() -> Result<()> { ----- stderr ----- warning: `VIRTUAL_ENV=[VENV]/` does not match the project environment path `.venv` and will be ignored + Using CPython 3.10.[X] interpreter at: [PYTHON-3.10] + Creating virtual environment at: .venv Resolved 1 package in [TIME] - Audited 1 package in [TIME] + Installed 1 package in [TIME] + + foo==1.0.0 (from file://[TEMP_DIR]/project) "###); Ok(()) diff --git a/crates/uv/tests/it/show_settings.rs b/crates/uv/tests/it/show_settings.rs index da2d38a32c34..4275db0ac491 100644 --- a/crates/uv/tests/it/show_settings.rs +++ b/crates/uv/tests/it/show_settings.rs @@ -2606,6 +2606,7 @@ fn resolve_tool() -> anyhow::Result<()> { from: None, with: [], with_requirements: [], + with_editable: [], python: None, refresh: None( Timestamp( @@ -3229,8 +3230,7 @@ fn resolve_config_file() -> anyhow::Result<()> { | 1 | [project] | ^^^^^^^ - unknown field `project`, expected one of `native-tls`, `offline`, `no-cache`, `cache-dir`, `preview`, `python-preference`, `python-downloads`, `concurrent-downloads`, `concurrent-builds`, `concurrent-installs`, `index`, `index-url`, `extra-index-url`, `no-index`, `find-links`, `index-strategy`, `keyring-provider`, `allow-insecure-host`, `resolution`, `prerelease`, `dependency-metadata`, `config-settings`, `no-build-isolation`, `no-build-isolation-package`, `exclude-newer`, `link-mode`, `compile-bytecode`, `no-sources`, `upgrade`, `upgrade-package`, `reinstall`, `reinstall-package`, `no-build`, `no-build-package`, `no-binary`, `no-binary-package`, `publish-url`, `trusted-publishing`, `pip`, `cache-keys`, `override-dependencies`, `constraint-dependencies`, `environments`, `workspace`, `sources`, `dev-dependencies`, `managed`, `package` - + unknown field `project`, expected one of `native-tls`, `offline`, `no-cache`, `cache-dir`, `preview`, `python-preference`, `python-downloads`, `concurrent-downloads`, `concurrent-builds`, `concurrent-installs`, `index`, `index-url`, `extra-index-url`, `no-index`, `find-links`, `index-strategy`, `keyring-provider`, `allow-insecure-host`, `resolution`, `prerelease`, `dependency-metadata`, `config-settings`, `no-build-isolation`, `no-build-isolation-package`, `exclude-newer`, `link-mode`, `compile-bytecode`, `no-sources`, `upgrade`, `upgrade-package`, `reinstall`, `reinstall-package`, `no-build`, `no-build-package`, `no-binary`, `no-binary-package`, `publish-url`, `trusted-publishing`, `pip`, `cache-keys`, `override-dependencies`, `constraint-dependencies`, `environments`, `workspace`, `sources`, `managed`, `package`, `default-groups`, `dev-dependencies` "### ); diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index 095c0b4fee43..7725743af7bc 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -383,9 +383,9 @@ fn mixed_requires_python() -> Result<()> { Ok(()) } -/// Sync development dependencies in a virtual workspace root. +/// Sync development dependencies in a (legacy) non-project workspace root. #[test] -fn virtual_workspace_dev_dependencies() -> Result<()> { +fn sync_legacy_non_project_dev_dependencies() -> Result<()> { let context = TestContext::new("3.12"); let pyproject_toml = context.temp_dir.child("pyproject.toml"); @@ -467,6 +467,125 @@ fn virtual_workspace_dev_dependencies() -> Result<()> { Ok(()) } +/// Sync development dependencies in a (legacy) non-project workspace root. +#[test] +fn sync_legacy_non_project_group() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [dependency-groups] + foo = ["anyio"] + bar = ["typing-extensions"] + + [tool.uv.workspace] + members = ["child"] + "#, + )?; + + let src = context.temp_dir.child("src").child("albatross"); + src.create_dir_all()?; + + let init = src.child("__init__.py"); + init.touch()?; + + let child = context.temp_dir.child("child"); + fs_err::create_dir_all(&child)?; + + let pyproject_toml = child.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "child" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["iniconfig>1"] + + [dependency-groups] + baz = ["typing-extensions"] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#, + )?; + + let src = child.child("src").child("albatross"); + src.create_dir_all()?; + + let init = src.child("__init__.py"); + init.touch()?; + + uv_snapshot!(context.filters(), context.sync(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 6 packages in [TIME] + Prepared 2 packages in [TIME] + Installed 2 packages in [TIME] + + child==0.1.0 (from file://[TEMP_DIR]/child) + + iniconfig==2.0.0 + "###); + + uv_snapshot!(context.filters(), context.sync().arg("--group").arg("foo"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 6 packages in [TIME] + Prepared 3 packages in [TIME] + Installed 3 packages in [TIME] + + anyio==4.3.0 + + idna==3.6 + + sniffio==1.3.1 + "###); + + uv_snapshot!(context.filters(), context.sync().arg("--only-group").arg("bar"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 6 packages in [TIME] + Prepared 1 package in [TIME] + Uninstalled 5 packages in [TIME] + Installed 1 package in [TIME] + - anyio==4.3.0 + - child==0.1.0 (from file://[TEMP_DIR]/child) + - idna==3.6 + - iniconfig==2.0.0 + - sniffio==1.3.1 + + typing-extensions==4.10.0 + "###); + + uv_snapshot!(context.filters(), context.sync().arg("--group").arg("baz"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 6 packages in [TIME] + Installed 2 packages in [TIME] + + child==0.1.0 (from file://[TEMP_DIR]/child) + + iniconfig==2.0.0 + "###); + + uv_snapshot!(context.filters(), context.sync().arg("--group").arg("bop"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Group `bop` is not defined in any project's `dependency-group` table + "###); + + Ok(()) +} + /// Use a `pip install` step to pre-install build dependencies for `--no-build-isolation`. #[test] fn sync_build_isolation() -> Result<()> { @@ -1012,6 +1131,553 @@ fn sync_dev() -> Result<()> { Ok(()) } +#[test] +fn sync_group() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["typing-extensions"] + + [dependency-groups] + foo = ["anyio"] + bar = ["requests"] + "#, + )?; + + context.lock().assert().success(); + + uv_snapshot!(context.filters(), context.sync(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 9 packages in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + typing-extensions==4.10.0 + "###); + + uv_snapshot!(context.filters(), context.sync().arg("--group").arg("foo"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 9 packages in [TIME] + Prepared 3 packages in [TIME] + Installed 3 packages in [TIME] + + anyio==4.3.0 + + idna==3.6 + + sniffio==1.3.1 + "###); + + uv_snapshot!(context.filters(), context.sync().arg("--only-group").arg("bar"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 9 packages in [TIME] + Prepared 4 packages in [TIME] + Uninstalled 3 packages in [TIME] + Installed 4 packages in [TIME] + - anyio==4.3.0 + + certifi==2024.2.2 + + charset-normalizer==3.3.2 + + requests==2.31.0 + - sniffio==1.3.1 + - typing-extensions==4.10.0 + + urllib3==2.2.1 + "###); + + uv_snapshot!(context.filters(), context.sync().arg("--group").arg("foo").arg("--group").arg("bar"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 9 packages in [TIME] + Installed 3 packages in [TIME] + + anyio==4.3.0 + + sniffio==1.3.1 + + typing-extensions==4.10.0 + "###); + + Ok(()) +} + +#[test] +fn sync_include_group() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["typing-extensions"] + + [dependency-groups] + foo = ["anyio", {include-group = "bar"}] + bar = ["iniconfig"] + "#, + )?; + + context.lock().assert().success(); + + uv_snapshot!(context.filters(), context.sync(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 6 packages in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + typing-extensions==4.10.0 + "###); + + uv_snapshot!(context.filters(), context.sync().arg("--group").arg("foo"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 6 packages in [TIME] + Prepared 4 packages in [TIME] + Installed 4 packages in [TIME] + + anyio==4.3.0 + + idna==3.6 + + iniconfig==2.0.0 + + sniffio==1.3.1 + "###); + + uv_snapshot!(context.filters(), context.sync().arg("--only-group").arg("bar"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 6 packages in [TIME] + Uninstalled 4 packages in [TIME] + - anyio==4.3.0 + - idna==3.6 + - sniffio==1.3.1 + - typing-extensions==4.10.0 + "###); + + uv_snapshot!(context.filters(), context.sync().arg("--group").arg("foo").arg("--group").arg("bar"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 6 packages in [TIME] + Installed 4 packages in [TIME] + + anyio==4.3.0 + + idna==3.6 + + sniffio==1.3.1 + + typing-extensions==4.10.0 + "###); + + uv_snapshot!(context.filters(), context.sync().arg("--only-group").arg("foo"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 6 packages in [TIME] + Uninstalled 1 package in [TIME] + - typing-extensions==4.10.0 + "###); + + Ok(()) +} + +#[test] +fn sync_exclude_group() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["typing-extensions"] + + [dependency-groups] + foo = ["anyio", {include-group = "bar"}] + bar = ["iniconfig"] + "#, + )?; + + context.lock().assert().success(); + + uv_snapshot!(context.filters(), context.sync().arg("--group").arg("foo"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 6 packages in [TIME] + Prepared 5 packages in [TIME] + Installed 5 packages in [TIME] + + anyio==4.3.0 + + idna==3.6 + + iniconfig==2.0.0 + + sniffio==1.3.1 + + typing-extensions==4.10.0 + "###); + + uv_snapshot!(context.filters(), context.sync().arg("--group").arg("foo").arg("--no-group").arg("foo"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 6 packages in [TIME] + Uninstalled 4 packages in [TIME] + - anyio==4.3.0 + - idna==3.6 + - iniconfig==2.0.0 + - sniffio==1.3.1 + "###); + + uv_snapshot!(context.filters(), context.sync().arg("--only-group").arg("bar"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 6 packages in [TIME] + Uninstalled 1 package in [TIME] + Installed 1 package in [TIME] + + iniconfig==2.0.0 + - typing-extensions==4.10.0 + "###); + + uv_snapshot!(context.filters(), context.sync().arg("--only-group").arg("bar").arg("--no-group").arg("bar"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 6 packages in [TIME] + Uninstalled 1 package in [TIME] + - iniconfig==2.0.0 + "###); + + Ok(()) +} + +#[test] +fn sync_dev_group() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["typing-extensions"] + + [tool.uv] + dev-dependencies = ["anyio"] + + [dependency-groups] + dev = ["iniconfig"] + "#, + )?; + + context.lock().assert().success(); + + uv_snapshot!(context.filters(), context.sync(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 6 packages in [TIME] + Prepared 5 packages in [TIME] + Installed 5 packages in [TIME] + + anyio==4.3.0 + + idna==3.6 + + iniconfig==2.0.0 + + sniffio==1.3.1 + + typing-extensions==4.10.0 + "###); + + Ok(()) +} + +#[test] +fn sync_non_existent_group() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["typing-extensions"] + + [dependency-groups] + foo = [] + bar = ["requests"] + "#, + )?; + + context.lock().assert().success(); + + // Requesting a non-existent group should fail. + uv_snapshot!(context.filters(), context.sync().arg("--group").arg("baz"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Group `baz` is not defined in the project's `dependency-group` table + "###); + + uv_snapshot!(context.filters(), context.sync().arg("--no-group").arg("baz"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Group `baz` is not defined in the project's `dependency-group` table + "###); + + // Requesting an empty group should succeed. + uv_snapshot!(context.filters(), context.sync().arg("--group").arg("foo"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 7 packages in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + typing-extensions==4.10.0 + "###); + + Ok(()) +} + +#[test] +fn sync_non_existent_default_group() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["typing-extensions"] + + [dependency-groups] + foo = [] + + [tool.uv] + default-groups = ["bar"] + "#, + )?; + + context.lock().assert().success(); + + uv_snapshot!(context.filters(), context.sync(), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Default group `bar` (from `tool.uv.default-groups`) is not defined in the project's `dependency-group` table + "###); + + Ok(()) +} + +#[test] +fn sync_default_groups() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["typing-extensions"] + + [dependency-groups] + dev = ["iniconfig"] + foo = ["anyio"] + bar = ["requests"] + "#, + )?; + + context.lock().assert().success(); + + // The `dev` group should be synced by default. + uv_snapshot!(context.filters(), context.sync(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 10 packages in [TIME] + Prepared 2 packages in [TIME] + Installed 2 packages in [TIME] + + iniconfig==2.0.0 + + typing-extensions==4.10.0 + "###); + + // If we remove it from the `default-groups` list, it should be removed. + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["typing-extensions"] + + [dependency-groups] + dev = ["iniconfig"] + foo = ["anyio"] + bar = ["requests"] + + [tool.uv] + default-groups = [] + "#, + )?; + + uv_snapshot!(context.filters(), context.sync(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 10 packages in [TIME] + Uninstalled 1 package in [TIME] + - iniconfig==2.0.0 + "###); + + // If we set a different default group, it should be synced instead. + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["typing-extensions"] + + [dependency-groups] + dev = ["iniconfig"] + foo = ["anyio"] + bar = ["requests"] + + [tool.uv] + default-groups = ["foo"] + "#, + )?; + + uv_snapshot!(context.filters(), context.sync(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 10 packages in [TIME] + Prepared 3 packages in [TIME] + Installed 3 packages in [TIME] + + anyio==4.3.0 + + idna==3.6 + + sniffio==1.3.1 + "###); + + // `--no-group` should remove from the defaults. + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["typing-extensions"] + + [dependency-groups] + dev = ["iniconfig"] + foo = ["anyio"] + bar = ["requests"] + + [tool.uv] + default-groups = ["foo"] + "#, + )?; + + uv_snapshot!(context.filters(), context.sync().arg("--no-group").arg("foo"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 10 packages in [TIME] + Uninstalled 3 packages in [TIME] + - anyio==4.3.0 + - idna==3.6 + - sniffio==1.3.1 + "###); + + // Using `--group` should include the defaults + uv_snapshot!(context.filters(), context.sync().arg("--group").arg("dev"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 10 packages in [TIME] + Installed 4 packages in [TIME] + + anyio==4.3.0 + + idna==3.6 + + iniconfig==2.0.0 + + sniffio==1.3.1 + "###); + + // Using `--only-group` should exclude the defaults + uv_snapshot!(context.filters(), context.sync().arg("--only-group").arg("dev"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 10 packages in [TIME] + Uninstalled 4 packages in [TIME] + - anyio==4.3.0 + - idna==3.6 + - sniffio==1.3.1 + - typing-extensions==4.10.0 + "###); + + Ok(()) +} + /// Regression test for . /// /// Previously, we would read metadata statically from pyproject.toml and write that to `uv.lock`. In @@ -1743,7 +2409,7 @@ fn sync_custom_environment_path() -> Result<()> { ----- stdout ----- ----- stderr ----- - error: Project virtual environment directory `[TEMP_DIR]/foo` cannot be used because because it is not a valid Python environment (no Python executable was found) + error: Project virtual environment directory `[TEMP_DIR]/foo` cannot be used because it is not a valid Python environment (no Python executable was found) "###); // But if it's just an incompatible virtual environment... @@ -1912,9 +2578,9 @@ fn sync_workspace_custom_environment_path() -> Result<()> { Ok(()) } -// Test for warnings when `VIRTUAL_ENV` is set but will not be respected. +/// Test for warnings when `VIRTUAL_ENV` is set but will not be respected. #[test] -fn sync_virtual_env_warning() -> Result<()> { +fn sync_legacy_non_project_warning() -> Result<()> { let context = TestContext::new("3.12"); let pyproject_toml = context.temp_dir.child("pyproject.toml"); @@ -2912,7 +3578,7 @@ fn sync_invalid_environment() -> Result<()> { ----- stdout ----- ----- stderr ----- - error: Project virtual environment directory `[VENV]/` cannot be used because because it is not a valid Python environment (no Python executable was found) + error: Project virtual environment directory `[VENV]/` cannot be used because it is not a valid Python environment (no Python executable was found) "###); // But if it's just an incompatible virtual environment... @@ -2978,7 +3644,7 @@ fn sync_invalid_environment() -> Result<()> { ----- stdout ----- ----- stderr ----- - error: Project virtual environment directory `[VENV]/` cannot be used because because it is not a valid Python environment (no Python executable was found) + error: Project virtual environment directory `[VENV]/` cannot be used because it is not a valid Python environment (no Python executable was found) "###); // But if it's not a virtual environment... diff --git a/crates/uv/tests/it/tool_install.rs b/crates/uv/tests/it/tool_install.rs index c827e3bc9ebc..380e3c43238e 100644 --- a/crates/uv/tests/it/tool_install.rs +++ b/crates/uv/tests/it/tool_install.rs @@ -11,7 +11,7 @@ use predicates::prelude::predicate; use uv_static::EnvVars; -use crate::common::{uv_snapshot, TestContext}; +use crate::common::{copy_dir_all, uv_snapshot, TestContext}; #[test] fn tool_install() { @@ -171,6 +171,51 @@ fn tool_install() { }); } +#[test] +fn tool_install_with_editable() -> anyhow::Result<()> { + let context = TestContext::new("3.12") + .with_filtered_counts() + .with_filtered_exe_suffix(); + let tool_dir = context.temp_dir.child("tools"); + let bin_dir = context.temp_dir.child("bin"); + let anyio_local = context.temp_dir.child("src").child("anyio_local"); + copy_dir_all( + context.workspace_root.join("scripts/packages/anyio_local"), + &anyio_local, + )?; + + uv_snapshot!(context.filters(), context.tool_install() + .arg("--with-editable") + .arg("./src/anyio_local") + .arg("--with") + .arg("iniconfig") + .arg("flask") + .env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str()) + .env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str()) + .env(EnvVars::PATH, bin_dir.as_os_str()), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved [N] packages in [TIME] + Prepared [N] packages in [TIME] + Installed [N] packages in [TIME] + + anyio==4.3.0+foo (from file://[TEMP_DIR]/src/anyio_local) + + blinker==1.7.0 + + click==8.1.7 + + flask==3.0.2 + + iniconfig==2.0.0 + + itsdangerous==2.1.2 + + jinja2==3.1.3 + + markupsafe==2.1.5 + + werkzeug==3.0.1 + Installed 1 executable: flask + "###); + + Ok(()) +} + #[test] fn tool_install_suggest_other_packages_with_executable() { let context = TestContext::new("3.12").with_filtered_exe_suffix(); diff --git a/crates/uv/tests/it/tool_run.rs b/crates/uv/tests/it/tool_run.rs index f8365caa2f44..7426a4ebc850 100644 --- a/crates/uv/tests/it/tool_run.rs +++ b/crates/uv/tests/it/tool_run.rs @@ -141,6 +141,7 @@ fn tool_run_at_version() { The following executables are provided by `pytest`: - py.test - pytest + Consider using `uv tool run --from pytest ` instead. ----- stderr ----- Resolved 4 packages in [TIME] @@ -202,6 +203,7 @@ fn tool_run_suggest_valid_commands() { The following executables are provided by `black`: - black - blackd + Consider using `uv tool run --from black ` instead. ----- stderr ----- Resolved 6 packages in [TIME] diff --git a/crates/uv/tests/it/tree.rs b/crates/uv/tests/it/tree.rs index a91461f68b81..c375b11a0cd3 100644 --- a/crates/uv/tests/it/tree.rs +++ b/crates/uv/tests/it/tree.rs @@ -1,9 +1,11 @@ -use crate::common::{uv_snapshot, TestContext}; use anyhow::Result; +use assert_cmd::assert::OutputAssertExt; use assert_fs::prelude::*; use indoc::formatdoc; use url::Url; +use crate::common::{uv_snapshot, TestContext}; + #[test] fn nested_dependencies() -> Result<()> { let context = TestContext::new("3.12"); @@ -14,7 +16,6 @@ fn nested_dependencies() -> Result<()> { [project] name = "project" version = "0.1.0" - # ... requires-python = ">=3.12" dependencies = [ "scikit-learn==1.4.1.post1" @@ -46,6 +47,73 @@ fn nested_dependencies() -> Result<()> { Ok(()) } +#[test] +fn nested_platform_dependencies() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [ + "jupyter-client" + ] + "#, + )?; + + uv_snapshot!(context.filters(), context.tree().arg("--python-platform").arg("linux"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + project v0.1.0 + └── jupyter-client v8.6.1 + ├── jupyter-core v5.7.2 + │ ├── platformdirs v4.2.0 + │ └── traitlets v5.14.2 + ├── python-dateutil v2.9.0.post0 + │ └── six v1.16.0 + ├── pyzmq v25.1.2 + ├── tornado v6.4 + └── traitlets v5.14.2 + + ----- stderr ----- + Resolved 12 packages in [TIME] + "### + ); + + uv_snapshot!(context.filters(), context.tree().arg("--universal"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + project v0.1.0 + └── jupyter-client v8.6.1 + ├── jupyter-core v5.7.2 + │ ├── platformdirs v4.2.0 + │ ├── pywin32 v306 + │ └── traitlets v5.14.2 + ├── python-dateutil v2.9.0.post0 + │ └── six v1.16.0 + ├── pyzmq v25.1.2 + │ └── cffi v1.16.0 + │ └── pycparser v2.21 + ├── tornado v6.4 + └── traitlets v5.14.2 + + ----- stderr ----- + Resolved 12 packages in [TIME] + "### + ); + + // `uv tree` should update the lockfile + let lock = context.read("uv.lock"); + assert!(!lock.is_empty()); + + Ok(()) +} + #[test] fn invert() -> Result<()> { let context = TestContext::new("3.12"); @@ -56,7 +124,6 @@ fn invert() -> Result<()> { [project] name = "project" version = "0.1.0" - # ... requires-python = ">=3.12" dependencies = [ "scikit-learn==1.4.1.post1" @@ -119,7 +186,6 @@ fn frozen() -> Result<()> { [project] name = "project" version = "0.1.0" - # ... requires-python = ">=3.12" dependencies = ["anyio"] "#, @@ -150,7 +216,6 @@ fn frozen() -> Result<()> { [project] name = "project" version = "0.1.0" - # ... requires-python = ">=3.12" dependencies = ["iniconfig"] "#, @@ -183,7 +248,6 @@ fn platform_dependencies() -> Result<()> { [project] name = "project" version = "0.1.0" - # ... requires-python = ">=3.12" dependencies = [ "black" @@ -272,8 +336,7 @@ fn platform_dependencies_inverted() -> Result<()> { )?; // When `--universal` is _not_ provided, `colorama` should _not_ be included. - #[cfg(not(windows))] - uv_snapshot!(context.filters(), context.tree().arg("--invert"), @r#" + uv_snapshot!(context.filters(), context.tree().arg("--invert").arg("--python-platform").arg("linux"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -282,7 +345,7 @@ fn platform_dependencies_inverted() -> Result<()> { ----- stderr ----- Resolved 3 packages in [TIME] - "#); + "###); // Unless `--python-platform` is set to `windows`, in which case it should be included. uv_snapshot!(context.filters(), context.tree().arg("--invert").arg("--python-platform").arg("windows"), @r#" @@ -310,7 +373,6 @@ fn repeated_dependencies() -> Result<()> { [project] name = "project" version = "0.1.0" - # ... requires-python = ">=3.12" dependencies = [ "anyio < 2 ; sys_platform == 'win32'", @@ -429,7 +491,6 @@ fn dev_dependencies() -> Result<()> { [project] name = "project" version = "0.1.0" - # ... requires-python = ">=3.12" dependencies = ["iniconfig"] @@ -482,7 +543,6 @@ fn dev_dependencies_inverted() -> Result<()> { [project] name = "project" version = "0.1.0" - # ... requires-python = ">=3.12" dependencies = ["iniconfig"] [tool.uv] @@ -525,7 +585,6 @@ fn optional_dependencies() -> Result<()> { [project] name = "project" version = "0.1.0" - # ... requires-python = ">=3.12" dependencies = ["iniconfig", "flask[dotenv]"] @@ -576,7 +635,6 @@ fn optional_dependencies_inverted() -> Result<()> { [project] name = "project" version = "0.1.0" - # ... requires-python = ">=3.12" dependencies = ["iniconfig", "flask[dotenv]"] @@ -624,3 +682,320 @@ fn optional_dependencies_inverted() -> Result<()> { Ok(()) } + +#[test] +fn package() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["scikit-learn==1.4.1.post1", "pandas"] + "#, + )?; + + uv_snapshot!(context.filters(), context.tree(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + project v0.1.0 + ├── pandas v2.2.1 + │ ├── numpy v1.26.4 + │ ├── python-dateutil v2.9.0.post0 + │ │ └── six v1.16.0 + │ ├── pytz v2024.1 + │ └── tzdata v2024.1 + └── scikit-learn v1.4.1.post1 + ├── joblib v1.3.2 + ├── numpy v1.26.4 + ├── scipy v1.12.0 + │ └── numpy v1.26.4 + └── threadpoolctl v3.4.0 + + ----- stderr ----- + Resolved 11 packages in [TIME] + "### + ); + + uv_snapshot!(context.filters(), context.tree().arg("--package").arg("scipy"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + scipy v1.12.0 + └── numpy v1.26.4 + + ----- stderr ----- + Resolved 11 packages in [TIME] + "### + ); + + uv_snapshot!(context.filters(), context.tree().arg("--package").arg("numpy").arg("--invert"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + numpy v1.26.4 + ├── pandas v2.2.1 + │ └── project v0.1.0 + ├── scikit-learn v1.4.1.post1 + │ └── project v0.1.0 + └── scipy v1.12.0 + └── scikit-learn v1.4.1.post1 (*) + (*) Package tree already displayed + + ----- stderr ----- + Resolved 11 packages in [TIME] + "### + ); + + // `uv tree` should update the lockfile + let lock = context.read("uv.lock"); + assert!(!lock.is_empty()); + + Ok(()) +} + +#[test] +fn group() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["typing-extensions"] + + [dependency-groups] + foo = ["anyio"] + bar = ["iniconfig"] + dev = ["sniffio"] + "#, + )?; + + context.lock().assert().success(); + + uv_snapshot!(context.filters(), context.tree(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + project v0.1.0 + ├── typing-extensions v4.10.0 + └── sniffio v1.3.1 (group: dev) + + ----- stderr ----- + Resolved 6 packages in [TIME] + "###); + + uv_snapshot!(context.filters(), context.tree().arg("--only-group").arg("bar"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + project v0.1.0 + └── iniconfig v2.0.0 (group: bar) + + ----- stderr ----- + Resolved 6 packages in [TIME] + "###); + + uv_snapshot!(context.filters(), context.tree().arg("--group").arg("foo"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + project v0.1.0 + ├── typing-extensions v4.10.0 + ├── sniffio v1.3.1 (group: dev) + └── anyio v4.3.0 (group: foo) + ├── idna v3.6 + └── sniffio v1.3.1 + + ----- stderr ----- + Resolved 6 packages in [TIME] + "###); + + uv_snapshot!(context.filters(), context.tree().arg("--group").arg("foo").arg("--group").arg("bar"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + project v0.1.0 + ├── typing-extensions v4.10.0 + ├── iniconfig v2.0.0 (group: bar) + ├── sniffio v1.3.1 (group: dev) + └── anyio v4.3.0 (group: foo) + ├── idna v3.6 + └── sniffio v1.3.1 + + ----- stderr ----- + Resolved 6 packages in [TIME] + "###); + + Ok(()) +} + +#[test] +fn cycle() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["testtools==2.3.0", "fixtures==3.0.0"] + "#, + )?; + + uv_snapshot!(context.filters(), context.tree().arg("--universal"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + project v0.1.0 + ├── fixtures v3.0.0 + │ ├── pbr v6.0.0 + │ ├── six v1.16.0 + │ └── testtools v2.3.0 + │ ├── extras v1.0.0 + │ ├── fixtures v3.0.0 (*) + │ ├── pbr v6.0.0 + │ ├── python-mimeparse v1.6.0 + │ ├── six v1.16.0 + │ ├── traceback2 v1.4.0 + │ │ └── linecache2 v1.0.0 + │ └── unittest2 v1.1.0 + │ ├── argparse v1.4.0 + │ ├── six v1.16.0 + │ └── traceback2 v1.4.0 (*) + └── testtools v2.3.0 (*) + (*) Package tree already displayed + + ----- stderr ----- + Resolved 11 packages in [TIME] + "### + ); + + uv_snapshot!(context.filters(), context.tree().arg("--package").arg("traceback2").arg("--package").arg("six"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + six v1.16.0 + traceback2 v1.4.0 + └── linecache2 v1.0.0 + + ----- stderr ----- + Resolved 11 packages in [TIME] + "### + ); + + uv_snapshot!(context.filters(), context.tree().arg("--package").arg("traceback2").arg("--package").arg("six").arg("--invert"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + six v1.16.0 + ├── fixtures v3.0.0 + │ ├── project v0.1.0 + │ └── testtools v2.3.0 + │ ├── fixtures v3.0.0 (*) + │ └── project v0.1.0 + ├── testtools v2.3.0 (*) + └── unittest2 v1.1.0 + └── testtools v2.3.0 (*) + traceback2 v1.4.0 + ├── testtools v2.3.0 (*) + └── unittest2 v1.1.0 (*) + (*) Package tree already displayed + + ----- stderr ----- + Resolved 11 packages in [TIME] + "### + ); + + // `uv tree` should update the lockfile + let lock = context.read("uv.lock"); + assert!(!lock.is_empty()); + + Ok(()) +} + +#[test] +fn workspace_dev() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["anyio"] + + [dependency-groups] + dev = ["child"] + + [tool.uv.workspace] + members = ["child"] + + [tool.uv.sources] + child = { workspace = true } + "#, + )?; + + let child = context.temp_dir.child("child"); + let pyproject_toml = child.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "child" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["iniconfig"] + "#, + )?; + + uv_snapshot!(context.filters(), context.tree().arg("--universal"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + project v0.1.0 + ├── anyio v4.3.0 + │ ├── idna v3.6 + │ └── sniffio v1.3.1 + └── child v0.1.0 (group: dev) + └── iniconfig v2.0.0 + + ----- stderr ----- + Resolved 6 packages in [TIME] + "### + ); + + // Under `--no-dev`, the member should still be included, since we show the entire workspace. + // But it shouldn't be considered a dependency of the root. + uv_snapshot!(context.filters(), context.tree().arg("--universal").arg("--no-dev"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + child v0.1.0 + └── iniconfig v2.0.0 + project v0.1.0 + └── anyio v4.3.0 + ├── idna v3.6 + └── sniffio v1.3.1 + + ----- stderr ----- + Resolved 6 packages in [TIME] + "### + ); + + // `uv tree` should update the lockfile + let lock = context.read("uv.lock"); + assert!(!lock.is_empty()); + + Ok(()) +} diff --git a/crates/uv/tests/it/workspace.rs b/crates/uv/tests/it/workspace.rs index b585ab36ea8f..e39721e93d8e 100644 --- a/crates/uv/tests/it/workspace.rs +++ b/crates/uv/tests/it/workspace.rs @@ -708,21 +708,21 @@ fn workspace_lock_idempotence_virtual_workspace() -> Result<()> { } /// Extract just the sources from the lockfile, to test path resolution. -#[derive(Deserialize, Serialize)] +#[derive(Deserialize, Serialize, Debug, PartialEq)] struct SourceLock { package: Vec, } impl SourceLock { - fn sources(self) -> BTreeMap { + fn sources(&self) -> BTreeMap { self.package - .into_iter() - .map(|package| (package.name, package.source)) + .iter() + .map(|package| (package.name.clone(), package.source.clone())) .collect() } } -#[derive(Deserialize, Serialize)] +#[derive(Deserialize, Serialize, Debug, PartialEq)] struct Package { name: String, source: toml::Value, @@ -1696,7 +1696,7 @@ fn workspace_member_name_shadows_dependencies() -> Result<()> { ----- stderr ----- Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] error: Failed to build: `foo @ file://[TEMP_DIR]/workspace/packages/foo` - Caused by: Failed to parse entry for: `anyio` + Caused by: Failed to parse entry: `anyio` Caused by: Package is not included as workspace package in `tool.uv.workspace` "### ); @@ -1761,3 +1761,147 @@ fn test_path_hopping() -> Result<()> { Ok(()) } + +/// `c` is a package in a git workspace, and it has a workspace dependency to `d`. Check that we +/// are correctly resolving `d` to a git dependency with a subdirectory and not relative to the +/// checkout directory. +#[test] +fn transitive_dep_in_git_workspace_no_root() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "a" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["c"] + + [tool.uv.sources] + c = { git = "https://github.com/astral-sh/workspace-virtual-root-test", subdirectory = "packages/c", rev = "fac39c8d4c5d0ef32744e2bb309bbe34a759fd46" } + "# + )?; + + context.lock().assert().success(); + + let lock1: SourceLock = + toml::from_str(&fs_err::read_to_string(context.temp_dir.child("uv.lock"))?)?; + + assert_json_snapshot!(lock1.sources(), @r###" + { + "a": { + "virtual": "." + }, + "anyio": { + "registry": "https://pypi.org/simple" + }, + "c": { + "git": "https://github.com/astral-sh/workspace-virtual-root-test?subdirectory=packages%2Fc&rev=fac39c8d4c5d0ef32744e2bb309bbe34a759fd46#fac39c8d4c5d0ef32744e2bb309bbe34a759fd46" + }, + "d": { + "git": "https://github.com/astral-sh/workspace-virtual-root-test?subdirectory=packages%2Fd&rev=fac39c8d4c5d0ef32744e2bb309bbe34a759fd46#fac39c8d4c5d0ef32744e2bb309bbe34a759fd46" + }, + "idna": { + "registry": "https://pypi.org/simple" + }, + "sniffio": { + "registry": "https://pypi.org/simple" + } + } + "###); + + // Check that we don't report a conflict here either. + pyproject_toml.write_str( + r#" + [project] + name = "a" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["c", "d"] + + [tool.uv.sources] + c = { git = "https://github.com/astral-sh/workspace-virtual-root-test", subdirectory = "packages/c", rev = "fac39c8d4c5d0ef32744e2bb309bbe34a759fd46" } + d = { git = "https://github.com/astral-sh/workspace-virtual-root-test", subdirectory = "packages/d", rev = "fac39c8d4c5d0ef32744e2bb309bbe34a759fd46" } + "# + )?; + + context.lock().assert().success(); + + let lock2: SourceLock = + toml::from_str(&fs_err::read_to_string(context.temp_dir.child("uv.lock"))?)?; + + assert_eq!(lock1, lock2, "sources changed"); + + Ok(()) +} + +/// `workspace-member-in-subdir` is a package in a git workspace, and it has a workspace dependency +/// to `uv-git-workspace-in-root`. Check that we are correctly resolving `uv-git-workspace-in-root` +/// to a git dependency without a subdirectory and not relative to the checkout directory. +#[test] +fn transitive_dep_in_git_workspace_with_root() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "git-with-root" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [ + "workspace-member-in-subdir", + ] + + [tool.uv.sources] + workspace-member-in-subdir = { git = "https://github.com/astral-sh/workspace-in-root-test", subdirectory = "workspace-member-in-subdir", rev = "d3ab48d2338296d47e28dbb2fb327c5e2ac4ac68" } + "# + )?; + + context.lock().assert().success(); + + let lock1: SourceLock = + toml::from_str(&fs_err::read_to_string(context.temp_dir.child("uv.lock"))?)?; + assert_json_snapshot!(lock1.sources(), @r###" + { + "git-with-root": { + "virtual": "." + }, + "uv-git-workspace-in-root": { + "git": "https://github.com/astral-sh/workspace-in-root-test?rev=d3ab48d2338296d47e28dbb2fb327c5e2ac4ac68#d3ab48d2338296d47e28dbb2fb327c5e2ac4ac68" + }, + "workspace-member-in-subdir": { + "git": "https://github.com/astral-sh/workspace-in-root-test?subdirectory=workspace-member-in-subdir&rev=d3ab48d2338296d47e28dbb2fb327c5e2ac4ac68#d3ab48d2338296d47e28dbb2fb327c5e2ac4ac68" + } + } + "###); + + // Check that we don't report a conflict here either + pyproject_toml.write_str( + r#" + [project] + name = "git-with-root" + version = "0.1.0" + description = "Add your description here" + readme = "README.md" + requires-python = ">=3.12" + dependencies = [ + "workspace-member-in-subdir", + "uv-git-workspace-in-root", + ] + + [tool.uv.sources] + workspace-member-in-subdir = { git = "https://github.com/astral-sh/workspace-in-root-test", subdirectory = "workspace-member-in-subdir", rev = "d3ab48d2338296d47e28dbb2fb327c5e2ac4ac68" } + uv-git-workspace-in-root = { git = "https://github.com/astral-sh/workspace-in-root-test", rev = "d3ab48d2338296d47e28dbb2fb327c5e2ac4ac68" } + "# + )?; + + context.lock().assert().success(); + let lock2: SourceLock = + toml::from_str(&fs_err::read_to_string(context.temp_dir.child("uv.lock"))?)?; + + assert_eq!(lock1, lock2, "sources changed"); + + Ok(()) +} diff --git a/docs/concepts/dependencies.md b/docs/concepts/dependencies.md index d9eca8b046a7..fd7c9d7d7108 100644 --- a/docs/concepts/dependencies.md +++ b/docs/concepts/dependencies.md @@ -136,16 +136,54 @@ dependencies = [ httpx = { git = "https://github.com/encode/httpx" } ``` -A revision, tag, or branch may also be included: +A revision (i.e., commit), tag, or branch may also be included: ```console $ uv add git+https://github.com/encode/httpx --tag 0.27.0 $ uv add git+https://github.com/encode/httpx --branch main -$ uv add git+https://github.com/encode/httpx --rev 326b943 +$ uv add git+https://github.com/encode/httpx --rev 326b9431c761e1ef1e00b9f760d1f654c8db48c6 ``` Git dependencies can also be manually added or edited in the `pyproject.toml` with the -`{ git = }` syntax. A target revision may be specified with one of: `rev`, `tag`, or `branch`. +`{ git = }` syntax. A target revision may be specified with one of: `rev` (i.e., commit), +`tag`, or `branch`. + +=== "tag" + + ```toml title="pyproject.toml" + [project] + dependencies = [ + "httpx", + ] + + [tool.uv.sources] + httpx = { git = "https://github.com/encode/httpx", tag = "0.27.0" } + ``` + +=== "branch" + + ```toml title="pyproject.toml" + [project] + dependencies = [ + "httpx", + ] + + [tool.uv.sources] + httpx = { git = "https://github.com/encode/httpx", branch = "main" } + ``` + +=== "rev" + + ```toml title="pyproject.toml" + [project] + dependencies = [ + "httpx", + ] + + [tool.uv.sources] + httpx = { git = "https://github.com/encode/httpx", rev = "326b9431c761e1ef1e00b9f760d1f654c8db48c6" } + ``` + A `subdirectory` may be specified if the package isn't in the repository root. ### URL @@ -221,8 +259,8 @@ $ uv add ~/projects/bar/ $ uv add --editable ~/projects/bar/ ``` - However, it is recommended to use [_workspaces_](./workspaces.md) instead of manual path - dependencies. + For multiple packages in the same repository, [_workspaces_](./workspaces.md) may be a better + fit. ### Workspace member @@ -355,23 +393,105 @@ $ uv add httpx --optional network Unlike optional dependencies, development dependencies are local-only and will _not_ be included in the project requirements when published to PyPI or other indexes. As such, development dependencies -are included under `[tool.uv]` instead of `[project]`. +are not included in the `[project]` table. Development dependencies can have entries in `tool.uv.sources` the same as normal dependencies. +To add a development dependency, use the `--dev` flag: + +```console +$ uv add --dev pytest +``` + +uv uses the `[dependency-groups]` table (as defined in [PEP 735](https://peps.python.org/pep-0735/)) +for declaration of development dependencies. The above command will create a `dev` group: + ```toml title="pyproject.toml" -[tool.uv] -dev-dependencies = [ +[dependency-groups] +dev = [ "pytest >=8.1.1,<9" ] ``` -To add a development dependency, include the `--dev` flag: +The `dev` group is special-cased; there are `--dev`, `--only-dev`, and `--no-dev` flags to toggle +inclusion or exclusion of its dependencies. Additionally, the `dev` group is +[synced by default](#default-groups). + +### Dependency groups + +Development dependencies can be divided into multiple groups, using the `--group` flag. + +For example, to add a development dependency in the `lint` group: ```console -$ uv add ruff --dev +$ uv add --group lint ruff +``` + +Which results in the following `[dependency-groups]` definition: + +```toml title="pyproject.toml" +[dependency-groups] +dev = [ + "pytest" +] +lint = [ + "ruff" +] ``` +Once groups are defined, the `--group`, `--only-group`, and `--no-group` options can be used to +include or exclude their dependencies. + +!!! tip + + The `--dev`, `--only-dev`, and `--no-dev` flags are equivalent to `--group dev`, + `--only-group dev`, and `--no-group dev` respectively. + +uv requires that all dependency groups are compatible with each other and resolves all groups +together when creating the lockfile. + +If dependencies declared in one group are not compatible with those in another group, uv will fail +to resolve the requirements of the project with an error. + +!!! note + + There is currently no way to declare conflicting dependency groups. See + [astral.sh/uv#6981](https://github.com/astral-sh/uv/issues/6981) to track support. + +### Default groups + +By default, uv includes the `dev` dependency group in the environment (e.g., during `uv run` or +`uv sync`). The default groups to include can be changed using the `tool.uv.default-groups` setting. + +```toml title="pyproject.toml" +[tool.uv] +default-groups = ["dev", "foo"] +``` + +!!! tip + + To exclude a default group during `uv run` or `uv sync`, use `--no-group `. + +### Legacy `dev-dependencies` + +Before `[dependency-groups]` was standardized, uv used the `tool.uv.dev-dependencies` field to +specify development dependencies, e.g.: + +```toml title="pyproject.toml" +[tool.uv] +dev-dependencies = [ + "pytest" +] +``` + +Dependencies declared in this section will be combined with the contents in the +`dependency-groups.dev`. Eventually, the `dev-dependencies` field will be deprecated and removed. + +!!! note + + If a `tool.uv.dev-dependencies` field exists, `uv add --dev` will use the existing section + instead of adding a new `dependency-groups.dev` section. + ## Build dependencies If a project is structured as [Python package](./projects.md#build-systems), it may declare diff --git a/docs/concepts/projects.md b/docs/concepts/projects.md index d4aa0766f805..253ad210b106 100644 --- a/docs/concepts/projects.md +++ b/docs/concepts/projects.md @@ -481,7 +481,7 @@ same time. uv requires that all optional dependencies ("extras") declared by the project are compatible with each other and resolves all optional dependencies together when creating the lockfile. -If optional dependencies declared in one group are not compatible with those in another group, uv +If optional dependencies declared in one extra are not compatible with those in another extra, uv will fail to resolve the requirements of the project with an error. !!! note @@ -727,8 +727,8 @@ no-build-isolation-package = ["cchardet"] Installing packages without build isolation requires that the package's build dependencies are installed in the project environment _prior_ to installing the package itself. This can be achieved -by separating out the build dependencies and the packages that require them into distinct optional -groups. For example: +by separating out the build dependencies and the packages that require them into distinct extras. +For example: ```toml title="pyproject.toml" [project] @@ -855,3 +855,9 @@ You could run the following sequence of commands to sync `flash-attn`: $ uv sync --extra build $ uv sync --extra build --extra compile ``` + +!!! note + + The `version` field in `tool.uv.dependency-metadata` is optional for registry-based + dependencies (when omitted, uv will assume the metadata applies to all versions of the package), + but _required_ for direct URL dependencies (like Git dependencies). diff --git a/docs/concepts/resolution.md b/docs/concepts/resolution.md index 77239e0dff4f..5ec45067a067 100644 --- a/docs/concepts/resolution.md +++ b/docs/concepts/resolution.md @@ -7,10 +7,10 @@ requirements of the requested packages are compatible. ## Dependencies -Most projects and packages have dependencies. Dependencies are other packages that are needed in +Most projects and packages have dependencies. Dependencies are other packages that are necessary in order for the current package to work. A package defines its dependencies as _requirements_, roughly a combination of a package name and acceptable versions. The dependencies defined by the current -project are called _direct dependencies_. The requirements added by each dependency of the current +project are called _direct dependencies_. The dependencies added by each dependency of the current project are called _indirect_ or _transitive dependencies_. !!! note @@ -34,10 +34,10 @@ To help demonstrate the resolution process, consider the following dependencies: In this example, the resolver must find a set of package versions which satisfies the project requirements. Since there is only one version of both `foo` and `bar`, those will be used. The resolution must also include the transitive dependencies, so a version of `lib` must be chosen. -`foo 1.0.0` allows all of the available versions of `lib`, but `bar 1.0.0` requires `lib>=2.0.0` so +`foo 1.0.0` allows all available versions of `lib`, but `bar 1.0.0` requires `lib>=2.0.0` so `lib 2.0.0` must be used. -In some resolutions, there is more than one solution. Consider the following dependencies: +In some resolutions, there may be more than one valid solution. Consider the following dependencies: - The project depends on `foo` and `bar`. @@ -49,21 +49,21 @@ In some resolutions, there is more than one solution. Consider the following dep - `bar 2.0.0` depends on `lib==1.0.0` - `lib` has two versions, 1.0.0 and 2.0.0. Both versions have no dependencies. -In this example, some version of both `foo` and `bar` must be picked, however, determining which +In this example, some version of both `foo` and `bar` must be selected; however, determining which version requires considering the dependencies of each version of `foo` and `bar`. `foo 2.0.0` and -`bar 2.0.0` cannot be installed together because they conflict on their required version of `lib`, -so the resolver must select either `foo 1.0.0` or `bar 1.0.0`. Both are valid solutions, and -different resolution algorithms may give either result. +`bar 2.0.0` cannot be installed together as they conflict on their required version of `lib`, so the +resolver must select either `foo 1.0.0` (along with `bar 2.0.0`) or `bar 1.0.0` (along with +`foo 1.0.0`). Both are valid solutions, and different resolution algorithms may yield either result. ## Platform markers Markers allow attaching an expression to requirements that indicate when the dependency should be -used. For example `bar; python_version<"3.9"` can be used to only require `bar` on Python 3.8 and -older. +used. For example `bar ; python_version < "3.9"` indicates that `bar` should only be installed on +Python 3.8 and earlier. -Markers are used to adjust a package's dependencies depending on the current environment or -platform. For example, markers can be used to change dependencies based on the operating system, the -CPU architecture, the Python version, the Python implementation, and more. +Markers are used to adjust a package's dependencies based on the current environment or platform. +For example, markers can be used to modify dependencies by operating system, CPU architecture, +Python version, Python implementation, and more. !!! note @@ -98,16 +98,20 @@ be used. A universal resolution is often more constrained than a platform-specif we need to take the requirements for all markers into account. During universal resolution, a minimum Python version must be specified. Project commands read the -minimum required version from `project.requires-python` in the `pyproject.toml`. When using the pip -interface, provide a value with the `--python-version` option, otherwise the current Python version -will be treated as a lower bound. For example, `--universal --python-version 3.9` writes a universal -resolution for Python 3.9 and later. +minimum required version from `project.requires-python` in the `pyproject.toml`. When using uv's pip +interface, provide a value with the `--python-version` option; otherwise, the current Python version +will be treated as a lower bound. For example, `--universal --python-version 3.9` performs a +universal resolution for Python 3.9 and later. -Setting the minimum Python version is important because all package versions we select have to be -compatible with the Python version range. For example, a universal resolution of `numpy<2` with -`--python-version 3.8` resolves to `numpy==1.24.4`, while `--python-version 3.9` resolves to -`numpy==1.26.4`, as `numpy` releases after 1.26.4 require at Python 3.9+. Note that we only consider -the lower bound of any Python requirement, upper bounds are always ignored. +During universal resolution, all selected dependency versions must be compatible with the _entire_ +`requires-python` range declared in the `pyproject.toml`. For example, if a project's +`requires-python` is `>=3.8`, then uv will not allow _any_ dependency versions that are limited to, +e.g., Python 3.9 and later, as they are not compatible with Python 3.8, the lower bound of the +project's supported range. In other words, the project's `requires-python` must be a subset of the +`requires-python` of all its dependencies. + +When evaluating `requires-python` ranges for dependencies, uv only considers lower bounds and +ignores upper bounds entirely. For example, `>=3.8, <4` is treated as `>=3.8`. ## Platform-specific resolution @@ -237,9 +241,9 @@ resolved versions, regardless of which packages are overlapping between the two. ## Dependency overrides -Dependency overrides allow bypassing failing or undesirable resolutions by overriding a package's -declared dependencies. Overrides are a useful last resort for cases in which you _know_ that a -dependency is compatible with a certain version of a package, despite the metadata indicating +Dependency overrides allow bypassing unsuccessful or undesirable resolutions by overriding a +package's declared dependencies. Overrides are a useful last resort for cases in which you _know_ +that a dependency is compatible with a certain version of a package, despite the metadata indicating otherwise. For example, if a transitive dependency declares the requirement `pydantic>=1.0,<2.0`, but _does_ @@ -302,6 +306,15 @@ For example, you can declare the metadata for `flash-attn`, allowing uv to resol the package from source (which itself requires installing `torch`): ```toml +[project] +name = "project" +version = "0.1.0" +requires-python = ">=3.12" +dependencies = ["flash-attn"] + +[tool.uv.sources] +flash-attn = { git = "https://github.com/Dao-AILab/flash-attention", tag = "v2.6.3" } + [[tool.uv.dependency-metadata]] name = "flash-attn" version = "2.6.3" @@ -313,6 +326,12 @@ package's metadata is incorrect or incomplete, or when a package is not availabl index. While dependency overrides allow overriding the allowed versions of a package globally, metadata overrides allow overriding the declared metadata of a _specific package_. +!!! note + + The `version` field in `tool.uv.dependency-metadata` is optional for registry-based + dependencies (when omitted, uv will assume the metadata applies to all versions of the package), + but _required_ for direct URL dependencies (like Git dependencies). + Entries in the `tool.uv.dependency-metadata` table follow the [Metadata 2.3](https://packaging.python.org/en/latest/specifications/core-metadata/) specification, though only `name`, `version`, `requires-dist`, `requires-python`, and `provides-extra` are read by @@ -382,3 +401,21 @@ reading and extracting archives in the following formats: For more details about the internals of the resolver, see the [resolver reference](../reference/resolver-internals.md) documentation. + +## Lockfile versioning + +The `uv.lock` file uses a versioned schema. The schema version is included in the `version` field of +the lockfile. + +Any given version of uv can read and write lockfiles with the same schema version, but will reject +lockfiles with a greater schema version. For example, if your uv version supports schema v1, +`uv lock` will error if it encounters an existing lockfile with schema v2. + +uv versions that support schema v2 _may_ be able to read lockfiles with schema v1 if the schema +update was backwards-compatible. However, this is not guaranteed, and uv may exit with an error if +it encounters a lockfile with an outdated schema version. + +The schema version is considered part of the public API, and so is only bumped in minor releases, as +a breaking change (see [Versioning](../reference/versioning.md)). As such, all uv patch versions +within a given minor uv release are guaranteed to have full lockfile compatibility. In other words, +lockfiles may only be rejected across minor releases. diff --git a/docs/configuration/authentication.md b/docs/configuration/authentication.md index 5aed34eb969e..8a6bcf002dbb 100644 --- a/docs/configuration/authentication.md +++ b/docs/configuration/authentication.md @@ -35,15 +35,18 @@ uv supports credentials over HTTP when querying package registries. Authentication can come from the following sources, in order of precedence: - The URL, e.g., `https://:@/...` -- A [`netrc`](https://everything.curl.dev/usingcurl/netrc) configuration file +- A [`.netrc`](https://everything.curl.dev/usingcurl/netrc) configuration file - A [keyring](https://github.com/jaraco/keyring) provider (requires opt-in) If authentication is found for a single net location (scheme, host, and port), it will be cached for the duration of the command and used for other queries to that net location. Authentication is not cached across invocations of uv. -Note `--keyring-provider subprocess` or `UV_KEYRING_PROVIDER=subprocess` must be provided to enable -keyring-based authentication. +`.netrc` authentication is enabled by default, and will respect the `NETRC` environment variable if +defined, falling back to `~/.netrc` if not. + +To enable keyring-based authentication, pass the `--keyring-provider subprocess` command-line +argument to uv, or set `UV_KEYRING_PROVIDER=subprocess`. Authentication may be used for hosts specified in the following contexts: diff --git a/docs/configuration/environment.md b/docs/configuration/environment.md index 2932054b0f0c..bd9206ced7be 100644 --- a/docs/configuration/environment.md +++ b/docs/configuration/environment.md @@ -79,6 +79,7 @@ uv respects the following environment variables: set, uv will use this username for publishing. - `UV_PUBLISH_PASSWORD`: Equivalent to the `--password` command-line argument in `uv publish`. If set, uv will use this password for publishing. +- `UV_PUBLISH_CHECK_URL`: Don't upload a file if it already exists on the index. The value is the URL of the index. - `UV_NO_SYNC`: Equivalent to the `--no-sync` command-line argument. If set, uv will skip updating the environment. - `UV_LOCKED`: Equivalent to the `--locked` command-line argument. If set, uv will assert that the @@ -100,6 +101,7 @@ uv respects the following environment variables: - `UV_PROJECT_ENVIRONMENT`: Specifies the path to the directory to use for a project virtual environment. See the [project documentation](../concepts/projects.md#configuring-the-project-environment-path) for more details. +- `UV_PYTHON_BIN_DIR`: Specifies the directory to place links to installed, managed Python executables. - `UV_PYTHON_INSTALL_DIR`: Specifies the directory for storing managed Python installations. - `UV_PYTHON_INSTALL_MIRROR`: Managed Python installations are downloaded from [`python-build-standalone`](https://github.com/indygreg/python-build-standalone). @@ -130,10 +132,10 @@ uv respects the following environment variables: - `HTTPS_PROXY`: Proxy for HTTPS requests. - `ALL_PROXY`: General proxy for all network requests. - `UV_HTTP_TIMEOUT`: Timeout (in seconds) for HTTP requests. (default: 30 s) +- `UV_REQUEST_TIMEOUT`: Timeout (in seconds) for HTTP requests. Equivalent to `UV_HTTP_TIMEOUT`. - `HTTP_TIMEOUT`: Timeout (in seconds) for HTTP requests. Equivalent to `UV_HTTP_TIMEOUT`. - `PYC_INVALIDATION_MODE`: The validation modes to use when run with `--compile`. See [`PycInvalidationMode`](https://docs.python.org/3/library/py_compile.html#py_compile.PycInvalidationMode). -- `UV_REQUEST_TIMEOUT`: Timeout (in seconds) for HTTP requests. - `VIRTUAL_ENV`: Used to detect an activated virtual environment. - `CONDA_PREFIX`: Used to detect an activated Conda environment. - `VIRTUAL_ENV_DISABLE_PROMPT`: If set to `1` before a virtual environment is activated, then the @@ -150,6 +152,7 @@ uv respects the following environment variables: Defaults to `12.0`, the least-recent non-EOL macOS version at time of writing. - `NO_COLOR`: Disables colored output (takes precedence over `FORCE_COLOR`). See [no-color.org](https://no-color.org). +- `UV_NO_PROGRESS`: Disables all progress output. For example, spinners and progress bars. - `FORCE_COLOR`: Forces colored output regardless of terminal support. See [force-color.org](https://force-color.org). - `CLICOLOR_FORCE`: Use to control color via `anstyle`. @@ -163,6 +166,13 @@ uv respects the following environment variables: - `GIT_INDEX_FILE`: Path to the index file for staged changes. Ignored by `uv` when performing fetch. - `GIT_OBJECT_DIRECTORY`: Path to where git object files are located. Ignored by `uv` when performing fetch. - `GIT_ALTERNATE_OBJECT_DIRECTORIES`: Alternate locations for git objects. Ignored by `uv` when performing fetch. +- `GIT_CEILING_DIRECTORIES`: Used in tests for better git isolation. + For example, we run some tests in ~/.local/share/uv/tests. + And if the user's `$HOME` directory is a git repository, + this will change the behavior of some tests. Setting + `GIT_CEILING_DIRECTORIES=/home/andrew/.local/share/uv/tests` will + prevent git from crawling up the directory tree past that point to find + parent git repositories. - `GITHUB_ACTIONS`: Used for trusted publishing via `uv publish`. - `ACTIONS_ID_TOKEN_REQUEST_URL`: Used for trusted publishing via `uv publish`. Contains the oidc token url. - `ACTIONS_ID_TOKEN_REQUEST_TOKEN`: Used for trusted publishing via `uv publish`. Contains the oidc request token. @@ -176,4 +186,8 @@ uv respects the following environment variables: - `JPY_SESSION_NAME`: Used to detect when running inside a Jupyter notebook. - `TRACING_DURATIONS_TEST_ROOT`: Use to create the tracing root directory via the `tracing-durations-export` feature. - `TRACING_DURATIONS_FILE`: Use to create the tracing durations file via the `tracing-durations-export` feature. -- `RUST_LOG`: Custom log level for verbose output, compatible with `tracing_subscriber`. +- `RUST_LOG`: If set, uv will use this value as the log level for its `--verbose` output. Accepts + any filter compatible with the `tracing_subscriber` crate. + For example, `RUST_LOG=trace` will enable trace-level logging. + See the [tracing documentation](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#example-syntax) + for more. diff --git a/docs/configuration/indexes.md b/docs/configuration/indexes.md index a16087eacfa5..2d3ce7e393d8 100644 --- a/docs/configuration/indexes.md +++ b/docs/configuration/indexes.md @@ -18,8 +18,6 @@ name = "pytorch" url = "https://download.pytorch.org/whl/cpu" ``` -Index names must only contain alphanumeric characters, dashes, or underscores. - Indexes are prioritized in the order in which they’re defined, such that the first index listed in the configuration file is the first index consulted when resolving dependencies, with indexes provided via the command line taking precedence over those in the configuration file. @@ -38,6 +36,9 @@ default = true The default index is always treated as lowest priority, regardless of its position in the list of indexes. +Index names may only contain alphanumeric characters, dashes, underscores, and periods, and must be +valid ASCII. + ## Pinning a package to an index A package can be pinned to a specific index by specifying the index in its `tool.uv.sources` entry. @@ -127,21 +128,22 @@ password (or access token). To authenticate with a provide index, either provide credentials via environment variables or embed them in the URL. -For example, given an index named `internal` that requires a username (`public`) and password +For example, given an index named `internal-proxy` that requires a username (`public`) and password (`koala`), define the index (without credentials) in your `pyproject.toml`: ```toml [[tool.uv.index]] -name = "internal" +name = "internal-proxy" url = "https://example.com/simple" ``` -From there, you can set the `UV_INDEX_INTERNAL_USERNAME` and `UV_INDEX_INTERNAL_PASSWORD` -environment variables, where `INTERNAL` is the uppercase version of the index name: +From there, you can set the `UV_INDEX_INTERNAL_PROXY_USERNAME` and +`UV_INDEX_INTERNAL_PROXY_PASSWORD` environment variables, where `INTERNAL_PROXY` is the uppercase +version of the index name, with non-alphanumeric characters replaced by underscores: ```sh -export UV_INDEX_INTERNAL_USERNAME=public -export UV_INDEX_INTERNAL_PASSWORD=koala +export UV_INDEX_INTERNAL_PROXY_USERNAME=public +export UV_INDEX_INTERNAL_PROXY_PASSWORD=koala ``` By providing credentials via environment variables, you can avoid storing sensitive information in diff --git a/docs/configuration/installer.md b/docs/configuration/installer.md new file mode 100644 index 000000000000..352689b5cc5a --- /dev/null +++ b/docs/configuration/installer.md @@ -0,0 +1,50 @@ +# Configuring the uv installer + +## Changing the install path + +By default, uv is installed to `~/.cargo/bin`. To change the installation path, use +`UV_INSTALL_DIR`: + +=== "macOS and Linux" + + ```console + $ curl -LsSf https://astral.sh/uv/install.sh | env UV_INSTALL_DIR="/custom/path" sh + ``` + +=== "Windows" + + ```powershell + $env:UV_INSTALL_DIR = "C:\Custom\Path" powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" + ``` + +## Disabling shell modifications + +The installer may also update your shell profiles to ensure the uv binary is on your `PATH`. To +disable this behavior, use `INSTALLER_NO_MODIFY_PATH`. For example: + +```console +$ curl -LsSf https://astral.sh/uv/install.sh | env INSTALLER_NO_MODIFY_PATH=1 sh +``` + +If installed with `INSTALLER_NO_MODIFY_PATH`, subsequent operations, like `uv self update`, will not +modify your shell profiles. + +## Unmanaged installations + +In ephemeral environments like CI, use `UV_UNMANAGED_INSTALL` to install uv to a specific path while +preventing the installer from modifying shell profiles or environment variables: + +```console +$ curl -LsSf https://astral.sh/uv/install.sh | env UV_UNMANAGED_INSTALL="/custom/path" sh +``` + +The use of `UV_UNMANAGED_INSTALL` will also disable self-updates (via `uv self update`). + +## Passing options to the install script + +Using environment variables is recommended because they are consistent across platforms. However, +options can be passed directly to the install script. For example, to see the available options: + +```console +$ curl -LsSf https://astral.sh/uv/install.sh | sh -s -- --help +``` diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index cc354a12e07b..e89a1534e75d 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -52,45 +52,8 @@ Request a specific version by including it in the URL: Alternatively, the installer or binaries can be downloaded directly from [GitHub](#github-releases). -#### Configuring installation - -By default, uv is installed to `~/.cargo/bin`. To change the installation path, use -`UV_INSTALL_DIR`: - -=== "macOS and Linux" - - ```console - $ curl -LsSf https://astral.sh/uv/install.sh | env UV_INSTALL_DIR="/custom/path" sh - ``` - -=== "Windows" - - ```powershell - $env:UV_INSTALL_DIR = "C:\Custom\Path" powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" - ``` - -The installer will also update your shell profiles to ensure the uv binary is on your `PATH`. To -disable this behavior, use `INSTALLER_NO_MODIFY_PATH`. For example: - -```console -$ curl -LsSf https://astral.sh/uv/install.sh | env INSTALLER_NO_MODIFY_PATH=1 sh -``` - -Using environment variables is recommended because they are consistent across platforms. However, -options can be passed directly to the install script. For example, to see the available options: - -```console -$ curl -LsSf https://astral.sh/uv/install.sh | sh -s -- --help -``` - -In ephemeral environments like CI, use `UV_UNMANAGED_INSTALL` to install uv to a specific path while -preventing the installer from modifying shell profiles or environment variables: - -```console -$ curl -LsSf https://astral.sh/uv/install.sh | env UV_UNMANAGED_INSTALL="/custom/path" sh -``` - -The use of `UV_UNMANAGED_INSTALL` will also disable self-updates (via `uv self update`). +See the documentation on [installer configuration](../configuration/installer.md) for details on +customizing your uv installation. ### PyPI diff --git a/docs/guides/integration/alternative-indexes.md b/docs/guides/integration/alternative-indexes.md index 5875237bf4e1..9e8142891056 100644 --- a/docs/guides/integration/alternative-indexes.md +++ b/docs/guides/integration/alternative-indexes.md @@ -62,6 +62,58 @@ $ # Configure the index URL with the username $ export UV_EXTRA_INDEX_URL=https://VssSessionToken@pkgs.dev.azure.com/{organisation}/{project}/_packaging/{feedName}/pypi/simple/ ``` +## Google Artifact Registry + +uv can install packages from +[Google Artifact Registry](https://cloud.google.com/artifact-registry/docs). Authenticate to a +repository using password authentication or using [`keyring`](https://github.com/jaraco/keyring) +package. + +!!! note + + This guide assumes `gcloud` CLI has previously been installed and setup. + +### Password authentication + +Credentials can be provided via "Basic" HTTP authentication scheme. Include access token in the +password field of the URL. Username must be `oauth2accesstoken`, otherwise authentication will fail. + +For example, with the token stored in the `$ARTIFACT_REGISTRY_TOKEN` environment variable, set the +index URL with: + +```bash +export ARTIFACT_REGISTRY_TOKEN=$(gcloud auth application-default print-access-token) +export UV_EXTRA_INDEX_URL=https://oauth2accesstoken:$ARTIFACT_REGISTRY_TOKEN@{region}-python.pkg.dev/{projectId}/{repositoryName}/simple +``` + +### Using `keyring` + +You can also authenticate to Artifact Registry using [`keyring`](https://github.com/jaraco/keyring) +package with +[`keyrings.google-artifactregistry-auth` plugin](https://github.com/GoogleCloudPlatform/artifact-registry-python-tools). +Because these two packages are required to authenticate to Artifact Registry, they must be +pre-installed from a source other than Artifact Registry. + +The `artifacts-keyring` plugin wraps [gcloud CLI](https://cloud.google.com/sdk/gcloud) to generate +short-lived access tokens, securely store them in system keyring and refresh them when they are +expired. + +uv only supports using the `keyring` package in +[subprocess mode](https://github.com/astral-sh/uv/blob/main/PIP_COMPATIBILITY.md#registry-authentication). +The `keyring` executable must be in the `PATH`, i.e., installed globally or in the active +environment. The `keyring` CLI requires a username in the URL and it must be `oauth2accesstoken`. + +```bash +# Pre-install keyring and Artifact Registry plugin from the public PyPI +uv tool install keyring --with keyrings.google-artifactregistry-auth + +# Enable keyring authentication +export UV_KEYRING_PROVIDER=subprocess + +# Configure the index URL with the username +export UV_EXTRA_INDEX_URL=https://oauth2accesstoken@{region}-python.pkg.dev/{projectId}/{repositoryName}/simple +``` + ## AWS CodeArtifact uv can install packages from @@ -118,4 +170,4 @@ uv publish ## Other indexes -uv is also known to work with JFrog's Artifactory and the Google Cloud Artifact Registry. +uv is also known to work with JFrog's Artifactory. diff --git a/docs/guides/integration/docker.md b/docs/guides/integration/docker.md index 01bdee81b200..05e5b9128d71 100644 --- a/docs/guides/integration/docker.md +++ b/docs/guides/integration/docker.md @@ -21,7 +21,7 @@ $ docker run ghcr.io/astral-sh/uv --help uv provides a distroless Docker image including the `uv` binary. The following tags are published: - `ghcr.io/astral-sh/uv:latest` -- `ghcr.io/astral-sh/uv:{major}.{minor}.{patch}`, e.g., `ghcr.io/astral-sh/uv:0.4.25` +- `ghcr.io/astral-sh/uv:{major}.{minor}.{patch}`, e.g., `ghcr.io/astral-sh/uv:0.4.29` - `ghcr.io/astral-sh/uv:{major}.{minor}`, e.g., `ghcr.io/astral-sh/uv:0.4` (the latest patch version) @@ -62,7 +62,7 @@ In addition, uv publishes the following images: As with the distroless image, each image is published with uv version tags as `ghcr.io/astral-sh/uv:{major}.{minor}.{patch}-{base}` and -`ghcr.io/astral-sh/uv:{major}.{minor}-{base}`, e.g., `ghcr.io/astral-sh/uv:0.4.25-alpine`. +`ghcr.io/astral-sh/uv:{major}.{minor}-{base}`, e.g., `ghcr.io/astral-sh/uv:0.4.29-alpine`. For more details, see the [GitHub Container](https://github.com/astral-sh/uv/pkgs/container/uv) page. @@ -100,13 +100,13 @@ Note this requires `curl` to be available. In either case, it is best practice to pin to a specific uv version, e.g., with: ```dockerfile -COPY --from=ghcr.io/astral-sh/uv:0.4.25 /uv /uvx /bin/ +COPY --from=ghcr.io/astral-sh/uv:0.4.29 /uv /uvx /bin/ ``` Or, with the installer: ```dockerfile -ADD https://astral.sh/uv/0.4.25/install.sh /uv-installer.sh +ADD https://astral.sh/uv/0.4.29/install.sh /uv-installer.sh ``` ### Installing a project diff --git a/docs/guides/integration/github.md b/docs/guides/integration/github.md index 049de6b9ead5..765afda2f6f3 100644 --- a/docs/guides/integration/github.md +++ b/docs/guides/integration/github.md @@ -40,7 +40,7 @@ jobs: uses: astral-sh/setup-uv@v3 with: # Install a specific version of uv. - version: "0.4.25" + version: "0.4.29" ``` ## Setting up Python diff --git a/docs/guides/integration/pre-commit.md b/docs/guides/integration/pre-commit.md index 6e01b1c458d1..b9cd3d39451d 100644 --- a/docs/guides/integration/pre-commit.md +++ b/docs/guides/integration/pre-commit.md @@ -8,7 +8,7 @@ To compile requirements via pre-commit, add the following to the `.pre-commit-co ```yaml title=".pre-commit-config.yaml" - repo: https://github.com/astral-sh/uv-pre-commit # uv version. - rev: 0.4.25 + rev: 0.4.29 hooks: # Compile requirements - id: pip-compile @@ -20,7 +20,7 @@ To compile alternative files, modify `args` and `files`: ```yaml title=".pre-commit-config.yaml" - repo: https://github.com/astral-sh/uv-pre-commit # uv version. - rev: 0.4.25 + rev: 0.4.29 hooks: # Compile requirements - id: pip-compile @@ -33,7 +33,7 @@ To run the hook over multiple files at the same time: ```yaml title=".pre-commit-config.yaml" - repo: https://github.com/astral-sh/uv-pre-commit # uv version. - rev: 0.4.25 + rev: 0.4.29 hooks: # Compile requirements - id: pip-compile diff --git a/docs/guides/publish.md b/docs/guides/publish.md index 6c44e3fa96d3..fb99f498cf5d 100644 --- a/docs/guides/publish.md +++ b/docs/guides/publish.md @@ -43,12 +43,9 @@ $ uv publish ``` Set a PyPI token with `--token` or `UV_PUBLISH_TOKEN`, or set a username with `--username` or -`UV_PUBLISH_USERNAME` and password with `--password` or `UV_PUBLISH_PASSWORD`. - -!!! info - - For publishing to PyPI from GitHub Actions, you don't need to set any credentials. Instead, - [add a trusted publisher to the PyPI project](https://docs.pypi.org/trusted-publishers/adding-a-publisher/). +`UV_PUBLISH_USERNAME` and password with `--password` or `UV_PUBLISH_PASSWORD`. For publishing to +PyPI from GitHub Actions, you don't need to set any credentials. Instead, +[add a trusted publisher to the PyPI project](https://docs.pypi.org/trusted-publishers/adding-a-publisher/). !!! note @@ -56,6 +53,15 @@ Set a PyPI token with `--token` or `UV_PUBLISH_TOKEN`, or set a username with `- generate a token. Using a token is equivalent to setting `--username __token__` and using the token as password. +Even though `uv publish` retries failed uploads, it can happen that publishing fails in the middle, +with some files uploaded and some files still missing. With PyPI, you can retry the exact same +command, existing identical files will be ignored. With other registries, use +`--check-url ` with the index URL (not the publish URL) the packages belong to. uv will +skip uploading files that are identical to files in the registry, and it will also handle raced +parallel uploads. Note that existing files need to match exactly with those previously uploaded to +the registry, this avoids accidentally publishing source distribution and wheels with different +contents for the same version. + ## Installing your package Test that the package can be installed and imported with `uv run`: diff --git a/docs/index.md b/docs/index.md index b40ee7c992db..7f4e34035ab1 100644 --- a/docs/index.md +++ b/docs/index.md @@ -16,7 +16,8 @@ An extremely fast Python package and project manager, written in Rust. ## Highlights -- 🚀 A single tool to replace `pip`, `pip-tools`, `pipx`, `poetry`, `pyenv`, `virtualenv`, and more. +- 🚀 A single tool to replace `pip`, `pip-tools`, `pipx`, `poetry`, `pyenv`, `twine`, `virtualenv`, + and more. - ⚡️ [10-100x faster](https://github.com/astral-sh/uv/blob/main/BENCHMARKS.md) than `pip`. - 🐍 [Installs and manages](#python-management) Python versions. - 🛠️ [Runs and installs](#tool-management) Python applications. @@ -83,6 +84,9 @@ All checks passed! See the [project guide](./guides/projects.md) to get started. +uv also supports building and publishing projects, even if they're not managed with uv. See the +[publish guide](./guides/publish.md) to learn more. + ## Tool management uv executes and installs command-line tools provided by Python packages, similar to `pipx`. @@ -162,7 +166,7 @@ Pinned `.python-version` to `pypy@3.11` See the [installing Python guide](./guides/install-python.md) to get started. -### Script support +## Script support uv manages dependencies and environments for single-file scripts. diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 143e2d5e5705..057d220d74df 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -89,7 +89,9 @@ uv run [OPTIONS] [COMMAND]

May also be set with the UV_INSECURE_HOST environment variable.

--cache-dir cache-dir

Path to the cache directory.

-

Defaults to $HOME/Library/Caches/uv on macOS, $XDG_CACHE_HOME/uv or $HOME/.cache/uv on Linux, and %LOCALAPPDATA%\uv\cache on Windows.

+

Defaults to $XDG_CACHE_HOME/uv or $HOME/.cache/uv on macOS and Linux, and %LOCALAPPDATA%\uv\cache on Windows.

+ +

To view the location of the cache directory, run uv cache dir.

May also be set with the UV_CACHE_DIR environment variable.

--color color-choice

Control colors in output

@@ -136,7 +138,7 @@ uv run [OPTIONS] [COMMAND]

Accepts both RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z) and local dates in the same format (e.g., 2006-12-02) in your system’s configured time zone.

May also be set with the UV_EXCLUDE_NEWER environment variable.

-
--extra extra

Include optional dependencies from the extra group name.

+
--extra extra

Include optional dependencies from the specified extra name.

May be provided more than once.

@@ -163,6 +165,10 @@ uv run [OPTIONS] [COMMAND]

Instead of checking if the lockfile is up-to-date, uses the versions in the lockfile as the source of truth. If the lockfile is missing, uv will exit with an error. If the pyproject.toml includes changes to dependencies that have not been included in the lockfile yet, they will not be present in the environment.

May also be set with the UV_FROZEN environment variable.

+
--group group

Include dependencies from the specified dependency group.

+ +

May be provided multiple times.

+
--help, -h

Display the concise help for this command

--index index

The URLs to use when resolving dependencies, in addition to the default index.

@@ -276,18 +282,25 @@ uv run [OPTIONS] [COMMAND]

Normally, configuration files are discovered in the current directory, parent directories, or user configuration directories.

May also be set with the UV_NO_CONFIG environment variable.

-
--no-dev

Omit development dependencies.

+
--no-dev

Omit the development dependency group.

+ +

This option is an alias of --no-group dev.

This option is only available when running in a project.

--no-editable

Install any editable dependencies, including the project and any workspace members, as non-editable

+
--no-group no-group

Exclude dependencies from the specified dependency group.

+ +

May be provided multiple times.

+
--no-index

Ignore the registry index (e.g., PyPI), instead relying on direct URL dependencies and those provided via --find-links

--no-progress

Hide all progress outputs.

For example, spinners or progress bars.

+

May also be set with the UV_NO_PROGRESS environment variable.

--no-project

Avoid discovering the project or workspace.

Instead of searching for projects in the current directory and parent directories, run in an isolated, ephemeral environment populated by the --with requirements.

@@ -307,7 +320,15 @@ uv run [OPTIONS] [COMMAND]

When disabled, uv will only use locally cached data and locally available files.

-
--only-dev

Omit non-development dependencies.

+
--only-dev

Only include the development dependency group.

+ +

Omit other dependencies. The project itself will also be omitted.

+ +

This option is an alias for --only-group dev.

+ +
--only-group only-group

Only include dependencies from the specified dependency group.

+ +

May be provided multiple times.

The project itself will also be omitted.

@@ -471,7 +492,9 @@ uv init [OPTIONS] [PATH]
  • none: Do not infer the author information
  • -
    --build-backend build-backend

    Initialize a build-backend of choice for the project

    +
    --build-backend build-backend

    Initialize a build-backend of choice for the project.

    + +

    Implicitly sets --package.

    Possible values:

    @@ -490,7 +513,9 @@ uv init [OPTIONS] [PATH]
    --cache-dir cache-dir

    Path to the cache directory.

    -

    Defaults to $HOME/Library/Caches/uv on macOS, $XDG_CACHE_HOME/uv or $HOME/.cache/uv on Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    +

    Defaults to $XDG_CACHE_HOME/uv or $HOME/.cache/uv on macOS and Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    + +

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    --color color-choice

    Control colors in output

    @@ -555,6 +580,7 @@ uv init [OPTIONS] [PATH]

    For example, spinners or progress bars.

    +

    May also be set with the UV_NO_PROGRESS environment variable.

    --no-python-downloads

    Disable automatic downloads of Python.

    --no-readme

    Do not create a README.md file

    @@ -571,7 +597,7 @@ uv init [OPTIONS] [PATH]

    Defines a [build-system] for the project.

    -

    This is the default behavior when using --lib.

    +

    This is the default behavior when using --lib or --build-backend.

    When using --app, this will include a [project.scripts] entrypoint and use a src/ project structure.

    @@ -678,7 +704,9 @@ uv add [OPTIONS] >
    --cache-dir cache-dir

    Path to the cache directory.

    -

    Defaults to $HOME/Library/Caches/uv on macOS, $XDG_CACHE_HOME/uv or $HOME/.cache/uv on Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    +

    Defaults to $XDG_CACHE_HOME/uv or $HOME/.cache/uv on macOS and Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    + +

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    --color color-choice

    Control colors in output

    @@ -714,7 +742,9 @@ uv add [OPTIONS] >

    The index given by this flag is given lower priority than all other indexes specified via the --index flag.

    May also be set with the UV_DEFAULT_INDEX environment variable.

    -
    --dev

    Add the requirements as development dependencies

    +
    --dev

    Add the requirements to the development dependency group.

    + +

    This option is an alias for --group dev.

    --directory directory

    Change to the given directory prior to running the command.

    @@ -733,7 +763,7 @@ uv add [OPTIONS] >

    May be provided more than once.

    -

    To add this dependency to an optional group in the current project instead, see --optional.

    +

    To add this dependency to an optional extra instead, see --optional.

    --extra-index-url extra-index-url

    (Deprecated: use --index instead) Extra URLs of package indexes to use, in addition to --index-url.

    @@ -754,6 +784,10 @@ uv add [OPTIONS] >

    The project environment will not be synced.

    May also be set with the UV_FROZEN environment variable.

    +
    --group group

    Add the requirements to the specified dependency group.

    + +

    These requirements will not be included in the published metadata for the project.

    +
    --help, -h

    Display the concise help for this command

    --index index

    The URLs to use when resolving dependencies, in addition to the default index.

    @@ -861,6 +895,7 @@ uv add [OPTIONS] >

    For example, spinners or progress bars.

    +

    May also be set with the UV_NO_PROGRESS environment variable.

    --no-python-downloads

    Disable automatic downloads of Python.

    --no-sources

    Ignore the tool.uv.sources table when resolving dependencies. Used to lock against the standards-compliant, publishable package metadata, as opposed to using any local or Git sources

    @@ -872,11 +907,11 @@ uv add [OPTIONS] >

    When disabled, uv will only use locally cached data and locally available files.

    -
    --optional optional

    Add the requirements to the specified optional dependency group.

    +
    --optional optional

    Add the requirements to the package’s optional dependencies for the specified extra.

    The group may then be activated when installing the project with the --extra flag.

    -

    To enable an optional dependency group for this requirement instead, see --extra.

    +

    To enable an optional extra for this requirement instead, see --extra.

    --package package

    Add the dependency to a specific package in the workspace

    @@ -1020,7 +1055,9 @@ uv remove [OPTIONS] ...

    May also be set with the UV_INSECURE_HOST environment variable.

    --cache-dir cache-dir

    Path to the cache directory.

    -

    Defaults to $HOME/Library/Caches/uv on macOS, $XDG_CACHE_HOME/uv or $HOME/.cache/uv on Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    +

    Defaults to $XDG_CACHE_HOME/uv or $HOME/.cache/uv on macOS and Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    + +

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    --color color-choice

    Control colors in output

    @@ -1056,7 +1093,9 @@ uv remove [OPTIONS] ...

    The index given by this flag is given lower priority than all other indexes specified via the --index flag.

    May also be set with the UV_DEFAULT_INDEX environment variable.

    -
    --dev

    Remove the packages from the development dependencies

    +
    --dev

    Remove the packages from the development dependency group.

    + +

    This option is an alias for --group dev.

    --directory directory

    Change to the given directory prior to running the command.

    @@ -1088,6 +1127,8 @@ uv remove [OPTIONS] ...

    The project environment will not be synced.

    May also be set with the UV_FROZEN environment variable.

    +
    --group group

    Remove the packages from the specified dependency group

    +
    --help, -h

    Display the concise help for this command

    --index index

    The URLs to use when resolving dependencies, in addition to the default index.

    @@ -1195,6 +1236,7 @@ uv remove [OPTIONS] ...

    For example, spinners or progress bars.

    +

    May also be set with the UV_NO_PROGRESS environment variable.

    --no-python-downloads

    Disable automatic downloads of Python.

    --no-sources

    Ignore the tool.uv.sources table when resolving dependencies. Used to lock against the standards-compliant, publishable package metadata, as opposed to using any local or Git sources

    @@ -1206,7 +1248,7 @@ uv remove [OPTIONS] ...

    When disabled, uv will only use locally cached data and locally available files.

    -
    --optional optional

    Remove the packages from the specified optional dependency group

    +
    --optional optional

    Remove the packages from the project’s optional dependencies for the specified extra

    --package package

    Remove the dependencies from a specific package in the workspace

    @@ -1338,7 +1380,9 @@ uv sync [OPTIONS]

    May also be set with the UV_INSECURE_HOST environment variable.

    --cache-dir cache-dir

    Path to the cache directory.

    -

    Defaults to $HOME/Library/Caches/uv on macOS, $XDG_CACHE_HOME/uv or $HOME/.cache/uv on Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    +

    Defaults to $XDG_CACHE_HOME/uv or $HOME/.cache/uv on macOS and Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    + +

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    --color color-choice

    Control colors in output

    @@ -1385,7 +1429,7 @@ uv sync [OPTIONS]

    Accepts both RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z) and local dates in the same format (e.g., 2006-12-02) in your system’s configured time zone.

    May also be set with the UV_EXCLUDE_NEWER environment variable.

    -
    --extra extra

    Include optional dependencies from the extra group name.

    +
    --extra extra

    Include optional dependencies from the specified extra name.

    May be provided more than once.

    @@ -1410,6 +1454,10 @@ uv sync [OPTIONS]

    Instead of checking if the lockfile is up-to-date, uses the versions in the lockfile as the source of truth. If the lockfile is missing, uv will exit with an error. If the pyproject.toml includes changes to dependencies that have not been included in the lockfile yet, they will not be present in the environment.

    May also be set with the UV_FROZEN environment variable.

    +
    --group group

    Include dependencies from the specified dependency group.

    + +

    May be provided multiple times.

    +
    --help, -h

    Display the concise help for this command

    --index index

    The URLs to use when resolving dependencies, in addition to the default index.

    @@ -1515,10 +1563,16 @@ uv sync [OPTIONS]

    Normally, configuration files are discovered in the current directory, parent directories, or user configuration directories.

    May also be set with the UV_NO_CONFIG environment variable.

    -
    --no-dev

    Omit development dependencies

    +
    --no-dev

    Omit the development dependency group.

    + +

    This option is an alias for --no-group dev.

    --no-editable

    Install any editable dependencies, including the project and any workspace members, as non-editable

    +
    --no-group no-group

    Exclude dependencies from the specified dependency group.

    + +

    May be provided multiple times.

    +
    --no-index

    Ignore the registry index (e.g., PyPI), instead relying on direct URL dependencies and those provided via --find-links

    --no-install-package no-install-package

    Do not install the given package(s).

    @@ -1537,6 +1591,7 @@ uv sync [OPTIONS]

    For example, spinners or progress bars.

    +

    May also be set with the UV_NO_PROGRESS environment variable.

    --no-python-downloads

    Disable automatic downloads of Python.

    --no-sources

    Ignore the tool.uv.sources table when resolving dependencies. Used to lock against the standards-compliant, publishable package metadata, as opposed to using any local or Git sources

    @@ -1545,7 +1600,15 @@ uv sync [OPTIONS]

    When disabled, uv will only use locally cached data and locally available files.

    -
    --only-dev

    Omit non-development dependencies.

    +
    --only-dev

    Only include the development dependency group.

    + +

    Omit other dependencies. The project itself will also be omitted.

    + +

    This option is an alias for --only-group dev.

    + +
    --only-group only-group

    Only include dependencies from the specified dependency group.

    + +

    May be provided multiple times.

    The project itself will also be omitted.

    @@ -1671,7 +1734,9 @@ uv lock [OPTIONS]

    May also be set with the UV_INSECURE_HOST environment variable.

    --cache-dir cache-dir

    Path to the cache directory.

    -

    Defaults to $HOME/Library/Caches/uv on macOS, $XDG_CACHE_HOME/uv or $HOME/.cache/uv on Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    +

    Defaults to $XDG_CACHE_HOME/uv or $HOME/.cache/uv on macOS and Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    + +

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    --color color-choice

    Control colors in output

    @@ -1706,6 +1771,10 @@ uv lock [OPTIONS]

    See --project to only change the project root directory.

    +
    --dry-run

    Perform a dry run, without writing the lockfile.

    + +

    In dry-run mode, uv will resolve the project’s dependencies and report on the resulting changes, but will not write the lockfile to disk.

    +
    --exclude-newer exclude-newer

    Limit candidate packages to those that were uploaded prior to the given date.

    Accepts both RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z) and local dates in the same format (e.g., 2006-12-02) in your system’s configured time zone.

    @@ -1837,6 +1906,7 @@ uv lock [OPTIONS]

    For example, spinners or progress bars.

    +

    May also be set with the UV_NO_PROGRESS environment variable.

    --no-python-downloads

    Disable automatic downloads of Python.

    --no-sources

    Ignore the tool.uv.sources table when resolving dependencies. Used to lock against the standards-compliant, publishable package metadata, as opposed to using any local or Git sources

    @@ -1963,7 +2033,9 @@ uv export [OPTIONS]

    May also be set with the UV_INSECURE_HOST environment variable.

    --cache-dir cache-dir

    Path to the cache directory.

    -

    Defaults to $HOME/Library/Caches/uv on macOS, $XDG_CACHE_HOME/uv or $HOME/.cache/uv on Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    +

    Defaults to $XDG_CACHE_HOME/uv or $HOME/.cache/uv on macOS and Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    + +

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    --color color-choice

    Control colors in output

    @@ -2003,7 +2075,7 @@ uv export [OPTIONS]

    Accepts both RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z) and local dates in the same format (e.g., 2006-12-02) in your system’s configured time zone.

    May also be set with the UV_EXCLUDE_NEWER environment variable.

    -
    --extra extra

    Include optional dependencies from the extra group name.

    +
    --extra extra

    Include optional dependencies from the specified extra name.

    May be provided more than once.

    @@ -2036,6 +2108,10 @@ uv export [OPTIONS]

    If a uv.lock does not exist, uv will exit with an error.

    May also be set with the UV_FROZEN environment variable.

    +
    --group group

    Include dependencies from the specified dependency group.

    + +

    May be provided multiple times.

    +
    --help, -h

    Display the concise help for this command

    --index index

    The URLs to use when resolving dependencies, in addition to the default index.

    @@ -2139,7 +2215,9 @@ uv export [OPTIONS]

    Normally, configuration files are discovered in the current directory, parent directories, or user configuration directories.

    May also be set with the UV_NO_CONFIG environment variable.

    -
    --no-dev

    Omit development dependencies

    +
    --no-dev

    Omit the development dependency group.

    + +

    This option is an alias for --no-group dev.

    --no-editable

    Install any editable dependencies, including the project and any workspace members, as non-editable

    @@ -2155,6 +2233,10 @@ uv export [OPTIONS]

    By default, all workspace members and their dependencies are included in the exported requirements file, with all of their dependencies. The --no-emit-workspace option allows exclusion of all the workspace members while retaining their dependencies.

    +
    --no-group no-group

    Exclude dependencies from the specified dependency group.

    + +

    May be provided multiple times.

    +
    --no-hashes

    Omit hashes in the generated output

    --no-header

    Exclude the comment header at the top of the generated output file

    @@ -2165,6 +2247,7 @@ uv export [OPTIONS]

    For example, spinners or progress bars.

    +

    May also be set with the UV_NO_PROGRESS environment variable.

    --no-python-downloads

    Disable automatic downloads of Python.

    --no-sources

    Ignore the tool.uv.sources table when resolving dependencies. Used to lock against the standards-compliant, publishable package metadata, as opposed to using any local or Git sources

    @@ -2173,7 +2256,15 @@ uv export [OPTIONS]

    When disabled, uv will only use locally cached data and locally available files.

    -
    --only-dev

    Omit non-development dependencies.

    +
    --only-dev

    Only include the development dependency group.

    + +

    Omit other dependencies. The project itself will also be omitted.

    + +

    This option is an alias for --only-group dev.

    + +
    --only-group only-group

    Only include dependencies from the specified dependency group.

    + +

    May be provided multiple times.

    The project itself will also be omitted.

    @@ -2291,7 +2382,9 @@ uv tree [OPTIONS]

    May also be set with the UV_INSECURE_HOST environment variable.

    --cache-dir cache-dir

    Path to the cache directory.

    -

    Defaults to $HOME/Library/Caches/uv on macOS, $XDG_CACHE_HOME/uv or $HOME/.cache/uv on Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    +

    Defaults to $XDG_CACHE_HOME/uv or $HOME/.cache/uv on macOS and Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    + +

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    --color color-choice

    Control colors in output

    @@ -2353,6 +2446,10 @@ uv tree [OPTIONS]

    If the lockfile is missing, uv will exit with an error.

    May also be set with the UV_FROZEN environment variable.

    +
    --group group

    Include dependencies from the specified dependency group.

    + +

    May be provided multiple times.

    +
    --help, -h

    Display the concise help for this command

    --index index

    The URLs to use when resolving dependencies, in addition to the default index.

    @@ -2460,7 +2557,13 @@ uv tree [OPTIONS]

    May also be set with the UV_NO_CONFIG environment variable.

    --no-dedupe

    Do not de-duplicate repeated dependencies. Usually, when a package has already displayed its dependencies, further occurrences will not re-display its dependencies, and will include a (*) to indicate it has already been shown. This flag will cause those duplicates to be repeated

    -
    --no-dev

    Omit development dependencies

    +
    --no-dev

    Omit the development dependency group.

    + +

    This option is an alias for --no-group dev.

    + +
    --no-group no-group

    Exclude dependencies from the specified dependency group.

    + +

    May be provided multiple times.

    --no-index

    Ignore the registry index (e.g., PyPI), instead relying on direct URL dependencies and those provided via --find-links

    @@ -2468,6 +2571,7 @@ uv tree [OPTIONS]

    For example, spinners or progress bars.

    +

    May also be set with the UV_NO_PROGRESS environment variable.

    --no-python-downloads

    Disable automatic downloads of Python.

    --no-sources

    Ignore the tool.uv.sources table when resolving dependencies. Used to lock against the standards-compliant, publishable package metadata, as opposed to using any local or Git sources

    @@ -2476,6 +2580,18 @@ uv tree [OPTIONS]

    When disabled, uv will only use locally cached data and locally available files.

    +
    --only-dev

    Only include the development dependency group.

    + +

    Omit other dependencies. The project itself will also be omitted.

    + +

    This option is an alias for --only-group dev.

    + +
    --only-group only-group

    Only include dependencies from the specified dependency group.

    + +

    May be provided multiple times.

    + +

    The project itself will also be omitted.

    +
    --package package

    Display only the specified packages

    --prerelease prerelease

    The strategy to use when considering pre-release versions.

    @@ -2677,7 +2793,9 @@ uv tool run [OPTIONS] [COMMAND]

    May also be set with the UV_INSECURE_HOST environment variable.

    --cache-dir cache-dir

    Path to the cache directory.

    -

    Defaults to $HOME/Library/Caches/uv on macOS, $XDG_CACHE_HOME/uv or $HOME/.cache/uv on Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    +

    Defaults to $XDG_CACHE_HOME/uv or $HOME/.cache/uv on macOS and Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    + +

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    --color color-choice

    Control colors in output

    @@ -2846,6 +2964,7 @@ uv tool run [OPTIONS] [COMMAND]

    For example, spinners or progress bars.

    +

    May also be set with the UV_NO_PROGRESS environment variable.

    --no-python-downloads

    Disable automatic downloads of Python.

    --no-sources

    Ignore the tool.uv.sources table when resolving dependencies. Used to lock against the standards-compliant, publishable package metadata, as opposed to using any local or Git sources

    @@ -2980,7 +3099,9 @@ uv tool install [OPTIONS]

    May also be set with the UV_INSECURE_HOST environment variable.

    --cache-dir cache-dir

    Path to the cache directory.

    -

    Defaults to $HOME/Library/Caches/uv on macOS, $XDG_CACHE_HOME/uv or $HOME/.cache/uv on Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    +

    Defaults to $XDG_CACHE_HOME/uv or $HOME/.cache/uv on macOS and Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    + +

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    --color color-choice

    Control colors in output

    @@ -3147,6 +3268,7 @@ uv tool install [OPTIONS]

    For example, spinners or progress bars.

    +

    May also be set with the UV_NO_PROGRESS environment variable.

    --no-python-downloads

    Disable automatic downloads of Python.

    --no-sources

    Ignore the tool.uv.sources table when resolving dependencies. Used to lock against the standards-compliant, publishable package metadata, as opposed to using any local or Git sources

    @@ -3240,6 +3362,8 @@ uv tool install [OPTIONS]
    --with with

    Include the following extra requirements

    +
    --with-editable with-editable

    Include the given packages as editables

    +
    --with-requirements with-requirements

    Run all requirements listed in the given requirements.txt files

    @@ -3279,7 +3403,9 @@ uv tool upgrade [OPTIONS] ...

    May also be set with the UV_INSECURE_HOST environment variable.

    --cache-dir cache-dir

    Path to the cache directory.

    -

    Defaults to $HOME/Library/Caches/uv on macOS, $XDG_CACHE_HOME/uv or $HOME/.cache/uv on Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    +

    Defaults to $XDG_CACHE_HOME/uv or $HOME/.cache/uv on macOS and Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    + +

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    --color color-choice

    Control colors in output

    @@ -3442,6 +3568,7 @@ uv tool upgrade [OPTIONS] ...

    For example, spinners or progress bars.

    +

    May also be set with the UV_NO_PROGRESS environment variable.

    --no-python-downloads

    Disable automatic downloads of Python.

    --no-sources

    Ignore the tool.uv.sources table when resolving dependencies. Used to lock against the standards-compliant, publishable package metadata, as opposed to using any local or Git sources

    @@ -3545,7 +3672,9 @@ uv tool list [OPTIONS]
    --cache-dir cache-dir

    Path to the cache directory.

    -

    Defaults to $HOME/Library/Caches/uv on macOS, $XDG_CACHE_HOME/uv or $HOME/.cache/uv on Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    +

    Defaults to $XDG_CACHE_HOME/uv or $HOME/.cache/uv on macOS and Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    + +

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    --color color-choice

    Control colors in output

    @@ -3592,6 +3721,7 @@ uv tool list [OPTIONS]

    For example, spinners or progress bars.

    +

    May also be set with the UV_NO_PROGRESS environment variable.

    --offline

    Disable network access.

    When disabled, uv will only use locally cached data and locally available files.

    @@ -3642,7 +3772,9 @@ uv tool uninstall [OPTIONS] ...
    --cache-dir cache-dir

    Path to the cache directory.

    -

    Defaults to $HOME/Library/Caches/uv on macOS, $XDG_CACHE_HOME/uv or $HOME/.cache/uv on Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    +

    Defaults to $XDG_CACHE_HOME/uv or $HOME/.cache/uv on macOS and Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    + +

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    --color color-choice

    Control colors in output

    @@ -3689,6 +3821,7 @@ uv tool uninstall [OPTIONS] ...

    For example, spinners or progress bars.

    +

    May also be set with the UV_NO_PROGRESS environment variable.

    --no-python-downloads

    Disable automatic downloads of Python.

    --offline

    Disable network access.

    @@ -3751,7 +3884,9 @@ uv tool update-shell [OPTIONS]
    --cache-dir cache-dir

    Path to the cache directory.

    -

    Defaults to $HOME/Library/Caches/uv on macOS, $XDG_CACHE_HOME/uv or $HOME/.cache/uv on Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    +

    Defaults to $XDG_CACHE_HOME/uv or $HOME/.cache/uv on macOS and Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    + +

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    --color color-choice

    Control colors in output

    @@ -3798,6 +3933,7 @@ uv tool update-shell [OPTIONS]

    For example, spinners or progress bars.

    +

    May also be set with the UV_NO_PROGRESS environment variable.

    --no-python-downloads

    Disable automatic downloads of Python.

    --offline

    Disable network access.

    @@ -3878,7 +4014,9 @@ uv tool dir [OPTIONS]
    --cache-dir cache-dir

    Path to the cache directory.

    -

    Defaults to $HOME/Library/Caches/uv on macOS, $XDG_CACHE_HOME/uv or $HOME/.cache/uv on Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    +

    Defaults to $XDG_CACHE_HOME/uv or $HOME/.cache/uv on macOS and Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    + +

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    --color color-choice

    Control colors in output

    @@ -3925,6 +4063,7 @@ uv tool dir [OPTIONS]

    For example, spinners or progress bars.

    +

    May also be set with the UV_NO_PROGRESS environment variable.

    --no-python-downloads

    Disable automatic downloads of Python.

    --offline

    Disable network access.

    @@ -4061,7 +4200,9 @@ uv python list [OPTIONS]
    --cache-dir cache-dir

    Path to the cache directory.

    -

    Defaults to $HOME/Library/Caches/uv on macOS, $XDG_CACHE_HOME/uv or $HOME/.cache/uv on Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    +

    Defaults to $XDG_CACHE_HOME/uv or $HOME/.cache/uv on macOS and Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    + +

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    --color color-choice

    Control colors in output

    @@ -4108,6 +4249,7 @@ uv python list [OPTIONS]

    For example, spinners or progress bars.

    +

    May also be set with the UV_NO_PROGRESS environment variable.

    --no-python-downloads

    Disable automatic downloads of Python.

    --offline

    Disable network access.

    @@ -4160,11 +4302,11 @@ Download and install Python versions. Multiple Python versions may be requested. -Supports CPython and PyPy. +Supports CPython and PyPy. CPython distributions are downloaded from the `python-build-standalone` project. PyPy distributions are downloaded from `python.org`. -CPython distributions are downloaded from the `python-build-standalone` project. +Python versions are installed into the uv Python directory, which can be retrieved with `uv python dir`. -Python versions are installed into the uv Python directory, which can be retrieved with `uv python dir`. A `python` executable is not made globally available, managed Python versions are only used in uv commands or in active virtual environments. +A `python` executable is not made globally available, managed Python versions are only used in uv commands or in active virtual environments. There is experimental support for adding Python executables to the `PATH` — use the `--preview` flag to enable this behavior. See `uv help python` to view supported request formats. @@ -4188,7 +4330,9 @@ uv python install [OPTIONS] [TARGETS]...
    --cache-dir cache-dir

    Path to the cache directory.

    -

    Defaults to $HOME/Library/Caches/uv on macOS, $XDG_CACHE_HOME/uv or $HOME/.cache/uv on Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    +

    Defaults to $XDG_CACHE_HOME/uv or $HOME/.cache/uv on macOS and Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    + +

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    --color color-choice

    Control colors in output

    @@ -4235,6 +4379,7 @@ uv python install [OPTIONS] [TARGETS]...

    For example, spinners or progress bars.

    +

    May also be set with the UV_NO_PROGRESS environment variable.

    --no-python-downloads

    Disable automatic downloads of Python.

    --offline

    Disable network access.

    @@ -4307,7 +4452,9 @@ uv python find [OPTIONS] [REQUEST]
    --cache-dir cache-dir

    Path to the cache directory.

    -

    Defaults to $HOME/Library/Caches/uv on macOS, $XDG_CACHE_HOME/uv or $HOME/.cache/uv on Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    +

    Defaults to $XDG_CACHE_HOME/uv or $HOME/.cache/uv on macOS and Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    + +

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    --color color-choice

    Control colors in output

    @@ -4354,6 +4501,7 @@ uv python find [OPTIONS] [REQUEST]

    For example, spinners or progress bars.

    +

    May also be set with the UV_NO_PROGRESS environment variable.

    --no-project

    Avoid discovering a project or workspace.

    Otherwise, when no request is provided, the Python requirement of a project in the current directory or parent directories will be used.

    @@ -4435,7 +4583,9 @@ uv python pin [OPTIONS] [REQUEST]
    --cache-dir cache-dir

    Path to the cache directory.

    -

    Defaults to $HOME/Library/Caches/uv on macOS, $XDG_CACHE_HOME/uv or $HOME/.cache/uv on Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    +

    Defaults to $XDG_CACHE_HOME/uv or $HOME/.cache/uv on macOS and Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    + +

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    --color color-choice

    Control colors in output

    @@ -4482,6 +4632,7 @@ uv python pin [OPTIONS] [REQUEST]

    For example, spinners or progress bars.

    +

    May also be set with the UV_NO_PROGRESS environment variable.

    --no-project

    Avoid validating the Python pin is compatible with the project or workspace.

    By default, a project or workspace is discovered in the current directory or any parent directory. If a workspace is found, the Python pin is validated against the workspace’s requires-python constraint.

    @@ -4542,6 +4693,8 @@ By default, Python installations are stored in the uv data directory at `$XDG_DA The Python installation directory may be overridden with `$UV_PYTHON_INSTALL_DIR`. +To view the directory where uv installs Python executables instead, use the `--bin` flag. Note that Python executables are only installed when preview mode is enabled. +

    Usage

    ``` @@ -4550,9 +4703,27 @@ uv python dir [OPTIONS]

    Options

    -
    --cache-dir cache-dir

    Path to the cache directory.

    +
    --bin

    Show the directory into which uv python will install Python executables.

    + +

    Note that this directory is only used when installing Python with preview mode enabled.

    + +

    The Python executable directory is determined according to the XDG standard and is derived from the following environment variables, in order of preference:

    + +
      +
    • $UV_PYTHON_BIN_DIR
    • + +
    • $XDG_BIN_HOME
    • + +
    • $XDG_DATA_HOME/../bin
    • + +
    • $HOME/.local/bin
    • +
    + +
    --cache-dir cache-dir

    Path to the cache directory.

    + +

    Defaults to $XDG_CACHE_HOME/uv or $HOME/.cache/uv on macOS and Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    -

    Defaults to $HOME/Library/Caches/uv on macOS, $XDG_CACHE_HOME/uv or $HOME/.cache/uv on Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    +

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    --color color-choice

    Control colors in output

    @@ -4599,6 +4770,7 @@ uv python dir [OPTIONS]

    For example, spinners or progress bars.

    +

    May also be set with the UV_NO_PROGRESS environment variable.

    --no-python-downloads

    Disable automatic downloads of Python.

    --offline

    Disable network access.

    @@ -4665,7 +4837,9 @@ uv python uninstall [OPTIONS] ...
    --cache-dir cache-dir

    Path to the cache directory.

    -

    Defaults to $HOME/Library/Caches/uv on macOS, $XDG_CACHE_HOME/uv or $HOME/.cache/uv on Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    +

    Defaults to $XDG_CACHE_HOME/uv or $HOME/.cache/uv on macOS and Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    + +

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    --color color-choice

    Control colors in output

    @@ -4712,6 +4886,7 @@ uv python uninstall [OPTIONS] ...

    For example, spinners or progress bars.

    +

    May also be set with the UV_NO_PROGRESS environment variable.

    --no-python-downloads

    Disable automatic downloads of Python.

    --offline

    Disable network access.

    @@ -4841,7 +5016,9 @@ uv pip compile [OPTIONS] ...

    May also be set with the UV_BUILD_CONSTRAINT environment variable.

    --cache-dir cache-dir

    Path to the cache directory.

    -

    Defaults to $HOME/Library/Caches/uv on macOS, $XDG_CACHE_HOME/uv or $HOME/.cache/uv on Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    +

    Defaults to $XDG_CACHE_HOME/uv or $HOME/.cache/uv on macOS and Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    + +

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    --color color-choice

    Control colors in output

    @@ -4901,7 +5078,7 @@ uv pip compile [OPTIONS] ...

    Accepts both RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z) and local dates in the same format (e.g., 2006-12-02) in your system’s configured time zone.

    May also be set with the UV_EXCLUDE_NEWER environment variable.

    -
    --extra extra

    Include optional dependencies from the extra group name; may be provided more than once.

    +
    --extra extra

    Include optional dependencies from the specified extra name; may be provided more than once.

    Only applies to pyproject.toml, setup.py, and setup.cfg sources.

    @@ -5028,6 +5205,7 @@ uv pip compile [OPTIONS] ...

    For example, spinners or progress bars.

    +

    May also be set with the UV_NO_PROGRESS environment variable.

    --no-python-downloads

    Disable automatic downloads of Python.

    --no-sources

    Ignore the tool.uv.sources table when resolving dependencies. Used to lock against the standards-compliant, publishable package metadata, as opposed to using any local or Git sources

    @@ -5252,7 +5430,9 @@ uv pip sync [OPTIONS] ...

    May also be set with the UV_BUILD_CONSTRAINT environment variable.

    --cache-dir cache-dir

    Path to the cache directory.

    -

    Defaults to $HOME/Library/Caches/uv on macOS, $XDG_CACHE_HOME/uv or $HOME/.cache/uv on Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    +

    Defaults to $XDG_CACHE_HOME/uv or $HOME/.cache/uv on macOS and Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    + +

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    --color color-choice

    Control colors in output

    @@ -5415,6 +5595,7 @@ uv pip sync [OPTIONS] ...

    For example, spinners or progress bars.

    +

    May also be set with the UV_NO_PROGRESS environment variable.

    --no-python-downloads

    Disable automatic downloads of Python.

    --no-sources

    Ignore the tool.uv.sources table when resolving dependencies. Used to lock against the standards-compliant, publishable package metadata, as opposed to using any local or Git sources

    @@ -5534,7 +5715,7 @@ uv pip sync [OPTIONS] ...

    May also be set with the UV_REQUIRE_HASHES environment variable.

    -
    --strict

    Validate the Python environment after completing the installation, to detect and with missing dependencies or other issues

    +
    --strict

    Validate the Python environment after completing the installation, to detect packages with missing dependencies or other issues

    --system

    Install packages into the system Python environment.

    @@ -5603,7 +5784,9 @@ uv pip install [OPTIONS] |--editable May also be set with the UV_BUILD_CONSTRAINT environment variable.

    --cache-dir cache-dir

    Path to the cache directory.

    -

    Defaults to $HOME/Library/Caches/uv on macOS, $XDG_CACHE_HOME/uv or $HOME/.cache/uv on Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    +

    Defaults to $XDG_CACHE_HOME/uv or $HOME/.cache/uv on macOS and Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    + +

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    --color color-choice

    Control colors in output

    @@ -5665,7 +5848,7 @@ uv pip install [OPTIONS] |--editable Accepts both RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z) and local dates in the same format (e.g., 2006-12-02) in your system’s configured time zone.

    May also be set with the UV_EXCLUDE_NEWER environment variable.

    -
    --extra extra

    Include optional dependencies from the extra group name; may be provided more than once.

    +
    --extra extra

    Include optional dependencies from the specified extra name; may be provided more than once.

    Only applies to pyproject.toml, setup.py, and setup.cfg sources.

    @@ -5787,6 +5970,7 @@ uv pip install [OPTIONS] |--editable For example, spinners or progress bars.

    +

    May also be set with the UV_NO_PROGRESS environment variable.

    --no-python-downloads

    Disable automatic downloads of Python.

    --no-sources

    Ignore the tool.uv.sources table when resolving dependencies. Used to lock against the standards-compliant, publishable package metadata, as opposed to using any local or Git sources

    @@ -5951,7 +6135,7 @@ uv pip install [OPTIONS] |--editable lowest-direct: Resolve the lowest compatible version of any direct dependencies, and the highest compatible version of any transitive dependencies -
    --strict

    Validate the Python environment after completing the installation, to detect and with missing dependencies or other issues

    +
    --strict

    Validate the Python environment after completing the installation, to detect packages with missing dependencies or other issues

    --system

    Install packages into the system Python environment.

    @@ -6013,7 +6197,9 @@ uv pip uninstall [OPTIONS] >

    May also be set with the UV_BREAK_SYSTEM_PACKAGES environment variable.

    --cache-dir cache-dir

    Path to the cache directory.

    -

    Defaults to $HOME/Library/Caches/uv on macOS, $XDG_CACHE_HOME/uv or $HOME/.cache/uv on Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    +

    Defaults to $XDG_CACHE_HOME/uv or $HOME/.cache/uv on macOS and Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    + +

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    --color color-choice

    Control colors in output

    @@ -6074,6 +6260,7 @@ uv pip uninstall [OPTIONS] >

    For example, spinners or progress bars.

    +

    May also be set with the UV_NO_PROGRESS environment variable.

    --no-python-downloads

    Disable automatic downloads of Python.

    --offline

    Disable network access.

    @@ -6150,7 +6337,9 @@ uv pip freeze [OPTIONS]
    --cache-dir cache-dir

    Path to the cache directory.

    -

    Defaults to $HOME/Library/Caches/uv on macOS, $XDG_CACHE_HOME/uv or $HOME/.cache/uv on Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    +

    Defaults to $XDG_CACHE_HOME/uv or $HOME/.cache/uv on macOS and Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    + +

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    --color color-choice

    Control colors in output

    @@ -6199,6 +6388,7 @@ uv pip freeze [OPTIONS]

    For example, spinners or progress bars.

    +

    May also be set with the UV_NO_PROGRESS environment variable.

    --no-python-downloads

    Disable automatic downloads of Python.

    --offline

    Disable network access.

    @@ -6271,7 +6461,9 @@ uv pip list [OPTIONS]
    --cache-dir cache-dir

    Path to the cache directory.

    -

    Defaults to $HOME/Library/Caches/uv on macOS, $XDG_CACHE_HOME/uv or $HOME/.cache/uv on Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    +

    Defaults to $XDG_CACHE_HOME/uv or $HOME/.cache/uv on macOS and Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    + +

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    --color color-choice

    Control colors in output

    @@ -6336,6 +6528,7 @@ uv pip list [OPTIONS]

    For example, spinners or progress bars.

    +

    May also be set with the UV_NO_PROGRESS environment variable.

    --no-python-downloads

    Disable automatic downloads of Python.

    --offline

    Disable network access.

    @@ -6414,7 +6607,9 @@ uv pip show [OPTIONS] [PACKAGE]...
    --cache-dir cache-dir

    Path to the cache directory.

    -

    Defaults to $HOME/Library/Caches/uv on macOS, $XDG_CACHE_HOME/uv or $HOME/.cache/uv on Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    +

    Defaults to $XDG_CACHE_HOME/uv or $HOME/.cache/uv on macOS and Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    + +

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    --color color-choice

    Control colors in output

    @@ -6463,6 +6658,7 @@ uv pip show [OPTIONS] [PACKAGE]...

    For example, spinners or progress bars.

    +

    May also be set with the UV_NO_PROGRESS environment variable.

    --no-python-downloads

    Disable automatic downloads of Python.

    --offline

    Disable network access.

    @@ -6535,7 +6731,9 @@ uv pip tree [OPTIONS]
    --cache-dir cache-dir

    Path to the cache directory.

    -

    Defaults to $HOME/Library/Caches/uv on macOS, $XDG_CACHE_HOME/uv or $HOME/.cache/uv on Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    +

    Defaults to $XDG_CACHE_HOME/uv or $HOME/.cache/uv on macOS and Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    + +

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    --color color-choice

    Control colors in output

    @@ -6589,6 +6787,7 @@ uv pip tree [OPTIONS]

    For example, spinners or progress bars.

    +

    May also be set with the UV_NO_PROGRESS environment variable.

    --no-python-downloads

    Disable automatic downloads of Python.

    --no-system
    --offline

    Disable network access.

    @@ -6667,7 +6866,9 @@ uv pip check [OPTIONS]
    --cache-dir cache-dir

    Path to the cache directory.

    -

    Defaults to $HOME/Library/Caches/uv on macOS, $XDG_CACHE_HOME/uv or $HOME/.cache/uv on Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    +

    Defaults to $XDG_CACHE_HOME/uv or $HOME/.cache/uv on macOS and Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    + +

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    --color color-choice

    Control colors in output

    @@ -6714,6 +6915,7 @@ uv pip check [OPTIONS]

    For example, spinners or progress bars.

    +

    May also be set with the UV_NO_PROGRESS environment variable.

    --no-python-downloads

    Disable automatic downloads of Python.

    --offline

    Disable network access.

    @@ -6817,7 +7019,9 @@ uv venv [OPTIONS] [PATH]

    May also be set with the UV_INSECURE_HOST environment variable.

    --cache-dir cache-dir

    Path to the cache directory.

    -

    Defaults to $HOME/Library/Caches/uv on macOS, $XDG_CACHE_HOME/uv or $HOME/.cache/uv on Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    +

    Defaults to $XDG_CACHE_HOME/uv or $HOME/.cache/uv on macOS and Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    + +

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    --color color-choice

    Control colors in output

    @@ -6952,6 +7156,7 @@ uv venv [OPTIONS] [PATH]

    For example, spinners or progress bars.

    +

    May also be set with the UV_NO_PROGRESS environment variable.

    --no-project

    Avoid discovering a project or workspace.

    By default, uv searches for projects in the current directory or any parent directory to determine the default path of the virtual environment and check for Python version constraints, if any.

    @@ -7077,7 +7282,9 @@ uv build [OPTIONS] [SRC]

    May also be set with the UV_BUILD_CONSTRAINT environment variable.

    --cache-dir cache-dir

    Path to the cache directory.

    -

    Defaults to $HOME/Library/Caches/uv on macOS, $XDG_CACHE_HOME/uv or $HOME/.cache/uv on Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    +

    Defaults to $XDG_CACHE_HOME/uv or $HOME/.cache/uv on macOS and Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    + +

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    --color color-choice

    Control colors in output

    @@ -7235,6 +7442,7 @@ uv build [OPTIONS] [SRC]

    For example, spinners or progress bars.

    +

    May also be set with the UV_NO_PROGRESS environment variable.

    --no-python-downloads

    Disable automatic downloads of Python.

    --no-sources

    Ignore the tool.uv.sources table when resolving dependencies. Used to lock against the standards-compliant, publishable package metadata, as opposed to using any local or Git sources

    @@ -7387,9 +7595,22 @@ uv publish [OPTIONS] [FILES]...

    May also be set with the UV_INSECURE_HOST environment variable.

    --cache-dir cache-dir

    Path to the cache directory.

    -

    Defaults to $HOME/Library/Caches/uv on macOS, $XDG_CACHE_HOME/uv or $HOME/.cache/uv on Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    +

    Defaults to $XDG_CACHE_HOME/uv or $HOME/.cache/uv on macOS and Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    + +

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    +
    --check-url check-url

    Check an index URL for existing files to skip duplicate uploads.

    + +

    This option allows retrying publishing that failed after only some, but not all files have been uploaded, and handles error due to parallel uploads of the same file.

    + +

    Before uploading, the index is checked. If the exact same file already exists in the index, the file will not be uploaded. If an error occurred during the upload, the index is checked again, to handle cases where the identical file was uploaded twice in parallel.

    + +

    The exact behavior will vary based on the index. When uploading to PyPI, uploading the same file succeeds even without --check-url, while most other indexes error.

    + +

    The index must provide one of the supported hashes (SHA-256, SHA-384, or SHA-512).

    + +

    May also be set with the UV_PUBLISH_CHECK_URL environment variable.

    --color color-choice

    Control colors in output

    [default: auto]

    @@ -7448,6 +7669,7 @@ uv publish [OPTIONS] [FILES]...

    For example, spinners or progress bars.

    +

    May also be set with the UV_NO_PROGRESS environment variable.

    --no-python-downloads

    Disable automatic downloads of Python.

    --offline

    Disable network access.

    @@ -7473,8 +7695,6 @@ uv publish [OPTIONS] [FILES]...

    Defaults to PyPI’s publish URL (<https://upload.pypi.org/legacy/>).

    -

    The default value is publish URL for PyPI (<https://upload.pypi.org/legacy/>).

    -

    May also be set with the UV_PUBLISH_URL environment variable.

    --python-preference python-preference

    Whether to prefer uv-managed or system Python installations.

    @@ -7563,7 +7783,9 @@ uv cache clean [OPTIONS] [PACKAGE]...
    --cache-dir cache-dir

    Path to the cache directory.

    -

    Defaults to $HOME/Library/Caches/uv on macOS, $XDG_CACHE_HOME/uv or $HOME/.cache/uv on Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    +

    Defaults to $XDG_CACHE_HOME/uv or $HOME/.cache/uv on macOS and Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    + +

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    --color color-choice

    Control colors in output

    @@ -7610,6 +7832,7 @@ uv cache clean [OPTIONS] [PACKAGE]...

    For example, spinners or progress bars.

    +

    May also be set with the UV_NO_PROGRESS environment variable.

    --no-python-downloads

    Disable automatic downloads of Python.

    --offline

    Disable network access.

    @@ -7666,7 +7889,9 @@ uv cache prune [OPTIONS]
    --cache-dir cache-dir

    Path to the cache directory.

    -

    Defaults to $HOME/Library/Caches/uv on macOS, $XDG_CACHE_HOME/uv or $HOME/.cache/uv on Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    +

    Defaults to $XDG_CACHE_HOME/uv or $HOME/.cache/uv on macOS and Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    + +

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    --ci

    Optimize the cache for persistence in a continuous integration environment, like GitHub Actions.

    @@ -7719,6 +7944,7 @@ uv cache prune [OPTIONS]

    For example, spinners or progress bars.

    +

    May also be set with the UV_NO_PROGRESS environment variable.

    --no-python-downloads

    Disable automatic downloads of Python.

    --offline

    Disable network access.

    @@ -7783,7 +8009,9 @@ uv cache dir [OPTIONS]
    --cache-dir cache-dir

    Path to the cache directory.

    -

    Defaults to $HOME/Library/Caches/uv on macOS, $XDG_CACHE_HOME/uv or $HOME/.cache/uv on Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    +

    Defaults to $XDG_CACHE_HOME/uv or $HOME/.cache/uv on macOS and Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    + +

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    --color color-choice

    Control colors in output

    @@ -7830,6 +8058,7 @@ uv cache dir [OPTIONS]

    For example, spinners or progress bars.

    +

    May also be set with the UV_NO_PROGRESS environment variable.

    --no-python-downloads

    Disable automatic downloads of Python.

    --offline

    Disable network access.

    @@ -7908,7 +8137,9 @@ uv self update [OPTIONS] [TARGET_VERSION]
    --cache-dir cache-dir

    Path to the cache directory.

    -

    Defaults to $HOME/Library/Caches/uv on macOS, $XDG_CACHE_HOME/uv or $HOME/.cache/uv on Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    +

    Defaults to $XDG_CACHE_HOME/uv or $HOME/.cache/uv on macOS and Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    + +

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    --color color-choice

    Control colors in output

    @@ -7955,6 +8186,7 @@ uv self update [OPTIONS] [TARGET_VERSION]

    For example, spinners or progress bars.

    +

    May also be set with the UV_NO_PROGRESS environment variable.

    --no-python-downloads

    Disable automatic downloads of Python.

    --offline

    Disable network access.

    @@ -8014,7 +8246,9 @@ uv version [OPTIONS]
    --cache-dir cache-dir

    Path to the cache directory.

    -

    Defaults to $HOME/Library/Caches/uv on macOS, $XDG_CACHE_HOME/uv or $HOME/.cache/uv on Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    +

    Defaults to $XDG_CACHE_HOME/uv or $HOME/.cache/uv on macOS and Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    + +

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    --color color-choice

    Control colors in output

    @@ -8061,6 +8295,7 @@ uv version [OPTIONS]

    For example, spinners or progress bars.

    +

    May also be set with the UV_NO_PROGRESS environment variable.

    --no-python-downloads

    Disable automatic downloads of Python.

    --offline

    Disable network access.

    @@ -8157,7 +8392,9 @@ uv help [OPTIONS] [COMMAND]...
    --cache-dir cache-dir

    Path to the cache directory.

    -

    Defaults to $HOME/Library/Caches/uv on macOS, $XDG_CACHE_HOME/uv or $HOME/.cache/uv on Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    +

    Defaults to $XDG_CACHE_HOME/uv or $HOME/.cache/uv on macOS and Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    + +

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    --color color-choice

    Control colors in output

    @@ -8206,6 +8443,7 @@ uv help [OPTIONS] [COMMAND]...

    For example, spinners or progress bars.

    +

    May also be set with the UV_NO_PROGRESS environment variable.

    --no-python-downloads

    Disable automatic downloads of Python.

    --offline

    Disable network access.

    diff --git a/docs/reference/settings.md b/docs/reference/settings.md index 531f6c0c4821..222eed8be524 100644 --- a/docs/reference/settings.md +++ b/docs/reference/settings.md @@ -30,10 +30,34 @@ constraint-dependencies = ["grpcio<1.65"] --- +### [`default-groups`](#default-groups) {: #default-groups } + +The list of `dependency-groups` to install by default. + +**Default value**: `["dev"]` + +**Type**: `list[str]` + +**Example usage**: + +```toml title="pyproject.toml" +[tool.uv] +default-groups = ["docs"] +``` + +--- + ### [`dev-dependencies`](#dev-dependencies) {: #dev-dependencies } -The project's development dependencies. Development dependencies will be installed by -default in `uv run` and `uv sync`, but will not appear in the project's published metadata. +The project's development dependencies. + +Development dependencies will be installed by default in `uv run` and `uv sync`, but will +not appear in the project's published metadata. + +Use of this field is not recommend anymore. Instead, use the `dependency-groups.dev` field +which is a standardized way to declare development dependencies. The contents of +`tool.uv.dev-dependencies` and `dependency-groups.dev` are combined to determine the the +final requirements of the `dev` dependency group. **Default value**: `[]` @@ -110,7 +134,7 @@ PyPI default index. **Example usage**: ```toml title="pyproject.toml" -[tool.uv] + [[tool.uv.index]] name = "pytorch" url = "https://download.pytorch.org/whl/cu121" @@ -198,6 +222,32 @@ package = false --- +### [`sources`](#sources) {: #sources } + +The sources to use when resolving dependencies. + +`tool.uv.sources` enriches the dependency metadata with additional sources, incorporated +during development. A dependency source can be a Git repository, a URL, a local path, or an +alternative registry. + +See [Dependencies](../concepts/dependencies.md) for more. + +**Default value**: `"[]"` + +**Type**: `dict` + +**Example usage**: + +```toml title="pyproject.toml" + +[tool.uv.sources] +httpx = { git = "https://github.com/encode/httpx", tag = "0.27.0" } +pytest = { url = "https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl" } +pydantic = { path = "/path/to/pydantic", editable = true } +``` + +--- + ### `workspace` #### [`exclude`](#workspace_exclude) {: #workspace_exclude } @@ -272,7 +322,6 @@ bypasses SSL verification and could expose you to MITM attacks. === "uv.toml" ```toml - allow-insecure-host = ["localhost:8080"] ``` @@ -300,7 +349,6 @@ Linux, and `%LOCALAPPDATA%\uv\cache` on Windows. === "uv.toml" ```toml - cache-dir = "./.uv_cache" ``` @@ -353,7 +401,6 @@ globs are interpreted as relative to the project directory. === "uv.toml" ```toml - cache-keys = [{ file = "pyproject.toml" }, { file = "requirements.txt" }, { git = { commit = true }] ``` @@ -387,7 +434,6 @@ ignore errors. === "uv.toml" ```toml - compile-bytecode = true ``` @@ -415,7 +461,6 @@ Defaults to the number of available CPU cores. === "uv.toml" ```toml - concurrent-builds = 4 ``` @@ -441,7 +486,6 @@ time. === "uv.toml" ```toml - concurrent-downloads = 4 ``` @@ -468,7 +512,6 @@ Defaults to the number of available CPU cores. === "uv.toml" ```toml - concurrent-installs = 4 ``` @@ -494,7 +537,6 @@ specified as `KEY=VALUE` pairs. === "uv.toml" ```toml - config-settings = { editable_mode = "compat" } ``` @@ -533,7 +575,6 @@ standard, though only the following fields are respected: === "uv.toml" ```toml - dependency-metadata = [ { name = "flask", version = "1.0.0", requires-dist = ["werkzeug"], requires-python = ">=3.6" }, ] @@ -564,7 +605,6 @@ system's configured time zone. === "uv.toml" ```toml - exclude-newer = "2006-12-02" ``` @@ -601,7 +641,6 @@ To control uv's resolution strategy when multiple indexes are present, see === "uv.toml" ```toml - extra-index-url = ["https://download.pytorch.org/whl/cpu"] ``` @@ -633,7 +672,6 @@ formats described above. === "uv.toml" ```toml - find-links = ["https://download.pytorch.org/whl/torch_stable.html"] ``` @@ -678,7 +716,6 @@ PyPI default index. === "pyproject.toml" ```toml - [tool.uv] [[tool.uv.index]] name = "pytorch" url = "https://download.pytorch.org/whl/cu121" @@ -686,7 +723,6 @@ PyPI default index. === "uv.toml" ```toml - [[tool.uv.index]] name = "pytorch" url = "https://download.pytorch.org/whl/cu121" @@ -722,7 +758,6 @@ same name to an alternate index. === "uv.toml" ```toml - index-strategy = "unsafe-best-match" ``` @@ -755,7 +790,6 @@ The index provided by this setting is given lower priority than any indexes spec === "uv.toml" ```toml - index-url = "https://test.pypi.org/simple" ``` @@ -783,7 +817,6 @@ use the `keyring` CLI to handle authentication. === "uv.toml" ```toml - keyring-provider = "subprocess" ``` @@ -816,7 +849,6 @@ Windows. === "uv.toml" ```toml - link-mode = "copy" ``` @@ -849,7 +881,6 @@ included in your system's certificate store. === "uv.toml" ```toml - native-tls = true ``` @@ -877,7 +908,6 @@ pre-built wheels to extract package metadata, if available. === "uv.toml" ```toml - no-binary = true ``` @@ -902,7 +932,6 @@ Don't install pre-built wheels for a specific package. === "uv.toml" ```toml - no-binary-package = ["ruff"] ``` @@ -931,7 +960,6 @@ distributions will exit with an error. === "uv.toml" ```toml - no-build = true ``` @@ -959,7 +987,6 @@ are already installed. === "uv.toml" ```toml - no-build-isolation = true ``` @@ -987,7 +1014,6 @@ are already installed. === "uv.toml" ```toml - no-build-isolation-package = ["package1", "package2"] ``` @@ -1012,7 +1038,6 @@ Don't build source distributions for a specific package. === "uv.toml" ```toml - no-build-package = ["ruff"] ``` @@ -1038,7 +1063,6 @@ duration of the operation. === "uv.toml" ```toml - no-cache = true ``` @@ -1064,7 +1088,6 @@ those provided via `--find-links`. === "uv.toml" ```toml - no-index = true ``` @@ -1091,7 +1114,6 @@ sources. === "uv.toml" ```toml - no-sources = true ``` @@ -1116,7 +1138,6 @@ Disable network access, relying only on locally cached data and locally availabl === "uv.toml" ```toml - offline = true ``` @@ -1151,7 +1172,6 @@ declared specifiers (`if-necessary-or-explicit`). === "uv.toml" ```toml - prerelease = "allow" ``` @@ -1176,7 +1196,6 @@ Whether to enable experimental, preview features. === "uv.toml" ```toml - preview = true ``` @@ -1202,7 +1221,6 @@ The URL for publishing packages to the Python package index (by default: === "uv.toml" ```toml - publish-url = "https://test.pypi.org/legacy/" ``` @@ -1231,7 +1249,6 @@ Whether to allow Python downloads. === "uv.toml" ```toml - python-downloads = "manual" ``` @@ -1262,7 +1279,6 @@ those that are downloaded and installed by uv. === "uv.toml" ```toml - python-preference = "managed" ``` @@ -1287,7 +1303,6 @@ Reinstall all packages, regardless of whether they're already installed. Implies === "uv.toml" ```toml - reinstall = true ``` @@ -1313,7 +1328,6 @@ Reinstall a specific package, regardless of whether it's already installed. Impl === "uv.toml" ```toml - reinstall-package = ["ruff"] ``` @@ -1345,7 +1359,6 @@ By default, uv will use the latest compatible version of each package (`highest` === "uv.toml" ```toml - resolution = "lowest-direct" ``` @@ -1374,7 +1387,6 @@ from a fork). === "uv.toml" ```toml - trusted-publishing = "always" ``` @@ -1399,7 +1411,6 @@ Allow package upgrades, ignoring pinned versions in any existing output file. === "uv.toml" ```toml - upgrade = true ``` @@ -1427,7 +1438,6 @@ Accepts both standalone package names (`ruff`) and version specifiers (`ruff<0.5 === "uv.toml" ```toml - upgrade-package = ["ruff"] ``` @@ -1890,7 +1900,7 @@ system's configured time zone. #### [`extra`](#pip_extra) {: #pip_extra } -Include optional dependencies from the extra group name; may be provided more than once. +Include optional dependencies from the specified extra; may be provided more than once. Only applies to `pyproject.toml`, `setup.py`, and `setup.cfg` sources. diff --git a/docs/reference/versioning.md b/docs/reference/versioning.md index 300605960210..62bab9abe6fd 100644 --- a/docs/reference/versioning.md +++ b/docs/reference/versioning.md @@ -7,3 +7,14 @@ uv does not yet have a stable API; once uv's API is stable (v1.0.0), the version adhere to [Semantic Versioning](https://semver.org/). uv's changelog can be [viewed on GitHub](https://github.com/astral-sh/uv/blob/main/CHANGELOG.md). + +## Cache versioning + +Cache versions are considered internal to uv, and so may be changed in a minor or patch release. See +[Cache versioning](../concepts/cache.md#cache-versioning) for more. + +## Lockfile versioning + +The `uv.lock` schema version is considered part of the public API, and so will only be incremented +in a minor release as a breaking change. See +[Lockfile versioning](../concepts/resolution.md#lockfile-versioning) for more. diff --git a/mkdocs.template.yml b/mkdocs.template.yml index b8817b5ebe2c..b5685b81cd7e 100644 --- a/mkdocs.template.yml +++ b/mkdocs.template.yml @@ -107,6 +107,7 @@ nav: - Environment variables: configuration/environment.md - Authentication: configuration/authentication.md - Package indexes: configuration/indexes.md + - Installer: configuration/installer.md - Integration guides: - guides/integration/index.md - Docker: guides/integration/docker.md diff --git a/pyproject.toml b/pyproject.toml index 07a6af884712..6a57687483b7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "uv" -version = "0.4.25" +version = "0.4.29" description = "An extremely fast Python package and project manager, written in Rust." authors = [{ name = "Astral Software Inc.", email = "hey@astral.sh" }] requires-python = ">=3.8" diff --git a/scripts/publish/test_publish.py b/scripts/publish/test_publish.py index eb6127e95861..4a20386681e4 100644 --- a/scripts/publish/test_publish.py +++ b/scripts/publish/test_publish.py @@ -6,9 +6,10 @@ # ] # /// -""" -Test `uv publish` by uploading a new version of astral-test- to one of -multiple indexes, exercising different options of passing credentials. +"""Test `uv publish`. + +Upload a new version of astral-test- to one of multiple indexes, exercising +different options of passing credentials. Locally, execute the credentials setting script, then run: ```shell @@ -43,66 +44,93 @@ Docs: https://docs.gitlab.com/ee/user/packages/pypi_repository/ **codeberg** -The username is astral-test-user, the password is a token (the actual account password would also -work). +The username is astral-test-user, the password is a token (the actual account password +would also work). Web: https://codeberg.org/astral-test-user/-/packages/pypi/astral-test-token/0.1.0 Docs: https://forgejo.org/docs/latest/user/packages/pypi/ """ import os import re +import time from argparse import ArgumentParser +from dataclasses import dataclass from pathlib import Path from shutil import rmtree -from subprocess import check_call +from subprocess import PIPE, check_call, check_output, run +from time import sleep import httpx from packaging.utils import parse_sdist_filename, parse_wheel_filename +from packaging.version import Version + +TEST_PYPI_PUBLISH_URL = "https://test.pypi.org/legacy/" cwd = Path(__file__).parent -# Map CLI target name to package name. + +@dataclass +class TargetConfiguration: + project_name: str + publish_url: str + index_url: str + + +# Map CLI target name to package name and index url. # Trusted publishing can only be tested on GitHub Actions, so we have separate local # and all targets. -local_targets = { - "pypi-token": "astral-test-token", - "pypi-password-env": "astral-test-password", - "pypi-keyring": "astral-test-keyring", - "gitlab": "astral-test-token", - "codeberg": "astral-test-token", - "cloudsmith": "astral-test-token", +local_targets: dict[str, TargetConfiguration] = { + "pypi-token": TargetConfiguration( + "astral-test-token", + TEST_PYPI_PUBLISH_URL, + "https://test.pypi.org/simple/", + ), + "pypi-password-env": TargetConfiguration( + "astral-test-password", + TEST_PYPI_PUBLISH_URL, + "https://test.pypi.org/simple/", + ), + "pypi-keyring": TargetConfiguration( + "astral-test-keyring", + "https://test.pypi.org/legacy/?astral-test-keyring", + "https://test.pypi.org/simple/", + ), + "gitlab": TargetConfiguration( + "astral-test-token", + "https://gitlab.com/api/v4/projects/61853105/packages/pypi", + "https://gitlab.com/api/v4/projects/61853105/packages/pypi/simple/", + ), + "codeberg": TargetConfiguration( + "astral-test-token", + "https://codeberg.org/api/packages/astral-test-user/pypi", + "https://codeberg.org/api/packages/astral-test-user/pypi/simple/", + ), + "cloudsmith": TargetConfiguration( + "astral-test-token", + "https://python.cloudsmith.io/astral-test/astral-test-1/", + "https://dl.cloudsmith.io/public/astral-test/astral-test-1/python/simple/", + ), } -all_targets = local_targets | { - "pypi-trusted-publishing": "astral-test-trusted-publishing" -} - -project_urls = { - "astral-test-password": ["https://test.pypi.org/simple/astral-test-password/"], - "astral-test-keyring": ["https://test.pypi.org/simple/astral-test-keyring/"], - "astral-test-trusted-publishing": [ - "https://test.pypi.org/simple/astral-test-trusted-publishing/" - ], - "astral-test-token": [ - "https://test.pypi.org/simple/astral-test-token/", - "https://gitlab.com/api/v4/projects/61853105/packages/pypi/simple/astral-test-token", - "https://codeberg.org/api/packages/astral-test-user/pypi/simple/astral-test-token", - "https://dl.cloudsmith.io/public/astral-test/astral-test-1/python/simple/astral-test-token/", - ], +all_targets: dict[str, TargetConfiguration] = local_targets | { + "pypi-trusted-publishing": TargetConfiguration( + "astral-test-trusted-publishing", + TEST_PYPI_PUBLISH_URL, + "https://test.pypi.org/simple/", + ) } -def get_new_version(project_name: str) -> str: - """Return the next free path version on pypi""" +def get_new_version(project_name: str, client: httpx.Client) -> Version: + """Return the next free patch version on all indexes of the package.""" # To keep the number of packages small we reuse them across targets, so we have to # pick a version that doesn't exist on any target yet versions = set() - for url in project_urls[project_name]: - try: - data = httpx.get(url).text - except httpx.HTTPError as err: - raise RuntimeError(f"Failed to fetch {url}") from err - href_text = "]+>([^<>]+)" - for filename in list(m.group(1) for m in re.finditer(href_text, data)): + for target_config in all_targets.values(): + if target_config.project_name != project_name: + continue + for filename in get_filenames( + target_config.index_url + project_name + "/", client + ): if filename.endswith(".whl"): [_name, version, _build, _tags] = parse_wheel_filename(filename) else: @@ -113,137 +141,242 @@ def get_new_version(project_name: str) -> str: # Bump the path version to obtain an empty version release = list(max_version.release) release[-1] += 1 - return ".".join(str(i) for i in release) + return Version(".".join(str(i) for i in release)) -def create_project(project_name: str, uv: Path): - if cwd.joinpath(project_name).exists(): - rmtree(cwd.joinpath(project_name)) - check_call([uv, "init", "--lib", project_name], cwd=cwd) - pyproject_toml = cwd.joinpath(project_name).joinpath("pyproject.toml") +def get_filenames(url: str, client: httpx.Client) -> list[str]: + """Get the filenames (source dists and wheels) from an index URL.""" + # Get with retries + error = None + for _ in range(5): + try: + response = client.get(url) + data = response.text + break + except httpx.HTTPError as err: + error = err + print(f"Error getting version, sleeping for 1s: {err}") + time.sleep(1) + else: + raise RuntimeError(f"Failed to fetch {url}") from error + # Works for the indexes in the list + href_text = r"([^<>]+)" + return [m.group(1) for m in re.finditer(href_text, data)] + + +def build_project_at_version( + project_name: str, version: Version, uv: Path, modified: bool = False +) -> Path: + """Build a source dist and a wheel with the project name and an unclaimed + version.""" + if modified: + dir_name = f"{project_name}-modified" + else: + dir_name = project_name + project_root = cwd.joinpath(dir_name) + + if project_root.exists(): + rmtree(project_root) + check_call([uv, "init", "--lib", "--name", project_name, dir_name], cwd=cwd) + pyproject_toml = project_root.joinpath("pyproject.toml") # Set to an unclaimed version toml = pyproject_toml.read_text() - new_version = get_new_version(project_name) - toml = re.sub('version = ".*"', f'version = "{new_version}"', toml) + toml = re.sub('version = ".*"', f'version = "{version}"', toml) pyproject_toml.write_text(toml) + # Modify the code so we get a different source dist and wheel + if modified: + init_py = ( + project_root.joinpath("src") + # dist info naming + .joinpath(project_name.replace("-", "_")) + .joinpath("__init__.py") + ) + init_py.write_text("x = 1") -def publish_project(target: str, uv: Path): - project_name = all_targets[target] + # Build the project + check_call([uv, "build"], cwd=project_root) - print(f"\nPublish {project_name} for {target}") + return project_root - # Create the project - create_project(project_name, uv) - # Build the project - check_call([uv, "build"], cwd=cwd.joinpath(project_name)) +def wait_for_index(index_url: str, project_name: str, version: Version, uv: Path): + """Check that the index URL was updated, wait up to 10s if necessary. - # Upload the project - if target == "pypi-token": - env = os.environ.copy() - env["UV_PUBLISH_TOKEN"] = os.environ["UV_TEST_PUBLISH_TOKEN"] - check_call( + Often enough the index takes a few seconds until the index is updated after an + upload. We need to specifically run this through uv since to query the same cache + (invalidation) as the registry client in skip existing in uv publish will later, + just `get_filenames` fails non-deterministically. + """ + for _ in range(10): + output = check_output( [ uv, - "publish", - "--publish-url", - "https://test.pypi.org/legacy/", + "pip", + "compile", + "--index", + index_url, + "--quiet", + "--generate-hashes", + "--no-header", + "--refresh-package", + project_name, + "-", ], - cwd=cwd.joinpath(project_name), - env=env, + text=True, + input=project_name, ) - elif target == "pypi-password-env": - env = os.environ.copy() - env["UV_PUBLISH_PASSWORD"] = os.environ["UV_TEST_PUBLISH_PASSWORD"] - check_call( - [ - uv, - "publish", - "--publish-url", - "https://test.pypi.org/legacy/", - "--username", - "__token__", - ], - cwd=cwd.joinpath(project_name), - env=env, + if f"{project_name}=={version}" in output and output.count("--hash") == 2: + break + + print( + f"uv pip compile not updated, missing 2 files for {version}: `{output.replace("\\\n ", "")}`, " + f"sleeping for 1s: `{index_url}`" ) - elif target == "pypi-keyring": - check_call( - [ - uv, - "publish", - "--publish-url", - "https://test.pypi.org/legacy/?astral-test-keyring", - "--username", - "__token__", - "--keyring-provider", - "subprocess", - ], - cwd=cwd.joinpath(project_name), + sleep(1) + + +def publish_project(target: str, uv: Path, client: httpx.Client): + """Test that: + + 1. An upload with a fresh version succeeds. + 2. If we're using PyPI, uploading the same files again succeeds. + 3. Check URL works and reports the files as skipped. + """ + project_name = all_targets[target].project_name + + print(f"\nPublish {project_name} for {target}") + + # The distributions are build to the dist directory of the project. + version = get_new_version(project_name, client) + project_dir = build_project_at_version(project_name, version, uv) + + # Upload configuration + publish_url = all_targets[target].publish_url + index_url = all_targets[target].index_url + env, extra_args = target_configuration(target) + env = {**os.environ, **env} + expected_filenames = [path.name for path in project_dir.joinpath("dist").iterdir()] + # Ignore the gitignore file in dist + expected_filenames.remove(".gitignore") + + print( + f"\n=== 1. Publishing a new version: {project_name} {version} {publish_url} ===" + ) + args = [uv, "publish", "--publish-url", publish_url, *extra_args] + check_call(args, cwd=project_dir, env=env) + + if publish_url == TEST_PYPI_PUBLISH_URL: + # Confirm pypi behaviour: Uploading the same file again is fine. + print(f"\n=== 2. Publishing {project_name} {version} again (PyPI) ===") + wait_for_index(index_url, project_name, version, uv) + args = [uv, "publish", "--publish-url", publish_url, *extra_args] + output = run( + args, cwd=project_dir, env=env, text=True, check=True, stderr=PIPE + ).stderr + if ( + output.count("Uploading") != len(expected_filenames) + or output.count("already exists") != 0 + ): + raise RuntimeError( + f"PyPI re-upload of the same files failed: " + f"{output.count("Uploading")}, {output.count("already exists")}\n" + f"---\n{output}\n---" + ) + + print(f"\n=== 3. Publishing {project_name} {version} again with check URL ===") + wait_for_index(index_url, project_name, version, uv) + args = [ + uv, + "publish", + "--publish-url", + publish_url, + "--check-url", + index_url, + *extra_args, + ] + output = run( + args, cwd=project_dir, env=env, text=True, check=True, stderr=PIPE + ).stderr + + if output.count("Uploading") != 0 or output.count("already exists") != len( + expected_filenames + ): + raise RuntimeError( + f"Re-upload with check URL failed: " + f"{output.count("Uploading")}, {output.count("already exists")}\n" + f"---\n{output}\n---" ) - elif target == "pypi-trusted-publishing": - check_call( - [ - uv, - "publish", - "--publish-url", - "https://test.pypi.org/legacy/", - "--trusted-publishing", - "always", - ], - cwd=cwd.joinpath(project_name), + + # Build a different source dist and wheel at the same version, so the upload fails + del project_dir + modified_project_dir = build_project_at_version( + project_name, version, uv, modified=True + ) + + print( + f"\n=== 4. Publishing modified {project_name} {version} " + f"again with skip existing (error test) ===" + ) + wait_for_index(index_url, project_name, version, uv) + args = [ + uv, + "publish", + "--publish-url", + publish_url, + "--check-url", + index_url, + *extra_args, + ] + result = run(args, cwd=modified_project_dir, env=env, text=True, stderr=PIPE) + + if ( + result.returncode == 0 + or "Local file and index file do not match for" not in result.stderr + ): + raise RuntimeError( + f"Re-upload with mismatching files should not have been started: " + f"Exit code {result.returncode}\n" + f"---\n{result.stderr}\n---" ) + + +def target_configuration(target: str) -> tuple[dict[str, str], list[str]]: + if target == "pypi-token": + extra_args = [] + env = {"UV_PUBLISH_TOKEN": os.environ["UV_TEST_PUBLISH_TOKEN"]} + elif target == "pypi-password-env": + extra_args = ["--username", "__token__"] + env = {"UV_PUBLISH_PASSWORD": os.environ["UV_TEST_PUBLISH_PASSWORD"]} + elif target == "pypi-keyring": + extra_args = ["--username", "__token__", "--keyring-provider", "subprocess"] + env = {} + elif target == "pypi-trusted-publishing": + extra_args = ["--trusted-publishing", "always"] + env = {} elif target == "gitlab": - env = os.environ.copy() - env["UV_PUBLISH_PASSWORD"] = os.environ["UV_TEST_PUBLISH_GITLAB_PAT"] - check_call( - [ - uv, - "publish", - "--publish-url", - "https://gitlab.com/api/v4/projects/61853105/packages/pypi", - "--username", - "astral-test-gitlab-pat", - ], - cwd=cwd.joinpath(project_name), - env=env, - ) + env = {"UV_PUBLISH_PASSWORD": os.environ["UV_TEST_PUBLISH_GITLAB_PAT"]} + extra_args = ["--username", "astral-test-gitlab-pat"] elif target == "codeberg": - env = os.environ.copy() - env["UV_PUBLISH_USERNAME"] = "astral-test-user" - env["UV_PUBLISH_PASSWORD"] = os.environ["UV_TEST_PUBLISH_CODEBERG_TOKEN"] - check_call( - [ - uv, - "publish", - "--publish-url", - "https://codeberg.org/api/packages/astral-test-user/pypi", - ], - cwd=cwd.joinpath(project_name), - env=env, - ) + extra_args = [] + env = { + "UV_PUBLISH_USERNAME": "astral-test-user", + "UV_PUBLISH_PASSWORD": os.environ["UV_TEST_PUBLISH_CODEBERG_TOKEN"], + } elif target == "cloudsmith": - env = os.environ.copy() - env["UV_PUBLISH_TOKEN"] = os.environ["UV_TEST_PUBLISH_CLOUDSMITH_TOKEN"] - check_call( - [ - uv, - "publish", - "--publish-url", - "https://python.cloudsmith.io/astral-test/astral-test-1/", - ], - cwd=cwd.joinpath(project_name), - env=env, - ) + extra_args = [] + env = { + "UV_PUBLISH_TOKEN": os.environ["UV_TEST_PUBLISH_CLOUDSMITH_TOKEN"], + } else: raise ValueError(f"Unknown target: {target}") + return env, extra_args def main(): parser = ArgumentParser() - target_choices = list(all_targets) + ["local", "all"] + target_choices = [*all_targets, "local", "all"] parser.add_argument("targets", choices=target_choices, nargs="+") parser.add_argument("--uv") args = parser.parse_args() @@ -264,8 +397,9 @@ def main(): else: targets = args.targets - for project_name in targets: - publish_project(project_name, uv) + with httpx.Client(timeout=120) as client: + for project_name in targets: + publish_project(project_name, uv, client) if __name__ == "__main__": diff --git a/uv.schema.json b/uv.schema.json index c2d679bb8740..15ed7a0e03af 100644 --- a/uv.schema.json +++ b/uv.schema.json @@ -86,6 +86,16 @@ "type": "string" } }, + "default-groups": { + "description": "The list of `dependency-groups` to install by default.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/GroupName" + } + }, "dependency-metadata": { "description": "Pre-defined static metadata for dependencies of the project (direct or transitive). When provided, enables the resolver to use the specified metadata instead of querying the registry or building the relevant package from source.\n\nMetadata should be provided in adherence with the [Metadata 2.3](https://packaging.python.org/en/latest/specifications/core-metadata/) standard, though only the following fields are respected:\n\n- `name`: The name of the package. - (Optional) `version`: The version of the package. If omitted, the metadata will be applied to all versions of the package. - (Optional) `requires-dist`: The dependencies of the package (e.g., `werkzeug>=0.14`). - (Optional) `requires-python`: The Python version required by the package (e.g., `>=3.10`). - (Optional) `provides-extras`: The extras provided by the package.", "type": [ @@ -398,7 +408,7 @@ ] }, "sources": { - "description": "The sources to use (e.g., workspace members, Git repositories, local paths) when resolving dependencies.", + "description": "The sources to use when resolving dependencies.\n\n`tool.uv.sources` enriches the dependency metadata with additional sources, incorporated during development. A dependency source can be a Git repository, a URL, a local path, or an alternative registry.\n\nSee [Dependencies](https://docs.astral.sh/uv/concepts/dependencies/) for more.", "anyOf": [ { "$ref": "#/definitions/ToolUvSources" @@ -579,6 +589,10 @@ }, "additionalProperties": false }, + "GroupName": { + "description": "The normalized name of a dependency group.\n\nSee: - - ", + "type": "string" + }, "Index": { "type": "object", "required": [ @@ -597,9 +611,13 @@ }, "name": { "description": "The name of the index.\n\nIndex names can be used to reference indexes elsewhere in the configuration. For example, you can pin a package to a specific index by name:\n\n```toml [[tool.uv.index]] name = \"pytorch\" url = \"https://download.pytorch.org/whl/cu121\"\n\n[tool.uv.sources] torch = { index = \"pytorch\" } ```", - "type": [ - "string", - "null" + "anyOf": [ + { + "$ref": "#/definitions/IndexName" + }, + { + "type": "null" + } ] }, "url": { @@ -612,6 +630,10 @@ } } }, + "IndexName": { + "description": "The normalized name of an index.\n\nIndex names may contain letters, digits, hyphens, underscores, and periods, and must be ASCII.", + "type": "string" + }, "IndexStrategy": { "oneOf": [ { @@ -833,7 +855,7 @@ ] }, "extra": { - "description": "Include optional dependencies from the extra group name; may be provided more than once.\n\nOnly applies to `pyproject.toml`, `setup.py`, and `setup.cfg` sources.", + "description": "Include optional dependencies from the specified extra; may be provided more than once.\n\nOnly applies to `pyproject.toml`, `setup.py`, and `setup.cfg` sources.", "type": [ "array", "null" @@ -1399,7 +1421,7 @@ ], "properties": { "index": { - "type": "string" + "$ref": "#/definitions/IndexName" }, "marker": { "$ref": "#/definitions/MarkerTree"