A tool to rename Python wheel packages for multi-version installation
Project description
🛞 third-wheel
A tool to rename Python wheel packages for multi-version installation.
Use Case
When you need to install multiple versions of the same Python package in a single environment (e.g., for regression testing), you can use this tool to rename one version's wheel so both can coexist:
# In your test code:
import icechunk_v1 # The v1 version
import icechunk # The v2 version
# Test that v2 can read data written by v1
Installation
# Use directly with uvx (recommended)
uvx third-wheel --help
# Or install globally
pip install third-wheel
End-to-End Example: icechunk v1 + v2
Here's a complete example of setting up both icechunk versions for regression testing:
# 1. Download and rename v1 in one command (specify target Python version for uvx)
uvx third-wheel download icechunk \
-i https://pypi.anaconda.org/scientific-python-nightly-wheels/simple \
--version "<2" \
--rename icechunk_v1 \
--python-version 3.12 \
-o ./wheels/
# 2. Download v2 wheel from nightly builds
uvx third-wheel download icechunk \
-i https://pypi.anaconda.org/scientific-python-nightly-wheels/simple \
--version ">=2.0.0.dev0" \
--python-version 3.12 \
-o ./wheels/
# 3. Create a venv and install both versions
uv venv
uv pip install ./wheels/icechunk_v1-*.whl # v1 as icechunk_v1
uv pip install ./wheels/icechunk-2*.whl # v2 as icechunk
# 4. Verify both work
uv run python -c "import icechunk_v1; print(f'v1: {icechunk_v1.__version__}')"
uv run python -c "import icechunk; print(f'v2: {icechunk.__version__}')"
Optional: Inspect a wheel before renaming to verify it uses underscore-prefix extensions:
uvx third-wheel inspect ./wheels/icechunk-*.whl
Commands
🛞 run
Run a PEP 723 inline script with multi-version package support. This is the easiest way to use third-wheel — just annotate your script's dependencies and run it:
# /// script
# dependencies = [
# "icechunk_v1", # icechunk<2
# "icechunk>=2",
# ]
# ///
import icechunk_v1 # old version
import icechunk # new version
print(f"v1: {icechunk_v1.__version__}")
print(f"v2: {icechunk.__version__}")
third-wheel run script.py
The comment after a dependency (# icechunk<2) tells third-wheel to install icechunk<2 from the index but rename the package to icechunk_v1. The script can then import icechunk_v1.
Rename annotation syntax:
| Annotation | Meaning |
|---|---|
"icechunk_v1", # icechunk<2 |
Install icechunk<2, rename to icechunk_v1 |
"zarr_v2", # zarr>=2,<3 |
Install zarr>=2,<3, rename to zarr_v2 |
"my_requests", # requests |
Install requests (any version), rename to my_requests |
For more complex setups, use the structured [tool.third-wheel] form:
# /// script
# dependencies = ["icechunk_v1", "icechunk>=2"]
# [tool.third-wheel]
# renames = [
# {original = "icechunk", new-name = "icechunk_v1", version = "<2"},
# ]
# ///
If both the comment syntax and [tool.third-wheel] specify the same new-name, the structured form takes priority.
CLI renames override or supplement script annotations:
# Add a rename not in the script metadata
third-wheel run script.py --rename "icechunk<2=icechunk_v1"
# Use a custom index for renamed packages
third-wheel run script.py -i https://pypi.anaconda.org/scientific-python-nightly-wheels/simple
The --rename format is ORIGINAL[VERSION_SPEC]=NEW_NAME.
Argument passing: Unknown flags are passed through to the script automatically. Use -- if a script flag conflicts with a third-wheel flag:
# --my-flag goes to the script
third-wheel run script.py --my-flag value
# Explicit separator for ambiguous flags
third-wheel run script.py -- --rename "this-goes-to-script"
Options:
--rename: Rename rule (can be specified multiple times)-i, --index-url: Package index URL for renamed packages (default: PyPI)--python-version: Target Python version (e.g.,3.12)-v, --verbose: Print diagnostic info about what third-wheel is doing
🛞 rename
Rename a wheel package:
third-wheel rename <wheel_path> <new_name> [-o <output_dir>]
# Examples:
third-wheel rename icechunk-1.0.0-cp312-cp312-linux_x86_64.whl icechunk_v1
third-wheel rename ./downloads/pkg.whl my_pkg_old -o ./renamed/
Options:
-o, --output: Output directory (default: same as input)--no-update-imports: Don't update import statements in Python files
🛞 download
Download a compatible wheel from a package index:
third-wheel download <package> [-o <output_dir>] [-i <index_url>] [--version <spec>] [--rename <new_name>]
# Examples:
third-wheel download numpy -o ./wheels/
third-wheel download icechunk -i https://pypi.anaconda.org/scientific-python-nightly-wheels/simple
third-wheel download requests --version ">=2.0,<3"
third-wheel download icechunk --version "<2" -i https://pypi.anaconda.org/scientific-python-nightly-wheels/simple
# Download and rename in one command:
third-wheel download icechunk --version "<2" --rename icechunk_v1 -o ./wheels/ \
-i https://pypi.anaconda.org/scientific-python-nightly-wheels/simple
Options:
-o, --output: Output directory (default: current directory)-i, --index-url: Package index URL (default: PyPI)--version: PEP 440 version specifier (e.g.,==1.0.0,<2,>=1.0,<2)--list: List available wheels without downloading--rename: Rename the downloaded wheel to this package name (combines download + rename)--python-version: Target Python version (e.g.,3.12). Useful withuvxto download wheels for a different Python than the one running third-wheel.
🔧 inspect
Inspect a wheel's structure before renaming:
third-wheel inspect <wheel_path> [--json]
# Example output:
# Wheel: icechunk-1.1.14-cp312-cp312-macosx_11_0_arm64.whl
# Distribution: icechunk
# Version: 1.1.14
#
# Compiled extensions (1):
# - icechunk/_icechunk_python.cpython-312-darwin.so (underscore prefix - renamable)
#
# This wheel uses underscore-prefix extensions.
# Renaming should work correctly.
🛞 serve
Start a PEP 503 proxy server that renames packages on-the-fly:
# Install with server extras
pip install third-wheel[server]
# Start proxy with CLI options
third-wheel serve \
-u https://pypi.anaconda.org/scientific-python-nightly-wheels/simple \
-r "icechunk=icechunk_v1:<2" \
--port 8000
# Or use a config file
third-wheel serve -c proxy.toml
Options:
-c, --config: Path to TOML config file-u, --upstream: Upstream index URL (can be specified multiple times)-r, --rename: Rename rule in formatoriginal=new_name[:version_spec]--host: Host to bind to (default: 127.0.0.1)--port: Port to listen on (default: 8000)
Config file format (proxy.toml):
[proxy]
host = "127.0.0.1"
port = 8000
[[proxy.upstreams]]
url = "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple/"
[renames]
icechunk = { name = "icechunk_v1", version = "<2" }
Using with uv:
# Start the proxy
third-wheel serve -u https://pypi.org/simple/ -r "requests=requests_old:<2"
# In another terminal, install from the proxy
uv pip install requests_old --index-url http://127.0.0.1:8000/simple/
The proxy:
- Lists virtual packages (renamed packages) at
/simple/ - Fetches the original package from upstream when requested
- Filters by version constraint if specified
- Renames the wheel on-the-fly during download
- Serves the renamed wheel to the client
🔧 How It Works
- Extracts the wheel (which is a ZIP file)
- Renames the package directory (
pkg/→pkg_v1/) - Renames the
.dist-infodirectory - Updates METADATA with the new package name
- Updates imports in all Python files (
from pkg import→from pkg_v1 import) - Regenerates RECORD with new file paths and SHA256 hashes
- Repacks as a new wheel with the renamed filename
🔧 Compiled Extensions
For wheels with compiled extensions (.so/.pyd files), renaming works only if the extension uses an underscore-prefix naming pattern:
| Pattern | Example | Renamable? |
|---|---|---|
_modulename.cpython-*.so |
_icechunk_python.cpython-312-darwin.so |
Yes |
modulename.cpython-*.so |
icechunk.cpython-312-darwin.so |
No |
Why underscore prefix matters
Python's import system requires the PyInit_<name> function inside the .so file to match the filename. When you have _mymodule.cpython-*.so:
- Python looks for
PyInit__mymodule(matches!) - The parent package directory can be renamed freely
from newpkg._mymodule import ...works because the.soname is unchanged
If the extension doesn't use the underscore prefix pattern, the tool will warn you and you should rebuild from source instead.
Limitations
- Wheels only: third-wheel can only rename wheel (
.whl) files, not sdists. If a package version only has sdists on PyPI (no wheels), it cannot be downloaded or renamed. Most modern packages publish wheels, but very old versions may not. - Compiled extensions without underscore prefix: Cannot be renamed without rebuilding
- Hardcoded package names in strings: Not automatically updated (only import statements are)
- Entry points: Updated in metadata but external scripts may need adjustment
Development
# Clone and setup
git clone <repo>
cd third-wheel
uv sync --all-extras
# Run tests
uv run pytest
# Lint and format
uv run ruff check src tests
uv run ruff format src tests
License
BSD-3-Clause
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file third_wheel-0.1.0.tar.gz.
File metadata
- Download URL: third_wheel-0.1.0.tar.gz
- Upload date:
- Size: 112.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
34f83e93c52e9d8dc46a4df83e223ecb5e999a3478ee1c627de60d58f4c0b570
|
|
| MD5 |
a5713b08209a65ed595d3f6aee3b1d97
|
|
| BLAKE2b-256 |
c5efac9e8a1ed978887a502f5f697da2c11c7b6a1052e93d9d31fdeea69940b2
|
Provenance
The following attestation bundles were made for third_wheel-0.1.0.tar.gz:
Publisher:
publish.yml on earth-mover/third-wheel
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
third_wheel-0.1.0.tar.gz -
Subject digest:
34f83e93c52e9d8dc46a4df83e223ecb5e999a3478ee1c627de60d58f4c0b570 - Sigstore transparency entry: 1052597324
- Sigstore integration time:
-
Permalink:
earth-mover/third-wheel@cdb919b627a2e3e858ed3fd9074389ef8b37a3ab -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/earth-mover
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@cdb919b627a2e3e858ed3fd9074389ef8b37a3ab -
Trigger Event:
release
-
Statement type:
File details
Details for the file third_wheel-0.1.0-py3-none-any.whl.
File metadata
- Download URL: third_wheel-0.1.0-py3-none-any.whl
- Upload date:
- Size: 31.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b596d867708386f0e32141d1bc036a35a91430fb4fa140db893f35e9f959e530
|
|
| MD5 |
ebbf454cc5f3db693daf510920cd673f
|
|
| BLAKE2b-256 |
be15a81f48f2b07f52c37a38d839d42771a9a047e3873b35eb52d348145fa260
|
Provenance
The following attestation bundles were made for third_wheel-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on earth-mover/third-wheel
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
third_wheel-0.1.0-py3-none-any.whl -
Subject digest:
b596d867708386f0e32141d1bc036a35a91430fb4fa140db893f35e9f959e530 - Sigstore transparency entry: 1052597354
- Sigstore integration time:
-
Permalink:
earth-mover/third-wheel@cdb919b627a2e3e858ed3fd9074389ef8b37a3ab -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/earth-mover
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@cdb919b627a2e3e858ed3fd9074389ef8b37a3ab -
Trigger Event:
release
-
Statement type: